Skip to main content

ryo_analysis/
detail_store.rs

1//! DetailStore - Cached symbol details for O(1) access.
2//!
3//! Extracts and caches detailed information from PureAST for fast lookup.
4//! Updated at Tick boundaries when symbols are modified.
5//!
6//! # Design
7//!
8//! - **Tick内読み取り**: O(1) via SecondaryMap
9//! - **Tick境界更新**: rebuild_affected() for changed symbols only
10//! - **Single Source**: PureFile remains the source of truth
11//!
12//! # Example
13//!
14//! ```ignore
15//! let ctx = AnalysisContext::from_path_files(files, "my_crate");
16//!
17//! // O(1) access during Tick
18//! if let Some(detail) = ctx.detail_store.function(symbol_id) {
19//!     println!("is_async: {}", detail.is_async);
20//! }
21//!
22//! // At Tick boundary
23//! ctx.detail_store.rebuild_affected(&changed_ids, &ctx.registry, &ctx.files);
24//! ```
25
26use crate::ast::ASTRegistry;
27use crate::context::ImHashMap;
28use crate::symbol::{SymbolId, SymbolRegistry};
29use crate::SymbolKind;
30use ryo_source::pure::{
31    PureEnum, PureFields, PureFile, PureFn, PureGenerics, PureImpl, PureItem, PureParam,
32    PureStruct, PureTrait, PureType, PureVis,
33};
34use ryo_symbol::{SymbolPathResolver, WorkspaceFilePath};
35use serde::{Deserialize, Serialize};
36use slotmap::SecondaryMap;
37use std::collections::HashMap;
38use std::sync::Arc;
39
40// ============================================================================
41// Common Types
42// ============================================================================
43
44/// Generic parameters info (simplified).
45#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
46pub struct GenericInfo {
47    /// Type parameters: ["T", "U"]
48    pub type_params: Vec<String>,
49    /// Lifetimes: ["'a", "'b"]
50    pub lifetimes: Vec<String>,
51    /// Const parameters: [("N", "usize")]
52    pub const_params: Vec<(String, String)>,
53}
54
55impl GenericInfo {
56    /// Check if there are no generics.
57    pub fn is_empty(&self) -> bool {
58        self.type_params.is_empty() && self.lifetimes.is_empty() && self.const_params.is_empty()
59    }
60}
61
62/// Parameter info for functions/methods.
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct ParamInfo {
65    /// Parameter name.
66    pub name: String,
67    /// Type as string.
68    pub ty: String,
69    /// Is this a self parameter?
70    pub is_self: bool,
71    /// Is mutable (for &mut self)?
72    pub is_mut: bool,
73}
74
75/// Field info for structs/enums.
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct FieldInfo {
78    /// Field name.
79    pub name: String,
80    /// Type as string.
81    pub ty: String,
82    /// Is publicly visible?
83    pub is_public: bool,
84}
85
86// ============================================================================
87// Detail Types
88// ============================================================================
89
90/// Function/method details.
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct FunctionDetail {
93    // --- Modifiers ---
94    /// Is async fn?
95    pub is_async: bool,
96    /// Is const fn?
97    pub is_const: bool,
98    /// Is unsafe fn?
99    pub is_unsafe: bool,
100
101    // --- Signature ---
102    /// Parameters.
103    pub params: Vec<ParamInfo>,
104    /// Return type (None = unit).
105    pub return_type: Option<String>,
106    /// Generic parameters.
107    pub generics: GenericInfo,
108
109    // --- Method-specific ---
110    /// Is this a method (in impl/trait)?
111    pub is_method: bool,
112    /// Self type for methods (e.g., "Foo" in `impl Foo`).
113    pub self_ty: Option<String>,
114    /// Trait being implemented (e.g., "Clone" in `impl Clone for Foo`).
115    pub trait_impl: Option<String>,
116    /// Has self parameter (&self, &mut self, self).
117    pub has_self: bool,
118
119    // --- Attributes ---
120    /// Attribute paths (e.g., ["deprecated", "allow", "inline"]).
121    #[serde(default)]
122    pub attrs: Vec<String>,
123}
124
125/// Struct kind.
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
127pub enum StructKind {
128    /// Named fields: `struct Foo { x: i32 }`
129    Named,
130    /// Tuple struct: `struct Foo(i32, i32)`
131    Tuple,
132    /// Unit struct: `struct Foo;`
133    Unit,
134}
135
136/// Struct details.
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
138pub struct StructDetail {
139    /// Fields.
140    pub fields: Vec<FieldInfo>,
141    /// Struct kind.
142    pub kind: StructKind,
143    /// Generic parameters.
144    pub generics: GenericInfo,
145    /// Attribute paths (e.g., ["derive", "serde"]).
146    #[serde(default)]
147    pub attrs: Vec<String>,
148}
149
150/// Enum variant info.
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152pub struct VariantInfo {
153    /// Variant name.
154    pub name: String,
155    /// Fields (empty for unit variants).
156    pub fields: Vec<FieldInfo>,
157    /// Discriminant value if specified.
158    pub discriminant: Option<String>,
159}
160
161/// Enum details.
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163pub struct EnumDetail {
164    /// Variants.
165    pub variants: Vec<VariantInfo>,
166    /// Generic parameters.
167    pub generics: GenericInfo,
168    /// Attribute paths (e.g., ["derive", "repr"]).
169    #[serde(default)]
170    pub attrs: Vec<String>,
171}
172
173/// Trait details.
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175pub struct TraitDetail {
176    /// Is unsafe trait?
177    pub is_unsafe: bool,
178    /// Is auto trait?
179    pub is_auto: bool,
180    /// Supertraits.
181    pub supertraits: Vec<String>,
182    /// Method names.
183    pub methods: Vec<String>,
184    /// Associated type names.
185    pub types: Vec<String>,
186    /// Generic parameters.
187    pub generics: GenericInfo,
188    /// Attribute paths (e.g., ["async_trait"]).
189    #[serde(default)]
190    pub attrs: Vec<String>,
191}
192
193/// Impl block details.
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct ImplDetail {
196    /// Is unsafe impl?
197    pub is_unsafe: bool,
198    /// Self type being implemented.
199    pub self_ty: String,
200    /// Trait being implemented (None for inherent impl).
201    pub trait_: Option<String>,
202    /// Method names.
203    pub methods: Vec<String>,
204    /// Generic parameters.
205    pub generics: GenericInfo,
206    /// Attribute paths (e.g., ["automatically_derived"]).
207    #[serde(default)]
208    pub attrs: Vec<String>,
209}
210
211// ============================================================================
212// DetailStore
213// ============================================================================
214
215/// Cached symbol details for O(1) access.
216///
217/// # Thread Safety
218///
219/// DetailStore is designed for single-threaded access within a Tick.
220/// Updates happen at Tick boundaries under Executor control.
221///
222/// NOTE: Deserialize is NOT derived because DetailStore uses SecondaryMap<SymbolId, ...>
223/// and SymbolId is process-specific (SlotMap key). Serialization is supported for
224/// debugging/inspection purposes only.
225#[derive(Clone, Serialize)]
226pub struct DetailStore {
227    functions: SecondaryMap<SymbolId, FunctionDetail>,
228    structs: SecondaryMap<SymbolId, StructDetail>,
229    enums: SecondaryMap<SymbolId, EnumDetail>,
230    traits: SecondaryMap<SymbolId, TraitDetail>,
231    impls: SecondaryMap<SymbolId, ImplDetail>,
232}
233
234impl DetailStore {
235    /// Create a new empty store.
236    pub fn new() -> Self {
237        Self {
238            functions: SecondaryMap::new(),
239            structs: SecondaryMap::new(),
240            enums: SecondaryMap::new(),
241            traits: SecondaryMap::new(),
242            impls: SecondaryMap::new(),
243        }
244    }
245
246    // ========================================================================
247    // O(1) Accessors
248    // ========================================================================
249
250    /// Get function detail.
251    #[inline]
252    pub fn function(&self, id: SymbolId) -> Option<&FunctionDetail> {
253        self.functions.get(id)
254    }
255
256    /// Get struct detail.
257    #[inline]
258    pub fn struct_(&self, id: SymbolId) -> Option<&StructDetail> {
259        self.structs.get(id)
260    }
261
262    /// Get enum detail.
263    #[inline]
264    pub fn enum_(&self, id: SymbolId) -> Option<&EnumDetail> {
265        self.enums.get(id)
266    }
267
268    /// Get trait detail.
269    #[inline]
270    pub fn trait_(&self, id: SymbolId) -> Option<&TraitDetail> {
271        self.traits.get(id)
272    }
273
274    /// Get impl detail.
275    #[inline]
276    pub fn impl_(&self, id: SymbolId) -> Option<&ImplDetail> {
277        self.impls.get(id)
278    }
279
280    // ========================================================================
281    // Build & Update
282    // ========================================================================
283
284    /// Build DetailStore from WorkspaceFilePath-keyed files.
285    ///
286    /// **Deprecated**: Use `rebuild_for_symbols` for incremental updates from ASTRegistry.
287    /// This method is only needed for initial construction before ASTRegistry exists.
288    #[deprecated(
289        since = "0.1.0",
290        note = "Use rebuild_for_symbols() for incremental updates. This is only for initial construction."
291    )]
292    pub fn build_all_workspace(
293        registry: &SymbolRegistry,
294        files: &HashMap<WorkspaceFilePath, PureFile>,
295        crate_name: &str,
296    ) -> Self {
297        let mut store = Self::new();
298        let resolver = SymbolPathResolver::new(crate_name);
299
300        // Build module-to-file mapping for efficient lookup
301        // module_path → (&WorkspaceFilePath, &PureFile)
302        let module_file_map: HashMap<String, (&WorkspaceFilePath, &PureFile)> = files
303            .iter()
304            .map(|(path, file)| {
305                let mod_path = resolver.module_path_str(path);
306                (mod_path, (path, file))
307            })
308            .collect();
309
310        for (id, path) in registry.iter() {
311            if let Some(kind) = registry.kind(id) {
312                let symbol_name = path.name();
313                let parent_path = path.parent().map(|p| p.to_string()).unwrap_or_default();
314
315                // For methods in impl blocks, the parent is "<impl Type>" which doesn't match
316                // module paths. We need to go one level up to find the actual module.
317                let module_path = if parent_path.contains("<impl") {
318                    path.parent()
319                        .and_then(|p| p.parent())
320                        .map(|p| p.to_string())
321                        .unwrap_or_default()
322                } else {
323                    parent_path
324                };
325
326                // Find the file containing this symbol
327                if let Some((_path, file)) = module_file_map.get(&module_path) {
328                    store.extract_detail_direct(id, kind, symbol_name, file);
329                }
330            }
331        }
332
333        store
334    }
335
336    /// Build from Arc-wrapped files (for use with AnalysisContext.files).
337    ///
338    /// This variant accepts `ImHashMap<WorkspaceFilePath, Arc<PureFile>>` which
339    /// is the storage format used by AnalysisContext after fork-on-write.
340    ///
341    /// **Deprecated**: Use `rebuild_for_symbols` for incremental updates from ASTRegistry.
342    /// This method is only needed for initial construction before ASTRegistry exists.
343    #[deprecated(
344        since = "0.1.0",
345        note = "Use rebuild_for_symbols() for incremental updates. This is only for initial construction."
346    )]
347    pub fn build_from_arc_files(
348        registry: &SymbolRegistry,
349        files: &ImHashMap<WorkspaceFilePath, Arc<PureFile>>,
350        crate_name: &str,
351    ) -> Self {
352        let mut store = Self::new();
353        let resolver = SymbolPathResolver::new(crate_name);
354
355        // Build module-to-file mapping for efficient lookup
356        // module_path → &PureFile
357        let module_file_map: HashMap<String, &PureFile> = files
358            .iter()
359            .map(|(path, file)| {
360                let mod_path = resolver.module_path_str(path);
361                (mod_path, file.as_ref())
362            })
363            .collect();
364
365        for (id, path) in registry.iter() {
366            if let Some(kind) = registry.kind(id) {
367                let symbol_name = path.name();
368                let parent_path = path.parent().map(|p| p.to_string()).unwrap_or_default();
369
370                // For methods in impl blocks, the parent is "<impl Type>" which doesn't match
371                // module paths. We need to go one level up to find the actual module.
372                let module_path = if parent_path.contains("<impl") {
373                    path.parent()
374                        .and_then(|p| p.parent())
375                        .map(|p| p.to_string())
376                        .unwrap_or_default()
377                } else {
378                    parent_path
379                };
380
381                // Find the file containing this symbol
382                if let Some(file) = module_file_map.get(&module_path) {
383                    store.extract_detail_direct(id, kind, symbol_name, file);
384                }
385            }
386        }
387
388        store
389    }
390
391    /// Rebuild details for affected symbols (Tick boundary update).
392    ///
393    /// Uses WorkspaceFilePath-keyed files for the new architecture.
394    pub fn rebuild_affected_workspace(
395        &mut self,
396        affected: &[SymbolId],
397        registry: &SymbolRegistry,
398        files: &ImHashMap<WorkspaceFilePath, Arc<PureFile>>,
399        crate_name: &str,
400    ) {
401        let resolver = SymbolPathResolver::new(crate_name);
402
403        // Build module-to-file mapping for efficient lookup
404        let module_file_map: HashMap<String, &PureFile> = files
405            .iter()
406            .map(|(path, file)| {
407                let mod_path = resolver.module_path_str(path);
408                (mod_path, file.as_ref())
409            })
410            .collect();
411
412        for &id in affected {
413            // Remove old details
414            self.remove(id);
415
416            // Re-extract if symbol still exists
417            if let Some(kind) = registry.kind(id) {
418                if let Some(path) = registry.resolve(id) {
419                    let symbol_name = path.name();
420                    let parent_path = path.parent().map(|p| p.to_string()).unwrap_or_default();
421
422                    // For methods in impl blocks, go up to the module level
423                    let module_path = if parent_path.contains("<impl") {
424                        path.parent()
425                            .and_then(|p| p.parent())
426                            .map(|p| p.to_string())
427                            .unwrap_or_default()
428                    } else {
429                        parent_path
430                    };
431
432                    // Find the file and extract details
433                    if let Some(file) = module_file_map.get(&module_path) {
434                        self.extract_detail_direct(id, kind, symbol_name, file);
435                    }
436                }
437            }
438        }
439    }
440
441    /// Remove all details for a symbol.
442    pub fn remove(&mut self, id: SymbolId) {
443        self.functions.remove(id);
444        self.structs.remove(id);
445        self.enums.remove(id);
446        self.traits.remove(id);
447        self.impls.remove(id);
448    }
449
450    /// Rebuild details for affected symbols using ASTRegistry.
451    ///
452    /// This is the Symbol-based incremental update path that:
453    /// 1. Removes existing details for affected symbols
454    /// 2. Extracts new details directly from ASTRegistry (no file I/O)
455    ///
456    /// Note: Method details require impl block context, so they are
457    /// rebuilt from the parent impl block if available.
458    pub fn rebuild_for_symbols(&mut self, affected_ids: &[SymbolId], ast_registry: &ASTRegistry) {
459        for &id in affected_ids {
460            // Remove existing detail
461            self.remove(id);
462
463            // Extract new detail from ASTRegistry
464            if let Some(item) = ast_registry.get(id) {
465                self.extract_from_item(id, item);
466            }
467        }
468    }
469
470    /// Extract and store detail from a PureItem.
471    fn extract_from_item(&mut self, id: SymbolId, item: &PureItem) {
472        match item {
473            PureItem::Fn(f) => {
474                // Top-level function (no self_ty or trait_impl)
475                let detail = build_function_detail(f, None, None);
476                self.functions.insert(id, detail);
477            }
478            PureItem::Struct(s) => {
479                let detail = build_struct_detail(s);
480                self.structs.insert(id, detail);
481            }
482            PureItem::Enum(e) => {
483                let detail = build_enum_detail(e);
484                self.enums.insert(id, detail);
485            }
486            PureItem::Trait(t) => {
487                let detail = build_trait_detail(t);
488                self.traits.insert(id, detail);
489            }
490            PureItem::Impl(i) => {
491                let detail = build_impl_detail(i);
492                self.impls.insert(id, detail);
493            }
494            // Other item types don't have details
495            _ => {}
496        }
497    }
498
499    /// Extract and store detail directly from a file (without span lookup).
500    fn extract_detail_direct(
501        &mut self,
502        id: SymbolId,
503        kind: SymbolKind,
504        symbol_name: &str,
505        file: &PureFile,
506    ) {
507        match kind {
508            SymbolKind::Function => {
509                // トップレベル関数のみを検索
510                if let Some(detail) = extract_toplevel_function_detail(file, symbol_name) {
511                    self.functions.insert(id, detail);
512                }
513            }
514            SymbolKind::Method => {
515                // impl/trait ブロック内のメソッドのみを検索
516                if let Some(detail) = extract_method_detail(file, symbol_name) {
517                    self.functions.insert(id, detail);
518                }
519            }
520            SymbolKind::Struct => {
521                if let Some(detail) = extract_struct_detail(file, symbol_name) {
522                    self.structs.insert(id, detail);
523                }
524            }
525            SymbolKind::Enum => {
526                if let Some(detail) = extract_enum_detail(file, symbol_name) {
527                    self.enums.insert(id, detail);
528                }
529            }
530            SymbolKind::Trait => {
531                if let Some(detail) = extract_trait_detail(file, symbol_name) {
532                    self.traits.insert(id, detail);
533                }
534            }
535            SymbolKind::Impl => {
536                if let Some(detail) = extract_impl_detail(file, symbol_name) {
537                    self.impls.insert(id, detail);
538                }
539            }
540            _ => {}
541        }
542    }
543
544    // ========================================================================
545    // Statistics
546    // ========================================================================
547
548    /// Get total number of cached details.
549    pub fn len(&self) -> usize {
550        self.functions.len()
551            + self.structs.len()
552            + self.enums.len()
553            + self.traits.len()
554            + self.impls.len()
555    }
556
557    /// Check if store is empty.
558    pub fn is_empty(&self) -> bool {
559        self.len() == 0
560    }
561}
562
563impl Default for DetailStore {
564    fn default() -> Self {
565        Self::new()
566    }
567}
568
569// ============================================================================
570// Extraction Functions
571// ============================================================================
572
573/// Convert PureGenerics to GenericInfo.
574fn convert_generics(generics: &PureGenerics) -> GenericInfo {
575    let mut info = GenericInfo::default();
576
577    for param in &generics.params {
578        match param {
579            ryo_source::pure::PureGenericParam::Type { name, .. } => {
580                info.type_params.push(name.clone());
581            }
582            ryo_source::pure::PureGenericParam::Lifetime { name, .. } => {
583                info.lifetimes.push(name.clone());
584            }
585            ryo_source::pure::PureGenericParam::Const { name, ty } => {
586                info.const_params.push((name.clone(), ty.clone()));
587            }
588        }
589    }
590
591    info
592}
593
594/// Convert PureType to string representation.
595fn type_to_string(ty: &PureType) -> String {
596    match ty {
597        PureType::Path(p) => p.clone(),
598        PureType::Ref {
599            lifetime,
600            is_mut,
601            ty,
602        } => {
603            let lt = lifetime
604                .as_ref()
605                .map(|l| format!("{} ", l))
606                .unwrap_or_default();
607            let m = if *is_mut { "mut " } else { "" };
608            format!("&{}{}{}", lt, m, type_to_string(ty))
609        }
610        PureType::Tuple(types) => {
611            let inner: Vec<_> = types.iter().map(type_to_string).collect();
612            format!("({})", inner.join(", "))
613        }
614        PureType::Array { ty, len } => format!("[{}; {}]", type_to_string(ty), len),
615        PureType::Slice(ty) => format!("[{}]", type_to_string(ty)),
616        PureType::Fn { params, ret } => {
617            let ps: Vec<_> = params.iter().map(type_to_string).collect();
618            let r = ret
619                .as_ref()
620                .map(|t| format!(" -> {}", type_to_string(t)))
621                .unwrap_or_default();
622            format!("fn({}){}", ps.join(", "), r)
623        }
624        PureType::ImplTrait(bounds) => format!("impl {}", bounds.join(" + ")),
625        PureType::TraitObject(bounds) => format!("dyn {}", bounds.join(" + ")),
626        PureType::Infer => "_".to_string(),
627        PureType::Never => "!".to_string(),
628        PureType::Other(s) => s.clone(),
629    }
630}
631
632/// Convert PureVis to is_public bool.
633fn is_public(vis: &PureVis) -> bool {
634    matches!(vis, PureVis::Public | PureVis::Crate)
635}
636
637/// Extract FunctionDetail for top-level functions only (not methods).
638fn extract_toplevel_function_detail(file: &PureFile, symbol_name: &str) -> Option<FunctionDetail> {
639    for item in &file.items {
640        if let PureItem::Fn(f) = item {
641            if f.name == symbol_name {
642                return Some(build_function_detail(f, None, None));
643            }
644        }
645    }
646    None
647}
648
649/// Extract FunctionDetail for methods in impl/trait blocks only.
650fn extract_method_detail(file: &PureFile, symbol_name: &str) -> Option<FunctionDetail> {
651    for item in &file.items {
652        // Check in impl blocks
653        if let PureItem::Impl(impl_block) = item {
654            for impl_item in &impl_block.items {
655                if let ryo_source::pure::PureImplItem::Fn(f) = impl_item {
656                    if f.name == symbol_name {
657                        return Some(build_function_detail(
658                            f,
659                            Some(&impl_block.self_ty),
660                            impl_block.trait_.as_deref(),
661                        ));
662                    }
663                }
664            }
665        }
666        // Check in trait blocks
667        if let PureItem::Trait(trait_block) = item {
668            for trait_item in &trait_block.items {
669                if let ryo_source::pure::PureTraitItem::Fn(f) = trait_item {
670                    if f.name == symbol_name {
671                        return Some(build_function_detail(f, None, Some(&trait_block.name)));
672                    }
673                }
674            }
675        }
676    }
677    None
678}
679
680#[cfg(test)]
681/// Extract FunctionDetail from PureFile by symbol name.
682fn extract_function_detail(
683    file: &PureFile,
684    symbol_name: &str,
685    self_ty: Option<&str>,
686    trait_impl: Option<&str>,
687) -> Option<FunctionDetail> {
688    // Find function by name
689    for item in &file.items {
690        if let PureItem::Fn(f) = item {
691            if f.name == symbol_name {
692                return Some(build_function_detail(f, self_ty, trait_impl));
693            }
694        }
695        // Check in impl blocks
696        if let PureItem::Impl(impl_block) = item {
697            for impl_item in &impl_block.items {
698                if let ryo_source::pure::PureImplItem::Fn(f) = impl_item {
699                    if f.name == symbol_name {
700                        return Some(build_function_detail(
701                            f,
702                            Some(&impl_block.self_ty),
703                            impl_block.trait_.as_deref(),
704                        ));
705                    }
706                }
707            }
708        }
709        // Check in trait blocks
710        if let PureItem::Trait(trait_block) = item {
711            for trait_item in &trait_block.items {
712                if let ryo_source::pure::PureTraitItem::Fn(f) = trait_item {
713                    if f.name == symbol_name {
714                        return Some(build_function_detail(f, None, Some(&trait_block.name)));
715                    }
716                }
717            }
718        }
719    }
720    None
721}
722
723/// Build FunctionDetail from PureFn.
724fn build_function_detail(
725    f: &PureFn,
726    self_ty: Option<&str>,
727    trait_impl: Option<&str>,
728) -> FunctionDetail {
729    let mut params = Vec::new();
730    let mut has_self = false;
731
732    for param in &f.params {
733        match param {
734            PureParam::SelfValue { is_ref, is_mut } => {
735                has_self = true;
736                let ty = if *is_ref {
737                    if *is_mut {
738                        "&mut Self"
739                    } else {
740                        "&Self"
741                    }
742                } else {
743                    "Self"
744                };
745                params.push(ParamInfo {
746                    name: "self".to_string(),
747                    ty: ty.to_string(),
748                    is_self: true,
749                    is_mut: *is_mut,
750                });
751            }
752            PureParam::Typed { name, ty } => {
753                params.push(ParamInfo {
754                    name: name.clone(),
755                    ty: type_to_string(ty),
756                    is_self: false,
757                    is_mut: false,
758                });
759            }
760        }
761    }
762
763    FunctionDetail {
764        is_async: f.is_async,
765        is_const: f.is_const,
766        is_unsafe: f.is_unsafe,
767        params,
768        return_type: f.ret.as_ref().map(type_to_string),
769        generics: convert_generics(&f.generics),
770        is_method: self_ty.is_some(),
771        self_ty: self_ty.map(String::from),
772        trait_impl: trait_impl.map(String::from),
773        has_self,
774        attrs: extract_attr_paths(&f.attrs),
775    }
776}
777
778/// Extract StructDetail from PureFile by symbol name.
779fn extract_struct_detail(file: &PureFile, symbol_name: &str) -> Option<StructDetail> {
780    for item in &file.items {
781        if let PureItem::Struct(s) = item {
782            if s.name == symbol_name {
783                return Some(build_struct_detail(s));
784            }
785        }
786    }
787    None
788}
789
790/// Build StructDetail from PureStruct.
791fn build_struct_detail(s: &PureStruct) -> StructDetail {
792    let (fields, kind) = match &s.fields {
793        PureFields::Named(fs) => {
794            let fields = fs
795                .iter()
796                .map(|f| FieldInfo {
797                    name: f.name.clone(),
798                    ty: type_to_string(&f.ty),
799                    is_public: is_public(&f.vis),
800                })
801                .collect();
802            (fields, StructKind::Named)
803        }
804        PureFields::Tuple(types) => {
805            let fields = types
806                .iter()
807                .enumerate()
808                .map(|(i, ty)| FieldInfo {
809                    name: i.to_string(),
810                    ty: type_to_string(ty),
811                    is_public: true, // tuple fields follow struct visibility
812                })
813                .collect();
814            (fields, StructKind::Tuple)
815        }
816        PureFields::Unit => (Vec::new(), StructKind::Unit),
817    };
818
819    StructDetail {
820        fields,
821        kind,
822        generics: convert_generics(&s.generics),
823        attrs: extract_attr_paths(&s.attrs),
824    }
825}
826
827/// Extract EnumDetail from PureFile by symbol name.
828fn extract_enum_detail(file: &PureFile, symbol_name: &str) -> Option<EnumDetail> {
829    for item in &file.items {
830        if let PureItem::Enum(e) = item {
831            if e.name == symbol_name {
832                return Some(build_enum_detail(e));
833            }
834        }
835    }
836    None
837}
838
839/// Build EnumDetail from PureEnum.
840fn build_enum_detail(e: &PureEnum) -> EnumDetail {
841    let variants = e
842        .variants
843        .iter()
844        .map(|v| {
845            let fields = match &v.fields {
846                PureFields::Named(fs) => fs
847                    .iter()
848                    .map(|f| FieldInfo {
849                        name: f.name.clone(),
850                        ty: type_to_string(&f.ty),
851                        is_public: is_public(&f.vis),
852                    })
853                    .collect(),
854                PureFields::Tuple(types) => types
855                    .iter()
856                    .enumerate()
857                    .map(|(i, ty)| FieldInfo {
858                        name: i.to_string(),
859                        ty: type_to_string(ty),
860                        is_public: true,
861                    })
862                    .collect(),
863                PureFields::Unit => Vec::new(),
864            };
865
866            VariantInfo {
867                name: v.name.clone(),
868                fields,
869                discriminant: v.discriminant.clone(),
870            }
871        })
872        .collect();
873
874    EnumDetail {
875        variants,
876        generics: convert_generics(&e.generics),
877        attrs: extract_attr_paths(&e.attrs),
878    }
879}
880
881/// Extract TraitDetail from PureFile by symbol name.
882fn extract_trait_detail(file: &PureFile, symbol_name: &str) -> Option<TraitDetail> {
883    for item in &file.items {
884        if let PureItem::Trait(t) = item {
885            if t.name == symbol_name {
886                return Some(build_trait_detail(t));
887            }
888        }
889    }
890    None
891}
892
893/// Build TraitDetail from PureTrait.
894fn build_trait_detail(t: &PureTrait) -> TraitDetail {
895    let mut methods = Vec::new();
896    let mut types = Vec::new();
897
898    for item in &t.items {
899        match item {
900            ryo_source::pure::PureTraitItem::Fn(f) => methods.push(f.name.clone()),
901            ryo_source::pure::PureTraitItem::Type { name, .. } => types.push(name.clone()),
902            _ => {}
903        }
904    }
905
906    TraitDetail {
907        is_unsafe: t.is_unsafe,
908        is_auto: t.is_auto,
909        supertraits: t.supertraits.clone(),
910        methods,
911        types,
912        generics: convert_generics(&t.generics),
913        attrs: extract_attr_paths(&t.attrs),
914    }
915}
916
917/// Extract ImplDetail from PureFile by symbol name.
918///
919/// Symbol name for impl blocks is like `<impl Trait for Type>` or `<impl Type>`.
920fn extract_impl_detail(file: &PureFile, symbol_name: &str) -> Option<ImplDetail> {
921    for item in &file.items {
922        if let PureItem::Impl(i) = item {
923            // Build the expected impl name and compare
924            let impl_name = if let Some(ref trait_name) = i.trait_ {
925                format!("<impl {} for {}>", trait_name, i.self_ty)
926            } else {
927                format!("<impl {}>", i.self_ty)
928            };
929            if impl_name == symbol_name {
930                return Some(build_impl_detail(i));
931            }
932        }
933    }
934    None
935}
936
937/// Build ImplDetail from PureImpl.
938fn build_impl_detail(i: &PureImpl) -> ImplDetail {
939    let methods = i
940        .items
941        .iter()
942        .filter_map(|item| {
943            if let ryo_source::pure::PureImplItem::Fn(f) = item {
944                Some(f.name.clone())
945            } else {
946                None
947            }
948        })
949        .collect();
950
951    ImplDetail {
952        is_unsafe: i.is_unsafe,
953        self_ty: i.self_ty.clone(),
954        trait_: i.trait_.clone(),
955        methods,
956        generics: convert_generics(&i.generics),
957        attrs: extract_attr_paths(&i.attrs),
958    }
959}
960
961/// Extract attribute representations from PureAttribute list.
962///
963/// Returns both the path and full representation with arguments:
964/// - `#[deprecated]` → ["deprecated"]
965/// - `#[allow(dead_code)]` → ["allow", "allow(dead_code)"]
966/// - `#[derive(Debug, Clone)]` → ["derive", "derive(Debug, Clone)"]
967fn extract_attr_paths(attrs: &[ryo_source::pure::PureAttribute]) -> Vec<String> {
968    use ryo_source::pure::PureAttrMeta;
969
970    let mut result = Vec::new();
971    for attr in attrs {
972        // Always include the path
973        result.push(attr.path.clone());
974
975        // If there are arguments, also include the full form
976        match &attr.meta {
977            PureAttrMeta::List(args) if !args.is_empty() => {
978                result.push(format!("{}({})", attr.path, args));
979            }
980            PureAttrMeta::NameValue(value) if !value.is_empty() => {
981                result.push(format!("{} = {}", attr.path, value));
982            }
983            _ => {}
984        }
985    }
986    result
987}
988
989#[cfg(test)]
990mod tests {
991    use super::*;
992
993    #[test]
994    fn test_generic_info_is_empty() {
995        let info = GenericInfo::default();
996        assert!(info.is_empty());
997
998        let info = GenericInfo {
999            type_params: vec!["T".to_string()],
1000            ..Default::default()
1001        };
1002        assert!(!info.is_empty());
1003    }
1004
1005    #[test]
1006    fn test_type_to_string() {
1007        assert_eq!(
1008            type_to_string(&PureType::Path("String".to_string())),
1009            "String"
1010        );
1011        assert_eq!(type_to_string(&PureType::Infer), "_");
1012        assert_eq!(type_to_string(&PureType::Never), "!");
1013    }
1014
1015    #[test]
1016    fn test_detail_store_new() {
1017        let store = DetailStore::new();
1018        assert!(store.is_empty());
1019        assert_eq!(store.len(), 0);
1020    }
1021
1022    #[test]
1023    fn test_extract_method_with_mut_self() {
1024        use ryo_source::pure::PureFile;
1025
1026        let source = r#"
1027            struct Foo;
1028            impl Foo {
1029                pub fn get_mut(&mut self) -> &mut i32 {
1030                    &mut self.0
1031                }
1032            }
1033        "#;
1034
1035        let file = PureFile::from_source(source).unwrap();
1036        let detail = extract_function_detail(&file, "get_mut", None, None);
1037
1038        assert!(detail.is_some(), "get_mut should be found");
1039        let detail = detail.unwrap();
1040        assert!(detail.has_self, "get_mut should have self");
1041        assert!(!detail.params.is_empty(), "get_mut should have params");
1042
1043        let first_param = &detail.params[0];
1044        assert!(first_param.is_self, "first param should be self");
1045        assert!(first_param.is_mut, "first param should be mut");
1046        assert_eq!(first_param.ty, "&mut Self", "self type should be &mut Self");
1047    }
1048
1049    #[test]
1050    fn test_extract_toplevel_vs_method_same_name() {
1051        use ryo_source::pure::PureFile;
1052
1053        // This simulates the case where both a free function and a method
1054        // have the same name "execute"
1055        let source = r#"
1056            struct Executor;
1057
1058            impl Executor {
1059                pub fn execute(&self, cmd: &str) -> bool {
1060                    true
1061                }
1062            }
1063
1064            pub fn execute(cmd: &str, config: &str) -> bool {
1065                false
1066            }
1067        "#;
1068
1069        let file = PureFile::from_source(source).unwrap();
1070
1071        // extract_toplevel_function_detail should only find the free function
1072        let toplevel_detail = extract_toplevel_function_detail(&file, "execute");
1073        assert!(
1074            toplevel_detail.is_some(),
1075            "toplevel execute should be found"
1076        );
1077        let toplevel_detail = toplevel_detail.unwrap();
1078        assert!(
1079            !toplevel_detail.has_self,
1080            "toplevel execute should NOT have self"
1081        );
1082        assert!(
1083            !toplevel_detail.params.iter().any(|p| p.is_self),
1084            "toplevel execute params should not contain self"
1085        );
1086
1087        // extract_method_detail should only find the method
1088        let method_detail = extract_method_detail(&file, "execute");
1089        assert!(method_detail.is_some(), "method execute should be found");
1090        let method_detail = method_detail.unwrap();
1091        assert!(method_detail.has_self, "method execute SHOULD have self");
1092        assert!(
1093            method_detail.params.iter().any(|p| p.is_self),
1094            "method execute params should contain self"
1095        );
1096        assert_eq!(
1097            method_detail.params[0].ty, "&Self",
1098            "method self should be &Self"
1099        );
1100    }
1101
1102    #[test]
1103    fn test_extract_method_with_ref_self() {
1104        use ryo_source::pure::PureFile;
1105
1106        let source = r#"
1107            struct Reader;
1108            impl Reader {
1109                pub fn read(&self, buf: &mut [u8]) -> usize {
1110                    0
1111                }
1112            }
1113        "#;
1114
1115        let file = PureFile::from_source(source).unwrap();
1116        let detail = extract_method_detail(&file, "read");
1117
1118        assert!(detail.is_some(), "read should be found");
1119        let detail = detail.unwrap();
1120        assert!(detail.has_self, "read should have self");
1121        assert!(!detail.params.is_empty(), "read should have params");
1122
1123        let first_param = &detail.params[0];
1124        assert!(first_param.is_self, "first param should be self");
1125        assert!(!first_param.is_mut, "first param should NOT be mut");
1126        assert_eq!(first_param.ty, "&Self", "self type should be &Self");
1127    }
1128
1129    #[test]
1130    fn test_extract_method_with_owned_self() {
1131        use ryo_source::pure::PureFile;
1132
1133        let source = r#"
1134            struct Consumer;
1135            impl Consumer {
1136                pub fn consume(self) -> i32 {
1137                    42
1138                }
1139            }
1140        "#;
1141
1142        let file = PureFile::from_source(source).unwrap();
1143        let detail = extract_method_detail(&file, "consume");
1144
1145        assert!(detail.is_some(), "consume should be found");
1146        let detail = detail.unwrap();
1147        assert!(detail.has_self, "consume should have self");
1148
1149        let first_param = &detail.params[0];
1150        assert!(first_param.is_self, "first param should be self");
1151        assert!(!first_param.is_mut, "first param should NOT be mut");
1152        assert_eq!(first_param.ty, "Self", "self type should be Self (owned)");
1153    }
1154
1155    #[test]
1156    fn test_extract_attrs_from_function() {
1157        use ryo_source::pure::PureFile;
1158
1159        let source = r#"
1160            #[deprecated(note = "use new_function instead")]
1161            #[inline]
1162            pub fn old_function() {}
1163        "#;
1164
1165        let file = PureFile::from_source(source).unwrap();
1166        let detail = extract_toplevel_function_detail(&file, "old_function");
1167
1168        assert!(detail.is_some(), "old_function should be found");
1169        let detail = detail.unwrap();
1170        assert!(
1171            detail.attrs.contains(&"deprecated".to_string()),
1172            "should have deprecated attr"
1173        );
1174        assert!(
1175            detail.attrs.contains(&"inline".to_string()),
1176            "should have inline attr"
1177        );
1178    }
1179
1180    #[test]
1181    fn test_extract_attrs_from_struct() {
1182        use ryo_source::pure::PureFile;
1183
1184        let source = r#"
1185            #[derive(Debug, Clone)]
1186            #[repr(C)]
1187            pub struct Config {
1188                pub name: String,
1189            }
1190        "#;
1191
1192        let file = PureFile::from_source(source).unwrap();
1193        let detail = extract_struct_detail(&file, "Config");
1194
1195        assert!(detail.is_some(), "Config should be found");
1196        let detail = detail.unwrap();
1197        assert!(
1198            detail.attrs.contains(&"derive".to_string()),
1199            "should have derive attr"
1200        );
1201        assert!(
1202            detail.attrs.contains(&"repr".to_string()),
1203            "should have repr attr"
1204        );
1205    }
1206}