deslop 0.1.0

A static analyzer that spots low-context and AI-assisted code patterns across naming, concurrency, security, performance, and test quality.
Documentation
mod walker;

use std::fs;
use std::time::Instant;

use anyhow::{Context, Result};
use rayon::prelude::*;

use crate::analysis::{ParsedFile, analyzer_for_path};
use crate::heuristics::evaluate_findings;
use crate::index::build_repository_index;
use crate::model::{ParseFailure, ScanOptions, ScanReport, TimingBreakdown};
use crate::scan::walker::discover_go_files;

pub fn scan_repository(options: &ScanOptions) -> Result<ScanReport> {
    let total_start = Instant::now();

    let discover_start = Instant::now();
    let discovered_files = discover_go_files(&options.root, options.respect_ignore)
        .with_context(|| format!("failed to walk {}", options.root.display()))?;
    let discover_ms = discover_start.elapsed().as_millis();

    let parse_start = Instant::now();
    let mut parsed_files = Vec::new();
    let mut parse_failures = Vec::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) => parsed_files.push(file),
            FileOutcome::Generated(_) => {}
            FileOutcome::Failed(failure) => parse_failures.push(failure),
        }
    }
    let parse_ms = parse_start.elapsed().as_millis();

    let index_start = Instant::now();
    let index = build_repository_index(&options.root, &parsed_files);
    let index_summary = index.summary();
    let index_ms = index_start.elapsed().as_millis();

    let heuristics_start = Instant::now();
    let findings = evaluate_findings(&parsed_files, &index);
    let heuristics_ms = heuristics_start.elapsed().as_millis();

    let files_analyzed = parsed_files.len();
    let functions_found = parsed_files.iter().map(|file| file.functions.len()).sum();
    let files = parsed_files.iter().map(ParsedFile::to_report).collect();

    Ok(ScanReport {
        root: options.root.clone(),
        files_discovered: discovered_files.len(),
        files_analyzed,
        functions_found,
        files,
        findings,
        index_summary,
        parse_failures,
        timings: TimingBreakdown {
            discover_ms,
            parse_ms,
            index_ms,
            heuristics_ms,
            total_ms: total_start.elapsed().as_millis(),
        },
    })
}

enum FileOutcome {
    Parsed(ParsedFile),
    Generated(std::path::PathBuf),
    Failed(ParseFailure),
}

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

fn analyze_file(path: &std::path::Path) -> FileOutcome {
    match fs::read_to_string(path) {
        Ok(source) => {
            if is_generated_source(&source) {
                return FileOutcome::Generated(path.to_path_buf());
            }

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

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

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

#[cfg(test)]
mod tests {
    use super::is_generated_source;

    #[test]
    fn detects_generated_files() {
        let generated = "// Code generated by mockery. DO NOT EDIT.\npackage sample\n";
        assert!(is_generated_source(generated));
    }
}