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 {
753                name, ty, is_mut, ..
754            } => {
755                params.push(ParamInfo {
756                    name: name.clone(),
757                    ty: type_to_string(ty),
758                    is_self: false,
759                    is_mut: *is_mut,
760                });
761            }
762        }
763    }
764
765    FunctionDetail {
766        is_async: f.is_async,
767        is_const: f.is_const,
768        is_unsafe: f.is_unsafe,
769        params,
770        return_type: f.ret.as_ref().map(type_to_string),
771        generics: convert_generics(&f.generics),
772        is_method: self_ty.is_some(),
773        self_ty: self_ty.map(String::from),
774        trait_impl: trait_impl.map(String::from),
775        has_self,
776        attrs: extract_attr_paths(&f.attrs),
777    }
778}
779
780/// Extract StructDetail from PureFile by symbol name.
781fn extract_struct_detail(file: &PureFile, symbol_name: &str) -> Option<StructDetail> {
782    for item in &file.items {
783        if let PureItem::Struct(s) = item {
784            if s.name == symbol_name {
785                return Some(build_struct_detail(s));
786            }
787        }
788    }
789    None
790}
791
792/// Build StructDetail from PureStruct.
793fn build_struct_detail(s: &PureStruct) -> StructDetail {
794    let (fields, kind) = match &s.fields {
795        PureFields::Named(fs) => {
796            let fields = fs
797                .iter()
798                .map(|f| FieldInfo {
799                    name: f.name.clone(),
800                    ty: type_to_string(&f.ty),
801                    is_public: is_public(&f.vis),
802                })
803                .collect();
804            (fields, StructKind::Named)
805        }
806        PureFields::Tuple(tuple_fields) => {
807            let fields = tuple_fields
808                .iter()
809                .enumerate()
810                .map(|(i, f)| FieldInfo {
811                    name: i.to_string(),
812                    ty: type_to_string(&f.ty),
813                    is_public: true, // tuple fields follow struct visibility
814                })
815                .collect();
816            (fields, StructKind::Tuple)
817        }
818        PureFields::Unit => (Vec::new(), StructKind::Unit),
819    };
820
821    StructDetail {
822        fields,
823        kind,
824        generics: convert_generics(&s.generics),
825        attrs: extract_attr_paths(&s.attrs),
826    }
827}
828
829/// Extract EnumDetail from PureFile by symbol name.
830fn extract_enum_detail(file: &PureFile, symbol_name: &str) -> Option<EnumDetail> {
831    for item in &file.items {
832        if let PureItem::Enum(e) = item {
833            if e.name == symbol_name {
834                return Some(build_enum_detail(e));
835            }
836        }
837    }
838    None
839}
840
841/// Build EnumDetail from PureEnum.
842fn build_enum_detail(e: &PureEnum) -> EnumDetail {
843    let variants = e
844        .variants
845        .iter()
846        .map(|v| {
847            let fields = match &v.fields {
848                PureFields::Named(fs) => fs
849                    .iter()
850                    .map(|f| FieldInfo {
851                        name: f.name.clone(),
852                        ty: type_to_string(&f.ty),
853                        is_public: is_public(&f.vis),
854                    })
855                    .collect(),
856                PureFields::Tuple(tuple_fields) => tuple_fields
857                    .iter()
858                    .enumerate()
859                    .map(|(i, f)| FieldInfo {
860                        name: i.to_string(),
861                        ty: type_to_string(&f.ty),
862                        is_public: true,
863                    })
864                    .collect(),
865                PureFields::Unit => Vec::new(),
866            };
867
868            VariantInfo {
869                name: v.name.clone(),
870                fields,
871                discriminant: v.discriminant.clone(),
872            }
873        })
874        .collect();
875
876    EnumDetail {
877        variants,
878        generics: convert_generics(&e.generics),
879        attrs: extract_attr_paths(&e.attrs),
880    }
881}
882
883/// Extract TraitDetail from PureFile by symbol name.
884fn extract_trait_detail(file: &PureFile, symbol_name: &str) -> Option<TraitDetail> {
885    for item in &file.items {
886        if let PureItem::Trait(t) = item {
887            if t.name == symbol_name {
888                return Some(build_trait_detail(t));
889            }
890        }
891    }
892    None
893}
894
895/// Build TraitDetail from PureTrait.
896fn build_trait_detail(t: &PureTrait) -> TraitDetail {
897    let mut methods = Vec::new();
898    let mut types = Vec::new();
899
900    for item in &t.items {
901        match item {
902            ryo_source::pure::PureTraitItem::Fn(f) => methods.push(f.name.clone()),
903            ryo_source::pure::PureTraitItem::Type { name, .. } => types.push(name.clone()),
904            _ => {}
905        }
906    }
907
908    TraitDetail {
909        is_unsafe: t.is_unsafe,
910        is_auto: t.is_auto,
911        supertraits: t.supertraits.clone(),
912        methods,
913        types,
914        generics: convert_generics(&t.generics),
915        attrs: extract_attr_paths(&t.attrs),
916    }
917}
918
919/// Extract ImplDetail from PureFile by symbol name.
920///
921/// Symbol name for impl blocks is like `<impl Trait for Type>` or `<impl Type>`.
922fn extract_impl_detail(file: &PureFile, symbol_name: &str) -> Option<ImplDetail> {
923    for item in &file.items {
924        if let PureItem::Impl(i) = item {
925            // Build the expected impl name and compare
926            let impl_name = if let Some(ref trait_name) = i.trait_ {
927                format!("<impl {} for {}>", trait_name, i.self_ty)
928            } else {
929                format!("<impl {}>", i.self_ty)
930            };
931            if impl_name == symbol_name {
932                return Some(build_impl_detail(i));
933            }
934        }
935    }
936    None
937}
938
939/// Build ImplDetail from PureImpl.
940fn build_impl_detail(i: &PureImpl) -> ImplDetail {
941    let methods = i
942        .items
943        .iter()
944        .filter_map(|item| {
945            if let ryo_source::pure::PureImplItem::Fn(f) = item {
946                Some(f.name.clone())
947            } else {
948                None
949            }
950        })
951        .collect();
952
953    ImplDetail {
954        is_unsafe: i.is_unsafe,
955        self_ty: i.self_ty.clone(),
956        trait_: i.trait_.clone(),
957        methods,
958        generics: convert_generics(&i.generics),
959        attrs: extract_attr_paths(&i.attrs),
960    }
961}
962
963/// Extract attribute representations from PureAttribute list.
964///
965/// Returns both the path and full representation with arguments:
966/// - `#[deprecated]` → ["deprecated"]
967/// - `#[allow(dead_code)]` → ["allow", "allow(dead_code)"]
968/// - `#[derive(Debug, Clone)]` → ["derive", "derive(Debug, Clone)"]
969fn extract_attr_paths(attrs: &[ryo_source::pure::PureAttribute]) -> Vec<String> {
970    use ryo_source::pure::PureAttrMeta;
971
972    let mut result = Vec::new();
973    for attr in attrs {
974        // Always include the path
975        result.push(attr.path.clone());
976
977        // If there are arguments, also include the full form
978        match &attr.meta {
979            PureAttrMeta::List(args) if !args.is_empty() => {
980                result.push(format!("{}({})", attr.path, args));
981            }
982            PureAttrMeta::NameValue(value) if !value.is_empty() => {
983                result.push(format!("{} = {}", attr.path, value));
984            }
985            _ => {}
986        }
987    }
988    result
989}
990
991#[cfg(test)]
992mod tests {
993    use super::*;
994
995    #[test]
996    fn test_generic_info_is_empty() {
997        let info = GenericInfo::default();
998        assert!(info.is_empty());
999
1000        let info = GenericInfo {
1001            type_params: vec!["T".to_string()],
1002            ..Default::default()
1003        };
1004        assert!(!info.is_empty());
1005    }
1006
1007    #[test]
1008    fn test_type_to_string() {
1009        assert_eq!(
1010            type_to_string(&PureType::Path("String".to_string())),
1011            "String"
1012        );
1013        assert_eq!(type_to_string(&PureType::Infer), "_");
1014        assert_eq!(type_to_string(&PureType::Never), "!");
1015    }
1016
1017    #[test]
1018    fn test_detail_store_new() {
1019        let store = DetailStore::new();
1020        assert!(store.is_empty());
1021        assert_eq!(store.len(), 0);
1022    }
1023
1024    #[test]
1025    fn test_extract_method_with_mut_self() {
1026        use ryo_source::pure::PureFile;
1027
1028        let source = r#"
1029            struct Foo;
1030            impl Foo {
1031                pub fn get_mut(&mut self) -> &mut i32 {
1032                    &mut self.0
1033                }
1034            }
1035        "#;
1036
1037        let file = PureFile::from_source(source).unwrap();
1038        let detail = extract_function_detail(&file, "get_mut", None, None);
1039
1040        assert!(detail.is_some(), "get_mut should be found");
1041        let detail = detail.unwrap();
1042        assert!(detail.has_self, "get_mut should have self");
1043        assert!(!detail.params.is_empty(), "get_mut should have params");
1044
1045        let first_param = &detail.params[0];
1046        assert!(first_param.is_self, "first param should be self");
1047        assert!(first_param.is_mut, "first param should be mut");
1048        assert_eq!(first_param.ty, "&mut Self", "self type should be &mut Self");
1049    }
1050
1051    #[test]
1052    fn test_extract_toplevel_vs_method_same_name() {
1053        use ryo_source::pure::PureFile;
1054
1055        // This simulates the case where both a free function and a method
1056        // have the same name "execute"
1057        let source = r#"
1058            struct Executor;
1059
1060            impl Executor {
1061                pub fn execute(&self, cmd: &str) -> bool {
1062                    true
1063                }
1064            }
1065
1066            pub fn execute(cmd: &str, config: &str) -> bool {
1067                false
1068            }
1069        "#;
1070
1071        let file = PureFile::from_source(source).unwrap();
1072
1073        // extract_toplevel_function_detail should only find the free function
1074        let toplevel_detail = extract_toplevel_function_detail(&file, "execute");
1075        assert!(
1076            toplevel_detail.is_some(),
1077            "toplevel execute should be found"
1078        );
1079        let toplevel_detail = toplevel_detail.unwrap();
1080        assert!(
1081            !toplevel_detail.has_self,
1082            "toplevel execute should NOT have self"
1083        );
1084        assert!(
1085            !toplevel_detail.params.iter().any(|p| p.is_self),
1086            "toplevel execute params should not contain self"
1087        );
1088
1089        // extract_method_detail should only find the method
1090        let method_detail = extract_method_detail(&file, "execute");
1091        assert!(method_detail.is_some(), "method execute should be found");
1092        let method_detail = method_detail.unwrap();
1093        assert!(method_detail.has_self, "method execute SHOULD have self");
1094        assert!(
1095            method_detail.params.iter().any(|p| p.is_self),
1096            "method execute params should contain self"
1097        );
1098        assert_eq!(
1099            method_detail.params[0].ty, "&Self",
1100            "method self should be &Self"
1101        );
1102    }
1103
1104    #[test]
1105    fn test_extract_method_with_ref_self() {
1106        use ryo_source::pure::PureFile;
1107
1108        let source = r#"
1109            struct Reader;
1110            impl Reader {
1111                pub fn read(&self, buf: &mut [u8]) -> usize {
1112                    0
1113                }
1114            }
1115        "#;
1116
1117        let file = PureFile::from_source(source).unwrap();
1118        let detail = extract_method_detail(&file, "read");
1119
1120        assert!(detail.is_some(), "read should be found");
1121        let detail = detail.unwrap();
1122        assert!(detail.has_self, "read should have self");
1123        assert!(!detail.params.is_empty(), "read should have params");
1124
1125        let first_param = &detail.params[0];
1126        assert!(first_param.is_self, "first param should be self");
1127        assert!(!first_param.is_mut, "first param should NOT be mut");
1128        assert_eq!(first_param.ty, "&Self", "self type should be &Self");
1129    }
1130
1131    #[test]
1132    fn test_extract_method_with_owned_self() {
1133        use ryo_source::pure::PureFile;
1134
1135        let source = r#"
1136            struct Consumer;
1137            impl Consumer {
1138                pub fn consume(self) -> i32 {
1139                    42
1140                }
1141            }
1142        "#;
1143
1144        let file = PureFile::from_source(source).unwrap();
1145        let detail = extract_method_detail(&file, "consume");
1146
1147        assert!(detail.is_some(), "consume should be found");
1148        let detail = detail.unwrap();
1149        assert!(detail.has_self, "consume should have self");
1150
1151        let first_param = &detail.params[0];
1152        assert!(first_param.is_self, "first param should be self");
1153        assert!(!first_param.is_mut, "first param should NOT be mut");
1154        assert_eq!(first_param.ty, "Self", "self type should be Self (owned)");
1155    }
1156
1157    #[test]
1158    fn test_extract_attrs_from_function() {
1159        use ryo_source::pure::PureFile;
1160
1161        let source = r#"
1162            #[deprecated(note = "use new_function instead")]
1163            #[inline]
1164            pub fn old_function() {}
1165        "#;
1166
1167        let file = PureFile::from_source(source).unwrap();
1168        let detail = extract_toplevel_function_detail(&file, "old_function");
1169
1170        assert!(detail.is_some(), "old_function should be found");
1171        let detail = detail.unwrap();
1172        assert!(
1173            detail.attrs.contains(&"deprecated".to_string()),
1174            "should have deprecated attr"
1175        );
1176        assert!(
1177            detail.attrs.contains(&"inline".to_string()),
1178            "should have inline attr"
1179        );
1180    }
1181
1182    #[test]
1183    fn test_extract_attrs_from_struct() {
1184        use ryo_source::pure::PureFile;
1185
1186        let source = r#"
1187            #[derive(Debug, Clone)]
1188            #[repr(C)]
1189            pub struct Config {
1190                pub name: String,
1191            }
1192        "#;
1193
1194        let file = PureFile::from_source(source).unwrap();
1195        let detail = extract_struct_detail(&file, "Config");
1196
1197        assert!(detail.is_some(), "Config should be found");
1198        let detail = detail.unwrap();
1199        assert!(
1200            detail.attrs.contains(&"derive".to_string()),
1201            "should have derive attr"
1202        );
1203        assert!(
1204            detail.attrs.contains(&"repr".to_string()),
1205            "should have repr attr"
1206        );
1207    }
1208}