deslop 0.2.0

A static analyzer that spots low-context and AI-assisted code patterns across naming, concurrency, security, performance, and test quality.
Documentation
mod comments;
mod context;
mod errors;
mod frameworks;
mod general;
mod performance;
#[cfg(test)]
mod tests;

use std::path::Path;

use tree_sitter::{Node, Parser};

use crate::analysis::go::fingerprint::build_function_fingerprint;
use crate::analysis::{
    AnalysisResult, Error, GoFileData, GoFunctionEvidence, Language, LanguageFileData, ParsedFile,
    ParsedFunction,
};

use self::comments::extract_doc_comment;
use self::context::{
    collect_busy_wait_lines, collect_ctx_factories, collect_goroutines, collect_loop_goroutines,
    collect_mutex_loops, collect_sleep_loops, collect_unmanaged_goroutines, has_ctx_param,
};
use self::errors::{collect_dropped_errors, collect_errorf_calls, collect_panic_errors};
use self::frameworks::{collect_gin_calls, collect_gorm_query_chains, collect_parse_input_calls};
use self::general::{
    build_test_summary, collect_calls, collect_go_structs, collect_imports,
    collect_interface_summaries, collect_local_strings, collect_package_vars, collect_pkg_strings,
    collect_struct_tags, collect_symbols, count_descendants, extract_receiver, find_package_name,
};
use self::performance::{
    collect_alloc_loops, collect_concat_loops, collect_db_query_calls, collect_fmt_loops,
    collect_json_loops, collect_reflect_loops,
};

pub(super) fn parse_file(path: &Path, source: &str) -> AnalysisResult<ParsedFile> {
    let mut parser = Parser::new();
    parser
        .set_language(&tree_sitter_go::LANGUAGE.into())
        .map_err(|error| Error::parser_configuration("Go", error.to_string()))?;

    let tree = parser
        .parse(source, None)
        .ok_or_else(|| Error::missing_parse_tree("Go"))?;

    let root = tree.root_node();
    let package_name = find_package_name(root, source);
    let is_test_file = path
        .file_name()
        .and_then(|name| name.to_str())
        .is_some_and(|name| name.ends_with("_test.go"));
    let imports = collect_imports(root, source);
    let package_string_literals = collect_pkg_strings(root, source);
    let struct_tags = collect_struct_tags(root, source);
    let symbols = collect_symbols(root, source);
    let package_vars = collect_package_vars(root, source);
    let interfaces = collect_interface_summaries(root, source);
    let go_structs = collect_go_structs(root, source);
    let functions = collect_functions(root, source, &imports, is_test_file);

    Ok(ParsedFile {
        language: Language::Go,
        path: path.to_path_buf(),
        package_name,
        is_test_file,
        syntax_error: root.has_error(),
        line_count: source.lines().count(),
        byte_size: source.len(),
        pkg_strings: package_string_literals,
        comments: Vec::new(),
        functions,
        imports,
        symbols,
        module_scope_calls: Vec::new(),
        top_level_bindings: Vec::new(),
        lang: LanguageFileData::Go(GoFileData {
            struct_tags,
            package_vars,
            interfaces,
            go_structs,
        }),
    })
}

fn collect_functions(
    root: Node<'_>,
    source: &str,
    imports: &[crate::analysis::ImportSpec],
    is_test_file: bool,
) -> Vec<ParsedFunction> {
    let mut functions = Vec::new();
    visit_for_functions(root, source, imports, is_test_file, &mut functions);
    functions.sort_by(|left, right| {
        left.fingerprint
            .start_line
            .cmp(&right.fingerprint.start_line)
            .then(left.fingerprint.name.cmp(&right.fingerprint.name))
    });
    functions
}

fn visit_for_functions(
    node: Node<'_>,
    source: &str,
    imports: &[crate::analysis::ImportSpec],
    is_test_file: bool,
    functions: &mut Vec<ParsedFunction>,
) {
    if matches!(node.kind(), "function_declaration" | "method_declaration")
        && let Some(parsed_function) = parse_function_node(node, source, imports, is_test_file)
    {
        functions.push(parsed_function);
    }

    let mut cursor = node.walk();
    for child in node.named_children(&mut cursor) {
        visit_for_functions(child, source, imports, is_test_file, functions);
    }
}

