deslop 0.1.0

A static analyzer that spots low-context and AI-assisted code patterns across naming, concurrency, security, performance, and test quality.
Documentation
use crate::analysis::{ParsedFile, ParsedFunction};
use crate::model::{Finding, Severity};

use super::common::import_alias_lookup;

pub(super) fn allocation_churn_findings(
    file: &ParsedFile,
    function: &ParsedFunction,
) -> Vec<Finding> {
    function
        .allocation_in_loop_lines
        .iter()
        .map(|line| Finding {
            rule_id: "allocation_churn_in_loop".to_string(),
            severity: Severity::Info,
            path: file.path.clone(),
            function_name: Some(function.fingerprint.name.clone()),
            start_line: *line,
            end_line: *line,
            message: format!(
                "function {} allocates new objects inside a loop",
                function.fingerprint.name
            ),
            evidence: vec![
                "make/new or buffer construction appears inside a loop".to_string(),
                "repeated per-iteration allocation can create avoidable heap churn".to_string(),
            ],
        })
        .collect()
}

pub(super) fn fmt_hot_path_findings(
    file: &ParsedFile,
    function: &ParsedFunction,
) -> Vec<Finding> {
    function
        .fmt_in_loop_lines
        .iter()
        .map(|line| Finding {
            rule_id: "fmt_hot_path".to_string(),
            severity: Severity::Info,
            path: file.path.clone(),
            function_name: Some(function.fingerprint.name.clone()),
            start_line: *line,
            end_line: *line,
            message: format!(
                "function {} formats strings with fmt inside a loop",
                function.fingerprint.name
            ),
            evidence: vec![
                "fmt formatting call appears inside a loop".to_string(),
                "fmt-heavy formatting in iterative paths can be expensive".to_string(),
            ],
        })
        .collect()
}

pub(super) fn reflection_hot_path_findings(
    file: &ParsedFile,
    function: &ParsedFunction,
) -> Vec<Finding> {
    function
        .reflection_in_loop_lines
        .iter()
        .map(|line| Finding {
            rule_id: "reflection_hot_path".to_string(),
            severity: Severity::Warning,
            path: file.path.clone(),
            function_name: Some(function.fingerprint.name.clone()),
            start_line: *line,
            end_line: *line,
            message: format!(
                "function {} uses reflection inside a loop",
                function.fingerprint.name
            ),
            evidence: vec![
                "reflect package call appears inside a loop".to_string(),
                "reflection in hot paths often adds avoidable overhead".to_string(),
            ],
        })
        .collect()
}

pub(super) fn string_concat_in_loop_findings(
    file: &ParsedFile,
    function: &ParsedFunction,
) -> Vec<Finding> {
    function
        .string_concat_in_loop_lines
        .iter()
        .map(|line| Finding {
            rule_id: "string_concat_in_loop".to_string(),
            severity: Severity::Info,
            path: file.path.clone(),
            function_name: Some(function.fingerprint.name.clone()),
            start_line: *line,
            end_line: *line,
            message: format!(
                "function {} concatenates strings inside a loop",
                function.fingerprint.name
            ),
            evidence: vec![
                "loop-local string concatenation can create repeated allocations".to_string(),
            ],
        })
        .collect()
}

pub(super) fn repeated_json_marshaling_findings(
    file: &ParsedFile,
    function: &ParsedFunction,
) -> Vec<Finding> {
    function
        .json_marshal_in_loop_lines
        .iter()
        .map(|line| Finding {
            rule_id: "repeated_json_marshaling".to_string(),
            severity: Severity::Info,
            path: file.path.clone(),
            function_name: Some(function.fingerprint.name.clone()),
            start_line: *line,
            end_line: *line,
            message: format!(
                "function {} marshals JSON inside a loop",
                function.fingerprint.name
            ),
            evidence: vec![
                "encoding/json marshal call appears inside a loop".to_string(),
                "repeated JSON serialization in iterative paths can become a hot allocation site"
                    .to_string(),
            ],
        })
        .collect()
}

pub(super) fn database_query_findings(
    file: &ParsedFile,
    function: &ParsedFunction,
) -> Vec<Finding> {
    let mut findings = Vec::new();

    for query_call in &function.db_query_calls {
        if query_call.in_loop && query_call.method_name != "Preload" {
            let receiver = query_call.receiver.as_deref().unwrap_or("<unknown>");
            findings.push(Finding {
                rule_id: "n_plus_one_query".to_string(),
                severity: Severity::Warning,
                path: file.path.clone(),
                function_name: Some(function.fingerprint.name.clone()),
                start_line: query_call.line,
                end_line: query_call.line,
                message: format!(
                    "function {} issues a database-style query inside a loop",
                    function.fingerprint.name
                ),
                evidence: vec![
                    format!("looped query method: {receiver}.{}", query_call.method_name),
                    "query execution inside loops often turns into N+1 access patterns"
                        .to_string(),
                ],
            });
        }

        let Some(query_text) = &query_call.query_text else {
            continue;
        };
        let normalized = query_text.to_ascii_uppercase();

        if normalized.starts_with("SELECT *") {
            findings.push(Finding {
                rule_id: "wide_select_query".to_string(),
                severity: Severity::Info,
                path: file.path.clone(),
                function_name: Some(function.fingerprint.name.clone()),
                start_line: query_call.line,
                end_line: query_call.line,
                message: format!(
                    "function {} issues a wide SELECT * query",
                    function.fingerprint.name
                ),
                evidence: vec![format!("query text: {query_text}")],
            });
        }

        if normalized.contains("LIKE '%")
            || normalized.contains(" ORDER BY ") && !normalized.contains(" LIMIT ")
        {
            findings.push(Finding {
                rule_id: "likely_unindexed_query".to_string(),
                severity: Severity::Info,
                path: file.path.clone(),
                function_name: Some(function.fingerprint.name.clone()),
                start_line: query_call.line,
                end_line: query_call.line,
                message: format!(
                    "function {} uses a query shape that may bypass effective indexing",
                    function.fingerprint.name
                ),
                evidence: vec![
                    format!("query text: {query_text}"),
                    "leading wildcard filters or ORDER BY without LIMIT often scale poorly"
                        .to_string(),
                ],
            });
        }
    }

    findings
}

pub(super) fn full_dataset_load_findings(
    file: &ParsedFile,
    function: &ParsedFunction,
) -> Vec<Finding> {
    let import_aliases = import_alias_lookup(&file.imports);

    function
        .calls
        .iter()
        .filter_map(|call| {
            let receiver = call.receiver.as_deref()?;
            let import_path = import_aliases.get(receiver)?;
            let evidence = match (import_path.as_str(), call.name.as_str()) {
                ("io", "ReadAll") | ("io/ioutil", "ReadAll") => {
                    Some(format!("{receiver}.{} reads the full stream into memory", call.name))
                }
                ("os", "ReadFile") => {
                    Some(format!("{receiver}.ReadFile loads the whole file before processing"))
                }
                _ => None,
            }?;

            Some(Finding {
                rule_id: "full_dataset_load".to_string(),
                severity: Severity::Info,
                path: file.path.clone(),
                function_name: Some(function.fingerprint.name.clone()),
                start_line: call.line,
                end_line: call.line,
                message: format!(
                    "function {} loads an entire payload into memory",
                    function.fingerprint.name
                ),
                evidence: vec![
                    format!("import alias {receiver} resolves to {import_path}"),
                    evidence,
                ],
            })
        })
        .collect()
}