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 std::collections::BTreeSet;

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

use super::common::{import_alias_lookup, is_potentially_blocking_call};

const COORDINATION_METHODS: &[&str] = &["Add", "Done", "Wait", "Go"];

pub(super) fn goroutine_shutdown_findings(
    file: &ParsedFile,
    function: &ParsedFunction,
) -> Vec<Finding> {
    function
        .goroutine_without_shutdown_lines
        .iter()
        .map(|line| Finding {
            rule_id: "goroutine_without_shutdown_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 {} launches a looping goroutine without an obvious shutdown path",
                function.fingerprint.name
            ),
            evidence: vec![
                "go func literal contains a loop".to_string(),
                "no ctx.Done() or done-channel shutdown signal was observed in the goroutine body"
                    .to_string(),
            ],
        })
        .collect()
}

pub(super) fn mutex_contention_findings(
    file: &ParsedFile,
    function: &ParsedFunction,
    imports: &[ImportSpec],
) -> Vec<Finding> {
    let mut findings = function
        .mutex_lock_in_loop_lines
        .iter()
        .map(|line| Finding {
            rule_id: "mutex_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 {} acquires a mutex inside a loop",
                function.fingerprint.name
            ),
            evidence: vec![
                "Lock or RLock appears inside a loop".to_string(),
                "repeated lock acquisition in iterative paths can create contention".to_string(),
            ],
        })
        .collect::<Vec<_>>();

    let import_aliases = import_alias_lookup(imports);
    let mut calls = function.calls.clone();
    calls.sort_by_key(|call| call.line);

    let mut active_locks = BTreeSet::new();
    let mut blocking_lines = BTreeSet::new();

    for call in calls {
        if let Some(receiver) = &call.receiver {
            if matches!(call.name.as_str(), "Lock" | "RLock") {
                active_locks.insert(receiver.clone());
                continue;
            }
            if matches!(call.name.as_str(), "Unlock" | "RUnlock") {
                active_locks.remove(receiver);
                continue;
            }
        }

        if !active_locks.is_empty() && is_potentially_blocking_call(&call, &import_aliases) {
            blocking_lines.insert(call.line);
        }
    }

    findings.extend(blocking_lines.into_iter().map(|line| Finding {
        rule_id: "blocking_call_while_locked".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 {} performs a potentially blocking call while a mutex appears held",
            function.fingerprint.name
        ),
        evidence: vec![
            "a blocking or external call was observed between Lock and Unlock".to_string(),
            "holding a mutex across I/O or sleeps can amplify contention".to_string(),
        ],
    }));

    findings
}

pub(super) fn goroutine_coordination_findings(
    file: &ParsedFile,
    function: &ParsedFunction,
) -> Vec<Finding> {
    if function.goroutine_launch_lines.is_empty() || has_obvious_coordination_signal(function) {
        return Vec::new();
    }

    let mut findings = function
        .goroutine_launch_lines
        .iter()
        .map(|line| Finding {
            rule_id: "goroutine_without_coordination".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 {} launches a goroutine without an obvious coordination signal",
                function.fingerprint.name
            ),
            evidence: vec![
                "raw go statement observed".to_string(),
                "no context.Context parameter or WaitGroup-like coordination call found"
                    .to_string(),
            ],
        })
        .collect::<Vec<_>>();

    findings.extend(function.goroutine_in_loop_lines.iter().map(|line| Finding {
        rule_id: "goroutine_spawn_in_loop".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 {} launches goroutines inside a loop without an obvious coordination signal",
            function.fingerprint.name
        ),
        evidence: vec![
            "raw go statement appears inside a loop".to_string(),
            "loop-local goroutine fan-out without context or WaitGroup-like coordination can grow unexpectedly"
                .to_string(),
        ],
    }));

    findings
}

fn has_obvious_coordination_signal(function: &ParsedFunction) -> bool {
    function.has_context_parameter
        || function.calls.iter().any(|call| {
            call.receiver.as_ref().is_some_and(|receiver| {
                COORDINATION_METHODS.contains(&call.name.as_str())
                    && matches!(receiver.as_str(), "wg" | "group" | "g" | "errGroup" | "errgroup")
            })
        })
}