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

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

pub(crate) const BINDING_LOCATION: &str = file!();

use super::{
    file_finding, first_await_after, function_finding, has_cancellation_pattern, is_std_mutex,
};

pub(crate) fn async_function_findings(
    file: &ParsedFile,
    function: &ParsedFunction,
) -> Vec<Finding> {
    let rust = function.rust_evidence();
    let mut findings = Vec::new();

    if (rust.is_async || !rust.await_points.is_empty()) && is_std_mutex(file, function) {
        for lock in rust.lock_calls {
            if let Some(await_line) = first_await_after(function, lock.line) {
                findings.push(function_finding(
                    file,
                    function,
                    "rust_async_std_mutex_await",
                    Severity::Error,
                    lock.line,
                    format!(
                        "function {} appears to hold std::sync::Mutex across .await",
                        function.fingerprint.name
                    ),
                    vec![
                        format!("lock line: {}", lock.line),
                        format!("await line: {await_line}"),
                    ],
                ));
            }
        }
    }

    for permit in rust.permit_acquires {
        if let Some(await_line) = first_await_after(function, permit.line) {
            findings.push(function_finding(
                file,
                function,
                "rust_async_hold_permit_across_await",
                Severity::Warning,
                permit.line,
                format!(
                    "function {} may hold a permit or pooled resource across .await",
                    function.fingerprint.name
                ),
                vec![
                    format!("permit/resource acquisition line: {}", permit.line),
                    format!("await line: {await_line}"),
                ],
            ));
        }
    }

    if !rust.spawn_calls.is_empty()
        && !rust.await_points.is_empty()
        && !has_cancellation_pattern(function)
    {
        findings.push(function_finding(
            file,
            function,
            "rust_async_spawn_cancel_at_await",
            Severity::Warning,
            rust.spawn_calls[0].line,
            format!(
                "function {} spawns async work without an obvious cancellation path",
                function.fingerprint.name
            ),
            vec!["no CancellationToken or select!-based shutdown branch detected".to_string()],
        ));
    }

    if !rust.select_macro_lines.is_empty() && function.body_text.contains("loop") {
        if !function.body_text.contains("pin_mut!") && !function.body_text.contains(".fuse()") {
            findings.push(function_finding(
                file,
                function,
                "rust_async_missing_fuse_pin",
                Severity::Warning,
                rust.select_macro_lines[0],
                format!(
                    "function {} reuses select! without fuse/pin markers",
                    function.fingerprint.name
                ),
                vec![
                    format!("macro calls observed={}", rust.macro_calls.len()),
                    "consider pinning and fusing reused futures before select! loops".to_string(),
                ],
            ));
        }

        if function.body_text.contains("select!") && function.body_text.contains("(") {
            findings.push(function_finding(
                file,
                function,
                "rust_async_recreate_future_in_select",
                Severity::Info,
                rust.select_macro_lines[0],
                format!(
                    "function {} may recreate futures inside a select! loop",
                    function.fingerprint.name
                ),
                vec![
                    format!(
                        "tracked future-like bindings={}",
                        rust.futures_created.len()
                    ),
                    "move long-lived futures outside the loop when they are polled repeatedly"
                        .to_string(),
                ],
            ));
        }
    }

    if rust.is_async
        && rust.await_points.is_empty()
        && (function.body_text.contains("loop {") || function.body_text.contains("while "))
        && !rust.blocking_calls.is_empty()
    {
        findings.push(function_finding(
            file,
            function,
            "rust_async_monopolize_executor",
            Severity::Warning,
            rust.blocking_calls[0].line,
            format!("async function {} may monopolize the executor", function.fingerprint.name),
            vec!["long-running loops with blocking work and no await can starve cooperative scheduling".to_string()],
        ));
    }

    if rust.drop_impl && !rust.blocking_calls.is_empty() {
        findings.push(function_finding(
            file,
            function,
            "rust_async_blocking_drop",
            Severity::Warning,
            rust.blocking_calls[0].line,
            format!(
                "Drop implementation {} does blocking work that may surface in async code",
                function.fingerprint.name
            ),
            vec![
                "prefer explicit async shutdown paths instead of blocking cleanup in Drop"
                    .to_string(),
            ],
        ));
    }

    if !rust.await_points.is_empty() {
        let lines = function.body_text.lines().collect::<Vec<_>>();
        for (index, line) in lines.iter().enumerate() {
            if line.contains(".await")
                && index > 0
                && index + 1 < lines.len()
                && (lines[index - 1].contains("self.") || lines[index - 1].contains("state."))
                && (lines[index + 1].contains("self.") || lines[index + 1].contains("state."))
            {
                findings.push(function_finding(
                    file,
                    function,
                    "rust_async_invariant_broken_at_await",
                    Severity::Warning,
                    function.fingerprint.start_line + index,
                    format!("function {} mutates related state around an await boundary", function.fingerprint.name),
                    vec!["consider keeping invariant-maintaining state updates contiguous or protected".to_string()],
                ));
                break;
            }
        }
    }

    findings
}

pub(crate) fn async_file_findings(file: &ParsedFile) -> Vec<Finding> {
    let mut findings = Vec::new();
    let mut order_edges = BTreeMap::<(String, String), usize>::new();

    for function in &file.functions {
        let rust = function.rust_evidence();
        let receivers = rust
            .lock_calls
            .iter()
            .filter(|call| is_lock_order_call(call))
            .filter_map(|call| call.receiver.clone())
            .collect::<Vec<_>>();

        for pair in receivers.windows(2) {
            let edge = (pair[0].clone(), pair[1].clone());
            order_edges
                .entry(edge)
                .or_insert(function.fingerprint.start_line);
        }
    }

    for ((left, right), line) in &order_edges {
        if let Some(reverse_line) = order_edges.get(&(right.clone(), left.clone())) {
            findings.push(file_finding(
                file,
                "rust_async_lock_order_cycle",
                Severity::Error,
                *line,
                format!(
                    "file {} contains conflicting lock acquisition order",
                    file.path.display()
                ),
                vec![
                    format!("observed lock order {left} -> {right} at line {line}"),
                    format!("observed reverse order {right} -> {left} at line {reverse_line}"),
                ],
            ));
        }
    }

    findings
}

fn is_lock_order_call(call: &crate::analysis::RuntimeCall) -> bool {
    if matches!(call.name.as_str(), "lock" | "lock_owned") {
        return true;
    }

    let receiver = call
        .receiver
        .as_deref()
        .unwrap_or_default()
        .to_ascii_lowercase();
    matches!(call.name.as_str(), "read" | "write")
        && (receiver.contains("lock") || receiver.contains("mutex") || receiver.contains("rwlock"))
}