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 crate::analysis::{ParsedFile, ParsedFunction};
use crate::model::{Finding, Severity};

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

use super::is_scanner_infra_file;
use super::{
    contains_any, first_line_with_any, has_numeric_narrowing_cast, has_secret_like_text,
    is_main_like_file, is_test_like,
};

pub(crate) const RULE_DEFINITIONS: &[crate::rules::catalog::RuleDefinition] = &[
    crate::rules::catalog::RuleDefinition {
        id: "rust_internal_anyhow_result",
        language: crate::rules::catalog::RuleLanguage::Rust,
        family: "boundary",
        default_severity: crate::rules::catalog::RuleDefaultSeverity::Warning,
        status: crate::rules::catalog::RuleStatus::Stable,
        configurability: &[
            crate::rules::catalog::RuleConfigurability::Disable,
            crate::rules::catalog::RuleConfigurability::Ignore,
            crate::rules::catalog::RuleConfigurability::SeverityOverride,
        ],
        description: "Internal library functions that return anyhow-style error surfaces instead of crate-local errors.",
        binding_location: crate::rules::catalog::bindings::RUST_BOUNDARY,
    },
    crate::rules::catalog::RuleDefinition {
        id: "rust_unbounded_read_to_string",
        language: crate::rules::catalog::RuleLanguage::Rust,
        family: "boundary",
        default_severity: crate::rules::catalog::RuleDefaultSeverity::Warning,
        status: crate::rules::catalog::RuleStatus::Stable,
        configurability: &[
            crate::rules::catalog::RuleConfigurability::Disable,
            crate::rules::catalog::RuleConfigurability::Ignore,
            crate::rules::catalog::RuleConfigurability::SeverityOverride,
        ],
        description: "Production code that reads an entire file into a string without a size bound.",
        binding_location: crate::rules::catalog::bindings::RUST_BOUNDARY,
    },
    crate::rules::catalog::RuleDefinition {
        id: "rust_check_then_open_path",
        language: crate::rules::catalog::RuleLanguage::Rust,
        family: "boundary",
        default_severity: crate::rules::catalog::RuleDefaultSeverity::Warning,
        status: crate::rules::catalog::RuleStatus::Stable,
        configurability: &[
            crate::rules::catalog::RuleConfigurability::Disable,
            crate::rules::catalog::RuleConfigurability::Ignore,
            crate::rules::catalog::RuleConfigurability::SeverityOverride,
        ],
        description: "Filesystem code that checks metadata or existence before opening a path.",
        binding_location: crate::rules::catalog::bindings::RUST_BOUNDARY,
    },
    crate::rules::catalog::RuleDefinition {
        id: "rust_secret_equality_compare",
        language: crate::rules::catalog::RuleLanguage::Rust,
        family: "boundary",
        default_severity: crate::rules::catalog::RuleDefaultSeverity::Warning,
        status: crate::rules::catalog::RuleStatus::Stable,
        configurability: &[
            crate::rules::catalog::RuleConfigurability::Disable,
            crate::rules::catalog::RuleConfigurability::Ignore,
            crate::rules::catalog::RuleConfigurability::SeverityOverride,
        ],
        description: "Direct equality or inequality comparisons on secret-like values.",
        binding_location: crate::rules::catalog::bindings::RUST_BOUNDARY,
    },
    crate::rules::catalog::RuleDefinition {
        id: "rust_narrowing_numeric_cast",
        language: crate::rules::catalog::RuleLanguage::Rust,
        family: "boundary",
        default_severity: crate::rules::catalog::RuleDefaultSeverity::Info,
        status: crate::rules::catalog::RuleStatus::Stable,
        configurability: &[
            crate::rules::catalog::RuleConfigurability::Disable,
            crate::rules::catalog::RuleConfigurability::Ignore,
            crate::rules::catalog::RuleConfigurability::SeverityOverride,
        ],
        description: "Numeric narrowing casts that may silently truncate or change precision.",
        binding_location: crate::rules::catalog::bindings::RUST_BOUNDARY,
    },
    crate::rules::catalog::RuleDefinition {
        id: "rust_manual_tempdir_lifecycle",
        language: crate::rules::catalog::RuleLanguage::Rust,
        family: "boundary",
        default_severity: crate::rules::catalog::RuleDefaultSeverity::Info,
        status: crate::rules::catalog::RuleStatus::Stable,
        configurability: &[
            crate::rules::catalog::RuleConfigurability::Disable,
            crate::rules::catalog::RuleConfigurability::Ignore,
            crate::rules::catalog::RuleConfigurability::SeverityOverride,
        ],
        description: "Manual temp-directory setup and cleanup that should usually use RAII helpers.",
        binding_location: crate::rules::catalog::bindings::RUST_BOUNDARY,
    },
];

pub(crate) fn boundary_file_findings(file: &ParsedFile) -> Vec<Finding> {
    let _ = file;
    Vec::new()
}

pub(crate) fn boundary_function_findings(
    file: &ParsedFile,
    function: &ParsedFunction,
) -> Vec<Finding> {
    if is_test_like(file, Some(function)) {
        return Vec::new();
    }

    let mut findings = Vec::new();
    findings.extend(internal_anyhow_findings(file, function));
    findings.extend(unbounded_read_findings(file, function));
    findings.extend(check_then_open_findings(file, function));
    findings.extend(secret_comparison_findings(file, function));
    findings.extend(narrowing_cast_findings(file, function));
    findings.extend(manual_tempdir_function_findings(file, function));
    findings
}

