Skip to main content

ryo_analysis/query/
derive_index.rs

1//! DeriveIndex: Fast lookup for derive trait validation.
2//!
3//! This index pre-computes the relationship between types and their derive traits,
4//! enabling O(1) lookup for derive possibility checks instead of O(N) AST traversal.
5
6use super::{CodeGraphV2, StdImplCache, TypeFlowGraphV2};
7use crate::ast::ASTRegistry;
8use crate::symbol::SymbolRegistry;
9use crate::SymbolId;
10use ryo_source::pure::{PureAttrMeta, PureFields, PureItem, PureType};
11use serde::Serialize;
12use slotmap::SecondaryMap;
13use smallvec::SmallVec;
14
15/// Index for fast derive trait validation.
16///
17/// # Memory Layout
18/// ```text
19/// DeriveIndex
20/// ├── symbol_derives: SecondaryMap<SymbolId, SmallVec<[String; 4]>>
21/// │   └── struct/enum → derive trait names (e.g., ["Debug", "Clone"])
22/// └── field_type_names: SecondaryMap<SymbolId, SmallVec<[String; 8]>>
23///     └── struct/enum → field type names (for derive validation)
24/// ```
25#[derive(Clone, Default, Debug, Serialize)]
26pub struct DeriveIndex {
27    /// struct/enum SymbolId → derive trait names.
28    /// Most types derive 2-4 traits, so SmallVec<[String; 4]> is optimal.
29    symbol_derives: SecondaryMap<SymbolId, SmallVec<[String; 4]>>,
30
31    /// struct/enum SymbolId → field type names.
32    /// Used for checking if all fields implement the trait.
33    field_type_names: SecondaryMap<SymbolId, SmallVec<[String; 8]>>,
34}
35
36impl DeriveIndex {
37    /// Create a new empty index.
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Build the index from ASTRegistry, CodeGraph, TypeFlow, and SymbolRegistry.
43    pub fn build(
44        ast_registry: &ASTRegistry,
45        code_graph: &CodeGraphV2,
46        typeflow: &TypeFlowGraphV2,
47        symbol_registry: &SymbolRegistry,
48    ) -> Self {
49        let mut index = Self::new();
50        index.rebuild_all(ast_registry, code_graph, typeflow, symbol_registry);
51        index
52    }
53
54    /// Rebuild the entire index.
55    pub fn rebuild_all(
56        &mut self,
57        ast_registry: &ASTRegistry,
58        code_graph: &CodeGraphV2,
59        typeflow: &TypeFlowGraphV2,
60        symbol_registry: &SymbolRegistry,
61    ) {
62        self.symbol_derives.clear();
63        self.field_type_names.clear();
64
65        for (id, item) in ast_registry.iter() {
66            self.index_item(id, item, code_graph, typeflow, symbol_registry);
67        }
68    }
69
70    /// Incrementally update for specific symbols.
71    ///
72    /// This is O(S) where S is the number of affected symbols,
73    /// instead of O(N) for full rebuild.
74    pub fn rebuild_for_symbols(
75        &mut self,
76        symbols: &[SymbolId],
77        ast_registry: &ASTRegistry,
78        code_graph: &CodeGraphV2,
79        typeflow: &TypeFlowGraphV2,
80        symbol_registry: &SymbolRegistry,
81    ) {
82        for &symbol_id in symbols {
83            // Remove old entries
84            self.symbol_derives.remove(symbol_id);
85            self.field_type_names.remove(symbol_id);
86
87            // Re-index if the symbol still exists
88            if let Some(item) = ast_registry.get(symbol_id) {
89                self.index_item(symbol_id, item, code_graph, typeflow, symbol_registry);
90            }
91        }
92    }
93
94    /// Index a single item.
95    fn index_item(
96        &mut self,
97        id: SymbolId,
98        item: &PureItem,
99        code_graph: &CodeGraphV2,
100        typeflow: &TypeFlowGraphV2,
101        symbol_registry: &SymbolRegistry,
102    ) {
103        let attrs = match item {
104            PureItem::Struct(s) => &s.attrs,
105            PureItem::Enum(e) => &e.attrs,
106            _ => return,
107        };
108
109        // Extract derive traits
110        let mut derives: SmallVec<[String; 4]> = SmallVec::new();
111        for attr in attrs {
112            if attr.path == "derive" {
113                if let PureAttrMeta::List(args) = &attr.meta {
114                    for trait_name in args.split(',').map(|s| s.trim()) {
115                        if !trait_name.is_empty() {
116                            derives.push(trait_name.to_string());
117                        }
118                    }
119                }
120            }
121        }
122
123        if !derives.is_empty() {
124            self.symbol_derives.insert(id, derives);
125        }
126
127        // Extract field type names from TypeFlow.
128        //
129        // Bug fix (precheck-fix-1): `code_graph.children_of(struct_id)` returns
130        // ALL graph children — for `pub struct StmtConverter;` this includes
131        // every method defined in `impl StmtConverter { ... }`. Pulling their
132        // return types in as "field types" caused `verify_precheck` to
133        // fabricate phantom fields (e.g. `[Result, Result, Result, Result]`)
134        // for unit structs and fail their `Default` derive check, which in
135        // turn rejected every speculative mutation at baseline.
136        //
137        // Filter to children whose `SymbolKind` is actually `Field` so we
138        // only walk struct fields (and enum variant fields, via SymbolKind),
139        // not methods or other associated items.
140        let mut field_types: SmallVec<[String; 8]> = SmallVec::new();
141        for child_id in code_graph.children_of(id) {
142            // Only consider true fields — skip methods, consts, associated
143            // types, and any other non-field children.
144            if !matches!(
145                symbol_registry.kind(child_id),
146                Some(crate::SymbolKind::Field)
147            ) {
148                continue;
149            }
150            // Get the type that this field uses (via TypeFlow)
151            for use_id in typeflow.types_used_by(child_id) {
152                if let Some(path) = symbol_registry.resolve(use_id) {
153                    field_types.push(path.name().to_string());
154                    break; // Only first type reference
155                }
156            }
157        }
158
159        // Augment with field type names extracted directly from the AST.
160        //
161        // The typeflow-driven path above never reports primitives
162        // (`builder_typeflow_v2.rs:906-907` deliberately skips them) and rarely
163        // reports std containers (`Vec`, `HashMap`, …) because their
164        // `SymbolId` is not registered for arbitrary user crates.
165        //
166        // Additionally, the typeflow path may fail to resolve user-defined
167        // types that are defined in the same file but whose `SymbolId` was not
168        // registered in the typeflow graph at indexing time (e.g. a bare
169        // `pub struct Bar;` used as a field type in the same crate).
170        //
171        // The AST-direct augmentation below closes both gaps:
172        // - For primitive / std-container heads: push via `walk_type_for_std_heads`.
173        // - For user-defined heads (not primitive, not std-container): push via
174        //   `walk_type_for_user_heads` so that `verify_precheck` can apply the
175        //   conservative user-defined-type check on them as well.
176        // Deduplication is applied in both cases.
177        let std_impls = StdImplCache::default();
178        let mut push_if_relevant = |field_types: &mut SmallVec<[String; 8]>, ty: &PureType| {
179            // Primitive / std-container heads.
180            walk_type_for_std_heads(ty, &std_impls, &mut |head| {
181                if !field_types.iter().any(|existing| existing == &head) {
182                    field_types.push(head);
183                }
184            });
185            // User-defined heads (complement to typeflow path).
186            walk_type_for_user_heads(ty, &std_impls, &mut |head| {
187                if !field_types.iter().any(|existing| existing == &head) {
188                    field_types.push(head);
189                }
190            });
191        };
192        match item {
193            PureItem::Struct(s) => {
194                collect_field_types_from_fields(&s.fields, &mut field_types, &mut push_if_relevant)
195            }
196            PureItem::Enum(e) => {
197                for variant in &e.variants {
198                    collect_field_types_from_fields(
199                        &variant.fields,
200                        &mut field_types,
201                        &mut push_if_relevant,
202                    );
203                }
204            }
205            _ => {}
206        }
207
208        if !field_types.is_empty() {
209            self.field_type_names.insert(id, field_types);
210        }
211    }
212
213    // ========================================================================
214    // Query Methods
215    // ========================================================================
216
217    /// Test-only helper: directly insert a `(symbol → derives, field_types)`
218    /// entry, bypassing the typeflow-driven `rebuild_*` path.
219    ///
220    /// This exists so callers can construct synthetic inputs that exercise
221    /// downstream consumers of the index (notably
222    /// `ryo_verification::GraphVerifier::verify_precheck`) at boundary
223    /// conditions that the natural build path filters out — for example a
224    /// `#[derive(Hash)]` struct whose field type is a primitive (`f32`), which
225    /// `builder_typeflow_v2.rs` skips during typeflow population
226    /// (`// Skip primitives - only add usages for user-defined types`) and
227    /// therefore never reaches the verifier's reject branch organically.
228    ///
229    /// Production code MUST NOT call this. The method is `#[doc(hidden)]` to
230    /// keep it out of the rendered API surface.
231    #[doc(hidden)]
232    pub fn insert_for_test(
233        &mut self,
234        id: SymbolId,
235        derives: impl IntoIterator<Item = String>,
236        field_types: impl IntoIterator<Item = String>,
237    ) {
238        let derives: SmallVec<[String; 4]> = derives.into_iter().collect();
239        let field_types: SmallVec<[String; 8]> = field_types.into_iter().collect();
240        if !derives.is_empty() {
241            self.symbol_derives.insert(id, derives);
242        }
243        if !field_types.is_empty() {
244            self.field_type_names.insert(id, field_types);
245        }
246    }
247
248    /// Iterate over all symbols and their derives.
249    pub fn iter_derives(&self) -> impl Iterator<Item = (SymbolId, &SmallVec<[String; 4]>)> {
250        self.symbol_derives.iter()
251    }
252
253    /// Get derive traits for a symbol.
254    pub fn get_derives(&self, id: SymbolId) -> Option<&SmallVec<[String; 4]>> {
255        self.symbol_derives.get(id)
256    }
257
258    /// Get field type names for a symbol.
259    pub fn get_field_types(&self, id: SymbolId) -> Option<&SmallVec<[String; 8]>> {
260        self.field_type_names.get(id)
261    }
262
263    /// Check if a symbol has a specific derive trait.
264    pub fn has_derive(&self, id: SymbolId, trait_name: &str) -> bool {
265        self.symbol_derives
266            .get(id)
267            .map(|derives| derives.iter().any(|d| d == trait_name))
268            .unwrap_or(false)
269    }
270
271    /// Get all symbols that derive a specific trait.
272    pub fn symbols_deriving(&self, trait_name: &str) -> Vec<SymbolId> {
273        self.symbol_derives
274            .iter()
275            .filter(|(_, derives)| derives.iter().any(|d| d == trait_name))
276            .map(|(id, _)| id)
277            .collect()
278    }
279
280    /// Get statistics about the index.
281    pub fn stats(&self) -> DeriveIndexStats {
282        let total_derives: usize = self.symbol_derives.values().map(|v| v.len()).sum();
283        let total_fields: usize = self.field_type_names.values().map(|v| v.len()).sum();
284
285        DeriveIndexStats {
286            symbols_with_derives: self.symbol_derives.len(),
287            total_derives,
288            symbols_with_fields: self.field_type_names.len(),
289            total_field_types: total_fields,
290        }
291    }
292}
293
294/// Walk a `PureFields` and feed each field's `PureType` to `push`.
295///
296/// Handles the three field shapes — named (`{ x: T }`), tuple (`(T, U)`), and
297/// unit — so callers don't have to repeat the match. Used by `index_item` for
298/// both struct fields and enum variant fields.
299fn collect_field_types_from_fields(
300    fields: &PureFields,
301    field_types: &mut SmallVec<[String; 8]>,
302    push: &mut dyn FnMut(&mut SmallVec<[String; 8]>, &PureType),
303) {
304    match fields {
305        PureFields::Named(named) => {
306            for f in named {
307                push(field_types, &f.ty);
308            }
309        }
310        PureFields::Tuple(tuple_fields) => {
311            for f in tuple_fields {
312                push(field_types, &f.ty);
313            }
314        }
315        PureFields::Unit => {}
316    }
317}
318
319/// Walk a `PureType`, surfacing each primitive- or std-container-typed sub-term
320/// to `sink`. Recurses into the structural composite variants so a `&f32` or
321/// `(f32, u32)` field has every relevant inner type reach the derive verifier.
322///
323/// Variants handled:
324/// - `PureType::Path(p)` — extracts the head name (`p` stripped of generics
325///   and module path), pushes when `StdImplCache::is_primitive` or
326///   `is_std_container` accepts it. Examples:
327///   - `f32` → `f32`
328///   - `std::collections::HashMap<K, V>` → `HashMap` (note: the head walk does
329///     **not** descend into the generic arguments encoded inside the string;
330///     see [Limitations] below).
331///   - `my_crate::Config` → nothing (user-defined; the typeflow path covers
332///     these via `SymbolId`).
333/// - `PureType::Ref { ty, .. }` — recurses into the referent (`&T: Hash`
334///   requires `T: Hash`, so primitives behind a reference still matter).
335/// - `PureType::Tuple(types)` — recurses into each element (`(A, B): Hash`
336///   requires every element to impl `Hash`).
337/// - `PureType::Array { ty, .. }` / `PureType::Slice(ty)` — recurses into the
338///   element type.
339/// - Other variants (`Fn`, `ImplTrait`, `TraitObject`, `Infer`, `Never`,
340///   `Other`) are deliberately skipped: they either can't appear as plain
341///   struct fields or carry trait bounds that the precheck can't reason about
342///   from the AST alone.
343///
344/// # Limitations
345///
346/// Generic arguments encoded inside a `PureType::Path` string (e.g. the `f32`
347/// in `Box<f32>`) are **not** descended into. Doing so would require parsing
348/// the type-string ourselves; that is left for a follow-up. The head walk
349/// still pushes `Box`, and `Box<f32>: Hash` happens to be safe to skip because
350/// `StdImplCache` advertises `Box: Hash` and the precheck does not yet inspect
351/// the conditional bound.
352fn walk_type_for_std_heads(ty: &PureType, std_impls: &StdImplCache, sink: &mut dyn FnMut(String)) {
353    match ty {
354        PureType::Path(p) => {
355            let pre_generic = p.split('<').next().unwrap_or(p).trim();
356            let head = pre_generic
357                .rsplit("::")
358                .next()
359                .unwrap_or(pre_generic)
360                .trim();
361            if !head.is_empty()
362                && (std_impls.is_primitive(head) || std_impls.is_std_container(head))
363            {
364                sink(head.to_string());
365            }
366        }
367        PureType::Ref { ty, .. } => walk_type_for_std_heads(ty, std_impls, sink),
368        PureType::Tuple(types) => {
369            for t in types {
370                walk_type_for_std_heads(t, std_impls, sink);
371            }
372        }
373        PureType::Array { ty, .. } => walk_type_for_std_heads(ty, std_impls, sink),
374        PureType::Slice(inner) => walk_type_for_std_heads(inner, std_impls, sink),
375        // Fn / ImplTrait / TraitObject / Infer / Never / Other: nothing to
376        // contribute to the precheck reject branch from the AST surface alone.
377        _ => {}
378    }
379}
380
381/// Walk a `PureType`, surfacing each **user-defined** (non-primitive,
382/// non-std-container) head type name to `sink`.
383///
384/// This is the complement of [`walk_type_for_std_heads`]: where that function
385/// only emits names accepted by `StdImplCache::is_primitive` /
386/// `is_std_container`, this function emits names that are neither — i.e.
387/// user-defined types that the typeflow path might have failed to resolve for
388/// a given workspace.
389///
390/// The same structural recursion rules apply (Ref, Tuple, Array, Slice).
391/// Generic arguments inside `PureType::Path` strings are not descended into
392/// (same limitation as `walk_type_for_std_heads`).
393///
394/// Names starting with a lowercase letter that look like lifetimes or generic
395/// parameters (single char, or all-lowercase short identifiers that are common
396/// generic names) are conservatively skipped to avoid treating `T`, `K`, `V`,
397/// `E`, etc. as user-defined struct names.
398fn walk_type_for_user_heads(ty: &PureType, std_impls: &StdImplCache, sink: &mut dyn FnMut(String)) {
399    match ty {
400        PureType::Path(p) => {
401            let pre_generic = p.split('<').next().unwrap_or(p).trim();
402            let head = pre_generic
403                .rsplit("::")
404                .next()
405                .unwrap_or(pre_generic)
406                .trim();
407            if head.is_empty() || std_impls.is_primitive(head) || std_impls.is_std_container(head) {
408                return; // handled by walk_type_for_std_heads or skip
409            }
410            // Conservative filter: skip single-char identifiers and common
411            // generic parameter names to avoid treating lifetime / type params
412            // as user-defined struct names.
413            if is_likely_generic_param(head) {
414                return;
415            }
416            sink(head.to_string());
417        }
418        PureType::Ref { ty, .. } => walk_type_for_user_heads(ty, std_impls, sink),
419        PureType::Tuple(types) => {
420            for t in types {
421                walk_type_for_user_heads(t, std_impls, sink);
422            }
423        }
424        PureType::Array { ty, .. } => walk_type_for_user_heads(ty, std_impls, sink),
425        PureType::Slice(inner) => walk_type_for_user_heads(inner, std_impls, sink),
426        _ => {}
427    }
428}
429
430/// Return true for identifiers that are almost certainly type/lifetime
431/// parameters rather than user-defined struct / enum names.
432///
433/// Heuristic:
434/// - Single ASCII letter (e.g. `T`, `K`, `V`, `E`, `F`, `N`).
435/// - Two-char identifiers like `Ok`, `Err` are legitimate types and must NOT
436///   be filtered; single-char is the primary filter.
437/// - Common multi-char generic param names (`Self`, `Item`, `Output`,
438///   `Error`) that appear frequently in trait bounds are kept so that a struct
439///   literally named `Item` would still be captured — these are uncommon
440///   enough that the FP risk is low.
441fn is_likely_generic_param(name: &str) -> bool {
442    let bytes = name.as_bytes();
443    // Single ASCII letter.
444    bytes.len() == 1 && bytes[0].is_ascii_alphabetic()
445}
446
447/// Statistics about the DeriveIndex.
448#[derive(Debug, Clone)]
449pub struct DeriveIndexStats {
450    /// Number of distinct symbols that carry at least one `#[derive(...)]`.
451    pub symbols_with_derives: usize,
452    /// Sum of derive entries across all symbols (counts duplicates per
453    /// symbol).
454    pub total_derives: usize,
455    /// Number of distinct symbols that have at least one indexed field
456    /// type.
457    pub symbols_with_fields: usize,
458    /// Sum of indexed field-type entries across all symbols.
459    pub total_field_types: usize,
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn test_derive_index_creation() {
468        let index = DeriveIndex::new();
469        assert_eq!(index.stats().symbols_with_derives, 0);
470    }
471
472    /// Regression test (precheck-fix-1):
473    /// `code_graph.children_of(struct_id)` returns every graph child, which
474    /// for a unit struct used to include all methods on an `impl` block.
475    /// Those methods' return types were then captured as "field types",
476    /// causing `verify_precheck` to fabricate phantom fields (e.g.
477    /// `[Result, Result, ...]`) for unit structs and to fail their derive
478    /// check at baseline, rejecting every speculative mutation. The fix
479    /// filters `children_of` to `SymbolKind::Field` only — this test locks
480    /// down that filter.
481    #[cfg(feature = "testing")]
482    #[test]
483    fn test_unit_struct_with_impl_methods_has_no_field_types() {
484        use crate::testing::ContextBuilder;
485        let source = r#"
486#[derive(Debug, Clone, Default)]
487pub struct UnitWithMethods;
488
489impl UnitWithMethods {
490    pub fn parse_a() -> Result<i32, String> { Ok(0) }
491    pub fn parse_b() -> Result<i32, String> { Ok(0) }
492}
493"#;
494        let ctx = ContextBuilder::new()
495            .with_file("src/lib.rs", source)
496            .build();
497
498        // Find the struct's SymbolId
499        let struct_id = ctx
500            .registry
501            .iter()
502            .find(|(_, p)| p.name() == "UnitWithMethods")
503            .map(|(id, _)| id)
504            .expect("UnitWithMethods symbol should exist");
505
506        // The struct has at least one derive recorded.
507        let derives = ctx
508            .derive_index
509            .get_derives(struct_id)
510            .expect("derives should be indexed");
511        assert!(
512            derives.iter().any(|d| d == "Default"),
513            "Default should be among the derives, got {:?}",
514            derives
515        );
516
517        // The struct is a unit struct → no real fields → field_type_names
518        // must be empty (None) instead of leaking method return types.
519        assert!(
520            ctx.derive_index.get_field_types(struct_id).is_none(),
521            "unit struct must not record any field types, got {:?}",
522            ctx.derive_index.get_field_types(struct_id)
523        );
524    }
525}