Skip to main content

ryo_executor/engine/impls/
inline_trait_caller_scanner.rs

1//! CrossCrate InlineTrait Phase 2c — caller crate scanner.
2//!
3//! Given a target trait `SymbolId`, walks the workspace's typeflow
4//! graph for containers (functions / impl blocks / etc.) that
5//! reference the trait, filters down to **cross-crate** callers (i.e.
6//! containers living in a different crate from the trait), and
7//! classifies each caller via the `classify_fn` helper from
8//! `ryo-mutations` (Phase 2a+2b).
9//!
10//! Each cross-crate caller produces a [`CallerRewrite`] entry listing
11//! the `CallerPattern` variants the executor must apply during the
12//! caller-side AST migration in Phase 2d:
13//!
14//! - `Dot`         — post-inline dispatch is unchanged; no AST edit.
15//! - `Ufcs`        — rewrite `<S as Trait>::m(..)` → `S::m(..)`.
16//! - `GenericBound`— strip `T: Trait` generic param and specialize.
17//! - `DynDispatch` — rewrite `&dyn Trait` → `&S`.
18//! - `Other`       — block inline (conservatively).
19//!
20//! Phase 2c stops at the **scan + classify** boundary. The executor
21//! does not rewrite caller crates yet; that engine arrives in Phase
22//! 2d, consuming the [`CallerRewrite`] entries produced here.
23
24use ryo_analysis::context::AnalysisContext;
25use ryo_analysis::query::CodeGraphV2;
26use ryo_analysis::ASTRegistry;
27use ryo_mutations::basic::trait_ops::cross_crate_caller_pattern::{classify_fn, CallerPattern};
28use ryo_source::pure::{PureItem, PureTraitItem, PureVis};
29use ryo_symbol::{SymbolId, SymbolKind, SymbolRegistry};
30use std::collections::{HashSet, VecDeque};
31
32/// Per-caller rewrite metadata returned by [`scan_cross_crate_callers`].
33#[derive(Debug, Clone)]
34pub struct CallerRewrite {
35    /// Caller container symbol (typically a `PureFn`).
36    pub caller_id: SymbolId,
37    /// Patterns detected in this caller; the executor's Phase 2d
38    /// engine will run one rewrite path per entry.
39    pub patterns: Vec<CallerPattern>,
40    /// Phase 4-A: whether the caller is `pub` (visible outside its
41    /// own crate). True only for `PureVis::Public`; `pub(crate)`,
42    /// `pub(super)`, `pub(in path)`, and private all map to false.
43    ///
44    /// When this is true AND the caller carries `GenericBound`, the
45    /// Phase 2g rewriter changes the caller's external API contract.
46    /// Downstream call sites in unrelated crates would have to be
47    /// updated. Phase 4-A surfaces this as a warning footnote on the
48    /// `MutationResult.description`; Phase 4-B will add the transitive
49    /// walk that decides whether the cascade actually leaves the
50    /// current workspace.
51    pub is_public: bool,
52}
53
54/// Scan every cross-crate caller of `trait_id` and classify each by
55/// caller-side rewrite pattern.
56///
57/// "Cross-crate" means the caller's path resolves to a different
58/// crate from the trait. Same-crate callers are intentionally
59/// ignored — the existing single-crate executor already handles
60/// those (or the v3.1 EXTERNAL_USE filter wholesale-skips the
61/// suggest, depending on the call site).
62///
63/// Returns an empty `Vec` when:
64/// - `trait_id` is not in the registry or has no name,
65/// - no cross-crate container references the trait,
66/// - every cross-crate caller's AST is missing or is not a `PureFn`
67///   (Phase 2c covers only function callers; struct / impl callers
68///   arrive in Phase 2d alongside the rewrite engine).
69///
70/// Takes a `SymbolRegistry` + `ASTRegistry` directly so it can be
71/// invoked from either an `AnalysisContext` (full pipeline) or an
72/// `ASTMutationContext` (executor `apply_to_registry` entry point),
73/// without depending on `detail_store` / `typeflow_graph` (which
74/// `ASTMutationContext` does not carry).
75pub fn scan_cross_crate_callers_raw(
76    ast_registry: &ASTRegistry,
77    symbol_registry: &SymbolRegistry,
78    trait_id: SymbolId,
79) -> Vec<CallerRewrite> {
80    let trait_crate = match symbol_registry.path(trait_id) {
81        Some(p) => p.crate_name().to_string(),
82        None => return Vec::new(),
83    };
84    let trait_name = match symbol_registry.path(trait_id) {
85        Some(p) => p.name().to_string(),
86        None => return Vec::new(),
87    };
88    if trait_crate.is_empty() || trait_name.is_empty() {
89        return Vec::new();
90    }
91
92    // Pull trait method names directly off the `PureTrait` AST so this
93    // helper does not need a `DetailStore` (which `ASTMutationContext`
94    // does not carry).
95    let trait_methods: Vec<String> = match ast_registry.get(trait_id) {
96        Some(PureItem::Trait(t)) => t
97            .items
98            .iter()
99            .filter_map(|it| match it {
100                PureTraitItem::Fn(f) => Some(f.name.clone()),
101                _ => None,
102            })
103            .collect(),
104        _ => Vec::new(),
105    };
106
107    // Brute-force scan: iterate every Function symbol in the registry,
108    // keep only those whose crate differs from the trait's crate, then
109    // classify via `classify_fn` against the trait name/methods.
110    //
111    // Rationale (Phase 0 caveat, RL060 epic): typeflow.type_users drops
112    // the container for `WhereBound` / `ParamType` / `ReturnType`
113    // usages, which is exactly where cross-crate callers live (generic
114    // bound + `&dyn Trait` parameter + return type). Brute-force
115    // registry iteration sidesteps the typeflow gap.
116    let mut out = Vec::new();
117    let fn_ids: Vec<SymbolId> = symbol_registry
118        .iter()
119        .filter(|(id, path)| {
120            matches!(symbol_registry.kind(*id), Some(SymbolKind::Function))
121                && path.crate_name() != trait_crate
122        })
123        .map(|(id, _)| id)
124        .collect();
125
126    for caller_id in fn_ids {
127        if let Some(PureItem::Fn(f)) = ast_registry.get(caller_id) {
128            let patterns = classify_fn(f, &trait_name, &trait_methods);
129            if !patterns.is_empty() {
130                let is_public = matches!(f.vis, PureVis::Public);
131                out.push(CallerRewrite {
132                    caller_id,
133                    patterns,
134                    is_public,
135                });
136            }
137        }
138    }
139    out
140}
141
142/// Convenience wrapper that reads the `ASTRegistry` and
143/// `SymbolRegistry` off an `AnalysisContext`. Used by the integration
144/// pin and by any caller that already holds a full analysis context.
145pub fn scan_cross_crate_callers(ctx: &AnalysisContext, trait_id: SymbolId) -> Vec<CallerRewrite> {
146    scan_cross_crate_callers_raw(&ctx.ast_registry, &ctx.registry, trait_id)
147}
148
149/// Phase 5-B: symmetric counterpart to [`scan_cross_crate_callers_raw`]
150/// that returns the callers living in the **same** crate as the trait
151/// (excluding the implementor's own impl block and any of its method
152/// children). Used by the suggester to detect the mixed-handling case
153/// where same-crate external use coexists with cross-crate callers —
154/// in that situation the Phase 3 gate must fall through to the v3.1
155/// EXTERNAL_USE filter chain rather than bypassing it.
156///
157/// `internal_ids` is the set of symbols that should NOT count as
158/// external callers (typically `impl_id` plus the `children_of`
159/// the impl block). The Phase 3 gate computes this set once and
160/// passes it to both this helper and the existing typeflow walk.
161pub fn scan_same_crate_external_callers_raw(
162    ast_registry: &ASTRegistry,
163    symbol_registry: &SymbolRegistry,
164    trait_id: SymbolId,
165    internal_ids: &HashSet<SymbolId>,
166) -> Vec<CallerRewrite> {
167    let trait_crate = match symbol_registry.path(trait_id) {
168        Some(p) => p.crate_name().to_string(),
169        None => return Vec::new(),
170    };
171    let trait_name = match symbol_registry.path(trait_id) {
172        Some(p) => p.name().to_string(),
173        None => return Vec::new(),
174    };
175    if trait_crate.is_empty() || trait_name.is_empty() {
176        return Vec::new();
177    }
178
179    let trait_methods: Vec<String> = match ast_registry.get(trait_id) {
180        Some(PureItem::Trait(t)) => t
181            .items
182            .iter()
183            .filter_map(|it| match it {
184                PureTraitItem::Fn(f) => Some(f.name.clone()),
185                _ => None,
186            })
187            .collect(),
188        _ => Vec::new(),
189    };
190
191    let mut out = Vec::new();
192    let fn_ids: Vec<SymbolId> = symbol_registry
193        .iter()
194        .filter(|(id, path)| {
195            matches!(symbol_registry.kind(*id), Some(SymbolKind::Function))
196                && path.crate_name() == trait_crate
197                && !internal_ids.contains(id)
198        })
199        .map(|(id, _)| id)
200        .collect();
201
202    for caller_id in fn_ids {
203        if let Some(PureItem::Fn(f)) = ast_registry.get(caller_id) {
204            let patterns = classify_fn(f, &trait_name, &trait_methods);
205            if !patterns.is_empty() {
206                let is_public = matches!(f.vis, PureVis::Public);
207                out.push(CallerRewrite {
208                    caller_id,
209                    patterns,
210                    is_public,
211                });
212            }
213        }
214    }
215    out
216}
217
218/// Convenience wrapper for [`scan_same_crate_external_callers_raw`]
219/// that takes an `AnalysisContext`.
220pub fn scan_same_crate_external_callers(
221    ctx: &AnalysisContext,
222    trait_id: SymbolId,
223    internal_ids: &HashSet<SymbolId>,
224) -> Vec<CallerRewrite> {
225    scan_same_crate_external_callers_raw(&ctx.ast_registry, &ctx.registry, trait_id, internal_ids)
226}
227
228// ============================================================================
229// Phase 4-B — transitive caller cascade verdict
230// ============================================================================
231
232/// Result of [`walk_transitive_callers`] — whether the cascade from a
233/// pub GenericBound caller can be safely rewritten given the in-
234/// workspace call graph.
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum CascadeVerdict {
237    /// Every transitive caller in the workspace eventually terminates
238    /// at a non-pub leaf (or has no callers and is itself non-pub).
239    /// The GenericBound rewrite can safely commit.
240    Safe,
241    /// At least one transitive caller is a pub fn whose own callers
242    /// are not visible in the workspace registry — i.e. an external
243    /// crate may reach it. The GenericBound rewrite must abort for
244    /// that caller chain to avoid silently breaking the downstream
245    /// API contract.
246    Escapes,
247}
248
249/// BFS over `code_graph.callers_of` starting at `root_caller_id` to
250/// decide whether the transitive caller cascade ever surfaces a pub
251/// API leaf with no in-workspace caller.
252///
253/// Definition: a *leaf* is a symbol whose own callers_of iterator
254/// returns nothing. If the leaf's AST visibility is `PureVis::Public`,
255/// that means an external crate could call into the cascade and would
256/// be broken by the GenericBound rewrite — verdict is `Escapes`.
257/// Otherwise the leaf is private (or non-`Public` like `pub(crate)` /
258/// `pub(super)`) and the cascade stays inside the workspace — verdict
259/// is `Safe` for that branch. The walk continues until either an
260/// `Escapes` is found (early-return) or the queue empties (`Safe`).
261///
262/// Visited set guards against cycles. The root itself is included in
263/// the leaf check, so a pub fn with zero callers immediately returns
264/// `Escapes`.
265pub fn walk_transitive_callers(
266    code_graph: &CodeGraphV2,
267    ast_registry: &ASTRegistry,
268    root_caller_id: SymbolId,
269) -> CascadeVerdict {
270    let mut visited: HashSet<SymbolId> = HashSet::new();
271    let mut queue: VecDeque<SymbolId> = VecDeque::new();
272    queue.push_back(root_caller_id);
273    visited.insert(root_caller_id);
274
275    while let Some(id) = queue.pop_front() {
276        let callers: Vec<SymbolId> = code_graph.callers_of(id).collect();
277
278        if callers.is_empty() {
279            // Leaf: if its AST is a `pub fn`, it may be reached from
280            // an external crate. Otherwise the cascade is workspace-
281            // internal for this branch.
282            if let Some(PureItem::Fn(f)) = ast_registry.get(id) {
283                if matches!(f.vis, PureVis::Public) {
284                    return CascadeVerdict::Escapes;
285                }
286            }
287            continue;
288        }
289
290        for caller in callers {
291            if visited.insert(caller) {
292                queue.push_back(caller);
293            }
294        }
295    }
296
297    CascadeVerdict::Safe
298}