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, MacroName, 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.coverage.hides_macros_or_records() {
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.as_str())
58 || !flagged.insert(u.name.clone())
59 {
60 continue;
61 }
62 if MacroName::from_str(&u.name).is_err() {
64 continue;
65 }
66 reasons.push(Reason::MacroUndefinedOnTarget {
67 source_path: subject.source_path.clone(),
68 macro_name: u.name,
69 line: file_line(subject.line_map, u.line),
70 });
71 }
72 let patch_records = extract_defined_records(subject.added_text);
73 let mut flagged = BTreeSet::new();
74 for u in record_uses {
75 if patch_records.contains(&u.name)
76 || defs.records.contains(u.name.as_str())
77 || !flagged.insert(u.name.clone())
78 {
79 continue;
80 }
81 let Ok(record_name) = RecordName::from_str(&u.name) else {
82 continue;
83 };
84 reasons.push(Reason::RecordUndefinedOnTarget {
85 source_path: subject.source_path.clone(),
86 record_name,
87 line: file_line(subject.line_map, u.line),
88 });
89 }
90 }
91 reasons
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
100pub enum IncludeCoverage {
101 #[default]
102 Complete,
103 FirstPartyUnread,
104 StdlibUnread,
105 BothUnread,
106}
107
108impl IncludeCoverage {
109 fn from_flags(first_party_unread: bool, stdlib_unread: bool) -> Self {
110 match (first_party_unread, stdlib_unread) {
111 (false, false) => Self::Complete,
112 (true, false) => Self::FirstPartyUnread,
113 (false, true) => Self::StdlibUnread,
114 (true, true) => Self::BothUnread,
115 }
116 }
117
118 #[must_use]
121 pub fn hides_macros_or_records(self) -> bool {
122 !matches!(self, Self::Complete)
123 }
124
125 #[must_use]
128 pub fn hides_types(self) -> bool {
129 matches!(self, Self::FirstPartyUnread | Self::BothUnread)
130 }
131}
132
133#[derive(Debug, Default)]
138pub struct TargetDefines {
139 pub macros: BTreeSet<MacroName>,
140 pub records: BTreeSet<RecordName>,
141 pub types: BTreeSet<(TypeName, Arity)>,
142 pub coverage: IncludeCoverage,
143 pub macro_attributes: bool,
146}
147
148enum HeaderSource {
150 Target,
151 PatchAdded,
152}
153
154struct IncludeWalk<'a> {
155 macros: BTreeSet<MacroName>,
156 records: BTreeSet<RecordName>,
157 types: BTreeSet<(TypeName, Arity)>,
158 macro_attributes: bool,
159 first_party_unread: bool,
160 stdlib_unread: bool,
161 visited: BTreeSet<RelativePath>,
162 stack: Vec<(RelativePath, usize, HeaderSource)>,
163 patch_added: &'a BTreeMap<RelativePath, String>,
164 target: &'a TargetTreeIndex,
165}
166
167impl IncludeWalk<'_> {
168 fn absorb(&mut self, content: &str) {
169 self.macros.extend(
170 extract_defined_macros(content)
171 .into_iter()
172 .filter_map(|m| MacroName::new(m).ok()),
173 );
174 self.records.extend(
175 extract_defined_records(content)
176 .into_iter()
177 .filter_map(|r| RecordName::new(r).ok()),
178 );
179 self.types.extend(extract_defined_types(content));
180 self.macro_attributes |= has_macro_expanded_attribute(content);
181 }
182
183 fn follow_includes(&mut self, content: &str, from: &RelativePath, depth: usize) {
184 for inc in extract_includes(content) {
185 if is_stdlib_include_lib(&inc.directive) {
186 self.stdlib_unread = true;
187 continue;
188 }
189 match resolve_include(self.target, from, &inc.directive) {
191 Ok(resolved) => self.push(resolved, depth, HeaderSource::Target),
192 Err(candidates) => {
193 let patch_hit = candidates
194 .into_iter()
195 .find(|c| self.patch_added.contains_key(c));
196 match patch_hit {
197 Some(path) => self.push(path, depth, HeaderSource::PatchAdded),
198 None => self.first_party_unread = true,
199 }
200 }
201 }
202 }
203 }
204
205 fn push(&mut self, path: RelativePath, depth: usize, source: HeaderSource) {
206 if !self.visited.insert(path.clone()) {
207 return;
208 }
209 if depth > MAX_INCLUDE_DEPTH {
210 self.first_party_unread = true;
211 return;
212 }
213 self.stack.push((path, depth, source));
214 }
215
216 fn finish(self) -> TargetDefines {
217 TargetDefines {
218 macros: self.macros,
219 records: self.records,
220 types: self.types,
221 coverage: IncludeCoverage::from_flags(self.first_party_unread, self.stdlib_unread),
222 macro_attributes: self.macro_attributes,
223 }
224 }
225}
226
227pub fn collect_target_defines(
231 subject: &AddedLinesSubject<'_>,
232 patch_added: &BTreeMap<RelativePath, String>,
233 target: &TargetTreeIndex,
234 read_target: &dyn Fn(&RelativePath) -> Option<String>,
235) -> TargetDefines {
236 let mut walk = IncludeWalk {
237 macros: BTreeSet::new(),
238 records: BTreeSet::new(),
239 types: BTreeSet::new(),
240 macro_attributes: false,
241 first_party_unread: false,
242 stdlib_unread: false,
243 visited: BTreeSet::new(),
244 stack: Vec::new(),
245 patch_added,
246 target,
247 };
248 walk.visited.insert(subject.source_path.clone());
249 if let Some(content) = read_target(subject.source_path) {
251 walk.absorb(&content);
252 walk.follow_includes(&content, subject.source_path, 1);
253 }
254 walk.follow_includes(subject.added_text, subject.source_path, 1);
255 while let Some((path, depth, source)) = walk.stack.pop() {
256 let content = match source {
257 HeaderSource::Target => read_target(&path),
258 HeaderSource::PatchAdded => walk.patch_added.get(&path).cloned(),
259 };
260 let Some(content) = content else {
261 walk.first_party_unread = true;
262 continue;
263 };
264 walk.absorb(&content);
265 walk.follow_includes(&content, &path, depth + 1);
266 }
267 walk.finish()
268}
269
270#[derive(Debug, Default)]
273pub struct MacroValueAnalysis {
274 pub reasons: Vec<Reason>,
275 pub checks: MacroValueTally,
276}
277
278pub fn analyse_macro_values(
287 subjects: &[AddedLinesSubject<'_>],
288 read_target: TreeReader<'_>,
289 read_source: Option<TreeReader<'_>>,
290) -> MacroValueAnalysis {
291 let mut analysis = MacroValueAnalysis::default();
292 for subject in subjects {
293 let uses = extract_macro_uses(subject.added_text);
294 if uses.is_empty() {
295 continue;
296 }
297 let Some(target_text) = read_target(subject.source_path) else {
299 continue;
300 };
301 let patch_macros = extract_defined_macros(subject.added_text);
302 let target_values = extract_defined_macro_values(&target_text);
303 let mut source_values: Option<Option<_>> = None;
304 let mut seen = BTreeSet::new();
305 for u in uses {
306 if is_predefined_macro(&u.name)
307 || patch_macros.contains(&u.name)
308 || !seen.insert(u.name.clone())
309 {
310 continue;
311 }
312 let Some(read_source) = read_source else {
313 analysis.checks.withheld_no_source += 1;
314 continue;
315 };
316 let source_values = source_values.get_or_insert_with(|| {
317 read_source(subject.source_path).map(|t| extract_defined_macro_values(&t))
318 });
319 let Some(source_values) = source_values.as_ref() else {
320 analysis.checks.withheld_no_source += 1;
321 continue;
322 };
323 match (
324 target_values.get(u.name.as_str()),
325 source_values.get(u.name.as_str()),
326 ) {
327 (Some(target_defs), Some(source_defs)) => {
328 let (Some(target_def), Some(source_def)) =
329 (single(target_defs), single(source_defs))
330 else {
331 analysis.checks.withheld_multiple_defines += 1;
332 continue;
333 };
334 analysis.checks.compared += 1;
335 if target_def != source_def {
336 analysis.reasons.push(Reason::MacroValueDrift {
337 source_path: subject.source_path.clone(),
338 macro_name: u.name,
339 source_value: rendered(source_def),
340 target_value: rendered(target_def),
341 line: file_line(subject.line_map, u.line),
342 });
343 }
344 }
345 _ => analysis.checks.withheld_definition_elsewhere += 1,
346 }
347 }
348 }
349 analysis
350}
351
352fn single(defs: &[MacroDef]) -> Option<&MacroDef> {
353 match defs {
354 [one] => Some(one),
355 _ => None,
356 }
357}
358
359fn rendered(def: &MacroDef) -> String {
362 match def.key.arity {
363 Some(n) => format!("({n} args) {}", def.body),
364 None => def.body.to_string(),
365 }
366}