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}