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 super::{
    Severity, assert_rules_absent, assert_rules_present, find_rule, report_has_rule,
    scan_generated_files, scan_python_files,
};

#[test]
fn test_python_phase5_instance_attribute_escalation() {
    let report = scan_python_files(&[(
        "pkg/heavy_state.py",
        python_fixture!("structure/heavy_state_positive.txt"),
    )]);

    let finding = find_rule(&report, "too_many_instance_attributes")
        .expect("expected too_many_instance_attributes finding");
    assert!(matches!(finding.severity, Severity::Warning));
    assert!(
        finding
            .evidence
            .iter()
            .any(|evidence| evidence == "tier=20_plus"),
        "expected the escalated 20-plus evidence tier"
    );
}

#[test]
fn test_python_structure_rule_family_positive() {
    let report = scan_python_files(&[
        (
            "pkg/god_function.py",
            python_fixture!("structure/god_function_positive.txt"),
        ),
        (
            "pkg/mixed_concerns.py",
            python_fixture!("structure/mixed_concerns_positive.txt"),
        ),
        (
            "pkg/presenter.py",
            python_fixture!("structure/over_abstracted_wrapper_positive.txt"),
        ),
        (
            "pkg/session_state.py",
            python_fixture!("structure/too_many_instance_attributes_positive.txt"),
        ),
        (
            "pkg/parser.py",
            python_fixture!("structure/name_responsibility_positive.txt"),
        ),
        (
            "pkg/billing.py",
            python_fixture!("structure/god_class_positive.txt"),
        ),
    ]);

    assert_rules_present(
        &report,
        &[
            "god_function",
            "mixed_concerns_function",
            "over_abstracted_wrapper",
            "too_many_instance_attributes",
            "name_responsibility_mismatch",
            "god_class",
        ],
    );
}

#[test]
fn test_python_structure_rule_family_negative() {
    let report = scan_python_files(&[
        (
            "pkg/god_function.py",
            python_fixture!("structure/god_function_negative.txt"),
        ),
        (
            "pkg/mixed_concerns.py",
            python_fixture!("structure/mixed_concerns_negative.txt"),
        ),
        (
            "pkg/presenter.py",
            python_fixture!("structure/over_abstracted_wrapper_negative.txt"),
        ),
        (
            "pkg/session_state.py",
            python_fixture!("structure/too_many_instance_attributes_negative.txt"),
        ),
        (
            "pkg/parser.py",
            python_fixture!("structure/name_responsibility_negative.txt"),
        ),
        (
            "pkg/billing.py",
            python_fixture!("structure/god_class_negative.txt"),
        ),
    ]);

    assert_rules_absent(
        &report,
        &[
            "god_function",
            "mixed_concerns_function",
            "over_abstracted_wrapper",
            "too_many_instance_attributes",
            "name_responsibility_mismatch",
            "god_class",
        ],
    );
}

#[test]
fn test_python_phase5_monolithic_module_rule() {
    let report = scan_generated_files(|workspace| {
        let mut module = String::from(python_fixture!(
            "integration/phase5/monolithic_module_prefix.txt"
        ));
        for index in 0..320 {
            module.push_str(&format!(
                "\ndef helper_{index}(payload):\n    record = str(payload).strip()\n    if not record:\n        return ''\n    return record.lower()\n"
            ));
        }
        workspace.write_file("pkg/module.py", &module);
    });

    assert!(
        report_has_rule(&report, "monolithic_module"),
        "expected monolithic_module to fire"
    );
}

#[test]
fn test_python_phase5_over_abstracted_wrapper_expansion() {
    let report = scan_python_files(&[(
        "pkg/presenter.py",
        python_fixture!("structure/over_abstracted_wrapper_positive.txt"),
    )]);

    assert!(
        report_has_rule(&report, "over_abstracted_wrapper"),
        "expected over_abstracted_wrapper to fire for a ceremonial wrapper class"
    );
}

#[test]
fn test_python_phase5_over_abstracted_wrapper_skips_lifecycle_classes() {
    let report = scan_python_files(&[(
        "pkg/runtime.py",
        python_fixture!("structure/over_abstracted_wrapper_negative.txt"),
    )]);

    assert!(
        !report_has_rule(&report, "over_abstracted_wrapper"),
        "did not expect over_abstracted_wrapper for lifecycle-heavy classes"
    );
}

