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 dead_code_keys, health_keys, relative_key_path, remap_keys_for_renames, styling_keys,
13};
14use crate::review_deltas::{boundary_edge_keys, cycle_keys};
15
16#[derive(Debug, Clone, Default)]
22pub struct AuditKeySnapshot {
23 pub type_aware_identity: Option<fallow_types::semantic::SemanticAnalysisIdentity>,
26 pub type_aware_gap_signature: Vec<String>,
29 pub syntactic_dead_code: Option<FxHashSet<String>>,
33 pub dead_code: FxHashSet<String>,
35 pub health: FxHashSet<String>,
37 pub styling: FxHashSet<String>,
39 pub dupes: FxHashSet<String>,
41 pub boundary_edges: FxHashSet<String>,
44 pub cycles: FxHashSet<String>,
46 pub public_api: FxHashSet<String>,
49 pub branching: FxHashMap<String, fallow_types::extract::FileBranching>,
53}
54
55impl AuditKeySnapshot {
56 #[must_use]
58 pub fn from_view(view: &AuditAnalysesView<'_>) -> Self {
59 let mut snapshot = Self::default();
60 if let Some(dead_code) = view.dead_code.as_ref() {
61 snapshot.type_aware_identity =
62 dead_code.type_aware.and_then(|meta| meta.identity.clone());
63 snapshot.type_aware_gap_signature = dead_code
64 .type_aware
65 .map_or_else(Vec::new, type_aware_gap_signature);
66 snapshot.syntactic_dead_code = dead_code.syntactic_keys.cloned();
67 snapshot.dead_code = dead_code_keys(dead_code.results, dead_code.root);
68 snapshot.boundary_edges = boundary_edge_keys(&dead_code.results.boundary_violations);
69 snapshot.cycles = cycle_keys(&dead_code.results.circular_dependencies, dead_code.root);
70 snapshot.public_api = dead_code.public_api.cloned().unwrap_or_default();
71 }
72 if let Some(health) = view.health.as_ref() {
73 snapshot.health = health_keys(health.report, health.root);
74 snapshot.styling = styling_keys(health.report, health.root);
75 snapshot.branching = health.branching.map_or_else(FxHashMap::default, |by_file| {
76 branching_keys(by_file, health.root)
77 });
78 }
79 if let Some(duplication) = view.duplication.as_ref() {
80 snapshot.dupes = duplication
81 .clone_groups
82 .iter()
83 .map(|group| crate::audit_keys::dupe_group_key(group, duplication.root))
84 .collect();
85 }
86 snapshot
87 }
88
89 pub fn remap_for_renames(&mut self, renames: &[RenamedFile], root: &Path) {
97 let rename_map: FxHashMap<String, String> = renames
98 .iter()
99 .filter_map(|rename| {
100 let from = relative_key_path(&rename.from, root);
101 let to = relative_key_path(&rename.to, root);
102 (from != to).then_some((from, to))
103 })
104 .collect();
105 if rename_map.is_empty() {
106 return;
107 }
108 self.dead_code = remap_keys_for_renames(&self.dead_code, &rename_map);
109 self.health = remap_keys_for_renames(&self.health, &rename_map);
110 self.styling = remap_keys_for_renames(&self.styling, &rename_map);
111 self.dupes = remap_keys_for_renames(&self.dupes, &rename_map);
112 self.cycles = remap_keys_for_renames(&self.cycles, &rename_map);
113 self.public_api = remap_keys_for_renames(&self.public_api, &rename_map);
114 self.branching = self
117 .branching
118 .drain()
119 .map(|(path, totals)| match rename_map.get(&path) {
120 Some(renamed) => (renamed.clone(), totals),
121 None => (path, totals),
122 })
123 .collect();
124 }
125
126 #[must_use]
129 pub fn to_programmatic(&self) -> AuditProgrammaticKeySnapshot {
130 let mut health = self.health.clone();
131 health.extend(self.styling.iter().cloned());
132 AuditProgrammaticKeySnapshot {
133 dead_code: self.dead_code.clone(),
134 health,
135 dupes: self.dupes.clone(),
136 }
137 }
138}
139
140#[must_use]
143pub fn branching_keys(
144 by_file: &fallow_engine::health::BranchingByFile,
145 root: &Path,
146) -> FxHashMap<String, fallow_types::extract::FileBranching> {
147 by_file
148 .iter()
149 .map(|(path, totals)| (relative_key_path(path, root), *totals))
150 .collect()
151}
152
153#[must_use]
164pub fn type_aware_attribution_degrade_reason(
165 base: Option<&AuditKeySnapshot>,
166 head: Option<&fallow_types::envelope::TypeAwareMeta>,
167) -> Option<&'static str> {
168 let base = base?;
169 let base_identity = base.type_aware_identity.as_ref();
170 let head_identity = head.and_then(|meta| meta.identity.as_ref());
171 if let (Some(base_identity), Some(head_identity)) = (base_identity, head_identity)
172 && !base_identity.incompatible_fields(head_identity).is_empty()
173 {
174 return Some("their semantic analysis identities are incompatible");
175 }
176 if let Some(head) = head
177 && base.type_aware_gap_signature != type_aware_gap_signature(head)
178 {
179 return Some("their incomplete semantic query reasons or omissions differ");
180 }
181 None
182}
183
184#[must_use]
187pub fn type_aware_degrade_warning(reason: &str) -> String {
188 format!(
189 "audit compared base and head with syntactic attribution because {reason} \
190(usually a tsconfig or compiler-options change between base and head); \
191type-aware refinement still applies to head findings, and \
192semantic-only findings stay out of the new-only gate for this run; set \
193audit.typeAware: false or pass --no-type-aware to keep the gate syntactic"
194 )
195}
196
197#[must_use]
199pub fn type_aware_gap_signature(meta: &fallow_types::envelope::TypeAwareMeta) -> Vec<String> {
200 let mut signature = meta
201 .queries
202 .iter()
203 .filter(|query| query.status != fallow_types::semantic::SemanticCompleteness::Complete)
204 .map(|query| {
205 let mut omissions = query
206 .omissions
207 .iter()
208 .map(|omission| format!("{:?}:{}", omission.reason_code, omission.count))
209 .collect::<Vec<_>>();
210 omissions.sort();
211 format!(
212 "{:?}:{:?}:{}",
213 query.capability,
214 query.reason_code,
215 omissions.join(",")
216 )
217 })
218 .collect::<Vec<_>>();
219 signature.sort();
220 signature
221}