1use std::collections::{HashMap, HashSet};
20use std::path::Path;
21use std::sync::Arc;
22
23use crate::import_map_builder::{build_import_map, collect_public_reexports};
24use crate::SymbolKind;
25use rayon::prelude::*;
26use ryo_source::pure::{
27 PureBlock, PureExpr, PureFields, PureFile, PureFn, PureImpl, PureImplItem, PureItem, PureStmt,
28 PureTraitItem, PureVis,
29};
30use ryo_symbol::{
31 CargoMetadataProvider, FileSpan, SymbolPathResolver, UseResolver, WorkspaceFilePath,
32 WorkspacePathResolver,
33};
34
35use crate::ast::ASTRegistry;
36use crate::detail_store::DetailStore;
37use crate::query::{
38 CodeEdgeV2, CodeGraphV2, DataFlowBuilderWorkspace, DataFlowGraphV2, TypeFlowBuilderV2,
39 TypeFlowGraphV2,
40};
41use crate::symbol::{
42 RegistryUpdate, RegistryUpdateBatch, SymbolId, SymbolPath, SymbolRegistry, Visibility,
43};
44
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 #[cfg(feature = "workspace")]
128 pub dep_resolver: std::sync::OnceLock<Option<ryo_metadata::WorkspaceResolver>>,
129 pub derive_index: crate::query::DeriveIndex,
131}
132
133#[derive(Debug, Clone, Default)]
135pub struct AnalysisConfig {
136 pub parallel: bool,
138 pub pub_only: bool,
140 pub uuid_mappings: Option<HashMap<String, String>>,
143}
144
145impl AnalysisConfig {
146 pub fn new() -> Self {
149 Self::default()
150 }
151
152 pub fn parallel(mut self) -> Self {
154 self.parallel = true;
155 self
156 }
157
158 pub fn pub_only(mut self) -> Self {
160 self.pub_only = true;
161 self
162 }
163
164 pub fn with_uuid_mappings(mut self, mappings: HashMap<String, String>) -> Self {
166 self.uuid_mappings = Some(mappings);
167 self
168 }
169}
170
171impl AnalysisContext {
172 pub fn from_workspace_root(path: impl AsRef<Path>) -> Result<Self, ContextError> {
192 use ryo_symbol::{CargoMetadataProvider, WorkspaceMetadataProvider};
193
194 let path = path.as_ref();
195
196 let metadata = CargoMetadataProvider::from_directory(path)
198 .map_err(|e| ContextError::Metadata(e.to_string()))?;
199
200 let workspace_root = metadata.workspace_root().to_path_buf();
201 let resolver = WorkspacePathResolver::new(workspace_root.clone());
202
203 let uuid_mappings = Self::load_uuid_mappings(&workspace_root);
205
206 let mut files = HashMap::new();
208 Self::load_dir(
209 &workspace_root,
210 &workspace_root,
211 &resolver,
212 &metadata,
213 &mut files,
214 )?;
215
216 let config = match uuid_mappings {
218 Some(mappings) => AnalysisConfig::default().with_uuid_mappings(mappings),
219 None => AnalysisConfig::default(),
220 };
221
222 Self::build_from_workspace_files(files, Arc::from(workspace_root.as_path()), config)
223 }
224
225 pub fn from_workspace_root_parallel(path: impl AsRef<Path>) -> Result<Self, ContextError> {
227 use ryo_symbol::{CargoMetadataProvider, WorkspaceMetadataProvider};
228
229 let path = path.as_ref();
230
231 let metadata = CargoMetadataProvider::from_directory(path)
232 .map_err(|e| ContextError::Metadata(e.to_string()))?;
233
234 let workspace_root = metadata.workspace_root().to_path_buf();
235 let resolver = WorkspacePathResolver::new(workspace_root.clone());
236
237 let uuid_mappings = Self::load_uuid_mappings(&workspace_root);
239
240 let mut files = HashMap::new();
241 Self::load_dir(
242 &workspace_root,
243 &workspace_root,
244 &resolver,
245 &metadata,
246 &mut files,
247 )?;
248
249 let config = match uuid_mappings {
251 Some(mappings) => AnalysisConfig::new()
252 .parallel()
253 .with_uuid_mappings(mappings),
254 None => AnalysisConfig::new().parallel(),
255 };
256
257 Self::build_from_workspace_files(files, Arc::from(workspace_root.as_path()), config)
258 }
259
260 fn load_uuid_mappings(workspace_root: &std::path::Path) -> Option<HashMap<String, String>> {
263 let path = workspace_root.join(".ryo").join("uuid-mapping.json");
264 let content = std::fs::read_to_string(&path).ok()?;
265 serde_json::from_str(&content).ok()
266 }
267
268 #[doc(hidden)]
281 #[cfg(any(test, feature = "testing"))]
282 pub fn save_uuid_mappings(&self) -> Result<(), ContextError> {
283 let ryo_dir = self.workspace_root.join(".ryo");
284 std::fs::create_dir_all(&ryo_dir)
285 .map_err(|e| ContextError::Io(format!("Failed to create .ryo directory: {}", e)))?;
286
287 let path = ryo_dir.join("uuid-mapping.json");
288 let mappings = self.registry.export_uuid_mapping_strings();
289
290 let content = serde_json::to_string_pretty(&mappings)
291 .map_err(|e| ContextError::Io(format!("Failed to serialize UUID mappings: {}", e)))?;
292
293 std::fs::write(&path, content)
294 .map_err(|e| ContextError::Io(format!("Failed to write UUID mappings: {}", e)))?;
295
296 Ok(())
297 }
298
299 fn load_dir(
301 _root: &Path,
302 dir: &Path,
303 resolver: &WorkspacePathResolver,
304 metadata: &CargoMetadataProvider,
305 files: &mut HashMap<WorkspaceFilePath, PureFile>,
306 ) -> Result<(), ContextError> {
307 if !dir.is_dir() {
308 return Ok(());
309 }
310
311 let dir_name = dir.file_name().and_then(|n| n.to_str()).unwrap_or("");
313 if matches!(
314 dir_name,
315 "target" | "node_modules" | ".git" | "dist" | "build"
316 ) {
317 return Ok(());
318 }
319
320 for entry in std::fs::read_dir(dir).map_err(|e| ContextError::Io(e.to_string()))? {
321 let entry = entry.map_err(|e| ContextError::Io(e.to_string()))?;
322 let path = entry.path();
323
324 if path.is_dir() {
325 Self::load_dir(_root, &path, resolver, metadata, files)?;
326 } else if path.extension().map(|e| e == "rs").unwrap_or(false) {
327 match Self::load_file(&path, resolver, metadata) {
328 Ok((wfp, file)) => {
329 files.insert(wfp, file);
330 }
331 Err(_e) => {
332 }
334 }
335 }
336 }
337
338 Ok(())
339 }
340
341 fn load_file(
343 path: &Path,
344 resolver: &WorkspacePathResolver,
345 metadata: &CargoMetadataProvider,
346 ) -> Result<(WorkspaceFilePath, PureFile), ContextError> {
347 let content = std::fs::read_to_string(path)
348 .map_err(|e| ContextError::Io(format!("{}: {}", path.display(), e)))?;
349
350 let file = PureFile::from_source(&content)
351 .map_err(|e| ContextError::Parse(format!("{}: {}", path.display(), e)))?;
352
353 let wfp = resolver
354 .resolve_with_provider(path, metadata)
355 .map_err(|e| ContextError::Resolve(format!("{}: {}", path.display(), e)))?;
356
357 Ok((wfp, file))
358 }
359
360 #[doc(hidden)]
380 pub fn from_workspace_files(files: HashMap<WorkspaceFilePath, PureFile>) -> Self {
381 let workspace_root = files
382 .keys()
383 .next()
384 .expect("from_workspace_files requires at least one file")
385 .workspace_root()
386 .into();
387
388 Self::build_from_workspace_files(files, workspace_root, AnalysisConfig::default())
390 .expect("build_from_workspace_files failed in test API")
391 }
392
393 pub fn from_im_files(files: ImHashMap<WorkspaceFilePath, Arc<PureFile>>) -> Self {
408 let workspace_root = files
410 .keys()
411 .next()
412 .expect("from_im_files requires at least one file")
413 .workspace_root()
414 .into();
415
416 Self {
417 workspace_root,
418 files,
419 original: HashMap::new(),
420 registry: SymbolRegistry::new(),
421 code_graph: CodeGraphV2::new(),
422 typeflow_graph: TypeFlowGraphV2::new(),
423 dataflow_graph: DataFlowGraphV2::new(),
424 detail_store: DetailStore::default(),
425 ast_registry: ASTRegistry::new(),
426 use_resolver: UseResolver::new(),
427 #[cfg(feature = "literal-search")]
428 literal_index: None,
429 #[cfg(feature = "workspace")]
430 dep_resolver: std::sync::OnceLock::new(),
431 derive_index: crate::query::DeriveIndex::new(),
432 }
433 }
434
435 fn build_from_workspace_files(
446 files: HashMap<WorkspaceFilePath, PureFile>,
447 workspace_root: Arc<Path>,
448 config: AnalysisConfig,
449 ) -> Result<Self, ContextError> {
450 let mut registry = SymbolRegistry::new();
451
452 if let Some(mappings) = config.uuid_mappings {
454 registry.preload_uuid_mapping_strings(mappings);
455 }
456
457 let mut code_graph = CodeGraphV2::new();
458
459 let crate_name = files
461 .keys()
462 .next()
463 .expect("No files loaded - cannot determine crate name")
464 .crate_name()
465 .as_str()
466 .to_string();
467
468 let original: HashMap<WorkspaceFilePath, String> = files
470 .iter()
471 .map(|(path, file)| Ok((path.clone(), file.to_source()?)))
472 .collect::<Result<HashMap<_, _>, ContextError>>()?;
473
474 let symbols = if config.parallel {
476 Self::collect_symbols_workspace_parallel(&files, &crate_name, config.pub_only)
477 } else {
478 Self::collect_symbols_workspace(&files, &crate_name, config.pub_only)
479 };
480
481 let mut symbol_ids: HashMap<String, SymbolId> = HashMap::new();
483
484 for (path, kind, pure_vis, file_path) in symbols {
485 let path_str = path.to_string();
486 if let Ok(id) = registry.register(path, kind) {
487 code_graph.add_node(id);
488 code_graph.add_to_kind_index(id, kind);
489 symbol_ids.insert(path_str, id);
490 let vis = pure_vis_to_visibility(&pure_vis);
492 let _ = registry.set_visibility(id, vis);
493 let span = FileSpan::new(file_path, 0, 0);
496 let _ = registry.set_span(id, span);
497 }
498 }
499
500 Self::build_contains_edges_workspace(&files, &crate_name, &symbol_ids, &mut code_graph);
502
503 let mut use_resolver = UseResolver::new();
506 for (file_path, file) in &files {
507 let file_crate_name = file_path.crate_name().as_str();
509 if let Ok(crate_name_obj) = ryo_symbol::CrateName::new(file_crate_name) {
510 let path_resolver = SymbolPathResolver::new(file_crate_name);
511 let mod_path_str = path_resolver.module_path_str(file_path);
512 if let Ok(module_path) = SymbolPath::parse(&mod_path_str) {
513 let import_map = build_import_map(file, &crate_name_obj, &module_path);
514 use_resolver.register(module_path, import_map);
515 }
516 }
517 }
518
519 for (file_path, file) in &files {
523 let file_crate_name = file_path.crate_name().as_str();
524 if let Ok(crate_name_obj) = ryo_symbol::CrateName::new(file_crate_name) {
525 let path_resolver = SymbolPathResolver::new(file_crate_name);
526 let mod_path_str = path_resolver.module_path_str(file_path);
527 if let Ok(module_path) = SymbolPath::parse(&mod_path_str) {
528 let reexports = collect_public_reexports(file, &crate_name_obj, &module_path);
529 for entry in reexports {
530 if let Ok(alias_path) = module_path.child(&entry.local_name) {
531 if let Some(canonical_id) = registry.lookup(&entry.full_path) {
533 if registry.lookup(&alias_path).is_none() {
534 let _ = registry.register_reexport(
535 canonical_id,
536 alias_path,
537 file_path.clone(),
538 );
539 }
540 }
541 }
542 }
543 }
544 }
545 }
546
547 Self::build_reference_edges_workspace(
549 &files,
550 &crate_name,
551 &symbol_ids,
552 &use_resolver,
553 ®istry,
554 &mut code_graph,
555 );
556
557 #[allow(deprecated)]
563 let mut detail_store = DetailStore::build_all_workspace(®istry, &files, &crate_name);
564
565 let im_files: ImHashMap<WorkspaceFilePath, Arc<PureFile>> = files
567 .into_iter()
568 .map(|(path, file)| (path, Arc::new(file)))
569 .collect();
570
571 #[allow(deprecated)]
574 let typeflow_graph =
575 TypeFlowBuilderV2::new_workspace(®istry, &im_files, &crate_name).build();
576
577 let dataflow_graph =
579 DataFlowBuilderWorkspace::new(®istry, &im_files, &crate_name).build();
580
581 let ast_registry = ASTRegistry::build_from_files(&im_files, ®istry, &crate_name);
583
584 let all_symbol_ids: Vec<SymbolId> = registry.iter().map(|(id, _)| id).collect();
601 detail_store.rebuild_for_symbols(&all_symbol_ids, &ast_registry);
602
603 #[cfg(feature = "literal-search")]
605 let literal_index =
606 crate::literal::LiteralIndex::build_from_workspace_files(&im_files, ®istry).ok();
607
608 let derive_index = crate::query::DeriveIndex::build(
610 &ast_registry,
611 &code_graph,
612 &typeflow_graph,
613 ®istry,
614 );
615
616 Ok(Self {
617 workspace_root,
618 registry,
619 code_graph,
620 typeflow_graph,
621 dataflow_graph,
622 detail_store,
623 ast_registry,
624 files: im_files,
625 original,
626 use_resolver,
627 #[cfg(feature = "literal-search")]
628 literal_index,
629 #[cfg(feature = "workspace")]
630 dep_resolver: std::sync::OnceLock::new(),
631 derive_index,
632 })
633 }
634
635 fn build_mod_path_index(files: &HashMap<WorkspaceFilePath, PureFile>) -> HashSet<String> {
647 files
648 .keys()
649 .map(|file_path| {
650 let resolver = SymbolPathResolver::new(file_path.crate_name().as_str());
651 resolver.module_path_str(file_path)
652 })
653 .collect()
654 }
655
656 fn parent_in_workspace(mod_path: &str, mod_paths: &HashSet<String>) -> bool {
660 mod_path
661 .rfind("::")
662 .map(|idx| mod_paths.contains(&mod_path[..idx]))
663 .unwrap_or(false)
664 }
665
666 fn collect_symbols_workspace(
668 files: &HashMap<WorkspaceFilePath, PureFile>,
669 _crate_name: &str,
670 pub_only: bool,
671 ) -> Vec<(SymbolPath, SymbolKind, PureVis, WorkspaceFilePath)> {
672 let mod_paths = Self::build_mod_path_index(files);
673 let mut symbols = Vec::new();
674
675 for (file_path, file) in files {
676 let file_crate_name = file_path.crate_name().as_str();
678 let resolver = SymbolPathResolver::new(file_crate_name);
679 let mod_path = resolver.module_path_str(file_path);
680 let has_parent = Self::parent_in_workspace(&mod_path, &mod_paths);
681 let mut file_symbols = Vec::new();
682 Self::collect_from_file(&mod_path, file, pub_only, has_parent, &mut file_symbols);
683 for (path, kind, vis) in file_symbols {
684 symbols.push((path, kind, vis, file_path.clone()));
685 }
686 }
687
688 symbols
689 }
690
691 fn collect_symbols_workspace_parallel(
693 files: &HashMap<WorkspaceFilePath, PureFile>,
694 _crate_name: &str,
695 pub_only: bool,
696 ) -> Vec<(SymbolPath, SymbolKind, PureVis, WorkspaceFilePath)> {
697 let mod_paths = Self::build_mod_path_index(files);
698 files
699 .par_iter()
700 .flat_map(|(file_path, file)| {
701 let file_crate_name = file_path.crate_name().as_str();
703 let resolver = SymbolPathResolver::new(file_crate_name);
704 let mod_path = resolver.module_path_str(file_path);
705 let has_parent = Self::parent_in_workspace(&mod_path, &mod_paths);
706 let mut symbols = Vec::new();
707 Self::collect_from_file(&mod_path, file, pub_only, has_parent, &mut symbols);
708 symbols
709 .into_iter()
710 .map(|(path, kind, vis)| (path, kind, vis, file_path.clone()))
711 .collect::<Vec<_>>()
712 })
713 .collect()
714 }
715
716 fn collect_from_file(
730 mod_path: &str,
731 file: &PureFile,
732 pub_only: bool,
733 has_parent: bool,
734 out: &mut Vec<(SymbolPath, SymbolKind, PureVis)>,
735 ) {
736 if !has_parent {
737 if let Ok(path) = SymbolPath::parse(mod_path) {
738 out.push((path, SymbolKind::Mod, PureVis::Public));
739 }
740 }
741
742 for item in &file.items {
744 Self::collect_from_item(mod_path, item, pub_only, out);
745 }
746 }
747
748 fn collect_from_item(
750 parent_path: &str,
751 item: &PureItem,
752 pub_only: bool,
753 out: &mut Vec<(SymbolPath, SymbolKind, PureVis)>,
754 ) {
755 let (name, kind, vis) = match item {
756 PureItem::Struct(s) => {
757 if pub_only && s.vis == PureVis::Private {
758 return;
759 }
760 if let Ok(parent) = SymbolPath::parse(parent_path) {
761 if let Ok(struct_path) = parent.child(&s.name) {
762 out.push((struct_path.clone(), SymbolKind::Struct, s.vis.clone()));
763 if let PureFields::Named(fields) = &s.fields {
765 for field in fields {
766 if let Ok(field_path) = struct_path.child(&field.name) {
767 out.push((field_path, SymbolKind::Field, field.vis.clone()));
768 }
769 }
770 }
771 }
772 }
773 return;
774 }
775 PureItem::Enum(e) => {
776 if pub_only && e.vis == PureVis::Private {
778 return;
779 }
780 let enum_path = format!("{}::{}", parent_path, e.name);
781 if let Ok(path) = SymbolPath::parse(&enum_path) {
782 out.push((path, SymbolKind::Enum, e.vis.clone()));
783 }
784 for variant in &e.variants {
786 let variant_path = format!("{}::{}", enum_path, variant.name);
787 if let Ok(path) = SymbolPath::parse(&variant_path) {
788 out.push((path, SymbolKind::Variant, e.vis.clone()));
790 }
791 }
792 return;
793 }
794 PureItem::Fn(f) => (f.name.clone(), SymbolKind::Function, f.vis.clone()),
795 PureItem::Trait(t) => {
796 if pub_only && t.vis == PureVis::Private {
797 return;
798 }
799 if let Ok(parent) = SymbolPath::parse(parent_path) {
800 if let Ok(trait_path) = parent.child(&t.name) {
801 out.push((trait_path.clone(), SymbolKind::Trait, t.vis.clone()));
802 for trait_item in &t.items {
804 let (item_name, item_kind) = match trait_item {
805 PureTraitItem::Fn(f) => (&f.name, SymbolKind::Method),
806 PureTraitItem::Const(c) => (&c.name, SymbolKind::Const),
807 PureTraitItem::Type { name, .. } => (name, SymbolKind::TypeAlias),
808 PureTraitItem::Other(_) => continue,
809 };
810 if let Ok(item_path) = trait_path.child(item_name) {
811 out.push((item_path, item_kind, t.vis.clone()));
813 }
814 }
815 }
816 }
817 return;
818 }
819 PureItem::Impl(i) => {
820 Self::collect_from_impl(parent_path, i, pub_only, out);
821 return;
822 }
823 PureItem::Mod(m) => {
824 if pub_only && m.vis == PureVis::Private {
825 return;
826 }
827 let mod_path = format!("{}::{}", parent_path, m.name);
828 if let Ok(path) = SymbolPath::parse(&mod_path) {
829 out.push((path, SymbolKind::Mod, m.vis.clone()));
830 }
831 for inner_item in &m.items {
833 Self::collect_from_item(&mod_path, inner_item, pub_only, out);
834 }
835 return;
836 }
837 PureItem::Use(_) => return,
838 PureItem::Const(c) => (c.name.clone(), SymbolKind::Const, c.vis.clone()),
839 PureItem::Static(s) => (s.name.clone(), SymbolKind::Static, s.vis.clone()),
840 PureItem::Type(t) => (t.name.clone(), SymbolKind::TypeAlias, t.vis.clone()),
841 PureItem::Macro(m) => {
861 let name = m.name.clone().unwrap_or_else(|| m.path.clone());
862 (name, SymbolKind::Macro, PureVis::Private)
863 }
864 PureItem::Other(_) => return,
865 PureItem::Verbatim(_) => return,
866 };
867
868 if pub_only && vis == PureVis::Private {
870 return;
871 }
872
873 let full_path = format!("{}::{}", parent_path, name);
874 if let Ok(path) = SymbolPath::parse(&full_path) {
875 out.push((path, kind, vis));
876 }
877 }
878
879 fn collect_from_impl(
887 parent_path: &str,
888 impl_block: &PureImpl,
889 pub_only: bool,
890 out: &mut Vec<(SymbolPath, SymbolKind, PureVis)>,
891 ) {
892 let parent = match SymbolPath::parse(parent_path) {
893 Ok(p) => p,
894 Err(_) => return,
895 };
896 let impl_target = &impl_block.self_ty;
897
898 let method_base = if let Some(ref trait_name) = impl_block.trait_ {
902 let impl_path = parent.child_trait_impl(trait_name, impl_target);
903 out.push((impl_path.clone(), SymbolKind::Impl, PureVis::Public));
904 impl_path
905 } else {
906 let impl_path = parent.child_inherent_impl(impl_target);
907 out.push((impl_path, SymbolKind::Impl, PureVis::Public));
908 let base_type = impl_target.split('<').next().unwrap_or(impl_target).trim();
912 match parent.child(base_type) {
913 Ok(p) => p,
914 Err(_) => return,
915 }
916 };
917
918 for item in &impl_block.items {
920 let (name, kind, vis) = match item {
921 PureImplItem::Fn(m) => (m.name.clone(), SymbolKind::Method, m.vis.clone()),
922 PureImplItem::Const(c) => (c.name.clone(), SymbolKind::Const, c.vis.clone()),
923 PureImplItem::Type(t) => (t.name.clone(), SymbolKind::TypeAlias, t.vis.clone()),
924 PureImplItem::Other(_) => continue,
925 };
926
927 if pub_only && vis == PureVis::Private {
928 continue;
929 }
930
931 if let Ok(path) = method_base.child(&name) {
932 out.push((path, kind, vis));
933 }
934 }
935 }
936
937 fn build_contains_edges_workspace(
941 _files: &HashMap<WorkspaceFilePath, PureFile>,
942 _crate_name: &str,
943 symbol_ids: &HashMap<String, SymbolId>,
944 graph: &mut CodeGraphV2,
945 ) {
946 for (path_str, &child_id) in symbol_ids {
948 if let Some(parent_path) = get_parent_path(path_str) {
949 if let Some(&parent_id) = symbol_ids.get(&parent_path) {
950 graph.add_edge(parent_id, child_id, CodeEdgeV2::Contains);
951 }
952 }
953 }
954 }
955
956 fn build_reference_edges_workspace(
958 files: &HashMap<WorkspaceFilePath, PureFile>,
959 _crate_name: &str,
960 symbol_ids: &HashMap<String, SymbolId>,
961 use_resolver: &UseResolver,
962 registry: &SymbolRegistry,
963 graph: &mut CodeGraphV2,
964 ) {
965 let method_index = build_method_name_index(symbol_ids, registry);
966 for (file_path, file) in files {
967 let file_crate_name = file_path.crate_name().as_str();
969 let resolver = SymbolPathResolver::new(file_crate_name);
970 let mod_path = resolver.module_path_str(file_path);
971 Self::build_edges_from_items(
972 &mod_path,
973 &file.items,
974 symbol_ids,
975 use_resolver,
976 registry,
977 graph,
978 &method_index,
979 );
980 }
981 }
982
983 fn build_edges_from_items(
985 parent_path: &str,
986 items: &[PureItem],
987 symbol_ids: &HashMap<String, SymbolId>,
988 use_resolver: &UseResolver,
989 registry: &SymbolRegistry,
990 graph: &mut CodeGraphV2,
991 method_index: &MethodNameIndex,
992 ) {
993 for item in items {
994 match item {
995 PureItem::Impl(impl_block) => {
996 Self::build_edges_from_impl(
997 parent_path,
998 impl_block,
999 symbol_ids,
1000 use_resolver,
1001 registry,
1002 graph,
1003 method_index,
1004 );
1005 }
1006 PureItem::Fn(func) => {
1007 let fn_path = format!("{}::{}", parent_path, func.name);
1008 Self::build_edges_from_fn(
1009 &fn_path,
1010 func,
1011 symbol_ids,
1012 use_resolver,
1013 registry,
1014 graph,
1015 method_index,
1016 );
1017 }
1018 PureItem::Struct(_) => {
1019 }
1021 PureItem::Mod(m) if !m.items.is_empty() => {
1022 let mod_path = format!("{}::{}", parent_path, m.name);
1023 Self::build_edges_from_items(
1024 &mod_path,
1025 &m.items,
1026 symbol_ids,
1027 use_resolver,
1028 registry,
1029 graph,
1030 method_index,
1031 );
1032 }
1033 _ => {}
1034 }
1035 }
1036 }
1037
1038 fn build_edges_from_item(
1043 parent_path: &str,
1044 item: &PureItem,
1045 symbol_ids: &HashMap<String, SymbolId>,
1046 use_resolver: &UseResolver,
1047 registry: &SymbolRegistry,
1048 graph: &mut CodeGraphV2,
1049 method_index: &MethodNameIndex,
1050 ) {
1051 match item {
1052 PureItem::Impl(impl_block) => {
1053 Self::build_edges_from_impl(
1054 parent_path,
1055 impl_block,
1056 symbol_ids,
1057 use_resolver,
1058 registry,
1059 graph,
1060 method_index,
1061 );
1062 }
1063 PureItem::Fn(func) => {
1064 let fn_path = format!("{}::{}", parent_path, func.name);
1065 Self::build_edges_from_fn(
1066 &fn_path,
1067 func,
1068 symbol_ids,
1069 use_resolver,
1070 registry,
1071 graph,
1072 method_index,
1073 );
1074 }
1075 PureItem::Struct(_) => {
1076 }
1078 PureItem::Mod(m) if !m.items.is_empty() => {
1079 let mod_path = format!("{}::{}", parent_path, m.name);
1080 Self::build_edges_from_items(
1081 &mod_path,
1082 &m.items,
1083 symbol_ids,
1084 use_resolver,
1085 registry,
1086 graph,
1087 method_index,
1088 );
1089 }
1090 _ => {}
1091 }
1092 }
1093
1094 fn build_edges_from_impl(
1096 parent_path: &str,
1097 impl_block: &PureImpl,
1098 symbol_ids: &HashMap<String, SymbolId>,
1099 use_resolver: &UseResolver,
1100 registry: &SymbolRegistry,
1101 graph: &mut CodeGraphV2,
1102 method_index: &MethodNameIndex,
1103 ) {
1104 let parent = match SymbolPath::parse(parent_path) {
1105 Ok(p) => p,
1106 Err(_) => return,
1107 };
1108 let impl_target = &impl_block.self_ty;
1109
1110 let impl_path = if let Some(ref trait_name) = &impl_block.trait_ {
1112 parent.child_trait_impl(trait_name, impl_target)
1113 } else {
1114 parent.child_inherent_impl(impl_target)
1115 };
1116
1117 let impl_id = match symbol_ids.get(&impl_path.to_string()) {
1119 Some(&id) => id,
1120 None => return,
1121 };
1122
1123 if let Some(ref trait_name) = &impl_block.trait_ {
1125 if let Some(trait_id) = Self::resolve_type_reference(
1126 parent_path,
1127 trait_name,
1128 symbol_ids,
1129 use_resolver,
1130 registry,
1131 ) {
1132 graph.add_edge(impl_id, trait_id, CodeEdgeV2::Implements);
1133 }
1134 }
1135
1136 let method_base = if impl_block.trait_.is_some() {
1142 impl_path
1143 } else {
1144 let base_type = impl_target.split('<').next().unwrap_or(impl_target).trim();
1146 match parent.child(base_type) {
1147 Ok(p) => p,
1148 Err(_) => return,
1149 }
1150 };
1151
1152 for item in &impl_block.items {
1153 if let PureImplItem::Fn(func) = item {
1154 if let Ok(method_path) = method_base.child(&func.name) {
1155 Self::build_edges_from_fn(
1156 &method_path.to_string(),
1157 func,
1158 symbol_ids,
1159 use_resolver,
1160 registry,
1161 graph,
1162 method_index,
1163 );
1164 }
1165 }
1166 }
1167 }
1168
1169 fn build_edges_from_fn(
1171 fn_path: &str,
1172 func: &PureFn,
1173 symbol_ids: &HashMap<String, SymbolId>,
1174 use_resolver: &UseResolver,
1175 registry: &SymbolRegistry,
1176 graph: &mut CodeGraphV2,
1177 method_index: &MethodNameIndex,
1178 ) {
1179 let fn_id = match symbol_ids.get(fn_path) {
1180 Some(&id) => id,
1181 None => return,
1182 };
1183
1184 let parent_path = get_parent_path(fn_path).unwrap_or_default();
1186
1187 let mut cx = CallsBuildContext {
1192 symbol_ids,
1193 use_resolver,
1194 registry,
1195 graph,
1196 method_index,
1197 };
1198 Self::build_calls_from_block(fn_id, &parent_path, &func.body, &mut cx);
1199 }
1200
1201 fn build_calls_from_block(
1203 caller_id: SymbolId,
1204 parent_path: &str,
1205 block: &PureBlock,
1206 cx: &mut CallsBuildContext<'_>,
1207 ) {
1208 for stmt in &block.stmts {
1209 match stmt {
1210 PureStmt::Local {
1211 init: Some(expr), ..
1212 }
1213 | PureStmt::Semi(expr)
1214 | PureStmt::Expr(expr) => {
1215 Self::build_calls_from_expr(caller_id, parent_path, expr, cx);
1216 }
1217 _ => {}
1218 }
1219 }
1220 }
1221
1222 fn build_calls_from_expr(
1224 caller_id: SymbolId,
1225 parent_path: &str,
1226 expr: &PureExpr,
1227 cx: &mut CallsBuildContext<'_>,
1228 ) {
1229 use ryo_source::pure::PureExpr;
1230
1231 match expr {
1232 PureExpr::Call { func, args } => {
1233 if let PureExpr::Path(path) = func.as_ref() {
1235 if let Some(callee_id) = Self::resolve_type_reference(
1236 parent_path,
1237 path,
1238 cx.symbol_ids,
1239 cx.use_resolver,
1240 cx.registry,
1241 ) {
1242 cx.graph.add_edge(caller_id, callee_id, CodeEdgeV2::Calls);
1243 }
1244 }
1245 for arg in args {
1247 Self::build_calls_from_expr(caller_id, parent_path, arg, cx);
1248 }
1249 Self::build_calls_from_expr(caller_id, parent_path, func, cx);
1251 }
1252 PureExpr::MethodCall {
1253 receiver,
1254 method,
1255 args,
1256 ..
1257 } => {
1258 let is_self_receiver = matches!(receiver.as_ref(), PureExpr::Path(name) if name == "self")
1262 || matches!(receiver.as_ref(), PureExpr::Field { expr, .. } if matches!(expr.as_ref(), PureExpr::Path(name) if name == "self"));
1263
1264 let mut resolved = false;
1265 if is_self_receiver {
1266 let sibling_path = format!("{}::{}", parent_path, method);
1268 if let Some(&callee_id) = cx.symbol_ids.get(&sibling_path) {
1269 if callee_id != caller_id {
1270 cx.graph.add_edge(caller_id, callee_id, CodeEdgeV2::Calls);
1271 resolved = true;
1272 }
1273 }
1274 }
1275
1276 if !resolved {
1288 if let Some(candidates) = cx.method_index.get(method.as_str()) {
1289 let explicit_hint = extract_receiver_type_hint(receiver);
1292 let type_hint = explicit_hint.or_else(|| {
1293 if is_self_receiver {
1294 extract_self_type_from_parent_path(parent_path)
1295 } else {
1296 None
1297 }
1298 });
1299
1300 if let Some(hint) = type_hint {
1301 let filtered: Vec<_> = candidates
1303 .iter()
1304 .copied()
1305 .filter(|&id| candidate_matches_type_hint(id, hint, cx.registry))
1306 .collect();
1307 if !filtered.is_empty() {
1308 for callee_id in filtered {
1309 if callee_id != caller_id {
1310 cx.graph.add_edge(caller_id, callee_id, CodeEdgeV2::Calls);
1311 }
1312 }
1313 } else {
1314 let is_common = is_common_trait_method(method);
1316 if !is_common || candidates.len() == 1 {
1317 for &callee_id in candidates {
1318 if callee_id != caller_id {
1319 cx.graph.add_edge(
1320 caller_id,
1321 callee_id,
1322 CodeEdgeV2::Calls,
1323 );
1324 }
1325 }
1326 }
1327 }
1328 } else {
1329 let is_common = is_common_trait_method(method);
1331 if !is_common || candidates.len() == 1 {
1332 for &callee_id in candidates {
1333 if callee_id != caller_id {
1334 cx.graph.add_edge(caller_id, callee_id, CodeEdgeV2::Calls);
1335 }
1336 }
1337 }
1338 }
1339 }
1340 }
1341
1342 Self::build_calls_from_expr(caller_id, parent_path, receiver, cx);
1344 for arg in args {
1345 Self::build_calls_from_expr(caller_id, parent_path, arg, cx);
1346 }
1347 }
1348 PureExpr::Block { block, .. } => {
1349 Self::build_calls_from_block(caller_id, parent_path, block, cx);
1350 }
1351 PureExpr::If {
1352 cond,
1353 then_branch,
1354 else_branch,
1355 } => {
1356 Self::build_calls_from_expr(caller_id, parent_path, cond, cx);
1357 Self::build_calls_from_block(caller_id, parent_path, then_branch, cx);
1358 if let Some(else_expr) = else_branch {
1359 Self::build_calls_from_expr(caller_id, parent_path, else_expr, cx);
1360 }
1361 }
1362 PureExpr::Match {
1363 expr: match_expr,
1364 arms,
1365 } => {
1366 Self::build_calls_from_expr(caller_id, parent_path, match_expr, cx);
1367 for arm in arms {
1368 Self::build_calls_from_expr(caller_id, parent_path, &arm.body, cx);
1369 if let Some(ref guard) = arm.guard {
1370 Self::build_calls_from_expr(caller_id, parent_path, guard, cx);
1371 }
1372 }
1373 }
1374 PureExpr::Loop { body: block, .. }
1375 | PureExpr::Async { body: block, .. }
1376 | PureExpr::Unsafe(block) => {
1377 Self::build_calls_from_block(caller_id, parent_path, block, cx);
1378 }
1379 PureExpr::While { cond, body, .. } => {
1380 Self::build_calls_from_expr(caller_id, parent_path, cond, cx);
1381 Self::build_calls_from_block(caller_id, parent_path, body, cx);
1382 }
1383 PureExpr::For {
1384 expr: iter_expr,
1385 body,
1386 ..
1387 } => {
1388 Self::build_calls_from_expr(caller_id, parent_path, iter_expr, cx);
1389 Self::build_calls_from_block(caller_id, parent_path, body, cx);
1390 }
1391 PureExpr::Closure { body, .. } => {
1392 Self::build_calls_from_expr(caller_id, parent_path, body, cx);
1393 }
1394 PureExpr::Binary { left, right, .. } => {
1395 Self::build_calls_from_expr(caller_id, parent_path, left, cx);
1396 Self::build_calls_from_expr(caller_id, parent_path, right, cx);
1397 }
1398 PureExpr::Unary { expr: inner, .. }
1399 | PureExpr::Field { expr: inner, .. }
1400 | PureExpr::Await(inner)
1401 | PureExpr::Try(inner)
1402 | PureExpr::Ref { expr: inner, .. }
1403 | PureExpr::Cast { expr: inner, .. } => {
1404 Self::build_calls_from_expr(caller_id, parent_path, inner, cx);
1405 }
1406 PureExpr::Index { expr: arr, index } => {
1407 Self::build_calls_from_expr(caller_id, parent_path, arr, cx);
1408 Self::build_calls_from_expr(caller_id, parent_path, index, cx);
1409 }
1410 PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
1411 for e in exprs {
1412 Self::build_calls_from_expr(caller_id, parent_path, e, cx);
1413 }
1414 }
1415 PureExpr::Struct { fields, .. } => {
1416 for (_, e) in fields {
1417 Self::build_calls_from_expr(caller_id, parent_path, e, cx);
1418 }
1419 }
1420 PureExpr::Return(Some(inner))
1421 | PureExpr::Break {
1422 expr: Some(inner), ..
1423 } => {
1424 Self::build_calls_from_expr(caller_id, parent_path, inner, cx);
1425 }
1426 PureExpr::Range { start, end, .. } => {
1427 if let Some(s) = start {
1428 Self::build_calls_from_expr(caller_id, parent_path, s, cx);
1429 }
1430 if let Some(e) = end {
1431 Self::build_calls_from_expr(caller_id, parent_path, e, cx);
1432 }
1433 }
1434 PureExpr::Let { expr: inner, .. } => {
1435 Self::build_calls_from_expr(caller_id, parent_path, inner, cx);
1436 }
1437 PureExpr::Repeat { expr: elem, len } => {
1438 Self::build_calls_from_expr(caller_id, parent_path, elem, cx);
1439 Self::build_calls_from_expr(caller_id, parent_path, len, cx);
1440 }
1441 _ => {}
1442 }
1443 }
1444
1445 fn resolve_type_reference(
1453 parent_path: &str,
1454 type_name: &str,
1455 symbol_ids: &HashMap<String, SymbolId>,
1456 use_resolver: &UseResolver,
1457 registry: &SymbolRegistry,
1458 ) -> Option<SymbolId> {
1459 let primitives = [
1461 "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize",
1462 "f32", "f64", "bool", "char", "str", "Self",
1463 ];
1464 if primitives.contains(&type_name) {
1465 return None;
1466 }
1467
1468 let module_path_str = strip_to_module_path(parent_path);
1472
1473 if let Ok(module_path) = SymbolPath::parse(&module_path_str) {
1475 if let Some(id) = use_resolver.resolve(&module_path, type_name, registry) {
1476 return Some(id);
1477 }
1478 }
1479
1480 if type_name.contains("::") {
1482 if let Some(&id) = symbol_ids.get(type_name) {
1483 return Some(id);
1484 }
1485
1486 if let Some(split_pos) = type_name.find("::") {
1489 let prefix = &type_name[..split_pos];
1490 let suffix = &type_name[split_pos..]; if let Ok(module_path) = SymbolPath::parse(&module_path_str) {
1492 if let Some(resolved_prefix_id) =
1493 use_resolver.resolve(&module_path, prefix, registry)
1494 {
1495 let resolved_prefix_path = registry.path(resolved_prefix_id);
1496 if let Some(full_path_str) = resolved_prefix_path {
1497 let combined = format!("{}{}", full_path_str, suffix);
1498 if let Some(&id) = symbol_ids.get(&combined) {
1499 return Some(id);
1500 }
1501 }
1502 }
1503 }
1504 }
1505 }
1506
1507 let qualified = format!("{}::{}", parent_path, type_name);
1509 if let Some(&id) = symbol_ids.get(&qualified) {
1510 return Some(id);
1511 }
1512
1513 let mut current_path = parent_path.to_string();
1515 while let Some(parent) = get_parent_path(¤t_path) {
1516 let qualified = format!("{}::{}", parent, type_name);
1517 if let Some(&id) = symbol_ids.get(&qualified) {
1518 return Some(id);
1519 }
1520 current_path = parent;
1521 }
1522
1523 symbol_ids.get(type_name).copied()
1525 }
1526
1527 pub fn registry(&self) -> &SymbolRegistry {
1529 &self.registry
1530 }
1531
1532 pub fn registry_mut(&mut self) -> &mut SymbolRegistry {
1534 &mut self.registry
1535 }
1536
1537 pub fn code_graph(&self) -> &CodeGraphV2 {
1539 &self.code_graph
1540 }
1541
1542 pub fn code_graph_mut(&mut self) -> &mut CodeGraphV2 {
1544 &mut self.code_graph
1545 }
1546
1547 pub fn typeflow_graph(&self) -> &TypeFlowGraphV2 {
1549 &self.typeflow_graph
1550 }
1551
1552 pub fn workspace_root(&self) -> &Path {
1554 &self.workspace_root
1555 }
1556
1557 pub fn file(&self, path: &WorkspaceFilePath) -> Option<&PureFile> {
1559 self.files.get(path).map(|arc| arc.as_ref())
1560 }
1561
1562 pub fn file_mut(&mut self, path: &WorkspaceFilePath) -> Option<&mut PureFile> {
1568 self.files.get_mut(path).map(Arc::make_mut)
1569 }
1570
1571 pub fn files(&self) -> &ImHashMap<WorkspaceFilePath, Arc<PureFile>> {
1573 &self.files
1574 }
1575
1576 pub fn files_mut(&mut self) -> &mut ImHashMap<WorkspaceFilePath, Arc<PureFile>> {
1578 &mut self.files
1579 }
1580
1581 pub fn original(&self, path: &WorkspaceFilePath) -> Option<&String> {
1583 self.original.get(path)
1584 }
1585
1586 pub fn file_count(&self) -> usize {
1588 self.files.len()
1589 }
1590
1591 pub fn is_empty(&self) -> bool {
1593 self.files.is_empty()
1594 }
1595
1596 pub fn detail_store(&self) -> &DetailStore {
1602 &self.detail_store
1603 }
1604
1605 pub fn detail_store_mut(&mut self) -> &mut DetailStore {
1607 &mut self.detail_store
1608 }
1609
1610 pub fn commit_changes(&mut self, updates: &RegistryUpdateBatch) {
1631 let affected_ids: Vec<SymbolId> =
1633 updates.into_iter().filter_map(|u| u.target_id()).collect();
1634
1635 for update in updates {
1637 let _old_kind = match update {
1639 RegistryUpdate::UpdateKind { id, .. } => self.registry.kind(*id),
1640 _ => None,
1641 };
1642
1643 if let Err(e) = update.clone().apply(&mut self.registry) {
1645 eprintln!("Warning: Failed to apply registry update: {:?}", e);
1646 continue;
1647 }
1648
1649 match update {
1651 RegistryUpdate::Add { path, kind, .. } => {
1652 if let Some(id) = self.registry.lookup(path) {
1653 self.code_graph.add_node(id);
1654 self.code_graph.add_to_kind_index(id, *kind);
1655 }
1656 }
1657 RegistryUpdate::Remove { id } => {
1658 self.code_graph.remove_node(*id);
1659 }
1660 RegistryUpdate::UpdateKind { id, new_kind } => {
1661 if self.code_graph.contains(*id) {
1664 self.code_graph.add_to_kind_index(*id, *new_kind);
1665 }
1666 }
1667 RegistryUpdate::Rename { .. }
1669 | RegistryUpdate::UpdateSpan { .. }
1670 | RegistryUpdate::UpdateVisibility { .. } => {}
1671 }
1672 }
1673
1674 let crate_name = self
1677 .files
1678 .keys()
1679 .next()
1680 .and_then(|path| SymbolPathResolver::from_workspace_path(path).ok())
1681 .map(|r| r.crate_name().to_string())
1682 .unwrap_or_else(|| "crate".to_string());
1683 self.detail_store.rebuild_affected_workspace(
1684 &affected_ids,
1685 &self.registry,
1686 &self.files,
1687 &crate_name,
1688 );
1689 }
1690
1691 pub fn rebuild_edges_for_files(&mut self, file_paths: &[WorkspaceFilePath]) {
1702 let mut symbol_ids: HashMap<String, SymbolId> = HashMap::new();
1704 for (id, _) in self.registry.iter() {
1705 if let Some(path) = self.registry.resolve(id) {
1706 symbol_ids.insert(path.to_string(), id);
1707 }
1708 }
1709
1710 for file_path in file_paths {
1711 for (id, _) in self.registry.iter() {
1713 if let Some(span) = self.registry.span(id) {
1714 if &span.file == file_path {
1715 self.code_graph.clear_outgoing_edges(id);
1716 }
1717 }
1718 }
1719
1720 if let Some(file) = self.files.get(file_path) {
1722 let file_crate_name = file_path.crate_name().as_str();
1723 let resolver = SymbolPathResolver::new(file_crate_name);
1724 let mod_path = resolver.module_path_str(file_path);
1725
1726 let method_index = build_method_name_index(&symbol_ids, &self.registry);
1727 Self::build_edges_from_items(
1728 &mod_path,
1729 &file.items,
1730 &symbol_ids,
1731 &self.use_resolver,
1732 &self.registry,
1733 &mut self.code_graph,
1734 &method_index,
1735 );
1736 }
1737 }
1738 }
1739
1740 pub fn rebuild_edges_for_symbols(&mut self, affected_ids: &[SymbolId]) {
1752 if affected_ids.is_empty() {
1753 return;
1754 }
1755
1756 let mut symbol_ids: HashMap<String, SymbolId> = HashMap::new();
1758 for (id, _) in self.registry.iter() {
1759 if let Some(path) = self.registry.resolve(id) {
1760 symbol_ids.insert(path.to_string(), id);
1761 }
1762 }
1763
1764 for &id in affected_ids {
1766 self.code_graph.clear_outgoing_edges(id);
1767 }
1768
1769 for &id in affected_ids {
1771 let parent_path = match self.registry.resolve(id) {
1773 Some(path) => {
1774 let path_str = path.to_string();
1776 path_str
1777 .rsplit_once("::")
1778 .map(|(parent, _)| parent.to_string())
1779 .unwrap_or_else(|| path_str.clone())
1780 }
1781 None => continue,
1782 };
1783
1784 if let Some(item) = self.ast_registry.get(id) {
1786 let method_index = build_method_name_index(&symbol_ids, &self.registry);
1787 Self::build_edges_from_item(
1788 &parent_path,
1789 item,
1790 &symbol_ids,
1791 &self.use_resolver,
1792 &self.registry,
1793 &mut self.code_graph,
1794 &method_index,
1795 );
1796 }
1797 }
1798 }
1799
1800 pub fn get_symbols_in_files(&self, file_paths: &[WorkspaceFilePath]) -> Vec<SymbolId> {
1807 let file_set: std::collections::HashSet<_> = file_paths.iter().collect();
1808 let mut symbols = Vec::new();
1809
1810 for (id, _) in self.registry.iter() {
1811 if let Some(span) = self.registry.span(id) {
1812 if file_set.contains(&span.file) {
1813 symbols.push(id);
1814 }
1815 }
1816 }
1817
1818 symbols
1819 }
1820
1821 pub fn rebuild_after_mutation(&mut self, modified_files: &[WorkspaceFilePath]) {
1828 let affected_symbols = self.get_symbols_in_files(modified_files);
1829 self.rebuild_after_mutation_by_symbols(&affected_symbols);
1830 }
1831
1832 pub fn rebuild_after_mutation_by_symbols(&mut self, affected_ids: &[SymbolId]) {
1850 if affected_ids.is_empty() {
1851 return;
1852 }
1853
1854 let crate_name = self
1856 .files
1857 .keys()
1858 .next()
1859 .map(|r| r.crate_name().to_string())
1860 .unwrap_or_else(|| "unknown".to_string());
1861
1862 let mut file_set = std::collections::HashSet::new();
1864 for &id in affected_ids {
1865 if let Some(span) = self.registry.span(id) {
1866 file_set.insert(span.file.clone());
1867 }
1868 }
1869 let _modified_files: Vec<_> = file_set.into_iter().collect();
1870
1871 self.rebuild_edges_for_symbols(affected_ids);
1873
1874 self.typeflow_graph =
1877 TypeFlowBuilderV2::build_from_ast_registry(&self.registry, &self.ast_registry);
1878
1879 self.dataflow_graph.clear_for_symbols(affected_ids);
1881 DataFlowBuilderWorkspace::new(&self.registry, &self.files, &crate_name)
1882 .build_incremental_by_symbols(
1883 &mut self.dataflow_graph,
1884 &self.ast_registry,
1885 affected_ids,
1886 );
1887
1888 self.detail_store
1890 .rebuild_for_symbols(affected_ids, &self.ast_registry);
1891
1892 self.derive_index.rebuild_for_symbols(
1894 affected_ids,
1895 &self.ast_registry,
1896 &self.code_graph,
1897 &self.typeflow_graph,
1898 &self.registry,
1899 );
1900 }
1901
1902 pub fn fork(&self) -> ExecutionContext<'_> {
1937 ExecutionContext {
1938 workspace_root: &self.workspace_root,
1939 registry: &self.registry,
1940 graph: &self.code_graph,
1941 files: self.files.clone(), }
1943 }
1944
1945 pub fn fork_rebuild(&self) -> Self {
1962 let files: HashMap<WorkspaceFilePath, PureFile> = self
1964 .files
1965 .iter()
1966 .map(|(path, arc)| (path.clone(), (**arc).clone()))
1967 .collect();
1968 Self::build_from_workspace_files(
1969 files,
1970 self.workspace_root.clone(),
1971 AnalysisConfig::default(),
1972 )
1973 .expect("fork_rebuild: source generation failed")
1974 }
1975
1976 pub fn fork_clone(&self) -> Self {
1993 Self {
1994 workspace_root: self.workspace_root.clone(),
1995 registry: self.registry.clone(),
1996 code_graph: self.code_graph.clone(),
1997 typeflow_graph: self.typeflow_graph.clone(),
1998 dataflow_graph: self.dataflow_graph.clone(),
1999 detail_store: self.detail_store.clone(),
2000 ast_registry: self.ast_registry.clone(),
2001 files: self.files.clone(), original: self.original.clone(),
2003 use_resolver: self.use_resolver.clone(),
2004 #[cfg(feature = "literal-search")]
2007 literal_index: None,
2008 #[cfg(feature = "workspace")]
2011 dep_resolver: std::sync::OnceLock::new(),
2012 derive_index: self.derive_index.clone(),
2013 }
2014 }
2015
2016 #[cfg(feature = "workspace")]
2026 pub fn workspace_dep_resolver(&self) -> Option<&ryo_metadata::WorkspaceResolver> {
2027 self.dep_resolver
2028 .get_or_init(|| {
2029 ryo_metadata::WorkspaceResolver::from_directory(&self.workspace_root).ok()
2030 })
2031 .as_ref()
2032 }
2033
2034 pub fn symbol_count(&self) -> usize {
2036 self.registry.len()
2037 }
2038
2039 pub fn snapshot_symbols(&self, symbols: &[SymbolId]) -> ContextSnapshot {
2055 let ast_items: HashMap<SymbolId, PureItem> = symbols
2056 .iter()
2057 .filter_map(|&id| self.ast_registry.get(id).map(|item| (id, item.clone())))
2058 .collect();
2059
2060 ContextSnapshot { ast_items }
2061 }
2062
2063 pub fn rollback(&mut self, snapshot: ContextSnapshot, affected_ids: &[SymbolId]) {
2069 for (id, item) in snapshot.ast_items {
2071 self.ast_registry.set(id, item);
2072 }
2073
2074 if !affected_ids.is_empty() {
2077 self.rebuild_after_mutation_by_symbols(affected_ids);
2078 }
2079 }
2080}
2081
2082#[derive(Debug, Clone)]
2087pub struct ContextSnapshot {
2088 pub ast_items: HashMap<SymbolId, PureItem>,
2090}
2091
2092pub struct ExecutionContext<'a> {
2129 pub workspace_root: &'a Path,
2131
2132 pub registry: &'a SymbolRegistry,
2137
2138 pub graph: &'a CodeGraphV2,
2140
2141 pub files: ImHashMap<WorkspaceFilePath, Arc<PureFile>>,
2147}
2148
2149impl<'a> ExecutionContext<'a> {
2150 pub fn file(&self, path: &WorkspaceFilePath) -> Option<&PureFile> {
2152 self.files.get(path).map(|arc| arc.as_ref())
2153 }
2154
2155 pub fn file_mut(&mut self, path: &WorkspaceFilePath) -> Option<&mut PureFile> {
2161 self.files.get_mut(path).map(Arc::make_mut)
2162 }
2163
2164 pub fn has_file(&self, path: &WorkspaceFilePath) -> bool {
2166 self.files.contains_key(path)
2167 }
2168
2169 pub fn file_count(&self) -> usize {
2171 self.files.len()
2172 }
2173}
2174
2175fn pure_vis_to_visibility(pure_vis: &PureVis) -> Visibility {
2177 match pure_vis {
2178 PureVis::Public => Visibility::Public,
2179 PureVis::Crate => Visibility::Crate,
2180 PureVis::Super => Visibility::Super,
2181 PureVis::Private => Visibility::Private,
2182 PureVis::In(path) => {
2183 SymbolPath::parse(path)
2185 .map(|p| Visibility::Restricted(Box::new(p)))
2186 .unwrap_or(Visibility::Private)
2187 }
2188 }
2189}
2190
2191fn get_parent_path(path: &str) -> Option<String> {
2193 let parts: Vec<&str> = path.rsplitn(2, "::").collect();
2194 if parts.len() == 2 {
2195 Some(parts[1].to_string())
2196 } else {
2197 None
2198 }
2199}
2200
2201fn strip_to_module_path(path: &str) -> String {
2210 if let Some(impl_pos) = path.find("::<impl ") {
2212 return path[..impl_pos].to_string();
2213 }
2214 path.to_string()
2215}
2216
2217fn is_common_trait_method(method: &str) -> bool {
2222 matches!(
2223 method,
2224 "new"
2225 | "default"
2226 | "fmt"
2227 | "clone"
2228 | "eq"
2229 | "ne"
2230 | "cmp"
2231 | "partial_cmp"
2232 | "hash"
2233 | "from"
2234 | "into"
2235 | "try_from"
2236 | "try_into"
2237 | "as_ref"
2238 | "as_mut"
2239 | "deref"
2240 | "deref_mut"
2241 | "drop"
2242 | "next"
2243 | "into_iter"
2244 | "iter"
2245 | "len"
2246 | "is_empty"
2247 )
2248}
2249
2250fn extract_receiver_type_hint(receiver: &PureExpr) -> Option<&str> {
2259 match receiver {
2260 PureExpr::Call { func, .. } => {
2262 if let PureExpr::Path(path) = func.as_ref() {
2263 let segments: Vec<&str> = path.rsplitn(2, "::").collect();
2265 if segments.len() == 2 {
2266 return Some(segments[1].rsplit("::").next().unwrap_or(segments[1]));
2269 }
2270 }
2271 None
2272 }
2273 PureExpr::Struct { path, .. } => path.rsplit("::").next(),
2275 _ => None,
2276 }
2277}
2278
2279fn extract_self_type_from_parent_path(parent_path: &str) -> Option<&str> {
2289 if let Some(impl_start) = parent_path.rfind("::<impl ") {
2291 let impl_segment = &parent_path[impl_start + 2..]; let inner = impl_segment.strip_prefix("<impl ")?.strip_suffix('>')?;
2294
2295 let self_ty = if let Some(pos) = inner.find(" for ") {
2297 &inner[pos + 5..]
2298 } else {
2299 inner
2300 };
2301
2302 let base = self_ty.split('<').next().unwrap_or(self_ty).trim();
2304 if !base.is_empty() {
2305 return Some(base);
2306 }
2307 }
2308
2309 parent_path.rsplit("::").next()
2311}
2312
2313fn candidate_matches_type_hint(
2318 candidate_id: SymbolId,
2319 type_hint: &str,
2320 registry: &SymbolRegistry,
2321) -> bool {
2322 if let Some(path) = registry.path(candidate_id) {
2323 for segment in path.segment_refs() {
2324 if segment.is_impl() {
2325 if let Some(self_ty) = segment.impl_self_ty() {
2326 let base = self_ty.split('<').next().unwrap_or(self_ty).trim();
2328 let base_name = base.rsplit("::").next().unwrap_or(base);
2330 return base_name == type_hint;
2331 }
2332 }
2333 }
2334 let segments: Vec<&str> = path.segments().collect();
2337 if segments.len() >= 2 {
2338 let parent_name = segments[segments.len() - 2];
2339 return parent_name == type_hint;
2340 }
2341 }
2342 false
2343}
2344
2345type MethodNameIndex = HashMap<String, Vec<SymbolId>>;
2349
2350struct CallsBuildContext<'a> {
2353 symbol_ids: &'a HashMap<String, SymbolId>,
2354 use_resolver: &'a UseResolver,
2355 registry: &'a SymbolRegistry,
2356 graph: &'a mut CodeGraphV2,
2357 method_index: &'a MethodNameIndex,
2358}
2359
2360fn build_method_name_index(
2366 symbol_ids: &HashMap<String, SymbolId>,
2367 registry: &SymbolRegistry,
2368) -> MethodNameIndex {
2369 let mut index: MethodNameIndex = HashMap::new();
2370 for (path, &id) in symbol_ids {
2371 let kind = registry.kind(id);
2373 let is_callable = matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Method));
2374 if !is_callable {
2375 continue;
2376 }
2377 if let Some(method_name) = path.rsplit("::").next() {
2378 index.entry(method_name.to_string()).or_default().push(id);
2379 }
2380 }
2381 index
2382}
2383
2384#[cfg(test)]
2385mod tests {
2386 use super::*;
2387 use ryo_symbol::{TestWorkspace, WorkspaceFilePath};
2388
2389 fn build_context_from_workspace(
2391 workspace: &TestWorkspace,
2392 crate_name: &str,
2393 ) -> AnalysisContext {
2394 let files: HashMap<WorkspaceFilePath, PureFile> = workspace
2395 .files_in_crate(crate_name)
2396 .into_iter()
2397 .filter_map(|path| {
2398 let abs = path.to_absolute();
2399 let content = std::fs::read_to_string(&abs).ok()?;
2400 let file = PureFile::from_source(&content).ok()?;
2401 Some((path, file))
2402 })
2403 .collect();
2404 let workspace_root = Arc::from(workspace.workspace_root());
2405 AnalysisContext::build_from_workspace_files(
2406 files,
2407 workspace_root,
2408 AnalysisConfig::default(),
2409 )
2410 .expect("build_from_workspace_files failed in test helper")
2411 }
2412
2413 #[test]
2414 fn test_get_parent_path() {
2415 assert_eq!(
2416 get_parent_path("mylib::handlers::handle"),
2417 Some("mylib::handlers".to_string())
2418 );
2419 assert_eq!(get_parent_path("mylib::foo"), Some("mylib".to_string()));
2420 assert_eq!(get_parent_path("mylib"), None);
2421 }
2422
2423 #[test]
2424 fn test_empty_context() {
2425 let workspace = TestWorkspace::builder()
2427 .crate_with_source("test_crate", "src/lib.rs", "")
2428 .build();
2429
2430 let ctx = build_context_from_workspace(&workspace, "test_crate");
2431
2432 assert_eq!(ctx.file_count(), 1);
2434 assert!(ctx.code_graph.node_count() <= 1);
2435 }
2436
2437 #[test]
2438 fn test_context_with_files() {
2439 let workspace = TestWorkspace::builder()
2440 .crate_with_source("mylib", "src/lib.rs", "pub fn foo() {}")
2441 .build();
2442
2443 let ctx = build_context_from_workspace(&workspace, "mylib");
2444
2445 assert_eq!(ctx.file_count(), 1);
2446 let workspace_path = ctx.files.keys().next().expect("should have one file");
2448 assert!(ctx.file(workspace_path).is_some());
2449 assert!(ctx.original(workspace_path).is_some());
2450
2451 let foo_path = SymbolPath::parse("mylib::foo").unwrap();
2453 assert!(
2454 ctx.registry.lookup(&foo_path).is_some(),
2455 "foo should be registered"
2456 );
2457 }
2458
2459 #[test]
2460 fn test_fork_creates_independent_files() {
2461 let workspace = TestWorkspace::builder()
2462 .crate_with_source("mylib", "src/lib.rs", "pub fn foo() {}")
2463 .build();
2464
2465 let original = build_context_from_workspace(&workspace, "mylib");
2466 let workspace_path = original.files.keys().next().expect("file exists").clone();
2467 let mut forked = original.fork();
2468
2469 forked.files.insert(
2471 workspace_path.clone(),
2472 Arc::new(PureFile::from_source("pub fn bar() {}").unwrap()),
2473 );
2474
2475 let original_source = original.file(&workspace_path).unwrap().to_source().unwrap();
2477 let forked_source = forked.file(&workspace_path).unwrap().to_source().unwrap();
2478
2479 assert!(original_source.contains("foo"), "Original should have foo");
2480 assert!(forked_source.contains("bar"), "Forked should have bar");
2481 assert!(
2482 !original_source.contains("bar"),
2483 "Original should not have bar"
2484 );
2485 }
2486
2487 #[test]
2488 fn test_fork_shares_registry() {
2489 let workspace = TestWorkspace::builder()
2490 .crate_with_source("mylib", "src/lib.rs", "pub struct Foo {}")
2491 .build();
2492
2493 let original = build_context_from_workspace(&workspace, "mylib");
2494 let forked = original.fork();
2495
2496 assert!(std::ptr::eq(
2499 &original.registry as *const _,
2500 forked.registry as *const _
2501 ));
2502
2503 assert_eq!(original.registry.len(), forked.registry.len());
2505 }
2506
2507 #[test]
2508 fn test_fork_rebuild_creates_independent_context() {
2509 let workspace = TestWorkspace::builder()
2510 .crate_with_source("mylib", "src/lib.rs", "pub fn foo() {}")
2511 .build();
2512
2513 let original = build_context_from_workspace(&workspace, "mylib");
2514 let rebuilt = original.fork_rebuild();
2515
2516 assert!(!std::ptr::eq(
2518 &original.registry as *const _,
2519 &rebuilt.registry as *const _
2520 ));
2521
2522 assert_eq!(original.registry.len(), rebuilt.registry.len());
2524 }
2525
2526 #[test]
2527 fn test_execution_context_file_access() {
2528 let workspace = TestWorkspace::builder()
2529 .crate_with_source("mylib", "src/lib.rs", "pub fn test_fn() {}")
2530 .build();
2531
2532 let ctx = build_context_from_workspace(&workspace, "mylib");
2533 let workspace_path = ctx.files.keys().next().expect("file exists").clone();
2534 let exec_ctx = ctx.fork();
2535
2536 assert!(exec_ctx.has_file(&workspace_path));
2537 assert_eq!(exec_ctx.file_count(), 1);
2538
2539 let file = exec_ctx.file(&workspace_path).unwrap();
2540 assert!(file.to_source().unwrap().contains("test_fn"));
2541 }
2542
2543 #[test]
2548 fn test_commit_changes_add_symbol() {
2549 use crate::SymbolKind;
2550 use crate::{FileSpan, RegistryUpdate, RegistryUpdateBatch, SymbolPath};
2551
2552 let workspace = TestWorkspace::builder()
2554 .crate_with_source("testcrate", "src/lib.rs", "")
2555 .build();
2556 let mut ctx = build_context_from_workspace(&workspace, "testcrate");
2557
2558 let mut batch = RegistryUpdateBatch::new();
2560 let path = SymbolPath::parse("testcrate::NewStruct").unwrap();
2561 let dummy_span = FileSpan::new(
2562 WorkspaceFilePath::new_for_test("src/test.rs", "/project", "testcrate"),
2563 0,
2564 100,
2565 );
2566 batch.push(RegistryUpdate::Add {
2567 path: path.clone(),
2568 kind: SymbolKind::Struct,
2569 span: dummy_span,
2570 });
2571
2572 let initial_nodes = ctx.code_graph.node_count();
2573 ctx.commit_changes(&batch);
2574
2575 let id = ctx.registry.lookup(&path);
2577 assert!(id.is_some(), "Symbol should be registered");
2578
2579 assert_eq!(
2581 ctx.code_graph.node_count(),
2582 initial_nodes + 1,
2583 "Graph should have one more node"
2584 );
2585 }
2586
2587 #[test]
2588 fn test_commit_changes_remove_symbol() {
2589 use crate::{RegistryUpdate, RegistryUpdateBatch, SymbolPath};
2590
2591 let workspace = TestWorkspace::builder()
2593 .crate_with_source("mylib", "src/lib.rs", "pub struct Foo {}")
2594 .build();
2595
2596 let mut ctx = build_context_from_workspace(&workspace, "mylib");
2597
2598 let foo_path = SymbolPath::parse("mylib::Foo").unwrap();
2600 let foo_id = ctx.registry.lookup(&foo_path);
2601 assert!(foo_id.is_some(), "Foo should exist initially");
2602 let foo_id = foo_id.unwrap();
2603
2604 let initial_nodes = ctx.code_graph.node_count();
2605
2606 let mut batch = RegistryUpdateBatch::new();
2608 batch.push(RegistryUpdate::Remove { id: foo_id });
2609
2610 ctx.commit_changes(&batch);
2611
2612 let foo_path_check = SymbolPath::parse("mylib::Foo").unwrap();
2614 assert!(
2615 ctx.registry.lookup(&foo_path_check).is_none(),
2616 "Symbol should be removed from registry"
2617 );
2618
2619 assert_eq!(
2621 ctx.code_graph.node_count(),
2622 initial_nodes - 1,
2623 "Graph should have one fewer node"
2624 );
2625 }
2626
2627 #[test]
2628 fn test_commit_changes_update_kind() {
2629 use crate::SymbolKind;
2630 use crate::{RegistryUpdate, RegistryUpdateBatch, SymbolPath};
2631
2632 let workspace = TestWorkspace::builder()
2634 .crate_with_source("mylib", "src/lib.rs", "pub struct Foo {}")
2635 .build();
2636
2637 let mut ctx = build_context_from_workspace(&workspace, "mylib");
2638
2639 let foo_path = SymbolPath::parse("mylib::Foo").unwrap();
2641 let foo_id = ctx.registry.lookup(&foo_path).expect("Foo should exist");
2642
2643 assert_eq!(ctx.registry.kind(foo_id), Some(SymbolKind::Struct));
2645
2646 let initial_nodes = ctx.code_graph.node_count();
2647
2648 let mut batch = RegistryUpdateBatch::new();
2650 batch.push(RegistryUpdate::UpdateKind {
2651 id: foo_id,
2652 new_kind: SymbolKind::Enum,
2653 });
2654
2655 ctx.commit_changes(&batch);
2656
2657 assert_eq!(
2659 ctx.registry.kind(foo_id),
2660 Some(SymbolKind::Enum),
2661 "Kind should be updated to Enum"
2662 );
2663
2664 assert_eq!(
2666 ctx.code_graph.node_count(),
2667 initial_nodes,
2668 "Node count should not change"
2669 );
2670 }
2671
2672 #[test]
2673 fn test_commit_changes_batch_multiple_updates() {
2674 use crate::SymbolKind;
2675 use crate::{FileSpan, RegistryUpdate, RegistryUpdateBatch, SymbolPath};
2676
2677 let workspace = TestWorkspace::builder()
2679 .crate_with_source("testcrate", "src/lib.rs", "")
2680 .build();
2681 let mut ctx = build_context_from_workspace(&workspace, "testcrate");
2682
2683 let mut batch = RegistryUpdateBatch::new();
2685 for name in ["Alpha", "Beta", "Gamma"] {
2686 let path = SymbolPath::parse(&format!("testcrate::{}", name)).unwrap();
2687 let dummy_span = FileSpan::new(
2688 WorkspaceFilePath::new_for_test("src/test.rs", "/project", "testcrate"),
2689 0,
2690 100,
2691 );
2692 batch.push(RegistryUpdate::Add {
2693 path,
2694 kind: SymbolKind::Struct,
2695 span: dummy_span,
2696 });
2697 }
2698
2699 let initial_nodes = ctx.code_graph.node_count();
2700 ctx.commit_changes(&batch);
2701
2702 assert_eq!(
2704 ctx.code_graph.node_count(),
2705 initial_nodes + 3,
2706 "Graph should have 3 more nodes"
2707 );
2708
2709 for name in ["Alpha", "Beta", "Gamma"] {
2711 let path = SymbolPath::parse(&format!("testcrate::{}", name)).unwrap();
2712 assert!(
2713 ctx.registry.lookup(&path).is_some(),
2714 "{} should be registered",
2715 name
2716 );
2717 }
2718 }
2719
2720 #[test]
2725 fn test_implements_edge() {
2726 let workspace = TestWorkspace::builder()
2727 .crate_with_source(
2728 "mylib",
2729 "src/lib.rs",
2730 r#"
2731 pub trait MyTrait {
2732 fn do_something(&self);
2733 }
2734
2735 pub struct MyStruct {}
2736
2737 impl MyTrait for MyStruct {
2738 fn do_something(&self) {}
2739 }
2740 "#,
2741 )
2742 .build();
2743
2744 let ctx = build_context_from_workspace(&workspace, "mylib");
2745
2746 let trait_path = SymbolPath::parse("mylib::MyTrait").unwrap();
2748 let trait_id = ctx.registry.lookup(&trait_path);
2749 assert!(trait_id.is_some(), "Trait should be registered");
2750
2751 let struct_path = SymbolPath::parse("mylib::MyStruct").unwrap();
2753 let struct_id = ctx.registry.lookup(&struct_path);
2754 assert!(struct_id.is_some(), "Struct should be registered");
2755
2756 let has_implementors = ctx
2758 .code_graph
2759 .implementors_of(trait_id.unwrap())
2760 .next()
2761 .is_some();
2762 assert!(
2763 has_implementors,
2764 "MyTrait should have at least one implementor"
2765 );
2766 }
2767
2768 #[test]
2769 fn test_uses_edge_from_struct_field() {
2770 let workspace = TestWorkspace::builder()
2771 .crate_with_source(
2772 "mylib",
2773 "src/lib.rs",
2774 r#"
2775 pub struct Inner {}
2776
2777 pub struct Outer {
2778 pub inner: Inner,
2779 }
2780 "#,
2781 )
2782 .build();
2783
2784 let ctx = build_context_from_workspace(&workspace, "mylib");
2785
2786 let outer_path = SymbolPath::parse("mylib::Outer").unwrap();
2787 let inner_path = SymbolPath::parse("mylib::Inner").unwrap();
2788
2789 let outer_id = ctx.registry.lookup(&outer_path);
2790 let inner_id = ctx.registry.lookup(&inner_path);
2791
2792 assert!(outer_id.is_some(), "Outer should be registered");
2793 assert!(inner_id.is_some(), "Inner should be registered");
2794
2795 let outer = outer_id.unwrap();
2797 let is_user = ctx
2798 .typeflow_graph
2799 .type_users(inner_id.unwrap())
2800 .any(|id| id == outer);
2801 assert!(is_user, "Outer should use Inner");
2802 }
2803
2804 #[test]
2805 fn test_uses_edge_from_fn_params() {
2806 let workspace = TestWorkspace::builder()
2807 .crate_with_source(
2808 "mylib",
2809 "src/lib.rs",
2810 r#"
2811 pub struct Config {}
2812
2813 pub fn process(config: Config) {}
2814 "#,
2815 )
2816 .build();
2817
2818 let ctx = build_context_from_workspace(&workspace, "mylib");
2819
2820 let fn_path = SymbolPath::parse("mylib::process").unwrap();
2821 let config_path = SymbolPath::parse("mylib::Config").unwrap();
2822
2823 let fn_id = ctx.registry.lookup(&fn_path);
2824 let config_id = ctx.registry.lookup(&config_path);
2825
2826 assert!(fn_id.is_some(), "Function should be registered");
2827 assert!(config_id.is_some(), "Config should be registered");
2828
2829 let fn_sym = fn_id.unwrap();
2831 let config_sym = config_id.unwrap();
2832
2833 let is_user = ctx
2834 .typeflow_graph
2835 .type_users(config_sym)
2836 .any(|id| id == fn_sym);
2837 assert!(is_user, "process should use Config");
2838 }
2839
2840 #[test]
2841 fn test_calls_edge() {
2842 let workspace = TestWorkspace::builder()
2843 .crate_with_source(
2844 "mylib",
2845 "src/lib.rs",
2846 r#"
2847 pub fn helper() {}
2848
2849 pub fn main_fn() {
2850 helper();
2851 }
2852 "#,
2853 )
2854 .build();
2855
2856 let ctx = build_context_from_workspace(&workspace, "mylib");
2857
2858 let main_path = SymbolPath::parse("mylib::main_fn").unwrap();
2859 let helper_path = SymbolPath::parse("mylib::helper").unwrap();
2860
2861 let main_id = ctx.registry.lookup(&main_path);
2862 let helper_id = ctx.registry.lookup(&helper_path);
2863
2864 assert!(main_id.is_some(), "main_fn should be registered");
2865 assert!(helper_id.is_some(), "helper should be registered");
2866
2867 let main = main_id.unwrap();
2869 let is_caller = ctx
2870 .code_graph
2871 .callers_of(helper_id.unwrap())
2872 .any(|id| id == main);
2873 assert!(is_caller, "main_fn should call helper");
2874 }
2875
2876 #[test]
2877 fn test_fork_clone_creates_independent_context() {
2878 let workspace = TestWorkspace::builder()
2879 .crate_with_source("mylib", "src/lib.rs", "pub fn foo() {}")
2880 .build();
2881
2882 let original = build_context_from_workspace(&workspace, "mylib");
2883 let cloned = original.fork_clone();
2884
2885 assert!(!std::ptr::eq(
2887 &original.registry as *const _,
2888 &cloned.registry as *const _
2889 ));
2890
2891 assert_eq!(original.registry.len(), cloned.registry.len());
2893 assert_eq!(
2894 original.code_graph.node_count(),
2895 cloned.code_graph.node_count()
2896 );
2897 }
2898
2899 #[test]
2900 fn test_fork_clone_is_faster_than_rebuild() {
2901 use std::time::Instant;
2902
2903 let workspace = TestWorkspace::builder()
2905 .crate_with_source("mylib", "src/lib.rs", "pub mod a; pub mod b;")
2906 .crate_with_source("mylib", "src/a.rs", "pub struct A { x: i32 }")
2907 .crate_with_source("mylib", "src/b.rs", "pub struct B { y: String }")
2908 .build();
2909
2910 let ctx = build_context_from_workspace(&workspace, "mylib");
2911
2912 let _ = ctx.fork_clone();
2914 let _ = ctx.fork_rebuild();
2915
2916 let t0 = Instant::now();
2918 for _ in 0..10 {
2919 let _ = ctx.fork_clone();
2920 }
2921 let clone_time = t0.elapsed();
2922
2923 let t1 = Instant::now();
2925 for _ in 0..10 {
2926 let _ = ctx.fork_rebuild();
2927 }
2928 let rebuild_time = t1.elapsed();
2929
2930 eprintln!(
2931 "fork_clone: {:?} avg, fork_rebuild: {:?} avg, speedup: {:.1}x",
2932 clone_time / 10,
2933 rebuild_time / 10,
2934 rebuild_time.as_nanos() as f64 / clone_time.as_nanos() as f64
2935 );
2936
2937 assert!(
2939 clone_time < rebuild_time,
2940 "fork_clone ({:?}) should be faster than fork_rebuild ({:?})",
2941 clone_time,
2942 rebuild_time
2943 );
2944 }
2945
2946 #[test]
2951 fn test_uuid_persistence_save_and_load() {
2952 use ryo_symbol::SymbolPath;
2953
2954 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
2955 let workspace_root: Arc<Path> = Arc::from(temp_dir.path());
2956
2957 std::fs::create_dir_all(temp_dir.path().join(".ryo")).unwrap();
2959
2960 let wfp = WorkspaceFilePath::new_for_test("src/lib.rs", temp_dir.path(), "test_crate");
2962
2963 let source = r#"pub struct TestStruct { pub field: i32 }"#;
2964 let file = PureFile::from_source(source).expect("Failed to parse");
2965
2966 let mut files = HashMap::new();
2967 files.insert(wfp.clone(), file);
2968
2969 let ctx1 = AnalysisContext::build_from_workspace_files(
2971 files.clone(),
2972 workspace_root.clone(),
2973 AnalysisConfig::default(),
2974 )
2975 .unwrap();
2976
2977 let path = SymbolPath::parse("test_crate::TestStruct").unwrap();
2979 let id1 = ctx1
2980 .registry
2981 .lookup(&path)
2982 .expect("TestStruct should be registered");
2983 let uuid1 = ctx1.registry.uuid(id1).expect("Should have UUID");
2984
2985 ctx1.save_uuid_mappings().expect("Failed to save");
2987
2988 let uuid_file = temp_dir.path().join(".ryo/uuid-mapping.json");
2990 assert!(uuid_file.exists(), "UUID file should exist");
2991
2992 let mappings =
2994 AnalysisContext::load_uuid_mappings(temp_dir.path()).expect("Should load mappings");
2995
2996 let config = AnalysisConfig::default().with_uuid_mappings(mappings);
2998 let ctx2 =
2999 AnalysisContext::build_from_workspace_files(files, workspace_root, config).unwrap();
3000
3001 let id2 = ctx2
3003 .registry
3004 .lookup(&path)
3005 .expect("TestStruct should exist");
3006 let uuid2 = ctx2.registry.uuid(id2).expect("Should have UUID");
3007
3008 assert_eq!(uuid1, uuid2, "UUID should be preserved across rebuilds");
3010 }
3011
3012 #[test]
3013 fn test_uuid_persistence_new_symbol_gets_new_uuid() {
3014 use ryo_symbol::SymbolPath;
3015
3016 let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
3017 let workspace_root: Arc<Path> = Arc::from(temp_dir.path());
3018 std::fs::create_dir_all(temp_dir.path().join(".ryo")).unwrap();
3019
3020 let wfp = WorkspaceFilePath::new_for_test("src/lib.rs", temp_dir.path(), "test_crate");
3021
3022 let source1 = "pub struct First;";
3024 let file1 = PureFile::from_source(source1).unwrap();
3025 let mut files1 = HashMap::new();
3026 files1.insert(wfp.clone(), file1);
3027
3028 let ctx1 = AnalysisContext::build_from_workspace_files(
3029 files1,
3030 workspace_root.clone(),
3031 AnalysisConfig::default(),
3032 )
3033 .unwrap();
3034
3035 let first_path = SymbolPath::parse("test_crate::First").unwrap();
3036 let first_id = ctx1.registry.lookup(&first_path).unwrap();
3037 let first_uuid = ctx1.registry.uuid(first_id).unwrap();
3038 ctx1.save_uuid_mappings().unwrap();
3039
3040 let source2 = "pub struct First;\npub struct Second;";
3042 let file2 = PureFile::from_source(source2).unwrap();
3043 let mut files2 = HashMap::new();
3044 files2.insert(wfp, file2);
3045
3046 let mappings = AnalysisContext::load_uuid_mappings(temp_dir.path()).unwrap();
3047 let config = AnalysisConfig::default().with_uuid_mappings(mappings);
3048 let ctx2 =
3049 AnalysisContext::build_from_workspace_files(files2, workspace_root, config).unwrap();
3050
3051 let first_id2 = ctx2.registry.lookup(&first_path).unwrap();
3053 let first_uuid2 = ctx2.registry.uuid(first_id2).unwrap();
3054 assert_eq!(first_uuid, first_uuid2, "First UUID preserved");
3055
3056 let second_path = SymbolPath::parse("test_crate::Second").unwrap();
3058 let second_id = ctx2.registry.lookup(&second_path).unwrap();
3059 let second_uuid = ctx2.registry.uuid(second_id).unwrap();
3060 assert_ne!(first_uuid, second_uuid, "Second has different UUID");
3061 }
3062
3063 #[test]
3068 fn test_impl_methods_registered_directly_on_struct() {
3069 let workspace = TestWorkspace::builder()
3070 .crate_with_source(
3071 "mylib",
3072 "src/lib.rs",
3073 r#"
3074 pub struct TodoList {
3075 items: Vec<String>,
3076 }
3077
3078 impl TodoList {
3079 pub fn new() -> Self {
3080 Self { items: vec![] }
3081 }
3082
3083 pub fn add(&mut self, item: String) {
3084 self.items.push(item);
3085 }
3086 }
3087 "#,
3088 )
3089 .build();
3090
3091 let ctx = build_context_from_workspace(&workspace, "mylib");
3092
3093 let new_path = SymbolPath::parse("mylib::TodoList::new").unwrap();
3095 let add_path = SymbolPath::parse("mylib::TodoList::add").unwrap();
3096
3097 assert!(
3098 ctx.registry.lookup(&new_path).is_some(),
3099 "new method should be registered as TodoList::new"
3100 );
3101 assert!(
3102 ctx.registry.lookup(&add_path).is_some(),
3103 "add method should be registered as TodoList::add"
3104 );
3105
3106 let impl_blocks: Vec<_> = ctx
3108 .registry
3109 .iter()
3110 .filter(|(_, path)| path.segment_refs().iter().any(|s| s.is_impl()))
3111 .collect();
3112
3113 assert_eq!(
3114 impl_blocks.len(),
3115 1,
3116 "impl block should be registered as <impl TodoList>"
3117 );
3118 assert!(
3119 impl_blocks[0].1.to_string() == "mylib::<impl TodoList>",
3120 "impl block path should be mylib::<impl TodoList>, found: {}",
3121 impl_blocks[0].1
3122 );
3123 }
3124
3125 #[test]
3126 fn test_multiple_impl_blocks_methods_merged_on_struct() {
3127 let workspace = TestWorkspace::builder()
3128 .crate_with_source(
3129 "mylib",
3130 "src/lib.rs",
3131 r#"
3132 pub struct TodoList;
3133
3134 impl TodoList {
3135 pub fn new() -> Self { Self }
3136 }
3137
3138 impl TodoList {
3139 pub fn add(&mut self, item: String) {}
3140 }
3141 "#,
3142 )
3143 .build();
3144
3145 let ctx = build_context_from_workspace(&workspace, "mylib");
3146
3147 let new_path = SymbolPath::parse("mylib::TodoList::new").unwrap();
3149 let add_path = SymbolPath::parse("mylib::TodoList::add").unwrap();
3150
3151 assert!(
3152 ctx.registry.lookup(&new_path).is_some(),
3153 "new from first impl should be registered"
3154 );
3155 assert!(
3156 ctx.registry.lookup(&add_path).is_some(),
3157 "add from second impl should be registered"
3158 );
3159
3160 let impl_blocks: Vec<_> = ctx
3162 .registry
3163 .iter()
3164 .filter(|(_, path)| path.segment_refs().iter().any(|s| s.is_impl()))
3165 .collect();
3166
3167 assert_eq!(
3168 impl_blocks.len(),
3169 1,
3170 "merged impl block should be registered"
3171 );
3172 assert!(
3173 impl_blocks[0].1.to_string() == "mylib::<impl TodoList>",
3174 "impl block path should be mylib::<impl TodoList>"
3175 );
3176 }
3177
3178 #[test]
3185 fn test_calls_edge_associated_fn_cross_module() {
3186 let workspace = TestWorkspace::builder()
3187 .crate_with_source(
3188 "mylib",
3189 "src/lib.rs",
3190 r#"
3191 pub mod types;
3192 pub mod handler;
3193 "#,
3194 )
3195 .crate_with_source(
3196 "mylib",
3197 "src/types.rs",
3198 r#"
3199 pub struct Router {}
3200 impl Router {
3201 pub fn new() -> Self { Router {} }
3202 }
3203 "#,
3204 )
3205 .crate_with_source(
3206 "mylib",
3207 "src/handler.rs",
3208 r#"
3209 use crate::types::Router;
3210 pub fn setup() {
3211 let _r = Router::new();
3212 }
3213 "#,
3214 )
3215 .build();
3216
3217 let ctx = build_context_from_workspace(&workspace, "mylib");
3218
3219 let setup_path = SymbolPath::parse("mylib::handler::setup").unwrap();
3220 let new_path = SymbolPath::parse("mylib::types::Router::new").unwrap();
3221
3222 let setup_id = ctx
3223 .registry
3224 .lookup(&setup_path)
3225 .expect("setup should be registered");
3226 let new_id = ctx
3227 .registry
3228 .lookup(&new_path)
3229 .expect("Router::new should be registered");
3230
3231 let callees: Vec<_> = ctx.code_graph.callees_of(setup_id).collect();
3232 assert!(
3233 callees.contains(&new_id),
3234 "setup should call Router::new, but callees = {:?}",
3235 callees
3236 );
3237 }
3238
3239 #[test]
3241 fn test_calls_edge_free_fn_cross_module_via_use() {
3242 let workspace = TestWorkspace::builder()
3243 .crate_with_source(
3244 "mylib",
3245 "src/lib.rs",
3246 r#"
3247 pub mod utils;
3248 pub mod handler;
3249 "#,
3250 )
3251 .crate_with_source(
3252 "mylib",
3253 "src/utils.rs",
3254 r#"
3255 pub fn helper() {}
3256 "#,
3257 )
3258 .crate_with_source(
3259 "mylib",
3260 "src/handler.rs",
3261 r#"
3262 use crate::utils::helper;
3263 pub fn process() {
3264 helper();
3265 }
3266 "#,
3267 )
3268 .build();
3269
3270 let ctx = build_context_from_workspace(&workspace, "mylib");
3271
3272 let process_path = SymbolPath::parse("mylib::handler::process").unwrap();
3273 let helper_path = SymbolPath::parse("mylib::utils::helper").unwrap();
3274
3275 let process_id = ctx
3276 .registry
3277 .lookup(&process_path)
3278 .expect("process should be registered");
3279 let helper_id = ctx
3280 .registry
3281 .lookup(&helper_path)
3282 .expect("helper should be registered");
3283
3284 let is_callee = ctx
3285 .code_graph
3286 .callees_of(process_id)
3287 .any(|id| id == helper_id);
3288 assert!(is_callee, "process should call helper via use import");
3289 }
3290
3291 #[test]
3294 fn test_calls_edge_associated_fn_new_not_filtered() {
3295 let workspace = TestWorkspace::builder()
3296 .crate_with_source(
3297 "mylib",
3298 "src/lib.rs",
3299 r#"
3300 pub struct Builder {}
3301 impl Builder {
3302 pub fn new() -> Self { Builder {} }
3303 pub fn build(self) -> String { String::new() }
3304 }
3305
3306 pub fn create() -> String {
3307 let b = Builder::new();
3308 b.build()
3309 }
3310 "#,
3311 )
3312 .build();
3313
3314 let ctx = build_context_from_workspace(&workspace, "mylib");
3315
3316 let create_path = SymbolPath::parse("mylib::create").unwrap();
3317 let new_path = SymbolPath::parse("mylib::Builder::new").unwrap();
3318
3319 let create_id = ctx
3320 .registry
3321 .lookup(&create_path)
3322 .expect("create should be registered");
3323 let new_id = ctx
3324 .registry
3325 .lookup(&new_path)
3326 .expect("Builder::new should be registered");
3327
3328 let callees: Vec<_> = ctx.code_graph.callees_of(create_id).collect();
3329 assert!(
3330 callees.contains(&new_id),
3331 "create should call Builder::new (associated fn, not filtered by is_common_trait_method), callees = {:?}",
3332 callees
3333 );
3334 }
3335
3336 #[test]
3338 fn test_calls_edge_qualified_path_call() {
3339 let workspace = TestWorkspace::builder()
3340 .crate_with_source(
3341 "mylib",
3342 "src/lib.rs",
3343 r#"
3344 pub mod utils {
3345 pub fn do_work() {}
3346 }
3347
3348 pub fn caller() {
3349 utils::do_work();
3350 }
3351 "#,
3352 )
3353 .build();
3354
3355 let ctx = build_context_from_workspace(&workspace, "mylib");
3356
3357 let caller_path = SymbolPath::parse("mylib::caller").unwrap();
3358 let do_work_path = SymbolPath::parse("mylib::utils::do_work").unwrap();
3359
3360 let caller_id = ctx
3361 .registry
3362 .lookup(&caller_path)
3363 .expect("caller should be registered");
3364 let do_work_id = ctx
3365 .registry
3366 .lookup(&do_work_path)
3367 .expect("do_work should be registered");
3368
3369 let is_callee = ctx
3370 .code_graph
3371 .callees_of(caller_id)
3372 .any(|id| id == do_work_id);
3373 assert!(
3374 is_callee,
3375 "caller should call utils::do_work via qualified path"
3376 );
3377 }
3378
3379 #[test]
3382 fn test_calls_edge_method_common_name_single_candidate() {
3383 let workspace = TestWorkspace::builder()
3384 .crate_with_source(
3385 "mylib",
3386 "src/lib.rs",
3387 r#"
3388 pub struct Data {}
3389 impl Data {
3390 pub fn clone(&self) -> Self { Data {} }
3391 }
3392
3393 pub fn process(d: Data) -> Data {
3394 d.clone()
3395 }
3396 "#,
3397 )
3398 .build();
3399
3400 let ctx = build_context_from_workspace(&workspace, "mylib");
3401
3402 let process_path = SymbolPath::parse("mylib::process").unwrap();
3403 let clone_path = SymbolPath::parse("mylib::Data::clone").unwrap();
3404
3405 let process_id = ctx
3406 .registry
3407 .lookup(&process_path)
3408 .expect("process should be registered");
3409 let clone_id = ctx
3410 .registry
3411 .lookup(&clone_path)
3412 .expect("Data::clone should be registered");
3413
3414 let callees: Vec<_> = ctx.code_graph.callees_of(process_id).collect();
3415 assert!(
3416 callees.contains(&clone_id),
3417 "process should call Data::clone even though 'clone' is in common trait list (single candidate), callees = {:?}",
3418 callees
3419 );
3420 }
3421
3422 #[test]
3424 fn test_calls_edge_method_many_candidates_not_blocked() {
3425 let workspace = TestWorkspace::builder()
3427 .crate_with_source(
3428 "mylib",
3429 "src/lib.rs",
3430 r#"
3431 pub struct A {} impl A { pub fn handle(&self) {} }
3432 pub struct B {} impl B { pub fn handle(&self) {} }
3433 pub struct C {} impl C { pub fn handle(&self) {} }
3434 pub struct D {} impl D { pub fn handle(&self) {} }
3435 pub struct E {} impl E { pub fn handle(&self) {} }
3436 pub struct F {} impl F { pub fn handle(&self) {} }
3437 pub struct G {} impl G { pub fn handle(&self) {} }
3438 pub struct H {} impl H { pub fn handle(&self) {} }
3439 pub struct I {} impl I { pub fn handle(&self) {} }
3440 pub struct J {} impl J { pub fn handle(&self) {} }
3441 pub struct K {} impl K { pub fn handle(&self) {} }
3442 pub struct L {} impl L { pub fn handle(&self) {} }
3443
3444 pub fn caller(a: A) {
3445 a.handle();
3446 }
3447 "#,
3448 )
3449 .build();
3450
3451 let ctx = build_context_from_workspace(&workspace, "mylib");
3452
3453 let caller_path = SymbolPath::parse("mylib::caller").unwrap();
3454 let caller_id = ctx
3455 .registry
3456 .lookup(&caller_path)
3457 .expect("caller should be registered");
3458
3459 let callees: Vec<_> = ctx.code_graph.callees_of(caller_id).collect();
3460 assert!(
3463 !callees.is_empty(),
3464 "caller should have callees for 'handle' method (12 candidates should not be blocked), callees = {:?}",
3465 callees
3466 );
3467 }
3468
3469 #[test]
3472 fn test_calls_edge_trait_impl_method_has_callees() {
3473 let workspace = TestWorkspace::builder()
3474 .crate_with_source(
3475 "mylib",
3476 "src/lib.rs",
3477 r#"
3478 pub fn helper() -> i32 { 42 }
3479
3480 pub trait MyTrait {
3481 fn do_work(&self) -> i32;
3482 }
3483
3484 pub struct Foo;
3485 impl MyTrait for Foo {
3486 fn do_work(&self) -> i32 {
3487 helper()
3488 }
3489 }
3490 "#,
3491 )
3492 .build();
3493
3494 let ctx = build_context_from_workspace(&workspace, "mylib");
3495
3496 let helper_path = SymbolPath::parse("mylib::helper").unwrap();
3498 let helper_id = ctx
3499 .registry
3500 .lookup(&helper_path)
3501 .expect("helper should be registered");
3502
3503 let method_path = SymbolPath::parse("mylib::<impl MyTrait for Foo>::do_work").unwrap();
3504 let method_id = ctx
3505 .registry
3506 .lookup(&method_path)
3507 .expect("trait impl method should be registered");
3508
3509 let callees: Vec<_> = ctx.code_graph.callees_of(method_id).collect();
3510 assert!(
3511 callees.contains(&helper_id),
3512 "Trait impl method do_work should call helper(), but callees = {:?}",
3513 callees
3514 );
3515 }
3516
3517 #[test]
3519 fn test_calls_edge_trait_impl_method_call_inside_body() {
3520 let workspace = TestWorkspace::builder()
3521 .crate_with_source(
3522 "mylib",
3523 "src/lib.rs",
3524 r#"
3525 pub struct Data {
3526 pub value: i32,
3527 }
3528 impl Data {
3529 pub fn process(&self) -> i32 { self.value }
3530 }
3531
3532 pub trait Transform {
3533 fn transform(&self) -> i32;
3534 }
3535
3536 impl Transform for Data {
3537 fn transform(&self) -> i32 {
3538 self.process()
3539 }
3540 }
3541 "#,
3542 )
3543 .build();
3544
3545 let ctx = build_context_from_workspace(&workspace, "mylib");
3546
3547 let transform_method_path =
3548 SymbolPath::parse("mylib::<impl Transform for Data>::transform").unwrap();
3549 let transform_id = ctx
3550 .registry
3551 .lookup(&transform_method_path)
3552 .expect("transform should be registered");
3553
3554 let process_path = SymbolPath::parse("mylib::Data::process").unwrap();
3555 let process_id = ctx
3556 .registry
3557 .lookup(&process_path)
3558 .expect("Data::process should be registered");
3559
3560 let callees: Vec<_> = ctx.code_graph.callees_of(transform_id).collect();
3561 assert!(
3562 callees.contains(&process_id),
3563 "Trait impl transform() should call self.process(), but callees = {:?}",
3564 callees
3565 );
3566 }
3567
3568 #[test]
3571 fn test_calls_edge_method_over_32_candidates_not_dropped() {
3572 let mut source = String::new();
3574 for i in 0..35 {
3575 source.push_str(&format!(
3576 "pub struct T{i} {{}}\nimpl T{i} {{ pub fn render(&self) -> i32 {{ {i} }} }}\n"
3577 ));
3578 }
3579 source.push_str(
3580 r#"
3581 pub fn caller(t: T0) -> i32 {
3582 t.render()
3583 }
3584 "#,
3585 );
3586
3587 let workspace = TestWorkspace::builder()
3588 .crate_with_source("mylib", "src/lib.rs", &source)
3589 .build();
3590
3591 let ctx = build_context_from_workspace(&workspace, "mylib");
3592
3593 let caller_path = SymbolPath::parse("mylib::caller").unwrap();
3594 let caller_id = ctx
3595 .registry
3596 .lookup(&caller_path)
3597 .expect("caller should be registered");
3598
3599 let callees: Vec<_> = ctx.code_graph.callees_of(caller_id).collect();
3600 assert!(
3601 !callees.is_empty(),
3602 "caller should have callees for 'render' even with 35 candidates (>32 limit), \
3603 but callees is empty — candidate limit silently drops all edges"
3604 );
3605 }
3606
3607 #[test]
3608 fn test_method_index_excludes_non_callable_symbols() {
3609 let source = r#"
3614 pub mod response {
3615 pub mod process {
3616 pub fn run() -> i32 { 42 }
3617 }
3618 }
3619 pub struct Engine;
3620 impl Engine {
3621 pub fn process(&self) -> i32 { 1 }
3622 }
3623 pub fn caller(e: Engine) -> i32 {
3624 e.process()
3625 }
3626 "#;
3627
3628 let workspace = TestWorkspace::builder()
3629 .crate_with_source("mylib", "src/lib.rs", source)
3630 .build();
3631
3632 let ctx = build_context_from_workspace(&workspace, "mylib");
3633
3634 let caller_path = SymbolPath::parse("mylib::caller").unwrap();
3635 let caller_id = ctx
3636 .registry
3637 .lookup(&caller_path)
3638 .expect("caller should be registered");
3639
3640 let callees: Vec<_> = ctx.code_graph.callees_of(caller_id).collect();
3641
3642 let mod_path = SymbolPath::parse("mylib::response::process").unwrap();
3644 let mod_id = ctx
3645 .registry
3646 .lookup(&mod_path)
3647 .expect("nested module mylib::response::process should be registered");
3648
3649 let has_mod_edge = callees.contains(&mod_id);
3650 assert!(
3651 !has_mod_edge,
3652 "method_index should not include module 'response::process' as a callee of caller(); \
3653 only Function/Method symbols should be in the method_index"
3654 );
3655
3656 assert!(
3658 !callees.is_empty(),
3659 "caller should have at least one callee (Engine::process)"
3660 );
3661 }
3662
3663 #[test]
3664 fn test_method_call_receiver_type_hint_filters_candidates() {
3665 let source = r#"
3669 pub trait Render {
3670 fn render(&self) -> String;
3671 }
3672 pub struct Html;
3673 impl Render for Html {
3674 fn render(&self) -> String { String::new() }
3675 }
3676 pub struct Json;
3677 impl Json {
3678 pub fn new() -> Self { Json }
3679 }
3680 impl Render for Json {
3681 fn render(&self) -> String { String::new() }
3682 }
3683 pub struct Xml;
3684 impl Render for Xml {
3685 fn render(&self) -> String { String::new() }
3686 }
3687 pub fn caller() -> String {
3688 Json::new().render()
3689 }
3690 "#;
3691
3692 let workspace = TestWorkspace::builder()
3693 .crate_with_source("mylib", "src/lib.rs", source)
3694 .build();
3695
3696 let ctx = build_context_from_workspace(&workspace, "mylib");
3697
3698 let caller_path = SymbolPath::parse("mylib::caller").unwrap();
3699 let caller_id = ctx
3700 .registry
3701 .lookup(&caller_path)
3702 .expect("caller should be registered");
3703
3704 let callees: Vec<_> = ctx.code_graph.callees_of(caller_id).collect();
3705
3706 let new_path = SymbolPath::parse("mylib::Json::new").unwrap();
3708 let new_id = ctx.registry.lookup(&new_path);
3709 if let Some(new_id) = new_id {
3710 assert!(callees.contains(&new_id), "Json::new should be a callee");
3711 }
3712
3713 let json_render = ctx
3717 .registry
3718 .lookup(&SymbolPath::parse("mylib::<impl Render for Json>::render").unwrap());
3719 let html_render = ctx
3720 .registry
3721 .lookup(&SymbolPath::parse("mylib::<impl Render for Html>::render").unwrap());
3722 let xml_render = ctx
3723 .registry
3724 .lookup(&SymbolPath::parse("mylib::<impl Render for Xml>::render").unwrap());
3725
3726 if let Some(json_id) = json_render {
3727 assert!(
3728 callees.contains(&json_id),
3729 "Json's render should be a callee (receiver type hint: Json from Json::new())"
3730 );
3731 }
3732
3733 if let Some(html_id) = html_render {
3735 assert!(
3736 !callees.contains(&html_id),
3737 "Html's render should NOT be a callee when receiver is Json::new(); \
3738 receiver type hint should filter it out"
3739 );
3740 }
3741
3742 if let Some(xml_id) = xml_render {
3743 assert!(
3744 !callees.contains(&xml_id),
3745 "Xml's render should NOT be a callee when receiver is Json::new(); \
3746 receiver type hint should filter it out"
3747 );
3748 }
3749 }
3750
3751 #[test]
3752 fn test_associated_fn_call_in_trait_impl_resolved_via_imports() {
3753 let source = r#"
3760 pub mod types {
3761 pub struct Body;
3762 impl Body {
3763 pub fn create() -> Self { Body }
3764 }
3765 }
3766 use crate::types::Body;
3767 pub trait Render {
3768 fn render(&self) -> Body;
3769 }
3770 pub struct Page;
3771 impl Render for Page {
3772 fn render(&self) -> Body {
3773 Body::create()
3774 }
3775 }
3776 "#;
3777
3778 let workspace = TestWorkspace::builder()
3779 .crate_with_source("mylib", "src/lib.rs", source)
3780 .build();
3781
3782 let ctx = build_context_from_workspace(&workspace, "mylib");
3783
3784 let render_path = SymbolPath::parse("mylib::<impl Render for Page>::render").unwrap();
3786 let render_id = ctx
3787 .registry
3788 .lookup(&render_path)
3789 .expect("Page::render should be registered");
3790
3791 let callees: Vec<_> = ctx.code_graph.callees_of(render_id).collect();
3792
3793 let create_path = SymbolPath::parse("mylib::types::Body::create").unwrap();
3795 let create_id = ctx
3796 .registry
3797 .lookup(&create_path)
3798 .expect("Body::create should be registered");
3799
3800 assert!(
3801 callees.contains(&create_id),
3802 "Body::create() should be a callee of Page::render(); \
3803 import resolution in trait impl methods must strip impl segments \
3804 from parent_path before querying UseResolver"
3805 );
3806 }
3807
3808 #[test]
3809 fn test_generic_impl_methods_registered_and_edges_built() {
3810 let workspace = TestWorkspace::builder()
3815 .crate_with_source(
3816 "mylib",
3817 "src/lib.rs",
3818 r#"
3819 pub struct Inner;
3820 impl Inner {
3821 pub fn create() -> Self { Inner }
3822 }
3823
3824 pub struct Router<S> {
3825 _marker: std::marker::PhantomData<S>,
3826 }
3827
3828 impl<S> Router<S> {
3829 pub fn new() -> Self {
3830 let _inner = Inner::create();
3831 Router { _marker: std::marker::PhantomData }
3832 }
3833
3834 pub fn route(self, path: &str) -> Self {
3835 self
3836 }
3837 }
3838 "#,
3839 )
3840 .build();
3841
3842 let ctx = build_context_from_workspace(&workspace, "mylib");
3843
3844 let new_path = SymbolPath::parse("mylib::Router::new").unwrap();
3846 let route_path = SymbolPath::parse("mylib::Router::route").unwrap();
3847
3848 let new_id = ctx
3849 .registry
3850 .lookup(&new_path)
3851 .expect("Router::new should be registered despite generic impl<S> Router<S>");
3852 assert!(
3853 ctx.registry.lookup(&route_path).is_some(),
3854 "Router::route should be registered despite generic impl<S> Router<S>"
3855 );
3856
3857 let impl_path_str = "mylib::<impl Router < S >>";
3859 let impl_path = SymbolPath::parse(impl_path_str).unwrap();
3860 assert!(
3861 ctx.registry.lookup(&impl_path).is_some(),
3862 "impl block <impl Router < S >> should be registered"
3863 );
3864
3865 let callees: Vec<_> = ctx.code_graph.callees_of(new_id).collect();
3867 let create_path = SymbolPath::parse("mylib::Inner::create").unwrap();
3868 let create_id = ctx
3869 .registry
3870 .lookup(&create_path)
3871 .expect("Inner::create should be registered");
3872 assert!(
3873 callees.contains(&create_id),
3874 "Router::new must have Inner::create() as callee; \
3875 build_edges_from_impl must strip generics from self_ty"
3876 );
3877 }
3878
3879 #[test]
3880 fn test_external_trait_impl_callees_built() {
3881 let workspace = TestWorkspace::builder()
3884 .crate_with_source(
3885 "mylib",
3886 "src/lib.rs",
3887 r#"
3888 pub trait MyWrite {
3889 fn write(&mut self, buf: &[u8]) -> usize;
3890 }
3891
3892 pub fn helper() -> usize { 42 }
3893
3894 pub struct Writer;
3895
3896 impl MyWrite for Writer {
3897 fn write(&mut self, buf: &[u8]) -> usize {
3898 helper()
3899 }
3900 }
3901 "#,
3902 )
3903 .build();
3904
3905 let ctx = build_context_from_workspace(&workspace, "mylib");
3906
3907 let impl_path_str = "mylib::<impl MyWrite for Writer>";
3909 let impl_path = SymbolPath::parse(impl_path_str).unwrap();
3910 assert!(
3911 ctx.registry.lookup(&impl_path).is_some(),
3912 "impl block should be registered: {}",
3913 impl_path_str,
3914 );
3915
3916 let method_path = SymbolPath::parse(&format!("{}::write", impl_path_str)).unwrap();
3917 let method_id = ctx
3918 .registry
3919 .lookup(&method_path)
3920 .expect("Writer::write should be registered under trait impl path");
3921
3922 let callees: Vec<_> = ctx.code_graph.callees_of(method_id).collect();
3924 let helper_path = SymbolPath::parse("mylib::helper").unwrap();
3925 let helper_id = ctx
3926 .registry
3927 .lookup(&helper_path)
3928 .expect("helper should be registered");
3929
3930 assert!(
3931 callees.contains(&helper_id),
3932 "trait impl method Writer::write must have helper() as callee; \
3933 callees = {:?}",
3934 callees
3935 );
3936 }
3937
3938 #[test]
3939 fn test_external_trait_impl_with_generics_callees_built() {
3940 let workspace = TestWorkspace::builder()
3942 .crate_with_source(
3943 "mylib",
3944 "src/lib.rs",
3945 r#"
3946 pub trait MyWrite {
3947 fn write(&mut self, buf: &[u8]) -> usize;
3948 }
3949
3950 pub fn process_buf(buf: &[u8]) -> usize { buf.len() }
3951
3952 pub struct Writer<'a> {
3953 _data: &'a [u8],
3954 }
3955
3956 impl<'a> MyWrite for Writer<'a> {
3957 fn write(&mut self, buf: &[u8]) -> usize {
3958 process_buf(buf)
3959 }
3960 }
3961 "#,
3962 )
3963 .build();
3964
3965 let ctx = build_context_from_workspace(&workspace, "mylib");
3966
3967 let impl_path_str = "mylib::<impl MyWrite for Writer < 'a >>";
3969 let impl_path = SymbolPath::parse(impl_path_str).unwrap();
3970 let impl_registered = ctx.registry.lookup(&impl_path).is_some();
3971
3972 let method_path = SymbolPath::parse(&format!("{}::write", impl_path_str)).unwrap();
3974 let method_id = ctx.registry.lookup(&method_path);
3975
3976 assert!(
3977 impl_registered,
3978 "impl block <impl MyWrite for Writer < 'a >> should be registered"
3979 );
3980 assert!(
3981 method_id.is_some(),
3982 "Writer::write should be registered under trait impl path"
3983 );
3984
3985 if let Some(mid) = method_id {
3986 let callees: Vec<_> = ctx.code_graph.callees_of(mid).collect();
3987 let process_path = SymbolPath::parse("mylib::process_buf").unwrap();
3988 let process_id = ctx
3989 .registry
3990 .lookup(&process_path)
3991 .expect("process_buf should be registered");
3992
3993 assert!(
3994 callees.contains(&process_id),
3995 "trait impl method with generics must have process_buf() as callee; \
3996 callees = {:?}",
3997 callees
3998 );
3999 }
4000 }
4001
4002 #[test]
4004 fn test_struct_trait_implements_chain_via_impl() {
4005 let workspace = TestWorkspace::builder()
4006 .crate_with_source(
4007 "mylib",
4008 "src/lib.rs",
4009 r#"
4010 pub trait MyTrait {
4011 fn do_something(&self);
4012 }
4013
4014 pub trait AnotherTrait {
4015 fn other(&self);
4016 }
4017
4018 pub struct MyStruct;
4019
4020 impl MyTrait for MyStruct {
4021 fn do_something(&self) {}
4022 }
4023
4024 impl AnotherTrait for MyStruct {
4025 fn other(&self) {}
4026 }
4027 "#,
4028 )
4029 .build();
4030
4031 let ctx = build_context_from_workspace(&workspace, "mylib");
4032
4033 let impl_mytrait_path = SymbolPath::parse("mylib::<impl MyTrait for MyStruct>").unwrap();
4035 let impl_another_path =
4036 SymbolPath::parse("mylib::<impl AnotherTrait for MyStruct>").unwrap();
4037 let mytrait_path = SymbolPath::parse("mylib::MyTrait").unwrap();
4038 let another_path = SymbolPath::parse("mylib::AnotherTrait").unwrap();
4039 let struct_path = SymbolPath::parse("mylib::MyStruct").unwrap();
4040
4041 let impl_mytrait_id = ctx
4042 .registry
4043 .lookup(&impl_mytrait_path)
4044 .expect("impl MyTrait for MyStruct should be registered");
4045 let impl_another_id = ctx
4046 .registry
4047 .lookup(&impl_another_path)
4048 .expect("impl AnotherTrait for MyStruct should be registered");
4049 let mytrait_id = ctx
4050 .registry
4051 .lookup(&mytrait_path)
4052 .expect("MyTrait should be registered");
4053 let another_id = ctx
4054 .registry
4055 .lookup(&another_path)
4056 .expect("AnotherTrait should be registered");
4057 let struct_id = ctx
4058 .registry
4059 .lookup(&struct_path)
4060 .expect("MyStruct should be registered");
4061
4062 let impl1_targets: Vec<_> = ctx
4064 .code_graph
4065 .outgoing_edges(impl_mytrait_id)
4066 .filter(|e| e.kind == CodeEdgeV2::Implements)
4067 .map(|e| e.to)
4068 .collect();
4069 assert!(
4070 impl1_targets.contains(&mytrait_id),
4071 "impl MyTrait for MyStruct should have Implements edge to MyTrait"
4072 );
4073
4074 let impl2_targets: Vec<_> = ctx
4075 .code_graph
4076 .outgoing_edges(impl_another_id)
4077 .filter(|e| e.kind == CodeEdgeV2::Implements)
4078 .map(|e| e.to)
4079 .collect();
4080 assert!(
4081 impl2_targets.contains(&another_id),
4082 "impl AnotherTrait for MyStruct should have Implements edge to AnotherTrait"
4083 );
4084
4085 let struct_implements: Vec<_> = ctx
4087 .code_graph
4088 .outgoing_edges(struct_id)
4089 .filter(|e| e.kind == CodeEdgeV2::Implements)
4090 .collect();
4091 assert!(
4092 struct_implements.is_empty(),
4093 "Struct should have no direct Implements edges; chain via Impl is needed"
4094 );
4095
4096 let struct_name = struct_path.name();
4098 let impl_ids_for_struct: Vec<_> = ctx
4099 .registry
4100 .iter_by_kind(SymbolKind::Impl)
4101 .filter(|&id| {
4102 ctx.registry
4103 .resolve(id)
4104 .and_then(|p| p.segment_refs().last())
4105 .and_then(|seg| seg.impl_self_ty())
4106 .map(|ty| ty.split('<').next().unwrap_or(ty).trim() == struct_name)
4107 .unwrap_or(false)
4108 })
4109 .collect();
4110 assert_eq!(
4111 impl_ids_for_struct.len(),
4112 2,
4113 "MyStruct should have 2 impl blocks"
4114 );
4115
4116 let mut trait_ids: Vec<_> = impl_ids_for_struct
4118 .iter()
4119 .flat_map(|&impl_id| {
4120 ctx.code_graph
4121 .outgoing_edges(impl_id)
4122 .filter(|e| e.kind == CodeEdgeV2::Implements)
4123 .map(|e| e.to)
4124 .collect::<Vec<_>>()
4125 })
4126 .collect();
4127 trait_ids.sort();
4128 trait_ids.dedup();
4129
4130 assert!(
4131 trait_ids.contains(&mytrait_id),
4132 "Struct → Impl → Trait chain should reach MyTrait"
4133 );
4134 assert!(
4135 trait_ids.contains(&another_id),
4136 "Struct → Impl → Trait chain should reach AnotherTrait"
4137 );
4138
4139 let implementors: Vec<_> = ctx.code_graph.implementors_of(mytrait_id).collect();
4141 assert!(
4142 implementors.contains(&impl_mytrait_id),
4143 "MyTrait should have impl block as implementor"
4144 );
4145
4146 for &impl_id in &implementors {
4148 if let Some(impl_path) = ctx.registry.resolve(impl_id) {
4149 if let Some(last_seg) = impl_path.segment_refs().last() {
4150 if let Some(self_ty) = last_seg.impl_self_ty() {
4151 let base = self_ty.split('<').next().unwrap_or(self_ty).trim();
4152 let resolved_struct = ctx.registry.lookup_by_name(base);
4153 assert!(
4154 resolved_struct.is_some(),
4155 "Impl self_ty '{}' should resolve to a registered struct",
4156 base
4157 );
4158 assert_eq!(
4159 resolved_struct.unwrap(),
4160 struct_id,
4161 "Impl self_ty should resolve to MyStruct"
4162 );
4163 }
4164 }
4165 }
4166 }
4167 }
4168
4169 #[test]
4171 fn test_self_method_resolves_via_impl_type_hint() {
4172 let workspace = TestWorkspace::builder()
4174 .crate_with_source(
4175 "mylib",
4176 "src/lib.rs",
4177 r#"
4178 pub trait IntoResponse {
4179 fn into_response(self) -> String;
4180 }
4181
4182 pub struct Html {
4183 content: String,
4184 }
4185
4186 impl Html {
4187 pub fn render(&self) -> String {
4188 self.content.clone()
4189 }
4190 }
4191
4192 pub struct Json {
4193 data: String,
4194 }
4195
4196 impl Json {
4197 pub fn render(&self) -> String {
4198 self.data.clone()
4199 }
4200 }
4201
4202 impl IntoResponse for Html {
4203 fn into_response(self) -> String {
4204 self.render()
4205 }
4206 }
4207 "#,
4208 )
4209 .build();
4210
4211 let ctx = build_context_from_workspace(&workspace, "mylib");
4212
4213 let into_response_path =
4214 SymbolPath::parse("mylib::<impl IntoResponse for Html>::into_response").unwrap();
4215 let html_render_path = SymbolPath::parse("mylib::Html::render").unwrap();
4216 let json_render_path = SymbolPath::parse("mylib::Json::render").unwrap();
4217
4218 let into_response_id = ctx
4219 .registry
4220 .lookup(&into_response_path)
4221 .expect("into_response should be registered");
4222 let html_render_id = ctx
4223 .registry
4224 .lookup(&html_render_path)
4225 .expect("Html::render should be registered");
4226 let json_render_id = ctx
4227 .registry
4228 .lookup(&json_render_path)
4229 .expect("Json::render should be registered");
4230
4231 let callees: Vec<_> = ctx.code_graph.callees_of(into_response_id).collect();
4232
4233 assert!(
4236 callees.contains(&html_render_id),
4237 "self.render() in Html's trait impl should resolve to Html::render; \
4238 callees = {:?}",
4239 callees
4240 );
4241 assert!(
4242 !callees.contains(&json_render_id),
4243 "self.render() in Html's trait impl should NOT resolve to Json::render; \
4244 callees = {:?}",
4245 callees
4246 );
4247 }
4248
4249 #[test]
4250 fn test_extract_self_type_from_parent_path() {
4251 assert_eq!(
4253 extract_self_type_from_parent_path("mylib::<impl IntoResponse for Html>"),
4254 Some("Html")
4255 );
4256 assert_eq!(
4258 extract_self_type_from_parent_path("mylib::<impl io::Write for Writer < '_ >>"),
4259 Some("Writer")
4260 );
4261 assert_eq!(
4263 extract_self_type_from_parent_path("mylib::<impl Router < S >>"),
4264 Some("Router")
4265 );
4266 assert_eq!(
4268 extract_self_type_from_parent_path("mylib::Html"),
4269 Some("Html")
4270 );
4271 }
4272}