backhopper_core/compat/
define_resolve.rs1use std::collections::BTreeSet;
14use std::str::FromStr;
15
16use crate::compat::added_file::is_stdlib_include_lib;
17use crate::compat::added_lines::{AddedLinesSubject, file_line};
18use crate::compat::qualified_call_resolve::TreeReader;
19use crate::compat::source_attributes::{
20 MacroDef, extract_defined_macro_values, extract_defined_macros, extract_defined_records,
21 extract_defined_types, extract_includes, extract_macro_uses, extract_record_uses,
22 has_macro_expanded_attribute, is_predefined_macro, resolve_include,
23};
24use crate::compat::target_tree_index::TargetTreeIndex;
25use crate::model::names::{Arity, RecordName, RelativePath, TypeName};
26use crate::model::verdict::{MacroValueTally, Reason};
27
28const MAX_INCLUDE_DEPTH: usize = 16;
30
31pub fn analyse_define_symbols(
35 subjects: &[AddedLinesSubject<'_>],
36 target: &TargetTreeIndex,
37 read_target: &dyn Fn(&RelativePath) -> Option<String>,
38) -> Vec<Reason> {
39 let mut reasons = Vec::new();
40 for subject in subjects {
41 let macro_uses = extract_macro_uses(subject.added_text);
42 let record_uses = extract_record_uses(subject.added_text);
43 if macro_uses.is_empty() && record_uses.is_empty() {
44 continue;
45 }
46 let defs = collect_target_defines(subject.source_path, target, read_target);
47 if !defs.defines_complete() {
50 continue;
51 }
52 let patch_macros = extract_defined_macros(subject.added_text);
53 let mut flagged = BTreeSet::new();
54 for u in macro_uses {
55 if is_predefined_macro(&u.name)
56 || patch_macros.contains(&u.name)
57 || defs.macros.contains(&u.name)
58 || !flagged.insert(u.name.clone())
59 {
60 continue;
61 }
62 reasons.push(Reason::MacroUndefinedOnTarget {
63 source_path: subject.source_path.clone(),
64 macro_name: u.name,
65 line: file_line(subject.line_map, u.line),
66 });
67 }
68 let patch_records = extract_defined_records(subject.added_text);
69 let mut flagged = BTreeSet::new();
70 for u in record_uses {
71 if patch_records.contains(&u.name)
72 || defs.records.contains(&u.name)
73 || !flagged.insert(u.name.clone())
74 {
75 continue;
76 }
77 let Ok(record_name) = RecordName::from_str(&u.name) else {
78 continue;
79 };
80 reasons.push(Reason::RecordUndefinedOnTarget {
81 source_path: subject.source_path.clone(),
82 record_name,
83 line: file_line(subject.line_map, u.line),
84 });
85 }
86 }
87 reasons
88}
89
90#[derive(Debug)]
97pub struct TargetDefines {
98 pub macros: BTreeSet<String>,
99 pub records: BTreeSet<String>,
100 pub types: BTreeSet<(TypeName, Arity)>,
101 pub complete: bool,
102 pub stdlib_unread: bool,
108 pub macro_attributes: bool,
111}
112
113impl TargetDefines {
114 #[must_use]
116 pub fn defines_complete(&self) -> bool {
117 self.complete && !self.stdlib_unread
118 }
119}
120
121pub fn collect_target_defines(
122 source_path: &RelativePath,
123 target: &TargetTreeIndex,
124 read_target: &dyn Fn(&RelativePath) -> Option<String>,
125) -> TargetDefines {
126 let mut out = TargetDefines {
127 macros: BTreeSet::new(),
128 records: BTreeSet::new(),
129 types: BTreeSet::new(),
130 complete: true,
131 stdlib_unread: false,
132 macro_attributes: false,
133 };
134 let mut visited: BTreeSet<RelativePath> = BTreeSet::new();
135 let mut stack = vec![(source_path.clone(), 0usize)];
136 while let Some((path, depth)) = stack.pop() {
137 if !visited.insert(path.clone()) {
138 continue;
139 }
140 if depth > MAX_INCLUDE_DEPTH {
141 out.complete = false;
142 continue;
143 }
144 let Some(content) = read_target(&path) else {
145 if depth > 0 {
148 out.complete = false;
149 }
150 continue;
151 };
152 out.macros.extend(extract_defined_macros(&content));
153 out.records.extend(extract_defined_records(&content));
154 out.types.extend(extract_defined_types(&content));
155 out.macro_attributes |= has_macro_expanded_attribute(&content);
156 for inc in extract_includes(&content) {
157 if is_stdlib_include_lib(&inc.directive) {
158 out.stdlib_unread = true;
159 continue;
160 }
161 match resolve_include(target, &path, &inc.directive) {
162 Ok(resolved) => stack.push((resolved, depth + 1)),
163 Err(_) => out.complete = false,
164 }
165 }
166 }
167 out
168}
169
170#[derive(Debug, Default)]
173pub struct MacroValueAnalysis {
174 pub reasons: Vec<Reason>,
175 pub checks: MacroValueTally,
176}
177
178pub fn analyse_macro_values(
187 subjects: &[AddedLinesSubject<'_>],
188 read_target: TreeReader<'_>,
189 read_source: Option<TreeReader<'_>>,
190) -> MacroValueAnalysis {
191 let mut analysis = MacroValueAnalysis::default();
192 for subject in subjects {
193 let uses = extract_macro_uses(subject.added_text);
194 if uses.is_empty() {
195 continue;
196 }
197 let Some(target_text) = read_target(subject.source_path) else {
200 continue;
201 };
202 let patch_macros = extract_defined_macros(subject.added_text);
203 let target_values = extract_defined_macro_values(&target_text);
204 let mut source_values: Option<Option<_>> = None;
205 let mut seen = BTreeSet::new();
206 for u in uses {
207 if is_predefined_macro(&u.name)
208 || patch_macros.contains(&u.name)
209 || !seen.insert(u.name.clone())
210 {
211 continue;
212 }
213 let Some(read_source) = read_source else {
214 analysis.checks.withheld_no_source += 1;
215 continue;
216 };
217 let source_values = source_values.get_or_insert_with(|| {
218 read_source(subject.source_path).map(|t| extract_defined_macro_values(&t))
219 });
220 let Some(source_values) = source_values.as_ref() else {
221 analysis.checks.withheld_no_source += 1;
222 continue;
223 };
224 match (target_values.get(&u.name), source_values.get(&u.name)) {
225 (Some(target_defs), Some(source_defs)) => {
226 let (Some(target_def), Some(source_def)) =
227 (single(target_defs), single(source_defs))
228 else {
229 analysis.checks.withheld_multiple_defines += 1;
230 continue;
231 };
232 analysis.checks.compared += 1;
233 if target_def != source_def {
234 analysis.reasons.push(Reason::MacroValueDrift {
235 source_path: subject.source_path.clone(),
236 macro_name: u.name,
237 source_value: rendered(source_def),
238 target_value: rendered(target_def),
239 line: file_line(subject.line_map, u.line),
240 });
241 }
242 }
243 _ => analysis.checks.withheld_definition_elsewhere += 1,
244 }
245 }
246 }
247 analysis
248}
249
250fn single(defs: &[MacroDef]) -> Option<&MacroDef> {
251 match defs {
252 [one] => Some(one),
253 _ => None,
254 }
255}
256
257fn rendered(def: &MacroDef) -> String {
260 match def.params {
261 Some(n) => format!("({n} args) {}", def.body),
262 None => def.body.clone(),
263 }
264}