fallow_api/audit_run/
snapshot.rs1use std::path::Path;
5
6use fallow_engine::changed_files::RenamedFile;
7use rustc_hash::{FxHashMap, FxHashSet};
8
9use super::{AuditAnalysesView, DeadCodeView};
10use crate::AuditProgrammaticKeySnapshot;
11use crate::audit_keys::{
12 ComplexityBaseline, complexity_baseline, complexity_baseline_keys, dead_code_audit_ledger,
13 dead_code_keys, relative_key_path, remap_complexity_baseline_for_renames,
14 remap_keys_for_renames, styling_keys,
15};
16use crate::review_deltas::{boundary_edge_keys, cycle_keys};
17
18#[derive(Debug, Clone, Default)]
24pub struct AuditKeySnapshot {
25 pub type_aware_identity: Option<fallow_types::semantic::SemanticAnalysisIdentity>,
28 pub type_aware_gap_signature: Vec<String>,
31 pub syntactic_dead_code: Option<FxHashSet<String>>,
35 pub dead_code: FxHashSet<String>,
37 pub unprovable_dead_code: FxHashSet<String>,
42 pub health: ComplexityBaseline,
45 pub styling: FxHashSet<String>,
47 pub dupes: FxHashSet<String>,
49 pub dupe_groups: Vec<crate::diff_risk::clones::BaseCloneGroup>,
52 pub boundary_edges: FxHashSet<String>,
55 pub cycles: FxHashSet<String>,
57 pub public_api: FxHashSet<String>,
60 pub branching: FxHashMap<String, fallow_types::extract::FileBranching>,
64 pub inherited_health: Vec<ComplexityBaseline>,
68}
69
70impl AuditKeySnapshot {
71 #[must_use]
73 pub fn from_view(view: &AuditAnalysesView<'_>) -> Self {
74 let mut snapshot = Self::default();
75 if let Some(dead_code) = view.dead_code.as_ref() {
76 snapshot.type_aware_identity =
77 dead_code.type_aware.and_then(|meta| meta.identity.clone());
78 snapshot.type_aware_gap_signature = dead_code
79 .type_aware
80 .map_or_else(Vec::new, type_aware_gap_signature);
81 snapshot.syntactic_dead_code = dead_code.syntactic_keys.cloned();
82 snapshot.dead_code = dead_code_keys(dead_code.results, dead_code.root);
83 snapshot.unprovable_dead_code =
84 fallow_engine::unprovable::unprovable_dead_code_collections(dead_code.config)
85 .into_iter()
86 .map(str::to_owned)
87 .collect();
88 snapshot.boundary_edges = boundary_edge_keys(&dead_code.results.boundary_violations);
89 snapshot.cycles = cycle_keys(&dead_code.results.circular_dependencies, dead_code.root);
90 snapshot.public_api = dead_code.public_api.cloned().unwrap_or_default();
91 }
92 if let Some(health) = view.health.as_ref() {
93 snapshot.health = complexity_baseline(health.report, health.root);
94 snapshot.styling = styling_keys(health.report, health.root);
95 snapshot.branching = health.branching.map_or_else(FxHashMap::default, |by_file| {
96 branching_keys(by_file, health.root)
97 });
98 }
99 if let Some(duplication) = view.duplication.as_ref() {
100 snapshot.dupes = duplication
101 .clone_groups
102 .iter()
103 .map(|group| crate::audit_keys::dupe_group_key(group, duplication.root))
104 .collect();
105 snapshot.dupe_groups = duplication
106 .clone_groups
107 .iter()
108 .map(|group| crate::diff_risk::clones::BaseCloneGroup {
109 instances: crate::diff_risk::clone_spans(group, duplication.root),
110 })
111 .collect();
112 }
113 snapshot
114 }
115
116 pub fn absorb_inherited(&mut self, inherited: Self) {
127 let inherited_syntactic = inherited
128 .syntactic_dead_code
129 .unwrap_or_else(|| inherited.dead_code.clone());
130 if let Some(syntactic) = self.syntactic_dead_code.as_mut() {
131 syntactic.extend(inherited_syntactic);
132 }
133 if self.type_aware_identity == inherited.type_aware_identity
136 && self.type_aware_gap_signature == inherited.type_aware_gap_signature
137 {
138 self.dead_code.extend(inherited.dead_code);
139 }
140 self.unprovable_dead_code
141 .extend(inherited.unprovable_dead_code);
142 self.styling.extend(inherited.styling);
143 self.dupes.extend(inherited.dupes);
144 self.boundary_edges.extend(inherited.boundary_edges);
145 self.cycles.extend(inherited.cycles);
146 self.public_api.extend(inherited.public_api);
147 self.inherited_health.push(inherited.health);
148 self.inherited_health.extend(inherited.inherited_health);
149 }
150
151 pub fn inherit_unprovable_dead_code(&mut self, head: &DeadCodeView<'_>) {
161 if self.unprovable_dead_code.is_empty() {
162 return;
163 }
164 let ledger = dead_code_audit_ledger(head.results, head.root, head.config, None);
165 let keys: Vec<String> = ledger
166 .records()
167 .iter()
168 .filter(|record| self.unprovable_dead_code.contains(record.collection))
169 .map(|record| record.stable_key.clone())
170 .collect();
171 if let Some(syntactic) = self.syntactic_dead_code.as_mut() {
172 syntactic.extend(keys.iter().cloned());
173 }
174 self.dead_code.extend(keys);
175 }
176
177 pub fn remap_for_renames(&mut self, renames: &[RenamedFile], root: &Path) {
185 let rename_map: FxHashMap<String, String> = renames
186 .iter()
187 .filter_map(|rename| {
188 let from = relative_key_path(&rename.from, root);
189 let to = relative_key_path(&rename.to, root);
190 (from != to).then_some((from, to))
191 })
192 .collect();
193 if rename_map.is_empty() {
194 return;
195 }
196 self.dead_code = remap_keys_for_renames(&self.dead_code, &rename_map);
197 self.health =
198 remap_complexity_baseline_for_renames(std::mem::take(&mut self.health), &rename_map);
199 self.styling = remap_keys_for_renames(&self.styling, &rename_map);
200 self.dupes = remap_keys_for_renames(&self.dupes, &rename_map);
201 self.cycles = remap_keys_for_renames(&self.cycles, &rename_map);
202 self.public_api = remap_keys_for_renames(&self.public_api, &rename_map);
203 self.branching = self
206 .branching
207 .drain()
208 .map(|(path, totals)| match rename_map.get(&path) {
209 Some(renamed) => (renamed.clone(), totals),
210 None => (path, totals),
211 })
212 .collect();
213 }
214
215 #[must_use]
218 pub fn to_programmatic(&self) -> AuditProgrammaticKeySnapshot {
219 let mut health = complexity_baseline_keys(&self.health);
220 health.extend(self.styling.iter().cloned());
221 AuditProgrammaticKeySnapshot {
222 dead_code: self.dead_code.clone(),
223 health,
224 dupes: self.dupes.clone(),
225 }
226 }
227}
228
229#[must_use]
232pub fn branching_keys(
233 by_file: &fallow_engine::health::BranchingByFile,
234 root: &Path,
235) -> FxHashMap<String, fallow_types::extract::FileBranching> {
236 by_file
237 .iter()
238 .map(|(path, totals)| (relative_key_path(path, root), *totals))
239 .collect()
240}
241
242#[must_use]
253pub fn type_aware_attribution_degrade_reason(
254 base: Option<&AuditKeySnapshot>,
255 head: Option<&fallow_types::envelope::TypeAwareMeta>,
256) -> Option<&'static str> {
257 let base = base?;
258 let base_identity = base.type_aware_identity.as_ref();
259 let head_identity = head.and_then(|meta| meta.identity.as_ref());
260 if let (Some(base_identity), Some(head_identity)) = (base_identity, head_identity)
261 && !base_identity.incompatible_fields(head_identity).is_empty()
262 {
263 return Some("their semantic analysis identities are incompatible");
264 }
265 if let Some(head) = head
266 && base.type_aware_gap_signature != type_aware_gap_signature(head)
267 {
268 return Some("their incomplete semantic query reasons or omissions differ");
269 }
270 None
271}
272
273#[must_use]
276pub fn type_aware_degrade_warning(reason: &str) -> String {
277 format!(
278 "audit compared base and head with syntactic attribution because {reason} \
279(usually a tsconfig or compiler-options change between base and head); \
280type-aware refinement still applies to head findings, and \
281semantic-only findings stay out of the new-only gate for this run; set \
282audit.typeAware: false or pass --no-type-aware to keep the gate syntactic"
283 )
284}
285
286#[must_use]
288pub fn type_aware_gap_signature(meta: &fallow_types::envelope::TypeAwareMeta) -> Vec<String> {
289 let mut signature = meta
290 .queries
291 .iter()
292 .filter(|query| query.status != fallow_types::semantic::SemanticCompleteness::Complete)
293 .map(|query| {
294 let mut omissions = query
295 .omissions
296 .iter()
297 .map(|omission| format!("{:?}:{}", omission.reason_code, omission.count))
298 .collect::<Vec<_>>();
299 omissions.sort();
300 format!(
301 "{:?}:{:?}:{}",
302 query.capability,
303 query.reason_code,
304 omissions.join(",")
305 )
306 })
307 .collect::<Vec<_>>();
308 signature.sort();
309 signature
310}