fn internal_anyhow_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
    let signature = function.signature_text.trim_start();
    if signature.starts_with("pub ") || is_main_like_file(file) {
        return Vec::new();
    }

    let Some(line) = first_line_with_any(
        &function.signature_text,
        function.fingerprint.start_line,
        &[
            "anyhow::Result",
            "anyhow::Error",
            "eyre::Result",
            "eyre::Report",
            "color_eyre::Result",
        ],
    ) else {
        return Vec::new();
    };

    vec![Finding {
        rule_id: "rust_internal_anyhow_result".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 {} returns an anyhow-style error surface in internal code",
            function.fingerprint.name
        ),
        evidence: vec![
            function.signature_text.trim().to_string(),
            "prefer a crate-local error enum for internal library code".to_string(),
        ],
    }]
}

fn unbounded_read_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
    if is_scanner_infra_file(file)
        || function.body_text.contains("take(max_bytes")
        || function.body_text.contains("InputTooLarge")
        || function.body_text.contains("max_bytes")
    {
        return Vec::new();
    }

    let Some(line) = first_line_with_any(
        &function.body_text,
        function.fingerprint.start_line,
        &[".read_to_string(", "read_to_string("],
    ) else {
        return Vec::new();
    };

    vec![Finding {
        rule_id: "rust_unbounded_read_to_string".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 {} reads an entire file into memory without an obvious bound",
            function.fingerprint.name
        ),
        evidence: vec![
            "fs::read_to_string or a similar full-file read was detected".to_string(),
            "prefer bounded readers or streaming for production paths".to_string(),
        ],
    }]
}

fn check_then_open_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
    if is_scanner_infra_file(file) {
        return Vec::new();
    }

    let mut check_line = None;
    for (offset, line) in function.body_text.lines().enumerate() {
        if check_line.is_none()
            && contains_any(line, &["exists(", "symlink_metadata(", "read_link("])
        {
            check_line = Some(function.fingerprint.start_line + offset);
            continue;
        }

        if check_line.is_some() && contains_any(line, &["File::open(", ".open(", "read_to_string("])
        {
            let start_line = check_line.unwrap_or(function.fingerprint.start_line);
            return vec![Finding {
                rule_id: "rust_check_then_open_path".to_string(),
                severity: Severity::Warning,
                path: file.path.clone(),
                function_name: Some(function.fingerprint.name.clone()),
                start_line,
                end_line: start_line,
                message: format!(
                    "function {} checks filesystem state before opening a path",
                    function.fingerprint.name
                ),
                evidence: vec![
                    "check-then-open flow may race on mutable filesystems".to_string(),
                    "canonicalize or open first with the desired flags when appropriate"
                        .to_string(),
                ],
            }];
        }
    }

    Vec::new()
}

fn secret_comparison_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
    if is_scanner_infra_file(file) {
        return Vec::new();
    }

    for (offset, line) in function.body_text.lines().enumerate() {
        if !(line.contains("==") || line.contains("!=")) {
            continue;
        }

        if !has_secret_like_text(line)
            && !function
                .local_binding_names
                .iter()
                .any(|name| has_secret_like_text(name))
        {
            continue;
        }

        return vec![Finding {
            rule_id: "rust_secret_equality_compare".to_string(),
            severity: Severity::Warning,
            path: file.path.clone(),
            function_name: Some(function.fingerprint.name.clone()),
            start_line: function.fingerprint.start_line + offset,
            end_line: function.fingerprint.start_line + offset,
            message: format!(
                "function {} compares a secret-like value with == or !=",
                function.fingerprint.name
            ),
            evidence: vec![line.trim().to_string()],
        }];
    }

    Vec::new()
}

fn narrowing_cast_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
    if is_scanner_infra_file(file) {
        return Vec::new();
    }

    for (offset, line) in function.body_text.lines().enumerate() {
        if !has_numeric_narrowing_cast(line) {
            continue;
        }

        return vec![Finding {
            rule_id: "rust_narrowing_numeric_cast".to_string(),
            severity: Severity::Info,
            path: file.path.clone(),
            function_name: Some(function.fingerprint.name.clone()),
            start_line: function.fingerprint.start_line + offset,
            end_line: function.fingerprint.start_line + offset,
            message: format!(
                "function {} uses a numeric narrowing cast",
                function.fingerprint.name
            ),
            evidence: vec![line.trim().to_string()],
        }];
    }

    Vec::new()
}

fn manual_tempdir_function_findings(file: &ParsedFile, function: &ParsedFunction) -> Vec<Finding> {
    if is_scanner_infra_file(file) {
        return Vec::new();
    }

    if !(contains_any(
        &function.body_text,
        &["tempfile::Builder::new()", "tempfile::Builder::new"],
    ) || contains_any(&function.body_text, &["std::env::temp_dir()", "temp_dir()"]))
    {
        return Vec::new();
    }

    if !contains_any(
        &function.body_text,
        &["remove_dir_all(", "fs::remove_dir_all(", ".remove_dir_all("],
    ) {
        return Vec::new();
    }

    let Some(line) = first_line_with_any(
        &function.body_text,
        function.fingerprint.start_line,
        &[
            "tempfile::Builder::new()",
            "std::env::temp_dir()",
            "temp_dir()",
        ],
    ) else {
        return Vec::new();
    };

    vec![Finding {
        rule_id: "rust_manual_tempdir_lifecycle".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 {} manually creates and cleans up a temp directory",
            function.fingerprint.name
        ),
        evidence: vec![
            "prefer tempfile::TempDir or another RAII helper when possible".to_string(),
            "manual cleanup is easy to get wrong in error paths".to_string(),
        ],
    }]
}