1use std::collections::HashMap;
20use std::path::Path;
21use std::sync::Arc;
22
23use crate::import_map_builder::{build_import_map, collect_public_reexports};
24use crate::SymbolKind;
25use rayon::prelude::*;
26use ryo_source::pure::{
27 PureBlock, PureExpr, PureFields, PureFile, PureFn, PureImpl, PureImplItem, PureItem, PureStmt,
28 PureTraitItem, PureVis,
29};
30use ryo_symbol::{
31 CargoMetadataProvider, FileSpan, SymbolPathResolver, UseResolver, WorkspaceFilePath,
32 WorkspacePathResolver,
33};
34
35use crate::ast::ASTRegistry;
36use crate::detail_store::DetailStore;
37use crate::query::{
38 CodeEdgeV2, CodeGraphV2, DataFlowBuilderWorkspace, DataFlowGraphV2, TypeFlowBuilderV2,
39 TypeFlowGraphV2,
40};
41use crate::symbol::{
42 RegistryUpdate, RegistryUpdateBatch, SymbolId, SymbolPath, SymbolRegistry, Visibility,
43};
44
45pub type ImHashMap<K, V> = im::HashMap<K, V>;
47
48#[derive(Debug, thiserror::Error)]
50pub enum ContextError {
51 #[error("Metadata error: {0}")]
54 Metadata(String),
55
56 #[error("IO error: {0}")]
58 Io(String),
59
60 #[error("Parse error: {0}")]
62 Parse(String),
63
64 #[error("Resolve error: {0}")]
66 Resolve(String),
67
68 #[error("Source generation error: {0}")]
71 SourceGen(#[from] ryo_source::pure::ToSynError),
72}
73
74pub struct AnalysisContext {
95 pub workspace_root: Arc<Path>,
97 pub registry: SymbolRegistry,
99 pub code_graph: CodeGraphV2,
101 pub typeflow_graph: TypeFlowGraphV2,
103 pub dataflow_graph: DataFlowGraphV2,
105 pub detail_store: DetailStore,
107 pub ast_registry: ASTRegistry,
112 pub files: ImHashMap<WorkspaceFilePath, Arc<PureFile>>,
115 pub original: HashMap<WorkspaceFilePath, String>,
117 pub use_resolver: UseResolver,
119 #[cfg(feature = "literal-search")]
121 pub literal_index: Option<crate::literal::LiteralIndex>,
122 pub derive_index: crate::query::DeriveIndex,
124}
125
126#[derive(Debug, Clone, Default)]
128pub struct AnalysisConfig {
129 pub parallel: bool,
131 pub pub_only: bool,
133 pub uuid_mappings: Option<HashMap<String, String>>,
136}
137
138impl AnalysisConfig {
139 pub fn new() -> Self {
142 Self::default()
143 }
144
145 pub fn parallel(mut self) -> Self {
147 self.parallel = true;
148 self
149 }
150
151 pub fn pub_only(mut self) -> Self {
153 self.pub_only = true;
154 self
155 }
156
157 pub fn with_uuid_mappings(mut self, mappings: HashMap<String, String>) -> Self {
159 self.uuid_mappings = Some(mappings);
160 self
161 }
162}
163
164impl AnalysisContext {
165 pub fn from_workspace_root(path: impl AsRef<Path>) -> Result<Self, ContextError> {
185 use ryo_symbol::{CargoMetadataProvider, WorkspaceMetadataProvider};
186
187 let path = path.as_ref();
188
189 let metadata = CargoMetadataProvider::from_directory(path)
191 .map_err(|e| ContextError::Metadata(e.to_string()))?;
192
193 let workspace_root = metadata.workspace_root().to_path_buf();
194 let resolver = WorkspacePathResolver::new(workspace_root.clone());
195
196 let uuid_mappings = Self::load_uuid_mappings(&workspace_root);
198
199 let mut files = HashMap::new();
201 Self::load_dir(
202 &workspace_root,
203 &workspace_root,
204 &resolver,
205 &metadata,
206 &mut files,
207 )?;
208
209 let config = match uuid_mappings {
211 Some(mappings) => AnalysisConfig::default().with_uuid_mappings(mappings),
212 None => AnalysisConfig::default(),
213 };
214
215 Self::build_from_workspace_files(files, Arc::from(workspace_root.as_path()), config)
216 }
217
218 pub fn from_workspace_root_parallel(path: impl AsRef<Path>) -> Result<Self, ContextError> {
220 use ryo_symbol::{CargoMetadataProvider, WorkspaceMetadataProvider};
221
222 let path = path.as_ref();
223
224 let metadata = CargoMetadataProvider::from_directory(path)
225 .map_err(|e| ContextError::Metadata(e.to_string()))?;
226
227 let workspace_root = metadata.workspace_root().to_path_buf();
228 let resolver = WorkspacePathResolver::new(workspace_root.clone());
229
230 let uuid_mappings = Self::load_uuid_mappings(&workspace_root);
232
233 let mut files = HashMap::new();
234 Self::load_dir(
235 &workspace_root,
236 &workspace_root,
237 &resolver,
238 &metadata,
239 &mut files,
240 )?;
241
242 let config = match uuid_mappings {
244 Some(mappings) => AnalysisConfig::new()
245 .parallel()
246 .with_uuid_mappings(mappings),
247 None => AnalysisConfig::new().parallel(),
248 };
249
250 Self::build_from_workspace_files(files, Arc::from(workspace_root.as_path()), config)
251 }
252
253 fn load_uuid_mappings(workspace_root: &std::path::Path) -> Option<HashMap<String, String>> {
256 let path = workspace_root.join(".ryo").join("uuid-mapping.json");
257 let content = std::fs::read_to_string(&path).ok()?;
258 serde_json::from_str(&content).ok()
259 }
260
261 #[doc(hidden)]
274 #[cfg(any(test, feature = "testing"))]
275 pub fn save_uuid_mappings(&self) -> Result<(), ContextError> {
276 let ryo_dir = self.workspace_root.join(".ryo");
277 std::fs::create_dir_all(&ryo_dir)
278 .map_err(|e| ContextError::Io(format!("Failed to create .ryo directory: {}", e)))?;
279
280 let path = ryo_dir.join("uuid-mapping.json");
281 let mappings = self.registry.export_uuid_mapping_strings();
282
283 let content = serde_json::to_string_pretty(&mappings)
284 .map_err(|e| ContextError::Io(format!("Failed to serialize UUID mappings: {}", e)))?;
285
286 std::fs::write(&path, content)
287 .map_err(|e| ContextError::Io(format!("Failed to write UUID mappings: {}", e)))?;
288
289 Ok(())
290 }
291
292 fn load_dir(
294 _root: &Path,
295 dir: &Path,
296 resolver: &WorkspacePathResolver,
297 metadata: &CargoMetadataProvider,
298 files: &mut HashMap<WorkspaceFilePath, PureFile>,
299 ) -> Result<(), ContextError> {
300 if !dir.is_dir() {
301 return Ok(());
302 }
303
304 let dir_name = dir.file_name().and_then(|n| n.to_str()).unwrap_or("");
306 if matches!(
307 dir_name,
308 "target" | "node_modules" | ".git" | "dist" | "build"
309 ) {
310 return Ok(());
311 }
312
313 for entry in std::fs::read_dir(dir).map_err(|e| ContextError::Io(e.to_string()))? {
314 let entry = entry.map_err(|e| ContextError::Io(e.to_string()))?;
315 let path = entry.path();
316
317 if path.is_dir() {
318 Self::load_dir(_root, &path, resolver, metadata, files)?;
319 } else if path.extension().map(|e| e == "rs").unwrap_or(false) {
320 match Self::load_file(&path, resolver, metadata) {
321 Ok((wfp, file)) => {
322 files.insert(wfp, file);
323 }
324 Err(_e) => {
325 }
327 }
328 }
329 }
330
331 Ok(())
332 }
333
334 fn load_file(
336 path: &Path,
337 resolver: &WorkspacePathResolver,
338 metadata: &CargoMetadataProvider,
339 ) -> Result<(WorkspaceFilePath, PureFile), ContextError> {
340 let content = std::fs::read_to_string(path)
341 .map_err(|e| ContextError::Io(format!("{}: {}", path.display(), e)))?;
342
343 let file = PureFile::from_source(&content)
344 .map_err(|e| ContextError::Parse(format!("{}: {}", path.display(), e)))?;
345
346 let wfp = resolver
347 .resolve_with_provider(path, metadata)
348 .map_err(|e| ContextError::Resolve(format!("{}: {}", path.display(), e)))?;
349
350 Ok((wfp, file))
351 }
352
353 #[doc(hidden)]
373 pub fn from_workspace_files(files: HashMap<WorkspaceFilePath, PureFile>) -> Self {
374 let workspace_root = files
375 .keys()
376 .next()
377 .expect("from_workspace_files requires at least one file")
378 .workspace_root()
379 .into();
380
381 Self::build_from_workspace_files(files, workspace_root, AnalysisConfig::default())
383 .expect("build_from_workspace_files failed in test API")
384 }
385
386 pub fn from_im_files(files: ImHashMap<WorkspaceFilePath, Arc<PureFile>>) -> Self {
401 let workspace_root = files
403 .keys()
404 .next()
405 .expect("from_im_files requires at least one file")
406 .workspace_root()
407 .into();
408
409 Self {
410 workspace_root,
411 files,
412 original: HashMap::new(),
413 registry: SymbolRegistry::new(),
414 code_graph: CodeGraphV2::new(),
415 typeflow_graph: TypeFlowGraphV2::new(),
416 dataflow_graph: DataFlowGraphV2::new(),
417 detail_store: DetailStore::default(),
418 ast_registry: ASTRegistry::new(),
419 use_resolver: UseResolver::new(),
420 #[cfg(feature = "literal-search")]
421 literal_index: None,
422 derive_index: crate::query::DeriveIndex::new(),
423 }
424 }
425
426 fn build_from_workspace_files(
437 files: HashMap<WorkspaceFilePath, PureFile>,
438 workspace_root: Arc<Path>,
439 config: AnalysisConfig,
440 ) -> Result<Self, ContextError> {
441 let mut registry = SymbolRegistry::new();
442
443 if let Some(mappings) = config.uuid_mappings {
445 registry.preload_uuid_mapping_strings(mappings);
446 }
447
448 let mut code_graph = CodeGraphV2::new();
449
450 let crate_name = files
452 .keys()
453 .next()
454 .expect("No files loaded - cannot determine crate name")
455 .crate_name()
456 .as_str()
457 .to_string();
458
459 let original: HashMap<WorkspaceFilePath, String> = files
461 .iter()
462 .map(|(path, file)| Ok((path.clone(), file.to_source()?)))
463 .collect::<Result<HashMap<_, _>, ContextError>>()?;
464
465 let symbols = if config.parallel {
467 Self::collect_symbols_workspace_parallel(&files, &crate_name, config.pub_only)
468 } else {
469 Self::collect_symbols_workspace(&files, &crate_name, config.pub_only)
470 };
471
472 let mut symbol_ids: HashMap<String, SymbolId> = HashMap::new();
474
475 for (path, kind, pure_vis, file_path) in symbols {
476 let path_str = path.to_string();
477 if let Ok(id) = registry.register(path, kind) {
478 code_graph.add_node(id);
479 code_graph.add_to_kind_index(id, kind);
480 symbol_ids.insert(path_str, id);
481 let vis = pure_vis_to_visibility(&pure_vis);
483 let _ = registry.set_visibility(id, vis);
484 let span = FileSpan::new(file_path, 0, 0);
487 let _ = registry.set_span(id, span);
488 }
489 }
490
491 Self::build_contains_edges_workspace(&files, &crate_name, &symbol_ids, &mut code_graph);
493
494 let mut use_resolver = UseResolver::new();
497 for (file_path, file) in &files {
498 let file_crate_name = file_path.crate_name().as_str();
500 if let Ok(crate_name_obj) = ryo_symbol::CrateName::new(file_crate_name) {
501 let path_resolver = SymbolPathResolver::new(file_crate_name);
502 let mod_path_str = path_resolver.module_path_str(file_path);
503 if let Ok(module_path) = SymbolPath::parse(&mod_path_str) {
504 let import_map = build_import_map(file, &crate_name_obj, &module_path);
505 use_resolver.register(module_path, import_map);
506 }
507 }
508 }
509
510 for (file_path, file) in &files {
514 let file_crate_name = file_path.crate_name().as_str();
515 if let Ok(crate_name_obj) = ryo_symbol::CrateName::new(file_crate_name) {
516 let path_resolver = SymbolPathResolver::new(file_crate_name);
517 let mod_path_str = path_resolver.module_path_str(file_path);
518 if let Ok(module_path) = SymbolPath::parse(&mod_path_str) {
519 let reexports = collect_public_reexports(file, &crate_name_obj, &module_path);
520 for entry in reexports {
521 if let Ok(alias_path) = module_path.child(&entry.local_name) {
522 if let Some(canonical_id) = registry.lookup(&entry.full_path) {
524 if registry.lookup(&alias_path).is_none() {
525 let _ = registry.register_reexport(
526 canonical_id,
527 alias_path,
528 file_path.clone(),
529 );
530 }
531 }
532 }
533 }
534 }
535 }
536 }
537
538 Self::build_reference_edges_workspace(
540 &files,
541 &crate_name,
542 &symbol_ids,
543 &use_resolver,
544 ®istry,
545 &mut code_graph,
546 );
547
548 #[allow(deprecated)]
554 let detail_store = DetailStore::build_all_workspace(®istry, &files, &crate_name);
555
556 let im_files: ImHashMap<WorkspaceFilePath, Arc<PureFile>> = files
558 .into_iter()
559 .map(|(path, file)| (path, Arc::new(file)))
560 .collect();
561
562 #[allow(deprecated)]
565 let typeflow_graph =
566 TypeFlowBuilderV2::new_workspace(®istry, &im_files, &crate_name).build();
567
568 let dataflow_graph =
570 DataFlowBuilderWorkspace::new(®istry, &im_files, &crate_name).build();
571
572 let ast_registry = ASTRegistry::build_from_files(&im_files, ®istry, &crate_name);
574
575 #[cfg(feature = "literal-search")]
577 let literal_index =
578 crate::literal::LiteralIndex::build_from_workspace_files(&im_files, ®istry).ok();
579
580 let derive_index = crate::query::DeriveIndex::build(
582 &ast_registry,
583 &code_graph,
584 &typeflow_graph,
585 ®istry,
586 );
587
588 Ok(Self {
589 workspace_root,
590 registry,
591 code_graph,
592 typeflow_graph,
593 dataflow_graph,
594 detail_store,
595 ast_registry,
596 files: im_files,
597 original,
598 use_resolver,
599 #[cfg(feature = "literal-search")]
600 literal_index,
601 derive_index,
602 })
603 }
604
605 fn collect_symbols_workspace(
607 files: &HashMap<WorkspaceFilePath, PureFile>,
608 _crate_name: &str,
609 pub_only: bool,
610 ) -> Vec<(SymbolPath, SymbolKind, PureVis, WorkspaceFilePath)> {
611 let mut symbols = Vec::new();
612
613 for (file_path, file) in files {
614 let file_crate_name = file_path.crate_name().as_str();
616 let resolver = SymbolPathResolver::new(file_crate_name);
617 let mod_path = resolver.module_path_str(file_path);
618 let mut file_symbols = Vec::new();
619 Self::collect_from_file(&mod_path, file, pub_only, &mut file_symbols);
620 for (path, kind, vis) in file_symbols {
621 symbols.push((path, kind, vis, file_path.clone()));
622 }
623 }
624
625 symbols
626 }
627
628 fn collect_symbols_workspace_parallel(
630 files: &HashMap<WorkspaceFilePath, PureFile>,
631 _crate_name: &str,
632 pub_only: bool,
633 ) -> Vec<(SymbolPath, SymbolKind, PureVis, WorkspaceFilePath)> {
634 files
635 .par_iter()
636 .flat_map(|(file_path, file)| {
637 let file_crate_name = file_path.crate_name().as_str();
639 let resolver = SymbolPathResolver::new(file_crate_name);
640 let mod_path = resolver.module_path_str(file_path);
641 let mut symbols = Vec::new();
642 Self::collect_from_file(&mod_path, file, pub_only, &mut symbols);
643 symbols
644 .into_iter()
645 .map(|(path, kind, vis)| (path, kind, vis, file_path.clone()))
646 .collect::<Vec<_>>()
647 })
648 .collect()
649 }
650
651 fn collect_from_file(
653 mod_path: &str,
654 file: &PureFile,
655 pub_only: bool,
656 out: &mut Vec<(SymbolPath, SymbolKind, PureVis)>,
657 ) {
658 if let Ok(path) = SymbolPath::parse(mod_path) {
660 out.push((path, SymbolKind::Mod, PureVis::Public));
661 }
662
663 for item in &file.items {
665 Self::collect_from_item(mod_path, item, pub_only, out);
666 }
667 }
668
669 fn collect_from_item(
671 parent_path: &str,
672 item: &PureItem,
673 pub_only: bool,
674 out: &mut Vec<(SymbolPath, SymbolKind, PureVis)>,
675 ) {
676 let (name, kind, vis) = match item {
677 PureItem::Struct(s) => {
678 if pub_only && s.vis == PureVis::Private {
679 return;
680 }
681 if let Ok(parent) = SymbolPath::parse(parent_path) {
682 if let Ok(struct_path) = parent.child(&s.name) {
683 out.push((struct_path.clone(), SymbolKind::Struct, s.vis.clone()));
684 if let PureFields::Named(fields) = &s.fields {
686 for field in fields {
687 if let Ok(field_path) = struct_path.child(&field.name) {
688 out.push((field_path, SymbolKind::Field, field.vis.clone()));
689 }
690 }
691 }
692 }
693 }
694 return;
695 }
696 PureItem::Enum(e) => {
697 if pub_only && e.vis == PureVis::Private {
699 return;
700 }
701 let enum_path = format!("{}::{}", parent_path, e.name);
702 if let Ok(path) = SymbolPath::parse(&enum_path) {
703 out.push((path, SymbolKind::Enum, e.vis.clone()));
704 }
705 for variant in &e.variants {
707 let variant_path = format!("{}::{}", enum_path, variant.name);
708 if let Ok(path) = SymbolPath::parse(&variant_path) {
709 out.push((path, SymbolKind::Variant, e.vis.clone()));
711 }
712 }
713 return;
714 }
715 PureItem::Fn(f) => (f.name.clone(), SymbolKind::Function, f.vis.clone()),
716 PureItem::Trait(t) => {
717 if pub_only && t.vis == PureVis::Private {
718 return;
719 }
720 if let Ok(parent) = SymbolPath::parse(parent_path) {
721 if let Ok(trait_path) = parent.child(&t.name) {
722 out.push((trait_path.clone(), SymbolKind::Trait, t.vis.clone()));
723 for trait_item in &t.items {
725 let (item_name, item_kind) = match trait_item {
726 PureTraitItem::Fn(f) => (&f.name, SymbolKind::Method),
727 PureTraitItem::Const(c) => (&c.name, SymbolKind::Const),
728 PureTraitItem::Type { name, .. } => (name, SymbolKind::TypeAlias),
729 PureTraitItem::Other(_) => continue,
730 };
731 if let Ok(item_path) = trait_path.child(item_name) {
732 out.push((item_path, item_kind, t.vis.clone()));
734 }
735 }
736 }
737 }
738 return;
739 }
740 PureItem::Impl(i) => {
741 Self::collect_from_impl(parent_path, i, pub_only, out);
742 return;
743 }
744 PureItem::Mod(m) => {
745 if pub_only && m.vis == PureVis::Private {
746 return;
747 }
748 let mod_path = format!("{}::{}", parent_path, m.name);
749 if let Ok(path) = SymbolPath::parse(&mod_path) {
750 out.push((path, SymbolKind::Mod, m.vis.clone()));
751 }
752 for inner_item in &m.items {
754 Self::collect_from_item(&mod_path, inner_item, pub_only, out);
755 }
756 return;
757 }
758 PureItem::Use(_) => return,
759 PureItem::Const(c) => (c.name.clone(), SymbolKind::Const, c.vis.clone()),
760 PureItem::Static(s) => (s.name.clone(), SymbolKind::Static, s.vis.clone()),
761 PureItem::Type(t) => (t.name.clone(), SymbolKind::TypeAlias, t.vis.clone()),
762 PureItem::Macro(_) => return,
763 PureItem::Other(_) => return,
764 };
765
766 if pub_only && vis == PureVis::Private {
768 return;
769 }
770
771 let full_path = format!("{}::{}", parent_path, name);
772 if let Ok(path) = SymbolPath::parse(&full_path) {
773 out.push((path, kind, vis));
774 }
775 }
776
777 fn collect_from_impl(
785 parent_path: &str,
786 impl_block: &PureImpl,
787 pub_only: bool,
788 out: &mut Vec<(SymbolPath, SymbolKind, PureVis)>,
789 ) {
790 let parent = match SymbolPath::parse(parent_path) {
791 Ok(p) => p,
792 Err(_) => return,
793 };
794 let impl_target = &impl_block.self_ty;
795
796 let method_base = if let Some(ref trait_name) = impl_block.trait_ {
800 let impl_path = parent.child_trait_impl(trait_name, impl_target);
801 out.push((impl_path.clone(), SymbolKind::Impl, PureVis::Public));
802 impl_path
803 } else {
804 let impl_path = parent.child_inherent_impl(impl_target);
805 out.push((impl_path, SymbolKind::Impl, PureVis::Public));
806 let base_type = impl_target.split('<').next().unwrap_or(impl_target).trim();
810 match parent.child(base_type) {
811 Ok(p) => p,
812 Err(_) => return,
813 }
814 };
815
816 for item in &impl_block.items {
818 let (name, kind, vis) = match item {
819 PureImplItem::Fn(m) => (m.name.clone(), SymbolKind::Method, m.vis.clone()),
820 PureImplItem::Const(c) => (c.name.clone(), SymbolKind::Const, c.vis.clone()),
821 PureImplItem::Type(t) => (t.name.clone(), SymbolKind::TypeAlias, t.vis.clone()),
822 PureImplItem::Other(_) => continue,
823 };
824
825 if pub_only && vis == PureVis::Private {
826 continue;
827 }
828
829 if let Ok(path) = method_base.child(&name) {
830 out.push((path, kind, vis));
831 }
832 }
833 }
834
835 fn build_contains_edges_workspace(
839 _files: &HashMap<WorkspaceFilePath, PureFile>,
840 _crate_name: &str,
841 symbol_ids: &HashMap<String, SymbolId>,
842 graph: &mut CodeGraphV2,
843 ) {
844 for (path_str, &child_id) in symbol_ids {
846 if let Some(parent_path) = get_parent_path(path_str) {
847 if let Some(&parent_id) = symbol_ids.get(&parent_path) {
848 graph.add_edge(parent_id, child_id, CodeEdgeV2::Contains);
849 }
850 }
851 }
852 }
853
854 fn build_reference_edges_workspace(
856 files: &HashMap<WorkspaceFilePath, PureFile>,
857 _crate_name: &str,
858 symbol_ids: &HashMap<String, SymbolId>,
859 use_resolver: &UseResolver,
860 registry: &SymbolRegistry,
861 graph: &mut CodeGraphV2,
862 ) {
863 let method_index = build_method_name_index(symbol_ids, registry);
864 for (file_path, file) in files {
865 let file_crate_name = file_path.crate_name().as_str();
867 let resolver = SymbolPathResolver::new(file_crate_name);
868 let mod_path = resolver.module_path_str(file_path);
869 Self::build_edges_from_items(
870 &mod_path,
871 &file.items,
872 symbol_ids,
873 use_resolver,
874 registry,
875 graph,
876 &method_index,
877 );
878 }
879 }
880
881 fn build_edges_from_items(
883 parent_path: &str,
884 items: &[PureItem],
885 symbol_ids: &HashMap<String, SymbolId>,
886 use_resolver: &UseResolver,
887 registry: &SymbolRegistry,
888 graph: &mut CodeGraphV2,
889 method_index: &MethodNameIndex,
890 ) {
891 for item in items {
892 match item {
893 PureItem::Impl(impl_block) => {
894 Self::build_edges_from_impl(
895 parent_path,
896 impl_block,
897 symbol_ids,
898 use_resolver,
899 registry,
900 graph,
901 method_index,
902 );
903 }
904 PureItem::Fn(func) => {
905 let fn_path = format!("{}::{}", parent_path, func.name);
906 Self::build_edges_from_fn(
907 &fn_path,
908 func,
909 symbol_ids,
910 use_resolver,
911 registry,
912 graph,
913 method_index,
914 );
915 }
916 PureItem::Struct(_) => {
917 }
919 PureItem::Mod(m) if !m.items.is_empty() => {
920 let mod_path = format!("{}::{}", parent_path, m.name);
921 Self::build_edges_from_items(
922 &mod_path,
923 &m.items,
924 symbol_ids,
925 use_resolver,
926 registry,
927 graph,
928 method_index,
929 );
930 }
931 _ => {}
932 }
933 }
934 }
935
936 fn build_edges_from_item(
941 parent_path: &str,
942 item: &PureItem,
943 symbol_ids: &HashMap<String, SymbolId>,
944 use_resolver: &UseResolver,
945 registry: &SymbolRegistry,
946 graph: &mut CodeGraphV2,
947 method_index: &MethodNameIndex,
948 ) {
949 match item {
950 PureItem::Impl(impl_block) => {
951 Self::build_edges_from_impl(
952 parent_path,
953 impl_block,
954 symbol_ids,
955 use_resolver,
956 registry,
957 graph,
958 method_index,
959 );
960 }
961 PureItem::Fn(func) => {
962 let fn_path = format!("{}::{}", parent_path, func.name);
963 Self::build_edges_from_fn(
964 &fn_path,
965 func,
966 symbol_ids,
967 use_resolver,
968 registry,
969 graph,
970 method_index,
971 );
972 }
973 PureItem::Struct(_) => {
974 }
976 PureItem::Mod(m) if !m.items.is_empty() => {
977 let mod_path = format!("{}::{}", parent_path, m.name);
978 Self::build_edges_from_items(
979 &mod_path,
980 &m.items,
981 symbol_ids,
982 use_resolver,
983 registry,
984 graph,
985 method_index,
986 );
987 }
988 _ => {}
989 }
990 }
991
992 fn build_edges_from_impl(
994 parent_path: &str,
995 impl_block: &PureImpl,
996 symbol_ids: &HashMap<String, SymbolId>,
997 use_resolver: &UseResolver,
998 registry: &SymbolRegistry,
999 graph: &mut CodeGraphV2,
1000 method_index: &MethodNameIndex,
1001 ) {
1002 let parent = match SymbolPath::parse(parent_path) {
1003 Ok(p) => p,
1004 Err(_) => return,
1005 };
1006 let impl_target = &impl_block.self_ty;
1007
1008 let impl_path = if let Some(ref trait_name) = &impl_block.trait_ {
1010 parent.child_trait_impl(trait_name, impl_target)
1011 } else {
1012 parent.child_inherent_impl(impl_target)
1013 };
1014
1015 let impl_id = match symbol_ids.get(&impl_path.to_string()) {
1017 Some(&id) => id,
1018 None => return,
1019 };
1020
1021 if let Some(ref trait_name) = &impl_block.trait_ {
1023 if let Some(trait_id) = Self::resolve_type_reference(
1024 parent_path,
1025 trait_name,
1026 symbol_ids,
1027 use_resolver,
1028 registry,
1029 ) {
1030 graph.add_edge(impl_id, trait_id, CodeEdgeV2::Implements);
1031 }
1032 }
1033
1034 let method_base = if impl_block.trait_.is_some() {
1040 impl_path
1041 } else {
1042 let base_type = impl_target.split('<').next().unwrap_or(impl_target).trim();
1044 match parent.child(base_type) {
1045 Ok(p) => p,
1046 Err(_) => return,
1047 }
1048 };
1049
1050 for item in &impl_block.items {
1051 if let PureImplItem::Fn(func) = item {
1052 if let Ok(method_path) = method_base.child(&func.name) {
1053 Self::build_edges_from_fn(
1054 &method_path.to_string(),
1055 func,
1056 symbol_ids,
1057 use_resolver,
1058 registry,
1059 graph,
1060 method_index,
1061 );
1062 }
1063 }
1064 }
1065 }
1066
1067 fn build_edges_from_fn(
1069 fn_path: &str,
1070 func: &PureFn,
1071 symbol_ids: &HashMap<String, SymbolId>,
1072 use_resolver: &UseResolver,
1073 registry: &SymbolRegistry,
1074 graph: &mut CodeGraphV2,
1075 method_index: &MethodNameIndex,
1076 ) {
1077 let fn_id = match symbol_ids.get(fn_path) {
1078 Some(&id) => id,
1079 None => return,
1080 };
1081
1082 let parent_path = get_parent_path(fn_path).unwrap_or_default();
1084
1085 let mut cx = CallsBuildContext {
1090 symbol_ids,
1091 use_resolver,
1092 registry,
1093 graph,
1094 method_index,
1095 };
1096 Self::build_calls_from_block(fn_id, &parent_path, &func.body, &mut cx);
1097 }
1098
1099 fn build_calls_from_block(
1101 caller_id: SymbolId,
1102 parent_path: &str,
1103 block: &PureBlock,
1104 cx: &mut CallsBuildContext<'_>,
1105 ) {
1106 for stmt in &block.stmts {
1107 match stmt {
1108 PureStmt::Local {
1109 init: Some(expr), ..
1110 }
1111 | PureStmt::Semi(expr)
1112 | PureStmt::Expr(expr) => {
1113 Self::build_calls_from_expr(caller_id, parent_path, expr, cx);
1114 }
1115 _ => {}
1116 }
1117 }
1118 }
1119
1120 fn build_calls_from_expr(
1122 caller_id: SymbolId,
1123 parent_path: &str,
1124 expr: &PureExpr,
1125 cx: &mut CallsBuildContext<'_>,
1126 ) {
1127 use ryo_source::pure::PureExpr;
1128
1129 match expr {
1130 PureExpr::Call { func, args } => {
1131 if let PureExpr::Path(path) = func.as_ref() {
1133 if let Some(callee_id) = Self::resolve_type_reference(
1134 parent_path,
1135 path,
1136 cx.symbol_ids,
1137 cx.use_resolver,
1138 cx.registry,
1139 ) {
1140 cx.graph.add_edge(caller_id, callee_id, CodeEdgeV2::Calls);
1141 }
1142 }
1143 for arg in args {
1145 Self::build_calls_from_expr(caller_id, parent_path, arg, cx);
1146 }
1147 Self::build_calls_from_expr(caller_id, parent_path, func, cx);
1149 }
1150 PureExpr::MethodCall {
1151 receiver,
1152 method,
1153 args,
1154 ..
1155 } => {
1156 let is_self_receiver = matches!(receiver.as_ref(), PureExpr::Path(name) if name == "self")
1160 || matches!(receiver.as_ref(), PureExpr::Field { expr, .. } if matches!(expr.as_ref(), PureExpr::Path(name) if name == "self"));
1161
1162 let mut resolved = false;
1163 if is_self_receiver {
1164 let sibling_path = format!("{}::{}", parent_path, method);
1166 if let Some(&callee_id) = cx.symbol_ids.get(&sibling_path) {
1167 if callee_id != caller_id {
1168 cx.graph.add_edge(caller_id, callee_id, CodeEdgeV2::Calls);
1169 resolved = true;
1170 }
1171 }
1172 }
1173
1174 if !resolved {
1186 if let Some(candidates) = cx.method_index.get(method.as_str()) {
1187 let explicit_hint = extract_receiver_type_hint(receiver);
1190 let type_hint = explicit_hint.or_else(|| {
1191 if is_self_receiver {
1192 extract_self_type_from_parent_path(parent_path)
1193 } else {
1194 None
1195 }
1196 });
1197
1198 if let Some(hint) = type_hint {
1199 let filtered: Vec<_> = candidates
1201 .iter()
1202 .copied()
1203 .filter(|&id| candidate_matches_type_hint(id, hint, cx.registry))
1204 .collect();
1205 if !filtered.is_empty() {
1206 for callee_id in filtered {
1207 if callee_id != caller_id {
1208 cx.graph.add_edge(caller_id, callee_id, CodeEdgeV2::Calls);
1209 }
1210 }
1211 } else {
1212 let is_common = is_common_trait_method(method);
1214 if !is_common || candidates.len() == 1 {
1215 for &callee_id in candidates {
1216 if callee_id != caller_id {
1217 cx.graph.add_edge(
1218 caller_id,
1219 callee_id,
1220 CodeEdgeV2::Calls,
1221 );
1222 }
1223 }
1224 }
1225 }
1226 } else {
1227 let is_common = is_common_trait_method(method);
1229 if !is_common || candidates.len() == 1 {
1230 for &callee_id in candidates {
1231 if callee_id != caller_id {
1232 cx.graph.add_edge(caller_id, callee_id, CodeEdgeV2::Calls);
1233 }
1234 }
1235 }
1236 }
1237 }
1238 }
1239
1240 Self::build_calls_from_expr(caller_id, parent_path, receiver, cx);
1242 for arg in args {
1243 Self::build_calls_from_expr(caller_id, parent_path, arg, cx);
1244 }
1245 }
1246 PureExpr::Block { block, .. } => {
1247 Self::build_calls_from_block(caller_id, parent_path, block, cx);
1248 }
1249 PureExpr::If {
1250 cond,
1251 then_branch,
1252 else_branch,
1253 } => {
1254 Self::build_calls_from_expr(caller_id, parent_path, cond, cx);
1255 Self::build_calls_from_block(caller_id, parent_path, then_branch, cx);
1256 if let Some(else_expr) = else_branch {
1257 Self::build_calls_from_expr(caller_id, parent_path, else_expr, cx);
1258 }
1259 }
1260 PureExpr::Match {
1261 expr: match_expr,
1262 arms,
1263 } => {
1264 Self::build_calls_from_expr(caller_id, parent_path, match_expr, cx);
1265 for arm in arms {
1266 Self::build_calls_from_expr(caller_id, parent_path, &arm.body, cx);
1267 if let Some(ref guard) = arm.guard {
1268 Self::build_calls_from_expr(caller_id, parent_path, guard, cx);
1269 }
1270 }
1271 }
1272 PureExpr::Loop { body: block, .. }
1273 | PureExpr::Async { body: block, .. }
1274 | PureExpr::Unsafe(block) => {
1275 Self::build_calls_from_block(caller_id, parent_path, block, cx);
1276 }
1277 PureExpr::While { cond, body, .. } => {
1278 Self::build_calls_from_expr(caller_id, parent_path, cond, cx);
1279 Self::build_calls_from_block(caller_id, parent_path, body, cx);
1280 }
1281 PureExpr::For {
1282 expr: iter_expr,
1283 body,
1284 ..
1285 } => {
1286 Self::build_calls_from_expr(caller_id, parent_path, iter_expr, cx);
1287 Self::build_calls_from_block(caller_id, parent_path, body, cx);
1288 }
1289 PureExpr::Closure { body, .. } => {
1290 Self::build_calls_from_expr(caller_id, parent_path, body, cx);
1291 }
1292 PureExpr::Binary { left, right, .. } => {
1293 Self::build_calls_from_expr(caller_id, parent_path, left, cx);
1294 Self::build_calls_from_expr(caller_id, parent_path, right, cx);
1295 }
1296 PureExpr::Unary { expr: inner, .. }
1297 | PureExpr::Field { expr: inner, .. }
1298 | PureExpr::Await(inner)
1299 | PureExpr::Try(inner)
1300 | PureExpr::Ref { expr: inner, .. }
1301 | PureExpr::Cast { expr: inner, .. } => {
1302 Self::build_calls_from_expr(caller_id, parent_path, inner, cx);
1303 }
1304 PureExpr::Index { expr: arr, index } => {
1305 Self::build_calls_from_expr(caller_id, parent_path, arr, cx);
1306 Self::build_calls_from_expr(caller_id, parent_path, index, cx);
1307 }
1308 PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
1309 for e in exprs {
1310 Self::build_calls_from_expr(caller_id, parent_path, e, cx);
1311 }
1312 }
1313 PureExpr::Struct { fields, .. } => {
1314 for (_, e) in fields {
1315 Self::build_calls_from_expr(caller_id, parent_path, e, cx);
1316 }
1317 }
1318 PureExpr::Return(Some(inner))
1319 | PureExpr::Break {
1320 expr: Some(inner), ..
1321 } => {
1322 Self::build_calls_from_expr(caller_id, parent_path, inner, cx);
1323 }
1324 PureExpr::Range { start, end, .. } => {
1325 if let Some(s) = start {
1326 Self::build_calls_from_expr(caller_id, parent_path, s, cx);
1327 }
1328 if let Some(e) = end {
1329 Self::build_calls_from_expr(caller_id, parent_path, e, cx);
1330 }
1331 }
1332 PureExpr::Let { expr: inner, .. } => {
1333 Self::build_calls_from_expr(caller_id, parent_path, inner, cx);
1334 }
1335 PureExpr::Repeat { expr: elem, len } => {
1336 Self::build_calls_from_expr(caller_id, parent_path, elem, cx);
1337 Self::build_calls_from_expr(caller_id, parent_path, len, cx);
1338 }
1339 _ => {}
1340 }
1341 }
1342
1343 fn resolve_type_reference(
1351 parent_path: &str,
1352 type_name: &str,
1353 symbol_ids: &HashMap<String, SymbolId>,
1354 use_resolver: &UseResolver,
1355 registry: &SymbolRegistry,
1356 ) -> Option<SymbolId> {
1357 let primitives = [
1359 "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize",
1360 "f32", "f64", "bool", "char", "str", "Self",
1361 ];
1362 if primitives.contains(&type_name) {
1363 return None;
1364 }
1365
1366 let module_path_str = strip_to_module_path(parent_path);
1370
1371 if let Ok(module_path) = SymbolPath::parse(&module_path_str) {
1373 if let Some(id) = use_resolver.resolve(&module_path, type_name, registry) {
1374 return Some(id);
1375 }
1376 }
1377
1378 if type_name.contains("::") {
1380 if let Some(&id) = symbol_ids.get(type_name) {
1381 return Some(id);
1382 }
1383
1384 if let Some(split_pos) = type_name.find("::") {
1387 let prefix = &type_name[..split_pos];
1388 let suffix = &type_name[split_pos..]; if let Ok(module_path) = SymbolPath::parse(&module_path_str) {
1390 if let Some(resolved_prefix_id) =
1391 use_resolver.resolve(&module_path, prefix, registry)
1392 {
1393 let resolved_prefix_path = registry.path(resolved_prefix_id);
1394 if let Some(full_path_str) = resolved_prefix_path {
1395 let combined = format!("{}{}", full_path_str, suffix);
1396 if let Some(&id) = symbol_ids.get(&combined) {
1397 return Some(id);
1398 }
1399 }
1400 }
1401 }
1402 }
1403 }
1404
1405 let qualified = format!("{}::{}", parent_path, type_name);
1407 if let Some(&id) = symbol_ids.get(&qualified) {
1408 return Some(id);
1409 }
1410
1411 let mut current_path = parent_path.to_string();
1413 while let Some(parent) = get_parent_path(¤t_path) {
1414 let qualified = format!("{}::{}", parent, type_name);
1415 if let Some(&id) = symbol_ids.get(&qualified) {
1416 return Some(id);
1417 }
1418 current_path = parent;
1419 }
1420
1421 symbol_ids.get(type_name).copied()
1423 }
1424
1425 pub fn registry(&self) -> &SymbolRegistry {
1427 &self.registry
1428 }
1429
1430 pub fn registry_mut(&mut self) -> &mut SymbolRegistry {
1432 &mut self.registry
1433 }
1434
1435 pub fn code_graph(&self) -> &CodeGraphV2 {
1437 &self.code_graph
1438 }
1439
1440 pub fn code_graph_mut(&mut self) -> &mut CodeGraphV2 {
1442 &mut self.code_graph
1443 }
1444
1445 pub fn typeflow_graph(&self) -> &TypeFlowGraphV2 {
1447 &self.typeflow_graph
1448 }
1449
1450 pub fn workspace_root(&self) -> &Path {
1452 &self.workspace_root
1453 }
1454
1455 pub fn file(&self, path: &WorkspaceFilePath) -> Option<&PureFile> {
1457 self.files.get(path).map(|arc| arc.as_ref())
1458 }
1459
1460 pub fn file_mut(&mut self, path: &WorkspaceFilePath) -> Option<&mut PureFile> {
1466 self.files.get_mut(path).map(Arc::make_mut)
1467 }
1468
1469 pub fn files(&self) -> &ImHashMap<WorkspaceFilePath, Arc<PureFile>> {
1471 &self.files
1472 }
1473
1474 pub fn files_mut(&mut self) -> &mut ImHashMap<WorkspaceFilePath, Arc<PureFile>> {
1476 &mut self.files
1477 }
1478
1479 pub fn original(&self, path: &WorkspaceFilePath) -> Option<&String> {
1481 self.original.get(path)
1482 }
1483
1484 pub fn file_count(&self) -> usize {
1486 self.files.len()
1487 }
1488
1489 pub fn is_empty(&self) -> bool {
1491 self.files.is_empty()
1492 }
1493
1494 pub fn detail_store(&self) -> &DetailStore {
1500 &self.detail_store
1501 }
1502
1503 pub fn detail_store_mut(&mut self) -> &mut DetailStore {
1505 &mut self.detail_store
1506 }
1507
1508 pub fn commit_changes(&mut self, updates: &RegistryUpdateBatch) {
1529 let affected_ids: Vec<SymbolId> =
1531 updates.into_iter().filter_map(|u| u.target_id()).collect();
1532
1533 for update in updates {
1535 let _old_kind = match update {
1537 RegistryUpdate::UpdateKind { id, .. } => self.registry.kind(*id),
1538 _ => None,
1539 };
1540
1541 if let Err(e) = update.clone().apply(&mut self.registry) {
1543 eprintln!("Warning: Failed to apply registry update: {:?}", e);
1544 continue;
1545 }
1546
1547 match update {
1549 RegistryUpdate::Add { path, kind, .. } => {
1550 if let Some(id) = self.registry.lookup(path) {
1551 self.code_graph.add_node(id);
1552 self.code_graph.add_to_kind_index(id, *kind);
1553 }
1554 }
1555 RegistryUpdate::Remove { id } => {
1556 self.code_graph.remove_node(*id);
1557 }
1558 RegistryUpdate::UpdateKind { id, new_kind } => {
1559 if self.code_graph.contains(*id) {
1562 self.code_graph.add_to_kind_index(*id, *new_kind);
1563 }
1564 }
1565 RegistryUpdate::Rename { .. }
1567 | RegistryUpdate::UpdateSpan { .. }
1568 | RegistryUpdate::UpdateVisibility { .. } => {}
1569 }
1570 }
1571
1572 let crate_name = self
1575 .files
1576 .keys()
1577 .next()
1578 .and_then(|path| SymbolPathResolver::from_workspace_path(path).ok())
1579 .map(|r| r.crate_name().to_string())
1580 .unwrap_or_else(|| "crate".to_string());
1581 self.detail_store.rebuild_affected_workspace(
1582 &affected_ids,
1583 &self.registry,
1584 &self.files,
1585 &crate_name,
1586 );
1587 }
1588
1589 pub fn rebuild_edges_for_files(&mut self, file_paths: &[WorkspaceFilePath]) {
1600 let mut symbol_ids: HashMap<String, SymbolId> = HashMap::new();
1602 for (id, _) in self.registry.iter() {
1603 if let Some(path) = self.registry.resolve(id) {
1604 symbol_ids.insert(path.to_string(), id);
1605 }
1606 }
1607
1608 for file_path in file_paths {
1609 for (id, _) in self.registry.iter() {
1611 if let Some(span) = self.registry.span(id) {
1612 if &span.file == file_path {
1613 self.code_graph.clear_outgoing_edges(id);
1614 }
1615 }
1616 }
1617
1618 if let Some(file) = self.files.get(file_path) {
1620 let file_crate_name = file_path.crate_name().as_str();
1621 let resolver = SymbolPathResolver::new(file_crate_name);
1622 let mod_path = resolver.module_path_str(file_path);
1623
1624 let method_index = build_method_name_index(&symbol_ids, &self.registry);
1625 Self::build_edges_from_items(
1626 &mod_path,
1627 &file.items,
1628 &symbol_ids,
1629 &self.use_resolver,
1630 &self.registry,
1631 &mut self.code_graph,
1632 &method_index,
1633 );
1634 }
1635 }
1636 }
1637
1638 pub fn rebuild_edges_for_symbols(&mut self, affected_ids: &[SymbolId]) {
1650 if affected_ids.is_empty() {
1651 return;
1652 }
1653
1654 let mut symbol_ids: HashMap<String, SymbolId> = HashMap::new();
1656 for (id, _) in self.registry.iter() {
1657 if let Some(path) = self.registry.resolve(id) {
1658 symbol_ids.insert(path.to_string(), id);
1659 }
1660 }
1661
1662 for &id in affected_ids {
1664 self.code_graph.clear_outgoing_edges(id);
1665 }
1666
1667 for &id in affected_ids {
1669 let parent_path = match self.registry.resolve(id) {
1671 Some(path) => {
1672 let path_str = path.to_string();
1674 path_str
1675 .rsplit_once("::")
1676 .map(|(parent, _)| parent.to_string())
1677 .unwrap_or_else(|| path_str.clone())
1678 }
1679 None => continue,
1680 };
1681
1682 if let Some(item) = self.ast_registry.get(id) {
1684 let method_index = build_method_name_index(&symbol_ids, &self.registry);
1685 Self::build_edges_from_item(
1686 &parent_path,
1687 item,
1688 &symbol_ids,
1689 &self.use_resolver,
1690 &self.registry,
1691 &mut self.code_graph,
1692 &method_index,
1693 );
1694 }
1695 }
1696 }
1697
1698 pub fn get_symbols_in_files(&self, file_paths: &[WorkspaceFilePath]) -> Vec<SymbolId> {
1705 let file_set: std::collections::HashSet<_> = file_paths.iter().collect();
1706 let mut symbols = Vec::new();
1707
1708 for (id, _) in self.registry.iter() {
1709 if let Some(span) = self.registry.span(id) {
1710 if file_set.contains(&span.file) {
1711 symbols.push(id);
1712 }
1713 }
1714 }
1715
1716 symbols
1717 }
1718
1719 pub fn rebuild_after_mutation(&mut self, modified_files: &[WorkspaceFilePath]) {
1726 let affected_symbols = self.get_symbols_in_files(modified_files);
1727 self.rebuild_after_mutation_by_symbols(&affected_symbols);
1728 }
1729
1730 pub fn rebuild_after_mutation_by_symbols(&mut self, affected_ids: &[SymbolId]) {
1748 if affected_ids.is_empty() {
1749 return;
1750 }
1751
1752 let crate_name = self
1754 .files
1755 .keys()
1756 .next()
1757 .map(|r| r.crate_name().to_string())
1758 .unwrap_or_else(|| "unknown".to_string());
1759
1760 let mut file_set = std::collections::HashSet::new();
1762 for &id in affected_ids {
1763 if let Some(span) = self.registry.span(id) {
1764 file_set.insert(span.file.clone());
1765 }
1766 }
1767 let _modified_files: Vec<_> = file_set.into_iter().collect();
1768
1769 self.rebuild_edges_for_symbols(affected_ids);
1771
1772 self.typeflow_graph =
1775 TypeFlowBuilderV2::build_from_ast_registry(&self.registry, &self.ast_registry);
1776
1777 self.dataflow_graph.clear_for_symbols(affected_ids);
1779 DataFlowBuilderWorkspace::new(&self.registry, &self.files, &crate_name)
1780 .build_incremental_by_symbols(
1781 &mut self.dataflow_graph,
1782 &self.ast_registry,
1783 affected_ids,
1784 );
1785
1786 self.detail_store
1788 .rebuild_for_symbols(affected_ids, &self.ast_registry);
1789
1790 self.derive_index.rebuild_for_symbols(
1792 affected_ids,
1793 &self.ast_registry,
1794 &self.code_graph,
1795 &self.typeflow_graph,
1796 &self.registry,
1797 );
1798 }
1799
1800 pub fn fork(&self) -> ExecutionContext<'_> {
1835 ExecutionContext {
1836 workspace_root: &self.workspace_root,
1837 registry: &self.registry,
1838 graph: &self.code_graph,
1839 files: self.files.clone(), }
1841 }
1842
1843 pub fn fork_rebuild(&self) -> Self {
1860 let files: HashMap<WorkspaceFilePath, PureFile> = self
1862 .files
1863 .iter()
1864 .map(|(path, arc)| (path.clone(), (**arc).clone()))
1865 .collect();
1866 Self::build_from_workspace_files(
1867 files,
1868 self.workspace_root.clone(),
1869 AnalysisConfig::default(),
1870 )
1871 .expect("fork_rebuild: source generation failed")
1872 }
1873
1874 pub fn fork_clone(&self) -> Self {
1891 Self {
1892 workspace_root: self.workspace_root.clone(),
1893 registry: self.registry.clone(),
1894 code_graph: self.code_graph.clone(),
1895 typeflow_graph: self.typeflow_graph.clone(),
1896 dataflow_graph: self.dataflow_graph.clone(),
1897 detail_store: self.detail_store.clone(),
1898 ast_registry: self.ast_registry.clone(),
1899 files: self.files.clone(), original: self.original.clone(),
1901 use_resolver: self.use_resolver.clone(),
1902 #[cfg(feature = "literal-search")]
1905 literal_index: None,
1906 derive_index: self.derive_index.clone(),
1907 }
1908 }
1909
1910 pub fn symbol_count(&self) -> usize {
1912 self.registry.len()
1913 }
1914
1915 pub fn snapshot_symbols(&self, symbols: &[SymbolId]) -> ContextSnapshot {
1931 let ast_items: HashMap<SymbolId, PureItem> = symbols
1932 .iter()
1933 .filter_map(|&id| self.ast_registry.get(id).map(|item| (id, item.clone())))
1934 .collect();
1935
1936 ContextSnapshot { ast_items }
1937 }
1938
1939 pub fn rollback(&mut self, snapshot: ContextSnapshot, affected_ids: &[SymbolId]) {
1945 for (id, item) in snapshot.ast_items {
1947 self.ast_registry.set(id, item);
1948 }
1949
1950 if !affected_ids.is_empty() {
1953 self.rebuild_after_mutation_by_symbols(affected_ids);
1954 }
1955 }
1956}
1957
1958#[derive(Debug, Clone)]
1963pub struct ContextSnapshot {
1964 pub ast_items: HashMap<SymbolId, PureItem>,
1966}
1967
1968pub struct ExecutionContext<'a> {
2005 pub workspace_root: &'a Path,
2007
2008 pub registry: &'a SymbolRegistry,
2013
2014 pub graph: &'a CodeGraphV2,
2016
2017 pub files: ImHashMap<WorkspaceFilePath, Arc<PureFile>>,
2023}
2024
2025impl<'a> ExecutionContext<'a> {
2026 pub fn file(&self, path: &WorkspaceFilePath) -> Option<&PureFile> {
2028 self.files.get(path).map(|arc| arc.as_ref())
2029 }
2030
2031 pub fn file_mut(&mut self, path: &WorkspaceFilePath) -> Option<&mut PureFile> {
2037 self.files.get_mut(path).map(Arc::make_mut)
2038 }
2039
2040 pub fn has_file(&self, path: &WorkspaceFilePath) -> bool {
2042 self.files.contains_key(path)
2043 }
2044
2045 pub fn file_count(&self) -> usize {
2047 self.files.len()
2048 }
2049}
2050
2051fn pure_vis_to_visibility(pure_vis: &PureVis) -> Visibility {
2053 match pure_vis {
2054 PureVis::Public => Visibility::Public,
2055 PureVis::Crate => Visibility::Crate,
2056 PureVis::Super => Visibility::Super,
2057 PureVis::Private => Visibility::Private,
2058 PureVis::In(path) => {
2059 SymbolPath::parse(path)
2061 .map(|p| Visibility::Restricted(Box::new(p)))
2062 .unwrap_or(Visibility::Private)
2063 }
2064 }
2065}
2066
2067fn get_parent_path(path: &str) -> Option<String> {
2069 let parts: Vec<&str> = path.rsplitn(2, "::").collect();
2070 if parts.len() == 2 {
2071 Some(parts[1].to_string())
2072 } else {
2073 None
2074 }
2075}
2076
2077fn strip_to_module_path(path: &str) -> String {
2086 if let Some(impl_pos) = path.find("::<impl ") {
2088 return path[..impl_pos].to_string();
2089 }
2090 path.to_string()
2091}
2092
2093fn is_common_trait_method(method: &str) -> bool {
2098 matches!(
2099 method,
2100 "new"
2101 | "default"
2102 | "fmt"
2103 | "clone"
2104 | "eq"
2105 | "ne"
2106 | "cmp"
2107 | "partial_cmp"
2108 | "hash"
2109 | "from"
2110 | "into"
2111 | "try_from"
2112 | "try_into"
2113 | "as_ref"
2114 | "as_mut"
2115 | "deref"
2116 | "deref_mut"
2117 | "drop"
2118 | "next"
2119 | "into_iter"
2120 | "iter"
2121 | "len"
2122 | "is_empty"
2123 )
2124}
2125
2126fn extract_receiver_type_hint(receiver: &PureExpr) -> Option<&str> {
2135 match receiver {
2136 PureExpr::Call { func, .. } => {
2138 if let PureExpr::Path(path) = func.as_ref() {
2139 let segments: Vec<&str> = path.rsplitn(2, "::").collect();
2141 if segments.len() == 2 {
2142 return Some(segments[1].rsplit("::").next().unwrap_or(segments[1]));
2145 }
2146 }
2147 None
2148 }
2149 PureExpr::Struct { path, .. } => path.rsplit("::").next(),
2151 _ => None,
2152 }
2153}
2154
2155fn extract_self_type_from_parent_path(parent_path: &str) -> Option<&str> {
2165 if let Some(impl_start) = parent_path.rfind("::<impl ") {
2167 let impl_segment = &parent_path[impl_start + 2..]; let inner = impl_segment.strip_prefix("<impl ")?.strip_suffix('>')?;
2170
2171 let self_ty = if let Some(pos) = inner.find(" for ") {
2173 &inner[pos + 5..]
2174 } else {
2175 inner
2176 };
2177
2178 let base = self_ty.split('<').next().unwrap_or(self_ty).trim();
2180 if !base.is_empty() {
2181 return Some(base);
2182 }
2183 }
2184
2185 parent_path.rsplit("::").next()
2187}
2188
2189fn candidate_matches_type_hint(
2194 candidate_id: SymbolId,
2195 type_hint: &str,
2196 registry: &SymbolRegistry,
2197) -> bool {
2198 if let Some(path) = registry.path(candidate_id) {
2199 for segment in path.segment_refs() {
2200 if segment.is_impl() {
2201 if let Some(self_ty) = segment.impl_self_ty() {
2202 let base = self_ty.split('<').next().unwrap_or(self_ty).trim();
2204 let base_name = base.rsplit("::").next().unwrap_or(base);
2206 return base_name == type_hint;
2207 }
2208 }
2209 }
2210 let segments: Vec<&str> = path.segments().collect();
2213 if segments.len() >= 2 {
2214 let parent_name = segments[segments.len() - 2];
2215 return parent_name == type_hint;
2216 }
2217 }
2218 false
2219}
2220
2221type MethodNameIndex = HashMap<String, Vec<SymbolId>>;
2225
2226struct CallsBuildContext<'a> {
2229 symbol_ids: &'a HashMap<String, SymbolId>,
2230 use_resolver: &'a UseResolver,
2231 registry: &'a SymbolRegistry,
2232 graph: &'a mut CodeGraphV2,
2233 method_index: &'a MethodNameIndex,
2234}
2235
2236fn build_method_name_index(
2242 symbol_ids: &HashMap<String, SymbolId>,
2243 registry: &SymbolRegistry,
2244) -> MethodNameIndex {
2245 let mut index: MethodNameIndex = HashMap::new();
2246 for (path, &id) in symbol_ids {
2247 let kind = registry.kind(id);
2249 let is_callable = matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Method));
2250 if !is_callable {
2251 continue;
2252 }
2253 if let Some(method_name) = path.rsplit("::").next() {
2254 index.entry(method_name.to_string()).or_default().push(id);
2255 }
2256 }
2257 index
2258}
2259
2260#[cfg(test)]
2261mod tests {
2262 use super::*;
2263 use ryo_symbol::{TestWorkspace, WorkspaceFilePath};
2264
2265 fn build_context_from_workspace(
2267 workspace: &TestWorkspace,
2268 crate_name: &str,
2269 ) -> AnalysisContext {
2270 let files: HashMap<WorkspaceFilePath, PureFile> = workspace
2271 .files_in_crate(crate_name)
2272 .into_iter()
2273 .filter_map(|path| {
2274 let abs = path.to_absolute();
2275 let content = std::fs::read_to_string(&abs).ok()?;
2276 let file = PureFile::from_source(&content).ok()?;
2277 Some((path, file))
2278 })
2279 .collect();
2280 let workspace_root = Arc::from(workspace.workspace_root());
2281 AnalysisContext::build_from_workspace_files(
2282 files,
2283 workspace_root,
2284 AnalysisConfig::default(),
2285 )
2286 .expect("build_from_workspace_files failed in test helper")
2287 }
2288
2289 #[test]
2290 fn test_get_parent_path() {
2291 assert_eq!(
2292 get_parent_path("mylib::handlers::handle"),
2293 Some("mylib::handlers".to_string())
2294 );
2295 assert_eq!(get_parent_path("mylib::foo"), Some("mylib".to_string()));
2296 assert_eq!(get_parent_path("mylib"), None);
2297 }
2298
2299 #[test]
2300 fn test_empty_context() {
2301 let workspace = TestWorkspace::builder()
2303 .crate_with_source("test_crate", "src/lib.rs", "")
2304 .build();
2305
2306 let ctx = build_context_from_workspace(&workspace, "test_crate");
2307
2308 assert_eq!(ctx.file_count(), 1);
2310 assert!(ctx.code_graph.node_count() <= 1);
2311 }
2312
2313 #[test]
2314 fn test_context_with_files() {
2315 let workspace = TestWorkspace::builder()
2316 .crate_with_source("mylib", "src/lib.rs", "pub fn foo() {}")
2317 .build();
2318
2319 let ctx = build_context_from_workspace(&workspace, "mylib");
2320
2321 assert_eq!(ctx.file_count(), 1);
2322 let workspace_path = ctx.files.keys().next().expect("should have one file");
2324 assert!(ctx.file(workspace_path).is_some());
2325 assert!(ctx.original(workspace_path).is_some());
2326
2327 let foo_path = SymbolPath::parse("mylib::foo").unwrap();
2329 assert!(
2330 ctx.registry.lookup(&foo_path).is_some(),
2331 "foo should be registered"
2332 );
2333 }
2334
2335 #[test]
2336 fn test_fork_creates_independent_files() {
2337 let workspace = TestWorkspace::builder()
2338 .crate_with_source("mylib", "src/lib.rs", "pub fn foo() {}")
2339 .build();
2340
2341 let original = build_context_from_workspace(&workspace, "mylib");
2342 let workspace_path = original.files.keys().next().expect("file exists").clone();
2343 let mut forked = original.fork();
2344
2345 forked.files.insert(
2347 workspace_path.clone(),
2348 Arc::new(PureFile::from_source("pub fn bar() {}").unwrap()),
2349 );
2350
2351 let original_source = original.file(&workspace_path).unwrap().to_source().unwrap();
2353 let forked_source = forked.file(&workspace_path).unwrap().to_source().unwrap();
2354
2355 assert!(original_source.contains("foo"), "Original should have foo");
2356 assert!(forked_source.contains("bar"), "Forked should have bar");
2357 assert!(
2358 !original_source.contains("bar"),
2359 "Original should not have bar"
2360 );
2361 }
2362
2363 #[test]
2364 fn test_fork_shares_registry() {
2365 let workspace = TestWorkspace::builder()
2366 .crate_with_source("mylib", "src/lib.rs", "pub struct Foo {}")
2367 .build();
2368
2369 let original = build_context_from_workspace(&workspace, "mylib");
2370 let forked = original.fork();
2371
2372 assert!(std::ptr::eq(
2375 &original.registry as *const _,
2376 forked.registry as *const _
2377 ));
2378
2379 assert_eq!(original.registry.len(), forked.registry.len());
2381 }
2382
2383 #[test]
2384 fn test_fork_rebuild_creates_independent_context() {
2385 let workspace = TestWorkspace::builder()
2386 .crate_with_source("mylib", "src/lib.rs", "pub fn foo() {}")
2387 .build();
2388
2389 let original = build_context_from_workspace(&workspace, "mylib");
2390 let rebuilt = original.fork_rebuild();
2391
2392 assert!(!std::ptr::eq(
2394 &original.registry as *const _,
2395 &rebuilt.registry as *const _
2396 ));
2397
2398 assert_eq!(original.registry.len(), rebuilt.registry.len());
2400 }
2401
2402 #[test]
2403 fn test_execution_context_file_access() {
2404 let workspace = TestWorkspace::builder()
2405 .crate_with_source("mylib", "src/lib.rs", "pub fn test_fn() {}")
2406 .build();
2407
2408 let ctx = build_context_from_workspace(&workspace, "mylib");
2409 let workspace_path = ctx.files.keys().next().expect("file exists").clone();
2410 let exec_ctx = ctx.fork();
2411
2412 assert!(exec_ctx.has_file(&workspace_path));
2413 assert_eq!(exec_ctx.file_count(), 1);
2414
2415 let file = exec_ctx.file(&workspace_path).unwrap();
2416 assert!(file.to_source().unwrap().contains("test_fn"));
2417 }
2418
2419 #[test]
2424 fn test_commit_changes_add_symbol() {
2425 use crate::SymbolKind;
2426 use crate::{FileSpan, RegistryUpdate, RegistryUpdateBatch, SymbolPath};
2427
2428 let workspace = TestWorkspace::builder()
2430 .crate_with_source("testcrate", "src/lib.rs", "")
2431 .build();
2432 let mut ctx = build_context_from_workspace(&workspace, "testcrate");
2433
2434 let mut batch = RegistryUpdateBatch::new();
2436 let path = SymbolPath::parse("testcrate::NewStruct").unwrap();
2437 let dummy_span = FileSpan::new(
2438 WorkspaceFilePath::new_for_test("src/test.rs", "/project", "testcrate"),
2439 0,
2440 100,
2441 );
2442 batch.push(RegistryUpdate::Add {
2443 path: path.clone(),
2444 kind: SymbolKind::Struct,
2445 span: dummy_span,
2446 });
2447
2448 let initial_nodes = ctx.code_graph.node_count();
2449 ctx.commit_changes(&batch);
2450
2451 let id = ctx.registry.lookup(&path);
2453 assert!(id.is_some(), "Symbol should be registered");
2454
2455 assert_eq!(
2457 ctx.code_graph.node_count(),
2458 initial_nodes + 1,
2459 "Graph should have one more node"
2460 );
2461 }
2462
2463 #[test]
2464 fn test_commit_changes_remove_symbol() {
2465 use crate::{RegistryUpdate, RegistryUpdateBatch, SymbolPath};
2466
2467 let workspace = TestWorkspace::builder()
2469 .crate_with_source("mylib", "src/lib.rs", "pub struct Foo {}")
2470 .build();
2471
2472 let mut ctx = build_context_from_workspace(&workspace, "mylib");
2473
2474 let foo_path = SymbolPath::parse("mylib::Foo").unwrap();
2476 let foo_id = ctx.registry.lookup(&foo_path);
2477 assert!(foo_id.is_some(), "Foo should exist initially");
2478 let foo_id = foo_id.unwrap();
2479
2480 let initial_nodes = ctx.code_graph.node_count();
2481
2482 let mut batch = RegistryUpdateBatch::new();
2484 batch.push(RegistryUpdate::Remove { id: foo_id });
2485
2486 ctx.commit_changes(&batch);
2487
2488 let foo_path_check = SymbolPath::parse("mylib::Foo").unwrap();
2490 assert!(
2491 ctx.registry.lookup(&foo_path_check).is_none(),
2492 "Symbol should be removed from registry"
2493 );
2494
2495 assert_eq!(
2497 ctx.code_graph.node_count(),
2498 initial_nodes - 1,
2499 "Graph should have one fewer node"
2500 );
2501 }
2502
2503 #[test]
2504 fn test_commit_changes_update_kind() {
2505 use crate::SymbolKind;
2506 use crate::{RegistryUpdate, RegistryUpdateBatch, SymbolPath};
2507
2508 let workspace = TestWorkspace::builder()
2510 .crate_with_source("mylib", "src/lib.rs", "pub struct Foo {}")
2511 .build();
2512
2513 let mut ctx = build_context_from_workspace(&workspace, "mylib");
2514
2515 let foo_path = SymbolPath::parse("mylib::Foo").unwrap();
2517 let foo_id = ctx.registry.lookup(&foo_path).expect("Foo should exist");
2518
2519 assert_eq!(ctx.registry.kind(foo_id), Some(SymbolKind::Struct));
2521
2522 let initial_nodes = ctx.code_graph.node_count();
2523
2524 let mut batch = RegistryUpdateBatch::new();
2526 batch.push(RegistryUpdate::UpdateKind {
2527 id: foo_id,
2528 new_kind: SymbolKind::Enum,
2529 });
2530
2531 ctx.commit_changes(&batch);
2532
2533 assert_eq!(
2535 ctx.registry.kind(foo_id),
2536 Some(SymbolKind::Enum),
2537 "Kind should be updated to Enum"
2538 );
2539
2540 assert_eq!(
2542 ctx.code_graph.node_count(),
2543 initial_nodes,
2544 "Node count should not change"
2545 );
2546 }
2547
2548 #[test]
2549 fn test_commit_changes_batch_multiple_updates() {
2550 use crate::SymbolKind;
2551 use crate::{FileSpan, RegistryUpdate, RegistryUpdateBatch, SymbolPath};
2552
2553 let workspace = TestWorkspace::builder()
2555 .crate_with_source("testcrate", "src/lib.rs", "")
2556 .build();
2557 let mut ctx = build_context_from_workspace(&workspace, "testcrate");
2558
2559 let mut batch = RegistryUpdateBatch::new();
2561 for name in ["Alpha", "Beta", "Gamma"] {
2562 let path = SymbolPath::parse(&format!("testcrate::{}", name)).unwrap();
2563 let dummy_span = FileSpan::new(
2564 WorkspaceFilePath::new_for_test("src/test.rs", "/project", "testcrate"),
2565 0,
2566 100,
2567 );
2568 batch.push(RegistryUpdate::Add {
2569 path,
2570 kind: SymbolKind::Struct,
2571 span: dummy_span,
2572 });
2573 }
2574
2575 let initial_nodes = ctx.code_graph.node_count();
2576 ctx.commit_changes(&batch);
2577
2578 assert_eq!(
2580 ctx.code_graph.node_count(),
2581 initial_nodes + 3,
2582 "Graph should have 3 more nodes"
2583 );
2584
2585 for name in ["Alpha", "Beta", "Gamma"] {
2587 let path = SymbolPath::parse(&format!("testcrate::{}", name)).unwrap();
2588 assert!(
2589 ctx.registry.lookup(&path).is_some(),
2590 "{} should be registered",
2591 name
2592 );
2593 }
2594 }
2595
2596 #[test]
2601 fn test_implements_edge() {
2602 let workspace = TestWorkspace::builder()
2603 .crate_with_source(
2604 "mylib",
2605 "src/lib.rs",
2606 r#"
2607 pub trait MyTrait {
2608 fn do_something(&self);
2609 }
2610
2611 pub struct MyStruct {}
2612
2613 impl MyTrait for MyStruct {
2614 fn do_something(&self) {}
2615 }
2616 "#,
2617 )
2618 .build();
2619
2620 let ctx = build_context_from_workspace(&workspace, "mylib");
2621
2622 let trait_path = SymbolPath::parse("mylib::MyTrait").unwrap();
2624 let trait_id = ctx.registry.lookup(&trait_path);
2625 assert!(trait_id.is_some(), "Trait should be registered");
2626
2627 let struct_path = SymbolPath::parse("mylib::MyStruct").unwrap();
2629 let struct_id = ctx.registry.lookup(&struct_path);
2630 assert!(struct_id.is_some(), "Struct should be registered");
2631
2632 let has_implementors = ctx
2634 .code_graph
2635 .implementors_of(trait_id.unwrap())
2636 .next()
2637 .is_some();
2638 assert!(
2639 has_implementors,
2640 "MyTrait should have at least one implementor"
2641 );
2642 }
2643
2644 #[test]
2645 fn test_uses_edge_from_struct_field() {
2646 let workspace = TestWorkspace::builder()
2647 .crate_with_source(
2648 "mylib",
2649 "src/lib.rs",
2650 r#"
2651 pub struct Inner {}
2652
2653 pub struct Outer {
2654 pub inner: Inner,
2655 }
2656 "#,
2657 )
2658 .build();
2659
2660 let ctx = build_context_from_workspace(&workspace, "mylib");
2661
2662 let outer_path = SymbolPath::parse("mylib::Outer").unwrap();
2663 let inner_path = SymbolPath::parse("mylib::Inner").unwrap();
2664
2665 let outer_id = ctx.registry.lookup(&outer_path);
2666 let inner_id = ctx.registry.lookup(&inner_path);
2667
2668 assert!(outer_id.is_some(), "Outer should be registered");
2669 assert!(inner_id.is_some(), "Inner should be registered");
2670
2671 let outer = outer_id.unwrap();
2673 let is_user = ctx
2674 .typeflow_graph
2675 .type_users(inner_id.unwrap())
2676 .any(|id| id == outer);
2677 assert!(is_user, "Outer should use Inner");
2678 }
2679
2680 #[test]
2681 fn test_uses_edge_from_fn_params() {
2682 let workspace = TestWorkspace::builder()
2683 .crate_with_source(
2684 "mylib",
2685 "src/lib.rs",
2686 r#"
2687 pub struct Config {}
2688
2689 pub fn process(config: Config) {}
2690 "#,
2691 )
2692 .build();
2693
2694 let ctx = build_context_from_workspace(&workspace, "mylib");
2695
2696 let fn_path = SymbolPath::parse("mylib::process").unwrap();
2697 let config_path = SymbolPath::parse("mylib::Config").unwrap();
2698
2699 let fn_id = ctx.registry.lookup(&fn_path);
2700 let config_id = ctx.registry.lookup(&config_path);
2701
2702 assert!(fn_id.is_some(), "Function should be registered");
2703 assert!(config_id.is_some(), "Config should be registered");
2704
2705 let fn_sym = fn_id.unwrap();
2707 let config_sym = config_id.unwrap();
2708
2709 let is_user = ctx
2710 .typeflow_graph
2711 .type_users(config_sym)
2712 .any(|id| id == fn_sym);
2713 assert!(is_user, "process should use Config");
2714 }
2715
2716 #[test]
2717 fn test_calls_edge() {
2718 let workspace = TestWorkspace::builder()
2719 .crate_with_source(
2720 "mylib",
2721 "src/lib.rs",
2722 r#"
2723 pub fn helper() {}
2724
2725 pub fn main_fn() {
2726 helper();
2727 }
2728 "#,
2729 )
2730 .build();
2731
2732 let ctx = build_context_from_workspace(&workspace, "mylib");
2733
2734 let main_path = SymbolPath::parse("mylib::main_fn").unwrap();
2735 let helper_path = SymbolPath::parse("mylib::helper").unwrap();
2736
2737 let main_id = ctx.registry.lookup(&main_path);
2738 let helper_id = ctx.registry.lookup(&helper_path);
2739
2740 assert!(main_id.is_some(), "main_fn should be registered");
2741 assert!(helper_id.is_some(), "helper should be registered");
2742
2743 let main = main_id.unwrap();
2745 let is_caller = ctx
2746 .code_graph
2747 .callers_of(helper_id.unwrap())
2748 .any(|id| id == main);
2749 assert!(is_caller, "main_fn should call helper");
2750 }
2751
2752 #[test]
2753 fn test_fork_clone_creates_independent_context() {
2754 let workspace = TestWorkspace::builder()
2755 .crate_with_source("mylib", "src/lib.rs", "pub fn foo() {}")
2756 .build();
2757
2758 let original = build_context_from_workspace(&workspace, "mylib");
2759 let cloned = original.fork_clone();
2760
2761 assert!(!std::ptr::eq(
2763 &original.registry as *const _,
2764 &cloned.registry as *const _
2765 ));
2766
2767 assert_eq!(original.registry.len(), cloned.registry.len());
2769 assert_eq!(
2770 original.code_graph.node_count(),
2771 cloned.code_graph.node_count()
2772 );
2773 }
2774
2775 #[test]
2776 fn test_fork_clone_is_faster_than_rebuild() {
2777 use std::time::Instant;
2778
2779 let workspace = TestWorkspace::builder()
2781 .crate_with_source("mylib", "src/lib.rs", "pub mod a; pub mod b;")
2782 .crate_with_source("mylib", "src/a.rs", "pub struct A { x: i32 }")
2783 .crate_with_source("mylib", "src/b.rs", "pub struct B { y: String }")
2784 .build();
2785
2786 let ctx = build_context_from_workspace(&workspace, "mylib");
2787
2788 let _ = ctx.fork_clone();
2790 let _ = ctx.fork_rebuild();
2791
2792 let t0 = Instant::now();
2794 for _ in 0..10 {
2795 let _ = ctx.fork_clone();
2796 }
2797 let clone_time = t0.elapsed();
2798
2799 let t1 = Instant::now();
2801 for _ in 0..10 {
2802 let _ = ctx.fork_rebuild();
2803 }
2804 let rebuild_time = t1.elapsed();
2805
2806 eprintln!(
2807 "fork_clone: {:?} avg, fork_rebuild: {:?} avg, speedup: {:.1}x",
2808 clone_time / 10,
2809 rebuild_time / 10,
2810 rebuild_time.as_nanos() as f64 / clone_time.as_nanos() as f64
2811 );
2812
2813 assert!(
2815 clone_time < rebuild_time,
2816 "fork_clone ({:?}) should be faster than fork_rebuild ({:?})",
2817 clone_time,
2818 rebuild_time
2819 );
2820 }
2821
2822 #[test]
2827 fn test_uuid_persistence_save_and_load() {
2828 use ryo_symbol::SymbolPath;
2829
2830 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
2831 let workspace_root: Arc<Path> = Arc::from(temp_dir.path());
2832
2833 std::fs::create_dir_all(temp_dir.path().join(".ryo")).unwrap();
2835
2836 let wfp = WorkspaceFilePath::new_for_test("src/lib.rs", temp_dir.path(), "test_crate");
2838
2839 let source = r#"pub struct TestStruct { pub field: i32 }"#;
2840 let file = PureFile::from_source(source).expect("Failed to parse");
2841
2842 let mut files = HashMap::new();
2843 files.insert(wfp.clone(), file);
2844
2845 let ctx1 = AnalysisContext::build_from_workspace_files(
2847 files.clone(),
2848 workspace_root.clone(),
2849 AnalysisConfig::default(),
2850 )
2851 .unwrap();
2852
2853 let path = SymbolPath::parse("test_crate::TestStruct").unwrap();
2855 let id1 = ctx1
2856 .registry
2857 .lookup(&path)
2858 .expect("TestStruct should be registered");
2859 let uuid1 = ctx1.registry.uuid(id1).expect("Should have UUID");
2860
2861 ctx1.save_uuid_mappings().expect("Failed to save");
2863
2864 let uuid_file = temp_dir.path().join(".ryo/uuid-mapping.json");
2866 assert!(uuid_file.exists(), "UUID file should exist");
2867
2868 let mappings =
2870 AnalysisContext::load_uuid_mappings(temp_dir.path()).expect("Should load mappings");
2871
2872 let config = AnalysisConfig::default().with_uuid_mappings(mappings);
2874 let ctx2 =
2875 AnalysisContext::build_from_workspace_files(files, workspace_root, config).unwrap();
2876
2877 let id2 = ctx2
2879 .registry
2880 .lookup(&path)
2881 .expect("TestStruct should exist");
2882 let uuid2 = ctx2.registry.uuid(id2).expect("Should have UUID");
2883
2884 assert_eq!(uuid1, uuid2, "UUID should be preserved across rebuilds");
2886 }
2887
2888 #[test]
2889 fn test_uuid_persistence_new_symbol_gets_new_uuid() {
2890 use ryo_symbol::SymbolPath;
2891
2892 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
2893 let workspace_root: Arc<Path> = Arc::from(temp_dir.path());
2894 std::fs::create_dir_all(temp_dir.path().join(".ryo")).unwrap();
2895
2896 let wfp = WorkspaceFilePath::new_for_test("src/lib.rs", temp_dir.path(), "test_crate");
2897
2898 let source1 = "pub struct First;";
2900 let file1 = PureFile::from_source(source1).unwrap();
2901 let mut files1 = HashMap::new();
2902 files1.insert(wfp.clone(), file1);
2903
2904 let ctx1 = AnalysisContext::build_from_workspace_files(
2905 files1,
2906 workspace_root.clone(),
2907 AnalysisConfig::default(),
2908 )
2909 .unwrap();
2910
2911 let first_path = SymbolPath::parse("test_crate::First").unwrap();
2912 let first_id = ctx1.registry.lookup(&first_path).unwrap();
2913 let first_uuid = ctx1.registry.uuid(first_id).unwrap();
2914 ctx1.save_uuid_mappings().unwrap();
2915
2916 let source2 = "pub struct First;\npub struct Second;";
2918 let file2 = PureFile::from_source(source2).unwrap();
2919 let mut files2 = HashMap::new();
2920 files2.insert(wfp, file2);
2921
2922 let mappings = AnalysisContext::load_uuid_mappings(temp_dir.path()).unwrap();
2923 let config = AnalysisConfig::default().with_uuid_mappings(mappings);
2924 let ctx2 =
2925 AnalysisContext::build_from_workspace_files(files2, workspace_root, config).unwrap();
2926
2927 let first_id2 = ctx2.registry.lookup(&first_path).unwrap();
2929 let first_uuid2 = ctx2.registry.uuid(first_id2).unwrap();
2930 assert_eq!(first_uuid, first_uuid2, "First UUID preserved");
2931
2932 let second_path = SymbolPath::parse("test_crate::Second").unwrap();
2934 let second_id = ctx2.registry.lookup(&second_path).unwrap();
2935 let second_uuid = ctx2.registry.uuid(second_id).unwrap();
2936 assert_ne!(first_uuid, second_uuid, "Second has different UUID");
2937 }
2938
2939 #[test]
2944 fn test_impl_methods_registered_directly_on_struct() {
2945 let workspace = TestWorkspace::builder()
2946 .crate_with_source(
2947 "mylib",
2948 "src/lib.rs",
2949 r#"
2950 pub struct TodoList {
2951 items: Vec<String>,
2952 }
2953
2954 impl TodoList {
2955 pub fn new() -> Self {
2956 Self { items: vec![] }
2957 }
2958
2959 pub fn add(&mut self, item: String) {
2960 self.items.push(item);
2961 }
2962 }
2963 "#,
2964 )
2965 .build();
2966
2967 let ctx = build_context_from_workspace(&workspace, "mylib");
2968
2969 let new_path = SymbolPath::parse("mylib::TodoList::new").unwrap();
2971 let add_path = SymbolPath::parse("mylib::TodoList::add").unwrap();
2972
2973 assert!(
2974 ctx.registry.lookup(&new_path).is_some(),
2975 "new method should be registered as TodoList::new"
2976 );
2977 assert!(
2978 ctx.registry.lookup(&add_path).is_some(),
2979 "add method should be registered as TodoList::add"
2980 );
2981
2982 let impl_blocks: Vec<_> = ctx
2984 .registry
2985 .iter()
2986 .filter(|(_, path)| path.segment_refs().iter().any(|s| s.is_impl()))
2987 .collect();
2988
2989 assert_eq!(
2990 impl_blocks.len(),
2991 1,
2992 "impl block should be registered as <impl TodoList>"
2993 );
2994 assert!(
2995 impl_blocks[0].1.to_string() == "mylib::<impl TodoList>",
2996 "impl block path should be mylib::<impl TodoList>, found: {}",
2997 impl_blocks[0].1
2998 );
2999 }
3000
3001 #[test]
3002 fn test_multiple_impl_blocks_methods_merged_on_struct() {
3003 let workspace = TestWorkspace::builder()
3004 .crate_with_source(
3005 "mylib",
3006 "src/lib.rs",
3007 r#"
3008 pub struct TodoList;
3009
3010 impl TodoList {
3011 pub fn new() -> Self { Self }
3012 }
3013
3014 impl TodoList {
3015 pub fn add(&mut self, item: String) {}
3016 }
3017 "#,
3018 )
3019 .build();
3020
3021 let ctx = build_context_from_workspace(&workspace, "mylib");
3022
3023 let new_path = SymbolPath::parse("mylib::TodoList::new").unwrap();
3025 let add_path = SymbolPath::parse("mylib::TodoList::add").unwrap();
3026
3027 assert!(
3028 ctx.registry.lookup(&new_path).is_some(),
3029 "new from first impl should be registered"
3030 );
3031 assert!(
3032 ctx.registry.lookup(&add_path).is_some(),
3033 "add from second impl should be registered"
3034 );
3035
3036 let impl_blocks: Vec<_> = ctx
3038 .registry
3039 .iter()
3040 .filter(|(_, path)| path.segment_refs().iter().any(|s| s.is_impl()))
3041 .collect();
3042
3043 assert_eq!(
3044 impl_blocks.len(),
3045 1,
3046 "merged impl block should be registered"
3047 );
3048 assert!(
3049 impl_blocks[0].1.to_string() == "mylib::<impl TodoList>",
3050 "impl block path should be mylib::<impl TodoList>"
3051 );
3052 }
3053
3054 #[test]
3061 fn test_calls_edge_associated_fn_cross_module() {
3062 let workspace = TestWorkspace::builder()
3063 .crate_with_source(
3064 "mylib",
3065 "src/lib.rs",
3066 r#"
3067 pub mod types;
3068 pub mod handler;
3069 "#,
3070 )
3071 .crate_with_source(
3072 "mylib",
3073 "src/types.rs",
3074 r#"
3075 pub struct Router {}
3076 impl Router {
3077 pub fn new() -> Self { Router {} }
3078 }
3079 "#,
3080 )
3081 .crate_with_source(
3082 "mylib",
3083 "src/handler.rs",
3084 r#"
3085 use crate::types::Router;
3086 pub fn setup() {
3087 let _r = Router::new();
3088 }
3089 "#,
3090 )
3091 .build();
3092
3093 let ctx = build_context_from_workspace(&workspace, "mylib");
3094
3095 let setup_path = SymbolPath::parse("mylib::handler::setup").unwrap();
3096 let new_path = SymbolPath::parse("mylib::types::Router::new").unwrap();
3097
3098 let setup_id = ctx
3099 .registry
3100 .lookup(&setup_path)
3101 .expect("setup should be registered");
3102 let new_id = ctx
3103 .registry
3104 .lookup(&new_path)
3105 .expect("Router::new should be registered");
3106
3107 let callees: Vec<_> = ctx.code_graph.callees_of(setup_id).collect();
3108 assert!(
3109 callees.contains(&new_id),
3110 "setup should call Router::new, but callees = {:?}",
3111 callees
3112 );
3113 }
3114
3115 #[test]
3117 fn test_calls_edge_free_fn_cross_module_via_use() {
3118 let workspace = TestWorkspace::builder()
3119 .crate_with_source(
3120 "mylib",
3121 "src/lib.rs",
3122 r#"
3123 pub mod utils;
3124 pub mod handler;
3125 "#,
3126 )
3127 .crate_with_source(
3128 "mylib",
3129 "src/utils.rs",
3130 r#"
3131 pub fn helper() {}
3132 "#,
3133 )
3134 .crate_with_source(
3135 "mylib",
3136 "src/handler.rs",
3137 r#"
3138 use crate::utils::helper;
3139 pub fn process() {
3140 helper();
3141 }
3142 "#,
3143 )
3144 .build();
3145
3146 let ctx = build_context_from_workspace(&workspace, "mylib");
3147
3148 let process_path = SymbolPath::parse("mylib::handler::process").unwrap();
3149 let helper_path = SymbolPath::parse("mylib::utils::helper").unwrap();
3150
3151 let process_id = ctx
3152 .registry
3153 .lookup(&process_path)
3154 .expect("process should be registered");
3155 let helper_id = ctx
3156 .registry
3157 .lookup(&helper_path)
3158 .expect("helper should be registered");
3159
3160 let is_callee = ctx
3161 .code_graph
3162 .callees_of(process_id)
3163 .any(|id| id == helper_id);
3164 assert!(is_callee, "process should call helper via use import");
3165 }
3166
3167 #[test]
3170 fn test_calls_edge_associated_fn_new_not_filtered() {
3171 let workspace = TestWorkspace::builder()
3172 .crate_with_source(
3173 "mylib",
3174 "src/lib.rs",
3175 r#"
3176 pub struct Builder {}
3177 impl Builder {
3178 pub fn new() -> Self { Builder {} }
3179 pub fn build(self) -> String { String::new() }
3180 }
3181
3182 pub fn create() -> String {
3183 let b = Builder::new();
3184 b.build()
3185 }
3186 "#,
3187 )
3188 .build();
3189
3190 let ctx = build_context_from_workspace(&workspace, "mylib");
3191
3192 let create_path = SymbolPath::parse("mylib::create").unwrap();
3193 let new_path = SymbolPath::parse("mylib::Builder::new").unwrap();
3194
3195 let create_id = ctx
3196 .registry
3197 .lookup(&create_path)
3198 .expect("create should be registered");
3199 let new_id = ctx
3200 .registry
3201 .lookup(&new_path)
3202 .expect("Builder::new should be registered");
3203
3204 let callees: Vec<_> = ctx.code_graph.callees_of(create_id).collect();
3205 assert!(
3206 callees.contains(&new_id),
3207 "create should call Builder::new (associated fn, not filtered by is_common_trait_method), callees = {:?}",
3208 callees
3209 );
3210 }
3211
3212 #[test]
3214 fn test_calls_edge_qualified_path_call() {
3215 let workspace = TestWorkspace::builder()
3216 .crate_with_source(
3217 "mylib",
3218 "src/lib.rs",
3219 r#"
3220 pub mod utils {
3221 pub fn do_work() {}
3222 }
3223
3224 pub fn caller() {
3225 utils::do_work();
3226 }
3227 "#,
3228 )
3229 .build();
3230
3231 let ctx = build_context_from_workspace(&workspace, "mylib");
3232
3233 let caller_path = SymbolPath::parse("mylib::caller").unwrap();
3234 let do_work_path = SymbolPath::parse("mylib::utils::do_work").unwrap();
3235
3236 let caller_id = ctx
3237 .registry
3238 .lookup(&caller_path)
3239 .expect("caller should be registered");
3240 let do_work_id = ctx
3241 .registry
3242 .lookup(&do_work_path)
3243 .expect("do_work should be registered");
3244
3245 let is_callee = ctx
3246 .code_graph
3247 .callees_of(caller_id)
3248 .any(|id| id == do_work_id);
3249 assert!(
3250 is_callee,
3251 "caller should call utils::do_work via qualified path"
3252 );
3253 }
3254
3255 #[test]
3258 fn test_calls_edge_method_common_name_single_candidate() {
3259 let workspace = TestWorkspace::builder()
3260 .crate_with_source(
3261 "mylib",
3262 "src/lib.rs",
3263 r#"
3264 pub struct Data {}
3265 impl Data {
3266 pub fn clone(&self) -> Self { Data {} }
3267 }
3268
3269 pub fn process(d: Data) -> Data {
3270 d.clone()
3271 }
3272 "#,
3273 )
3274 .build();
3275
3276 let ctx = build_context_from_workspace(&workspace, "mylib");
3277
3278 let process_path = SymbolPath::parse("mylib::process").unwrap();
3279 let clone_path = SymbolPath::parse("mylib::Data::clone").unwrap();
3280
3281 let process_id = ctx
3282 .registry
3283 .lookup(&process_path)
3284 .expect("process should be registered");
3285 let clone_id = ctx
3286 .registry
3287 .lookup(&clone_path)
3288 .expect("Data::clone should be registered");
3289
3290 let callees: Vec<_> = ctx.code_graph.callees_of(process_id).collect();
3291 assert!(
3292 callees.contains(&clone_id),
3293 "process should call Data::clone even though 'clone' is in common trait list (single candidate), callees = {:?}",
3294 callees
3295 );
3296 }
3297
3298 #[test]
3300 fn test_calls_edge_method_many_candidates_not_blocked() {
3301 let workspace = TestWorkspace::builder()
3303 .crate_with_source(
3304 "mylib",
3305 "src/lib.rs",
3306 r#"
3307 pub struct A {} impl A { pub fn handle(&self) {} }
3308 pub struct B {} impl B { pub fn handle(&self) {} }
3309 pub struct C {} impl C { pub fn handle(&self) {} }
3310 pub struct D {} impl D { pub fn handle(&self) {} }
3311 pub struct E {} impl E { pub fn handle(&self) {} }
3312 pub struct F {} impl F { pub fn handle(&self) {} }
3313 pub struct G {} impl G { pub fn handle(&self) {} }
3314 pub struct H {} impl H { pub fn handle(&self) {} }
3315 pub struct I {} impl I { pub fn handle(&self) {} }
3316 pub struct J {} impl J { pub fn handle(&self) {} }
3317 pub struct K {} impl K { pub fn handle(&self) {} }
3318 pub struct L {} impl L { pub fn handle(&self) {} }
3319
3320 pub fn caller(a: A) {
3321 a.handle();
3322 }
3323 "#,
3324 )
3325 .build();
3326
3327 let ctx = build_context_from_workspace(&workspace, "mylib");
3328
3329 let caller_path = SymbolPath::parse("mylib::caller").unwrap();
3330 let caller_id = ctx
3331 .registry
3332 .lookup(&caller_path)
3333 .expect("caller should be registered");
3334
3335 let callees: Vec<_> = ctx.code_graph.callees_of(caller_id).collect();
3336 assert!(
3339 !callees.is_empty(),
3340 "caller should have callees for 'handle' method (12 candidates should not be blocked), callees = {:?}",
3341 callees
3342 );
3343 }
3344
3345 #[test]
3348 fn test_calls_edge_trait_impl_method_has_callees() {
3349 let workspace = TestWorkspace::builder()
3350 .crate_with_source(
3351 "mylib",
3352 "src/lib.rs",
3353 r#"
3354 pub fn helper() -> i32 { 42 }
3355
3356 pub trait MyTrait {
3357 fn do_work(&self) -> i32;
3358 }
3359
3360 pub struct Foo;
3361 impl MyTrait for Foo {
3362 fn do_work(&self) -> i32 {
3363 helper()
3364 }
3365 }
3366 "#,
3367 )
3368 .build();
3369
3370 let ctx = build_context_from_workspace(&workspace, "mylib");
3371
3372 let helper_path = SymbolPath::parse("mylib::helper").unwrap();
3374 let helper_id = ctx
3375 .registry
3376 .lookup(&helper_path)
3377 .expect("helper should be registered");
3378
3379 let method_path = SymbolPath::parse("mylib::<impl MyTrait for Foo>::do_work").unwrap();
3380 let method_id = ctx
3381 .registry
3382 .lookup(&method_path)
3383 .expect("trait impl method should be registered");
3384
3385 let callees: Vec<_> = ctx.code_graph.callees_of(method_id).collect();
3386 assert!(
3387 callees.contains(&helper_id),
3388 "Trait impl method do_work should call helper(), but callees = {:?}",
3389 callees
3390 );
3391 }
3392
3393 #[test]
3395 fn test_calls_edge_trait_impl_method_call_inside_body() {
3396 let workspace = TestWorkspace::builder()
3397 .crate_with_source(
3398 "mylib",
3399 "src/lib.rs",
3400 r#"
3401 pub struct Data {
3402 pub value: i32,
3403 }
3404 impl Data {
3405 pub fn process(&self) -> i32 { self.value }
3406 }
3407
3408 pub trait Transform {
3409 fn transform(&self) -> i32;
3410 }
3411
3412 impl Transform for Data {
3413 fn transform(&self) -> i32 {
3414 self.process()
3415 }
3416 }
3417 "#,
3418 )
3419 .build();
3420
3421 let ctx = build_context_from_workspace(&workspace, "mylib");
3422
3423 let transform_method_path =
3424 SymbolPath::parse("mylib::<impl Transform for Data>::transform").unwrap();
3425 let transform_id = ctx
3426 .registry
3427 .lookup(&transform_method_path)
3428 .expect("transform should be registered");
3429
3430 let process_path = SymbolPath::parse("mylib::Data::process").unwrap();
3431 let process_id = ctx
3432 .registry
3433 .lookup(&process_path)
3434 .expect("Data::process should be registered");
3435
3436 let callees: Vec<_> = ctx.code_graph.callees_of(transform_id).collect();
3437 assert!(
3438 callees.contains(&process_id),
3439 "Trait impl transform() should call self.process(), but callees = {:?}",
3440 callees
3441 );
3442 }
3443
3444 #[test]
3447 fn test_calls_edge_method_over_32_candidates_not_dropped() {
3448 let mut source = String::new();
3450 for i in 0..35 {
3451 source.push_str(&format!(
3452 "pub struct T{i} {{}}\nimpl T{i} {{ pub fn render(&self) -> i32 {{ {i} }} }}\n"
3453 ));
3454 }
3455 source.push_str(
3456 r#"
3457 pub fn caller(t: T0) -> i32 {
3458 t.render()
3459 }
3460 "#,
3461 );
3462
3463 let workspace = TestWorkspace::builder()
3464 .crate_with_source("mylib", "src/lib.rs", &source)
3465 .build();
3466
3467 let ctx = build_context_from_workspace(&workspace, "mylib");
3468
3469 let caller_path = SymbolPath::parse("mylib::caller").unwrap();
3470 let caller_id = ctx
3471 .registry
3472 .lookup(&caller_path)
3473 .expect("caller should be registered");
3474
3475 let callees: Vec<_> = ctx.code_graph.callees_of(caller_id).collect();
3476 assert!(
3477 !callees.is_empty(),
3478 "caller should have callees for 'render' even with 35 candidates (>32 limit), \
3479 but callees is empty — candidate limit silently drops all edges"
3480 );
3481 }
3482
3483 #[test]
3484 fn test_method_index_excludes_non_callable_symbols() {
3485 let source = r#"
3490 pub mod response {
3491 pub mod process {
3492 pub fn run() -> i32 { 42 }
3493 }
3494 }
3495 pub struct Engine;
3496 impl Engine {
3497 pub fn process(&self) -> i32 { 1 }
3498 }
3499 pub fn caller(e: Engine) -> i32 {
3500 e.process()
3501 }
3502 "#;
3503
3504 let workspace = TestWorkspace::builder()
3505 .crate_with_source("mylib", "src/lib.rs", source)
3506 .build();
3507
3508 let ctx = build_context_from_workspace(&workspace, "mylib");
3509
3510 let caller_path = SymbolPath::parse("mylib::caller").unwrap();
3511 let caller_id = ctx
3512 .registry
3513 .lookup(&caller_path)
3514 .expect("caller should be registered");
3515
3516 let callees: Vec<_> = ctx.code_graph.callees_of(caller_id).collect();
3517
3518 let mod_path = SymbolPath::parse("mylib::response::process").unwrap();
3520 let mod_id = ctx
3521 .registry
3522 .lookup(&mod_path)
3523 .expect("nested module mylib::response::process should be registered");
3524
3525 let has_mod_edge = callees.contains(&mod_id);
3526 assert!(
3527 !has_mod_edge,
3528 "method_index should not include module 'response::process' as a callee of caller(); \
3529 only Function/Method symbols should be in the method_index"
3530 );
3531
3532 assert!(
3534 !callees.is_empty(),
3535 "caller should have at least one callee (Engine::process)"
3536 );
3537 }
3538
3539 #[test]
3540 fn test_method_call_receiver_type_hint_filters_candidates() {
3541 let source = r#"
3545 pub trait Render {
3546 fn render(&self) -> String;
3547 }
3548 pub struct Html;
3549 impl Render for Html {
3550 fn render(&self) -> String { String::new() }
3551 }
3552 pub struct Json;
3553 impl Json {
3554 pub fn new() -> Self { Json }
3555 }
3556 impl Render for Json {
3557 fn render(&self) -> String { String::new() }
3558 }
3559 pub struct Xml;
3560 impl Render for Xml {
3561 fn render(&self) -> String { String::new() }
3562 }
3563 pub fn caller() -> String {
3564 Json::new().render()
3565 }
3566 "#;
3567
3568 let workspace = TestWorkspace::builder()
3569 .crate_with_source("mylib", "src/lib.rs", source)
3570 .build();
3571
3572 let ctx = build_context_from_workspace(&workspace, "mylib");
3573
3574 let caller_path = SymbolPath::parse("mylib::caller").unwrap();
3575 let caller_id = ctx
3576 .registry
3577 .lookup(&caller_path)
3578 .expect("caller should be registered");
3579
3580 let callees: Vec<_> = ctx.code_graph.callees_of(caller_id).collect();
3581
3582 let new_path = SymbolPath::parse("mylib::Json::new").unwrap();
3584 let new_id = ctx.registry.lookup(&new_path);
3585 if let Some(new_id) = new_id {
3586 assert!(callees.contains(&new_id), "Json::new should be a callee");
3587 }
3588
3589 let json_render = ctx
3593 .registry
3594 .lookup(&SymbolPath::parse("mylib::<impl Render for Json>::render").unwrap());
3595 let html_render = ctx
3596 .registry
3597 .lookup(&SymbolPath::parse("mylib::<impl Render for Html>::render").unwrap());
3598 let xml_render = ctx
3599 .registry
3600 .lookup(&SymbolPath::parse("mylib::<impl Render for Xml>::render").unwrap());
3601
3602 if let Some(json_id) = json_render {
3603 assert!(
3604 callees.contains(&json_id),
3605 "Json's render should be a callee (receiver type hint: Json from Json::new())"
3606 );
3607 }
3608
3609 if let Some(html_id) = html_render {
3611 assert!(
3612 !callees.contains(&html_id),
3613 "Html's render should NOT be a callee when receiver is Json::new(); \
3614 receiver type hint should filter it out"
3615 );
3616 }
3617
3618 if let Some(xml_id) = xml_render {
3619 assert!(
3620 !callees.contains(&xml_id),
3621 "Xml's render should NOT be a callee when receiver is Json::new(); \
3622 receiver type hint should filter it out"
3623 );
3624 }
3625 }
3626
3627 #[test]
3628 fn test_associated_fn_call_in_trait_impl_resolved_via_imports() {
3629 let source = r#"
3636 pub mod types {
3637 pub struct Body;
3638 impl Body {
3639 pub fn create() -> Self { Body }
3640 }
3641 }
3642 use crate::types::Body;
3643 pub trait Render {
3644 fn render(&self) -> Body;
3645 }
3646 pub struct Page;
3647 impl Render for Page {
3648 fn render(&self) -> Body {
3649 Body::create()
3650 }
3651 }
3652 "#;
3653
3654 let workspace = TestWorkspace::builder()
3655 .crate_with_source("mylib", "src/lib.rs", source)
3656 .build();
3657
3658 let ctx = build_context_from_workspace(&workspace, "mylib");
3659
3660 let render_path = SymbolPath::parse("mylib::<impl Render for Page>::render").unwrap();
3662 let render_id = ctx
3663 .registry
3664 .lookup(&render_path)
3665 .expect("Page::render should be registered");
3666
3667 let callees: Vec<_> = ctx.code_graph.callees_of(render_id).collect();
3668
3669 let create_path = SymbolPath::parse("mylib::types::Body::create").unwrap();
3671 let create_id = ctx
3672 .registry
3673 .lookup(&create_path)
3674 .expect("Body::create should be registered");
3675
3676 assert!(
3677 callees.contains(&create_id),
3678 "Body::create() should be a callee of Page::render(); \
3679 import resolution in trait impl methods must strip impl segments \
3680 from parent_path before querying UseResolver"
3681 );
3682 }
3683
3684 #[test]
3685 fn test_generic_impl_methods_registered_and_edges_built() {
3686 let workspace = TestWorkspace::builder()
3691 .crate_with_source(
3692 "mylib",
3693 "src/lib.rs",
3694 r#"
3695 pub struct Inner;
3696 impl Inner {
3697 pub fn create() -> Self { Inner }
3698 }
3699
3700 pub struct Router<S> {
3701 _marker: std::marker::PhantomData<S>,
3702 }
3703
3704 impl<S> Router<S> {
3705 pub fn new() -> Self {
3706 let _inner = Inner::create();
3707 Router { _marker: std::marker::PhantomData }
3708 }
3709
3710 pub fn route(self, path: &str) -> Self {
3711 self
3712 }
3713 }
3714 "#,
3715 )
3716 .build();
3717
3718 let ctx = build_context_from_workspace(&workspace, "mylib");
3719
3720 let new_path = SymbolPath::parse("mylib::Router::new").unwrap();
3722 let route_path = SymbolPath::parse("mylib::Router::route").unwrap();
3723
3724 let new_id = ctx
3725 .registry
3726 .lookup(&new_path)
3727 .expect("Router::new should be registered despite generic impl<S> Router<S>");
3728 assert!(
3729 ctx.registry.lookup(&route_path).is_some(),
3730 "Router::route should be registered despite generic impl<S> Router<S>"
3731 );
3732
3733 let impl_path_str = "mylib::<impl Router < S >>";
3735 let impl_path = SymbolPath::parse(impl_path_str).unwrap();
3736 assert!(
3737 ctx.registry.lookup(&impl_path).is_some(),
3738 "impl block <impl Router < S >> should be registered"
3739 );
3740
3741 let callees: Vec<_> = ctx.code_graph.callees_of(new_id).collect();
3743 let create_path = SymbolPath::parse("mylib::Inner::create").unwrap();
3744 let create_id = ctx
3745 .registry
3746 .lookup(&create_path)
3747 .expect("Inner::create should be registered");
3748 assert!(
3749 callees.contains(&create_id),
3750 "Router::new must have Inner::create() as callee; \
3751 build_edges_from_impl must strip generics from self_ty"
3752 );
3753 }
3754
3755 #[test]
3756 fn test_external_trait_impl_callees_built() {
3757 let workspace = TestWorkspace::builder()
3760 .crate_with_source(
3761 "mylib",
3762 "src/lib.rs",
3763 r#"
3764 pub trait MyWrite {
3765 fn write(&mut self, buf: &[u8]) -> usize;
3766 }
3767
3768 pub fn helper() -> usize { 42 }
3769
3770 pub struct Writer;
3771
3772 impl MyWrite for Writer {
3773 fn write(&mut self, buf: &[u8]) -> usize {
3774 helper()
3775 }
3776 }
3777 "#,
3778 )
3779 .build();
3780
3781 let ctx = build_context_from_workspace(&workspace, "mylib");
3782
3783 let impl_path_str = "mylib::<impl MyWrite for Writer>";
3785 let impl_path = SymbolPath::parse(impl_path_str).unwrap();
3786 assert!(
3787 ctx.registry.lookup(&impl_path).is_some(),
3788 "impl block should be registered: {}",
3789 impl_path_str,
3790 );
3791
3792 let method_path = SymbolPath::parse(&format!("{}::write", impl_path_str)).unwrap();
3793 let method_id = ctx
3794 .registry
3795 .lookup(&method_path)
3796 .expect("Writer::write should be registered under trait impl path");
3797
3798 let callees: Vec<_> = ctx.code_graph.callees_of(method_id).collect();
3800 let helper_path = SymbolPath::parse("mylib::helper").unwrap();
3801 let helper_id = ctx
3802 .registry
3803 .lookup(&helper_path)
3804 .expect("helper should be registered");
3805
3806 assert!(
3807 callees.contains(&helper_id),
3808 "trait impl method Writer::write must have helper() as callee; \
3809 callees = {:?}",
3810 callees
3811 );
3812 }
3813
3814 #[test]
3815 fn test_external_trait_impl_with_generics_callees_built() {
3816 let workspace = TestWorkspace::builder()
3818 .crate_with_source(
3819 "mylib",
3820 "src/lib.rs",
3821 r#"
3822 pub trait MyWrite {
3823 fn write(&mut self, buf: &[u8]) -> usize;
3824 }
3825
3826 pub fn process_buf(buf: &[u8]) -> usize { buf.len() }
3827
3828 pub struct Writer<'a> {
3829 _data: &'a [u8],
3830 }
3831
3832 impl<'a> MyWrite for Writer<'a> {
3833 fn write(&mut self, buf: &[u8]) -> usize {
3834 process_buf(buf)
3835 }
3836 }
3837 "#,
3838 )
3839 .build();
3840
3841 let ctx = build_context_from_workspace(&workspace, "mylib");
3842
3843 let impl_path_str = "mylib::<impl MyWrite for Writer < 'a >>";
3845 let impl_path = SymbolPath::parse(impl_path_str).unwrap();
3846 let impl_registered = ctx.registry.lookup(&impl_path).is_some();
3847
3848 let method_path = SymbolPath::parse(&format!("{}::write", impl_path_str)).unwrap();
3850 let method_id = ctx.registry.lookup(&method_path);
3851
3852 assert!(
3853 impl_registered,
3854 "impl block <impl MyWrite for Writer < 'a >> should be registered"
3855 );
3856 assert!(
3857 method_id.is_some(),
3858 "Writer::write should be registered under trait impl path"
3859 );
3860
3861 if let Some(mid) = method_id {
3862 let callees: Vec<_> = ctx.code_graph.callees_of(mid).collect();
3863 let process_path = SymbolPath::parse("mylib::process_buf").unwrap();
3864 let process_id = ctx
3865 .registry
3866 .lookup(&process_path)
3867 .expect("process_buf should be registered");
3868
3869 assert!(
3870 callees.contains(&process_id),
3871 "trait impl method with generics must have process_buf() as callee; \
3872 callees = {:?}",
3873 callees
3874 );
3875 }
3876 }
3877
3878 #[test]
3880 fn test_struct_trait_implements_chain_via_impl() {
3881 let workspace = TestWorkspace::builder()
3882 .crate_with_source(
3883 "mylib",
3884 "src/lib.rs",
3885 r#"
3886 pub trait MyTrait {
3887 fn do_something(&self);
3888 }
3889
3890 pub trait AnotherTrait {
3891 fn other(&self);
3892 }
3893
3894 pub struct MyStruct;
3895
3896 impl MyTrait for MyStruct {
3897 fn do_something(&self) {}
3898 }
3899
3900 impl AnotherTrait for MyStruct {
3901 fn other(&self) {}
3902 }
3903 "#,
3904 )
3905 .build();
3906
3907 let ctx = build_context_from_workspace(&workspace, "mylib");
3908
3909 let impl_mytrait_path = SymbolPath::parse("mylib::<impl MyTrait for MyStruct>").unwrap();
3911 let impl_another_path =
3912 SymbolPath::parse("mylib::<impl AnotherTrait for MyStruct>").unwrap();
3913 let mytrait_path = SymbolPath::parse("mylib::MyTrait").unwrap();
3914 let another_path = SymbolPath::parse("mylib::AnotherTrait").unwrap();
3915 let struct_path = SymbolPath::parse("mylib::MyStruct").unwrap();
3916
3917 let impl_mytrait_id = ctx
3918 .registry
3919 .lookup(&impl_mytrait_path)
3920 .expect("impl MyTrait for MyStruct should be registered");
3921 let impl_another_id = ctx
3922 .registry
3923 .lookup(&impl_another_path)
3924 .expect("impl AnotherTrait for MyStruct should be registered");
3925 let mytrait_id = ctx
3926 .registry
3927 .lookup(&mytrait_path)
3928 .expect("MyTrait should be registered");
3929 let another_id = ctx
3930 .registry
3931 .lookup(&another_path)
3932 .expect("AnotherTrait should be registered");
3933 let struct_id = ctx
3934 .registry
3935 .lookup(&struct_path)
3936 .expect("MyStruct should be registered");
3937
3938 let impl1_targets: Vec<_> = ctx
3940 .code_graph
3941 .outgoing_edges(impl_mytrait_id)
3942 .filter(|e| e.kind == CodeEdgeV2::Implements)
3943 .map(|e| e.to)
3944 .collect();
3945 assert!(
3946 impl1_targets.contains(&mytrait_id),
3947 "impl MyTrait for MyStruct should have Implements edge to MyTrait"
3948 );
3949
3950 let impl2_targets: Vec<_> = ctx
3951 .code_graph
3952 .outgoing_edges(impl_another_id)
3953 .filter(|e| e.kind == CodeEdgeV2::Implements)
3954 .map(|e| e.to)
3955 .collect();
3956 assert!(
3957 impl2_targets.contains(&another_id),
3958 "impl AnotherTrait for MyStruct should have Implements edge to AnotherTrait"
3959 );
3960
3961 let struct_implements: Vec<_> = ctx
3963 .code_graph
3964 .outgoing_edges(struct_id)
3965 .filter(|e| e.kind == CodeEdgeV2::Implements)
3966 .collect();
3967 assert!(
3968 struct_implements.is_empty(),
3969 "Struct should have no direct Implements edges; chain via Impl is needed"
3970 );
3971
3972 let struct_name = struct_path.name();
3974 let impl_ids_for_struct: Vec<_> = ctx
3975 .registry
3976 .iter_by_kind(SymbolKind::Impl)
3977 .filter(|&id| {
3978 ctx.registry
3979 .resolve(id)
3980 .and_then(|p| p.segment_refs().last())
3981 .and_then(|seg| seg.impl_self_ty())
3982 .map(|ty| ty.split('<').next().unwrap_or(ty).trim() == struct_name)
3983 .unwrap_or(false)
3984 })
3985 .collect();
3986 assert_eq!(
3987 impl_ids_for_struct.len(),
3988 2,
3989 "MyStruct should have 2 impl blocks"
3990 );
3991
3992 let mut trait_ids: Vec<_> = impl_ids_for_struct
3994 .iter()
3995 .flat_map(|&impl_id| {
3996 ctx.code_graph
3997 .outgoing_edges(impl_id)
3998 .filter(|e| e.kind == CodeEdgeV2::Implements)
3999 .map(|e| e.to)
4000 .collect::<Vec<_>>()
4001 })
4002 .collect();
4003 trait_ids.sort();
4004 trait_ids.dedup();
4005
4006 assert!(
4007 trait_ids.contains(&mytrait_id),
4008 "Struct → Impl → Trait chain should reach MyTrait"
4009 );
4010 assert!(
4011 trait_ids.contains(&another_id),
4012 "Struct → Impl → Trait chain should reach AnotherTrait"
4013 );
4014
4015 let implementors: Vec<_> = ctx.code_graph.implementors_of(mytrait_id).collect();
4017 assert!(
4018 implementors.contains(&impl_mytrait_id),
4019 "MyTrait should have impl block as implementor"
4020 );
4021
4022 for &impl_id in &implementors {
4024 if let Some(impl_path) = ctx.registry.resolve(impl_id) {
4025 if let Some(last_seg) = impl_path.segment_refs().last() {
4026 if let Some(self_ty) = last_seg.impl_self_ty() {
4027 let base = self_ty.split('<').next().unwrap_or(self_ty).trim();
4028 let resolved_struct = ctx.registry.lookup_by_name(base);
4029 assert!(
4030 resolved_struct.is_some(),
4031 "Impl self_ty '{}' should resolve to a registered struct",
4032 base
4033 );
4034 assert_eq!(
4035 resolved_struct.unwrap(),
4036 struct_id,
4037 "Impl self_ty should resolve to MyStruct"
4038 );
4039 }
4040 }
4041 }
4042 }
4043 }
4044
4045 #[test]
4047 fn test_self_method_resolves_via_impl_type_hint() {
4048 let workspace = TestWorkspace::builder()
4050 .crate_with_source(
4051 "mylib",
4052 "src/lib.rs",
4053 r#"
4054 pub trait IntoResponse {
4055 fn into_response(self) -> String;
4056 }
4057
4058 pub struct Html {
4059 content: String,
4060 }
4061
4062 impl Html {
4063 pub fn render(&self) -> String {
4064 self.content.clone()
4065 }
4066 }
4067
4068 pub struct Json {
4069 data: String,
4070 }
4071
4072 impl Json {
4073 pub fn render(&self) -> String {
4074 self.data.clone()
4075 }
4076 }
4077
4078 impl IntoResponse for Html {
4079 fn into_response(self) -> String {
4080 self.render()
4081 }
4082 }
4083 "#,
4084 )
4085 .build();
4086
4087 let ctx = build_context_from_workspace(&workspace, "mylib");
4088
4089 let into_response_path =
4090 SymbolPath::parse("mylib::<impl IntoResponse for Html>::into_response").unwrap();
4091 let html_render_path = SymbolPath::parse("mylib::Html::render").unwrap();
4092 let json_render_path = SymbolPath::parse("mylib::Json::render").unwrap();
4093
4094 let into_response_id = ctx
4095 .registry
4096 .lookup(&into_response_path)
4097 .expect("into_response should be registered");
4098 let html_render_id = ctx
4099 .registry
4100 .lookup(&html_render_path)
4101 .expect("Html::render should be registered");
4102 let json_render_id = ctx
4103 .registry
4104 .lookup(&json_render_path)
4105 .expect("Json::render should be registered");
4106
4107 let callees: Vec<_> = ctx.code_graph.callees_of(into_response_id).collect();
4108
4109 assert!(
4112 callees.contains(&html_render_id),
4113 "self.render() in Html's trait impl should resolve to Html::render; \
4114 callees = {:?}",
4115 callees
4116 );
4117 assert!(
4118 !callees.contains(&json_render_id),
4119 "self.render() in Html's trait impl should NOT resolve to Json::render; \
4120 callees = {:?}",
4121 callees
4122 );
4123 }
4124
4125 #[test]
4126 fn test_extract_self_type_from_parent_path() {
4127 assert_eq!(
4129 extract_self_type_from_parent_path("mylib::<impl IntoResponse for Html>"),
4130 Some("Html")
4131 );
4132 assert_eq!(
4134 extract_self_type_from_parent_path("mylib::<impl io::Write for Writer < '_ >>"),
4135 Some("Writer")
4136 );
4137 assert_eq!(
4139 extract_self_type_from_parent_path("mylib::<impl Router < S >>"),
4140 Some("Router")
4141 );
4142 assert_eq!(
4144 extract_self_type_from_parent_path("mylib::Html"),
4145 Some("Html")
4146 );
4147 }
4148}