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 std::fs;
use std::path::{Path, PathBuf};

use deslop::syntax_error_for_source;

const CORPUS_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/corpus/regressions");
// Intentional maintenance guard. If this changes, review the fixture diff and update
// [guides/inventory-regression-guards.md] in the same change.
const EXPECTED_CORPUS_ENTRY_COUNT: usize = 9;

#[test]
fn corpus_regressions_cover_malformed_generated_and_edge_cases() {
    let entries = collect_sources(Path::new(CORPUS_ROOT));
    assert_eq!(
        entries.len(),
        EXPECTED_CORPUS_ENTRY_COUNT,
        "corpus regression inventory changed; if intentional, update EXPECTED_CORPUS_ENTRY_COUNT and guides/inventory-regression-guards.md"
    );

    let mut category_counts = BTreeMap::<String, usize>::new();
    for entry in entries {
        let source = fs::read_to_string(&entry)
            .unwrap_or_else(|error| panic!("failed to read {}: {error}", entry.display()));
        let relative = entry
            .strip_prefix(CORPUS_ROOT)
            .unwrap_or_else(|error| panic!("failed to strip corpus prefix: {error}"));
        let language_extension = relative
            .components()
            .next()
            .and_then(|component| component.as_os_str().to_str())
            .and_then(|language| match language {
                "go" => Some("go"),
                "python" => Some("py"),
                "rust" => Some("rs"),
                _ => None,
            })
            .unwrap_or_else(|| panic!("unexpected corpus language in {}", relative.display()));
        let synthetic_path = PathBuf::from(format!("corpus_fixture.{language_extension}"));
        let syntax_error = syntax_error_for_source(&synthetic_path, &source)
            .unwrap_or_else(|error| panic!("failed to parse {}: {error}", entry.display()));

        let category = relative
            .parent()
            .and_then(|parent| parent.file_name())
            .and_then(|name| name.to_str())
            .unwrap_or("<unknown>");
        *category_counts.entry(category.to_string()).or_insert(0) += 1;

        match category {
            "malformed" => {
                assert!(
                    syntax_error,
                    "{} should keep syntax_error=true",
                    relative.display()
                );
            }
            "generated" | "edge_cases" => {
                assert!(
                    !syntax_error,
                    "{} should parse as a valid corpus regression",
                    relative.display()
                );
            }
            other => panic!(
                "unexpected corpus category {other} in {}",
                relative.display()
            ),
        }
    }

    assert_eq!(
        category_counts,
        BTreeMap::from([
            ("edge_cases".to_string(), 3),
            ("generated".to_string(), 3),
            ("malformed".to_string(), 3),
        ]),
        "corpus category breakdown changed; if intentional, update the grouped counts and guides/inventory-regression-guards.md"
    );
}

fn collect_sources(root: &Path) -> Vec<PathBuf> {
    let mut files = Vec::new();
    collect_sources_recursive(root, &mut files);
    files.sort();
    files
}

fn collect_sources_recursive(dir: &Path, files: &mut Vec<PathBuf>) {
    let entries = fs::read_dir(dir)
        .unwrap_or_else(|error| panic!("failed to read directory {}: {error}", dir.display()));

    for entry in entries {
        let entry = entry
            .unwrap_or_else(|error| panic!("failed to read entry in {}: {error}", dir.display()));
        let path = entry.path();
        if path.is_dir() {
            collect_sources_recursive(&path, files);
        } else if path
            .extension()
            .and_then(|extension| extension.to_str())
            .is_some_and(|extension| extension == "txt")
        {
            files.push(path);
        }
    }
}