use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
use std::time::{Duration, Instant};
use fallow_config::{AuditGate, OutputFormat};
use fallow_engine::changed_files::clear_ambient_git_env;
use rustc_hash::{FxHashMap, FxHashSet};
use xxhash_rust::xxh3::xxh3_64;
pub use fallow_api::{AuditAttribution, AuditSummary, AuditVerdict};
#[cfg(test)]
use crate::base_worktree::git_rev_parse;
use crate::base_worktree::{BaseWorktree, git_toplevel, sweep_old_reusable_caches};
use crate::check::{CheckOptions, CheckResult, IssueFilters, TraceOptions};
use crate::dupes::{DupesMode, DupesOptions, DupesResult};
use crate::error::emit_error;
use crate::health::{HealthOptions, HealthResult};
pub struct AuditResult {
pub verdict: AuditVerdict,
pub summary: AuditSummary,
pub attribution: AuditAttribution,
pub base_snapshot: Option<AuditKeySnapshot>,
pub base_snapshot_skipped: bool,
pub changed_files_count: usize,
pub changed_files: Vec<PathBuf>,
pub base_ref: String,
pub base_description: Option<String>,
pub head_sha: Option<String>,
pub output: OutputFormat,
pub performance: bool,
pub check: Option<CheckResult>,
pub dupes: Option<DupesResult>,
pub health: Option<HealthResult>,
pub elapsed: Duration,
pub review_deltas: Option<crate::audit_brief::ReviewDeltas>,
pub weakening_signals: Vec<weakening::WeakeningSignal>,
pub routing: Option<routing::RoutingFacts>,
pub decision_surface: Option<crate::audit_decision_surface::DecisionSurface>,
pub graph_snapshot_hash: Option<String>,
pub change_anchors: Vec<crate::audit_walkthrough::ChangeAnchor>,
pub diff_index: Option<fallow_output::DiffIndex>,
}
pub struct AuditOptions<'a> {
pub root: &'a std::path::Path,
pub config_path: &'a Option<std::path::PathBuf>,
pub cache_dir: &'a std::path::Path,
pub output: OutputFormat,
pub no_cache: bool,
pub threads: usize,
pub quiet: bool,
pub allow_remote_extends: bool,
pub changed_since: Option<&'a str>,
pub production: bool,
pub production_dead_code: Option<bool>,
pub production_health: Option<bool>,
pub production_dupes: Option<bool>,
pub workspace: Option<&'a [String]>,
pub changed_workspaces: Option<&'a str>,
pub explain: bool,
pub explain_skipped: bool,
pub performance: bool,
pub group_by: Option<crate::GroupBy>,
pub dead_code_baseline: Option<&'a std::path::Path>,
pub health_baseline: Option<&'a std::path::Path>,
pub dupes_baseline: Option<&'a std::path::Path>,
pub max_crap: Option<f64>,
pub coverage: Option<&'a std::path::Path>,
pub coverage_root: Option<&'a std::path::Path>,
pub gate: AuditGate,
pub include_entry_exports: bool,
pub css: bool,
pub css_deep: bool,
pub runtime_coverage: Option<&'a std::path::Path>,
pub min_invocations_hot: u64,
pub brief: bool,
pub max_decisions: usize,
pub walkthrough_guide: bool,
pub walkthrough: bool,
pub mark_viewed: &'a [std::path::PathBuf],
pub show_cleared: bool,
pub walkthrough_file: Option<&'a std::path::Path>,
pub show_deprioritized: bool,
}
#[path = "audit_base_ref.rs"]
mod base_ref;
#[path = "audit_cache.rs"]
mod cache;
#[cfg(test)]
use base_ref::{auto_detect_base_ref, parse_audit_base_override};
use base_ref::{get_head_sha, resolve_base_ref};
#[cfg(test)]
use cache::{
AUDIT_BASE_SNAPSHOT_CACHE_VERSION, CachedAuditKeySnapshot, audit_base_snapshot_cache_dir,
audit_base_snapshot_cache_file, cached_from_snapshot, config_file_fingerprint,
ensure_audit_base_snapshot_cache_dir, snapshot_from_cached,
};
use cache::{
AuditBaseSnapshotCacheKey, audit_base_snapshot_cache_key, load_cached_base_snapshot,
save_cached_base_snapshot, sorted_keys,
};
fn styling_finding_gates(rules: &fallow_config::RulesConfig, code: &str) -> bool {
let severity = match code {
"css-token-drift" => rules.css_token_drift,
"css-duplicate-block" => rules.css_duplicate_block,
"css-selector-complexity" => rules.css_selector_complexity,
"css-dead-surface" => rules.css_dead_surface,
"css-broken-reference" => rules.css_broken_reference,
_ => fallow_config::Severity::Warn,
};
severity == fallow_config::Severity::Error
}
fn compute_verdict(
check: Option<&CheckResult>,
dupes: Option<&DupesResult>,
health: Option<&HealthResult>,
) -> AuditVerdict {
let mut has_errors = false;
let mut has_warnings = false;
if let Some(result) = check {
if crate::check::has_error_severity_issues(
&result.results,
&result.config.rules,
Some(&result.config),
) {
has_errors = true;
} else if result.results.total_issues() > 0 {
has_warnings = true;
}
}
if let Some(result) = health
&& !result.report.findings.is_empty()
{
has_errors = true;
}
if let Some(result) = health
&& result
.report
.styling_findings
.iter()
.any(|finding| styling_finding_gates(&result.config.rules, &finding.code))
{
has_errors = true;
}
if let Some(result) = dupes
&& !result.report.clone_groups.is_empty()
{
if result.threshold > 0.0 && result.report.stats.duplication_percentage > result.threshold {
has_errors = true;
} else {
has_warnings = true;
}
}
if has_errors {
AuditVerdict::Fail
} else if has_warnings {
AuditVerdict::Warn
} else {
AuditVerdict::Pass
}
}
fn build_summary(
check: Option<&CheckResult>,
dupes: Option<&DupesResult>,
health: Option<&HealthResult>,
) -> AuditSummary {
let dead_code_issues = check.map_or(0, |r| r.results.total_issues());
let dead_code_has_errors = check.is_some_and(|r| {
crate::check::has_error_severity_issues(&r.results, &r.config.rules, Some(&r.config))
});
let complexity_findings = health.map_or(0, |r| r.report.findings.len());
let max_cyclomatic = health.and_then(|r| r.report.findings.iter().map(|f| f.cyclomatic).max());
let duplication_clone_groups = dupes.map_or(0, |r| r.report.clone_groups.len());
AuditSummary {
dead_code_issues,
dead_code_has_errors,
complexity_findings,
max_cyclomatic,
duplication_clone_groups,
}
}
fn compute_audit_attribution(
check: Option<&CheckResult>,
dupes: Option<&DupesResult>,
health: Option<&HealthResult>,
base: Option<&AuditKeySnapshot>,
gate: AuditGate,
) -> AuditAttribution {
let dead_code = check
.map(|r| {
count_introduced(
&dead_code_keys(&r.results, &r.config.root),
base.map(|b| &b.dead_code),
)
})
.unwrap_or_default();
let complexity = health
.map(|r| {
count_introduced(
&health_keys(&r.report, &r.config.root),
base.map(|b| &b.health),
)
})
.unwrap_or_default();
let duplication = dupes
.map(|r| {
count_introduced(
&dupes_keys(&r.report, &r.config.root),
base.map(|b| &b.dupes),
)
})
.unwrap_or_default();
AuditAttribution {
gate,
dead_code_introduced: dead_code.0,
dead_code_inherited: dead_code.1,
complexity_introduced: complexity.0,
complexity_inherited: complexity.1,
duplication_introduced: duplication.0,
duplication_inherited: duplication.1,
}
}
fn compute_introduced_verdict(
check: Option<&CheckResult>,
dupes: Option<&DupesResult>,
health: Option<&HealthResult>,
base: Option<&AuditKeySnapshot>,
) -> AuditVerdict {
let mut has_errors = false;
let mut has_warnings = false;
if let Some(result) = check {
let (errors, warnings) = introduced_check_verdict(result, base);
has_errors |= errors;
has_warnings |= warnings;
}
if let Some(result) = health {
has_errors |= introduced_health_has_errors(result, base);
}
if let Some(result) = health
&& result
.report
.styling_findings
.iter()
.any(|finding| introduced_styling_finding_gates(result, base, finding))
{
has_errors = true;
}
if let Some(result) = dupes {
let (errors, warnings) = introduced_dupes_verdict(result, base);
has_errors |= errors;
has_warnings |= warnings;
}
if has_errors {
AuditVerdict::Fail
} else if has_warnings {
AuditVerdict::Warn
} else {
AuditVerdict::Pass
}
}
fn introduced_check_verdict(result: &CheckResult, base: Option<&AuditKeySnapshot>) -> (bool, bool) {
let base_keys = base.map(|b| &b.dead_code);
let mut introduced = result.results.clone();
retain_introduced_dead_code(&mut introduced, &result.config.root, base_keys);
if crate::check::has_error_severity_issues(
&introduced,
&result.config.rules,
Some(&result.config),
) {
(true, false)
} else if introduced.total_issues() > 0 {
(false, true)
} else {
(false, false)
}
}
fn introduced_health_has_errors(result: &HealthResult, base: Option<&AuditKeySnapshot>) -> bool {
let base_keys = base.map(|b| &b.health);
result.report.findings.iter().any(|finding| {
!base_keys
.is_some_and(|keys| keys.contains(&health_finding_key(finding, &result.config.root)))
})
}
fn introduced_styling_finding_gates(
result: &HealthResult,
base: Option<&AuditKeySnapshot>,
finding: &fallow_output::StylingFinding,
) -> bool {
styling_finding_gates(&result.config.rules, &finding.code)
&& !base.is_some_and(|snapshot| {
snapshot
.styling
.contains(&styling_finding_key(finding, &result.config.root))
})
}
fn introduced_dupes_verdict(result: &DupesResult, base: Option<&AuditKeySnapshot>) -> (bool, bool) {
let base_keys = base.map(|b| &b.dupes);
let introduced = result
.report
.clone_groups
.iter()
.filter(|group| {
!base_keys
.is_some_and(|keys| keys.contains(&dupe_group_key(group, &result.config.root)))
})
.count();
if introduced == 0 {
return (false, false);
}
if result.threshold > 0.0 && result.report.stats.duplication_percentage > result.threshold {
(true, false)
} else {
(false, true)
}
}
pub struct AuditKeySnapshot {
dead_code: FxHashSet<String>,
health: FxHashSet<String>,
styling: FxHashSet<String>,
dupes: FxHashSet<String>,
boundary_edges: FxHashSet<String>,
cycles: FxHashSet<String>,
public_api: FxHashSet<String>,
}
fn count_introduced(keys: &FxHashSet<String>, base: Option<&FxHashSet<String>>) -> (usize, usize) {
let Some(base) = base else {
return (0, 0);
};
keys.iter().fold((0, 0), |(introduced, inherited), key| {
if base.contains(key) {
(introduced, inherited + 1)
} else {
(introduced + 1, inherited)
}
})
}
fn ambient_git_env_hint() -> Option<String> {
use fallow_engine::changed_files::AMBIENT_GIT_ENV_VARS;
for var in AMBIENT_GIT_ENV_VARS {
if let Ok(value) = std::env::var(var)
&& !value.is_empty()
{
return Some(format!(
"{var}={value} is set in the environment; if fallow is being \
invoked from a git hook this can interfere with worktree operations. Re-run \
with `env -u {var} fallow audit` to confirm."
));
}
}
None
}
fn compute_base_snapshot(
opts: &AuditOptions<'_>,
base_ref: &str,
changed_files: &FxHashSet<PathBuf>,
base_sha: Option<&str>,
) -> Result<AuditKeySnapshot, ExitCode> {
let Some(worktree) = BaseWorktree::create(opts.root, base_ref, base_sha) else {
use std::fmt::Write as _;
let mut message =
format!("could not create a temporary worktree for base ref '{base_ref}'");
if let Some(hint) = ambient_git_env_hint() {
let _ = write!(message, "\n hint: {hint}");
}
return Err(emit_error(&message, 2, opts.output));
};
let base_root = base_analysis_root(opts.root, worktree.path());
let base_cache_dir = remap_cache_dir_for_base_worktree(opts.root, &base_root, opts.cache_dir);
let current_config_path = opts
.config_path
.clone()
.or_else(|| fallow_config::FallowConfig::find_config_path(opts.root));
let base_opts =
build_base_audit_options(opts, &base_root, ¤t_config_path, &base_cache_dir);
let base_changed_files = remap_focus_files(changed_files, opts.root, &base_root);
let check_production = opts.production_dead_code.unwrap_or(opts.production);
let health_production = opts.production_health.unwrap_or(opts.production);
let share_dead_code_parse_with_health = check_production == health_production;
let empty_changed_files = FxHashSet::default();
let base_changed_files_ref = base_changed_files.as_ref().unwrap_or(&empty_changed_files);
let (check_res, dupes_res) = rayon::join(
|| {
run_audit_check(
&base_opts,
None,
base_changed_files_ref,
share_dead_code_parse_with_health,
)
},
|| run_audit_dupes(&base_opts, None, base_changed_files.as_ref(), None),
);
let mut check = check_res?;
let dupes = dupes_res?;
let base_public_api = if opts.brief {
public_api_keys_from_check(check.as_ref(), &base_root)
} else {
FxHashSet::default()
};
let shared_parse = if share_dead_code_parse_with_health {
check.as_mut().and_then(|r| r.shared_parse.take())
} else {
None
};
let health = run_audit_health(&base_opts, None, shared_parse)?;
if let Some(ref mut check) = check {
check.shared_parse = None;
}
Ok(snapshot_from_results(
check.as_ref(),
dupes.as_ref(),
health.as_ref(),
base_public_api,
))
}
fn snapshot_from_results(
check: Option<&CheckResult>,
dupes: Option<&DupesResult>,
health: Option<&HealthResult>,
public_api: FxHashSet<String>,
) -> AuditKeySnapshot {
let (boundary_edges, cycles) = check.map_or_else(
|| (FxHashSet::default(), FxHashSet::default()),
|r| {
(
review_deltas::boundary_edge_keys(&r.results.boundary_violations),
review_deltas::cycle_keys(&r.results.circular_dependencies, &r.config.root),
)
},
);
AuditKeySnapshot {
dead_code: check.map_or_else(FxHashSet::default, |r| {
dead_code_keys(&r.results, &r.config.root)
}),
health: health.map_or_else(FxHashSet::default, |r| {
health_keys(&r.report, &r.config.root)
}),
styling: health.map_or_else(FxHashSet::default, |r| {
styling_keys(&r.report, &r.config.root)
}),
dupes: dupes.map_or_else(FxHashSet::default, |r| {
dupes_keys(&r.report, &r.config.root)
}),
boundary_edges,
cycles,
public_api,
}
}
fn public_api_keys_from_check(check: Option<&CheckResult>, root: &Path) -> FxHashSet<String> {
let Some(check) = check else {
return FxHashSet::default();
};
let Some(graph) = check
.shared_parse
.as_ref()
.and_then(|sp| sp.analysis_output.as_ref())
.and_then(|out| out.graph.as_ref())
else {
return FxHashSet::default();
};
review_deltas::public_export_keys_for(graph, &check.config, &check.workspaces, root)
}
#[expect(
clippy::ref_option,
reason = "AuditOptions.config_path is &Option<PathBuf>; the borrow is stored into the returned struct"
)]
fn build_base_audit_options<'a>(
opts: &AuditOptions<'a>,
base_root: &'a Path,
current_config_path: &'a Option<PathBuf>,
base_cache_dir: &'a Path,
) -> AuditOptions<'a> {
AuditOptions {
root: base_root,
config_path: current_config_path,
cache_dir: base_cache_dir,
output: opts.output,
no_cache: opts.no_cache,
threads: opts.threads,
quiet: true,
allow_remote_extends: opts.allow_remote_extends,
changed_since: None,
production: opts.production,
production_dead_code: opts.production_dead_code,
production_health: opts.production_health,
production_dupes: opts.production_dupes,
workspace: opts.workspace,
changed_workspaces: None,
explain: false,
explain_skipped: false,
performance: false,
group_by: opts.group_by,
dead_code_baseline: None,
health_baseline: None,
dupes_baseline: None,
max_crap: opts.max_crap,
coverage: opts.coverage,
coverage_root: opts.coverage_root,
gate: AuditGate::All,
include_entry_exports: opts.include_entry_exports,
css: opts.css,
css_deep: opts.css_deep,
runtime_coverage: None,
min_invocations_hot: opts.min_invocations_hot,
brief: false,
max_decisions: 4,
walkthrough_guide: false,
walkthrough: false,
mark_viewed: &[],
show_cleared: false,
walkthrough_file: None,
show_deprioritized: false,
}
}
fn base_analysis_root(current_root: &Path, base_worktree_root: &Path) -> PathBuf {
let Some(git_root) = git_toplevel(current_root) else {
return base_worktree_root.to_path_buf();
};
let current_root =
dunce::canonicalize(current_root).unwrap_or_else(|_| current_root.to_path_buf());
match current_root.strip_prefix(&git_root) {
Ok(relative) => base_worktree_root.join(relative),
Err(err) => {
tracing::warn!(
current_root = %current_root.display(),
git_root = %git_root.display(),
error = %err,
"Could not remap audit base root into the base worktree; falling back to worktree root"
);
base_worktree_root.to_path_buf()
}
}
}
fn current_keys_as_base_keys(
check: Option<&CheckResult>,
dupes: Option<&DupesResult>,
health: Option<&HealthResult>,
) -> AuditKeySnapshot {
let public_api = check
.and_then(|r| r.public_api_keys.clone())
.unwrap_or_default();
snapshot_from_results(check, dupes, health, public_api)
}
fn can_reuse_current_as_base(
opts: &AuditOptions<'_>,
base_ref: &str,
changed_files: &FxHashSet<PathBuf>,
) -> bool {
let Some(git_root) = git_toplevel(opts.root) else {
return false;
};
let cache_dir = opts.cache_dir.to_path_buf();
let canonical_cache_dir = dunce::canonicalize(&cache_dir).ok();
let mut reader: Option<BaseFileReader> = None;
for path in changed_files {
if is_fallow_cache_artifact(path, &cache_dir, canonical_cache_dir.as_deref()) {
continue;
}
if !is_analysis_input(path) {
if is_non_behavioral_doc(path) {
continue;
}
return false;
}
let Ok(current) = std::fs::read_to_string(path) else {
return false;
};
let Ok(relative) = path.strip_prefix(&git_root) else {
return false;
};
let reader = match reader.as_mut() {
Some(reader) => reader,
None => {
let Some(spawned) = BaseFileReader::spawn(opts.root) else {
return false;
};
reader.insert(spawned)
}
};
let Some(base) = reader.read(base_ref, relative) else {
return false;
};
if current == base {
continue;
}
if !js_ts_tokens_equivalent(path, ¤t, &base) {
return false;
}
}
true
}
struct BaseFileReader {
child: Option<crate::signal::ScopedChild>,
stdin: Option<std::process::ChildStdin>,
stdout: std::io::BufReader<std::process::ChildStdout>,
}
impl BaseFileReader {
fn spawn(root: &Path) -> Option<Self> {
let mut command = Command::new("git");
command
.args(["cat-file", "--batch"])
.current_dir(root)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null());
clear_ambient_git_env(&mut command);
let mut child = crate::signal::ScopedChild::spawn(&mut command).ok()?;
let stdin = child.take_stdin()?;
let stdout = child.take_stdout()?;
Some(Self {
child: Some(child),
stdin: Some(stdin),
stdout: std::io::BufReader::new(stdout),
})
}
fn read(&mut self, base_ref: &str, relative: &Path) -> Option<String> {
use std::io::{BufRead, Read};
let relative = relative.to_string_lossy().replace('\\', "/");
if relative.contains('\n') {
return None;
}
let stdin = self.stdin.as_mut()?;
writeln!(stdin, "{base_ref}:{relative}").ok()?;
stdin.flush().ok()?;
let mut header = String::new();
if self.stdout.read_line(&mut header).ok()? == 0 {
return None;
}
if header.trim_end().ends_with(" missing") {
return None;
}
let size: usize = header.trim_end().rsplit(' ').next()?.parse().ok()?;
let mut buf = vec![0u8; size];
self.stdout.read_exact(&mut buf).ok()?;
let mut newline = [0u8; 1];
self.stdout.read_exact(&mut newline).ok()?;
Some(String::from_utf8_lossy(&buf).into_owned())
}
}
impl Drop for BaseFileReader {
fn drop(&mut self) {
self.stdin.take();
if let Some(child) = self.child.take() {
let _ = child.wait();
}
}
}
fn is_fallow_cache_artifact(
path: &Path,
cache_dir: &Path,
canonical_cache_dir: Option<&Path>,
) -> bool {
path.starts_with(cache_dir)
|| canonical_cache_dir.is_some_and(|canonical| path.starts_with(canonical))
}
fn remap_cache_dir_for_base_worktree(
current_root: &Path,
base_worktree_root: &Path,
cache_dir: &Path,
) -> PathBuf {
if cache_dir.is_absolute()
&& let Ok(relative) = cache_dir.strip_prefix(current_root)
{
return base_worktree_root.join(relative);
}
cache_dir.to_path_buf()
}
fn is_analysis_input(path: &Path) -> bool {
matches!(
path.extension().and_then(|ext| ext.to_str()),
Some(
"js" | "jsx"
| "ts"
| "tsx"
| "mjs"
| "mts"
| "cjs"
| "cts"
| "vue"
| "svelte"
| "astro"
| "mdx"
| "css"
| "scss"
)
)
}
fn is_non_behavioral_doc(path: &Path) -> bool {
matches!(
path.extension().and_then(|ext| ext.to_str()),
Some("md" | "markdown" | "txt" | "rst" | "adoc")
)
}
fn js_ts_tokens_equivalent(path: &Path, current: &str, base: &str) -> bool {
if current.contains("fallow-ignore") || base.contains("fallow-ignore") {
return false;
}
if !matches!(
path.extension().and_then(|ext| ext.to_str()),
Some("js" | "jsx" | "ts" | "tsx" | "mjs" | "mts" | "cjs" | "cts")
) {
return false;
}
fallow_engine::duplicates::source_token_kinds_equivalent(path, current, base, false)
}
fn remap_focus_files(
files: &FxHashSet<PathBuf>,
from_root: &Path,
to_root: &Path,
) -> Option<FxHashSet<PathBuf>> {
let mut remapped = FxHashSet::default();
for file in files {
if let Ok(relative) = file.strip_prefix(from_root) {
remapped.insert(to_root.join(relative));
}
}
if remapped.is_empty() {
return None;
}
Some(remapped)
}
#[cfg(test)]
use std::time::SystemTime;
#[cfg(test)]
use crate::base_worktree::{
ReusableWorktreeLock, WorktreeCleanupGuard, audit_worktree_pid, days_to_duration,
is_fallow_audit_worktree_path, is_reusable_audit_worktree_path, list_audit_worktrees,
materialize_base_dependency_context, parse_worktree_list, paths_equal, process_is_alive,
remove_audit_worktree, reusable_audit_worktree_path, reusable_worktree_last_used_path,
reusable_worktree_lock_path, reusable_worktree_sha_path, sweep_orphan_audit_worktrees_in,
touch_last_used, unregister_worktree,
};
pub use fallow_api::audit_keys as keys;
#[path = "audit_review_deltas.rs"]
pub mod review_deltas;
#[path = "audit_weakening.rs"]
pub mod weakening;
#[path = "audit_routing.rs"]
pub mod routing;
use keys::{
dead_code_keys, dupe_group_key, dupes_keys, health_finding_key, health_keys,
retain_introduced_dead_code, styling_finding_key, styling_keys,
};
struct HeadAnalyses {
check: Option<CheckResult>,
dupes: Option<DupesResult>,
health: Option<HealthResult>,
}
type HeadAndBaseResult = (
Result<HeadAnalyses, ExitCode>,
Option<Result<AuditKeySnapshot, ExitCode>>,
);
fn run_audit_head_and_base(
opts: &AuditOptions<'_>,
changed_since: Option<&str>,
changed_files: &FxHashSet<PathBuf>,
base_ref: &str,
base_cache_key: Option<&AuditBaseSnapshotCacheKey>,
run_base: bool,
) -> HeadAndBaseResult {
if run_base {
let base_sha = base_cache_key.map(|key| key.base_sha.as_str());
let (h, b) = rayon::join(
|| run_audit_head_analyses(opts, changed_since, changed_files),
|| compute_base_snapshot(opts, base_ref, changed_files, base_sha),
);
(h, Some(b))
} else {
(
run_audit_head_analyses(opts, changed_since, changed_files),
None,
)
}
}
struct AuditResultParts {
verdict: AuditVerdict,
summary: AuditSummary,
attribution: AuditAttribution,
base_snapshot: Option<AuditKeySnapshot>,
base_snapshot_skipped: bool,
changed_files_count: usize,
changed_files: FxHashSet<PathBuf>,
base_ref: String,
base_description: Option<String>,
head_sha: Option<String>,
output: OutputFormat,
performance: bool,
check: Option<CheckResult>,
dupes: Option<DupesResult>,
health: Option<HealthResult>,
elapsed: Duration,
review_deltas: Option<crate::audit_brief::ReviewDeltas>,
weakening_signals: Vec<weakening::WeakeningSignal>,
routing: Option<routing::RoutingFacts>,
decision_surface: Option<crate::audit_decision_surface::DecisionSurface>,
graph_snapshot_hash: Option<String>,
change_anchors: Vec<crate::audit_walkthrough::ChangeAnchor>,
diff_index: Option<fallow_output::DiffIndex>,
}
#[derive(Default)]
struct AuditBriefData {
review_deltas: Option<crate::audit_brief::ReviewDeltas>,
weakening_signals: Vec<weakening::WeakeningSignal>,
routing: Option<routing::RoutingFacts>,
decision_surface: Option<crate::audit_decision_surface::DecisionSurface>,
graph_snapshot_hash: Option<String>,
change_anchors: Vec<crate::audit_walkthrough::ChangeAnchor>,
diff_index: Option<fallow_output::DiffIndex>,
}
#[derive(Clone, Copy)]
struct AuditBriefDataInput<'a> {
opts: &'a AuditOptions<'a>,
check: Option<&'a CheckResult>,
dupes: Option<&'a DupesResult>,
health: Option<&'a HealthResult>,
base_snapshot: Option<&'a AuditKeySnapshot>,
changed_files: &'a FxHashSet<PathBuf>,
base_ref: &'a str,
head_sha: Option<&'a str>,
}
fn run_audit_head_analyses(
opts: &AuditOptions<'_>,
changed_since: Option<&str>,
changed_files: &FxHashSet<PathBuf>,
) -> Result<HeadAnalyses, ExitCode> {
let check_production = opts.production_dead_code.unwrap_or(opts.production);
let health_production = opts.production_health.unwrap_or(opts.production);
let dupes_production = opts.production_dupes.unwrap_or(opts.production);
let share_dead_code_parse_with_health = check_production == health_production;
let share_dead_code_files_with_dupes =
share_dead_code_parse_with_health && check_production == dupes_production;
let mut check = run_audit_check(
opts,
changed_since,
changed_files,
share_dead_code_parse_with_health,
)?;
let dupes_files = if share_dead_code_files_with_dupes {
check
.as_ref()
.and_then(|r| r.shared_parse.as_ref().map(|sp| sp.files.clone()))
} else {
None
};
let dupes = run_audit_dupes(opts, changed_since, Some(changed_files), dupes_files)?;
if opts.brief
&& let Some(ref mut check) = check
{
check.impact_closure = compute_brief_impact_closure(opts.root, check, changed_files);
check.public_api_keys = Some(public_api_keys_from_check(Some(check), opts.root));
check.partition_order = compute_brief_partition_order(opts.root, check, changed_files);
check.focus_facts = compute_brief_focus_facts(opts.root, check, changed_files);
check.export_lines = compute_brief_export_lines(opts.root, check, changed_files);
check.internal_consumers =
compute_brief_internal_consumers(opts.root, check, changed_files);
}
let shared_parse = if share_dead_code_parse_with_health {
check.as_mut().and_then(|r| r.shared_parse.take())
} else {
None
};
let health = run_audit_health(opts, changed_since, shared_parse)?;
Ok(HeadAnalyses {
check,
dupes,
health,
})
}
fn compute_brief_impact_closure(
root: &std::path::Path,
check: &CheckResult,
changed_files: &FxHashSet<PathBuf>,
) -> Option<fallow_engine::module_graph::ImpactClosurePaths> {
let graph = check
.shared_parse
.as_ref()
.and_then(|sp| sp.analysis_output.as_ref())
.and_then(|out| out.graph.as_ref())?;
fallow_engine::module_graph::impact_closure_for_changed_paths(graph, root, changed_files)
}
fn compute_brief_partition_order(
root: &std::path::Path,
check: &CheckResult,
changed_files: &FxHashSet<PathBuf>,
) -> Option<fallow_engine::module_graph::PartitionOrderPaths> {
let graph = check
.shared_parse
.as_ref()
.and_then(|sp| sp.analysis_output.as_ref())
.and_then(|out| out.graph.as_ref())?;
fallow_engine::module_graph::partition_order_for_changed_paths(graph, root, changed_files)
}
fn compute_brief_export_lines(
root: &std::path::Path,
check: &CheckResult,
changed_files: &FxHashSet<PathBuf>,
) -> Option<FxHashMap<String, Vec<(String, u32)>>> {
let graph = check
.shared_parse
.as_ref()
.and_then(|sp| sp.analysis_output.as_ref())
.and_then(|out| out.graph.as_ref())?;
fallow_engine::module_graph::export_lines_for_changed_paths(graph, root, changed_files)
}
fn compute_brief_internal_consumers(
root: &std::path::Path,
check: &CheckResult,
changed_files: &FxHashSet<PathBuf>,
) -> Option<FxHashMap<String, u64>> {
let graph = check
.shared_parse
.as_ref()
.and_then(|sp| sp.analysis_output.as_ref())
.and_then(|out| out.graph.as_ref())?;
fallow_engine::module_graph::internal_consumers_for_changed_paths(graph, root, changed_files)
}
fn compute_brief_focus_facts(
root: &std::path::Path,
check: &CheckResult,
changed_files: &FxHashSet<PathBuf>,
) -> Option<Vec<fallow_engine::module_graph::FocusFileFactsPaths>> {
let graph = check
.shared_parse
.as_ref()
.and_then(|sp| sp.analysis_output.as_ref())
.and_then(|out| out.graph.as_ref())?;
fallow_engine::module_graph::focus_facts_for_changed_paths(graph, root, changed_files)
}
pub fn execute_audit(opts: &AuditOptions<'_>) -> Result<AuditResult, ExitCode> {
let start = Instant::now();
let (base_ref, base_description) = resolve_base_ref(opts)?;
let Some(mut changed_files) = crate::check::get_changed_files(opts.root, &base_ref) else {
return Err(emit_error(
&format!(
"could not determine changed files for base ref '{base_ref}'. Verify the ref exists in this git repository"
),
2,
opts.output,
));
};
if let Some(walkthrough_file) = opts.walkthrough_file
&& let Ok(walkthrough_file) = dunce::canonicalize(walkthrough_file)
{
changed_files.remove(&walkthrough_file);
}
let changed_files_count = changed_files.len();
if changed_files.is_empty() {
return Ok(empty_audit_result(
base_ref,
base_description,
opts,
start.elapsed(),
));
}
sweep_old_reusable_caches(
opts.root,
crate::base_worktree::resolve_cache_max_age_with_options(
opts.root,
opts.config_path.as_ref(),
opts.allow_remote_extends,
),
opts.quiet,
);
let changed_since = Some(base_ref.as_str());
let needs_real_base_snapshot = matches!(opts.gate, AuditGate::NewOnly)
&& !can_reuse_current_as_base(opts, &base_ref, &changed_files);
let base_cache_key = if needs_real_base_snapshot {
audit_base_snapshot_cache_key(opts, &base_ref, &changed_files)?
} else {
None
};
let cached_base_snapshot = base_cache_key
.as_ref()
.and_then(|key| load_cached_base_snapshot(opts, key));
let (head_res, base_res) = run_audit_head_and_base(
opts,
changed_since,
&changed_files,
&base_ref,
base_cache_key.as_ref(),
needs_real_base_snapshot && cached_base_snapshot.is_none(),
);
assemble_audit_result(AuditAssemblyInput {
opts,
head_res,
base_res,
cached_base_snapshot,
base_cache_key,
changed_files,
changed_files_count,
base_ref,
base_description,
start,
})
}
struct AuditAssemblyInput<'a> {
opts: &'a AuditOptions<'a>,
head_res: Result<HeadAnalyses, ExitCode>,
base_res: Option<Result<AuditKeySnapshot, ExitCode>>,
cached_base_snapshot: Option<AuditKeySnapshot>,
base_cache_key: Option<AuditBaseSnapshotCacheKey>,
changed_files: FxHashSet<PathBuf>,
changed_files_count: usize,
base_ref: String,
base_description: Option<String>,
start: Instant,
}
fn assemble_audit_result(input: AuditAssemblyInput<'_>) -> Result<AuditResult, ExitCode> {
let opts = input.opts;
let head = input.head_res?;
let mut check_result = head.check;
let dupes_result = head.dupes;
let health_result = head.health;
let (base_snapshot, base_snapshot_skipped) = resolve_base_snapshot(
opts,
input.cached_base_snapshot,
input.base_res,
input.base_cache_key.as_ref(),
CurrentAnalysisRefs {
check: check_result.as_ref(),
dupes: dupes_result.as_ref(),
health: health_result.as_ref(),
},
)?;
drop_check_shared_parse(&mut check_result);
let (attribution, verdict, summary) = compute_audit_outcome(
opts.gate,
check_result.as_ref(),
dupes_result.as_ref(),
health_result.as_ref(),
base_snapshot.as_ref(),
);
let head_sha = get_head_sha(opts.root);
let brief = compute_audit_brief_data(AuditBriefDataInput {
opts,
check: check_result.as_ref(),
dupes: dupes_result.as_ref(),
health: health_result.as_ref(),
base_snapshot: base_snapshot.as_ref(),
changed_files: &input.changed_files,
base_ref: &input.base_ref,
head_sha: head_sha.as_deref(),
});
Ok(build_audit_result(AuditResultParts {
verdict,
summary,
attribution,
base_snapshot,
base_snapshot_skipped,
changed_files_count: input.changed_files_count,
changed_files: input.changed_files,
base_ref: input.base_ref,
base_description: input.base_description,
head_sha,
output: opts.output,
performance: opts.performance,
check: check_result,
dupes: dupes_result,
health: health_result,
elapsed: input.start.elapsed(),
review_deltas: brief.review_deltas,
weakening_signals: brief.weakening_signals,
routing: brief.routing,
decision_surface: brief.decision_surface,
graph_snapshot_hash: brief.graph_snapshot_hash,
change_anchors: brief.change_anchors,
diff_index: brief.diff_index,
}))
}
fn drop_check_shared_parse(check_result: &mut Option<CheckResult>) {
if let Some(check) = check_result {
check.shared_parse = None;
}
}
fn compute_audit_brief_data(input: AuditBriefDataInput<'_>) -> AuditBriefData {
if !input.opts.brief {
return AuditBriefData::default();
}
let (review_deltas, weakening_signals, routing) = compute_brief_e3_data(
input.opts,
input.check,
input.base_snapshot,
input.changed_files,
input.base_ref,
);
let decision_surface = Some(compute_decision_surface(
input.opts,
input.check,
review_deltas.as_ref(),
routing.as_ref(),
));
let diff_evidence =
compute_brief_diff_evidence(input.opts.root, input.base_ref, input.opts.walkthrough_file);
let change_anchors = diff_evidence.change_anchors;
let graph_snapshot_hash = Some(compute_graph_snapshot_hash(
input.check,
input.dupes,
input.health,
input.base_ref,
input.head_sha,
&change_anchors,
));
AuditBriefData {
review_deltas,
weakening_signals,
routing,
decision_surface,
graph_snapshot_hash,
change_anchors,
diff_index: diff_evidence.diff_index,
}
}
fn compute_graph_snapshot_hash(
check: Option<&CheckResult>,
dupes: Option<&DupesResult>,
health: Option<&HealthResult>,
base_ref: &str,
head_sha: Option<&str>,
change_anchors: &[crate::audit_walkthrough::ChangeAnchor],
) -> String {
let public_api = check
.and_then(|c| c.public_api_keys.clone())
.unwrap_or_default();
let snapshot = snapshot_from_results(check, dupes, health, public_api);
let mut bytes: Vec<u8> = Vec::new();
for set in [
&snapshot.dead_code,
&snapshot.health,
&snapshot.dupes,
&snapshot.boundary_edges,
&snapshot.cycles,
&snapshot.public_api,
] {
for key in sorted_keys(set) {
bytes.extend_from_slice(key.as_bytes());
bytes.push(0);
}
bytes.push(1);
}
let mut anchor_ids: Vec<&str> = change_anchors
.iter()
.map(|a| a.change_anchor.as_str())
.collect();
anchor_ids.sort_unstable();
for id in anchor_ids {
bytes.extend_from_slice(id.as_bytes());
bytes.push(0);
}
bytes.push(1);
bytes.extend_from_slice(base_ref.as_bytes());
bytes.push(0);
bytes.extend_from_slice(head_sha.unwrap_or("").as_bytes());
format!("graph:{:016x}", xxh3_64(&bytes))
}
#[derive(Default)]
struct BriefDiffEvidence {
change_anchors: Vec<crate::audit_walkthrough::ChangeAnchor>,
diff_index: Option<fallow_output::DiffIndex>,
}
fn compute_brief_diff_evidence(
root: &std::path::Path,
base_ref: &str,
walkthrough_file: Option<&std::path::Path>,
) -> BriefDiffEvidence {
let excluded_file = walkthrough_file_relative_to_root(root, walkthrough_file);
if let (Some(raw), Some(index)) = (
crate::report::ci::diff_filter::shared_diff_raw(),
crate::report::ci::diff_filter::shared_diff_index(),
) {
let mut change_anchors = crate::audit_walkthrough::parse_change_anchors(raw);
if let Some(excluded) = excluded_file.as_deref() {
change_anchors.retain(|anchor| anchor.file != excluded);
}
return BriefDiffEvidence {
change_anchors,
diff_index: Some(index.clone()),
};
}
let Ok(diff) = fallow_engine::changed_files::try_get_changed_diff(root, base_ref) else {
return BriefDiffEvidence::default();
};
let mut change_anchors = crate::audit_walkthrough::parse_change_anchors(&diff);
if let Some(excluded) = excluded_file.as_deref() {
change_anchors.retain(|anchor| anchor.file != excluded);
}
BriefDiffEvidence {
change_anchors,
diff_index: Some(fallow_output::DiffIndex::from_unified_diff(&diff)),
}
}
fn walkthrough_file_relative_to_root(
root: &Path,
walkthrough_file: Option<&Path>,
) -> Option<String> {
let root = dunce::canonicalize(root).ok()?;
let file = dunce::canonicalize(walkthrough_file?).ok()?;
let relative = file.strip_prefix(root).ok()?;
Some(relative.to_string_lossy().replace('\\', "/"))
}
fn compute_decision_surface(
opts: &AuditOptions<'_>,
check: Option<&CheckResult>,
review_deltas: Option<&crate::audit_brief::ReviewDeltas>,
routing: Option<&routing::RoutingFacts>,
) -> crate::audit_decision_surface::DecisionSurface {
use crate::audit_decision_surface::{
CoordinationAnchor, DecisionInputs, extract_decision_surface,
};
let (Some(check), Some(deltas)) = (check, review_deltas) else {
return crate::audit_decision_surface::DecisionSurface::default();
};
let root = &check.config.root;
let boundary_anchors = decision_boundary_anchors(check, deltas, root);
let closure = check.impact_closure.as_ref();
let mut coordination: Vec<CoordinationAnchor> = closure
.map(|c| aggregate_coordination_gaps(&c.coordination_gap))
.unwrap_or_default();
let affected_not_shown = closure.map_or(0, |c| c.affected_not_shown.len() as u64);
let empty_routing = routing::RoutingFacts::default();
let routing = routing.unwrap_or(&empty_routing);
let root_owned = root.clone();
let head_source = move |rel: &str| std::fs::read_to_string(root_owned.join(rel)).ok();
for anchor in &mut coordination {
anchor.line = resolve_export_line(
check.export_lines.as_ref(),
&anchor.changed_file,
&anchor.consumed_symbols,
);
}
let public_api_anchor_line = deltas.public_api_added.first().map_or(0, |key| {
let mut parts = key.splitn(2, "::");
let path = parts.next().unwrap_or_default();
let name = parts.next().unwrap_or_default();
resolve_export_line(check.export_lines.as_ref(), path, &[name.to_string()])
});
let rename_old_path = |rel: &str| -> Option<String> {
crate::report::ci::diff_filter::shared_diff_index()
.and_then(|idx| idx.old_path_for_root_relative(rel))
.map(std::borrow::Cow::into_owned)
};
let internal_consumers_map = check.internal_consumers.as_ref();
let internal_consumers = |rel: &str| -> u64 {
internal_consumers_map
.and_then(|map| map.get(rel))
.copied()
.unwrap_or(0)
};
extract_decision_surface(&DecisionInputs {
deltas,
boundary_anchors: &boundary_anchors,
coordination: &coordination,
public_api_anchor_line,
affected_not_shown,
routing,
head_source: &head_source,
rename_old_path: &rename_old_path,
internal_consumers: &internal_consumers,
cap: opts.max_decisions,
})
}
fn decision_boundary_anchors(
check: &CheckResult,
deltas: &crate::audit_brief::ReviewDeltas,
root: &std::path::Path,
) -> Vec<crate::audit_decision_surface::BoundaryAnchor> {
use crate::audit_decision_surface::BoundaryAnchor;
let mut boundary_anchors: Vec<BoundaryAnchor> = Vec::new();
let mut seen_pairs: FxHashSet<String> = FxHashSet::default();
for finding in &check.results.boundary_violations {
let key = review_deltas::boundary_edge_key(finding);
if !deltas.boundary_introduced.contains(&key) || !seen_pairs.insert(key.clone()) {
continue;
}
boundary_anchors.push(BoundaryAnchor {
zone_pair_key: key,
from_file: keys::relative_key_path(&finding.violation.from_path, root),
from_zone: finding.violation.from_zone.clone(),
to_zone: finding.violation.to_zone.clone(),
line: finding.violation.line,
});
}
boundary_anchors
}
fn resolve_export_line(
export_lines: Option<&FxHashMap<String, Vec<(String, u32)>>>,
rel: &str,
symbols: &[String],
) -> u32 {
let Some(exports) = export_lines.and_then(|map| map.get(rel)) else {
return 0;
};
exports
.iter()
.find(|(name, _)| symbols.iter().any(|s| name == s))
.or_else(|| exports.first())
.map_or(0, |(_, line)| *line)
}
fn aggregate_coordination_gaps(
gaps: &[fallow_engine::module_graph::CoordinationGapPaths],
) -> Vec<crate::audit_decision_surface::CoordinationAnchor> {
use crate::audit_decision_surface::CoordinationAnchor;
let mut by_file: FxHashMap<String, (u64, FxHashSet<String>)> = FxHashMap::default();
for gap in gaps {
let entry = by_file
.entry(gap.changed_file.clone())
.or_insert_with(|| (0, FxHashSet::default()));
entry.0 += 1;
for symbol in &gap.consumed_symbols {
entry.1.insert(symbol.clone());
}
}
let mut anchors: Vec<CoordinationAnchor> = by_file
.into_iter()
.map(|(changed_file, (consumer_count, symbols))| {
let mut consumed_symbols: Vec<String> = symbols.into_iter().collect();
consumed_symbols.sort_unstable();
CoordinationAnchor {
changed_file,
consumed_symbols,
consumer_count,
line: 0,
}
})
.collect();
anchors.sort_by(|a, b| a.changed_file.cmp(&b.changed_file));
anchors
}
fn compute_brief_e3_data(
opts: &AuditOptions<'_>,
check: Option<&CheckResult>,
base_snapshot: Option<&AuditKeySnapshot>,
changed_files: &FxHashSet<PathBuf>,
base_ref: &str,
) -> (
Option<crate::audit_brief::ReviewDeltas>,
Vec<weakening::WeakeningSignal>,
Option<routing::RoutingFacts>,
) {
let deltas = check.zip(base_snapshot).map(|(check, base)| {
let head_boundary = review_deltas::boundary_edge_keys(&check.results.boundary_violations);
let head_cycles =
review_deltas::cycle_keys(&check.results.circular_dependencies, &check.config.root);
let head_public_api = check.public_api_keys.clone().unwrap_or_default();
crate::audit_brief::build_review_deltas(
&head_boundary,
&base.boundary_edges,
&head_cycles,
&base.cycles,
&head_public_api,
&base.public_api,
)
});
let weakening_signals = compute_weakening_signals(opts.root, base_ref, changed_files);
let routing =
check.map(|check| routing::compute_routing(opts.root, &check.config, changed_files));
(deltas, weakening_signals, routing)
}
fn compute_weakening_signals(
root: &Path,
base_ref: &str,
changed_files: &FxHashSet<PathBuf>,
) -> Vec<weakening::WeakeningSignal> {
let Some(git_root) = git_toplevel(root) else {
return Vec::new();
};
let Some(mut reader) = BaseFileReader::spawn(root) else {
return Vec::new();
};
let mut signals = Vec::new();
let mut files: Vec<&PathBuf> = changed_files.iter().collect();
files.sort();
for abs in files {
let Ok(relative) = abs.strip_prefix(&git_root) else {
continue;
};
let rel_str = relative.to_string_lossy().replace('\\', "/");
let head = std::fs::read_to_string(abs).unwrap_or_default();
let base = reader.read(base_ref, relative).unwrap_or_default();
signals.extend(weakening_signals_for_file(&rel_str, &base, &head));
}
signals
}
fn weakening_signals_for_file(
rel_str: &str,
base: &str,
head: &str,
) -> Vec<weakening::WeakeningSignal> {
use weakening::WeakeningKind;
let mut signals = Vec::new();
if weakening::is_test_file(rel_str) {
extend_weakening_signals(
&mut signals,
WeakeningKind::TestWeakened,
rel_str,
weakening::detect_test_weakening(base, head)
.into_iter()
.map(|token| format!("{token} added")),
);
extend_weakening_signals(
&mut signals,
WeakeningKind::TestWeakened,
rel_str,
weakening::detect_removed_tests(base, head),
);
}
extend_weakening_signals(
&mut signals,
WeakeningKind::SuppressionAdded,
rel_str,
weakening::detect_added_suppressions(base, head),
);
extend_weakening_signals(
&mut signals,
WeakeningKind::ThresholdLowered,
rel_str,
weakening::detect_lowered_thresholds(base, head),
);
if weakening::is_ci_file(rel_str) {
extend_weakening_signals(
&mut signals,
WeakeningKind::SecurityCheckRemoved,
rel_str,
weakening::detect_removed_security_steps(base, head),
);
}
signals
}
fn extend_weakening_signals(
signals: &mut Vec<weakening::WeakeningSignal>,
kind: weakening::WeakeningKind,
file: &str,
evidences: impl IntoIterator<Item = String>,
) {
signals.extend(
evidences
.into_iter()
.map(|evidence| weakening::WeakeningSignal {
kind,
file: file.to_owned(),
evidence,
}),
);
}
fn compute_audit_outcome(
gate: AuditGate,
check: Option<&CheckResult>,
dupes: Option<&DupesResult>,
health: Option<&HealthResult>,
base: Option<&AuditKeySnapshot>,
) -> (AuditAttribution, AuditVerdict, AuditSummary) {
let attribution = compute_audit_attribution(check, dupes, health, base, gate);
let verdict = compute_audit_verdict(gate, check, dupes, health, base);
let summary = build_summary(check, dupes, health);
crate::telemetry::note_final_result_count(
summary.dead_code_issues + summary.complexity_findings + summary.duplication_clone_groups,
);
(attribution, verdict, summary)
}
#[derive(Clone, Copy)]
struct CurrentAnalysisRefs<'a> {
check: Option<&'a CheckResult>,
dupes: Option<&'a DupesResult>,
health: Option<&'a HealthResult>,
}
fn resolve_base_snapshot(
opts: &AuditOptions<'_>,
cached_base_snapshot: Option<AuditKeySnapshot>,
base_res: Option<Result<AuditKeySnapshot, ExitCode>>,
base_cache_key: Option<&AuditBaseSnapshotCacheKey>,
current: CurrentAnalysisRefs<'_>,
) -> Result<(Option<AuditKeySnapshot>, bool), ExitCode> {
if !matches!(opts.gate, AuditGate::NewOnly) {
return Ok((None, false));
}
if let Some(snapshot) = cached_base_snapshot {
return Ok((Some(snapshot), false));
}
if let Some(base_res) = base_res {
let snapshot = base_res?;
if let Some(key) = base_cache_key {
save_cached_base_snapshot(opts, key, &snapshot);
}
return Ok((Some(snapshot), false));
}
let CurrentAnalysisRefs {
check,
dupes,
health,
} = current;
Ok((Some(current_keys_as_base_keys(check, dupes, health)), true))
}
fn compute_audit_verdict(
gate: AuditGate,
check: Option<&CheckResult>,
dupes: Option<&DupesResult>,
health: Option<&HealthResult>,
base: Option<&AuditKeySnapshot>,
) -> AuditVerdict {
if matches!(gate, AuditGate::NewOnly) {
compute_introduced_verdict(check, dupes, health, base)
} else {
compute_verdict(check, dupes, health)
}
}
fn build_audit_result(parts: AuditResultParts) -> AuditResult {
AuditResult {
verdict: parts.verdict,
summary: parts.summary,
attribution: parts.attribution,
base_snapshot: parts.base_snapshot,
base_snapshot_skipped: parts.base_snapshot_skipped,
changed_files_count: parts.changed_files_count,
changed_files: parts.changed_files.into_iter().collect(),
base_ref: parts.base_ref,
base_description: parts.base_description,
head_sha: parts.head_sha,
output: parts.output,
performance: parts.performance,
check: parts.check,
dupes: parts.dupes,
health: parts.health,
elapsed: parts.elapsed,
review_deltas: parts.review_deltas,
weakening_signals: parts.weakening_signals,
routing: parts.routing,
decision_surface: parts.decision_surface,
graph_snapshot_hash: parts.graph_snapshot_hash,
change_anchors: parts.change_anchors,
diff_index: parts.diff_index,
}
}
fn empty_audit_result(
base_ref: String,
base_description: Option<String>,
opts: &AuditOptions<'_>,
elapsed: Duration,
) -> AuditResult {
crate::telemetry::note_final_result_count(0);
let head_sha = get_head_sha(opts.root);
let graph_snapshot_hash = if opts.brief {
Some(compute_graph_snapshot_hash(
None,
None,
None,
&base_ref,
head_sha.as_deref(),
&[],
))
} else {
None
};
AuditResult {
verdict: AuditVerdict::Pass,
summary: AuditSummary {
dead_code_issues: 0,
dead_code_has_errors: false,
complexity_findings: 0,
max_cyclomatic: None,
duplication_clone_groups: 0,
},
attribution: AuditAttribution {
gate: opts.gate,
..AuditAttribution::default()
},
base_snapshot: None,
base_snapshot_skipped: false,
changed_files_count: 0,
changed_files: Vec::new(),
base_ref,
base_description,
head_sha,
output: opts.output,
performance: opts.performance,
check: None,
dupes: None,
health: None,
elapsed,
review_deltas: None,
weakening_signals: Vec::new(),
routing: None,
decision_surface: None,
graph_snapshot_hash,
change_anchors: Vec::new(),
diff_index: None,
}
}
fn run_audit_check<'a>(
opts: &'a AuditOptions<'a>,
changed_since: Option<&'a str>,
changed_files: &FxHashSet<PathBuf>,
retain_modules_for_health: bool,
) -> Result<Option<CheckResult>, ExitCode> {
let filters = IssueFilters::default();
let retain_modules_for_health = retain_modules_for_health || opts.brief;
let trace_opts = TraceOptions {
trace_export: None,
trace_file: None,
trace_dependency: None,
impact_closure: None,
performance: opts.performance,
};
match crate::check::execute_check(&CheckOptions {
root: opts.root,
config_path: opts.config_path,
output: opts.output,
no_cache: opts.no_cache,
threads: opts.threads,
quiet: opts.quiet,
allow_remote_extends: opts.allow_remote_extends,
fail_on_issues: false,
filters: &filters,
changed_since,
diff_index: None,
use_shared_diff_index: true,
baseline: opts.dead_code_baseline,
save_baseline: None,
sarif_file: None,
production: opts.production_dead_code.unwrap_or(opts.production),
production_override: opts.production_dead_code,
workspace: opts.workspace,
changed_workspaces: opts.changed_workspaces,
group_by: opts.group_by,
include_dupes: false,
trace_opts: &trace_opts,
explain: opts.explain,
top: None,
file: &[],
include_entry_exports: opts.include_entry_exports,
summary: false,
regression_opts: crate::regression::RegressionOpts {
fail_on_regression: false,
tolerance: crate::regression::Tolerance::Absolute(0),
regression_baseline_file: None,
save_target: crate::regression::SaveRegressionTarget::None,
scoped: true,
quiet: opts.quiet,
output: opts.output,
},
retain_modules_for_health,
defer_performance: false,
}) {
Ok(mut result) => {
fallow_engine::changed_files::filter_results_by_changed_files(
&mut result.results,
changed_files,
);
Ok(Some(result))
}
Err(code) => Err(code),
}
}
fn run_audit_dupes<'a>(
opts: &'a AuditOptions<'a>,
changed_since: Option<&'a str>,
changed_files: Option<&'a FxHashSet<PathBuf>>,
pre_discovered: Option<Vec<fallow_types::discover::DiscoveredFile>>,
) -> Result<Option<DupesResult>, ExitCode> {
let dupes_cfg = match crate::load_config_for_analysis(
opts.root,
opts.config_path,
crate::ConfigLoadOptions {
output: opts.output,
no_cache: opts.no_cache,
threads: opts.threads,
production_override: opts
.production_dupes
.or_else(|| opts.production.then_some(true)),
quiet: opts.quiet,
allow_remote_extends: opts.allow_remote_extends,
},
fallow_config::ProductionAnalysis::Dupes,
) {
Ok(c) => c.duplicates,
Err(code) => return Err(code),
};
let dupes_opts = build_audit_dupes_options(opts, changed_since, changed_files, &dupes_cfg);
let dupes_run = if let Some(files) = pre_discovered {
crate::dupes::execute_dupes_with_files(&dupes_opts, files)
} else {
crate::dupes::execute_dupes(&dupes_opts)
};
match dupes_run {
Ok(r) => Ok(Some(r)),
Err(code) => Err(code),
}
}
fn build_audit_dupes_options<'a>(
opts: &'a AuditOptions<'a>,
changed_since: Option<&'a str>,
changed_files: Option<&'a FxHashSet<PathBuf>>,
dupes_cfg: &fallow_config::DuplicatesConfig,
) -> DupesOptions<'a> {
DupesOptions {
root: opts.root,
config_path: opts.config_path,
output: opts.output,
no_cache: opts.no_cache,
threads: opts.threads,
quiet: opts.quiet,
allow_remote_extends: opts.allow_remote_extends,
mode: Some(DupesMode::from(dupes_cfg.mode)),
min_tokens: Some(dupes_cfg.min_tokens),
min_lines: Some(dupes_cfg.min_lines),
min_occurrences: Some(dupes_cfg.min_occurrences),
threshold: Some(dupes_cfg.threshold),
skip_local: dupes_cfg.skip_local,
cross_language: dupes_cfg.cross_language,
ignore_imports: Some(dupes_cfg.ignore_imports),
top: None,
baseline_path: opts.dupes_baseline,
save_baseline_path: None,
production: opts.production_dupes.unwrap_or(opts.production),
production_override: opts.production_dupes,
trace: None,
changed_since,
diff_index: None,
use_shared_diff_index: true,
changed_files,
workspace: opts.workspace,
changed_workspaces: opts.changed_workspaces,
explain: opts.explain,
explain_skipped: opts.explain_skipped,
summary: false,
group_by: opts.group_by,
performance: false,
}
}
fn run_audit_health<'a>(
opts: &'a AuditOptions<'a>,
changed_since: Option<&'a str>,
shared_parse: Option<fallow_engine::health::HealthSharedParseData>,
) -> Result<Option<HealthResult>, ExitCode> {
let runtime_coverage = match opts.runtime_coverage {
Some(path) => match crate::health::coverage::prepare_options(
path,
opts.min_invocations_hot,
None,
None,
opts.output,
) {
Ok(options) => Some(options),
Err(code) => return Err(code),
},
None => None,
};
let health_opts = build_audit_health_options(opts, changed_since, runtime_coverage);
let health_run = if let Some(shared) = shared_parse {
crate::health::execute_health_with_shared_parse(&health_opts, shared)
} else {
crate::health::execute_health(&health_opts)
};
match health_run {
Ok(r) => Ok(Some(r)),
Err(code) => Err(code),
}
}
fn build_audit_health_options<'a>(
opts: &'a AuditOptions<'a>,
changed_since: Option<&'a str>,
runtime_coverage: Option<fallow_engine::health::RuntimeCoverageOptions>,
) -> HealthOptions<'a> {
HealthOptions {
root: opts.root,
config_path: opts.config_path,
output: opts.output,
no_cache: opts.no_cache,
threads: opts.threads,
quiet: opts.quiet,
thresholds: fallow_engine::health::HealthThresholdOverrides {
max_cyclomatic: None,
max_cognitive: None,
max_crap: opts.max_crap,
},
top: None,
sort: fallow_engine::health::HealthSort::Cyclomatic,
production: opts.production_health.unwrap_or(opts.production),
production_override: opts.production_health,
allow_remote_extends: opts.allow_remote_extends,
changed_since,
diff_index: None,
use_shared_diff_index: true,
workspace: opts.workspace,
changed_workspaces: opts.changed_workspaces,
baseline: opts.health_baseline,
save_baseline: None,
complexity: true,
file_scores: false,
coverage_gaps: false,
config_activates_coverage_gaps: false,
hotspots: false,
ownership: false,
ownership_emails: None,
targets: false,
css: opts.css,
css_deep: opts.css_deep,
force_full: false,
score_only_output: false,
enforce_coverage_gap_gate: false,
effort: None,
score: false,
gates: fallow_engine::health::HealthGateOptions::default(),
since: None,
min_commits: None,
explain: opts.explain,
summary: false,
save_snapshot: None,
trend: false,
coverage_inputs: fallow_engine::health::HealthCoverageInputs {
coverage: opts.coverage,
coverage_root: opts.coverage_root,
},
performance: opts.performance,
runtime_coverage,
churn_file: None,
complexity_breakdown: false,
group_by: opts.group_by.map(Into::into),
}
}
#[path = "audit_output.rs"]
mod output;
pub use output::audit_json_header_input;
pub use output::{
insert_audit_dead_code_json, insert_audit_duplication_json, insert_audit_health_json,
print_audit_findings, print_audit_result,
};
pub fn run_audit(opts: &AuditOptions<'_>, gate_marker: Option<&str>) -> ExitCode {
if let Err(e) = fallow_engine::health::validate_coverage_root_absolute(opts.coverage_root) {
return emit_error(&e, 2, opts.output);
}
let coverage_resolved = opts
.coverage
.map(|p| crate::health::scoring::resolve_relative_to_root(p, Some(opts.root)));
let runtime_coverage_resolved = opts
.runtime_coverage
.map(|p| crate::health::scoring::resolve_relative_to_root(p, Some(opts.root)));
let resolved_opts = AuditOptions {
coverage: coverage_resolved.as_deref(),
runtime_coverage: runtime_coverage_resolved.as_deref(),
..*opts
};
match execute_audit(&resolved_opts) {
Ok(result) => {
record_audit_impact(opts, gate_marker, &result);
print_audit_command_result(opts, &result)
}
Err(code) => code,
}
}
fn record_audit_impact(opts: &AuditOptions<'_>, gate_marker: Option<&str>, result: &AuditResult) {
let mut findings = result
.check
.as_ref()
.map(|c| crate::impact::collect_dead_code_findings(&c.results))
.unwrap_or_default();
if let Some(health) = result.health.as_ref() {
findings.extend(crate::impact::collect_complexity_findings(&health.report));
}
let clones = result
.dupes
.as_ref()
.map(|d| crate::impact::collect_clone_findings(&d.report))
.unwrap_or_default();
let empty_supps: Vec<fallow_types::results::ActiveSuppression> = Vec::new();
let suppressions = result.check.as_ref().map_or(empty_supps.as_slice(), |c| {
c.results.active_suppressions.as_slice()
});
let attribution = crate::impact::AttributionInput {
root: opts.root,
scope: crate::impact::Scope::ChangedFiles(&result.changed_files),
findings,
clones,
suppressions,
};
crate::impact::record_audit_run(
opts.root,
&result.summary,
&crate::impact::AuditRunRecord {
verdict: result.verdict,
gate: gate_marker.is_some(),
git_sha: result.head_sha.as_deref(),
version: env!("CARGO_PKG_VERSION"),
timestamp: &crate::vital_signs::chrono_timestamp(),
attribution: Some(&attribution),
},
);
}
fn print_audit_command_result(opts: &AuditOptions<'_>, result: &AuditResult) -> ExitCode {
if opts.walkthrough_guide {
return crate::audit_brief::print_walkthrough_guide_result(result);
}
if opts.walkthrough {
return crate::audit_brief::print_walkthrough_human_result(
result,
opts.root,
opts.cache_dir,
opts.mark_viewed,
opts.show_cleared,
opts.quiet,
);
}
if let Some(path) = opts.walkthrough_file {
return crate::audit_brief::print_walkthrough_file_result(result, path);
}
if opts.brief {
return crate::audit_brief::print_brief_result(
result,
result.diff_index.as_ref(),
opts.quiet,
opts.explain,
opts.show_deprioritized,
);
}
print_audit_result(result, opts.quiet, opts.explain)
}
#[must_use]
pub fn run_decision_surface(opts: &AuditOptions<'_>) -> ExitCode {
let brief_opts = AuditOptions {
brief: true,
..*opts
};
match execute_audit(&brief_opts) {
Ok(result) => crate::audit_brief::print_decision_surface_result(&result, opts.quiet),
Err(code) => code,
}
}
#[cfg(test)]
#[path = "audit_tests.rs"]
mod tests;