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 clients;
mod gorm;
mod sql;

use std::collections::BTreeMap;

use crate::analysis::{GormChainStep, GormQueryChain, ParsedFile, ParsedFunction};
use crate::model::{Finding, Severity};

pub(crate) const BINDING_LOCATION: &str = file!();
pub(crate) const CLIENTS_BINDING_LOCATION: &str = clients::BINDING_LOCATION;
pub(crate) const GORM_BINDING_LOCATION: &str = gorm::BINDING_LOCATION;
pub(crate) const SQL_BINDING_LOCATION: &str = sql::BINDING_LOCATION;

use super::gin::prepare_like_call_lines;
use super::{
    BodyLine, body_lines, has_import_path, has_sql_like_import, import_aliases_for,
    is_likely_non_request_workload, is_request_path_function, join_lines,
};

use self::clients::{pool_lifecycle_findings, service_client_lifecycle_findings};

use self::gorm::{
    duplicate_find_then_preload_findings, find_all_then_manual_paginate_findings,
    gorm_chain_findings, gorm_loop_findings, gorm_query_shape_findings, orm_tuning_findings,
};
use self::sql::{
    exists_via_count_star_findings, nested_transaction_findings,
    repeated_same_query_template_findings, row_by_row_upsert_findings,
    rows_to_struct_per_row_findings, scan_into_map_findings, select_or_get_inside_loop_findings,
    sql_loop_findings, sql_query_shape_findings, sqlx_select_unbounded_findings,
    unbounded_in_clause_findings,
};

pub(crate) fn data_access_performance_findings(
    file: &ParsedFile,
    function: &ParsedFunction,
) -> Vec<Finding> {
    let mut findings = Vec::new();
    let lines = body_lines(function);
    let request_path = is_request_path_function(file, function);

    findings.extend(gorm_chain_findings(file, function, request_path));
    findings.extend(gorm_loop_findings(file, function));
    findings.extend(select_or_get_inside_loop_findings(file, function));
    findings.extend(repeated_same_query_template_findings(file, function));
    findings.extend(exists_via_count_star_findings(file, function));
    findings.extend(duplicate_find_then_preload_findings(file, function));
    findings.extend(gorm_query_shape_findings(file, function, request_path));
    findings.extend(sql_query_shape_findings(
        file,
        function,
        &lines,
        request_path,
    ));
    findings.extend(unbounded_in_clause_findings(file, function, &lines));
    findings.extend(scan_into_map_findings(file, function, &lines));
    findings.extend(rows_to_struct_per_row_findings(file, function, &lines));
    findings.extend(sqlx_select_unbounded_findings(
        file,
        function,
        &lines,
        request_path,
    ));
    findings.extend(sql_loop_findings(file, function, &lines));
    findings.extend(pool_lifecycle_findings(
        file,
        function,
        &lines,
        request_path,
    ));
    findings.extend(service_client_lifecycle_findings(
        file,
        function,
        &lines,
        request_path,
    ));
    findings.extend(orm_tuning_findings(file, function, &lines, request_path));
    findings.extend(find_all_then_manual_paginate_findings(
        file, function, &lines,
    ));
    findings.extend(row_by_row_upsert_findings(file, function, &lines));

    if request_path {
        findings.extend(nested_transaction_findings(file, function, &lines));
        for alias in import_aliases_for(file, "database/sql") {
            for body_line in &lines {
                if body_line.text.contains(&format!("{alias}.Open("))
                    || body_line.text.contains(&format!("{alias}.OpenDB("))
                {
                    findings.push(Finding {
                        rule_id: "sql_open_per_request".to_string(),
                        severity: Severity::Warning,
                        path: file.path.clone(),
                        function_name: Some(function.fingerprint.name.clone()),
                        start_line: body_line.line,
                        end_line: body_line.line,
                        message: format!(
                            "function {} opens a database/sql handle on a request path",
                            function.fingerprint.name
                        ),
                        evidence: vec![
                            format!("{}.Open(...) observed at line {}", alias, body_line.line),
                            "database pools are usually initialized once and reused across requests"
                                .to_string(),
                        ],
                    });
                }
            }
        }

        let mut prepare_groups = BTreeMap::<String, Vec<usize>>::new();
        for (line, query_text) in prepare_like_call_lines(&lines) {
            prepare_groups.entry(query_text).or_default().push(line);
        }

        for (query_text, prepare_lines) in prepare_groups {
            if prepare_lines.len() < 2 {
                continue;
            }

            findings.push(Finding {
                rule_id: "prepare_on_every_request_same_sql".to_string(),
                severity: Severity::Info,
                path: file.path.clone(),
                function_name: Some(function.fingerprint.name.clone()),
                start_line: prepare_lines[1],
                end_line: prepare_lines[1],
                message: format!(
                    "function {} prepares the same SQL multiple times on a request path",
                    function.fingerprint.name
                ),
                evidence: vec![
                    format!(
                        "Prepare-like calls for the same query were observed at lines {}",
                        join_lines(&prepare_lines)
                    ),
                    format!("query text: {query_text}"),
                ],
            });
        }

        if has_sql_like_import(file) {
            for call in &function.calls {
                if matches!(call.name.as_str(), "Ping" | "PingContext") {
                    findings.push(Finding {
                        rule_id: "db_ping_per_request".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 {} pings a database handle on a request path",
                            function.fingerprint.name
                        ),
                        evidence: vec![
                            format!(
                                "{}{} observed at line {}",
                                call.receiver
                                    .as_deref()
                                    .map(|receiver| format!("{receiver}."))
                                    .unwrap_or_default(),
                                call.name,
                                call.line
                            ),
                            "database connectivity checks are usually better handled during startup or explicit health checks"
                                .to_string(),
                        ],
                    });
                }
            }
        }
    }

    findings
}