#[test]
fn test_python_phase5_over_abstracted_wrapper_skips_dataclass_wrappers() {
    let report = scan_python_files(&[(
        "pkg/presenter.py",
        python_fixture!("structure/over_abstracted_wrapper_dataclass_negative.txt"),
    )]);

    assert!(
        !report_has_rule(&report, "over_abstracted_wrapper"),
        "did not expect over_abstracted_wrapper for dataclass wrappers"
    );
}

#[test]
fn test_python_phase5_name_responsibility_mismatch_expansion() {
    let report = scan_python_files(&[
        (
            "pkg/parser.py",
            python_fixture!("structure/name_responsibility_parser_positive.txt"),
        ),
        (
            "pkg/report_helper.py",
            python_fixture!("structure/name_responsibility_helper_positive.txt"),
        ),
    ]);

    assert!(
        report.findings.iter().any(|finding| {
            finding.rule_id == "name_responsibility_mismatch"
                && (finding.function_name.as_deref() == Some("parse_user")
                    || finding.path.ends_with("pkg/report_helper.py"))
        }),
        "expected expanded name_responsibility_mismatch anchors to fire"
    );
}

#[test]
fn test_python_phase5_name_responsibility_mismatch_skips_honest_transformers() {
    let report = scan_python_files(&[(
        "pkg/parser.py",
        python_fixture!("structure/name_responsibility_negative.txt"),
    )]);

    assert!(
        !report_has_rule(&report, "name_responsibility_mismatch"),
        "did not expect name_responsibility_mismatch for honest parse helpers"
    );
}

#[test]
fn test_python_phase5_name_responsibility_mismatch_skips_select_queries() {
    let report = scan_python_files(&[(
        "pkg/database.py",
        python_fixture!("structure/name_responsibility_sql_read_negative.txt"),
    )]);

    assert!(
        !report_has_rule(&report, "name_responsibility_mismatch"),
        "did not expect name_responsibility_mismatch for read helpers issuing SELECT queries"
    );
}

#[test]
fn test_python_phase5_name_responsibility_mismatch_skips_contextmanager_factories() {
    let report = scan_python_files(&[(
        "pkg/context.py",
        python_fixture!("structure/name_responsibility_contextmanager_negative.txt"),
    )]);

    assert!(
        !report_has_rule(&report, "name_responsibility_mismatch"),
        "did not expect name_responsibility_mismatch for context manager factories"
    );
}

#[test]
fn test_python_phase5_monolithic_module_skips_broad_legitimate_modules() {
    let report = scan_generated_files(|workspace| {
        let mut registry_module = String::from(python_fixture!(
            "integration/phase5/legit_registry_prefix.txt"
        ));
        for index in 0..500 {
            registry_module.push_str(&format!(
                "\ndef provide_{index}():\n    value = 'entry_{index}'\n    register(value, value)\n    return REGISTRY[value]\n"
            ));
        }

        let mut schema_module = String::from(python_fixture!(
            "integration/phase5/legit_schemas_prefix.txt"
        ));
        for index in 0..320 {
            schema_module.push_str(&format!(
                "\nclass EventSchema{index}:\n    event_id = 'event_{index}'\n    source = 'api'\n    kind = 'schema'\n    version = {index}\n"
            ));
        }

        let mut api_surface_module = String::from(python_fixture!(
            "integration/phase5/legit_api_surface_prefix.txt"
        ));
        for index in 0..520 {
            api_surface_module.push_str(&format!(
                "\ndef route_{index}(request):\n    payload = {{'route': {index}, 'request': request}}\n    return render(payload)\n"
            ));
        }

        workspace.write_file("pkg/registry.py", &registry_module);
        workspace.write_file("pkg/schemas.py", &schema_module);
        workspace.write_file("pkg/api_surface.py", &api_surface_module);
    });

    let flagged_paths = report
        .findings
        .iter()
        .filter(|finding| finding.rule_id == "monolithic_module")
        .map(|finding| finding.path.to_string_lossy().into_owned())
        .collect::<Vec<_>>();
    assert!(
        flagged_paths.is_empty(),
        "did not expect broad-but-legitimate modules to be flagged: {flagged_paths:?}"
    );
}