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));
}
}