use std::path::Path;
use fallow_engine::changed_files::RenamedFile;
use rustc_hash::{FxHashMap, FxHashSet};
use super::{AuditAnalysesView, DeadCodeView};
use crate::AuditProgrammaticKeySnapshot;
use crate::audit_keys::{
ComplexityBaseline, complexity_baseline, complexity_baseline_keys, dead_code_audit_ledger,
dead_code_keys, relative_key_path, remap_complexity_baseline_for_renames,
remap_keys_for_renames, styling_keys,
};
use crate::review_deltas::{boundary_edge_keys, cycle_keys};
#[derive(Debug, Clone, Default)]
pub struct AuditKeySnapshot {
pub type_aware_identity: Option<fallow_types::semantic::SemanticAnalysisIdentity>,
pub type_aware_gap_signature: Vec<String>,
pub syntactic_dead_code: Option<FxHashSet<String>>,
pub dead_code: FxHashSet<String>,
pub unprovable_dead_code: FxHashSet<String>,
pub health: ComplexityBaseline,
pub styling: FxHashSet<String>,
pub dupes: FxHashSet<String>,
pub dupe_groups: Vec<crate::diff_risk::clones::BaseCloneGroup>,
pub boundary_edges: FxHashSet<String>,
pub cycles: FxHashSet<String>,
pub public_api: FxHashSet<String>,
pub branching: FxHashMap<String, fallow_types::extract::FileBranching>,
pub inherited_health: Vec<ComplexityBaseline>,
}
impl AuditKeySnapshot {
#[must_use]
pub fn from_view(view: &AuditAnalysesView<'_>) -> Self {
let mut snapshot = Self::default();
if let Some(dead_code) = view.dead_code.as_ref() {
snapshot.type_aware_identity =
dead_code.type_aware.and_then(|meta| meta.identity.clone());
snapshot.type_aware_gap_signature = dead_code
.type_aware
.map_or_else(Vec::new, type_aware_gap_signature);
snapshot.syntactic_dead_code = dead_code.syntactic_keys.cloned();
snapshot.dead_code = dead_code_keys(dead_code.results, dead_code.root);
snapshot.unprovable_dead_code =
fallow_engine::unprovable::unprovable_dead_code_collections(dead_code.config)
.into_iter()
.map(str::to_owned)
.collect();
snapshot.boundary_edges = boundary_edge_keys(&dead_code.results.boundary_violations);
snapshot.cycles = cycle_keys(&dead_code.results.circular_dependencies, dead_code.root);
snapshot.public_api = dead_code.public_api.cloned().unwrap_or_default();
}
if let Some(health) = view.health.as_ref() {
snapshot.health = complexity_baseline(health.report, health.root);
snapshot.styling = styling_keys(health.report, health.root);
snapshot.branching = health.branching.map_or_else(FxHashMap::default, |by_file| {
branching_keys(by_file, health.root)
});
}
if let Some(duplication) = view.duplication.as_ref() {
snapshot.dupes = duplication
.clone_groups
.iter()
.map(|group| crate::audit_keys::dupe_group_key(group, duplication.root))
.collect();
snapshot.dupe_groups = duplication
.clone_groups
.iter()
.map(|group| crate::diff_risk::clones::BaseCloneGroup {
instances: crate::diff_risk::clone_spans(group, duplication.root),
})
.collect();
}
snapshot
}
pub fn absorb_inherited(&mut self, inherited: Self) {
let inherited_syntactic = inherited
.syntactic_dead_code
.unwrap_or_else(|| inherited.dead_code.clone());
if let Some(syntactic) = self.syntactic_dead_code.as_mut() {
syntactic.extend(inherited_syntactic);
}
if self.type_aware_identity == inherited.type_aware_identity
&& self.type_aware_gap_signature == inherited.type_aware_gap_signature
{
self.dead_code.extend(inherited.dead_code);
}
self.unprovable_dead_code
.extend(inherited.unprovable_dead_code);
self.styling.extend(inherited.styling);
self.dupes.extend(inherited.dupes);
self.boundary_edges.extend(inherited.boundary_edges);
self.cycles.extend(inherited.cycles);
self.public_api.extend(inherited.public_api);
self.inherited_health.push(inherited.health);
self.inherited_health.extend(inherited.inherited_health);
}
pub fn inherit_unprovable_dead_code(&mut self, head: &DeadCodeView<'_>) {
if self.unprovable_dead_code.is_empty() {
return;
}
let ledger = dead_code_audit_ledger(head.results, head.root, head.config, None);
let keys: Vec<String> = ledger
.records()
.iter()
.filter(|record| self.unprovable_dead_code.contains(record.collection))
.map(|record| record.stable_key.clone())
.collect();
if let Some(syntactic) = self.syntactic_dead_code.as_mut() {
syntactic.extend(keys.iter().cloned());
}
self.dead_code.extend(keys);
}
pub fn remap_for_renames(&mut self, renames: &[RenamedFile], root: &Path) {
let rename_map: FxHashMap<String, String> = renames
.iter()
.filter_map(|rename| {
let from = relative_key_path(&rename.from, root);
let to = relative_key_path(&rename.to, root);
(from != to).then_some((from, to))
})
.collect();
if rename_map.is_empty() {
return;
}
self.dead_code = remap_keys_for_renames(&self.dead_code, &rename_map);
self.health =
remap_complexity_baseline_for_renames(std::mem::take(&mut self.health), &rename_map);
self.styling = remap_keys_for_renames(&self.styling, &rename_map);
self.dupes = remap_keys_for_renames(&self.dupes, &rename_map);
self.cycles = remap_keys_for_renames(&self.cycles, &rename_map);
self.public_api = remap_keys_for_renames(&self.public_api, &rename_map);
self.branching = self
.branching
.drain()
.map(|(path, totals)| match rename_map.get(&path) {
Some(renamed) => (renamed.clone(), totals),
None => (path, totals),
})
.collect();
}
#[must_use]
pub fn to_programmatic(&self) -> AuditProgrammaticKeySnapshot {
let mut health = complexity_baseline_keys(&self.health);
health.extend(self.styling.iter().cloned());
AuditProgrammaticKeySnapshot {
dead_code: self.dead_code.clone(),
health,
dupes: self.dupes.clone(),
}
}
}
#[must_use]
pub fn branching_keys(
by_file: &fallow_engine::health::BranchingByFile,
root: &Path,
) -> FxHashMap<String, fallow_types::extract::FileBranching> {
by_file
.iter()
.map(|(path, totals)| (relative_key_path(path, root), *totals))
.collect()
}
#[must_use]
pub fn type_aware_attribution_degrade_reason(
base: Option<&AuditKeySnapshot>,
head: Option<&fallow_types::envelope::TypeAwareMeta>,
) -> Option<&'static str> {
let base = base?;
let base_identity = base.type_aware_identity.as_ref();
let head_identity = head.and_then(|meta| meta.identity.as_ref());
if let (Some(base_identity), Some(head_identity)) = (base_identity, head_identity)
&& !base_identity.incompatible_fields(head_identity).is_empty()
{
return Some("their semantic analysis identities are incompatible");
}
if let Some(head) = head
&& base.type_aware_gap_signature != type_aware_gap_signature(head)
{
return Some("their incomplete semantic query reasons or omissions differ");
}
None
}
#[must_use]
pub fn type_aware_degrade_warning(reason: &str) -> String {
format!(
"audit compared base and head with syntactic attribution because {reason} \
(usually a tsconfig or compiler-options change between base and head); \
type-aware refinement still applies to head findings, and \
semantic-only findings stay out of the new-only gate for this run; set \
audit.typeAware: false or pass --no-type-aware to keep the gate syntactic"
)
}
#[must_use]
pub fn type_aware_gap_signature(meta: &fallow_types::envelope::TypeAwareMeta) -> Vec<String> {
let mut signature = meta
.queries
.iter()
.filter(|query| query.status != fallow_types::semantic::SemanticCompleteness::Complete)
.map(|query| {
let mut omissions = query
.omissions
.iter()
.map(|omission| format!("{:?}:{}", omission.reason_code, omission.count))
.collect::<Vec<_>>();
omissions.sort();
format!(
"{:?}:{:?}:{}",
query.capability,
query.reason_code,
omissions.join(",")
)
})
.collect::<Vec<_>>();
signature.sort();
signature
}