mod base_files;
mod base_ref;
mod outcome;
mod scope;
mod snapshot;
#[cfg(test)]
mod tests;
use std::path::{Path, PathBuf};
use fallow_config::{AuditGate, ResolvedConfig, RulesConfig};
use fallow_engine::changed_files::RenamedFile;
use fallow_engine::repo_refs::{BaseAnalysisRoot, resolve_base_analysis_root};
use fallow_output::HealthReport;
use fallow_types::duplicates::CloneGroup;
use fallow_types::results::AnalysisResults;
use rustc_hash::FxHashSet;
pub use base_files::{BaseFileReader, BaseRead, can_reuse_current_as_base};
pub use base_ref::{
AuditBaseError, AuditBaseOrigin, parse_audit_base_override, resolve_audit_base,
};
pub use outcome::{
DupeDemotionDiffSource, SharedDiff, compare, demote_preexisting_dupe_introductions, outcome,
styling_finding_gates, styling_rule_severity,
};
pub use scope::{
BaseCoverageInputs, base_coverage_inputs, base_focus_files, remap_focus_files, renamed_files,
scope_dependency_findings,
};
pub use snapshot::{
AuditKeySnapshot, branching_keys, type_aware_attribution_degrade_reason,
type_aware_degrade_warning, type_aware_gap_signature,
};
use crate::audit_keys::AuditComparison;
use crate::{AuditAttribution, AuditSummary, AuditVerdict};
pub struct DeadCodeView<'a> {
pub results: &'a AnalysisResults,
pub config: &'a ResolvedConfig,
pub root: &'a Path,
pub type_aware: Option<&'a fallow_types::envelope::TypeAwareMeta>,
pub syntactic_keys: Option<&'a FxHashSet<String>>,
pub public_api: Option<&'a FxHashSet<String>>,
}
pub struct DuplicationView<'a> {
pub clone_groups: Vec<&'a CloneGroup>,
pub root: &'a Path,
pub duplication_percentage: f64,
pub threshold: f64,
}
pub struct HealthView<'a> {
pub report: &'a HealthReport,
pub root: &'a Path,
pub rules: &'a RulesConfig,
pub branching: Option<&'a fallow_engine::health::BranchingByFile>,
}
#[derive(Default)]
pub struct AuditAnalysesView<'a> {
pub dead_code: Option<DeadCodeView<'a>>,
pub duplication: Option<DuplicationView<'a>>,
pub health: Option<HealthView<'a>>,
}
pub trait AuditAnalyses {
fn view(&self) -> AuditAnalysesView<'_>;
fn dead_code_results_mut(&mut self) -> Option<&mut AnalysisResults>;
fn health_report_mut(&mut self) -> Option<&mut HealthReport>;
fn record_type_aware_warning(&mut self, warning: &str);
}
pub trait BaseCheckout {
fn path(&self) -> &Path;
}
impl BaseCheckout for fallow_engine::repo_refs::TemporaryBaseWorktree {
fn path(&self) -> &Path {
Self::path(self)
}
}
pub trait AuditBackend: Sync {
type Analyses: AuditAnalyses + Send;
type Checkout: BaseCheckout;
type CacheKey: Sync;
type Error: Send;
fn prepare(&self) {}
fn run_head(&self, changed_files: &FxHashSet<PathBuf>) -> Result<Self::Analyses, Self::Error>;
fn create_base_checkout(
&self,
base_ref: &str,
base_sha: Option<&str>,
) -> Result<Self::Checkout, Self::Error>;
fn run_base(
&self,
base_root: &Path,
focus: Option<&FxHashSet<PathBuf>>,
) -> Result<Self::Analyses, Self::Error>;
fn base_cache_key(
&self,
_base_ref: &str,
_focus: &FxHashSet<PathBuf>,
) -> Result<Option<Self::CacheKey>, Self::Error> {
Ok(None)
}
fn cached_base_sha<'k>(&self, _key: &'k Self::CacheKey) -> Option<&'k str> {
None
}
fn load_cached_base(&self, _key: &Self::CacheKey) -> Option<AuditKeySnapshot> {
None
}
fn save_cached_base(&self, _key: &Self::CacheKey, _snapshot: &AuditKeySnapshot) {}
fn shared_diff(&self) -> Option<SharedDiff<'_>> {
None
}
}
pub struct AuditRunInput<'a> {
pub root: &'a Path,
pub gate: AuditGate,
pub base_ref: &'a str,
pub cache_dir: Option<&'a Path>,
pub changed_files: FxHashSet<PathBuf>,
}
#[derive(Debug, Default)]
pub struct AuditBase {
pub snapshot: Option<AuditKeySnapshot>,
pub skipped: bool,
}
pub struct AuditAttributionInput<'a> {
pub root: &'a Path,
pub gate: AuditGate,
pub base_ref: &'a str,
pub base: AuditBase,
pub renames: &'a [RenamedFile],
pub shared_diff: Option<SharedDiff<'a>>,
}
#[derive(Debug)]
pub struct AuditOutcome {
pub verdict: AuditVerdict,
pub summary: AuditSummary,
pub attribution: AuditAttribution,
pub comparison: AuditComparison,
pub base_snapshot: Option<AuditKeySnapshot>,
pub base_snapshot_skipped: bool,
pub dupe_demotion_diff_source: Option<DupeDemotionDiffSource>,
pub type_aware_degrade_warning: Option<String>,
}
pub(crate) fn programmatic_base_snapshot(
outcome: &AuditOutcome,
) -> Option<crate::AuditProgrammaticKeySnapshot> {
outcome
.base_snapshot
.as_ref()
.map(AuditKeySnapshot::to_programmatic)
}
pub struct AuditRun<A> {
pub analyses: A,
pub changed_files: FxHashSet<PathBuf>,
pub outcome: AuditOutcome,
}
pub fn run<B: AuditBackend>(
backend: &B,
input: AuditRunInput<'_>,
) -> Result<Option<AuditRun<B::Analyses>>, B::Error> {
let AuditRunInput {
root,
gate,
base_ref,
cache_dir,
changed_files,
} = input;
if changed_files.is_empty() {
return Ok(None);
}
backend.prepare();
let needs_real_base = matches!(gate, AuditGate::NewOnly)
&& !can_reuse_current_as_base(root, cache_dir, base_ref, &changed_files);
let renames = if needs_real_base {
renamed_files(root, base_ref)
} else {
Vec::new()
};
let focus = base_focus_files(&changed_files, &renames);
let cache_key = if needs_real_base {
backend.base_cache_key(base_ref, &focus)?
} else {
None
};
let cached = cache_key
.as_ref()
.and_then(|key| backend.load_cached_base(key));
let (head, fresh_base) = if needs_real_base && cached.is_none() {
let base_sha = cache_key
.as_ref()
.and_then(|key| backend.cached_base_sha(key));
let (head, base) = rayon::join(
|| backend.run_head(&changed_files),
|| base_snapshot(backend, root, base_ref, &focus, base_sha),
);
(head, Some(base))
} else {
(backend.run_head(&changed_files), None)
};
let mut analyses = head?;
scope_dependency_findings_of(&mut analyses, &changed_files);
let base = if !matches!(gate, AuditGate::NewOnly) {
AuditBase::default()
} else if let Some(snapshot) = cached {
AuditBase {
snapshot: Some(snapshot),
skipped: false,
}
} else if let Some(fresh) = fresh_base {
let snapshot = fresh?;
if let Some(key) = cache_key.as_ref() {
backend.save_cached_base(key, &snapshot);
}
AuditBase {
snapshot: Some(snapshot),
skipped: false,
}
} else {
AuditBase {
snapshot: Some(AuditKeySnapshot::from_view(&analyses.view())),
skipped: true,
}
};
let outcome = attribute(
&mut analyses,
AuditAttributionInput {
root,
gate,
base_ref,
base,
renames: &renames,
shared_diff: backend.shared_diff(),
},
);
Ok(Some(AuditRun {
analyses,
changed_files,
outcome,
}))
}
pub fn attribute<A: AuditAnalyses>(
analyses: &mut A,
input: AuditAttributionInput<'_>,
) -> AuditOutcome {
let AuditAttributionInput {
root,
gate,
base_ref,
base,
renames,
shared_diff,
} = input;
let AuditBase {
snapshot: mut base_snapshot,
skipped,
} = base;
if !skipped && let Some(snapshot) = base_snapshot.as_mut() {
snapshot.remap_for_renames(renames, root);
}
let degrade_reason = {
let view = analyses.view();
type_aware_attribution_degrade_reason(
base_snapshot.as_ref(),
view.dead_code
.as_ref()
.and_then(|dead_code| dead_code.type_aware),
)
};
let type_aware_degrade_warning = degrade_reason.map(type_aware_degrade_warning);
if let Some(warning) = type_aware_degrade_warning.as_deref() {
analyses.record_type_aware_warning(warning);
}
let (comparison, dupe_demotion_diff_source, (attribution, verdict, summary)) = {
let view = analyses.view();
let mut comparison = compare(&view, base_snapshot.as_ref(), degrade_reason.is_some());
let source = demote_preexisting_dupe_introductions(
&mut comparison,
&view,
root,
base_ref,
shared_diff,
);
let result = outcome(gate, &view, &comparison, base_snapshot.is_some());
(comparison, source, result)
};
if base_snapshot.is_some() {
if let Some(results) = analyses.dead_code_results_mut() {
comparison.dead_code.annotate_results(results);
}
if let Some(report) = analyses.health_report_mut() {
for (finding, introduced) in report
.findings
.iter_mut()
.zip(comparison.health.introduced())
{
finding.introduced = Some(introduced);
}
}
}
AuditOutcome {
verdict,
summary,
attribution,
comparison,
base_snapshot,
base_snapshot_skipped: skipped,
dupe_demotion_diff_source,
type_aware_degrade_warning,
}
}
fn base_snapshot<B: AuditBackend>(
backend: &B,
root: &Path,
base_ref: &str,
focus: &FxHashSet<PathBuf>,
base_sha: Option<&str>,
) -> Result<AuditKeySnapshot, B::Error> {
let checkout = backend.create_base_checkout(base_ref, base_sha)?;
let base_root = match resolve_base_analysis_root(root, checkout.path()) {
BaseAnalysisRoot::Present(base_root) => {
dunce::canonicalize(&base_root).unwrap_or(base_root)
}
BaseAnalysisRoot::NewInHead(_) => return Ok(AuditKeySnapshot::default()),
};
let base_focus = remap_focus_files(focus, root, &base_root);
let mut base = backend.run_base(&base_root, base_focus.as_ref())?;
if let Some(focus) = base_focus.as_ref() {
scope_dependency_findings_of(&mut base, focus);
}
let snapshot = AuditKeySnapshot::from_view(&base.view());
drop(checkout);
Ok(snapshot)
}
fn scope_dependency_findings_of<A: AuditAnalyses>(
analyses: &mut A,
changed_files: &FxHashSet<PathBuf>,
) {
let Some(root) = analyses
.view()
.dead_code
.map(|dead_code| dead_code.root.to_path_buf())
else {
return;
};
if let Some(results) = analyses.dead_code_results_mut() {
scope_dependency_findings(results, &root, changed_files);
}
}