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