Skip to main content

ryo_analysis/
context.rs

1//! AnalysisContext - Unified initialization for code analysis structures.
2//!
3//! Provides a single entry point for constructing SymbolRegistry, CodeGraphV2,
4//! and optionally DataFlowGraph from source files.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use ryo_analysis::AnalysisContext;
10//!
11//! // Production: Use from_workspace_root (recommended)
12//! let ctx = AnalysisContext::from_workspace_root("/path/to/project")?;
13//!
14//! // Access components
15//! let symbol = ctx.registry.lookup(&path);
16//! let callers = ctx.code_graph.callers_of(symbol_id);
17//! ```
18
19use std::collections::HashMap;
20use std::path::Path;
21use std::sync::Arc;
22
23use crate::import_map_builder::{build_import_map, collect_public_reexports};
24use crate::SymbolKind;
25use rayon::prelude::*;
26use ryo_source::pure::{
27    PureBlock, PureExpr, PureFields, PureFile, PureFn, PureImpl, PureImplItem, PureItem, PureStmt,
28    PureTraitItem, PureVis,
29};
30use ryo_symbol::{
31    CargoMetadataProvider, FileSpan, SymbolPathResolver, UseResolver, WorkspaceFilePath,
32    WorkspacePathResolver,
33};
34
35use crate::ast::ASTRegistry;
36use crate::detail_store::DetailStore;
37use crate::query::{
38    CodeEdgeV2, CodeGraphV2, DataFlowBuilderWorkspace, DataFlowGraphV2, TypeFlowBuilderV2,
39    TypeFlowGraphV2,
40};
41use crate::symbol::{
42    RegistryUpdate, RegistryUpdateBatch, SymbolId, SymbolPath, SymbolRegistry, Visibility,
43};
44
45/// Persistent HashMap with O(log n) clone via structural sharing.
46pub type ImHashMap<K, V> = im::HashMap<K, V>;
47
48/// Error type for AnalysisContext operations.
49#[derive(Debug, thiserror::Error)]
50pub enum ContextError {
51    /// Failure originating from `cargo metadata` or workspace metadata
52    /// resolution. Carries a rendered message for diagnostics.
53    #[error("Metadata error: {0}")]
54    Metadata(String),
55
56    /// Underlying filesystem I/O failure during context construction.
57    #[error("IO error: {0}")]
58    Io(String),
59
60    /// Source-file parse failure (syn / tree-sitter level).
61    #[error("Parse error: {0}")]
62    Parse(String),
63
64    /// SymbolPath or import resolution failure during context wiring.
65    #[error("Resolve error: {0}")]
66    Resolve(String),
67
68    /// Failure while regenerating Rust source from a mutated AST; wraps the
69    /// underlying [`ryo_source::pure::ToSynError`].
70    #[error("Source generation error: {0}")]
71    SourceGen(#[from] ryo_source::pure::ToSynError),
72}
73
74/// Unified context containing all analysis structures.
75///
76/// This is the recommended way to initialize code analysis from source files.
77/// Executor receives this context and works with SymbolId-based Intent.
78///
79/// # Fork-on-Write Pattern
80///
81/// AnalysisContext supports fork-on-write for speculative execution:
82///
83/// ```ignore
84/// let forked = ctx.fork();  // O(log n) structural sharing
85/// // Mutations on forked don't affect original
86/// ```
87///
88/// Uses `im::HashMap<FileId, Arc<PureFile>>` for efficient cloning:
89/// - Clone is O(log n) via structural sharing
90/// - Modifications only copy affected nodes
91/// - Arc ensures PureFile is shared until modified
92///
93/// See `docs/parallel-execution-design.md` for design details.
94pub struct AnalysisContext {
95    /// Workspace root path (shared via Arc for efficient cloning).
96    pub workspace_root: Arc<Path>,
97    /// Symbol registry for path ↔ SymbolId mapping.
98    pub registry: SymbolRegistry,
99    /// Code graph for symbol relationships.
100    pub code_graph: CodeGraphV2,
101    /// Type flow graph for type relationships.
102    pub typeflow_graph: TypeFlowGraphV2,
103    /// Data flow graph for variable-level flow analysis.
104    pub dataflow_graph: DataFlowGraphV2,
105    /// Cached symbol details for O(1) access.
106    pub detail_store: DetailStore,
107    /// Complete AST storage for v2 mutation engine.
108    ///
109    /// Stores full PureItem per symbol, replacing file-based mutation approach.
110    /// Used by ASTMutationEngine for registry-only execution.
111    pub ast_registry: ASTRegistry,
112    /// Source files with structural sharing for O(log n) fork.
113    /// Key: WorkspaceFilePath (self-contained, no external registry needed)
114    pub files: ImHashMap<WorkspaceFilePath, Arc<PureFile>>,
115    /// Original source strings for diff generation.
116    pub original: HashMap<WorkspaceFilePath, String>,
117    /// Use statement resolver for cross-crate symbol resolution.
118    pub use_resolver: UseResolver,
119    /// Literal search index (feature-gated).
120    #[cfg(feature = "literal-search")]
121    pub literal_index: Option<crate::literal::LiteralIndex>,
122    /// Derive index for fast derive trait validation (O(1) lookup).
123    pub derive_index: crate::query::DeriveIndex,
124}
125
126/// Configuration for context building.
127#[derive(Debug, Clone, Default)]
128pub struct AnalysisConfig {
129    /// Build with parallel processing.
130    pub parallel: bool,
131    /// Only include public items (default: false = include all).
132    pub pub_only: bool,
133    /// Preloaded UUID mappings from previous session.
134    /// SymbolPath (string) → UUID (string) for JSON compatibility.
135    pub uuid_mappings: Option<HashMap<String, String>>,
136}
137
138impl AnalysisConfig {
139    /// Construct a default configuration (sequential build, all symbols
140    /// included, no preloaded UUID mappings).
141    pub fn new() -> Self {
142        Self::default()
143    }
144
145    /// Enable parallel processing during context build (builder-style).
146    pub fn parallel(mut self) -> Self {
147        self.parallel = true;
148        self
149    }
150
151    /// Only include public items, excluding private symbols.
152    pub fn pub_only(mut self) -> Self {
153        self.pub_only = true;
154        self
155    }
156
157    /// Set preloaded UUID mappings from previous session.
158    pub fn with_uuid_mappings(mut self, mappings: HashMap<String, String>) -> Self {
159        self.uuid_mappings = Some(mappings);
160        self
161    }
162}
163
164impl AnalysisContext {
165    // ========================================================================
166    // Public API - Primary Entry Point
167    // ========================================================================
168
169    /// Create context from workspace root path.
170    ///
171    /// **This is the recommended API for workspace-aware analysis.**
172    ///
173    /// Automatically discovers and loads all Rust files in the workspace,
174    /// using CargoMetadataProvider for accurate crate name resolution.
175    /// Also loads UUID mappings from `.ryo/uuid-mapping.json` if present.
176    ///
177    /// # Example
178    /// ```ignore
179    /// use ryo_analysis::AnalysisContext;
180    ///
181    /// let ctx = AnalysisContext::from_workspace_root("/path/to/project")?;
182    /// let symbol = ctx.registry.lookup(&path);
183    /// ```
184    pub fn from_workspace_root(path: impl AsRef<Path>) -> Result<Self, ContextError> {
185        use ryo_symbol::{CargoMetadataProvider, WorkspaceMetadataProvider};
186
187        let path = path.as_ref();
188
189        // 1. Create CargoMetadataProvider for workspace info
190        let metadata = CargoMetadataProvider::from_directory(path)
191            .map_err(|e| ContextError::Metadata(e.to_string()))?;
192
193        let workspace_root = metadata.workspace_root().to_path_buf();
194        let resolver = WorkspacePathResolver::new(workspace_root.clone());
195
196        // 2. Load UUID mappings from previous session
197        let uuid_mappings = Self::load_uuid_mappings(&workspace_root);
198
199        // 3. Load all Rust files from workspace
200        let mut files = HashMap::new();
201        Self::load_dir(
202            &workspace_root,
203            &workspace_root,
204            &resolver,
205            &metadata,
206            &mut files,
207        )?;
208
209        // 4. Build context with UUID mappings
210        let config = match uuid_mappings {
211            Some(mappings) => AnalysisConfig::default().with_uuid_mappings(mappings),
212            None => AnalysisConfig::default(),
213        };
214
215        Self::build_from_workspace_files(files, Arc::from(workspace_root.as_path()), config)
216    }
217
218    /// Create context from workspace root with parallel processing.
219    pub fn from_workspace_root_parallel(path: impl AsRef<Path>) -> Result<Self, ContextError> {
220        use ryo_symbol::{CargoMetadataProvider, WorkspaceMetadataProvider};
221
222        let path = path.as_ref();
223
224        let metadata = CargoMetadataProvider::from_directory(path)
225            .map_err(|e| ContextError::Metadata(e.to_string()))?;
226
227        let workspace_root = metadata.workspace_root().to_path_buf();
228        let resolver = WorkspacePathResolver::new(workspace_root.clone());
229
230        // Load UUID mappings from previous session
231        let uuid_mappings = Self::load_uuid_mappings(&workspace_root);
232
233        let mut files = HashMap::new();
234        Self::load_dir(
235            &workspace_root,
236            &workspace_root,
237            &resolver,
238            &metadata,
239            &mut files,
240        )?;
241
242        // Build context with UUID mappings
243        let config = match uuid_mappings {
244            Some(mappings) => AnalysisConfig::new()
245                .parallel()
246                .with_uuid_mappings(mappings),
247            None => AnalysisConfig::new().parallel(),
248        };
249
250        Self::build_from_workspace_files(files, Arc::from(workspace_root.as_path()), config)
251    }
252
253    /// Load UUID mappings from `.ryo/uuid-mapping.json`.
254    /// Returns None if file doesn't exist or is invalid.
255    fn load_uuid_mappings(workspace_root: &std::path::Path) -> Option<HashMap<String, String>> {
256        let path = workspace_root.join(".ryo").join("uuid-mapping.json");
257        let content = std::fs::read_to_string(&path).ok()?;
258        serde_json::from_str(&content).ok()
259    }
260
261    /// Save UUID mappings to `.ryo/uuid-mapping.json`.
262    ///
263    /// # Deprecated
264    ///
265    /// **Use `UuidPersistence` trait instead.** This method performs direct file I/O
266    /// which is not testable and bypasses the storage abstraction layer.
267    ///
268    /// Production code should use:
269    /// - `Api::save_uuid_mappings()` (which uses `FileUuidStorage` internally)
270    /// - `ContextLoad::take_and_save()` (for CLI standalone mode)
271    ///
272    /// This method is preserved for internal tests only.
273    #[doc(hidden)]
274    #[cfg(any(test, feature = "testing"))]
275    pub fn save_uuid_mappings(&self) -> Result<(), ContextError> {
276        let ryo_dir = self.workspace_root.join(".ryo");
277        std::fs::create_dir_all(&ryo_dir)
278            .map_err(|e| ContextError::Io(format!("Failed to create .ryo directory: {}", e)))?;
279
280        let path = ryo_dir.join("uuid-mapping.json");
281        let mappings = self.registry.export_uuid_mapping_strings();
282
283        let content = serde_json::to_string_pretty(&mappings)
284            .map_err(|e| ContextError::Io(format!("Failed to serialize UUID mappings: {}", e)))?;
285
286        std::fs::write(&path, content)
287            .map_err(|e| ContextError::Io(format!("Failed to write UUID mappings: {}", e)))?;
288
289        Ok(())
290    }
291
292    /// Load Rust files recursively from a directory.
293    fn load_dir(
294        _root: &Path,
295        dir: &Path,
296        resolver: &WorkspacePathResolver,
297        metadata: &CargoMetadataProvider,
298        files: &mut HashMap<WorkspaceFilePath, PureFile>,
299    ) -> Result<(), ContextError> {
300        if !dir.is_dir() {
301            return Ok(());
302        }
303
304        // Skip common non-source directories
305        let dir_name = dir.file_name().and_then(|n| n.to_str()).unwrap_or("");
306        if matches!(
307            dir_name,
308            "target" | "node_modules" | ".git" | "dist" | "build"
309        ) {
310            return Ok(());
311        }
312
313        for entry in std::fs::read_dir(dir).map_err(|e| ContextError::Io(e.to_string()))? {
314            let entry = entry.map_err(|e| ContextError::Io(e.to_string()))?;
315            let path = entry.path();
316
317            if path.is_dir() {
318                Self::load_dir(_root, &path, resolver, metadata, files)?;
319            } else if path.extension().map(|e| e == "rs").unwrap_or(false) {
320                match Self::load_file(&path, resolver, metadata) {
321                    Ok((wfp, file)) => {
322                        files.insert(wfp, file);
323                    }
324                    Err(_e) => {
325                        // Skip unparseable files silently (common for macro-generated code)
326                    }
327                }
328            }
329        }
330
331        Ok(())
332    }
333
334    /// Load and parse a single Rust file.
335    fn load_file(
336        path: &Path,
337        resolver: &WorkspacePathResolver,
338        metadata: &CargoMetadataProvider,
339    ) -> Result<(WorkspaceFilePath, PureFile), ContextError> {
340        let content = std::fs::read_to_string(path)
341            .map_err(|e| ContextError::Io(format!("{}: {}", path.display(), e)))?;
342
343        let file = PureFile::from_source(&content)
344            .map_err(|e| ContextError::Parse(format!("{}: {}", path.display(), e)))?;
345
346        let wfp = resolver
347            .resolve_with_provider(path, metadata)
348            .map_err(|e| ContextError::Resolve(format!("{}: {}", path.display(), e)))?;
349
350        Ok((wfp, file))
351    }
352
353    // ========================================================================
354    // Test-only API (for unit/integration tests with explicit WFP injection)
355    // ========================================================================
356
357    /// Create context from WorkspaceFilePath-keyed files with full analysis.
358    ///
359    /// **This is a test-only API.** For production code, use `from_workspace_root`.
360    ///
361    /// This API allows tests to inject pre-constructed `WorkspaceFilePath` instances
362    /// without filesystem access, enabling deterministic unit testing.
363    ///
364    /// Unlike `from_im_files`, this performs full symbol analysis to populate
365    /// the SymbolRegistry and CodeGraphV2. Use this for tests that need
366    /// symbol-based features like pre-checks or impact analysis.
367    ///
368    /// Note: This includes private items by default, which is typically what tests need.
369    ///
370    /// # Panics
371    /// Panics if files is empty.
372    #[doc(hidden)]
373    pub fn from_workspace_files(files: HashMap<WorkspaceFilePath, PureFile>) -> Self {
374        let workspace_root = files
375            .keys()
376            .next()
377            .expect("from_workspace_files requires at least one file")
378            .workspace_root()
379            .into();
380
381        // Default: include all symbols (pub_only: false)
382        Self::build_from_workspace_files(files, workspace_root, AnalysisConfig::default())
383            .expect("build_from_workspace_files failed in test API")
384    }
385
386    // ========================================================================
387    // Internal / Speculative Execution Support
388    // ========================================================================
389
390    /// Create a minimal context from WorkspaceFilePath-keyed files.
391    ///
392    /// This is useful for speculative execution where you want to work with
393    /// a snapshot of files without full re-analysis.
394    ///
395    /// Note: This creates a partial context with empty symbol registry and code graph.
396    /// Suitable for mutation execution but not for symbol-based queries.
397    ///
398    /// # Panics
399    /// Panics if files is empty (cannot infer workspace_root).
400    pub fn from_im_files(files: ImHashMap<WorkspaceFilePath, Arc<PureFile>>) -> Self {
401        // Extract workspace_root from the first file's WorkspaceFilePath
402        let workspace_root = files
403            .keys()
404            .next()
405            .expect("from_im_files requires at least one file")
406            .workspace_root()
407            .into();
408
409        Self {
410            workspace_root,
411            files,
412            original: HashMap::new(),
413            registry: SymbolRegistry::new(),
414            code_graph: CodeGraphV2::new(),
415            typeflow_graph: TypeFlowGraphV2::new(),
416            dataflow_graph: DataFlowGraphV2::new(),
417            detail_store: DetailStore::default(),
418            ast_registry: ASTRegistry::new(),
419            use_resolver: UseResolver::new(),
420            #[cfg(feature = "literal-search")]
421            literal_index: None,
422            derive_index: crate::query::DeriveIndex::new(),
423        }
424    }
425
426    // ========================================================================
427    // Internal Build Implementation
428    // ========================================================================
429
430    /// Build context from WorkspaceFilePath-keyed files (internal implementation).
431    ///
432    /// # Panics
433    /// Panics if `files` is empty (no crate_name can be inferred). All public
434    /// callers (`from_workspace_files`, `from_im_files`, `fork_rebuild`)
435    /// document the same precondition.
436    fn build_from_workspace_files(
437        files: HashMap<WorkspaceFilePath, PureFile>,
438        workspace_root: Arc<Path>,
439        config: AnalysisConfig,
440    ) -> Result<Self, ContextError> {
441        let mut registry = SymbolRegistry::new();
442
443        // Preload UUID mappings from previous session before registering symbols
444        if let Some(mappings) = config.uuid_mappings {
445            registry.preload_uuid_mapping_strings(mappings);
446        }
447
448        let mut code_graph = CodeGraphV2::new();
449
450        // Resolve crate_name from first file's WorkspaceFilePath
451        let crate_name = files
452            .keys()
453            .next()
454            .expect("No files loaded - cannot determine crate name")
455            .crate_name()
456            .as_str()
457            .to_string();
458
459        // Extract original source before analysis
460        let original: HashMap<WorkspaceFilePath, String> = files
461            .iter()
462            .map(|(path, file)| Ok((path.clone(), file.to_source()?)))
463            .collect::<Result<HashMap<_, _>, ContextError>>()?;
464
465        // Phase 1: Collect all symbols
466        let symbols = if config.parallel {
467            Self::collect_symbols_workspace_parallel(&files, &crate_name, config.pub_only)
468        } else {
469            Self::collect_symbols_workspace(&files, &crate_name, config.pub_only)
470        };
471
472        // Phase 2: Register symbols and build graph
473        let mut symbol_ids: HashMap<String, SymbolId> = HashMap::new();
474
475        for (path, kind, pure_vis, file_path) in symbols {
476            let path_str = path.to_string();
477            if let Ok(id) = registry.register(path, kind) {
478                code_graph.add_node(id);
479                code_graph.add_to_kind_index(id, kind);
480                symbol_ids.insert(path_str, id);
481                // Set visibility
482                let vis = pure_vis_to_visibility(&pure_vis);
483                let _ = registry.set_visibility(id, vis);
484                // Set span info for correct file path resolution
485                // This enables FileDumper to write to the correct file (main.rs vs lib.rs)
486                let span = FileSpan::new(file_path, 0, 0);
487                let _ = registry.set_span(id, span);
488            }
489        }
490
491        // Phase 3: Build edges (Contains relationships)
492        Self::build_contains_edges_workspace(&files, &crate_name, &symbol_ids, &mut code_graph);
493
494        // Phase 4: Build UseResolver for cross-crate symbol resolution
495        // (Must be before Phase 5 to enable use-aware type resolution)
496        let mut use_resolver = UseResolver::new();
497        for (file_path, file) in &files {
498            // Use the crate_name from each file's WorkspaceFilePath for correct multi-crate workspace support
499            let file_crate_name = file_path.crate_name().as_str();
500            if let Ok(crate_name_obj) = ryo_symbol::CrateName::new(file_crate_name) {
501                let path_resolver = SymbolPathResolver::new(file_crate_name);
502                let mod_path_str = path_resolver.module_path_str(file_path);
503                if let Ok(module_path) = SymbolPath::parse(&mod_path_str) {
504                    let import_map = build_import_map(file, &crate_name_obj, &module_path);
505                    use_resolver.register(module_path, import_map);
506                }
507            }
508        }
509
510        // Phase 4.5: Register public re-exports in SymbolRegistry
511        // This populates alias_to_canonical so that registry.lookup() can resolve
512        // re-export paths (e.g., tokio::sync::Mutex → tokio::sync::mutex::Mutex).
513        for (file_path, file) in &files {
514            let file_crate_name = file_path.crate_name().as_str();
515            if let Ok(crate_name_obj) = ryo_symbol::CrateName::new(file_crate_name) {
516                let path_resolver = SymbolPathResolver::new(file_crate_name);
517                let mod_path_str = path_resolver.module_path_str(file_path);
518                if let Ok(module_path) = SymbolPath::parse(&mod_path_str) {
519                    let reexports = collect_public_reexports(file, &crate_name_obj, &module_path);
520                    for entry in reexports {
521                        if let Ok(alias_path) = module_path.child(&entry.local_name) {
522                            // Only register if canonical symbol exists and alias differs
523                            if let Some(canonical_id) = registry.lookup(&entry.full_path) {
524                                if registry.lookup(&alias_path).is_none() {
525                                    let _ = registry.register_reexport(
526                                        canonical_id,
527                                        alias_path,
528                                        file_path.clone(),
529                                    );
530                                }
531                            }
532                        }
533                    }
534                }
535            }
536        }
537
538        // Phase 5: Build call/use edges (uses UseResolver for import resolution)
539        Self::build_reference_edges_workspace(
540            &files,
541            &crate_name,
542            &symbol_ids,
543            &use_resolver,
544            &registry,
545            &mut code_graph,
546        );
547
548        // Note: CodeGraphV2 builds indices incrementally (no rebuild needed)
549
550        // Phase 6: Build detail store
551        // Note: Using deprecated file-based method for initial construction
552        // (ASTRegistry not yet available at this point)
553        #[allow(deprecated)]
554        let detail_store = DetailStore::build_all_workspace(&registry, &files, &crate_name);
555
556        // Convert to im::HashMap with Arc for structural sharing
557        let im_files: ImHashMap<WorkspaceFilePath, Arc<PureFile>> = files
558            .into_iter()
559            .map(|(path, file)| (path, Arc::new(file)))
560            .collect();
561
562        // Phase 7: Build TypeFlowGraph
563        // Note: Using deprecated file-based method for initial construction
564        #[allow(deprecated)]
565        let typeflow_graph =
566            TypeFlowBuilderV2::new_workspace(&registry, &im_files, &crate_name).build();
567
568        // Phase 8: Build DataFlowGraph
569        let dataflow_graph =
570            DataFlowBuilderWorkspace::new(&registry, &im_files, &crate_name).build();
571
572        // Phase 9: Build ASTRegistry (complete PureItem per symbol)
573        let ast_registry = ASTRegistry::build_from_files(&im_files, &registry, &crate_name);
574
575        // Phase 11: Build LiteralIndex (feature-gated)
576        #[cfg(feature = "literal-search")]
577        let literal_index =
578            crate::literal::LiteralIndex::build_from_workspace_files(&im_files, &registry).ok();
579
580        // Phase 12: Build DeriveIndex for fast derive validation
581        let derive_index = crate::query::DeriveIndex::build(
582            &ast_registry,
583            &code_graph,
584            &typeflow_graph,
585            &registry,
586        );
587
588        Ok(Self {
589            workspace_root,
590            registry,
591            code_graph,
592            typeflow_graph,
593            dataflow_graph,
594            detail_store,
595            ast_registry,
596            files: im_files,
597            original,
598            use_resolver,
599            #[cfg(feature = "literal-search")]
600            literal_index,
601            derive_index,
602        })
603    }
604
605    /// Collect symbols from WorkspaceFilePath-keyed files sequentially.
606    fn collect_symbols_workspace(
607        files: &HashMap<WorkspaceFilePath, PureFile>,
608        _crate_name: &str,
609        pub_only: bool,
610    ) -> Vec<(SymbolPath, SymbolKind, PureVis, WorkspaceFilePath)> {
611        let mut symbols = Vec::new();
612
613        for (file_path, file) in files {
614            // Use the crate_name from each file's WorkspaceFilePath for correct multi-crate workspace support
615            let file_crate_name = file_path.crate_name().as_str();
616            let resolver = SymbolPathResolver::new(file_crate_name);
617            let mod_path = resolver.module_path_str(file_path);
618            let mut file_symbols = Vec::new();
619            Self::collect_from_file(&mod_path, file, pub_only, &mut file_symbols);
620            for (path, kind, vis) in file_symbols {
621                symbols.push((path, kind, vis, file_path.clone()));
622            }
623        }
624
625        symbols
626    }
627
628    /// Collect symbols from WorkspaceFilePath-keyed files in parallel.
629    fn collect_symbols_workspace_parallel(
630        files: &HashMap<WorkspaceFilePath, PureFile>,
631        _crate_name: &str,
632        pub_only: bool,
633    ) -> Vec<(SymbolPath, SymbolKind, PureVis, WorkspaceFilePath)> {
634        files
635            .par_iter()
636            .flat_map(|(file_path, file)| {
637                // Use the crate_name from each file's WorkspaceFilePath for correct multi-crate workspace support
638                let file_crate_name = file_path.crate_name().as_str();
639                let resolver = SymbolPathResolver::new(file_crate_name);
640                let mod_path = resolver.module_path_str(file_path);
641                let mut symbols = Vec::new();
642                Self::collect_from_file(&mod_path, file, pub_only, &mut symbols);
643                symbols
644                    .into_iter()
645                    .map(|(path, kind, vis)| (path, kind, vis, file_path.clone()))
646                    .collect::<Vec<_>>()
647            })
648            .collect()
649    }
650
651    /// Collect symbols from a single file.
652    fn collect_from_file(
653        mod_path: &str,
654        file: &PureFile,
655        pub_only: bool,
656        out: &mut Vec<(SymbolPath, SymbolKind, PureVis)>,
657    ) {
658        // Add module itself (modules are public by default in the context of collection)
659        if let Ok(path) = SymbolPath::parse(mod_path) {
660            out.push((path, SymbolKind::Mod, PureVis::Public));
661        }
662
663        // Collect items
664        for item in &file.items {
665            Self::collect_from_item(mod_path, item, pub_only, out);
666        }
667    }
668
669    /// Collect symbols from an item.
670    fn collect_from_item(
671        parent_path: &str,
672        item: &PureItem,
673        pub_only: bool,
674        out: &mut Vec<(SymbolPath, SymbolKind, PureVis)>,
675    ) {
676        let (name, kind, vis) = match item {
677            PureItem::Struct(s) => {
678                if pub_only && s.vis == PureVis::Private {
679                    return;
680                }
681                if let Ok(parent) = SymbolPath::parse(parent_path) {
682                    if let Ok(struct_path) = parent.child(&s.name) {
683                        out.push((struct_path.clone(), SymbolKind::Struct, s.vis.clone()));
684                        // Collect named fields
685                        if let PureFields::Named(fields) = &s.fields {
686                            for field in fields {
687                                if let Ok(field_path) = struct_path.child(&field.name) {
688                                    out.push((field_path, SymbolKind::Field, field.vis.clone()));
689                                }
690                            }
691                        }
692                    }
693                }
694                return;
695            }
696            PureItem::Enum(e) => {
697                // Skip private enums if pub_only mode
698                if pub_only && e.vis == PureVis::Private {
699                    return;
700                }
701                let enum_path = format!("{}::{}", parent_path, e.name);
702                if let Ok(path) = SymbolPath::parse(&enum_path) {
703                    out.push((path, SymbolKind::Enum, e.vis.clone()));
704                }
705                // Collect enum variants
706                for variant in &e.variants {
707                    let variant_path = format!("{}::{}", enum_path, variant.name);
708                    if let Ok(path) = SymbolPath::parse(&variant_path) {
709                        // Variants inherit enum's visibility
710                        out.push((path, SymbolKind::Variant, e.vis.clone()));
711                    }
712                }
713                return;
714            }
715            PureItem::Fn(f) => (f.name.clone(), SymbolKind::Function, f.vis.clone()),
716            PureItem::Trait(t) => {
717                if pub_only && t.vis == PureVis::Private {
718                    return;
719                }
720                if let Ok(parent) = SymbolPath::parse(parent_path) {
721                    if let Ok(trait_path) = parent.child(&t.name) {
722                        out.push((trait_path.clone(), SymbolKind::Trait, t.vis.clone()));
723                        // Collect trait items (methods, consts, associated types)
724                        for trait_item in &t.items {
725                            let (item_name, item_kind) = match trait_item {
726                                PureTraitItem::Fn(f) => (&f.name, SymbolKind::Method),
727                                PureTraitItem::Const(c) => (&c.name, SymbolKind::Const),
728                                PureTraitItem::Type { name, .. } => (name, SymbolKind::TypeAlias),
729                                PureTraitItem::Other(_) => continue,
730                            };
731                            if let Ok(item_path) = trait_path.child(item_name) {
732                                // Trait items inherit trait's visibility
733                                out.push((item_path, item_kind, t.vis.clone()));
734                            }
735                        }
736                    }
737                }
738                return;
739            }
740            PureItem::Impl(i) => {
741                Self::collect_from_impl(parent_path, i, pub_only, out);
742                return;
743            }
744            PureItem::Mod(m) => {
745                if pub_only && m.vis == PureVis::Private {
746                    return;
747                }
748                let mod_path = format!("{}::{}", parent_path, m.name);
749                if let Ok(path) = SymbolPath::parse(&mod_path) {
750                    out.push((path, SymbolKind::Mod, m.vis.clone()));
751                }
752                // Recursively collect from inline module
753                for inner_item in &m.items {
754                    Self::collect_from_item(&mod_path, inner_item, pub_only, out);
755                }
756                return;
757            }
758            PureItem::Use(_) => return,
759            PureItem::Const(c) => (c.name.clone(), SymbolKind::Const, c.vis.clone()),
760            PureItem::Static(s) => (s.name.clone(), SymbolKind::Static, s.vis.clone()),
761            PureItem::Type(t) => (t.name.clone(), SymbolKind::TypeAlias, t.vis.clone()),
762            PureItem::Macro(_) => return,
763            PureItem::Other(_) => return,
764        };
765
766        // Skip private items if pub_only mode
767        if pub_only && vis == PureVis::Private {
768            return;
769        }
770
771        let full_path = format!("{}::{}", parent_path, name);
772        if let Ok(path) = SymbolPath::parse(&full_path) {
773            out.push((path, kind, vis));
774        }
775    }
776
777    /// Collect symbols from an impl block.
778    ///
779    /// Core design:
780    /// - Impl blocks are registered as symbols (for trait operations)
781    /// - Methods are registered directly on parent type (plain impl) or trait path (trait impl)
782    /// - Impl block path: <impl Type> or <impl Trait for Type>
783    /// - Method path: Type::method or <impl Trait for Type>::method
784    fn collect_from_impl(
785        parent_path: &str,
786        impl_block: &PureImpl,
787        pub_only: bool,
788        out: &mut Vec<(SymbolPath, SymbolKind, PureVis)>,
789    ) {
790        let parent = match SymbolPath::parse(parent_path) {
791            Ok(p) => p,
792            Err(_) => return,
793        };
794        let impl_target = &impl_block.self_ty;
795
796        // Register impl block and determine method base path
797        // - Trait impl: methods under <impl Trait for Type>::method
798        // - Plain impl: impl block registered as <impl Type>, methods under Type::method
799        let method_base = if let Some(ref trait_name) = impl_block.trait_ {
800            let impl_path = parent.child_trait_impl(trait_name, impl_target);
801            out.push((impl_path.clone(), SymbolKind::Impl, PureVis::Public));
802            impl_path
803        } else {
804            let impl_path = parent.child_inherent_impl(impl_target);
805            out.push((impl_path, SymbolKind::Impl, PureVis::Public));
806            // Strip generic parameters: "Router < S >" → "Router"
807            // validate_rust_identifier rejects names with '<' / spaces,
808            // so we must use the base type name for the method path.
809            let base_type = impl_target.split('<').next().unwrap_or(impl_target).trim();
810            match parent.child(base_type) {
811                Ok(p) => p,
812                Err(_) => return,
813            }
814        };
815
816        // Register methods and associated items under the method base path
817        for item in &impl_block.items {
818            let (name, kind, vis) = match item {
819                PureImplItem::Fn(m) => (m.name.clone(), SymbolKind::Method, m.vis.clone()),
820                PureImplItem::Const(c) => (c.name.clone(), SymbolKind::Const, c.vis.clone()),
821                PureImplItem::Type(t) => (t.name.clone(), SymbolKind::TypeAlias, t.vis.clone()),
822                PureImplItem::Other(_) => continue,
823            };
824
825            if pub_only && vis == PureVis::Private {
826                continue;
827            }
828
829            if let Ok(path) = method_base.child(&name) {
830                out.push((path, kind, vis));
831            }
832        }
833    }
834
835    /// Build Contains edges (parent → child).
836    ///
837    /// Note: This function is file-agnostic as it only uses symbol_ids.
838    fn build_contains_edges_workspace(
839        _files: &HashMap<WorkspaceFilePath, PureFile>,
840        _crate_name: &str,
841        symbol_ids: &HashMap<String, SymbolId>,
842        graph: &mut CodeGraphV2,
843    ) {
844        // Build parent-child relationships based on path hierarchy
845        for (path_str, &child_id) in symbol_ids {
846            if let Some(parent_path) = get_parent_path(path_str) {
847                if let Some(&parent_id) = symbol_ids.get(&parent_path) {
848                    graph.add_edge(parent_id, child_id, CodeEdgeV2::Contains);
849                }
850            }
851        }
852    }
853
854    /// Build reference edges from WorkspaceFilePath-keyed files.
855    fn build_reference_edges_workspace(
856        files: &HashMap<WorkspaceFilePath, PureFile>,
857        _crate_name: &str,
858        symbol_ids: &HashMap<String, SymbolId>,
859        use_resolver: &UseResolver,
860        registry: &SymbolRegistry,
861        graph: &mut CodeGraphV2,
862    ) {
863        let method_index = build_method_name_index(symbol_ids, registry);
864        for (file_path, file) in files {
865            // Use the crate_name from each file's WorkspaceFilePath for correct multi-crate workspace support
866            let file_crate_name = file_path.crate_name().as_str();
867            let resolver = SymbolPathResolver::new(file_crate_name);
868            let mod_path = resolver.module_path_str(file_path);
869            Self::build_edges_from_items(
870                &mod_path,
871                &file.items,
872                symbol_ids,
873                use_resolver,
874                registry,
875                graph,
876                &method_index,
877            );
878        }
879    }
880
881    /// Build edges from a list of items.
882    fn build_edges_from_items(
883        parent_path: &str,
884        items: &[PureItem],
885        symbol_ids: &HashMap<String, SymbolId>,
886        use_resolver: &UseResolver,
887        registry: &SymbolRegistry,
888        graph: &mut CodeGraphV2,
889        method_index: &MethodNameIndex,
890    ) {
891        for item in items {
892            match item {
893                PureItem::Impl(impl_block) => {
894                    Self::build_edges_from_impl(
895                        parent_path,
896                        impl_block,
897                        symbol_ids,
898                        use_resolver,
899                        registry,
900                        graph,
901                        method_index,
902                    );
903                }
904                PureItem::Fn(func) => {
905                    let fn_path = format!("{}::{}", parent_path, func.name);
906                    Self::build_edges_from_fn(
907                        &fn_path,
908                        func,
909                        symbol_ids,
910                        use_resolver,
911                        registry,
912                        graph,
913                        method_index,
914                    );
915                }
916                PureItem::Struct(_) => {
917                    // Struct field type edges handled by TypeFlowGraphV2
918                }
919                PureItem::Mod(m) if !m.items.is_empty() => {
920                    let mod_path = format!("{}::{}", parent_path, m.name);
921                    Self::build_edges_from_items(
922                        &mod_path,
923                        &m.items,
924                        symbol_ids,
925                        use_resolver,
926                        registry,
927                        graph,
928                        method_index,
929                    );
930                }
931                _ => {}
932            }
933        }
934    }
935
936    /// Build edges from a single PureItem (for symbol-based incremental updates).
937    ///
938    /// This is the core method for Phase 2 symbol-based updates, allowing
939    /// edge rebuilding directly from ASTRegistry without file I/O.
940    fn build_edges_from_item(
941        parent_path: &str,
942        item: &PureItem,
943        symbol_ids: &HashMap<String, SymbolId>,
944        use_resolver: &UseResolver,
945        registry: &SymbolRegistry,
946        graph: &mut CodeGraphV2,
947        method_index: &MethodNameIndex,
948    ) {
949        match item {
950            PureItem::Impl(impl_block) => {
951                Self::build_edges_from_impl(
952                    parent_path,
953                    impl_block,
954                    symbol_ids,
955                    use_resolver,
956                    registry,
957                    graph,
958                    method_index,
959                );
960            }
961            PureItem::Fn(func) => {
962                let fn_path = format!("{}::{}", parent_path, func.name);
963                Self::build_edges_from_fn(
964                    &fn_path,
965                    func,
966                    symbol_ids,
967                    use_resolver,
968                    registry,
969                    graph,
970                    method_index,
971                );
972            }
973            PureItem::Struct(_) => {
974                // Struct field type edges handled by TypeFlowGraphV2
975            }
976            PureItem::Mod(m) if !m.items.is_empty() => {
977                let mod_path = format!("{}::{}", parent_path, m.name);
978                Self::build_edges_from_items(
979                    &mod_path,
980                    &m.items,
981                    symbol_ids,
982                    use_resolver,
983                    registry,
984                    graph,
985                    method_index,
986                );
987            }
988            _ => {}
989        }
990    }
991
992    /// Build edges from an impl block.
993    fn build_edges_from_impl(
994        parent_path: &str,
995        impl_block: &PureImpl,
996        symbol_ids: &HashMap<String, SymbolId>,
997        use_resolver: &UseResolver,
998        registry: &SymbolRegistry,
999        graph: &mut CodeGraphV2,
1000        method_index: &MethodNameIndex,
1001    ) {
1002        let parent = match SymbolPath::parse(parent_path) {
1003            Ok(p) => p,
1004            Err(_) => return,
1005        };
1006        let impl_target = &impl_block.self_ty;
1007
1008        // Build impl block path
1009        let impl_path = if let Some(ref trait_name) = &impl_block.trait_ {
1010            parent.child_trait_impl(trait_name, impl_target)
1011        } else {
1012            parent.child_inherent_impl(impl_target)
1013        };
1014
1015        // Get impl block's SymbolId
1016        let impl_id = match symbol_ids.get(&impl_path.to_string()) {
1017            Some(&id) => id,
1018            None => return,
1019        };
1020
1021        // Implements edge: impl block -> trait
1022        if let Some(ref trait_name) = &impl_block.trait_ {
1023            if let Some(trait_id) = Self::resolve_type_reference(
1024                parent_path,
1025                trait_name,
1026                symbol_ids,
1027                use_resolver,
1028                registry,
1029            ) {
1030                graph.add_edge(impl_id, trait_id, CodeEdgeV2::Implements);
1031            }
1032        }
1033
1034        // Note: Uses edge (impl -> self type) handled by TypeFlowGraphV2
1035
1036        // Process methods in impl block
1037        // - Trait impl: methods registered under <impl Trait for Type>::method
1038        // - Plain impl: methods registered under Type::method
1039        let method_base = if impl_block.trait_.is_some() {
1040            impl_path
1041        } else {
1042            // Strip generic parameters: "Router < S >" → "Router"
1043            let base_type = impl_target.split('<').next().unwrap_or(impl_target).trim();
1044            match parent.child(base_type) {
1045                Ok(p) => p,
1046                Err(_) => return,
1047            }
1048        };
1049
1050        for item in &impl_block.items {
1051            if let PureImplItem::Fn(func) = item {
1052                if let Ok(method_path) = method_base.child(&func.name) {
1053                    Self::build_edges_from_fn(
1054                        &method_path.to_string(),
1055                        func,
1056                        symbol_ids,
1057                        use_resolver,
1058                        registry,
1059                        graph,
1060                        method_index,
1061                    );
1062                }
1063            }
1064        }
1065    }
1066
1067    /// Build edges from a function.
1068    fn build_edges_from_fn(
1069        fn_path: &str,
1070        func: &PureFn,
1071        symbol_ids: &HashMap<String, SymbolId>,
1072        use_resolver: &UseResolver,
1073        registry: &SymbolRegistry,
1074        graph: &mut CodeGraphV2,
1075        method_index: &MethodNameIndex,
1076    ) {
1077        let fn_id = match symbol_ids.get(fn_path) {
1078            Some(&id) => id,
1079            None => return,
1080        };
1081
1082        // Get parent path for type resolution
1083        let parent_path = get_parent_path(fn_path).unwrap_or_default();
1084
1085        // Note: Uses edges (type references) are handled by TypeFlowGraphV2.
1086        // Only Calls edges remain in CodeGraphV2.
1087
1088        // Calls edges from function body
1089        let mut cx = CallsBuildContext {
1090            symbol_ids,
1091            use_resolver,
1092            registry,
1093            graph,
1094            method_index,
1095        };
1096        Self::build_calls_from_block(fn_id, &parent_path, &func.body, &mut cx);
1097    }
1098
1099    /// Build Calls edges from a block.
1100    fn build_calls_from_block(
1101        caller_id: SymbolId,
1102        parent_path: &str,
1103        block: &PureBlock,
1104        cx: &mut CallsBuildContext<'_>,
1105    ) {
1106        for stmt in &block.stmts {
1107            match stmt {
1108                PureStmt::Local {
1109                    init: Some(expr), ..
1110                }
1111                | PureStmt::Semi(expr)
1112                | PureStmt::Expr(expr) => {
1113                    Self::build_calls_from_expr(caller_id, parent_path, expr, cx);
1114                }
1115                _ => {}
1116            }
1117        }
1118    }
1119
1120    /// Build Calls edges from an expression.
1121    fn build_calls_from_expr(
1122        caller_id: SymbolId,
1123        parent_path: &str,
1124        expr: &PureExpr,
1125        cx: &mut CallsBuildContext<'_>,
1126    ) {
1127        use ryo_source::pure::PureExpr;
1128
1129        match expr {
1130            PureExpr::Call { func, args } => {
1131                // Try to resolve the function being called
1132                if let PureExpr::Path(path) = func.as_ref() {
1133                    if let Some(callee_id) = Self::resolve_type_reference(
1134                        parent_path,
1135                        path,
1136                        cx.symbol_ids,
1137                        cx.use_resolver,
1138                        cx.registry,
1139                    ) {
1140                        cx.graph.add_edge(caller_id, callee_id, CodeEdgeV2::Calls);
1141                    }
1142                }
1143                // Recurse into arguments
1144                for arg in args {
1145                    Self::build_calls_from_expr(caller_id, parent_path, arg, cx);
1146                }
1147                // Recurse into function expression
1148                Self::build_calls_from_expr(caller_id, parent_path, func, cx);
1149            }
1150            PureExpr::MethodCall {
1151                receiver,
1152                method,
1153                args,
1154                ..
1155            } => {
1156                // Heuristic method resolution without type inference:
1157                // 1. self.method() → look up sibling method via parent_path
1158                // 2. General method call → use method_index for name-based lookup
1159                let is_self_receiver = matches!(receiver.as_ref(), PureExpr::Path(name) if name == "self")
1160                    || matches!(receiver.as_ref(), PureExpr::Field { expr, .. } if matches!(expr.as_ref(), PureExpr::Path(name) if name == "self"));
1161
1162                let mut resolved = false;
1163                if is_self_receiver {
1164                    // self.method() — try parent_path::method (sibling in same impl)
1165                    let sibling_path = format!("{}::{}", parent_path, method);
1166                    if let Some(&callee_id) = cx.symbol_ids.get(&sibling_path) {
1167                        if callee_id != caller_id {
1168                            cx.graph.add_edge(caller_id, callee_id, CodeEdgeV2::Calls);
1169                            resolved = true;
1170                        }
1171                    }
1172                }
1173
1174                // Fallback: name-based lookup via method_index
1175                // Skip if already resolved via self.
1176                //
1177                // Resolution strategy:
1178                // 1. Try receiver type hint (e.g., Json::new().render() → type=Json)
1179                //    If hint available, filter candidates to matching self-type only.
1180                // 2. self.method() with no explicit type hint: extract self_ty from
1181                //    parent_path (impl block) as implicit type hint.
1182                // 3. No hint: for common trait methods (clone, from, etc.), only resolve
1183                //    when there's a single unambiguous candidate.
1184                // 4. No hint, non-common: add all candidates (over-approximation).
1185                if !resolved {
1186                    if let Some(candidates) = cx.method_index.get(method.as_str()) {
1187                        // Try explicit type hint from receiver expression first,
1188                        // then fall back to implicit hint from self receiver's impl self_ty
1189                        let explicit_hint = extract_receiver_type_hint(receiver);
1190                        let type_hint = explicit_hint.or_else(|| {
1191                            if is_self_receiver {
1192                                extract_self_type_from_parent_path(parent_path)
1193                            } else {
1194                                None
1195                            }
1196                        });
1197
1198                        if let Some(hint) = type_hint {
1199                            // Type hint available: filter candidates by self-type
1200                            let filtered: Vec<_> = candidates
1201                                .iter()
1202                                .copied()
1203                                .filter(|&id| candidate_matches_type_hint(id, hint, cx.registry))
1204                                .collect();
1205                            if !filtered.is_empty() {
1206                                for callee_id in filtered {
1207                                    if callee_id != caller_id {
1208                                        cx.graph.add_edge(caller_id, callee_id, CodeEdgeV2::Calls);
1209                                    }
1210                                }
1211                            } else {
1212                                // Hint didn't match any candidate: fall back to all
1213                                let is_common = is_common_trait_method(method);
1214                                if !is_common || candidates.len() == 1 {
1215                                    for &callee_id in candidates {
1216                                        if callee_id != caller_id {
1217                                            cx.graph.add_edge(
1218                                                caller_id,
1219                                                callee_id,
1220                                                CodeEdgeV2::Calls,
1221                                            );
1222                                        }
1223                                    }
1224                                }
1225                            }
1226                        } else {
1227                            // No type hint: use existing heuristic
1228                            let is_common = is_common_trait_method(method);
1229                            if !is_common || candidates.len() == 1 {
1230                                for &callee_id in candidates {
1231                                    if callee_id != caller_id {
1232                                        cx.graph.add_edge(caller_id, callee_id, CodeEdgeV2::Calls);
1233                                    }
1234                                }
1235                            }
1236                        }
1237                    }
1238                }
1239
1240                // Recurse into receiver and arguments
1241                Self::build_calls_from_expr(caller_id, parent_path, receiver, cx);
1242                for arg in args {
1243                    Self::build_calls_from_expr(caller_id, parent_path, arg, cx);
1244                }
1245            }
1246            PureExpr::Block { block, .. } => {
1247                Self::build_calls_from_block(caller_id, parent_path, block, cx);
1248            }
1249            PureExpr::If {
1250                cond,
1251                then_branch,
1252                else_branch,
1253            } => {
1254                Self::build_calls_from_expr(caller_id, parent_path, cond, cx);
1255                Self::build_calls_from_block(caller_id, parent_path, then_branch, cx);
1256                if let Some(else_expr) = else_branch {
1257                    Self::build_calls_from_expr(caller_id, parent_path, else_expr, cx);
1258                }
1259            }
1260            PureExpr::Match {
1261                expr: match_expr,
1262                arms,
1263            } => {
1264                Self::build_calls_from_expr(caller_id, parent_path, match_expr, cx);
1265                for arm in arms {
1266                    Self::build_calls_from_expr(caller_id, parent_path, &arm.body, cx);
1267                    if let Some(ref guard) = arm.guard {
1268                        Self::build_calls_from_expr(caller_id, parent_path, guard, cx);
1269                    }
1270                }
1271            }
1272            PureExpr::Loop { body: block, .. }
1273            | PureExpr::Async { body: block, .. }
1274            | PureExpr::Unsafe(block) => {
1275                Self::build_calls_from_block(caller_id, parent_path, block, cx);
1276            }
1277            PureExpr::While { cond, body, .. } => {
1278                Self::build_calls_from_expr(caller_id, parent_path, cond, cx);
1279                Self::build_calls_from_block(caller_id, parent_path, body, cx);
1280            }
1281            PureExpr::For {
1282                expr: iter_expr,
1283                body,
1284                ..
1285            } => {
1286                Self::build_calls_from_expr(caller_id, parent_path, iter_expr, cx);
1287                Self::build_calls_from_block(caller_id, parent_path, body, cx);
1288            }
1289            PureExpr::Closure { body, .. } => {
1290                Self::build_calls_from_expr(caller_id, parent_path, body, cx);
1291            }
1292            PureExpr::Binary { left, right, .. } => {
1293                Self::build_calls_from_expr(caller_id, parent_path, left, cx);
1294                Self::build_calls_from_expr(caller_id, parent_path, right, cx);
1295            }
1296            PureExpr::Unary { expr: inner, .. }
1297            | PureExpr::Field { expr: inner, .. }
1298            | PureExpr::Await(inner)
1299            | PureExpr::Try(inner)
1300            | PureExpr::Ref { expr: inner, .. }
1301            | PureExpr::Cast { expr: inner, .. } => {
1302                Self::build_calls_from_expr(caller_id, parent_path, inner, cx);
1303            }
1304            PureExpr::Index { expr: arr, index } => {
1305                Self::build_calls_from_expr(caller_id, parent_path, arr, cx);
1306                Self::build_calls_from_expr(caller_id, parent_path, index, cx);
1307            }
1308            PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
1309                for e in exprs {
1310                    Self::build_calls_from_expr(caller_id, parent_path, e, cx);
1311                }
1312            }
1313            PureExpr::Struct { fields, .. } => {
1314                for (_, e) in fields {
1315                    Self::build_calls_from_expr(caller_id, parent_path, e, cx);
1316                }
1317            }
1318            PureExpr::Return(Some(inner))
1319            | PureExpr::Break {
1320                expr: Some(inner), ..
1321            } => {
1322                Self::build_calls_from_expr(caller_id, parent_path, inner, cx);
1323            }
1324            PureExpr::Range { start, end, .. } => {
1325                if let Some(s) = start {
1326                    Self::build_calls_from_expr(caller_id, parent_path, s, cx);
1327                }
1328                if let Some(e) = end {
1329                    Self::build_calls_from_expr(caller_id, parent_path, e, cx);
1330                }
1331            }
1332            PureExpr::Let { expr: inner, .. } => {
1333                Self::build_calls_from_expr(caller_id, parent_path, inner, cx);
1334            }
1335            PureExpr::Repeat { expr: elem, len } => {
1336                Self::build_calls_from_expr(caller_id, parent_path, elem, cx);
1337                Self::build_calls_from_expr(caller_id, parent_path, len, cx);
1338            }
1339            _ => {}
1340        }
1341    }
1342
1343    /// Resolve a type/trait reference to a SymbolId.
1344    ///
1345    /// Tries multiple resolution strategies:
1346    /// 1. UseResolver (import resolution via use statements)
1347    /// 2. Qualified path as-is (e.g., "std::io::Read")
1348    /// 3. Simple name in current module (e.g., "Foo" -> "module::Foo")
1349    /// 4. Simple name in parent modules
1350    fn resolve_type_reference(
1351        parent_path: &str,
1352        type_name: &str,
1353        symbol_ids: &HashMap<String, SymbolId>,
1354        use_resolver: &UseResolver,
1355        registry: &SymbolRegistry,
1356    ) -> Option<SymbolId> {
1357        // Skip primitive types
1358        let primitives = [
1359            "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize",
1360            "f32", "f64", "bool", "char", "str", "Self",
1361        ];
1362        if primitives.contains(&type_name) {
1363            return None;
1364        }
1365
1366        // Extract the module path by stripping impl segments and method names.
1367        // UseResolver stores import maps keyed by module path (e.g., "mylib::mod_a"),
1368        // not by impl block path (e.g., "mylib::mod_a::<impl Trait for Type>::method").
1369        let module_path_str = strip_to_module_path(parent_path);
1370
1371        // 1. Try UseResolver for import resolution
1372        if let Ok(module_path) = SymbolPath::parse(&module_path_str) {
1373            if let Some(id) = use_resolver.resolve(&module_path, type_name, registry) {
1374                return Some(id);
1375            }
1376        }
1377
1378        // 2. Try qualified path as-is
1379        if type_name.contains("::") {
1380            if let Some(&id) = symbol_ids.get(type_name) {
1381                return Some(id);
1382            }
1383
1384            // 2b. Split at first "::" and resolve prefix via imports, then append suffix.
1385            // e.g., "Router::new" → resolve "Router" via imports → "mylib::types::Router" + "::new"
1386            if let Some(split_pos) = type_name.find("::") {
1387                let prefix = &type_name[..split_pos];
1388                let suffix = &type_name[split_pos..]; // includes "::"
1389                if let Ok(module_path) = SymbolPath::parse(&module_path_str) {
1390                    if let Some(resolved_prefix_id) =
1391                        use_resolver.resolve(&module_path, prefix, registry)
1392                    {
1393                        let resolved_prefix_path = registry.path(resolved_prefix_id);
1394                        if let Some(full_path_str) = resolved_prefix_path {
1395                            let combined = format!("{}{}", full_path_str, suffix);
1396                            if let Some(&id) = symbol_ids.get(&combined) {
1397                                return Some(id);
1398                            }
1399                        }
1400                    }
1401                }
1402            }
1403        }
1404
1405        // 3. Try in current module
1406        let qualified = format!("{}::{}", parent_path, type_name);
1407        if let Some(&id) = symbol_ids.get(&qualified) {
1408            return Some(id);
1409        }
1410
1411        // 4. Try in parent modules
1412        let mut current_path = parent_path.to_string();
1413        while let Some(parent) = get_parent_path(&current_path) {
1414            let qualified = format!("{}::{}", parent, type_name);
1415            if let Some(&id) = symbol_ids.get(&qualified) {
1416                return Some(id);
1417            }
1418            current_path = parent;
1419        }
1420
1421        // 5. Try as top-level (crate root)
1422        symbol_ids.get(type_name).copied()
1423    }
1424
1425    /// Get the registry.
1426    pub fn registry(&self) -> &SymbolRegistry {
1427        &self.registry
1428    }
1429
1430    /// Get mutable registry.
1431    pub fn registry_mut(&mut self) -> &mut SymbolRegistry {
1432        &mut self.registry
1433    }
1434
1435    /// Get the code graph.
1436    pub fn code_graph(&self) -> &CodeGraphV2 {
1437        &self.code_graph
1438    }
1439
1440    /// Get mutable code graph.
1441    pub fn code_graph_mut(&mut self) -> &mut CodeGraphV2 {
1442        &mut self.code_graph
1443    }
1444
1445    /// Get the TypeFlow graph.
1446    pub fn typeflow_graph(&self) -> &TypeFlowGraphV2 {
1447        &self.typeflow_graph
1448    }
1449
1450    /// Get the workspace root path.
1451    pub fn workspace_root(&self) -> &Path {
1452        &self.workspace_root
1453    }
1454
1455    /// Get a file by path.
1456    pub fn file(&self, path: &WorkspaceFilePath) -> Option<&PureFile> {
1457        self.files.get(path).map(|arc| arc.as_ref())
1458    }
1459
1460    /// Get mutable file by path (copy-on-write).
1461    ///
1462    /// Uses `Arc::make_mut` for copy-on-write semantics:
1463    /// - If Arc is uniquely owned, returns mutable reference directly
1464    /// - If Arc is shared, clones the PureFile first
1465    pub fn file_mut(&mut self, path: &WorkspaceFilePath) -> Option<&mut PureFile> {
1466        self.files.get_mut(path).map(Arc::make_mut)
1467    }
1468
1469    /// Get all files.
1470    pub fn files(&self) -> &ImHashMap<WorkspaceFilePath, Arc<PureFile>> {
1471        &self.files
1472    }
1473
1474    /// Get mutable files map.
1475    pub fn files_mut(&mut self) -> &mut ImHashMap<WorkspaceFilePath, Arc<PureFile>> {
1476        &mut self.files
1477    }
1478
1479    /// Get original source for a file (for diff generation).
1480    pub fn original(&self, path: &WorkspaceFilePath) -> Option<&String> {
1481        self.original.get(path)
1482    }
1483
1484    /// Get file count.
1485    pub fn file_count(&self) -> usize {
1486        self.files.len()
1487    }
1488
1489    /// Check if context is empty.
1490    pub fn is_empty(&self) -> bool {
1491        self.files.is_empty()
1492    }
1493
1494    // ========================================================================
1495    // Detail Store Access
1496    // ========================================================================
1497
1498    /// Get the detail store.
1499    pub fn detail_store(&self) -> &DetailStore {
1500        &self.detail_store
1501    }
1502
1503    /// Get mutable detail store.
1504    pub fn detail_store_mut(&mut self) -> &mut DetailStore {
1505        &mut self.detail_store
1506    }
1507
1508    // ========================================================================
1509    // ========================================================================
1510    // Tick Boundary Operations
1511    // ========================================================================
1512
1513    /// Commit changes at Tick boundary.
1514    ///
1515    /// Applies registry updates and incrementally updates CodeGraphV2 indices.
1516    /// This is O(k) where k is the number of updates, not O(n) for full rebuild.
1517    ///
1518    /// # Arguments
1519    ///
1520    /// * `updates` - Registry updates collected during this Tick
1521    ///
1522    /// # Performance
1523    ///
1524    /// | Operation | Before (rebuild_indices) | After (incremental) |
1525    /// |-----------|--------------------------|---------------------|
1526    /// | 10 updates, 10k symbols | O(10k) | O(10) |
1527    /// | 100 updates, 10k symbols | O(10k) | O(100) |
1528    pub fn commit_changes(&mut self, updates: &RegistryUpdateBatch) {
1529        // Collect affected IDs before applying updates (for detail store)
1530        let affected_ids: Vec<SymbolId> =
1531            updates.into_iter().filter_map(|u| u.target_id()).collect();
1532
1533        // 1. Apply registry updates and update graph incrementally
1534        for update in updates {
1535            // Get old kind before applying (for UpdateKind)
1536            let _old_kind = match update {
1537                RegistryUpdate::UpdateKind { id, .. } => self.registry.kind(*id),
1538                _ => None,
1539            };
1540
1541            // Apply to registry
1542            if let Err(e) = update.clone().apply(&mut self.registry) {
1543                eprintln!("Warning: Failed to apply registry update: {:?}", e);
1544                continue;
1545            }
1546
1547            // Update graph incrementally
1548            match update {
1549                RegistryUpdate::Add { path, kind, .. } => {
1550                    if let Some(id) = self.registry.lookup(path) {
1551                        self.code_graph.add_node(id);
1552                        self.code_graph.add_to_kind_index(id, *kind);
1553                    }
1554                }
1555                RegistryUpdate::Remove { id } => {
1556                    self.code_graph.remove_node(*id);
1557                }
1558                RegistryUpdate::UpdateKind { id, new_kind } => {
1559                    // Note: CodeGraphV2 kind index is updated by re-adding
1560                    // (remove_from_index not needed as add_to_kind_index is idempotent)
1561                    if self.code_graph.contains(*id) {
1562                        self.code_graph.add_to_kind_index(*id, *new_kind);
1563                    }
1564                }
1565                // Rename/UpdateSpan/UpdateVisibility don't affect graph structure
1566                RegistryUpdate::Rename { .. }
1567                | RegistryUpdate::UpdateSpan { .. }
1568                | RegistryUpdate::UpdateVisibility { .. } => {}
1569            }
1570        }
1571
1572        // 2. Update detail store for affected symbols
1573        // Resolve crate_name dynamically from files
1574        let crate_name = self
1575            .files
1576            .keys()
1577            .next()
1578            .and_then(|path| SymbolPathResolver::from_workspace_path(path).ok())
1579            .map(|r| r.crate_name().to_string())
1580            .unwrap_or_else(|| "crate".to_string());
1581        self.detail_store.rebuild_affected_workspace(
1582            &affected_ids,
1583            &self.registry,
1584            &self.files,
1585            &crate_name,
1586        );
1587    }
1588
1589    /// Rebuild edges for symbols in the specified files.
1590    ///
1591    /// This is used for incremental updates after mutation:
1592    /// 1. Clear outgoing edges for all symbols in the files
1593    /// 2. Rebuild edges from the updated AST
1594    ///
1595    /// # Performance
1596    ///
1597    /// O(k * m) where k is the number of files and m is symbols per file.
1598    /// Much faster than full rebuild for small changes.
1599    pub fn rebuild_edges_for_files(&mut self, file_paths: &[WorkspaceFilePath]) {
1600        // Collect symbol IDs and build symbol_ids map
1601        let mut symbol_ids: HashMap<String, SymbolId> = HashMap::new();
1602        for (id, _) in self.registry.iter() {
1603            if let Some(path) = self.registry.resolve(id) {
1604                symbol_ids.insert(path.to_string(), id);
1605            }
1606        }
1607
1608        for file_path in file_paths {
1609            // 1. Find symbols in this file and clear their outgoing edges
1610            for (id, _) in self.registry.iter() {
1611                if let Some(span) = self.registry.span(id) {
1612                    if &span.file == file_path {
1613                        self.code_graph.clear_outgoing_edges(id);
1614                    }
1615                }
1616            }
1617
1618            // 2. Rebuild edges from the file's AST
1619            if let Some(file) = self.files.get(file_path) {
1620                let file_crate_name = file_path.crate_name().as_str();
1621                let resolver = SymbolPathResolver::new(file_crate_name);
1622                let mod_path = resolver.module_path_str(file_path);
1623
1624                let method_index = build_method_name_index(&symbol_ids, &self.registry);
1625                Self::build_edges_from_items(
1626                    &mod_path,
1627                    &file.items,
1628                    &symbol_ids,
1629                    &self.use_resolver,
1630                    &self.registry,
1631                    &mut self.code_graph,
1632                    &method_index,
1633                );
1634            }
1635        }
1636    }
1637
1638    /// Rebuild code_graph edges for the given symbol IDs (Phase 2: Symbol-based).
1639    ///
1640    /// This method rebuilds edges directly from ASTRegistry without file I/O:
1641    /// 1. Clear outgoing edges for affected symbols
1642    /// 2. Get AST from ASTRegistry for each symbol
1643    /// 3. Rebuild edges from the AST
1644    ///
1645    /// # Performance
1646    ///
1647    /// O(S) where S is the number of affected symbols.
1648    /// Much faster than file-based updates for targeted changes.
1649    pub fn rebuild_edges_for_symbols(&mut self, affected_ids: &[SymbolId]) {
1650        if affected_ids.is_empty() {
1651            return;
1652        }
1653
1654        // Build symbol_ids map for edge resolution
1655        let mut symbol_ids: HashMap<String, SymbolId> = HashMap::new();
1656        for (id, _) in self.registry.iter() {
1657            if let Some(path) = self.registry.resolve(id) {
1658                symbol_ids.insert(path.to_string(), id);
1659            }
1660        }
1661
1662        // 1. Clear outgoing edges for affected symbols
1663        for &id in affected_ids {
1664            self.code_graph.clear_outgoing_edges(id);
1665        }
1666
1667        // 2. Rebuild edges from ASTRegistry
1668        for &id in affected_ids {
1669            // Get parent path from symbol path
1670            let parent_path = match self.registry.resolve(id) {
1671                Some(path) => {
1672                    // Get the parent module path (everything before the last segment)
1673                    let path_str = path.to_string();
1674                    path_str
1675                        .rsplit_once("::")
1676                        .map(|(parent, _)| parent.to_string())
1677                        .unwrap_or_else(|| path_str.clone())
1678                }
1679                None => continue,
1680            };
1681
1682            // Get AST from ASTRegistry
1683            if let Some(item) = self.ast_registry.get(id) {
1684                let method_index = build_method_name_index(&symbol_ids, &self.registry);
1685                Self::build_edges_from_item(
1686                    &parent_path,
1687                    item,
1688                    &symbol_ids,
1689                    &self.use_resolver,
1690                    &self.registry,
1691                    &mut self.code_graph,
1692                    &method_index,
1693                );
1694            }
1695        }
1696    }
1697
1698    /// Get all symbols defined in the specified files.
1699    ///
1700    /// Uses `registry.span()` to determine which file each symbol belongs to.
1701    ///
1702    /// # Returns
1703    /// Vector of SymbolIds for symbols defined in the given files.
1704    pub fn get_symbols_in_files(&self, file_paths: &[WorkspaceFilePath]) -> Vec<SymbolId> {
1705        let file_set: std::collections::HashSet<_> = file_paths.iter().collect();
1706        let mut symbols = Vec::new();
1707
1708        for (id, _) in self.registry.iter() {
1709            if let Some(span) = self.registry.span(id) {
1710                if file_set.contains(&span.file) {
1711                    symbols.push(id);
1712                }
1713            }
1714        }
1715
1716        symbols
1717    }
1718
1719    /// Rebuild all analysis graphs after mutation execution.
1720    ///
1721    /// Converts file paths to symbol IDs and delegates to the Symbol-based
1722    /// rebuild path. All graphs are rebuilt from ASTRegistry (no file I/O).
1723    ///
1724    /// Note: ast_registry and files are already updated by execute_v2.
1725    pub fn rebuild_after_mutation(&mut self, modified_files: &[WorkspaceFilePath]) {
1726        let affected_symbols = self.get_symbols_in_files(modified_files);
1727        self.rebuild_after_mutation_by_symbols(&affected_symbols);
1728    }
1729
1730    /// Rebuild analysis graphs for the given affected symbol IDs.
1731    ///
1732    /// This is the Symbol-based update path (Phase 2) that rebuilds all graphs
1733    /// directly from ASTRegistry, avoiding file reconstruction overhead.
1734    ///
1735    /// All graphs are now built from ASTRegistry (no file I/O):
1736    /// 1. code_graph: Incremental edge rebuild for affected symbols
1737    /// 2. typeflow_graph: Full rebuild from ASTRegistry
1738    /// 3. dataflow_graph: Incremental rebuild for affected symbols
1739    /// 4. detail_store: Incremental rebuild for affected symbols
1740    ///
1741    /// # Performance
1742    ///
1743    /// - code_graph: O(S) where S is affected symbols
1744    /// - typeflow_graph: O(N) where N is total symbols (full rebuild, but no file I/O)
1745    /// - dataflow_graph: O(S) incremental
1746    /// - detail_store: O(S) incremental
1747    pub fn rebuild_after_mutation_by_symbols(&mut self, affected_ids: &[SymbolId]) {
1748        if affected_ids.is_empty() {
1749            return;
1750        }
1751
1752        // Get crate_name for builders
1753        let crate_name = self
1754            .files
1755            .keys()
1756            .next()
1757            .map(|r| r.crate_name().to_string())
1758            .unwrap_or_else(|| "unknown".to_string());
1759
1760        // Extract unique file paths for components that still need file-based update
1761        let mut file_set = std::collections::HashSet::new();
1762        for &id in affected_ids {
1763            if let Some(span) = self.registry.span(id) {
1764                file_set.insert(span.file.clone());
1765            }
1766        }
1767        let _modified_files: Vec<_> = file_set.into_iter().collect();
1768
1769        // 1. Rebuild code_graph edges (Phase 2: pure symbol-based, no file I/O)
1770        self.rebuild_edges_for_symbols(affected_ids);
1771
1772        // 2. Rebuild typeflow_graph (Phase 2: pure symbol-based, no file I/O)
1773        // Full rebuild from ASTRegistry - avoids file reconstruction overhead.
1774        self.typeflow_graph =
1775            TypeFlowBuilderV2::build_from_ast_registry(&self.registry, &self.ast_registry);
1776
1777        // 3. Rebuild dataflow_graph (Phase 2: pure symbol-based, no file I/O)
1778        self.dataflow_graph.clear_for_symbols(affected_ids);
1779        DataFlowBuilderWorkspace::new(&self.registry, &self.files, &crate_name)
1780            .build_incremental_by_symbols(
1781                &mut self.dataflow_graph,
1782                &self.ast_registry,
1783                affected_ids,
1784            );
1785
1786        // 4. Rebuild detail_store (Phase 2: pure symbol-based, no file I/O)
1787        self.detail_store
1788            .rebuild_for_symbols(affected_ids, &self.ast_registry);
1789
1790        // 5. Rebuild derive_index (Phase 2: pure symbol-based, O(S) incremental)
1791        self.derive_index.rebuild_for_symbols(
1792            affected_ids,
1793            &self.ast_registry,
1794            &self.code_graph,
1795            &self.typeflow_graph,
1796            &self.registry,
1797        );
1798    }
1799
1800    // ========================================================================
1801    // Fork Operations (for Speculative Execution)
1802    // ========================================================================
1803
1804    /// Create an ExecutionContext for parallel Mutation execution.
1805    ///
1806    /// Registry and Graph are shared as read-only references.
1807    /// Files use O(log n) structural sharing via `im::HashMap`.
1808    ///
1809    /// # Design
1810    ///
1811    /// During parallel Mutation execution:
1812    /// - Registry: Read-only, shared across all Mutations
1813    /// - Graph: Read-only, shared across all Mutations
1814    /// - Files: O(log n) clone via structural sharing, copy-on-write
1815    /// - Changes to Registry are collected as `RegistryUpdate` deltas
1816    ///
1817    /// # Performance
1818    ///
1819    /// Clone is O(log n) due to `im::HashMap` structural sharing.
1820    /// Modifications use copy-on-write via `Arc::make_mut`.
1821    ///
1822    /// # Example
1823    ///
1824    /// ```ignore
1825    /// let ctx = AnalysisContext::from_path_files(files, "my_crate");
1826    /// let mut exec_ctx = ctx.fork();  // O(log n) clone
1827    ///
1828    /// // Modify files in exec_ctx - original is unchanged
1829    /// exec_ctx.file_mut(&path).unwrap().modify(...);  // Copy-on-write
1830    ///
1831    /// // Registry is read-only
1832    /// let symbol = exec_ctx.registry.lookup(&path);
1833    /// ```
1834    pub fn fork(&self) -> ExecutionContext<'_> {
1835        ExecutionContext {
1836            workspace_root: &self.workspace_root,
1837            registry: &self.registry,
1838            graph: &self.code_graph,
1839            files: self.files.clone(), // O(log n) structural sharing
1840        }
1841    }
1842
1843    /// Create a new AnalysisContext with files cloned.
1844    ///
1845    /// Registry, graph, and detail store are rebuilt from the cloned files.
1846    /// Use this when you need a completely independent context with
1847    /// mutable Registry access.
1848    ///
1849    /// # Note
1850    ///
1851    /// This is more expensive than `fork()` as it rebuilds all indices.
1852    /// Prefer `fork()` for parallel Mutation execution.
1853    ///
1854    /// # Panics
1855    /// Panics if the current context has zero files (cannot infer
1856    /// crate_name during rebuild) or if any file fails source generation
1857    /// during rebuild. Either situation indicates a corrupted internal
1858    /// state that should not occur in normal use.
1859    pub fn fork_rebuild(&self) -> Self {
1860        // Convert im::HashMap<WorkspaceFilePath, Arc<PureFile>> back to HashMap
1861        let files: HashMap<WorkspaceFilePath, PureFile> = self
1862            .files
1863            .iter()
1864            .map(|(path, arc)| (path.clone(), (**arc).clone()))
1865            .collect();
1866        Self::build_from_workspace_files(
1867            files,
1868            self.workspace_root.clone(),
1869            AnalysisConfig::default(),
1870        )
1871        .expect("fork_rebuild: source generation failed")
1872    }
1873
1874    /// Create an owned clone of AnalysisContext without rebuilding.
1875    ///
1876    /// This is much faster than `fork_rebuild()` because it clones the
1877    /// existing graph structures directly instead of rebuilding from files.
1878    ///
1879    /// # Performance
1880    ///
1881    /// - Files: O(log n) structural sharing via `im::HashMap`
1882    /// - All other fields: Direct Clone (no rebuild)
1883    ///
1884    /// Expected: < 1ms vs ~100ms for `fork_rebuild()`
1885    ///
1886    /// # Use Case
1887    ///
1888    /// Use this for precheck scenarios where you need a mutable owned
1889    /// context but don't need to rebuild graphs from scratch.
1890    pub fn fork_clone(&self) -> Self {
1891        Self {
1892            workspace_root: self.workspace_root.clone(),
1893            registry: self.registry.clone(),
1894            code_graph: self.code_graph.clone(),
1895            typeflow_graph: self.typeflow_graph.clone(),
1896            dataflow_graph: self.dataflow_graph.clone(),
1897            detail_store: self.detail_store.clone(),
1898            ast_registry: self.ast_registry.clone(),
1899            files: self.files.clone(), // O(log n) structural sharing
1900            original: self.original.clone(),
1901            use_resolver: self.use_resolver.clone(),
1902            // LiteralIndex is not cloned (Tantivy Index is complex).
1903            // Precheck doesn't need literal search, so None is acceptable.
1904            #[cfg(feature = "literal-search")]
1905            literal_index: None,
1906            derive_index: self.derive_index.clone(),
1907        }
1908    }
1909
1910    /// Get the number of registered symbols.
1911    pub fn symbol_count(&self) -> usize {
1912        self.registry.len()
1913    }
1914
1915    // ========================================================================
1916    // Snapshot/Rollback for Efficient Speculative Execution
1917    // ========================================================================
1918
1919    /// Take a snapshot of specified symbols before mutation.
1920    ///
1921    /// This enables efficient speculative execution by only saving the
1922    /// symbols that will be modified, rather than cloning the entire context.
1923    ///
1924    /// # Usage
1925    /// ```ignore
1926    /// let snapshot = ctx.snapshot_symbols(&affected_ids);
1927    /// // ... apply mutation ...
1928    /// ctx.rollback(snapshot, &affected_ids);
1929    /// ```
1930    pub fn snapshot_symbols(&self, symbols: &[SymbolId]) -> ContextSnapshot {
1931        let ast_items: HashMap<SymbolId, PureItem> = symbols
1932            .iter()
1933            .filter_map(|&id| self.ast_registry.get(id).map(|item| (id, item.clone())))
1934            .collect();
1935
1936        ContextSnapshot { ast_items }
1937    }
1938
1939    /// Rollback to a previous snapshot.
1940    ///
1941    /// Restores AST items and rebuilds analysis graphs for affected symbols.
1942    /// This is more efficient than fork_clone when processing many mutations
1943    /// sequentially on the same context.
1944    pub fn rollback(&mut self, snapshot: ContextSnapshot, affected_ids: &[SymbolId]) {
1945        // 1. Restore AST items
1946        for (id, item) in snapshot.ast_items {
1947            self.ast_registry.set(id, item);
1948        }
1949
1950        // 2. Rebuild analysis graphs for affected symbols
1951        // This restores code_graph, typeflow_graph, dataflow_graph, detail_store, derive_index
1952        if !affected_ids.is_empty() {
1953            self.rebuild_after_mutation_by_symbols(affected_ids);
1954        }
1955    }
1956}
1957
1958/// Snapshot of context state for rollback.
1959///
1960/// Used by `AnalysisContext::snapshot_symbols()` and `rollback()` for
1961/// efficient speculative execution without full context cloning.
1962#[derive(Debug, Clone)]
1963pub struct ContextSnapshot {
1964    /// Saved AST items (only the symbols that will be modified).
1965    pub ast_items: HashMap<SymbolId, PureItem>,
1966}
1967
1968// ============================================================================
1969// ExecutionContext - Fork for Parallel Mutation Execution
1970// ============================================================================
1971
1972/// Context for parallel Mutation execution.
1973///
1974/// Created by `AnalysisContext::fork()`. Shares Registry and Graph as
1975/// read-only references, with Files using O(log n) structural sharing.
1976///
1977/// # Design
1978///
1979/// ```text
1980/// AnalysisContext (owner)
1981/// ├── workspace_root: Arc<Path>   │
1982/// ├── registry: SymbolRegistry  ──┤
1983/// ├── graph: CodeGraphV2         │  shared (read-only)
1984/// └── files: im::HashMap<...>    │
1985///                                 │
1986/// ExecutionContext<'a>            │
1987/// ├── workspace_root: &'a Path     ←┘
1988/// ├── registry: &'a SymbolRegistry ←┘
1989/// ├── graph: &'a CodeGraphV2      ←┘
1990/// └── files: im::HashMap<...>  (O(log n) clone, copy-on-write)
1991/// ```
1992///
1993/// # Performance
1994///
1995/// - Clone: O(log n) via `im::HashMap` structural sharing
1996/// - Modification: Copy-on-write via `Arc::make_mut`
1997///
1998/// # Collecting Changes
1999///
2000/// Mutations return `RegistryUpdate` deltas instead of modifying Registry directly.
2001/// These are collected and applied to AnalysisContext at Tick end.
2002///
2003/// See `docs/parallel-execution-design.md` for the full design.
2004pub struct ExecutionContext<'a> {
2005    /// Read-only reference to workspace root.
2006    pub workspace_root: &'a Path,
2007
2008    /// Read-only reference to SymbolRegistry.
2009    ///
2010    /// Mutations should collect changes as `RegistryUpdate` instead of
2011    /// modifying directly.
2012    pub registry: &'a SymbolRegistry,
2013
2014    /// Read-only reference to CodeGraphV2.
2015    pub graph: &'a CodeGraphV2,
2016
2017    /// Files with O(log n) clone via structural sharing.
2018    ///
2019    /// Uses `im::HashMap<WorkspaceFilePath, Arc<PureFile>>` for:
2020    /// - O(log n) clone (structural sharing)
2021    /// - Copy-on-write modification via `Arc::make_mut`
2022    pub files: ImHashMap<WorkspaceFilePath, Arc<PureFile>>,
2023}
2024
2025impl<'a> ExecutionContext<'a> {
2026    /// Get a file by path.
2027    pub fn file(&self, path: &WorkspaceFilePath) -> Option<&PureFile> {
2028        self.files.get(path).map(|arc| arc.as_ref())
2029    }
2030
2031    /// Get a mutable file by path (copy-on-write).
2032    ///
2033    /// Uses `Arc::make_mut` for copy-on-write semantics:
2034    /// - If Arc is uniquely owned, returns mutable reference directly
2035    /// - If Arc is shared, clones the PureFile first
2036    pub fn file_mut(&mut self, path: &WorkspaceFilePath) -> Option<&mut PureFile> {
2037        self.files.get_mut(path).map(Arc::make_mut)
2038    }
2039
2040    /// Check if a file exists.
2041    pub fn has_file(&self, path: &WorkspaceFilePath) -> bool {
2042        self.files.contains_key(path)
2043    }
2044
2045    /// Get the number of files.
2046    pub fn file_count(&self) -> usize {
2047        self.files.len()
2048    }
2049}
2050
2051/// Convert PureVis to Visibility.
2052fn pure_vis_to_visibility(pure_vis: &PureVis) -> Visibility {
2053    match pure_vis {
2054        PureVis::Public => Visibility::Public,
2055        PureVis::Crate => Visibility::Crate,
2056        PureVis::Super => Visibility::Super,
2057        PureVis::Private => Visibility::Private,
2058        PureVis::In(path) => {
2059            // Try to parse as SymbolPath, fallback to Private if invalid
2060            SymbolPath::parse(path)
2061                .map(|p| Visibility::Restricted(Box::new(p)))
2062                .unwrap_or(Visibility::Private)
2063        }
2064    }
2065}
2066
2067/// Get parent path from a symbol path string.
2068fn get_parent_path(path: &str) -> Option<String> {
2069    let parts: Vec<&str> = path.rsplitn(2, "::").collect();
2070    if parts.len() == 2 {
2071        Some(parts[1].to_string())
2072    } else {
2073        None
2074    }
2075}
2076
2077/// Strip impl segments and trailing names from a path to get the module path.
2078///
2079/// UseResolver stores import maps keyed by module path (e.g., "mylib::module"),
2080/// but during edge building, parent_path may include impl block segments:
2081/// - `"mylib::mod_a::<impl Trait for Type>::method"` → `"mylib::mod_a"`
2082/// - `"mylib::mod_a::<impl Type>::method"` → `"mylib::mod_a"`
2083/// - `"mylib::mod_a::function"` → `"mylib::mod_a"` (if function isn't a module)
2084/// - `"mylib::mod_a"` → `"mylib::mod_a"` (already a module path)
2085fn strip_to_module_path(path: &str) -> String {
2086    // Find the first impl segment and truncate before it
2087    if let Some(impl_pos) = path.find("::<impl ") {
2088        return path[..impl_pos].to_string();
2089    }
2090    path.to_string()
2091}
2092
2093/// Check if a method name is a common std trait method that would produce too many false positives.
2094///
2095/// These methods are implemented by many types (Display::fmt, Clone::clone, etc.)
2096/// and name-based matching would create noise rather than signal.
2097fn is_common_trait_method(method: &str) -> bool {
2098    matches!(
2099        method,
2100        "new"
2101            | "default"
2102            | "fmt"
2103            | "clone"
2104            | "eq"
2105            | "ne"
2106            | "cmp"
2107            | "partial_cmp"
2108            | "hash"
2109            | "from"
2110            | "into"
2111            | "try_from"
2112            | "try_into"
2113            | "as_ref"
2114            | "as_mut"
2115            | "deref"
2116            | "deref_mut"
2117            | "drop"
2118            | "next"
2119            | "into_iter"
2120            | "iter"
2121            | "len"
2122            | "is_empty"
2123    )
2124}
2125
2126/// Extract a type hint from a method call receiver expression.
2127///
2128/// Without full type inference, we can only extract type information
2129/// from certain receiver patterns:
2130/// - `Type::method(...)` → type hint is "Type"
2131/// - `Type { ... }` → type hint is "Type"
2132///
2133/// Returns the base type name (without generics) if extractable.
2134fn extract_receiver_type_hint(receiver: &PureExpr) -> Option<&str> {
2135    match receiver {
2136        // Type::method(...) → e.g., Json::new() → "Json"
2137        PureExpr::Call { func, .. } => {
2138            if let PureExpr::Path(path) = func.as_ref() {
2139                // "Json::new" → "Json", "std::collections::HashMap::new" → "HashMap"
2140                let segments: Vec<&str> = path.rsplitn(2, "::").collect();
2141                if segments.len() == 2 {
2142                    // Return the segment before the last `::`
2143                    // For "Json::new" → segments = ["new", "Json"]
2144                    return Some(segments[1].rsplit("::").next().unwrap_or(segments[1]));
2145                }
2146            }
2147            None
2148        }
2149        // StructType { ... } → "StructType"
2150        PureExpr::Struct { path, .. } => path.rsplit("::").next(),
2151        _ => None,
2152    }
2153}
2154
2155/// Extract the self type name from a parent path (impl block path).
2156///
2157/// For trait impl: `mylib::<impl MyTrait for MyStruct>` → `Some("MyStruct")`
2158/// For inherent impl: `mylib::<impl MyStruct>` → `Some("MyStruct")`
2159/// For plain type: `mylib::MyStruct` → `Some("MyStruct")`
2160/// For non-impl: `mylib::module` → `Some("module")`
2161///
2162/// Used to provide implicit type hints for `self.method()` calls
2163/// when explicit type hints are unavailable.
2164fn extract_self_type_from_parent_path(parent_path: &str) -> Option<&str> {
2165    // Check if parent_path ends with an impl segment: ...::<impl ...>
2166    if let Some(impl_start) = parent_path.rfind("::<impl ") {
2167        let impl_segment = &parent_path[impl_start + 2..]; // skip "::"
2168                                                           // impl_segment = "<impl Trait for Type>" or "<impl Type>"
2169        let inner = impl_segment.strip_prefix("<impl ")?.strip_suffix('>')?;
2170
2171        // Extract self_ty: after " for " if trait impl, otherwise the whole inner
2172        let self_ty = if let Some(pos) = inner.find(" for ") {
2173            &inner[pos + 5..]
2174        } else {
2175            inner
2176        };
2177
2178        // Strip generics: "MyStruct < T >" → "MyStruct"
2179        let base = self_ty.split('<').next().unwrap_or(self_ty).trim();
2180        if !base.is_empty() {
2181            return Some(base);
2182        }
2183    }
2184
2185    // Fallback: last segment of the path (for inherent methods under Type::method)
2186    parent_path.rsplit("::").next()
2187}
2188
2189/// Check if a method candidate's impl self-type matches the given type hint.
2190///
2191/// The candidate's SymbolPath contains an impl segment like `<impl Trait for Type>`.
2192/// We extract the base type name and compare it against the hint.
2193fn candidate_matches_type_hint(
2194    candidate_id: SymbolId,
2195    type_hint: &str,
2196    registry: &SymbolRegistry,
2197) -> bool {
2198    if let Some(path) = registry.path(candidate_id) {
2199        for segment in path.segment_refs() {
2200            if segment.is_impl() {
2201                if let Some(self_ty) = segment.impl_self_ty() {
2202                    // Strip generics: "Json < T >" → "Json"
2203                    let base = self_ty.split('<').next().unwrap_or(self_ty).trim();
2204                    // Strip leading path: "my_crate::Json" → "Json"
2205                    let base_name = base.rsplit("::").next().unwrap_or(base);
2206                    return base_name == type_hint;
2207                }
2208            }
2209        }
2210        // Inherent method without impl segment: check parent
2211        // Path like "mylib::Json::method" → parent type is "Json"
2212        let segments: Vec<&str> = path.segments().collect();
2213        if segments.len() >= 2 {
2214            let parent_name = segments[segments.len() - 2];
2215            return parent_name == type_hint;
2216        }
2217    }
2218    false
2219}
2220
2221/// Reverse index: method name → Vec<SymbolId>.
2222///
2223/// Used for heuristic resolution of method calls without type inference.
2224type MethodNameIndex = HashMap<String, Vec<SymbolId>>;
2225
2226/// Shared context passed through `build_calls_from_*` recursion to avoid
2227/// exceeding clippy's `too_many_arguments` limit.
2228struct CallsBuildContext<'a> {
2229    symbol_ids: &'a HashMap<String, SymbolId>,
2230    use_resolver: &'a UseResolver,
2231    registry: &'a SymbolRegistry,
2232    graph: &'a mut CodeGraphV2,
2233    method_index: &'a MethodNameIndex,
2234}
2235
2236/// Build a method name index from symbol_ids.
2237///
2238/// Extracts the last segment of each qualified path and maps it to the SymbolId.
2239/// Only includes symbols that are actually callable (Function or Method).
2240/// Modules, structs, enums, traits, etc. with the same name are excluded.
2241fn build_method_name_index(
2242    symbol_ids: &HashMap<String, SymbolId>,
2243    registry: &SymbolRegistry,
2244) -> MethodNameIndex {
2245    let mut index: MethodNameIndex = HashMap::new();
2246    for (path, &id) in symbol_ids {
2247        // Only include callable symbols (Function or Method)
2248        let kind = registry.kind(id);
2249        let is_callable = matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Method));
2250        if !is_callable {
2251            continue;
2252        }
2253        if let Some(method_name) = path.rsplit("::").next() {
2254            index.entry(method_name.to_string()).or_default().push(id);
2255        }
2256    }
2257    index
2258}
2259
2260#[cfg(test)]
2261mod tests {
2262    use super::*;
2263    use ryo_symbol::{TestWorkspace, WorkspaceFilePath};
2264
2265    /// Build AnalysisContext from TestWorkspace
2266    fn build_context_from_workspace(
2267        workspace: &TestWorkspace,
2268        crate_name: &str,
2269    ) -> AnalysisContext {
2270        let files: HashMap<WorkspaceFilePath, PureFile> = workspace
2271            .files_in_crate(crate_name)
2272            .into_iter()
2273            .filter_map(|path| {
2274                let abs = path.to_absolute();
2275                let content = std::fs::read_to_string(&abs).ok()?;
2276                let file = PureFile::from_source(&content).ok()?;
2277                Some((path, file))
2278            })
2279            .collect();
2280        let workspace_root = Arc::from(workspace.workspace_root());
2281        AnalysisContext::build_from_workspace_files(
2282            files,
2283            workspace_root,
2284            AnalysisConfig::default(),
2285        )
2286        .expect("build_from_workspace_files failed in test helper")
2287    }
2288
2289    #[test]
2290    fn test_get_parent_path() {
2291        assert_eq!(
2292            get_parent_path("mylib::handlers::handle"),
2293            Some("mylib::handlers".to_string())
2294        );
2295        assert_eq!(get_parent_path("mylib::foo"), Some("mylib".to_string()));
2296        assert_eq!(get_parent_path("mylib"), None);
2297    }
2298
2299    #[test]
2300    fn test_empty_context() {
2301        // Create minimal workspace with empty lib.rs
2302        let workspace = TestWorkspace::builder()
2303            .crate_with_source("test_crate", "src/lib.rs", "")
2304            .build();
2305
2306        let ctx = build_context_from_workspace(&workspace, "test_crate");
2307
2308        // Context should have minimal content (one empty file)
2309        assert_eq!(ctx.file_count(), 1);
2310        assert!(ctx.code_graph.node_count() <= 1);
2311    }
2312
2313    #[test]
2314    fn test_context_with_files() {
2315        let workspace = TestWorkspace::builder()
2316            .crate_with_source("mylib", "src/lib.rs", "pub fn foo() {}")
2317            .build();
2318
2319        let ctx = build_context_from_workspace(&workspace, "mylib");
2320
2321        assert_eq!(ctx.file_count(), 1);
2322        // Get file by WorkspaceFilePath
2323        let workspace_path = ctx.files.keys().next().expect("should have one file");
2324        assert!(ctx.file(workspace_path).is_some());
2325        assert!(ctx.original(workspace_path).is_some());
2326
2327        // Symbol should be registered
2328        let foo_path = SymbolPath::parse("mylib::foo").unwrap();
2329        assert!(
2330            ctx.registry.lookup(&foo_path).is_some(),
2331            "foo should be registered"
2332        );
2333    }
2334
2335    #[test]
2336    fn test_fork_creates_independent_files() {
2337        let workspace = TestWorkspace::builder()
2338            .crate_with_source("mylib", "src/lib.rs", "pub fn foo() {}")
2339            .build();
2340
2341        let original = build_context_from_workspace(&workspace, "mylib");
2342        let workspace_path = original.files.keys().next().expect("file exists").clone();
2343        let mut forked = original.fork();
2344
2345        // Replace the Arc with a new one containing different content
2346        forked.files.insert(
2347            workspace_path.clone(),
2348            Arc::new(PureFile::from_source("pub fn bar() {}").unwrap()),
2349        );
2350
2351        // Original should be unchanged
2352        let original_source = original.file(&workspace_path).unwrap().to_source().unwrap();
2353        let forked_source = forked.file(&workspace_path).unwrap().to_source().unwrap();
2354
2355        assert!(original_source.contains("foo"), "Original should have foo");
2356        assert!(forked_source.contains("bar"), "Forked should have bar");
2357        assert!(
2358            !original_source.contains("bar"),
2359            "Original should not have bar"
2360        );
2361    }
2362
2363    #[test]
2364    fn test_fork_shares_registry() {
2365        let workspace = TestWorkspace::builder()
2366            .crate_with_source("mylib", "src/lib.rs", "pub struct Foo {}")
2367            .build();
2368
2369        let original = build_context_from_workspace(&workspace, "mylib");
2370        let forked = original.fork();
2371
2372        // Forked registry is a reference to the same registry
2373        // (pointer equality - same memory address)
2374        assert!(std::ptr::eq(
2375            &original.registry as *const _,
2376            forked.registry as *const _
2377        ));
2378
2379        // Both should have the same number of symbols
2380        assert_eq!(original.registry.len(), forked.registry.len());
2381    }
2382
2383    #[test]
2384    fn test_fork_rebuild_creates_independent_context() {
2385        let workspace = TestWorkspace::builder()
2386            .crate_with_source("mylib", "src/lib.rs", "pub fn foo() {}")
2387            .build();
2388
2389        let original = build_context_from_workspace(&workspace, "mylib");
2390        let rebuilt = original.fork_rebuild();
2391
2392        // Rebuilt has its own registry (different memory address)
2393        assert!(!std::ptr::eq(
2394            &original.registry as *const _,
2395            &rebuilt.registry as *const _
2396        ));
2397
2398        // But same content
2399        assert_eq!(original.registry.len(), rebuilt.registry.len());
2400    }
2401
2402    #[test]
2403    fn test_execution_context_file_access() {
2404        let workspace = TestWorkspace::builder()
2405            .crate_with_source("mylib", "src/lib.rs", "pub fn test_fn() {}")
2406            .build();
2407
2408        let ctx = build_context_from_workspace(&workspace, "mylib");
2409        let workspace_path = ctx.files.keys().next().expect("file exists").clone();
2410        let exec_ctx = ctx.fork();
2411
2412        assert!(exec_ctx.has_file(&workspace_path));
2413        assert_eq!(exec_ctx.file_count(), 1);
2414
2415        let file = exec_ctx.file(&workspace_path).unwrap();
2416        assert!(file.to_source().unwrap().contains("test_fn"));
2417    }
2418
2419    // ========================================================================
2420    // commit_changes incremental update tests
2421    // ========================================================================
2422
2423    #[test]
2424    fn test_commit_changes_add_symbol() {
2425        use crate::SymbolKind;
2426        use crate::{FileSpan, RegistryUpdate, RegistryUpdateBatch, SymbolPath};
2427
2428        // Create minimal workspace to satisfy crate_name requirement
2429        let workspace = TestWorkspace::builder()
2430            .crate_with_source("testcrate", "src/lib.rs", "")
2431            .build();
2432        let mut ctx = build_context_from_workspace(&workspace, "testcrate");
2433
2434        // Create a batch with an Add update
2435        let mut batch = RegistryUpdateBatch::new();
2436        let path = SymbolPath::parse("testcrate::NewStruct").unwrap();
2437        let dummy_span = FileSpan::new(
2438            WorkspaceFilePath::new_for_test("src/test.rs", "/project", "testcrate"),
2439            0,
2440            100,
2441        );
2442        batch.push(RegistryUpdate::Add {
2443            path: path.clone(),
2444            kind: SymbolKind::Struct,
2445            span: dummy_span,
2446        });
2447
2448        let initial_nodes = ctx.code_graph.node_count();
2449        ctx.commit_changes(&batch);
2450
2451        // Verify symbol was added to registry
2452        let id = ctx.registry.lookup(&path);
2453        assert!(id.is_some(), "Symbol should be registered");
2454
2455        // Verify node was added to graph
2456        assert_eq!(
2457            ctx.code_graph.node_count(),
2458            initial_nodes + 1,
2459            "Graph should have one more node"
2460        );
2461    }
2462
2463    #[test]
2464    fn test_commit_changes_remove_symbol() {
2465        use crate::{RegistryUpdate, RegistryUpdateBatch, SymbolPath};
2466
2467        // Create context with existing symbol
2468        let workspace = TestWorkspace::builder()
2469            .crate_with_source("mylib", "src/lib.rs", "pub struct Foo {}")
2470            .build();
2471
2472        let mut ctx = build_context_from_workspace(&workspace, "mylib");
2473
2474        // Find the Foo struct's ID
2475        let foo_path = SymbolPath::parse("mylib::Foo").unwrap();
2476        let foo_id = ctx.registry.lookup(&foo_path);
2477        assert!(foo_id.is_some(), "Foo should exist initially");
2478        let foo_id = foo_id.unwrap();
2479
2480        let initial_nodes = ctx.code_graph.node_count();
2481
2482        // Create a batch with a Remove update
2483        let mut batch = RegistryUpdateBatch::new();
2484        batch.push(RegistryUpdate::Remove { id: foo_id });
2485
2486        ctx.commit_changes(&batch);
2487
2488        // Verify symbol was removed from registry
2489        let foo_path_check = SymbolPath::parse("mylib::Foo").unwrap();
2490        assert!(
2491            ctx.registry.lookup(&foo_path_check).is_none(),
2492            "Symbol should be removed from registry"
2493        );
2494
2495        // Verify node was removed from graph
2496        assert_eq!(
2497            ctx.code_graph.node_count(),
2498            initial_nodes - 1,
2499            "Graph should have one fewer node"
2500        );
2501    }
2502
2503    #[test]
2504    fn test_commit_changes_update_kind() {
2505        use crate::SymbolKind;
2506        use crate::{RegistryUpdate, RegistryUpdateBatch, SymbolPath};
2507
2508        // Create context with existing symbol
2509        let workspace = TestWorkspace::builder()
2510            .crate_with_source("mylib", "src/lib.rs", "pub struct Foo {}")
2511            .build();
2512
2513        let mut ctx = build_context_from_workspace(&workspace, "mylib");
2514
2515        // Find the Foo struct's ID
2516        let foo_path = SymbolPath::parse("mylib::Foo").unwrap();
2517        let foo_id = ctx.registry.lookup(&foo_path).expect("Foo should exist");
2518
2519        // Verify initial kind
2520        assert_eq!(ctx.registry.kind(foo_id), Some(SymbolKind::Struct));
2521
2522        let initial_nodes = ctx.code_graph.node_count();
2523
2524        // Create a batch to change Struct -> Enum
2525        let mut batch = RegistryUpdateBatch::new();
2526        batch.push(RegistryUpdate::UpdateKind {
2527            id: foo_id,
2528            new_kind: SymbolKind::Enum,
2529        });
2530
2531        ctx.commit_changes(&batch);
2532
2533        // Verify kind was updated in registry
2534        assert_eq!(
2535            ctx.registry.kind(foo_id),
2536            Some(SymbolKind::Enum),
2537            "Kind should be updated to Enum"
2538        );
2539
2540        // Node count should remain the same
2541        assert_eq!(
2542            ctx.code_graph.node_count(),
2543            initial_nodes,
2544            "Node count should not change"
2545        );
2546    }
2547
2548    #[test]
2549    fn test_commit_changes_batch_multiple_updates() {
2550        use crate::SymbolKind;
2551        use crate::{FileSpan, RegistryUpdate, RegistryUpdateBatch, SymbolPath};
2552
2553        // Create minimal workspace to satisfy crate_name requirement
2554        let workspace = TestWorkspace::builder()
2555            .crate_with_source("testcrate", "src/lib.rs", "")
2556            .build();
2557        let mut ctx = build_context_from_workspace(&workspace, "testcrate");
2558
2559        // Create a batch with multiple Add updates
2560        let mut batch = RegistryUpdateBatch::new();
2561        for name in ["Alpha", "Beta", "Gamma"] {
2562            let path = SymbolPath::parse(&format!("testcrate::{}", name)).unwrap();
2563            let dummy_span = FileSpan::new(
2564                WorkspaceFilePath::new_for_test("src/test.rs", "/project", "testcrate"),
2565                0,
2566                100,
2567            );
2568            batch.push(RegistryUpdate::Add {
2569                path,
2570                kind: SymbolKind::Struct,
2571                span: dummy_span,
2572            });
2573        }
2574
2575        let initial_nodes = ctx.code_graph.node_count();
2576        ctx.commit_changes(&batch);
2577
2578        // All three symbols should be added
2579        assert_eq!(
2580            ctx.code_graph.node_count(),
2581            initial_nodes + 3,
2582            "Graph should have 3 more nodes"
2583        );
2584
2585        // Verify all symbols exist
2586        for name in ["Alpha", "Beta", "Gamma"] {
2587            let path = SymbolPath::parse(&format!("testcrate::{}", name)).unwrap();
2588            assert!(
2589                ctx.registry.lookup(&path).is_some(),
2590                "{} should be registered",
2591                name
2592            );
2593        }
2594    }
2595
2596    // ========================================================================
2597    // Reference edges tests (Implements, Uses, Calls)
2598    // ========================================================================
2599
2600    #[test]
2601    fn test_implements_edge() {
2602        let workspace = TestWorkspace::builder()
2603            .crate_with_source(
2604                "mylib",
2605                "src/lib.rs",
2606                r#"
2607                pub trait MyTrait {
2608                    fn do_something(&self);
2609                }
2610
2611                pub struct MyStruct {}
2612
2613                impl MyTrait for MyStruct {
2614                    fn do_something(&self) {}
2615                }
2616                "#,
2617            )
2618            .build();
2619
2620        let ctx = build_context_from_workspace(&workspace, "mylib");
2621
2622        // Find the trait
2623        let trait_path = SymbolPath::parse("mylib::MyTrait").unwrap();
2624        let trait_id = ctx.registry.lookup(&trait_path);
2625        assert!(trait_id.is_some(), "Trait should be registered");
2626
2627        // Find the struct
2628        let struct_path = SymbolPath::parse("mylib::MyStruct").unwrap();
2629        let struct_id = ctx.registry.lookup(&struct_path);
2630        assert!(struct_id.is_some(), "Struct should be registered");
2631
2632        // Check implementors
2633        let has_implementors = ctx
2634            .code_graph
2635            .implementors_of(trait_id.unwrap())
2636            .next()
2637            .is_some();
2638        assert!(
2639            has_implementors,
2640            "MyTrait should have at least one implementor"
2641        );
2642    }
2643
2644    #[test]
2645    fn test_uses_edge_from_struct_field() {
2646        let workspace = TestWorkspace::builder()
2647            .crate_with_source(
2648                "mylib",
2649                "src/lib.rs",
2650                r#"
2651                pub struct Inner {}
2652
2653                pub struct Outer {
2654                    pub inner: Inner,
2655                }
2656                "#,
2657            )
2658            .build();
2659
2660        let ctx = build_context_from_workspace(&workspace, "mylib");
2661
2662        let outer_path = SymbolPath::parse("mylib::Outer").unwrap();
2663        let inner_path = SymbolPath::parse("mylib::Inner").unwrap();
2664
2665        let outer_id = ctx.registry.lookup(&outer_path);
2666        let inner_id = ctx.registry.lookup(&inner_path);
2667
2668        assert!(outer_id.is_some(), "Outer should be registered");
2669        assert!(inner_id.is_some(), "Inner should be registered");
2670
2671        // Check type usage: Outer uses Inner (via TypeFlow)
2672        let outer = outer_id.unwrap();
2673        let is_user = ctx
2674            .typeflow_graph
2675            .type_users(inner_id.unwrap())
2676            .any(|id| id == outer);
2677        assert!(is_user, "Outer should use Inner");
2678    }
2679
2680    #[test]
2681    fn test_uses_edge_from_fn_params() {
2682        let workspace = TestWorkspace::builder()
2683            .crate_with_source(
2684                "mylib",
2685                "src/lib.rs",
2686                r#"
2687                pub struct Config {}
2688
2689                pub fn process(config: Config) {}
2690                "#,
2691            )
2692            .build();
2693
2694        let ctx = build_context_from_workspace(&workspace, "mylib");
2695
2696        let fn_path = SymbolPath::parse("mylib::process").unwrap();
2697        let config_path = SymbolPath::parse("mylib::Config").unwrap();
2698
2699        let fn_id = ctx.registry.lookup(&fn_path);
2700        let config_id = ctx.registry.lookup(&config_path);
2701
2702        assert!(fn_id.is_some(), "Function should be registered");
2703        assert!(config_id.is_some(), "Config should be registered");
2704
2705        // Check type usage: process uses Config (via TypeFlow)
2706        let fn_sym = fn_id.unwrap();
2707        let config_sym = config_id.unwrap();
2708
2709        let is_user = ctx
2710            .typeflow_graph
2711            .type_users(config_sym)
2712            .any(|id| id == fn_sym);
2713        assert!(is_user, "process should use Config");
2714    }
2715
2716    #[test]
2717    fn test_calls_edge() {
2718        let workspace = TestWorkspace::builder()
2719            .crate_with_source(
2720                "mylib",
2721                "src/lib.rs",
2722                r#"
2723                pub fn helper() {}
2724
2725                pub fn main_fn() {
2726                    helper();
2727                }
2728                "#,
2729            )
2730            .build();
2731
2732        let ctx = build_context_from_workspace(&workspace, "mylib");
2733
2734        let main_path = SymbolPath::parse("mylib::main_fn").unwrap();
2735        let helper_path = SymbolPath::parse("mylib::helper").unwrap();
2736
2737        let main_id = ctx.registry.lookup(&main_path);
2738        let helper_id = ctx.registry.lookup(&helper_path);
2739
2740        assert!(main_id.is_some(), "main_fn should be registered");
2741        assert!(helper_id.is_some(), "helper should be registered");
2742
2743        // Check Calls edge: main_fn calls helper
2744        let main = main_id.unwrap();
2745        let is_caller = ctx
2746            .code_graph
2747            .callers_of(helper_id.unwrap())
2748            .any(|id| id == main);
2749        assert!(is_caller, "main_fn should call helper");
2750    }
2751
2752    #[test]
2753    fn test_fork_clone_creates_independent_context() {
2754        let workspace = TestWorkspace::builder()
2755            .crate_with_source("mylib", "src/lib.rs", "pub fn foo() {}")
2756            .build();
2757
2758        let original = build_context_from_workspace(&workspace, "mylib");
2759        let cloned = original.fork_clone();
2760
2761        // Cloned has its own registry (different memory address)
2762        assert!(!std::ptr::eq(
2763            &original.registry as *const _,
2764            &cloned.registry as *const _
2765        ));
2766
2767        // But same content
2768        assert_eq!(original.registry.len(), cloned.registry.len());
2769        assert_eq!(
2770            original.code_graph.node_count(),
2771            cloned.code_graph.node_count()
2772        );
2773    }
2774
2775    #[test]
2776    fn test_fork_clone_is_faster_than_rebuild() {
2777        use std::time::Instant;
2778
2779        // Create a context with multiple files
2780        let workspace = TestWorkspace::builder()
2781            .crate_with_source("mylib", "src/lib.rs", "pub mod a; pub mod b;")
2782            .crate_with_source("mylib", "src/a.rs", "pub struct A { x: i32 }")
2783            .crate_with_source("mylib", "src/b.rs", "pub struct B { y: String }")
2784            .build();
2785
2786        let ctx = build_context_from_workspace(&workspace, "mylib");
2787
2788        // Warm up
2789        let _ = ctx.fork_clone();
2790        let _ = ctx.fork_rebuild();
2791
2792        // Benchmark fork_clone (10 iterations)
2793        let t0 = Instant::now();
2794        for _ in 0..10 {
2795            let _ = ctx.fork_clone();
2796        }
2797        let clone_time = t0.elapsed();
2798
2799        // Benchmark fork_rebuild (10 iterations)
2800        let t1 = Instant::now();
2801        for _ in 0..10 {
2802            let _ = ctx.fork_rebuild();
2803        }
2804        let rebuild_time = t1.elapsed();
2805
2806        eprintln!(
2807            "fork_clone: {:?} avg, fork_rebuild: {:?} avg, speedup: {:.1}x",
2808            clone_time / 10,
2809            rebuild_time / 10,
2810            rebuild_time.as_nanos() as f64 / clone_time.as_nanos() as f64
2811        );
2812
2813        // fork_clone should be significantly faster
2814        assert!(
2815            clone_time < rebuild_time,
2816            "fork_clone ({:?}) should be faster than fork_rebuild ({:?})",
2817            clone_time,
2818            rebuild_time
2819        );
2820    }
2821
2822    // ========================================================================
2823    // UUID Persistence Tests
2824    // ========================================================================
2825
2826    #[test]
2827    fn test_uuid_persistence_save_and_load() {
2828        use ryo_symbol::SymbolPath;
2829
2830        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
2831        let workspace_root: Arc<Path> = Arc::from(temp_dir.path());
2832
2833        // Create .ryo directory for UUID storage
2834        std::fs::create_dir_all(temp_dir.path().join(".ryo")).unwrap();
2835
2836        // Create test files using TestWorkspace pattern
2837        let wfp = WorkspaceFilePath::new_for_test("src/lib.rs", temp_dir.path(), "test_crate");
2838
2839        let source = r#"pub struct TestStruct { pub field: i32 }"#;
2840        let file = PureFile::from_source(source).expect("Failed to parse");
2841
2842        let mut files = HashMap::new();
2843        files.insert(wfp.clone(), file);
2844
2845        // Build first context (generates UUIDs)
2846        let ctx1 = AnalysisContext::build_from_workspace_files(
2847            files.clone(),
2848            workspace_root.clone(),
2849            AnalysisConfig::default(),
2850        )
2851        .unwrap();
2852
2853        // Find TestStruct
2854        let path = SymbolPath::parse("test_crate::TestStruct").unwrap();
2855        let id1 = ctx1
2856            .registry
2857            .lookup(&path)
2858            .expect("TestStruct should be registered");
2859        let uuid1 = ctx1.registry.uuid(id1).expect("Should have UUID");
2860
2861        // Save UUID mappings
2862        ctx1.save_uuid_mappings().expect("Failed to save");
2863
2864        // Verify file exists
2865        let uuid_file = temp_dir.path().join(".ryo/uuid-mapping.json");
2866        assert!(uuid_file.exists(), "UUID file should exist");
2867
2868        // Load mappings for second context
2869        let mappings =
2870            AnalysisContext::load_uuid_mappings(temp_dir.path()).expect("Should load mappings");
2871
2872        // Rebuild with loaded mappings
2873        let config = AnalysisConfig::default().with_uuid_mappings(mappings);
2874        let ctx2 =
2875            AnalysisContext::build_from_workspace_files(files, workspace_root, config).unwrap();
2876
2877        // Find TestStruct again (different SymbolId)
2878        let id2 = ctx2
2879            .registry
2880            .lookup(&path)
2881            .expect("TestStruct should exist");
2882        let uuid2 = ctx2.registry.uuid(id2).expect("Should have UUID");
2883
2884        // UUID should be preserved
2885        assert_eq!(uuid1, uuid2, "UUID should be preserved across rebuilds");
2886    }
2887
2888    #[test]
2889    fn test_uuid_persistence_new_symbol_gets_new_uuid() {
2890        use ryo_symbol::SymbolPath;
2891
2892        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
2893        let workspace_root: Arc<Path> = Arc::from(temp_dir.path());
2894        std::fs::create_dir_all(temp_dir.path().join(".ryo")).unwrap();
2895
2896        let wfp = WorkspaceFilePath::new_for_test("src/lib.rs", temp_dir.path(), "test_crate");
2897
2898        // First context with one struct
2899        let source1 = "pub struct First;";
2900        let file1 = PureFile::from_source(source1).unwrap();
2901        let mut files1 = HashMap::new();
2902        files1.insert(wfp.clone(), file1);
2903
2904        let ctx1 = AnalysisContext::build_from_workspace_files(
2905            files1,
2906            workspace_root.clone(),
2907            AnalysisConfig::default(),
2908        )
2909        .unwrap();
2910
2911        let first_path = SymbolPath::parse("test_crate::First").unwrap();
2912        let first_id = ctx1.registry.lookup(&first_path).unwrap();
2913        let first_uuid = ctx1.registry.uuid(first_id).unwrap();
2914        ctx1.save_uuid_mappings().unwrap();
2915
2916        // Second context with two structs
2917        let source2 = "pub struct First;\npub struct Second;";
2918        let file2 = PureFile::from_source(source2).unwrap();
2919        let mut files2 = HashMap::new();
2920        files2.insert(wfp, file2);
2921
2922        let mappings = AnalysisContext::load_uuid_mappings(temp_dir.path()).unwrap();
2923        let config = AnalysisConfig::default().with_uuid_mappings(mappings);
2924        let ctx2 =
2925            AnalysisContext::build_from_workspace_files(files2, workspace_root, config).unwrap();
2926
2927        // First should keep UUID
2928        let first_id2 = ctx2.registry.lookup(&first_path).unwrap();
2929        let first_uuid2 = ctx2.registry.uuid(first_id2).unwrap();
2930        assert_eq!(first_uuid, first_uuid2, "First UUID preserved");
2931
2932        // Second should have new UUID
2933        let second_path = SymbolPath::parse("test_crate::Second").unwrap();
2934        let second_id = ctx2.registry.lookup(&second_path).unwrap();
2935        let second_uuid = ctx2.registry.uuid(second_id).unwrap();
2936        assert_ne!(first_uuid, second_uuid, "Second has different UUID");
2937    }
2938
2939    // ========================================================================
2940    // Core Entity: impl methods registered directly on struct
2941    // ========================================================================
2942
2943    #[test]
2944    fn test_impl_methods_registered_directly_on_struct() {
2945        let workspace = TestWorkspace::builder()
2946            .crate_with_source(
2947                "mylib",
2948                "src/lib.rs",
2949                r#"
2950                pub struct TodoList {
2951                    items: Vec<String>,
2952                }
2953
2954                impl TodoList {
2955                    pub fn new() -> Self {
2956                        Self { items: vec![] }
2957                    }
2958
2959                    pub fn add(&mut self, item: String) {
2960                        self.items.push(item);
2961                    }
2962                }
2963                "#,
2964            )
2965            .build();
2966
2967        let ctx = build_context_from_workspace(&workspace, "mylib");
2968
2969        // メソッドが直接Structパスで登録される(implブロック経由なし)
2970        let new_path = SymbolPath::parse("mylib::TodoList::new").unwrap();
2971        let add_path = SymbolPath::parse("mylib::TodoList::add").unwrap();
2972
2973        assert!(
2974            ctx.registry.lookup(&new_path).is_some(),
2975            "new method should be registered as TodoList::new"
2976        );
2977        assert!(
2978            ctx.registry.lookup(&add_path).is_some(),
2979            "add method should be registered as TodoList::add"
2980        );
2981
2982        // impl block自体も登録される(trait操作のため)
2983        let impl_blocks: Vec<_> = ctx
2984            .registry
2985            .iter()
2986            .filter(|(_, path)| path.segment_refs().iter().any(|s| s.is_impl()))
2987            .collect();
2988
2989        assert_eq!(
2990            impl_blocks.len(),
2991            1,
2992            "impl block should be registered as <impl TodoList>"
2993        );
2994        assert!(
2995            impl_blocks[0].1.to_string() == "mylib::<impl TodoList>",
2996            "impl block path should be mylib::<impl TodoList>, found: {}",
2997            impl_blocks[0].1
2998        );
2999    }
3000
3001    #[test]
3002    fn test_multiple_impl_blocks_methods_merged_on_struct() {
3003        let workspace = TestWorkspace::builder()
3004            .crate_with_source(
3005                "mylib",
3006                "src/lib.rs",
3007                r#"
3008                pub struct TodoList;
3009
3010                impl TodoList {
3011                    pub fn new() -> Self { Self }
3012                }
3013
3014                impl TodoList {
3015                    pub fn add(&mut self, item: String) {}
3016                }
3017                "#,
3018            )
3019            .build();
3020
3021        let ctx = build_context_from_workspace(&workspace, "mylib");
3022
3023        // 両方のimplブロックのメソッドがStructに登録される
3024        let new_path = SymbolPath::parse("mylib::TodoList::new").unwrap();
3025        let add_path = SymbolPath::parse("mylib::TodoList::add").unwrap();
3026
3027        assert!(
3028            ctx.registry.lookup(&new_path).is_some(),
3029            "new from first impl should be registered"
3030        );
3031        assert!(
3032            ctx.registry.lookup(&add_path).is_some(),
3033            "add from second impl should be registered"
3034        );
3035
3036        // impl blockがマージされて1つ登録される
3037        let impl_blocks: Vec<_> = ctx
3038            .registry
3039            .iter()
3040            .filter(|(_, path)| path.segment_refs().iter().any(|s| s.is_impl()))
3041            .collect();
3042
3043        assert_eq!(
3044            impl_blocks.len(),
3045            1,
3046            "merged impl block should be registered"
3047        );
3048        assert!(
3049            impl_blocks[0].1.to_string() == "mylib::<impl TodoList>",
3050            "impl block path should be mylib::<impl TodoList>"
3051        );
3052    }
3053
3054    // =====================================================================
3055    // BUG#3: graph chain callers/callees resolution tests
3056    // =====================================================================
3057
3058    /// P0: Associated function call across modules via `use` import.
3059    /// `Router::new()` in handler.rs should create a Calls edge to types::Router::new.
3060    #[test]
3061    fn test_calls_edge_associated_fn_cross_module() {
3062        let workspace = TestWorkspace::builder()
3063            .crate_with_source(
3064                "mylib",
3065                "src/lib.rs",
3066                r#"
3067                pub mod types;
3068                pub mod handler;
3069                "#,
3070            )
3071            .crate_with_source(
3072                "mylib",
3073                "src/types.rs",
3074                r#"
3075                pub struct Router {}
3076                impl Router {
3077                    pub fn new() -> Self { Router {} }
3078                }
3079                "#,
3080            )
3081            .crate_with_source(
3082                "mylib",
3083                "src/handler.rs",
3084                r#"
3085                use crate::types::Router;
3086                pub fn setup() {
3087                    let _r = Router::new();
3088                }
3089                "#,
3090            )
3091            .build();
3092
3093        let ctx = build_context_from_workspace(&workspace, "mylib");
3094
3095        let setup_path = SymbolPath::parse("mylib::handler::setup").unwrap();
3096        let new_path = SymbolPath::parse("mylib::types::Router::new").unwrap();
3097
3098        let setup_id = ctx
3099            .registry
3100            .lookup(&setup_path)
3101            .expect("setup should be registered");
3102        let new_id = ctx
3103            .registry
3104            .lookup(&new_path)
3105            .expect("Router::new should be registered");
3106
3107        let callees: Vec<_> = ctx.code_graph.callees_of(setup_id).collect();
3108        assert!(
3109            callees.contains(&new_id),
3110            "setup should call Router::new, but callees = {:?}",
3111            callees
3112        );
3113    }
3114
3115    /// P0: Cross-module free function call via `use` import.
3116    #[test]
3117    fn test_calls_edge_free_fn_cross_module_via_use() {
3118        let workspace = TestWorkspace::builder()
3119            .crate_with_source(
3120                "mylib",
3121                "src/lib.rs",
3122                r#"
3123                pub mod utils;
3124                pub mod handler;
3125                "#,
3126            )
3127            .crate_with_source(
3128                "mylib",
3129                "src/utils.rs",
3130                r#"
3131                pub fn helper() {}
3132                "#,
3133            )
3134            .crate_with_source(
3135                "mylib",
3136                "src/handler.rs",
3137                r#"
3138                use crate::utils::helper;
3139                pub fn process() {
3140                    helper();
3141                }
3142                "#,
3143            )
3144            .build();
3145
3146        let ctx = build_context_from_workspace(&workspace, "mylib");
3147
3148        let process_path = SymbolPath::parse("mylib::handler::process").unwrap();
3149        let helper_path = SymbolPath::parse("mylib::utils::helper").unwrap();
3150
3151        let process_id = ctx
3152            .registry
3153            .lookup(&process_path)
3154            .expect("process should be registered");
3155        let helper_id = ctx
3156            .registry
3157            .lookup(&helper_path)
3158            .expect("helper should be registered");
3159
3160        let is_callee = ctx
3161            .code_graph
3162            .callees_of(process_id)
3163            .any(|id| id == helper_id);
3164        assert!(is_callee, "process should call helper via use import");
3165    }
3166
3167    /// P1: Method call `self.method()` where method name is in common trait list ("new").
3168    /// Associated function calls (Type::new()) should NOT be filtered by is_common_trait_method.
3169    #[test]
3170    fn test_calls_edge_associated_fn_new_not_filtered() {
3171        let workspace = TestWorkspace::builder()
3172            .crate_with_source(
3173                "mylib",
3174                "src/lib.rs",
3175                r#"
3176                pub struct Builder {}
3177                impl Builder {
3178                    pub fn new() -> Self { Builder {} }
3179                    pub fn build(self) -> String { String::new() }
3180                }
3181
3182                pub fn create() -> String {
3183                    let b = Builder::new();
3184                    b.build()
3185                }
3186                "#,
3187            )
3188            .build();
3189
3190        let ctx = build_context_from_workspace(&workspace, "mylib");
3191
3192        let create_path = SymbolPath::parse("mylib::create").unwrap();
3193        let new_path = SymbolPath::parse("mylib::Builder::new").unwrap();
3194
3195        let create_id = ctx
3196            .registry
3197            .lookup(&create_path)
3198            .expect("create should be registered");
3199        let new_id = ctx
3200            .registry
3201            .lookup(&new_path)
3202            .expect("Builder::new should be registered");
3203
3204        let callees: Vec<_> = ctx.code_graph.callees_of(create_id).collect();
3205        assert!(
3206            callees.contains(&new_id),
3207            "create should call Builder::new (associated fn, not filtered by is_common_trait_method), callees = {:?}",
3208            callees
3209        );
3210    }
3211
3212    /// P0: Qualified path call with multiple segments (e.g., `module::func()`).
3213    #[test]
3214    fn test_calls_edge_qualified_path_call() {
3215        let workspace = TestWorkspace::builder()
3216            .crate_with_source(
3217                "mylib",
3218                "src/lib.rs",
3219                r#"
3220                pub mod utils {
3221                    pub fn do_work() {}
3222                }
3223
3224                pub fn caller() {
3225                    utils::do_work();
3226                }
3227                "#,
3228            )
3229            .build();
3230
3231        let ctx = build_context_from_workspace(&workspace, "mylib");
3232
3233        let caller_path = SymbolPath::parse("mylib::caller").unwrap();
3234        let do_work_path = SymbolPath::parse("mylib::utils::do_work").unwrap();
3235
3236        let caller_id = ctx
3237            .registry
3238            .lookup(&caller_path)
3239            .expect("caller should be registered");
3240        let do_work_id = ctx
3241            .registry
3242            .lookup(&do_work_path)
3243            .expect("do_work should be registered");
3244
3245        let is_callee = ctx
3246            .code_graph
3247            .callees_of(caller_id)
3248            .any(|id| id == do_work_id);
3249        assert!(
3250            is_callee,
3251            "caller should call utils::do_work via qualified path"
3252        );
3253    }
3254
3255    /// P1: Method call with common trait name ("clone") should resolve
3256    /// when there is exactly one candidate (unambiguous).
3257    #[test]
3258    fn test_calls_edge_method_common_name_single_candidate() {
3259        let workspace = TestWorkspace::builder()
3260            .crate_with_source(
3261                "mylib",
3262                "src/lib.rs",
3263                r#"
3264                pub struct Data {}
3265                impl Data {
3266                    pub fn clone(&self) -> Self { Data {} }
3267                }
3268
3269                pub fn process(d: Data) -> Data {
3270                    d.clone()
3271                }
3272                "#,
3273            )
3274            .build();
3275
3276        let ctx = build_context_from_workspace(&workspace, "mylib");
3277
3278        let process_path = SymbolPath::parse("mylib::process").unwrap();
3279        let clone_path = SymbolPath::parse("mylib::Data::clone").unwrap();
3280
3281        let process_id = ctx
3282            .registry
3283            .lookup(&process_path)
3284            .expect("process should be registered");
3285        let clone_id = ctx
3286            .registry
3287            .lookup(&clone_path)
3288            .expect("Data::clone should be registered");
3289
3290        let callees: Vec<_> = ctx.code_graph.callees_of(process_id).collect();
3291        assert!(
3292            callees.contains(&clone_id),
3293            "process should call Data::clone even though 'clone' is in common trait list (single candidate), callees = {:?}",
3294            callees
3295        );
3296    }
3297
3298    /// P1: Candidate limit should not block resolution when candidates <= 32.
3299    #[test]
3300    fn test_calls_edge_method_many_candidates_not_blocked() {
3301        // Create 12 types each with a "handle" method — exceeds old limit of 8
3302        let workspace = TestWorkspace::builder()
3303            .crate_with_source(
3304                "mylib",
3305                "src/lib.rs",
3306                r#"
3307                pub struct A {} impl A { pub fn handle(&self) {} }
3308                pub struct B {} impl B { pub fn handle(&self) {} }
3309                pub struct C {} impl C { pub fn handle(&self) {} }
3310                pub struct D {} impl D { pub fn handle(&self) {} }
3311                pub struct E {} impl E { pub fn handle(&self) {} }
3312                pub struct F {} impl F { pub fn handle(&self) {} }
3313                pub struct G {} impl G { pub fn handle(&self) {} }
3314                pub struct H {} impl H { pub fn handle(&self) {} }
3315                pub struct I {} impl I { pub fn handle(&self) {} }
3316                pub struct J {} impl J { pub fn handle(&self) {} }
3317                pub struct K {} impl K { pub fn handle(&self) {} }
3318                pub struct L {} impl L { pub fn handle(&self) {} }
3319
3320                pub fn caller(a: A) {
3321                    a.handle();
3322                }
3323                "#,
3324            )
3325            .build();
3326
3327        let ctx = build_context_from_workspace(&workspace, "mylib");
3328
3329        let caller_path = SymbolPath::parse("mylib::caller").unwrap();
3330        let caller_id = ctx
3331            .registry
3332            .lookup(&caller_path)
3333            .expect("caller should be registered");
3334
3335        let callees: Vec<_> = ctx.code_graph.callees_of(caller_id).collect();
3336        // Without type inference, all 12 "handle" methods are candidates (over-approximation).
3337        // The key assertion: at least some callees exist (not 0 due to candidate cap).
3338        assert!(
3339            !callees.is_empty(),
3340            "caller should have callees for 'handle' method (12 candidates should not be blocked), callees = {:?}",
3341            callees
3342        );
3343    }
3344
3345    /// Trait impl method body should have callees resolved
3346    /// (direct function call inside trait impl).
3347    #[test]
3348    fn test_calls_edge_trait_impl_method_has_callees() {
3349        let workspace = TestWorkspace::builder()
3350            .crate_with_source(
3351                "mylib",
3352                "src/lib.rs",
3353                r#"
3354                pub fn helper() -> i32 { 42 }
3355
3356                pub trait MyTrait {
3357                    fn do_work(&self) -> i32;
3358                }
3359
3360                pub struct Foo;
3361                impl MyTrait for Foo {
3362                    fn do_work(&self) -> i32 {
3363                        helper()
3364                    }
3365                }
3366                "#,
3367            )
3368            .build();
3369
3370        let ctx = build_context_from_workspace(&workspace, "mylib");
3371
3372        // Verify symbols are registered
3373        let helper_path = SymbolPath::parse("mylib::helper").unwrap();
3374        let helper_id = ctx
3375            .registry
3376            .lookup(&helper_path)
3377            .expect("helper should be registered");
3378
3379        let method_path = SymbolPath::parse("mylib::<impl MyTrait for Foo>::do_work").unwrap();
3380        let method_id = ctx
3381            .registry
3382            .lookup(&method_path)
3383            .expect("trait impl method should be registered");
3384
3385        let callees: Vec<_> = ctx.code_graph.callees_of(method_id).collect();
3386        assert!(
3387            callees.contains(&helper_id),
3388            "Trait impl method do_work should call helper(), but callees = {:?}",
3389            callees
3390        );
3391    }
3392
3393    /// Trait impl method with self.method() call should have callees resolved.
3394    #[test]
3395    fn test_calls_edge_trait_impl_method_call_inside_body() {
3396        let workspace = TestWorkspace::builder()
3397            .crate_with_source(
3398                "mylib",
3399                "src/lib.rs",
3400                r#"
3401                pub struct Data {
3402                    pub value: i32,
3403                }
3404                impl Data {
3405                    pub fn process(&self) -> i32 { self.value }
3406                }
3407
3408                pub trait Transform {
3409                    fn transform(&self) -> i32;
3410                }
3411
3412                impl Transform for Data {
3413                    fn transform(&self) -> i32 {
3414                        self.process()
3415                    }
3416                }
3417                "#,
3418            )
3419            .build();
3420
3421        let ctx = build_context_from_workspace(&workspace, "mylib");
3422
3423        let transform_method_path =
3424            SymbolPath::parse("mylib::<impl Transform for Data>::transform").unwrap();
3425        let transform_id = ctx
3426            .registry
3427            .lookup(&transform_method_path)
3428            .expect("transform should be registered");
3429
3430        let process_path = SymbolPath::parse("mylib::Data::process").unwrap();
3431        let process_id = ctx
3432            .registry
3433            .lookup(&process_path)
3434            .expect("Data::process should be registered");
3435
3436        let callees: Vec<_> = ctx.code_graph.callees_of(transform_id).collect();
3437        assert!(
3438            callees.contains(&process_id),
3439            "Trait impl transform() should call self.process(), but callees = {:?}",
3440            callees
3441        );
3442    }
3443
3444    /// Method call with >32 candidates should NOT be silently dropped.
3445    /// Reproduces axum `into_response` pattern: 57 impls caused 0 callees.
3446    #[test]
3447    fn test_calls_edge_method_over_32_candidates_not_dropped() {
3448        // Create 35 types each with a "render" method to exceed the limit of 32
3449        let mut source = String::new();
3450        for i in 0..35 {
3451            source.push_str(&format!(
3452                "pub struct T{i} {{}}\nimpl T{i} {{ pub fn render(&self) -> i32 {{ {i} }} }}\n"
3453            ));
3454        }
3455        source.push_str(
3456            r#"
3457            pub fn caller(t: T0) -> i32 {
3458                t.render()
3459            }
3460            "#,
3461        );
3462
3463        let workspace = TestWorkspace::builder()
3464            .crate_with_source("mylib", "src/lib.rs", &source)
3465            .build();
3466
3467        let ctx = build_context_from_workspace(&workspace, "mylib");
3468
3469        let caller_path = SymbolPath::parse("mylib::caller").unwrap();
3470        let caller_id = ctx
3471            .registry
3472            .lookup(&caller_path)
3473            .expect("caller should be registered");
3474
3475        let callees: Vec<_> = ctx.code_graph.callees_of(caller_id).collect();
3476        assert!(
3477            !callees.is_empty(),
3478            "caller should have callees for 'render' even with 35 candidates (>32 limit), \
3479             but callees is empty — candidate limit silently drops all edges"
3480        );
3481    }
3482
3483    #[test]
3484    fn test_method_index_excludes_non_callable_symbols() {
3485        // BUG: method_index includes modules, structs, enums that share the method name.
3486        // When a nested module like "response::process" exists (3 segments = passes current filter),
3487        // calling x.process() should NOT create an edge to the module.
3488        // This mirrors axum's axum_core::response::into_response (module) vs .into_response() (method).
3489        let source = r#"
3490            pub mod response {
3491                pub mod process {
3492                    pub fn run() -> i32 { 42 }
3493                }
3494            }
3495            pub struct Engine;
3496            impl Engine {
3497                pub fn process(&self) -> i32 { 1 }
3498            }
3499            pub fn caller(e: Engine) -> i32 {
3500                e.process()
3501            }
3502        "#;
3503
3504        let workspace = TestWorkspace::builder()
3505            .crate_with_source("mylib", "src/lib.rs", source)
3506            .build();
3507
3508        let ctx = build_context_from_workspace(&workspace, "mylib");
3509
3510        let caller_path = SymbolPath::parse("mylib::caller").unwrap();
3511        let caller_id = ctx
3512            .registry
3513            .lookup(&caller_path)
3514            .expect("caller should be registered");
3515
3516        let callees: Vec<_> = ctx.code_graph.callees_of(caller_id).collect();
3517
3518        // The nested module "response::process" should NOT be a callee
3519        let mod_path = SymbolPath::parse("mylib::response::process").unwrap();
3520        let mod_id = ctx
3521            .registry
3522            .lookup(&mod_path)
3523            .expect("nested module mylib::response::process should be registered");
3524
3525        let has_mod_edge = callees.contains(&mod_id);
3526        assert!(
3527            !has_mod_edge,
3528            "method_index should not include module 'response::process' as a callee of caller(); \
3529             only Function/Method symbols should be in the method_index"
3530        );
3531
3532        // Engine::process SHOULD be a callee
3533        assert!(
3534            !callees.is_empty(),
3535            "caller should have at least one callee (Engine::process)"
3536        );
3537    }
3538
3539    #[test]
3540    fn test_method_call_receiver_type_hint_filters_candidates() {
3541        // When the receiver is a constructor call like `Foo::new()`, we can infer
3542        // the receiver type is `Foo` and filter method_index candidates to only
3543        // methods on `Foo`, instead of ALL methods with the same name.
3544        let source = r#"
3545            pub trait Render {
3546                fn render(&self) -> String;
3547            }
3548            pub struct Html;
3549            impl Render for Html {
3550                fn render(&self) -> String { String::new() }
3551            }
3552            pub struct Json;
3553            impl Json {
3554                pub fn new() -> Self { Json }
3555            }
3556            impl Render for Json {
3557                fn render(&self) -> String { String::new() }
3558            }
3559            pub struct Xml;
3560            impl Render for Xml {
3561                fn render(&self) -> String { String::new() }
3562            }
3563            pub fn caller() -> String {
3564                Json::new().render()
3565            }
3566        "#;
3567
3568        let workspace = TestWorkspace::builder()
3569            .crate_with_source("mylib", "src/lib.rs", source)
3570            .build();
3571
3572        let ctx = build_context_from_workspace(&workspace, "mylib");
3573
3574        let caller_path = SymbolPath::parse("mylib::caller").unwrap();
3575        let caller_id = ctx
3576            .registry
3577            .lookup(&caller_path)
3578            .expect("caller should be registered");
3579
3580        let callees: Vec<_> = ctx.code_graph.callees_of(caller_id).collect();
3581
3582        // Json::new should be a callee
3583        let new_path = SymbolPath::parse("mylib::Json::new").unwrap();
3584        let new_id = ctx.registry.lookup(&new_path);
3585        if let Some(new_id) = new_id {
3586            assert!(callees.contains(&new_id), "Json::new should be a callee");
3587        }
3588
3589        // Json's render should be a callee (receiver = Json::new())
3590        // Html's render and Xml's render should NOT be callees
3591        // because we can infer the receiver type is Json from Json::new()
3592        let json_render = ctx
3593            .registry
3594            .lookup(&SymbolPath::parse("mylib::<impl Render for Json>::render").unwrap());
3595        let html_render = ctx
3596            .registry
3597            .lookup(&SymbolPath::parse("mylib::<impl Render for Html>::render").unwrap());
3598        let xml_render = ctx
3599            .registry
3600            .lookup(&SymbolPath::parse("mylib::<impl Render for Xml>::render").unwrap());
3601
3602        if let Some(json_id) = json_render {
3603            assert!(
3604                callees.contains(&json_id),
3605                "Json's render should be a callee (receiver type hint: Json from Json::new())"
3606            );
3607        }
3608
3609        // These should NOT be callees if receiver type hint works
3610        if let Some(html_id) = html_render {
3611            assert!(
3612                !callees.contains(&html_id),
3613                "Html's render should NOT be a callee when receiver is Json::new(); \
3614                 receiver type hint should filter it out"
3615            );
3616        }
3617
3618        if let Some(xml_id) = xml_render {
3619            assert!(
3620                !callees.contains(&xml_id),
3621                "Xml's render should NOT be a callee when receiver is Json::new(); \
3622                 receiver type hint should filter it out"
3623            );
3624        }
3625    }
3626
3627    #[test]
3628    fn test_associated_fn_call_in_trait_impl_resolved_via_imports() {
3629        // BUG: When Body::new() is called inside a trait impl method,
3630        // resolve_type_reference receives parent_path with impl segment
3631        // (e.g., "mylib::<impl Render for Page>::render") but UseResolver
3632        // stores import maps keyed by module path ("mylib"). The impl
3633        // segment causes import resolution to fail, so Body::new() is
3634        // not detected as a callee.
3635        let source = r#"
3636            pub mod types {
3637                pub struct Body;
3638                impl Body {
3639                    pub fn create() -> Self { Body }
3640                }
3641            }
3642            use crate::types::Body;
3643            pub trait Render {
3644                fn render(&self) -> Body;
3645            }
3646            pub struct Page;
3647            impl Render for Page {
3648                fn render(&self) -> Body {
3649                    Body::create()
3650                }
3651            }
3652        "#;
3653
3654        let workspace = TestWorkspace::builder()
3655            .crate_with_source("mylib", "src/lib.rs", source)
3656            .build();
3657
3658        let ctx = build_context_from_workspace(&workspace, "mylib");
3659
3660        // Find Page's render method
3661        let render_path = SymbolPath::parse("mylib::<impl Render for Page>::render").unwrap();
3662        let render_id = ctx
3663            .registry
3664            .lookup(&render_path)
3665            .expect("Page::render should be registered");
3666
3667        let callees: Vec<_> = ctx.code_graph.callees_of(render_id).collect();
3668
3669        // Body::create should be a callee
3670        let create_path = SymbolPath::parse("mylib::types::Body::create").unwrap();
3671        let create_id = ctx
3672            .registry
3673            .lookup(&create_path)
3674            .expect("Body::create should be registered");
3675
3676        assert!(
3677            callees.contains(&create_id),
3678            "Body::create() should be a callee of Page::render(); \
3679             import resolution in trait impl methods must strip impl segments \
3680             from parent_path before querying UseResolver"
3681        );
3682    }
3683
3684    #[test]
3685    fn test_generic_impl_methods_registered_and_edges_built() {
3686        // BUG: impl<S> Router<S> { pub fn new() -> Self { ... } }
3687        // collect_from_impl AND build_edges_from_impl both call
3688        // parent.child("Router < S >") which fails validate_rust_identifier
3689        // → Err → return → methods never registered / edges never built.
3690        let workspace = TestWorkspace::builder()
3691            .crate_with_source(
3692                "mylib",
3693                "src/lib.rs",
3694                r#"
3695                pub struct Inner;
3696                impl Inner {
3697                    pub fn create() -> Self { Inner }
3698                }
3699
3700                pub struct Router<S> {
3701                    _marker: std::marker::PhantomData<S>,
3702                }
3703
3704                impl<S> Router<S> {
3705                    pub fn new() -> Self {
3706                        let _inner = Inner::create();
3707                        Router { _marker: std::marker::PhantomData }
3708                    }
3709
3710                    pub fn route(self, path: &str) -> Self {
3711                        self
3712                    }
3713                }
3714                "#,
3715            )
3716            .build();
3717
3718        let ctx = build_context_from_workspace(&workspace, "mylib");
3719
3720        // Methods must be registered under the base type name (without generics)
3721        let new_path = SymbolPath::parse("mylib::Router::new").unwrap();
3722        let route_path = SymbolPath::parse("mylib::Router::route").unwrap();
3723
3724        let new_id = ctx
3725            .registry
3726            .lookup(&new_path)
3727            .expect("Router::new should be registered despite generic impl<S> Router<S>");
3728        assert!(
3729            ctx.registry.lookup(&route_path).is_some(),
3730            "Router::route should be registered despite generic impl<S> Router<S>"
3731        );
3732
3733        // impl block itself should also be registered
3734        let impl_path_str = "mylib::<impl Router < S >>";
3735        let impl_path = SymbolPath::parse(impl_path_str).unwrap();
3736        assert!(
3737            ctx.registry.lookup(&impl_path).is_some(),
3738            "impl block <impl Router < S >> should be registered"
3739        );
3740
3741        // Callees edges must also be built for generic impl methods
3742        let callees: Vec<_> = ctx.code_graph.callees_of(new_id).collect();
3743        let create_path = SymbolPath::parse("mylib::Inner::create").unwrap();
3744        let create_id = ctx
3745            .registry
3746            .lookup(&create_path)
3747            .expect("Inner::create should be registered");
3748        assert!(
3749            callees.contains(&create_id),
3750            "Router::new must have Inner::create() as callee; \
3751             build_edges_from_impl must strip generics from self_ty"
3752        );
3753    }
3754
3755    #[test]
3756    fn test_external_trait_impl_callees_built() {
3757        // NG-6: impl ExternalTrait for LocalType のメソッドのcalleesが0になる問題
3758        // bodyでプロジェクト内関数を呼び出しているのに、calleesエッジが構築されない
3759        let workspace = TestWorkspace::builder()
3760            .crate_with_source(
3761                "mylib",
3762                "src/lib.rs",
3763                r#"
3764                pub trait MyWrite {
3765                    fn write(&mut self, buf: &[u8]) -> usize;
3766                }
3767
3768                pub fn helper() -> usize { 42 }
3769
3770                pub struct Writer;
3771
3772                impl MyWrite for Writer {
3773                    fn write(&mut self, buf: &[u8]) -> usize {
3774                        helper()
3775                    }
3776                }
3777                "#,
3778            )
3779            .build();
3780
3781        let ctx = build_context_from_workspace(&workspace, "mylib");
3782
3783        // Method should be registered under impl path
3784        let impl_path_str = "mylib::<impl MyWrite for Writer>";
3785        let impl_path = SymbolPath::parse(impl_path_str).unwrap();
3786        assert!(
3787            ctx.registry.lookup(&impl_path).is_some(),
3788            "impl block should be registered: {}",
3789            impl_path_str,
3790        );
3791
3792        let method_path = SymbolPath::parse(&format!("{}::write", impl_path_str)).unwrap();
3793        let method_id = ctx
3794            .registry
3795            .lookup(&method_path)
3796            .expect("Writer::write should be registered under trait impl path");
3797
3798        // Callees should include helper()
3799        let callees: Vec<_> = ctx.code_graph.callees_of(method_id).collect();
3800        let helper_path = SymbolPath::parse("mylib::helper").unwrap();
3801        let helper_id = ctx
3802            .registry
3803            .lookup(&helper_path)
3804            .expect("helper should be registered");
3805
3806        assert!(
3807            callees.contains(&helper_id),
3808            "trait impl method Writer::write must have helper() as callee; \
3809             callees = {:?}",
3810            callees
3811        );
3812    }
3813
3814    #[test]
3815    fn test_external_trait_impl_with_generics_callees_built() {
3816        // NG-6の亜種: impl ExternalTrait for GenericType<'_>
3817        let workspace = TestWorkspace::builder()
3818            .crate_with_source(
3819                "mylib",
3820                "src/lib.rs",
3821                r#"
3822                pub trait MyWrite {
3823                    fn write(&mut self, buf: &[u8]) -> usize;
3824                }
3825
3826                pub fn process_buf(buf: &[u8]) -> usize { buf.len() }
3827
3828                pub struct Writer<'a> {
3829                    _data: &'a [u8],
3830                }
3831
3832                impl<'a> MyWrite for Writer<'a> {
3833                    fn write(&mut self, buf: &[u8]) -> usize {
3834                        process_buf(buf)
3835                    }
3836                }
3837                "#,
3838            )
3839            .build();
3840
3841        let ctx = build_context_from_workspace(&workspace, "mylib");
3842
3843        // Check impl block registration with lifetime param
3844        let impl_path_str = "mylib::<impl MyWrite for Writer < 'a >>";
3845        let impl_path = SymbolPath::parse(impl_path_str).unwrap();
3846        let impl_registered = ctx.registry.lookup(&impl_path).is_some();
3847
3848        // Method should be registered
3849        let method_path = SymbolPath::parse(&format!("{}::write", impl_path_str)).unwrap();
3850        let method_id = ctx.registry.lookup(&method_path);
3851
3852        assert!(
3853            impl_registered,
3854            "impl block <impl MyWrite for Writer < 'a >> should be registered"
3855        );
3856        assert!(
3857            method_id.is_some(),
3858            "Writer::write should be registered under trait impl path"
3859        );
3860
3861        if let Some(mid) = method_id {
3862            let callees: Vec<_> = ctx.code_graph.callees_of(mid).collect();
3863            let process_path = SymbolPath::parse("mylib::process_buf").unwrap();
3864            let process_id = ctx
3865                .registry
3866                .lookup(&process_path)
3867                .expect("process_buf should be registered");
3868
3869            assert!(
3870                callees.contains(&process_id),
3871                "trait impl method with generics must have process_buf() as callee; \
3872                 callees = {:?}",
3873                callees
3874            );
3875        }
3876    }
3877
3878    /// NG-2: Struct → Impl → Trait のチェーン追跡に必要なグラフ構造の検証
3879    #[test]
3880    fn test_struct_trait_implements_chain_via_impl() {
3881        let workspace = TestWorkspace::builder()
3882            .crate_with_source(
3883                "mylib",
3884                "src/lib.rs",
3885                r#"
3886                pub trait MyTrait {
3887                    fn do_something(&self);
3888                }
3889
3890                pub trait AnotherTrait {
3891                    fn other(&self);
3892                }
3893
3894                pub struct MyStruct;
3895
3896                impl MyTrait for MyStruct {
3897                    fn do_something(&self) {}
3898                }
3899
3900                impl AnotherTrait for MyStruct {
3901                    fn other(&self) {}
3902                }
3903                "#,
3904            )
3905            .build();
3906
3907        let ctx = build_context_from_workspace(&workspace, "mylib");
3908
3909        // Verify graph structure: Impl blocks have Implements edges to Traits
3910        let impl_mytrait_path = SymbolPath::parse("mylib::<impl MyTrait for MyStruct>").unwrap();
3911        let impl_another_path =
3912            SymbolPath::parse("mylib::<impl AnotherTrait for MyStruct>").unwrap();
3913        let mytrait_path = SymbolPath::parse("mylib::MyTrait").unwrap();
3914        let another_path = SymbolPath::parse("mylib::AnotherTrait").unwrap();
3915        let struct_path = SymbolPath::parse("mylib::MyStruct").unwrap();
3916
3917        let impl_mytrait_id = ctx
3918            .registry
3919            .lookup(&impl_mytrait_path)
3920            .expect("impl MyTrait for MyStruct should be registered");
3921        let impl_another_id = ctx
3922            .registry
3923            .lookup(&impl_another_path)
3924            .expect("impl AnotherTrait for MyStruct should be registered");
3925        let mytrait_id = ctx
3926            .registry
3927            .lookup(&mytrait_path)
3928            .expect("MyTrait should be registered");
3929        let another_id = ctx
3930            .registry
3931            .lookup(&another_path)
3932            .expect("AnotherTrait should be registered");
3933        let struct_id = ctx
3934            .registry
3935            .lookup(&struct_path)
3936            .expect("MyStruct should be registered");
3937
3938        // Impl → Trait edges exist
3939        let impl1_targets: Vec<_> = ctx
3940            .code_graph
3941            .outgoing_edges(impl_mytrait_id)
3942            .filter(|e| e.kind == CodeEdgeV2::Implements)
3943            .map(|e| e.to)
3944            .collect();
3945        assert!(
3946            impl1_targets.contains(&mytrait_id),
3947            "impl MyTrait for MyStruct should have Implements edge to MyTrait"
3948        );
3949
3950        let impl2_targets: Vec<_> = ctx
3951            .code_graph
3952            .outgoing_edges(impl_another_id)
3953            .filter(|e| e.kind == CodeEdgeV2::Implements)
3954            .map(|e| e.to)
3955            .collect();
3956        assert!(
3957            impl2_targets.contains(&another_id),
3958            "impl AnotherTrait for MyStruct should have Implements edge to AnotherTrait"
3959        );
3960
3961        // Struct has NO direct Implements edges (this is the expected structure)
3962        let struct_implements: Vec<_> = ctx
3963            .code_graph
3964            .outgoing_edges(struct_id)
3965            .filter(|e| e.kind == CodeEdgeV2::Implements)
3966            .collect();
3967        assert!(
3968            struct_implements.is_empty(),
3969            "Struct should have no direct Implements edges; chain via Impl is needed"
3970        );
3971
3972        // Verify chain: Struct name can be matched against Impl self_ty
3973        let struct_name = struct_path.name();
3974        let impl_ids_for_struct: Vec<_> = ctx
3975            .registry
3976            .iter_by_kind(SymbolKind::Impl)
3977            .filter(|&id| {
3978                ctx.registry
3979                    .resolve(id)
3980                    .and_then(|p| p.segment_refs().last())
3981                    .and_then(|seg| seg.impl_self_ty())
3982                    .map(|ty| ty.split('<').next().unwrap_or(ty).trim() == struct_name)
3983                    .unwrap_or(false)
3984            })
3985            .collect();
3986        assert_eq!(
3987            impl_ids_for_struct.len(),
3988            2,
3989            "MyStruct should have 2 impl blocks"
3990        );
3991
3992        // Follow Implements edges from matching Impl blocks → Traits
3993        let mut trait_ids: Vec<_> = impl_ids_for_struct
3994            .iter()
3995            .flat_map(|&impl_id| {
3996                ctx.code_graph
3997                    .outgoing_edges(impl_id)
3998                    .filter(|e| e.kind == CodeEdgeV2::Implements)
3999                    .map(|e| e.to)
4000                    .collect::<Vec<_>>()
4001            })
4002            .collect();
4003        trait_ids.sort();
4004        trait_ids.dedup();
4005
4006        assert!(
4007            trait_ids.contains(&mytrait_id),
4008            "Struct → Impl → Trait chain should reach MyTrait"
4009        );
4010        assert!(
4011            trait_ids.contains(&another_id),
4012            "Struct → Impl → Trait chain should reach AnotherTrait"
4013        );
4014
4015        // Verify ImplementedBy reverse: Trait → Impl, Impl self_ty → Struct
4016        let implementors: Vec<_> = ctx.code_graph.implementors_of(mytrait_id).collect();
4017        assert!(
4018            implementors.contains(&impl_mytrait_id),
4019            "MyTrait should have impl block as implementor"
4020        );
4021
4022        // Resolve Impl → Struct by matching self_ty name
4023        for &impl_id in &implementors {
4024            if let Some(impl_path) = ctx.registry.resolve(impl_id) {
4025                if let Some(last_seg) = impl_path.segment_refs().last() {
4026                    if let Some(self_ty) = last_seg.impl_self_ty() {
4027                        let base = self_ty.split('<').next().unwrap_or(self_ty).trim();
4028                        let resolved_struct = ctx.registry.lookup_by_name(base);
4029                        assert!(
4030                            resolved_struct.is_some(),
4031                            "Impl self_ty '{}' should resolve to a registered struct",
4032                            base
4033                        );
4034                        assert_eq!(
4035                            resolved_struct.unwrap(),
4036                            struct_id,
4037                            "Impl self_ty should resolve to MyStruct"
4038                        );
4039                    }
4040                }
4041            }
4042        }
4043    }
4044
4045    /// NG-4: self.method() で parent_path から self_ty を抽出してtype_hintとして使用
4046    #[test]
4047    fn test_self_method_resolves_via_impl_type_hint() {
4048        // self.render() in trait impl should resolve to inherent render(), not all render()
4049        let workspace = TestWorkspace::builder()
4050            .crate_with_source(
4051                "mylib",
4052                "src/lib.rs",
4053                r#"
4054                pub trait IntoResponse {
4055                    fn into_response(self) -> String;
4056                }
4057
4058                pub struct Html {
4059                    content: String,
4060                }
4061
4062                impl Html {
4063                    pub fn render(&self) -> String {
4064                        self.content.clone()
4065                    }
4066                }
4067
4068                pub struct Json {
4069                    data: String,
4070                }
4071
4072                impl Json {
4073                    pub fn render(&self) -> String {
4074                        self.data.clone()
4075                    }
4076                }
4077
4078                impl IntoResponse for Html {
4079                    fn into_response(self) -> String {
4080                        self.render()
4081                    }
4082                }
4083                "#,
4084            )
4085            .build();
4086
4087        let ctx = build_context_from_workspace(&workspace, "mylib");
4088
4089        let into_response_path =
4090            SymbolPath::parse("mylib::<impl IntoResponse for Html>::into_response").unwrap();
4091        let html_render_path = SymbolPath::parse("mylib::Html::render").unwrap();
4092        let json_render_path = SymbolPath::parse("mylib::Json::render").unwrap();
4093
4094        let into_response_id = ctx
4095            .registry
4096            .lookup(&into_response_path)
4097            .expect("into_response should be registered");
4098        let html_render_id = ctx
4099            .registry
4100            .lookup(&html_render_path)
4101            .expect("Html::render should be registered");
4102        let json_render_id = ctx
4103            .registry
4104            .lookup(&json_render_path)
4105            .expect("Json::render should be registered");
4106
4107        let callees: Vec<_> = ctx.code_graph.callees_of(into_response_id).collect();
4108
4109        // self.render() in impl IntoResponse for Html should resolve to Html::render,
4110        // NOT Json::render (which is a different type's render method)
4111        assert!(
4112            callees.contains(&html_render_id),
4113            "self.render() in Html's trait impl should resolve to Html::render; \
4114             callees = {:?}",
4115            callees
4116        );
4117        assert!(
4118            !callees.contains(&json_render_id),
4119            "self.render() in Html's trait impl should NOT resolve to Json::render; \
4120             callees = {:?}",
4121            callees
4122        );
4123    }
4124
4125    #[test]
4126    fn test_extract_self_type_from_parent_path() {
4127        // Trait impl
4128        assert_eq!(
4129            extract_self_type_from_parent_path("mylib::<impl IntoResponse for Html>"),
4130            Some("Html")
4131        );
4132        // Trait impl with generics
4133        assert_eq!(
4134            extract_self_type_from_parent_path("mylib::<impl io::Write for Writer < '_ >>"),
4135            Some("Writer")
4136        );
4137        // Inherent impl
4138        assert_eq!(
4139            extract_self_type_from_parent_path("mylib::<impl Router < S >>"),
4140            Some("Router")
4141        );
4142        // Plain type path
4143        assert_eq!(
4144            extract_self_type_from_parent_path("mylib::Html"),
4145            Some("Html")
4146        );
4147    }
4148}