use std::collections::{BTreeMap, BTreeSet};
use std::str::FromStr;
use crate::compat::added_file::is_stdlib_include_lib;
use crate::compat::added_lines::{AddedLinesSubject, file_line};
use crate::compat::qualified_call_resolve::TreeReader;
use crate::compat::source_attributes::{
MacroDef, extract_defined_macro_values, extract_defined_macros, extract_defined_records,
extract_defined_types, extract_includes, extract_macro_uses, extract_record_uses,
has_macro_expanded_attribute, is_predefined_macro, resolve_include,
};
use crate::compat::target_tree_index::TargetTreeIndex;
use crate::model::names::{Arity, RecordName, RelativePath, TypeName};
use crate::model::verdict::{MacroValueTally, Reason};
const MAX_INCLUDE_DEPTH: usize = 16;
pub fn analyse_define_symbols(
subjects: &[AddedLinesSubject<'_>],
patch_added: &BTreeMap<RelativePath, String>,
target: &TargetTreeIndex,
read_target: &dyn Fn(&RelativePath) -> Option<String>,
) -> Vec<Reason> {
let mut reasons = Vec::new();
for subject in subjects {
let macro_uses = extract_macro_uses(subject.added_text);
let record_uses = extract_record_uses(subject.added_text);
if macro_uses.is_empty() && record_uses.is_empty() {
continue;
}
let defs = collect_target_defines(subject, patch_added, target, read_target);
if !defs.defines_complete() {
continue;
}
let patch_macros = extract_defined_macros(subject.added_text);
let mut flagged = BTreeSet::new();
for u in macro_uses {
if is_predefined_macro(&u.name)
|| patch_macros.contains(&u.name)
|| defs.macros.contains(&u.name)
|| !flagged.insert(u.name.clone())
{
continue;
}
reasons.push(Reason::MacroUndefinedOnTarget {
source_path: subject.source_path.clone(),
macro_name: u.name,
line: file_line(subject.line_map, u.line),
});
}
let patch_records = extract_defined_records(subject.added_text);
let mut flagged = BTreeSet::new();
for u in record_uses {
if patch_records.contains(&u.name)
|| defs.records.contains(&u.name)
|| !flagged.insert(u.name.clone())
{
continue;
}
let Ok(record_name) = RecordName::from_str(&u.name) else {
continue;
};
reasons.push(Reason::RecordUndefinedOnTarget {
source_path: subject.source_path.clone(),
record_name,
line: file_line(subject.line_map, u.line),
});
}
}
reasons
}
#[derive(Debug)]
pub struct TargetDefines {
pub macros: BTreeSet<String>,
pub records: BTreeSet<String>,
pub types: BTreeSet<(TypeName, Arity)>,
pub complete: bool,
pub stdlib_unread: bool,
pub macro_attributes: bool,
}
impl TargetDefines {
#[must_use]
pub fn defines_complete(&self) -> bool {
self.complete && !self.stdlib_unread
}
}
impl Default for TargetDefines {
fn default() -> Self {
Self {
macros: BTreeSet::new(),
records: BTreeSet::new(),
types: BTreeSet::new(),
complete: true,
stdlib_unread: false,
macro_attributes: false,
}
}
}
enum HeaderSource {
Target,
PatchAdded,
}
struct IncludeWalk<'a> {
out: TargetDefines,
visited: BTreeSet<RelativePath>,
stack: Vec<(RelativePath, usize, HeaderSource)>,
patch_added: &'a BTreeMap<RelativePath, String>,
target: &'a TargetTreeIndex,
}
impl IncludeWalk<'_> {
fn absorb(&mut self, content: &str) {
self.out.macros.extend(extract_defined_macros(content));
self.out.records.extend(extract_defined_records(content));
self.out.types.extend(extract_defined_types(content));
self.out.macro_attributes |= has_macro_expanded_attribute(content);
}
fn follow_includes(&mut self, content: &str, from: &RelativePath, depth: usize) {
for inc in extract_includes(content) {
if is_stdlib_include_lib(&inc.directive) {
self.out.stdlib_unread = true;
continue;
}
match resolve_include(self.target, from, &inc.directive) {
Ok(resolved) => self.push(resolved, depth, HeaderSource::Target),
Err(candidates) => {
let patch_hit = candidates
.into_iter()
.find(|c| self.patch_added.contains_key(c));
match patch_hit {
Some(path) => self.push(path, depth, HeaderSource::PatchAdded),
None => self.out.complete = false,
}
}
}
}
}
fn push(&mut self, path: RelativePath, depth: usize, source: HeaderSource) {
if !self.visited.insert(path.clone()) {
return;
}
if depth > MAX_INCLUDE_DEPTH {
self.out.complete = false;
return;
}
self.stack.push((path, depth, source));
}
}
pub fn collect_target_defines(
subject: &AddedLinesSubject<'_>,
patch_added: &BTreeMap<RelativePath, String>,
target: &TargetTreeIndex,
read_target: &dyn Fn(&RelativePath) -> Option<String>,
) -> TargetDefines {
let mut walk = IncludeWalk {
out: TargetDefines::default(),
visited: BTreeSet::new(),
stack: Vec::new(),
patch_added,
target,
};
walk.visited.insert(subject.source_path.clone());
if let Some(content) = read_target(subject.source_path) {
walk.absorb(&content);
walk.follow_includes(&content, subject.source_path, 1);
}
walk.follow_includes(subject.added_text, subject.source_path, 1);
while let Some((path, depth, source)) = walk.stack.pop() {
let content = match source {
HeaderSource::Target => read_target(&path),
HeaderSource::PatchAdded => walk.patch_added.get(&path).cloned(),
};
let Some(content) = content else {
walk.out.complete = false;
continue;
};
walk.absorb(&content);
walk.follow_includes(&content, &path, depth + 1);
}
walk.out
}
#[derive(Debug, Default)]
pub struct MacroValueAnalysis {
pub reasons: Vec<Reason>,
pub checks: MacroValueTally,
}
pub fn analyse_macro_values(
subjects: &[AddedLinesSubject<'_>],
read_target: TreeReader<'_>,
read_source: Option<TreeReader<'_>>,
) -> MacroValueAnalysis {
let mut analysis = MacroValueAnalysis::default();
for subject in subjects {
let uses = extract_macro_uses(subject.added_text);
if uses.is_empty() {
continue;
}
let Some(target_text) = read_target(subject.source_path) else {
continue;
};
let patch_macros = extract_defined_macros(subject.added_text);
let target_values = extract_defined_macro_values(&target_text);
let mut source_values: Option<Option<_>> = None;
let mut seen = BTreeSet::new();
for u in uses {
if is_predefined_macro(&u.name)
|| patch_macros.contains(&u.name)
|| !seen.insert(u.name.clone())
{
continue;
}
let Some(read_source) = read_source else {
analysis.checks.withheld_no_source += 1;
continue;
};
let source_values = source_values.get_or_insert_with(|| {
read_source(subject.source_path).map(|t| extract_defined_macro_values(&t))
});
let Some(source_values) = source_values.as_ref() else {
analysis.checks.withheld_no_source += 1;
continue;
};
match (target_values.get(&u.name), source_values.get(&u.name)) {
(Some(target_defs), Some(source_defs)) => {
let (Some(target_def), Some(source_def)) =
(single(target_defs), single(source_defs))
else {
analysis.checks.withheld_multiple_defines += 1;
continue;
};
analysis.checks.compared += 1;
if target_def != source_def {
analysis.reasons.push(Reason::MacroValueDrift {
source_path: subject.source_path.clone(),
macro_name: u.name,
source_value: rendered(source_def),
target_value: rendered(target_def),
line: file_line(subject.line_map, u.line),
});
}
}
_ => analysis.checks.withheld_definition_elsewhere += 1,
}
}
}
analysis
}
fn single(defs: &[MacroDef]) -> Option<&MacroDef> {
match defs {
[one] => Some(one),
_ => None,
}
}
fn rendered(def: &MacroDef) -> String {
match def.params {
Some(n) => format!("({n} args) {}", def.body),
None => def.body.clone(),
}
}