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() {
49 continue;
50 }
51 let patch_macros = extract_defined_macros(subject.added_text);
52 let mut flagged = BTreeSet::new();
53 for u in macro_uses {
54 if is_predefined_macro(&u.name)
55 || patch_macros.contains(&u.name)
56 || defs.macros.contains(&u.name)
57 || !flagged.insert(u.name.clone())
58 {
59 continue;
60 }
61 reasons.push(Reason::MacroUndefinedOnTarget {
62 source_path: subject.source_path.clone(),
63 macro_name: u.name,
64 line: file_line(subject.line_map, u.line),
65 });
66 }
67 let patch_records = extract_defined_records(subject.added_text);
68 let mut flagged = BTreeSet::new();
69 for u in record_uses {
70 if patch_records.contains(&u.name)
71 || defs.records.contains(&u.name)
72 || !flagged.insert(u.name.clone())
73 {
74 continue;
75 }
76 let Ok(record_name) = RecordName::from_str(&u.name) else {
77 continue;
78 };
79 reasons.push(Reason::RecordUndefinedOnTarget {
80 source_path: subject.source_path.clone(),
81 record_name,
82 line: file_line(subject.line_map, u.line),
83 });
84 }
85 }
86 reasons
87}
88
89#[derive(Debug)]
96pub struct TargetDefines {
97 pub macros: BTreeSet<String>,
98 pub records: BTreeSet<String>,
99 pub types: BTreeSet<(TypeName, Arity)>,
100 pub complete: bool,
101 pub stdlib_unread: bool,
107 pub macro_attributes: bool,
110}
111
112impl TargetDefines {
113 #[must_use]
115 pub fn defines_complete(&self) -> bool {
116 self.complete && !self.stdlib_unread
117 }
118}
119
120pub fn collect_target_defines(
121 source_path: &RelativePath,
122 target: &TargetTreeIndex,
123 read_target: &dyn Fn(&RelativePath) -> Option<String>,
124) -> TargetDefines {
125 let mut out = TargetDefines {
126 macros: BTreeSet::new(),
127 records: BTreeSet::new(),
128 types: BTreeSet::new(),
129 complete: true,
130 stdlib_unread: false,
131 macro_attributes: false,
132 };
133 let mut visited: BTreeSet<RelativePath> = BTreeSet::new();
134 let mut stack = vec![(source_path.clone(), 0usize)];
135 while let Some((path, depth)) = stack.pop() {
136 if !visited.insert(path.clone()) {
137 continue;
138 }
139 if depth > MAX_INCLUDE_DEPTH {
140 out.complete = false;
141 continue;
142 }
143 let Some(content) = read_target(&path) else {
144 if depth > 0 {
146 out.complete = false;
147 }
148 continue;
149 };
150 out.macros.extend(extract_defined_macros(&content));
151 out.records.extend(extract_defined_records(&content));
152 out.types.extend(extract_defined_types(&content));
153 out.macro_attributes |= has_macro_expanded_attribute(&content);
154 for inc in extract_includes(&content) {
155 if is_stdlib_include_lib(&inc.directive) {
156 out.stdlib_unread = true;
157 continue;
158 }
159 match resolve_include(target, &path, &inc.directive) {
160 Ok(resolved) => stack.push((resolved, depth + 1)),
161 Err(_) => out.complete = false,
162 }
163 }
164 }
165 out
166}
167
168#[derive(Debug, Default)]
171pub struct MacroValueAnalysis {
172 pub reasons: Vec<Reason>,
173 pub checks: MacroValueTally,
174}
175
176pub fn analyse_macro_values(
185 subjects: &[AddedLinesSubject<'_>],
186 read_target: TreeReader<'_>,
187 read_source: Option<TreeReader<'_>>,
188) -> MacroValueAnalysis {
189 let mut analysis = MacroValueAnalysis::default();
190 for subject in subjects {
191 let uses = extract_macro_uses(subject.added_text);
192 if uses.is_empty() {
193 continue;
194 }
195 let Some(target_text) = read_target(subject.source_path) else {
197 continue;
198 };
199 let patch_macros = extract_defined_macros(subject.added_text);
200 let target_values = extract_defined_macro_values(&target_text);
201 let mut source_values: Option<Option<_>> = None;
202 let mut seen = BTreeSet::new();
203 for u in uses {
204 if is_predefined_macro(&u.name)
205 || patch_macros.contains(&u.name)
206 || !seen.insert(u.name.clone())
207 {
208 continue;
209 }
210 let Some(read_source) = read_source else {
211 analysis.checks.withheld_no_source += 1;
212 continue;
213 };
214 let source_values = source_values.get_or_insert_with(|| {
215 read_source(subject.source_path).map(|t| extract_defined_macro_values(&t))
216 });
217 let Some(source_values) = source_values.as_ref() else {
218 analysis.checks.withheld_no_source += 1;
219 continue;
220 };
221 match (target_values.get(&u.name), source_values.get(&u.name)) {
222 (Some(target_defs), Some(source_defs)) => {
223 let (Some(target_def), Some(source_def)) =
224 (single(target_defs), single(source_defs))
225 else {
226 analysis.checks.withheld_multiple_defines += 1;
227 continue;
228 };
229 analysis.checks.compared += 1;
230 if target_def != source_def {
231 analysis.reasons.push(Reason::MacroValueDrift {
232 source_path: subject.source_path.clone(),
233 macro_name: u.name,
234 source_value: rendered(source_def),
235 target_value: rendered(target_def),
236 line: file_line(subject.line_map, u.line),
237 });
238 }
239 }
240 _ => analysis.checks.withheld_definition_elsewhere += 1,
241 }
242 }
243 }
244 analysis
245}
246
247fn single(defs: &[MacroDef]) -> Option<&MacroDef> {
248 match defs {
249 [one] => Some(one),
250 _ => None,
251 }
252}
253
254fn rendered(def: &MacroDef) -> String {
257 match def.params {
258 Some(n) => format!("({n} args) {}", def.body),
259 None => def.body.clone(),
260 }
261}