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;
use crate::model::Finding;

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

use super::{
    business_value_like, credential_like, enabled_like, field_type_mentions, file_finding,
    secret_like, sensitive_default_like, struct_severity,
};

pub(crate) fn domain_findings(file: &ParsedFile) -> Vec<Finding> {
    let mut findings = Vec::new();

    for summary in file.structs() {
        for field in &summary.fields {
            if business_value_like(&field.name) && field.is_primitive {
                findings.push(file_finding(
                    file,
                    "rust_domain_raw_primitive",
                    struct_severity(summary),
                    field.line,
                    format!(
                        "struct {} uses a raw primitive for business data",
                        summary.name
                    ),
                    vec![format!(
                        "field {} has primitive type {}",
                        field.name, field.type_text
                    )],
                ));
            }

            if matches!(field.type_text.as_str(), "f32" | "f64")
                && (field.name.to_ascii_lowercase().contains("price")
                    || field.name.to_ascii_lowercase().contains("amount")
                    || field.name.to_ascii_lowercase().contains("money"))
            {
                findings.push(file_finding(
                    file,
                    "rust_domain_float_for_money",
                    struct_severity(summary),
                    field.line,
                    format!(
                        "struct {} uses floating-point storage for money-like data",
                        summary.name
                    ),
                    vec![format!("field {} has type {}", field.name, field.type_text)],
                ));
            }
        }

        let has_toggle = summary
            .fields
            .iter()
            .find(|field| field.is_bool && enabled_like(&field.name));
        let credential = summary
            .fields
            .iter()
            .find(|field| field.is_option && credential_like(&field.name));
        if let (Some(toggle), Some(credential)) = (has_toggle, credential) {
            findings.push(file_finding(
                file,
                "rust_domain_impossible_combination",
                struct_severity(summary),
                toggle.line,
                format!(
                    "struct {} mixes a toggle boolean with optional credentials",
                    summary.name
                ),
                vec![
                    format!("toggle field: {}", toggle.name),
                    format!("credential field: {}", credential.name),
                ],
            ));
        }

        if (summary.has_default_derive || summary.impl_default)
            && summary
                .fields
                .iter()
                .any(|field| sensitive_default_like(&field.name))
        {
            findings.push(file_finding(
                file,
                "rust_domain_default_produces_invalid",
                struct_severity(summary),
                summary.line,
                format!(
                    "struct {} has a Default implementation that may create invalid state",
                    summary.name
                ),
                vec![
                    format!("derives={}", summary.derives.join(", ")),
                    "sensitive or config-like fields are present on a type with Default"
                        .to_string(),
                ],
            ));
        }

        if summary.has_debug_derive && summary.fields.iter().any(|field| secret_like(&field.name)) {
            findings.push(file_finding(
                file,
                "rust_debug_secret",
                struct_severity(summary),
                summary.line,
                format!(
                    "struct {} derives Debug while carrying secret-like fields",
                    summary.name
                ),
                vec![
                    format!("derives={}", summary.derives.join(", ")),
                    "derive(Debug) can accidentally expose credentials in logs".to_string(),
                ],
            ));
        }

        if summary.has_deserialize_derive
            && summary
                .fields
                .iter()
                .any(|field| secret_like(&field.name) || sensitive_default_like(&field.name))
        {
            findings.push(file_finding(
                file,
                "rust_serde_sensitive_deserialize",
                struct_severity(summary),
                summary.line,
                format!(
                    "struct {} derives Deserialize for sensitive fields",
                    summary.name
                ),
                vec![
                    format!("derives={}", summary.derives.join(", ")),
                    "consider validation or custom Deserialize logic for sensitive inputs"
                        .to_string(),
                ],
            ));
        }

        if summary.has_serialize_derive
            && summary.fields.iter().any(|field| secret_like(&field.name))
        {
            findings.push(file_finding(
                file,
                "rust_serde_sensitive_serialize",
                struct_severity(summary),
                summary.line,
                format!(
                    "struct {} derives Serialize for secret-like fields",
                    summary.name
                ),
                vec![
                    format!("derives={}", summary.derives.join(", ")),
                    "consider skip_serializing or redaction for secret material".to_string(),
                ],
            ));
        }

        if summary
            .fields
            .iter()
            .any(|field| field_type_mentions(field, "Option<") && secret_like(&field.name))
            && summary.has_default_derive
        {
            findings.push(file_finding(
                file,
                "rust_domain_optional_secret_default",
                struct_severity(summary),
                summary.line,
                format!(
                    "struct {} defaults an optional secret-like field",
                    summary.name
                ),
                vec![
                    "defaults can hide whether sensitive configuration is actually valid"
                        .to_string(),
                ],
            ));
        }
    }

    findings
}