Skip to main content

backhopper_xref_reader/
suite_matcher.rs

1// Copyright (C) 2026 Michael S. Klishin and Contributors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// See LICENSE-APACHE and LICENSE-MIT for details.
4
5//! AST-precise `SuiteMatcher`.
6//!
7//! Parses each suite via [`SourceReader`] and answers
8//! "does this suite reference module M?" by looking at the parsed
9//! `ModuleData`'s external calls, unresolved calls, and `-import`
10//! declarations. Suites that fail to load (no `-module` attribute,
11//! invalid UTF-8, parser bailout) fall through to a
12//! [`SubstringMatcher`].
13//!
14//! Each suite file is parsed at most once and the result is cached
15//! across calls.
16
17use 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    /// Whether the given suite was successfully parsed (its references
47    /// came from the AST) or fell back to the substring matcher.
48    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
93/// Modules the suite refers to: every external call target, every
94/// unresolved-function partial that names a module, every imported
95/// module.
96fn 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}