1use std::collections::{BTreeMap, 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 patch_added: &BTreeMap<RelativePath, String>,
37 target: &TargetTreeIndex,
38 read_target: &dyn Fn(&RelativePath) -> Option<String>,
39) -> Vec<Reason> {
40 let mut reasons = Vec::new();
41 for subject in subjects {
42 let macro_uses = extract_macro_uses(subject.added_text);
43 let record_uses = extract_record_uses(subject.added_text);
44 if macro_uses.is_empty() && record_uses.is_empty() {
45 continue;
46 }
47 let defs = collect_target_defines(subject, patch_added, target, read_target);
48 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)]
99pub struct TargetDefines {
100 pub macros: BTreeSet<String>,
101 pub records: BTreeSet<String>,
102 pub types: BTreeSet<(TypeName, Arity)>,
103 pub complete: bool,
104 pub stdlib_unread: bool,
110 pub macro_attributes: bool,
113}
114
115impl TargetDefines {
116 #[must_use]
118 pub fn defines_complete(&self) -> bool {
119 self.complete && !self.stdlib_unread
120 }
121}
122
123impl Default for TargetDefines {
125 fn default() -> Self {
126 Self {
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 }
135}
136
137enum HeaderSource {
139 Target,
140 PatchAdded,
141}
142
143struct IncludeWalk<'a> {
144 out: TargetDefines,
145 visited: BTreeSet<RelativePath>,
146 stack: Vec<(RelativePath, usize, HeaderSource)>,
147 patch_added: &'a BTreeMap<RelativePath, String>,
148 target: &'a TargetTreeIndex,
149}
150
151impl IncludeWalk<'_> {
152 fn absorb(&mut self, content: &str) {
153 self.out.macros.extend(extract_defined_macros(content));
154 self.out.records.extend(extract_defined_records(content));
155 self.out.types.extend(extract_defined_types(content));
156 self.out.macro_attributes |= has_macro_expanded_attribute(content);
157 }
158
159 fn follow_includes(&mut self, content: &str, from: &RelativePath, depth: usize) {
160 for inc in extract_includes(content) {
161 if is_stdlib_include_lib(&inc.directive) {
162 self.out.stdlib_unread = true;
163 continue;
164 }
165 match resolve_include(self.target, from, &inc.directive) {
167 Ok(resolved) => self.push(resolved, depth, HeaderSource::Target),
168 Err(candidates) => {
169 let patch_hit = candidates
170 .into_iter()
171 .find(|c| self.patch_added.contains_key(c));
172 match patch_hit {
173 Some(path) => self.push(path, depth, HeaderSource::PatchAdded),
174 None => self.out.complete = false,
175 }
176 }
177 }
178 }
179 }
180
181 fn push(&mut self, path: RelativePath, depth: usize, source: HeaderSource) {
182 if !self.visited.insert(path.clone()) {
183 return;
184 }
185 if depth > MAX_INCLUDE_DEPTH {
186 self.out.complete = false;
187 return;
188 }
189 self.stack.push((path, depth, source));
190 }
191}
192
193pub fn collect_target_defines(
197 subject: &AddedLinesSubject<'_>,
198 patch_added: &BTreeMap<RelativePath, String>,
199 target: &TargetTreeIndex,
200 read_target: &dyn Fn(&RelativePath) -> Option<String>,
201) -> TargetDefines {
202 let mut walk = IncludeWalk {
203 out: TargetDefines::default(),
204 visited: BTreeSet::new(),
205 stack: Vec::new(),
206 patch_added,
207 target,
208 };
209 walk.visited.insert(subject.source_path.clone());
210 if let Some(content) = read_target(subject.source_path) {
212 walk.absorb(&content);
213 walk.follow_includes(&content, subject.source_path, 1);
214 }
215 walk.follow_includes(subject.added_text, subject.source_path, 1);
216 while let Some((path, depth, source)) = walk.stack.pop() {
217 let content = match source {
218 HeaderSource::Target => read_target(&path),
219 HeaderSource::PatchAdded => walk.patch_added.get(&path).cloned(),
220 };
221 let Some(content) = content else {
222 walk.out.complete = false;
223 continue;
224 };
225 walk.absorb(&content);
226 walk.follow_includes(&content, &path, depth + 1);
227 }
228 walk.out
229}
230
231#[derive(Debug, Default)]
234pub struct MacroValueAnalysis {
235 pub reasons: Vec<Reason>,
236 pub checks: MacroValueTally,
237}
238
239pub fn analyse_macro_values(
248 subjects: &[AddedLinesSubject<'_>],
249 read_target: TreeReader<'_>,
250 read_source: Option<TreeReader<'_>>,
251) -> MacroValueAnalysis {
252 let mut analysis = MacroValueAnalysis::default();
253 for subject in subjects {
254 let uses = extract_macro_uses(subject.added_text);
255 if uses.is_empty() {
256 continue;
257 }
258 let Some(target_text) = read_target(subject.source_path) else {
260 continue;
261 };
262 let patch_macros = extract_defined_macros(subject.added_text);
263 let target_values = extract_defined_macro_values(&target_text);
264 let mut source_values: Option<Option<_>> = None;
265 let mut seen = BTreeSet::new();
266 for u in uses {
267 if is_predefined_macro(&u.name)
268 || patch_macros.contains(&u.name)
269 || !seen.insert(u.name.clone())
270 {
271 continue;
272 }
273 let Some(read_source) = read_source else {
274 analysis.checks.withheld_no_source += 1;
275 continue;
276 };
277 let source_values = source_values.get_or_insert_with(|| {
278 read_source(subject.source_path).map(|t| extract_defined_macro_values(&t))
279 });
280 let Some(source_values) = source_values.as_ref() else {
281 analysis.checks.withheld_no_source += 1;
282 continue;
283 };
284 match (target_values.get(&u.name), source_values.get(&u.name)) {
285 (Some(target_defs), Some(source_defs)) => {
286 let (Some(target_def), Some(source_def)) =
287 (single(target_defs), single(source_defs))
288 else {
289 analysis.checks.withheld_multiple_defines += 1;
290 continue;
291 };
292 analysis.checks.compared += 1;
293 if target_def != source_def {
294 analysis.reasons.push(Reason::MacroValueDrift {
295 source_path: subject.source_path.clone(),
296 macro_name: u.name,
297 source_value: rendered(source_def),
298 target_value: rendered(target_def),
299 line: file_line(subject.line_map, u.line),
300 });
301 }
302 }
303 _ => analysis.checks.withheld_definition_elsewhere += 1,
304 }
305 }
306 }
307 analysis
308}
309
310fn single(defs: &[MacroDef]) -> Option<&MacroDef> {
311 match defs {
312 [one] => Some(one),
313 _ => None,
314 }
315}
316
317fn rendered(def: &MacroDef) -> String {
320 match def.params {
321 Some(n) => format!("({n} args) {}", def.body),
322 None => def.body.clone(),
323 }
324}