fn parse_function_node(
    node: Node<'_>,
    source: &str,
    imports: &[crate::analysis::ImportSpec],
    is_test_file: bool,
) -> Option<ParsedFunction> {
    let body_node = node.child_by_field_name("body")?;
    let signature_text = source
        .get(node.start_byte()..body_node.start_byte())
        .unwrap_or_default()
        .to_string();
    let calls = collect_calls(body_node, source);
    let local_string_literals = collect_local_strings(body_node, source);
    let type_assertion_count = count_descendants(body_node, "type_assertion_expression");
    let has_context_parameter = has_ctx_param(node, source, imports);
    let doc_comment = extract_doc_comment(source, node.start_position().row);
    let function_name = source
        .get(node.child_by_field_name("name")?.byte_range())?
        .to_string();
    let test_summary = build_test_summary(&function_name, body_node, source, &calls, is_test_file);
    let is_test_function = test_summary.is_some();
    let dropped_error_lines = collect_dropped_errors(body_node, source);
    let panic_on_error_lines = collect_panic_errors(body_node, source);
    let errorf_calls = collect_errorf_calls(body_node, source);
    let context_factory_calls = collect_ctx_factories(body_node, source, imports);
    let goroutine_launch_lines = collect_goroutines(body_node);
    let goroutine_in_loop_lines = collect_loop_goroutines(body_node);
    let goroutine_without_shutdown_lines = collect_unmanaged_goroutines(body_node, source);
    let sleep_in_loop_lines = collect_sleep_loops(body_node, source, imports);
    let busy_wait_lines = collect_busy_wait_lines(body_node, source);
    let mutex_lock_in_loop_lines = collect_mutex_loops(body_node, source);
    let allocation_in_loop_lines = collect_alloc_loops(body_node, source, imports);
    let fmt_in_loop_lines = collect_fmt_loops(body_node, source, imports);
    let reflection_in_loop_lines = collect_reflect_loops(body_node, source, imports);
    let string_concat_in_loop_lines = collect_concat_loops(body_node, source);
    let json_marshal_in_loop_lines = collect_json_loops(body_node, source, imports);
    let db_query_calls = collect_db_query_calls(body_node, source);
    let gorm_query_chains = collect_gorm_query_chains(body_node, source, imports);
    let parse_input_calls = collect_parse_input_calls(body_node, source, imports);
    let gin_calls = collect_gin_calls(body_node, source, imports);
    let body_text = source
        .get(body_node.byte_range())
        .unwrap_or_default()
        .to_string();
    let receiver_type = node
        .child_by_field_name("receiver")
        .and_then(|receiver| extract_receiver(receiver, source))
        .map(|(name, _)| name);
    let fingerprint = build_function_fingerprint(
        node,
        source,
        receiver_type,
        type_assertion_count,
        calls.len(),
    )?;

    Some(ParsedFunction {
        fingerprint,
        signature_text,
        body_start_line: body_node.start_position().row + 1,
        calls,
        is_test_function,
        local_binding_names: Vec::new(),
        doc_comment,
        body_text,
        local_strings: local_string_literals,
        test_summary,
        go: Some(GoFunctionEvidence {
            has_context_parameter,
            context_factory_calls,
            dropped_errors: dropped_error_lines,
            panic_errors: panic_on_error_lines,
            errorf_calls,
            goroutines: goroutine_launch_lines,
            loop_goroutines: goroutine_in_loop_lines,
            unmanaged_goroutines: goroutine_without_shutdown_lines,
            sleep_loops: sleep_in_loop_lines,
            busy_wait_lines,
            mutex_loops: mutex_lock_in_loop_lines,
            alloc_loops: allocation_in_loop_lines,
            fmt_loops: fmt_in_loop_lines,
            reflect_loops: reflection_in_loop_lines,
            concat_loops: string_concat_in_loop_lines,
            json_loops: json_marshal_in_loop_lines,
            db_query_calls,
            gorm_query_chains,
            parse_input_calls,
            gin_calls,
        }),
        python: None,
        rust: None,
    })
}