sniff-cli 0.1.1

An exhaustive LLM-backed slop finder for codebases
Documentation
use super::*;
use std::collections::HashSet;
use std::path::Path;

fn parse_js_ts_symbols(source_text: &str, file_path: &str) -> LocalFileSymbols {
    let allocator = oxc_allocator::Allocator::default();
    let source_type = oxc_span::SourceType::from_path(Path::new(file_path)).unwrap_or_default();
    let parser = oxc_parser::Parser::new(&allocator, source_text, source_type);
    let parsed = parser.parse();

    let line_index = LineIndex::new(source_text);
    let mut extractor = oxc::OxcExtractor {
        source: source_text,
        line_index,
        file_path: file_path.to_string(),
        methods: Vec::new(),
        definitions: Vec::new(),
        imports: Vec::new(),
        exports: Vec::new(),
        references: Vec::new(),
        scopes: vec![HashSet::new()],
        next_id: 0,
        current_name_hint: None,
        in_class: false,
        in_function_body: false,
        is_exported_context: false,
    };

    extractor.visit_program(&parsed.program);

    LocalFileSymbols {
        file_path: file_path.to_string(),
        definitions: extractor.definitions,
        imports: extractor.imports,
        exports: extractor.exports,
        references: extractor.references,
    }
}

fn parse_python_symbols(source_text: &str, file_path: &str) -> LocalFileSymbols {
    let mut symbols = LocalFileSymbols {
        file_path: file_path.to_string(),
        definitions: Vec::new(),
        imports: Vec::new(),
        exports: Vec::new(),
        references: Vec::new(),
    };

    let parsed = rustpython_parser::parse(source_text, rustpython_parser::Mode::Module, file_path);
    if let Ok(ast) = parsed
        && let rustpython_ast::Mod::Module(module) = ast
    {
        let line_index = LineIndex::new(source_text);
        let mut extractor = python::PyExtractor {
            source: source_text,
            line_index,
            file_path: file_path.to_string(),
            methods: Vec::new(),
            definitions: Vec::new(),
            imports: Vec::new(),
            exports: Vec::new(),
            references: Vec::new(),
            scopes: vec![HashSet::new()],
            next_id: 0,
            parent_is_class: false,
            in_function_body: false,
            scanned: false,
        };

        for stmt in module.body {
            extractor.visit_stmt(stmt);
        }

        symbols.definitions = extractor.definitions;
        symbols.imports = extractor.imports;
        symbols.exports = extractor.exports;
        symbols.references = extractor.references;
    }

    symbols
}

fn parse_rust_symbols(source_text: &str, file_path: &str) -> LocalFileSymbols {
    let mut symbols = LocalFileSymbols {
        file_path: file_path.to_string(),
        definitions: Vec::new(),
        imports: Vec::new(),
        exports: Vec::new(),
        references: Vec::new(),
    };

    if let Ok(ast) = syn::parse_file(source_text) {
        let mut extractor = rust::RustExtractor {
            source: source_text,
            file_path: file_path.to_string(),
            methods: Vec::new(),
            definitions: Vec::new(),
            imports: Vec::new(),
            exports: Vec::new(),
            references: Vec::new(),
            scopes: vec![HashSet::new()],
            next_id: 0,
            in_impl: false,
            current_impl_type: None,
        };

        extractor.visit_file(&ast);
        symbols.definitions = extractor.definitions;
        symbols.imports = extractor.imports;
        symbols.exports = extractor.exports;
        symbols.references = extractor.references;
    }

    for def in &symbols.definitions {
        if !def.name.starts_with('_')
            && !symbols
                .exports
                .iter()
                .any(|e| e.local_symbol_name == def.name)
        {
            symbols.exports.push(ExportRecord {
                exported_name: def.name.clone(),
                local_symbol_name: def.name.clone(),
                source_module: None,
                source_symbol_name: None,
            });
        }
    }

    symbols
}

fn parse_kotlin_symbols(
    source_bytes: &[u8],
    file_path: &str,
    adapter: &LanguageAdapter,
) -> LocalFileSymbols {
    let mut symbols = LocalFileSymbols {
        file_path: file_path.to_string(),
        definitions: Vec::new(),
        imports: Vec::new(),
        exports: Vec::new(),
        references: Vec::new(),
    };

    let mut parser = match kotlin::get_parser(adapter) {
        Some(p) => p,
        None => return symbols,
    };

    if let Some(tree) = parser.parse(source_bytes, None) {
        let mut extractor = kotlin::SymbolExtractor {
            source_bytes,
            language: &adapter.name,
            adapter,
            definitions: Vec::new(),
            imports: Vec::new(),
            exports: Vec::new(),
            references: Vec::new(),
            scopes: vec![HashSet::new()],
            next_id: 0,
        };

        extractor.visit(tree.root_node());
        symbols.definitions = extractor.definitions;
        symbols.imports = extractor.imports;
        symbols.exports = extractor.exports;
        symbols.references = extractor.references;
    }

    symbols
}

fn parse_go_symbols(
    source_bytes: &[u8],
    file_path: &str,
    adapter: &LanguageAdapter,
) -> LocalFileSymbols {
    let mut symbols = LocalFileSymbols {
        file_path: file_path.to_string(),
        definitions: Vec::new(),
        imports: Vec::new(),
        exports: Vec::new(),
        references: Vec::new(),
    };

    let mut parser = match go::get_parser(adapter) {
        Some(p) => p,
        None => return symbols,
    };

    if let Some(tree) = parser.parse(source_bytes, None) {
        let mut extractor = go::SymbolExtractor {
            source_bytes,
            language: &adapter.name,
            adapter,
            definitions: Vec::new(),
            imports: Vec::new(),
            exports: Vec::new(),
            references: Vec::new(),
            scopes: vec![HashSet::new()],
            next_id: 0,
        };

        extractor.visit(tree.root_node());
        symbols.definitions = extractor.definitions;
        symbols.imports = extractor.imports;
        symbols.exports = extractor.exports;
        symbols.references = extractor.references;

        if adapter.name == "go" {
            for def in &symbols.definitions {
                if let Some(first_char) = def.name.chars().next()
                    && first_char.is_uppercase()
                {
                    symbols.exports.push(ExportRecord {
                        exported_name: def.name.clone(),
                        local_symbol_name: def.name.clone(),
                        source_module: None,
                        source_symbol_name: None,
                    });
                }
            }
        }
    }

    symbols
}

pub(crate) fn parse_symbols_for_language(
    source_text: &str,
    source_bytes: &[u8],
    file_path: &str,
    adapter: &LanguageAdapter,
) -> LocalFileSymbols {
    match adapter.name.as_str() {
        "javascript" | "typescript" => parse_js_ts_symbols(source_text, file_path),
        "python" => parse_python_symbols(source_text, file_path),
        "rust" => parse_rust_symbols(source_text, file_path),
        "kotlin" => parse_kotlin_symbols(source_bytes, file_path, adapter),
        "go" => parse_go_symbols(source_bytes, file_path, adapter),
        _ => LocalFileSymbols {
            file_path: file_path.to_string(),
            definitions: Vec::new(),
            imports: Vec::new(),
            exports: Vec::new(),
            references: Vec::new(),
        },
    }
}