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

use rayon::prelude::*;

use crate::analysis::{ParsedFile, backend_for_path};
use crate::io::canonicalize_within_root;
use crate::model::ParseFailure;
use crate::{DEFAULT_MAX_BYTES, read_to_string_limited};

use super::suppression::{SuppressionDirective, parse_suppression_directives};

pub(super) fn analyze_discovered_files(
    discovered_files: &[PathBuf],
) -> (
    Vec<ParsedFile>,
    Vec<ParseFailure>,
    BTreeMap<PathBuf, Vec<SuppressionDirective>>,
) {
    let mut parsed_files = Vec::new();
    let mut parse_failures = Vec::new();
    let mut suppressions = BTreeMap::new();
    let mut outcomes = discovered_files
        .par_iter()
        .map(|path| analyze_file(path))
        .collect::<Vec<_>>();
    outcomes.sort_by(|left, right| left.path().cmp(right.path()));

    for outcome in outcomes {
        match outcome {
            FileOutcome::Parsed {
                file,
                suppressions: file_suppressions,
            } => {
                suppressions.insert(file.path.clone(), file_suppressions);
                parsed_files.push(*file);
            }
            FileOutcome::Generated(_) => {}
            FileOutcome::Failed(failure) => parse_failures.push(failure),
        }
    }

    (parsed_files, parse_failures, suppressions)
}

pub(super) fn is_generated(source: &str) -> bool {
    source.lines().take(5).any(|line| {
        let normalized = line.trim();
        normalized.contains("Code generated") && normalized.contains("DO NOT EDIT")
    })
}

enum FileOutcome {
    Parsed {
        file: Box<ParsedFile>,
        suppressions: Vec<SuppressionDirective>,
    },
    Generated(PathBuf),
    Failed(ParseFailure),
}

impl FileOutcome {
    fn path(&self) -> &Path {
        match self {
            Self::Parsed { file, .. } => &file.path,
            Self::Generated(path) => path,
            Self::Failed(failure) => &failure.path,
        }
    }
}

fn analyze_file(path: &Path) -> FileOutcome {
    let path = match canonicalize_within_root(path.parent().unwrap_or(path), path) {
        Ok(path) => path,
        Err(error) => {
            return FileOutcome::Failed(ParseFailure {
                path: path.to_path_buf(),
                message: error.to_string(),
            });
        }
    };

    match read_to_string_limited(&path, DEFAULT_MAX_BYTES) {
        Ok(source) => {
            if is_generated(&source) {
                return FileOutcome::Generated(path.clone());
            }

            let suppressions = parse_suppression_directives(&source);

            let Some(analyzer) = backend_for_path(&path) else {
                return FileOutcome::Failed(ParseFailure {
                    path: path.clone(),
                    message: format!("no analyzer registered for {}", path.display()),
                });
            };

            match analyzer.parse_file(&path, &source) {
                Ok(file) => FileOutcome::Parsed {
                    file: Box::new(file),
                    suppressions,
                },
                Err(error) => FileOutcome::Failed(ParseFailure {
                    path: path.clone(),
                    message: error.to_string(),
                }),
            }
        }
        Err(error) => FileOutcome::Failed(ParseFailure {
            path,
            message: error.to_string(),
        }),
    }
}