backhopper_xref_reader/
suite_matcher.rs1use std::collections::{BTreeMap, BTreeSet};
18use std::fs;
19use std::path::{Path, PathBuf};
20
21use backhopper_core::model::names::ModuleName;
22use backhopper_core::suites::{SubstringMatcher, SuiteMatcher};
23use backhopper_xref_graph::{CallTarget, FunctionRef};
24
25use crate::model::ModuleData;
26use crate::reader::SourceReader;
27
28#[derive(Debug, Default)]
29pub struct AstSuiteMatcher {
30 reader: SourceReader,
31 parsed: BTreeMap<PathBuf, ParsedSuite>,
32 fallback: SubstringMatcher,
33}
34
35#[derive(Debug)]
36enum ParsedSuite {
37 Loaded(BTreeSet<ModuleName>),
38 Failed,
39}
40
41impl AstSuiteMatcher {
42 pub fn new() -> Self {
43 Self::default()
44 }
45
46 pub fn parsed_cleanly(&self, suite_path: &Path) -> bool {
49 matches!(self.parsed.get(suite_path), Some(ParsedSuite::Loaded(_)))
50 }
51
52 fn ensure_loaded(&mut self, suite_path: &Path) -> &ParsedSuite {
53 if !self.parsed.contains_key(suite_path) {
54 let parsed = load_suite_modules(&self.reader, suite_path);
55 self.parsed.insert(suite_path.to_path_buf(), parsed);
56 }
57 &self.parsed[suite_path]
58 }
59}
60
61impl SuiteMatcher for AstSuiteMatcher {
62 fn modules_referenced_in_suite(
63 &mut self,
64 suite_path: &Path,
65 triggering: &[ModuleName],
66 ) -> BTreeSet<ModuleName> {
67 match self.ensure_loaded(suite_path) {
68 ParsedSuite::Loaded(refs) => triggering
69 .iter()
70 .filter(|m| refs.contains(*m))
71 .cloned()
72 .collect(),
73 ParsedSuite::Failed => self
74 .fallback
75 .modules_referenced_in_suite(suite_path, triggering),
76 }
77 }
78}
79
80fn load_suite_modules(reader: &SourceReader, suite_path: &Path) -> ParsedSuite {
81 let Ok(text) = fs::read_to_string(suite_path) else {
82 return ParsedSuite::Failed;
83 };
84 let Ok((maybe_module, _warnings)) = reader.read_one(suite_path, &text) else {
85 return ParsedSuite::Failed;
86 };
87 let Some(module) = maybe_module else {
88 return ParsedSuite::Failed;
89 };
90 ParsedSuite::Loaded(collect_referenced_modules(&module))
91}
92
93fn collect_referenced_modules(data: &ModuleData) -> BTreeSet<ModuleName> {
97 let mut out: BTreeSet<ModuleName> = BTreeSet::new();
98 for call in &data.external_calls {
99 if let CallTarget::External(fn_ref) = &call.callee
100 && let Some(m) = module_of(fn_ref)
101 {
102 out.insert(m.clone());
103 }
104 }
105 for unresolved in &data.unresolved {
106 if let Some(m) = module_of(&unresolved.partial) {
107 out.insert(m.clone());
108 }
109 }
110 for module in data.imports.keys() {
111 out.insert(module.clone());
112 }
113 out
114}
115
116fn module_of(fn_ref: &FunctionRef) -> Option<&ModuleName> {
117 match fn_ref {
118 FunctionRef::Concrete(mfa) => Some(&mfa.module),
119 FunctionRef::UnresolvedFunction { module, .. } => Some(module),
120 _ => None,
121 }
122}