sniff-cli 0.1.1

An exhaustive LLM-backed slop finder for codebases
Documentation
use super::core_helpers::*;
use super::python_blocks::block_end;
use super::python_refs_helpers::collect_python_refs;
use super::*;
use crate::types::{ExportRecord, MethodRecord, SymbolDefinition, SymbolKind};
use std::collections::HashSet;

#[allow(clippy::too_many_arguments)]
fn build_method_record(
    file_path: &str,
    source: &str,
    line_starts: &[usize],
    name: &str,
    start_line: usize,
    end_line: usize,
    param_count: usize,
    is_exported: bool,
) -> MethodRecord {
    let start_off = line_starts[start_line - 1];
    let end_off = if end_line < line_starts.len() {
        line_starts[end_line]
    } else {
        source.len()
    };
    MethodRecord {
        name: name.to_string(),
        file_path: file_path.to_string(),
        source: source[start_off..end_off].to_string(),
        loc: end_line.saturating_sub(start_line) + 1,
        param_count,
        start_line,
        end_line,
        is_exported,
        language: "python".to_string(),
        nesting_depth: 0,
        references: Vec::new(),
        real_ref_count: 0,
    }
}

fn push_method(methods: &mut Vec<MethodRecord>, method: MethodRecord) {
    methods.push(method);
}

pub(super) fn record_python_function(
    extractor: &mut PyExtractor<'_>,
    idx: usize,
    trimmed: &str,
    lines: &[&str],
) -> Option<(String, usize, HashSet<String>)> {
    let name = parse_python_name(trimmed)?;
    let indent = line_indent(lines[idx]);
    let params = parse_python_params(trimmed);
    let end_idx = block_end(lines, idx, indent);
    let start_line = idx + 1;
    let end_line = end_idx + 1;
    let shadowed: HashSet<String> = params.into_iter().collect();
    let shadowed_len = shadowed.len();
    extractor.definitions.push(SymbolDefinition {
        id: extractor.next_id,
        name: name.clone(),
        kind: SymbolKind::Function,
        start_line,
        end_line,
        is_exported: !name.starts_with('_'),
        owner_type: None,
    });
    extractor.next_id += 1;
    let method = build_method_record(
        &extractor.file_path,
        extractor.source,
        &extractor.line_index.line_starts,
        &name,
        start_line,
        end_line,
        shadowed_len,
        !name.starts_with('_'),
    );
    push_method(&mut extractor.methods, method);
    Some((name, end_idx, shadowed))
}

pub(super) fn scan_python_defs_and_imports(
    extractor: &mut PyExtractor<'_>,
) -> Vec<(usize, usize, HashSet<String>, String)> {
    if extractor.scanned {
        return Vec::new();
    }
    extractor.scanned = true;

    let lines: Vec<&str> = extractor.source.lines().collect();
    let mut idx = 0usize;
    let mut spans = Vec::new();
    let mut import_block: Option<String> = None;
    while idx < lines.len() {
        let trimmed = lines[idx].trim();
        if trimmed.is_empty() || trimmed.starts_with('#') {
            idx += 1;
            continue;
        }

        if let Some(block) = import_block.as_mut() {
            if !block.is_empty() {
                block.push(' ');
            }
            block.push_str(trimmed);
            if trimmed.contains(')') {
                let import_stmt = block.trim().to_string();
                import_block = None;
                if let Some(import_records) = parse_python_namespace_imports(&import_stmt)
                    .or_else(|| parse_python_from_imports(&import_stmt))
                {
                    for import_record in import_records {
                        if import_record.imported_name == "*" {
                            extractor.exports.push(ExportRecord {
                                exported_name: "*".to_string(),
                                local_symbol_name: "*".to_string(),
                                source_module: Some(import_record.source_module.clone()),
                                source_symbol_name: Some("*".to_string()),
                            });
                        } else {
                            extractor.exports.push(ExportRecord {
                                exported_name: import_record.local_name.clone(),
                                local_symbol_name: import_record.local_name.clone(),
                                source_module: Some(import_record.source_module.clone()),
                                source_symbol_name: Some(import_record.imported_name.clone()),
                            });
                        }
                        extractor.imports.push(import_record);
                    }
                }
            }
            idx += 1;
            continue;
        }

        if let Some(import_records) =
            parse_python_namespace_imports(trimmed).or_else(|| parse_python_from_imports(trimmed))
        {
            for import_record in import_records {
                if import_record.imported_name == "*" {
                    extractor.exports.push(ExportRecord {
                        exported_name: "*".to_string(),
                        local_symbol_name: "*".to_string(),
                        source_module: Some(import_record.source_module.clone()),
                        source_symbol_name: Some("*".to_string()),
                    });
                } else {
                    extractor.exports.push(ExportRecord {
                        exported_name: import_record.local_name.clone(),
                        local_symbol_name: import_record.local_name.clone(),
                        source_module: Some(import_record.source_module.clone()),
                        source_symbol_name: Some(import_record.imported_name.clone()),
                    });
                }
                extractor.imports.push(import_record);
            }
            idx += 1;
            continue;
        }

        if trimmed.starts_with("from ") && trimmed.contains(" import (") {
            import_block = Some(trimmed.to_string());
            idx += 1;
            continue;
        }

        if let Some((_name, end_idx, shadowed)) =
            record_python_function(extractor, idx, trimmed, &lines)
        {
            spans.push((idx, end_idx, shadowed, trimmed.to_string()));
            idx = end_idx + 1;
            continue;
        }

        for reference in collect_python_refs(trimmed, &HashSet::new()) {
            extractor.references.push(SymbolReference {
                name: reference,
                line: idx + 1,
                snippet: trimmed.to_string(),
                resolved_symbol: None,
            });
        }

        idx += 1;
    }
    spans
}