sniff-cli 0.1.1

An exhaustive LLM-backed slop finder for codebases
Documentation
use super::*;

fn parse_import(trimmed: &str) -> Option<ImportRecord> {
    let rest = trimmed.strip_prefix("import ")?;
    let rest = rest.trim().trim_end_matches(';');
    if rest.starts_with('(') {
        return None;
    }
    if let Some((local, source)) = rest.split_once(' ') {
        Some(ImportRecord {
            local_name: local.trim().to_string(),
            source_module: source
                .trim()
                .trim_matches('"')
                .trim_matches('\'')
                .to_string(),
            imported_name: "*".to_string(),
        })
    } else {
        Some(ImportRecord {
            local_name: rest.trim().trim_matches('"').to_string(),
            source_module: rest.trim().trim_matches('"').to_string(),
            imported_name: "*".to_string(),
        })
    }
}

pub(crate) fn parse_func_name(trimmed: &str) -> Option<(Option<String>, String, usize)> {
    let rest = trimmed.strip_prefix("func ")?;
    let rest = rest.trim_start();
    if rest.starts_with('(') {
        let recv_end = rest.find(')')?;
        let recv = rest[1..recv_end].trim();
        let recv_name = recv
            .split_whitespace()
            .last()?
            .trim_start_matches('*')
            .to_string();
        let after = rest[recv_end + 1..].trim_start();
        let name = after.split('(').next()?.trim().to_string();
        let params = after
            .split_once('(')
            .and_then(|(_, rem)| rem.split_once(')').map(|(inside, _)| inside))
            .unwrap_or("");
        return Some((
            Some(recv_name),
            name,
            params.split(',').filter(|p| !p.trim().is_empty()).count(),
        ));
    }

    let name = rest.split('(').next()?.trim().to_string();
    let params = rest
        .split_once('(')
        .and_then(|(_, rem)| rem.split_once(')').map(|(inside, _)| inside))
        .unwrap_or("");
    Some((
        None,
        name,
        params.split(',').filter(|p| !p.trim().is_empty()).count(),
    ))
}

fn record_go_import(trimmed: &str, imports: &mut Vec<ImportRecord>) -> bool {
    if let Some(import_record) = parse_import(trimmed) {
        imports.push(import_record);
        return true;
    }
    false
}

fn record_go_function(
    lines: &[&str],
    idx: usize,
    next_id: &mut usize,
    definitions: &mut Vec<SymbolDefinition>,
) -> Option<usize> {
    let trimmed = lines[idx].trim();
    let (recv, name, _) = parse_func_name(trimmed)?;
    let end = super::shared::scan_block_end(lines, idx);
    definitions.push(SymbolDefinition {
        id: *next_id,
        name: name.clone(),
        kind: if recv.is_some() {
            SymbolKind::Method
        } else {
            SymbolKind::Function
        },
        start_line: idx + 1,
        end_line: end + 1,
        is_exported: name
            .chars()
            .next()
            .map(|c| c.is_uppercase())
            .unwrap_or(false),
        owner_type: recv,
    });
    *next_id += 1;
    Some(end)
}

pub(crate) fn scan_go_defs_and_imports(extractor: &mut SymbolExtractor<'_>) -> Vec<(usize, usize)> {
    let source = String::from_utf8_lossy(extractor.source_bytes);
    let lines: Vec<&str> = source.lines().collect();
    let mut ranges = Vec::new();
    let mut i = 0usize;
    while i < lines.len() {
        let trimmed = lines[i].trim();
        if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("package ") {
            i += 1;
            continue;
        }

        if record_go_import(trimmed, &mut extractor.imports) {
            i += 1;
            continue;
        }

        if let Some(end) = record_go_function(
            &lines,
            i,
            &mut extractor.next_id,
            &mut extractor.definitions,
        ) {
            ranges.push((i, end));
            i = end + 1;
            continue;
        }

        i += 1;
    }
    let _ = extractor.adapter;
    let _ = extractor.language;
    ranges
}