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;
10use crate::AuditProgrammaticKeySnapshot;
11use crate::audit_keys::{
12 ComplexityBaseline, complexity_baseline, complexity_baseline_keys, dead_code_keys,
13 relative_key_path, remap_complexity_baseline_for_renames, remap_keys_for_renames, styling_keys,
14};
15use crate::review_deltas::{boundary_edge_keys, cycle_keys};
16
17#[derive(Debug, Clone, Default)]
23pub struct AuditKeySnapshot {
24 pub type_aware_identity: Option<fallow_types::semantic::SemanticAnalysisIdentity>,
27 pub type_aware_gap_signature: Vec<String>,
30 pub syntactic_dead_code: Option<FxHashSet<String>>,
34 pub dead_code: FxHashSet<String>,
36 pub health: ComplexityBaseline,
39 pub styling: FxHashSet<String>,
41 pub dupes: FxHashSet<String>,
43 pub dupe_groups: Vec<crate::diff_risk::clones::BaseCloneGroup>,
46 pub boundary_edges: FxHashSet<String>,
49 pub cycles: FxHashSet<String>,
51 pub public_api: FxHashSet<String>,
54 pub branching: FxHashMap<String, fallow_types::extract::FileBranching>,
58 pub inherited_health: Vec<ComplexityBaseline>,
62}
63
64impl AuditKeySnapshot {
65 #[must_use]
67 pub fn from_view(view: &AuditAnalysesView<'_>) -> Self {
68 let mut snapshot = Self::default();
69 if let Some(dead_code) = view.dead_code.as_ref() {
70 snapshot.type_aware_identity =
71 dead_code.type_aware.and_then(|meta| meta.identity.clone());
72 snapshot.type_aware_gap_signature = dead_code
73 .type_aware
74 .map_or_else(Vec::new, type_aware_gap_signature);
75 snapshot.syntactic_dead_code = dead_code.syntactic_keys.cloned();
76 snapshot.dead_code = dead_code_keys(dead_code.results, dead_code.root);
77 snapshot.boundary_edges = boundary_edge_keys(&dead_code.results.boundary_violations);
78 snapshot.cycles = cycle_keys(&dead_code.results.circular_dependencies, dead_code.root);
79 snapshot.public_api = dead_code.public_api.cloned().unwrap_or_default();
80 }
81 if let Some(health) = view.health.as_ref() {
82 snapshot.health = complexity_baseline(health.report, health.root);
83 snapshot.styling = styling_keys(health.report, health.root);
84 snapshot.branching = health.branching.map_or_else(FxHashMap::default, |by_file| {
85 branching_keys(by_file, health.root)
86 });
87 }
88 if let Some(duplication) = view.duplication.as_ref() {
89 snapshot.dupes = duplication
90 .clone_groups
91 .iter()
92 .map(|group| crate::audit_keys::dupe_group_key(group, duplication.root))
93 .collect();
94 snapshot.dupe_groups = duplication
95 .clone_groups
96 .iter()
97 .map(|group| crate::diff_risk::clones::BaseCloneGroup {
98 instances: crate::diff_risk::clone_spans(group, duplication.root),
99 })
100 .collect();
101 }
102 snapshot
103 }
104
105 pub fn absorb_inherited(&mut self, inherited: Self) {
116 let inherited_syntactic = inherited
117 .syntactic_dead_code
118 .unwrap_or_else(|| inherited.dead_code.clone());
119 if let Some(syntactic) = self.syntactic_dead_code.as_mut() {
120 syntactic.extend(inherited_syntactic);
121 }
122 if self.type_aware_identity == inherited.type_aware_identity
125 && self.type_aware_gap_signature == inherited.type_aware_gap_signature
126 {
127 self.dead_code.extend(inherited.dead_code);
128 }
129 self.styling.extend(inherited.styling);
130 self.dupes.extend(inherited.dupes);
131 self.boundary_edges.extend(inherited.boundary_edges);
132 self.cycles.extend(inherited.cycles);
133 self.public_api.extend(inherited.public_api);
134 self.inherited_health.push(inherited.health);
135 self.inherited_health.extend(inherited.inherited_health);
136 }
137
138 pub fn remap_for_renames(&mut self, renames: &[RenamedFile], root: &Path) {
146 let rename_map: FxHashMap<String, String> = renames
147 .iter()
148 .filter_map(|rename| {
149 let from = relative_key_path(&rename.from, root);
150 let to = relative_key_path(&rename.to, root);
151 (from != to).then_some((from, to))
152 })
153 .collect();
154 if rename_map.is_empty() {
155 return;
156 }
157 self.dead_code = remap_keys_for_renames(&self.dead_code, &rename_map);
158 self.health =
159 remap_complexity_baseline_for_renames(std::mem::take(&mut self.health), &rename_map);
160 self.styling = remap_keys_for_renames(&self.styling, &rename_map);
161 self.dupes = remap_keys_for_renames(&self.dupes, &rename_map);
162 self.cycles = remap_keys_for_renames(&self.cycles, &rename_map);
163 self.public_api = remap_keys_for_renames(&self.public_api, &rename_map);
164 self.branching = self
167 .branching
168 .drain()
169 .map(|(path, totals)| match rename_map.get(&path) {
170 Some(renamed) => (renamed.clone(), totals),
171 None => (path, totals),
172 })
173 .collect();
174 }
175
176 #[must_use]
179 pub fn to_programmatic(&self) -> AuditProgrammaticKeySnapshot {
180 let mut health = complexity_baseline_keys(&self.health);
181 health.extend(self.styling.iter().cloned());
182 AuditProgrammaticKeySnapshot {
183 dead_code: self.dead_code.clone(),
184 health,
185 dupes: self.dupes.clone(),
186 }
187 }
188}
189
190#[must_use]
193pub fn branching_keys(
194 by_file: &fallow_engine::health::BranchingByFile,
195 root: &Path,
196) -> FxHashMap<String, fallow_types::extract::FileBranching> {
197 by_file
198 .iter()
199 .map(|(path, totals)| (relative_key_path(path, root), *totals))
200 .collect()
201}
202
203#[must_use]
214pub fn type_aware_attribution_degrade_reason(
215 base: Option<&AuditKeySnapshot>,
216 head: Option<&fallow_types::envelope::TypeAwareMeta>,
217) -> Option<&'static str> {
218 let base = base?;
219 let base_identity = base.type_aware_identity.as_ref();
220 let head_identity = head.and_then(|meta| meta.identity.as_ref());
221 if let (Some(base_identity), Some(head_identity)) = (base_identity, head_identity)
222 && !base_identity.incompatible_fields(head_identity).is_empty()
223 {
224 return Some("their semantic analysis identities are incompatible");
225 }
226 if let Some(head) = head
227 && base.type_aware_gap_signature != type_aware_gap_signature(head)
228 {
229 return Some("their incomplete semantic query reasons or omissions differ");
230 }
231 None
232}
233
234#[must_use]
237pub fn type_aware_degrade_warning(reason: &str) -> String {
238 format!(
239 "audit compared base and head with syntactic attribution because {reason} \
240(usually a tsconfig or compiler-options change between base and head); \
241type-aware refinement still applies to head findings, and \
242semantic-only findings stay out of the new-only gate for this run; set \
243audit.typeAware: false or pass --no-type-aware to keep the gate syntactic"
244 )
245}
246
247#[must_use]
249pub fn type_aware_gap_signature(meta: &fallow_types::envelope::TypeAwareMeta) -> Vec<String> {
250 let mut signature = meta
251 .queries
252 .iter()
253 .filter(|query| query.status != fallow_types::semantic::SemanticCompleteness::Complete)
254 .map(|query| {
255 let mut omissions = query
256 .omissions
257 .iter()
258 .map(|omission| format!("{:?}:{}", omission.reason_code, omission.count))
259 .collect::<Vec<_>>();
260 omissions.sort();
261 format!(
262 "{:?}:{:?}:{}",
263 query.capability,
264 query.reason_code,
265 omissions.join(",")
266 )
267 })
268 .collect::<Vec<_>>();
269 signature.sort();
270 signature
271}