use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use crate::import_map_builder::{build_import_map, collect_public_reexports};
use crate::SymbolKind;
use rayon::prelude::*;
use ryo_source::pure::{
PureBlock, PureExpr, PureFields, PureFile, PureFn, PureImpl, PureImplItem, PureItem, PureStmt,
PureTraitItem, PureVis,
};
use ryo_symbol::{
CargoMetadataProvider, FileSpan, SymbolPathResolver, UseResolver, WorkspaceFilePath,
WorkspacePathResolver,
};
use crate::ast::ASTRegistry;
use crate::detail_store::DetailStore;
use crate::query::{
CodeEdgeV2, CodeGraphV2, DataFlowBuilderWorkspace, DataFlowGraphV2, TypeFlowBuilderV2,
TypeFlowGraphV2,
};
use crate::symbol::{
RegistryUpdate, RegistryUpdateBatch, SymbolId, SymbolPath, SymbolRegistry, Visibility,
};
pub type ImHashMap<K, V> = im::HashMap<K, V>;
#[derive(Debug, thiserror::Error)]
pub enum ContextError {
#[error("Metadata error: {0}")]
Metadata(String),
#[error("IO error: {0}")]
Io(String),
#[error("Parse error: {0}")]
Parse(String),
#[error("Resolve error: {0}")]
Resolve(String),
#[error("Source generation error: {0}")]
SourceGen(#[from] ryo_source::pure::ToSynError),
}
pub struct AnalysisContext {
pub workspace_root: Arc<Path>,
pub registry: SymbolRegistry,
pub code_graph: CodeGraphV2,
pub typeflow_graph: TypeFlowGraphV2,
pub dataflow_graph: DataFlowGraphV2,
pub detail_store: DetailStore,
pub ast_registry: ASTRegistry,
pub files: ImHashMap<WorkspaceFilePath, Arc<PureFile>>,
pub original: HashMap<WorkspaceFilePath, String>,
pub use_resolver: UseResolver,
#[cfg(feature = "literal-search")]
pub literal_index: Option<crate::literal::LiteralIndex>,
pub derive_index: crate::query::DeriveIndex,
}
#[derive(Debug, Clone, Default)]
pub struct AnalysisConfig {
pub parallel: bool,
pub pub_only: bool,
pub uuid_mappings: Option<HashMap<String, String>>,
}
impl AnalysisConfig {
pub fn new() -> Self {
Self::default()
}
pub fn parallel(mut self) -> Self {
self.parallel = true;
self
}
pub fn pub_only(mut self) -> Self {
self.pub_only = true;
self
}
pub fn with_uuid_mappings(mut self, mappings: HashMap<String, String>) -> Self {
self.uuid_mappings = Some(mappings);
self
}
}
impl AnalysisContext {
pub fn from_workspace_root(path: impl AsRef<Path>) -> Result<Self, ContextError> {
use ryo_symbol::{CargoMetadataProvider, WorkspaceMetadataProvider};
let path = path.as_ref();
let metadata = CargoMetadataProvider::from_directory(path)
.map_err(|e| ContextError::Metadata(e.to_string()))?;
let workspace_root = metadata.workspace_root().to_path_buf();
let resolver = WorkspacePathResolver::new(workspace_root.clone());
let uuid_mappings = Self::load_uuid_mappings(&workspace_root);
let mut files = HashMap::new();
Self::load_dir(
&workspace_root,
&workspace_root,
&resolver,
&metadata,
&mut files,
)?;
let config = match uuid_mappings {
Some(mappings) => AnalysisConfig::default().with_uuid_mappings(mappings),
None => AnalysisConfig::default(),
};
Self::build_from_workspace_files(files, Arc::from(workspace_root.as_path()), config)
}
pub fn from_workspace_root_parallel(path: impl AsRef<Path>) -> Result<Self, ContextError> {
use ryo_symbol::{CargoMetadataProvider, WorkspaceMetadataProvider};
let path = path.as_ref();
let metadata = CargoMetadataProvider::from_directory(path)
.map_err(|e| ContextError::Metadata(e.to_string()))?;
let workspace_root = metadata.workspace_root().to_path_buf();
let resolver = WorkspacePathResolver::new(workspace_root.clone());
let uuid_mappings = Self::load_uuid_mappings(&workspace_root);
let mut files = HashMap::new();
Self::load_dir(
&workspace_root,
&workspace_root,
&resolver,
&metadata,
&mut files,
)?;
let config = match uuid_mappings {
Some(mappings) => AnalysisConfig::new()
.parallel()
.with_uuid_mappings(mappings),
None => AnalysisConfig::new().parallel(),
};
Self::build_from_workspace_files(files, Arc::from(workspace_root.as_path()), config)
}
fn load_uuid_mappings(workspace_root: &std::path::Path) -> Option<HashMap<String, String>> {
let path = workspace_root.join(".ryo").join("uuid-mapping.json");
let content = std::fs::read_to_string(&path).ok()?;
serde_json::from_str(&content).ok()
}
#[doc(hidden)]
#[cfg(any(test, feature = "testing"))]
pub fn save_uuid_mappings(&self) -> Result<(), ContextError> {
let ryo_dir = self.workspace_root.join(".ryo");
std::fs::create_dir_all(&ryo_dir)
.map_err(|e| ContextError::Io(format!("Failed to create .ryo directory: {}", e)))?;
let path = ryo_dir.join("uuid-mapping.json");
let mappings = self.registry.export_uuid_mapping_strings();
let content = serde_json::to_string_pretty(&mappings)
.map_err(|e| ContextError::Io(format!("Failed to serialize UUID mappings: {}", e)))?;
std::fs::write(&path, content)
.map_err(|e| ContextError::Io(format!("Failed to write UUID mappings: {}", e)))?;
Ok(())
}
fn load_dir(
_root: &Path,
dir: &Path,
resolver: &WorkspacePathResolver,
metadata: &CargoMetadataProvider,
files: &mut HashMap<WorkspaceFilePath, PureFile>,
) -> Result<(), ContextError> {
if !dir.is_dir() {
return Ok(());
}
let dir_name = dir.file_name().and_then(|n| n.to_str()).unwrap_or("");
if matches!(
dir_name,
"target" | "node_modules" | ".git" | "dist" | "build"
) {
return Ok(());
}
for entry in std::fs::read_dir(dir).map_err(|e| ContextError::Io(e.to_string()))? {
let entry = entry.map_err(|e| ContextError::Io(e.to_string()))?;
let path = entry.path();
if path.is_dir() {
Self::load_dir(_root, &path, resolver, metadata, files)?;
} else if path.extension().map(|e| e == "rs").unwrap_or(false) {
match Self::load_file(&path, resolver, metadata) {
Ok((wfp, file)) => {
files.insert(wfp, file);
}
Err(_e) => {
}
}
}
}
Ok(())
}
fn load_file(
path: &Path,
resolver: &WorkspacePathResolver,
metadata: &CargoMetadataProvider,
) -> Result<(WorkspaceFilePath, PureFile), ContextError> {
let content = std::fs::read_to_string(path)
.map_err(|e| ContextError::Io(format!("{}: {}", path.display(), e)))?;
let file = PureFile::from_source(&content)
.map_err(|e| ContextError::Parse(format!("{}: {}", path.display(), e)))?;
let wfp = resolver
.resolve_with_provider(path, metadata)
.map_err(|e| ContextError::Resolve(format!("{}: {}", path.display(), e)))?;
Ok((wfp, file))
}
#[doc(hidden)]
pub fn from_workspace_files(files: HashMap<WorkspaceFilePath, PureFile>) -> Self {
let workspace_root = files
.keys()
.next()
.expect("from_workspace_files requires at least one file")
.workspace_root()
.into();
Self::build_from_workspace_files(files, workspace_root, AnalysisConfig::default())
.expect("build_from_workspace_files failed in test API")
}
pub fn from_im_files(files: ImHashMap<WorkspaceFilePath, Arc<PureFile>>) -> Self {
let workspace_root = files
.keys()
.next()
.expect("from_im_files requires at least one file")
.workspace_root()
.into();
Self {
workspace_root,
files,
original: HashMap::new(),
registry: SymbolRegistry::new(),
code_graph: CodeGraphV2::new(),
typeflow_graph: TypeFlowGraphV2::new(),
dataflow_graph: DataFlowGraphV2::new(),
detail_store: DetailStore::default(),
ast_registry: ASTRegistry::new(),
use_resolver: UseResolver::new(),
#[cfg(feature = "literal-search")]
literal_index: None,
derive_index: crate::query::DeriveIndex::new(),
}
}
fn build_from_workspace_files(
files: HashMap<WorkspaceFilePath, PureFile>,
workspace_root: Arc<Path>,
config: AnalysisConfig,
) -> Result<Self, ContextError> {
let mut registry = SymbolRegistry::new();
if let Some(mappings) = config.uuid_mappings {
registry.preload_uuid_mapping_strings(mappings);
}
let mut code_graph = CodeGraphV2::new();
let crate_name = files
.keys()
.next()
.expect("No files loaded - cannot determine crate name")
.crate_name()
.as_str()
.to_string();
let original: HashMap<WorkspaceFilePath, String> = files
.iter()
.map(|(path, file)| Ok((path.clone(), file.to_source()?)))
.collect::<Result<HashMap<_, _>, ContextError>>()?;
let symbols = if config.parallel {
Self::collect_symbols_workspace_parallel(&files, &crate_name, config.pub_only)
} else {
Self::collect_symbols_workspace(&files, &crate_name, config.pub_only)
};
let mut symbol_ids: HashMap<String, SymbolId> = HashMap::new();
for (path, kind, pure_vis, file_path) in symbols {
let path_str = path.to_string();
if let Ok(id) = registry.register(path, kind) {
code_graph.add_node(id);
code_graph.add_to_kind_index(id, kind);
symbol_ids.insert(path_str, id);
let vis = pure_vis_to_visibility(&pure_vis);
let _ = registry.set_visibility(id, vis);
let span = FileSpan::new(file_path, 0, 0);
let _ = registry.set_span(id, span);
}
}
Self::build_contains_edges_workspace(&files, &crate_name, &symbol_ids, &mut code_graph);
let mut use_resolver = UseResolver::new();
for (file_path, file) in &files {
let file_crate_name = file_path.crate_name().as_str();
if let Ok(crate_name_obj) = ryo_symbol::CrateName::new(file_crate_name) {
let path_resolver = SymbolPathResolver::new(file_crate_name);
let mod_path_str = path_resolver.module_path_str(file_path);
if let Ok(module_path) = SymbolPath::parse(&mod_path_str) {
let import_map = build_import_map(file, &crate_name_obj, &module_path);
use_resolver.register(module_path, import_map);
}
}
}
for (file_path, file) in &files {
let file_crate_name = file_path.crate_name().as_str();
if let Ok(crate_name_obj) = ryo_symbol::CrateName::new(file_crate_name) {
let path_resolver = SymbolPathResolver::new(file_crate_name);
let mod_path_str = path_resolver.module_path_str(file_path);
if let Ok(module_path) = SymbolPath::parse(&mod_path_str) {
let reexports = collect_public_reexports(file, &crate_name_obj, &module_path);
for entry in reexports {
if let Ok(alias_path) = module_path.child(&entry.local_name) {
if let Some(canonical_id) = registry.lookup(&entry.full_path) {
if registry.lookup(&alias_path).is_none() {
let _ = registry.register_reexport(
canonical_id,
alias_path,
file_path.clone(),
);
}
}
}
}
}
}
}
Self::build_reference_edges_workspace(
&files,
&crate_name,
&symbol_ids,
&use_resolver,
®istry,
&mut code_graph,
);
#[allow(deprecated)]
let detail_store = DetailStore::build_all_workspace(®istry, &files, &crate_name);
let im_files: ImHashMap<WorkspaceFilePath, Arc<PureFile>> = files
.into_iter()
.map(|(path, file)| (path, Arc::new(file)))
.collect();
#[allow(deprecated)]
let typeflow_graph =
TypeFlowBuilderV2::new_workspace(®istry, &im_files, &crate_name).build();
let dataflow_graph =
DataFlowBuilderWorkspace::new(®istry, &im_files, &crate_name).build();
let ast_registry = ASTRegistry::build_from_files(&im_files, ®istry, &crate_name);
#[cfg(feature = "literal-search")]
let literal_index =
crate::literal::LiteralIndex::build_from_workspace_files(&im_files, ®istry).ok();
let derive_index = crate::query::DeriveIndex::build(
&ast_registry,
&code_graph,
&typeflow_graph,
®istry,
);
Ok(Self {
workspace_root,
registry,
code_graph,
typeflow_graph,
dataflow_graph,
detail_store,
ast_registry,
files: im_files,
original,
use_resolver,
#[cfg(feature = "literal-search")]
literal_index,
derive_index,
})
}
fn collect_symbols_workspace(
files: &HashMap<WorkspaceFilePath, PureFile>,
_crate_name: &str,
pub_only: bool,
) -> Vec<(SymbolPath, SymbolKind, PureVis, WorkspaceFilePath)> {
let mut symbols = Vec::new();
for (file_path, file) in files {
let file_crate_name = file_path.crate_name().as_str();
let resolver = SymbolPathResolver::new(file_crate_name);
let mod_path = resolver.module_path_str(file_path);
let mut file_symbols = Vec::new();
Self::collect_from_file(&mod_path, file, pub_only, &mut file_symbols);
for (path, kind, vis) in file_symbols {
symbols.push((path, kind, vis, file_path.clone()));
}
}
symbols
}
fn collect_symbols_workspace_parallel(
files: &HashMap<WorkspaceFilePath, PureFile>,
_crate_name: &str,
pub_only: bool,
) -> Vec<(SymbolPath, SymbolKind, PureVis, WorkspaceFilePath)> {
files
.par_iter()
.flat_map(|(file_path, file)| {
let file_crate_name = file_path.crate_name().as_str();
let resolver = SymbolPathResolver::new(file_crate_name);
let mod_path = resolver.module_path_str(file_path);
let mut symbols = Vec::new();
Self::collect_from_file(&mod_path, file, pub_only, &mut symbols);
symbols
.into_iter()
.map(|(path, kind, vis)| (path, kind, vis, file_path.clone()))
.collect::<Vec<_>>()
})
.collect()
}
fn collect_from_file(
mod_path: &str,
file: &PureFile,
pub_only: bool,
out: &mut Vec<(SymbolPath, SymbolKind, PureVis)>,
) {
if let Ok(path) = SymbolPath::parse(mod_path) {
out.push((path, SymbolKind::Mod, PureVis::Public));
}
for item in &file.items {
Self::collect_from_item(mod_path, item, pub_only, out);
}
}
fn collect_from_item(
parent_path: &str,
item: &PureItem,
pub_only: bool,
out: &mut Vec<(SymbolPath, SymbolKind, PureVis)>,
) {
let (name, kind, vis) = match item {
PureItem::Struct(s) => {
if pub_only && s.vis == PureVis::Private {
return;
}
if let Ok(parent) = SymbolPath::parse(parent_path) {
if let Ok(struct_path) = parent.child(&s.name) {
out.push((struct_path.clone(), SymbolKind::Struct, s.vis.clone()));
if let PureFields::Named(fields) = &s.fields {
for field in fields {
if let Ok(field_path) = struct_path.child(&field.name) {
out.push((field_path, SymbolKind::Field, field.vis.clone()));
}
}
}
}
}
return;
}
PureItem::Enum(e) => {
if pub_only && e.vis == PureVis::Private {
return;
}
let enum_path = format!("{}::{}", parent_path, e.name);
if let Ok(path) = SymbolPath::parse(&enum_path) {
out.push((path, SymbolKind::Enum, e.vis.clone()));
}
for variant in &e.variants {
let variant_path = format!("{}::{}", enum_path, variant.name);
if let Ok(path) = SymbolPath::parse(&variant_path) {
out.push((path, SymbolKind::Variant, e.vis.clone()));
}
}
return;
}
PureItem::Fn(f) => (f.name.clone(), SymbolKind::Function, f.vis.clone()),
PureItem::Trait(t) => {
if pub_only && t.vis == PureVis::Private {
return;
}
if let Ok(parent) = SymbolPath::parse(parent_path) {
if let Ok(trait_path) = parent.child(&t.name) {
out.push((trait_path.clone(), SymbolKind::Trait, t.vis.clone()));
for trait_item in &t.items {
let (item_name, item_kind) = match trait_item {
PureTraitItem::Fn(f) => (&f.name, SymbolKind::Method),
PureTraitItem::Const(c) => (&c.name, SymbolKind::Const),
PureTraitItem::Type { name, .. } => (name, SymbolKind::TypeAlias),
PureTraitItem::Other(_) => continue,
};
if let Ok(item_path) = trait_path.child(item_name) {
out.push((item_path, item_kind, t.vis.clone()));
}
}
}
}
return;
}
PureItem::Impl(i) => {
Self::collect_from_impl(parent_path, i, pub_only, out);
return;
}
PureItem::Mod(m) => {
if pub_only && m.vis == PureVis::Private {
return;
}
let mod_path = format!("{}::{}", parent_path, m.name);
if let Ok(path) = SymbolPath::parse(&mod_path) {
out.push((path, SymbolKind::Mod, m.vis.clone()));
}
for inner_item in &m.items {
Self::collect_from_item(&mod_path, inner_item, pub_only, out);
}
return;
}
PureItem::Use(_) => return,
PureItem::Const(c) => (c.name.clone(), SymbolKind::Const, c.vis.clone()),
PureItem::Static(s) => (s.name.clone(), SymbolKind::Static, s.vis.clone()),
PureItem::Type(t) => (t.name.clone(), SymbolKind::TypeAlias, t.vis.clone()),
PureItem::Macro(_) => return,
PureItem::Other(_) => return,
};
if pub_only && vis == PureVis::Private {
return;
}
let full_path = format!("{}::{}", parent_path, name);
if let Ok(path) = SymbolPath::parse(&full_path) {
out.push((path, kind, vis));
}
}
fn collect_from_impl(
parent_path: &str,
impl_block: &PureImpl,
pub_only: bool,
out: &mut Vec<(SymbolPath, SymbolKind, PureVis)>,
) {
let parent = match SymbolPath::parse(parent_path) {
Ok(p) => p,
Err(_) => return,
};
let impl_target = &impl_block.self_ty;
let method_base = if let Some(ref trait_name) = impl_block.trait_ {
let impl_path = parent.child_trait_impl(trait_name, impl_target);
out.push((impl_path.clone(), SymbolKind::Impl, PureVis::Public));
impl_path
} else {
let impl_path = parent.child_inherent_impl(impl_target);
out.push((impl_path, SymbolKind::Impl, PureVis::Public));
let base_type = impl_target.split('<').next().unwrap_or(impl_target).trim();
match parent.child(base_type) {
Ok(p) => p,
Err(_) => return,
}
};
for item in &impl_block.items {
let (name, kind, vis) = match item {
PureImplItem::Fn(m) => (m.name.clone(), SymbolKind::Method, m.vis.clone()),
PureImplItem::Const(c) => (c.name.clone(), SymbolKind::Const, c.vis.clone()),
PureImplItem::Type(t) => (t.name.clone(), SymbolKind::TypeAlias, t.vis.clone()),
PureImplItem::Other(_) => continue,
};
if pub_only && vis == PureVis::Private {
continue;
}
if let Ok(path) = method_base.child(&name) {
out.push((path, kind, vis));
}
}
}
fn build_contains_edges_workspace(
_files: &HashMap<WorkspaceFilePath, PureFile>,
_crate_name: &str,
symbol_ids: &HashMap<String, SymbolId>,
graph: &mut CodeGraphV2,
) {
for (path_str, &child_id) in symbol_ids {
if let Some(parent_path) = get_parent_path(path_str) {
if let Some(&parent_id) = symbol_ids.get(&parent_path) {
graph.add_edge(parent_id, child_id, CodeEdgeV2::Contains);
}
}
}
}
fn build_reference_edges_workspace(
files: &HashMap<WorkspaceFilePath, PureFile>,
_crate_name: &str,
symbol_ids: &HashMap<String, SymbolId>,
use_resolver: &UseResolver,
registry: &SymbolRegistry,
graph: &mut CodeGraphV2,
) {
let method_index = build_method_name_index(symbol_ids, registry);
for (file_path, file) in files {
let file_crate_name = file_path.crate_name().as_str();
let resolver = SymbolPathResolver::new(file_crate_name);
let mod_path = resolver.module_path_str(file_path);
Self::build_edges_from_items(
&mod_path,
&file.items,
symbol_ids,
use_resolver,
registry,
graph,
&method_index,
);
}
}
fn build_edges_from_items(
parent_path: &str,
items: &[PureItem],
symbol_ids: &HashMap<String, SymbolId>,
use_resolver: &UseResolver,
registry: &SymbolRegistry,
graph: &mut CodeGraphV2,
method_index: &MethodNameIndex,
) {
for item in items {
match item {
PureItem::Impl(impl_block) => {
Self::build_edges_from_impl(
parent_path,
impl_block,
symbol_ids,
use_resolver,
registry,
graph,
method_index,
);
}
PureItem::Fn(func) => {
let fn_path = format!("{}::{}", parent_path, func.name);
Self::build_edges_from_fn(
&fn_path,
func,
symbol_ids,
use_resolver,
registry,
graph,
method_index,
);
}
PureItem::Struct(_) => {
}
PureItem::Mod(m) if !m.items.is_empty() => {
let mod_path = format!("{}::{}", parent_path, m.name);
Self::build_edges_from_items(
&mod_path,
&m.items,
symbol_ids,
use_resolver,
registry,
graph,
method_index,
);
}
_ => {}
}
}
}
fn build_edges_from_item(
parent_path: &str,
item: &PureItem,
symbol_ids: &HashMap<String, SymbolId>,
use_resolver: &UseResolver,
registry: &SymbolRegistry,
graph: &mut CodeGraphV2,
method_index: &MethodNameIndex,
) {
match item {
PureItem::Impl(impl_block) => {
Self::build_edges_from_impl(
parent_path,
impl_block,
symbol_ids,
use_resolver,
registry,
graph,
method_index,
);
}
PureItem::Fn(func) => {
let fn_path = format!("{}::{}", parent_path, func.name);
Self::build_edges_from_fn(
&fn_path,
func,
symbol_ids,
use_resolver,
registry,
graph,
method_index,
);
}
PureItem::Struct(_) => {
}
PureItem::Mod(m) if !m.items.is_empty() => {
let mod_path = format!("{}::{}", parent_path, m.name);
Self::build_edges_from_items(
&mod_path,
&m.items,
symbol_ids,
use_resolver,
registry,
graph,
method_index,
);
}
_ => {}
}
}
fn build_edges_from_impl(
parent_path: &str,
impl_block: &PureImpl,
symbol_ids: &HashMap<String, SymbolId>,
use_resolver: &UseResolver,
registry: &SymbolRegistry,
graph: &mut CodeGraphV2,
method_index: &MethodNameIndex,
) {
let parent = match SymbolPath::parse(parent_path) {
Ok(p) => p,
Err(_) => return,
};
let impl_target = &impl_block.self_ty;
let impl_path = if let Some(ref trait_name) = &impl_block.trait_ {
parent.child_trait_impl(trait_name, impl_target)
} else {
parent.child_inherent_impl(impl_target)
};
let impl_id = match symbol_ids.get(&impl_path.to_string()) {
Some(&id) => id,
None => return,
};
if let Some(ref trait_name) = &impl_block.trait_ {
if let Some(trait_id) = Self::resolve_type_reference(
parent_path,
trait_name,
symbol_ids,
use_resolver,
registry,
) {
graph.add_edge(impl_id, trait_id, CodeEdgeV2::Implements);
}
}
let method_base = if impl_block.trait_.is_some() {
impl_path
} else {
let base_type = impl_target.split('<').next().unwrap_or(impl_target).trim();
match parent.child(base_type) {
Ok(p) => p,
Err(_) => return,
}
};
for item in &impl_block.items {
if let PureImplItem::Fn(func) = item {
if let Ok(method_path) = method_base.child(&func.name) {
Self::build_edges_from_fn(
&method_path.to_string(),
func,
symbol_ids,
use_resolver,
registry,
graph,
method_index,
);
}
}
}
}
fn build_edges_from_fn(
fn_path: &str,
func: &PureFn,
symbol_ids: &HashMap<String, SymbolId>,
use_resolver: &UseResolver,
registry: &SymbolRegistry,
graph: &mut CodeGraphV2,
method_index: &MethodNameIndex,
) {
let fn_id = match symbol_ids.get(fn_path) {
Some(&id) => id,
None => return,
};
let parent_path = get_parent_path(fn_path).unwrap_or_default();
let mut cx = CallsBuildContext {
symbol_ids,
use_resolver,
registry,
graph,
method_index,
};
Self::build_calls_from_block(fn_id, &parent_path, &func.body, &mut cx);
}
fn build_calls_from_block(
caller_id: SymbolId,
parent_path: &str,
block: &PureBlock,
cx: &mut CallsBuildContext<'_>,
) {
for stmt in &block.stmts {
match stmt {
PureStmt::Local {
init: Some(expr), ..
}
| PureStmt::Semi(expr)
| PureStmt::Expr(expr) => {
Self::build_calls_from_expr(caller_id, parent_path, expr, cx);
}
_ => {}
}
}
}
fn build_calls_from_expr(
caller_id: SymbolId,
parent_path: &str,
expr: &PureExpr,
cx: &mut CallsBuildContext<'_>,
) {
use ryo_source::pure::PureExpr;
match expr {
PureExpr::Call { func, args } => {
if let PureExpr::Path(path) = func.as_ref() {
if let Some(callee_id) = Self::resolve_type_reference(
parent_path,
path,
cx.symbol_ids,
cx.use_resolver,
cx.registry,
) {
cx.graph.add_edge(caller_id, callee_id, CodeEdgeV2::Calls);
}
}
for arg in args {
Self::build_calls_from_expr(caller_id, parent_path, arg, cx);
}
Self::build_calls_from_expr(caller_id, parent_path, func, cx);
}
PureExpr::MethodCall {
receiver,
method,
args,
..
} => {
let is_self_receiver = matches!(receiver.as_ref(), PureExpr::Path(name) if name == "self")
|| matches!(receiver.as_ref(), PureExpr::Field { expr, .. } if matches!(expr.as_ref(), PureExpr::Path(name) if name == "self"));
let mut resolved = false;
if is_self_receiver {
let sibling_path = format!("{}::{}", parent_path, method);
if let Some(&callee_id) = cx.symbol_ids.get(&sibling_path) {
if callee_id != caller_id {
cx.graph.add_edge(caller_id, callee_id, CodeEdgeV2::Calls);
resolved = true;
}
}
}
if !resolved {
if let Some(candidates) = cx.method_index.get(method.as_str()) {
let explicit_hint = extract_receiver_type_hint(receiver);
let type_hint = explicit_hint.or_else(|| {
if is_self_receiver {
extract_self_type_from_parent_path(parent_path)
} else {
None
}
});
if let Some(hint) = type_hint {
let filtered: Vec<_> = candidates
.iter()
.copied()
.filter(|&id| candidate_matches_type_hint(id, hint, cx.registry))
.collect();
if !filtered.is_empty() {
for callee_id in filtered {
if callee_id != caller_id {
cx.graph.add_edge(caller_id, callee_id, CodeEdgeV2::Calls);
}
}
} else {
let is_common = is_common_trait_method(method);
if !is_common || candidates.len() == 1 {
for &callee_id in candidates {
if callee_id != caller_id {
cx.graph.add_edge(
caller_id,
callee_id,
CodeEdgeV2::Calls,
);
}
}
}
}
} else {
let is_common = is_common_trait_method(method);
if !is_common || candidates.len() == 1 {
for &callee_id in candidates {
if callee_id != caller_id {
cx.graph.add_edge(caller_id, callee_id, CodeEdgeV2::Calls);
}
}
}
}
}
}
Self::build_calls_from_expr(caller_id, parent_path, receiver, cx);
for arg in args {
Self::build_calls_from_expr(caller_id, parent_path, arg, cx);
}
}
PureExpr::Block { block, .. } => {
Self::build_calls_from_block(caller_id, parent_path, block, cx);
}
PureExpr::If {
cond,
then_branch,
else_branch,
} => {
Self::build_calls_from_expr(caller_id, parent_path, cond, cx);
Self::build_calls_from_block(caller_id, parent_path, then_branch, cx);
if let Some(else_expr) = else_branch {
Self::build_calls_from_expr(caller_id, parent_path, else_expr, cx);
}
}
PureExpr::Match {
expr: match_expr,
arms,
} => {
Self::build_calls_from_expr(caller_id, parent_path, match_expr, cx);
for arm in arms {
Self::build_calls_from_expr(caller_id, parent_path, &arm.body, cx);
if let Some(ref guard) = arm.guard {
Self::build_calls_from_expr(caller_id, parent_path, guard, cx);
}
}
}
PureExpr::Loop { body: block, .. }
| PureExpr::Async { body: block, .. }
| PureExpr::Unsafe(block) => {
Self::build_calls_from_block(caller_id, parent_path, block, cx);
}
PureExpr::While { cond, body, .. } => {
Self::build_calls_from_expr(caller_id, parent_path, cond, cx);
Self::build_calls_from_block(caller_id, parent_path, body, cx);
}
PureExpr::For {
expr: iter_expr,
body,
..
} => {
Self::build_calls_from_expr(caller_id, parent_path, iter_expr, cx);
Self::build_calls_from_block(caller_id, parent_path, body, cx);
}
PureExpr::Closure { body, .. } => {
Self::build_calls_from_expr(caller_id, parent_path, body, cx);
}
PureExpr::Binary { left, right, .. } => {
Self::build_calls_from_expr(caller_id, parent_path, left, cx);
Self::build_calls_from_expr(caller_id, parent_path, right, cx);
}
PureExpr::Unary { expr: inner, .. }
| PureExpr::Field { expr: inner, .. }
| PureExpr::Await(inner)
| PureExpr::Try(inner)
| PureExpr::Ref { expr: inner, .. }
| PureExpr::Cast { expr: inner, .. } => {
Self::build_calls_from_expr(caller_id, parent_path, inner, cx);
}
PureExpr::Index { expr: arr, index } => {
Self::build_calls_from_expr(caller_id, parent_path, arr, cx);
Self::build_calls_from_expr(caller_id, parent_path, index, cx);
}
PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
for e in exprs {
Self::build_calls_from_expr(caller_id, parent_path, e, cx);
}
}
PureExpr::Struct { fields, .. } => {
for (_, e) in fields {
Self::build_calls_from_expr(caller_id, parent_path, e, cx);
}
}
PureExpr::Return(Some(inner))
| PureExpr::Break {
expr: Some(inner), ..
} => {
Self::build_calls_from_expr(caller_id, parent_path, inner, cx);
}
PureExpr::Range { start, end, .. } => {
if let Some(s) = start {
Self::build_calls_from_expr(caller_id, parent_path, s, cx);
}
if let Some(e) = end {
Self::build_calls_from_expr(caller_id, parent_path, e, cx);
}
}
PureExpr::Let { expr: inner, .. } => {
Self::build_calls_from_expr(caller_id, parent_path, inner, cx);
}
PureExpr::Repeat { expr: elem, len } => {
Self::build_calls_from_expr(caller_id, parent_path, elem, cx);
Self::build_calls_from_expr(caller_id, parent_path, len, cx);
}
_ => {}
}
}
fn resolve_type_reference(
parent_path: &str,
type_name: &str,
symbol_ids: &HashMap<String, SymbolId>,
use_resolver: &UseResolver,
registry: &SymbolRegistry,
) -> Option<SymbolId> {
let primitives = [
"i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize",
"f32", "f64", "bool", "char", "str", "Self",
];
if primitives.contains(&type_name) {
return None;
}
let module_path_str = strip_to_module_path(parent_path);
if let Ok(module_path) = SymbolPath::parse(&module_path_str) {
if let Some(id) = use_resolver.resolve(&module_path, type_name, registry) {
return Some(id);
}
}
if type_name.contains("::") {
if let Some(&id) = symbol_ids.get(type_name) {
return Some(id);
}
if let Some(split_pos) = type_name.find("::") {
let prefix = &type_name[..split_pos];
let suffix = &type_name[split_pos..]; if let Ok(module_path) = SymbolPath::parse(&module_path_str) {
if let Some(resolved_prefix_id) =
use_resolver.resolve(&module_path, prefix, registry)
{
let resolved_prefix_path = registry.path(resolved_prefix_id);
if let Some(full_path_str) = resolved_prefix_path {
let combined = format!("{}{}", full_path_str, suffix);
if let Some(&id) = symbol_ids.get(&combined) {
return Some(id);
}
}
}
}
}
}
let qualified = format!("{}::{}", parent_path, type_name);
if let Some(&id) = symbol_ids.get(&qualified) {
return Some(id);
}
let mut current_path = parent_path.to_string();
while let Some(parent) = get_parent_path(¤t_path) {
let qualified = format!("{}::{}", parent, type_name);
if let Some(&id) = symbol_ids.get(&qualified) {
return Some(id);
}
current_path = parent;
}
symbol_ids.get(type_name).copied()
}
pub fn registry(&self) -> &SymbolRegistry {
&self.registry
}
pub fn registry_mut(&mut self) -> &mut SymbolRegistry {
&mut self.registry
}
pub fn code_graph(&self) -> &CodeGraphV2 {
&self.code_graph
}
pub fn code_graph_mut(&mut self) -> &mut CodeGraphV2 {
&mut self.code_graph
}
pub fn typeflow_graph(&self) -> &TypeFlowGraphV2 {
&self.typeflow_graph
}
pub fn workspace_root(&self) -> &Path {
&self.workspace_root
}
pub fn file(&self, path: &WorkspaceFilePath) -> Option<&PureFile> {
self.files.get(path).map(|arc| arc.as_ref())
}
pub fn file_mut(&mut self, path: &WorkspaceFilePath) -> Option<&mut PureFile> {
self.files.get_mut(path).map(Arc::make_mut)
}
pub fn files(&self) -> &ImHashMap<WorkspaceFilePath, Arc<PureFile>> {
&self.files
}
pub fn files_mut(&mut self) -> &mut ImHashMap<WorkspaceFilePath, Arc<PureFile>> {
&mut self.files
}
pub fn original(&self, path: &WorkspaceFilePath) -> Option<&String> {
self.original.get(path)
}
pub fn file_count(&self) -> usize {
self.files.len()
}
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
pub fn detail_store(&self) -> &DetailStore {
&self.detail_store
}
pub fn detail_store_mut(&mut self) -> &mut DetailStore {
&mut self.detail_store
}
pub fn commit_changes(&mut self, updates: &RegistryUpdateBatch) {
let affected_ids: Vec<SymbolId> =
updates.into_iter().filter_map(|u| u.target_id()).collect();
for update in updates {
let _old_kind = match update {
RegistryUpdate::UpdateKind { id, .. } => self.registry.kind(*id),
_ => None,
};
if let Err(e) = update.clone().apply(&mut self.registry) {
eprintln!("Warning: Failed to apply registry update: {:?}", e);
continue;
}
match update {
RegistryUpdate::Add { path, kind, .. } => {
if let Some(id) = self.registry.lookup(path) {
self.code_graph.add_node(id);
self.code_graph.add_to_kind_index(id, *kind);
}
}
RegistryUpdate::Remove { id } => {
self.code_graph.remove_node(*id);
}
RegistryUpdate::UpdateKind { id, new_kind } => {
if self.code_graph.contains(*id) {
self.code_graph.add_to_kind_index(*id, *new_kind);
}
}
RegistryUpdate::Rename { .. }
| RegistryUpdate::UpdateSpan { .. }
| RegistryUpdate::UpdateVisibility { .. } => {}
}
}
let crate_name = self
.files
.keys()
.next()
.and_then(|path| SymbolPathResolver::from_workspace_path(path).ok())
.map(|r| r.crate_name().to_string())
.unwrap_or_else(|| "crate".to_string());
self.detail_store.rebuild_affected_workspace(
&affected_ids,
&self.registry,
&self.files,
&crate_name,
);
}
pub fn rebuild_edges_for_files(&mut self, file_paths: &[WorkspaceFilePath]) {
let mut symbol_ids: HashMap<String, SymbolId> = HashMap::new();
for (id, _) in self.registry.iter() {
if let Some(path) = self.registry.resolve(id) {
symbol_ids.insert(path.to_string(), id);
}
}
for file_path in file_paths {
for (id, _) in self.registry.iter() {
if let Some(span) = self.registry.span(id) {
if &span.file == file_path {
self.code_graph.clear_outgoing_edges(id);
}
}
}
if let Some(file) = self.files.get(file_path) {
let file_crate_name = file_path.crate_name().as_str();
let resolver = SymbolPathResolver::new(file_crate_name);
let mod_path = resolver.module_path_str(file_path);
let method_index = build_method_name_index(&symbol_ids, &self.registry);
Self::build_edges_from_items(
&mod_path,
&file.items,
&symbol_ids,
&self.use_resolver,
&self.registry,
&mut self.code_graph,
&method_index,
);
}
}
}
pub fn rebuild_edges_for_symbols(&mut self, affected_ids: &[SymbolId]) {
if affected_ids.is_empty() {
return;
}
let mut symbol_ids: HashMap<String, SymbolId> = HashMap::new();
for (id, _) in self.registry.iter() {
if let Some(path) = self.registry.resolve(id) {
symbol_ids.insert(path.to_string(), id);
}
}
for &id in affected_ids {
self.code_graph.clear_outgoing_edges(id);
}
for &id in affected_ids {
let parent_path = match self.registry.resolve(id) {
Some(path) => {
let path_str = path.to_string();
path_str
.rsplit_once("::")
.map(|(parent, _)| parent.to_string())
.unwrap_or_else(|| path_str.clone())
}
None => continue,
};
if let Some(item) = self.ast_registry.get(id) {
let method_index = build_method_name_index(&symbol_ids, &self.registry);
Self::build_edges_from_item(
&parent_path,
item,
&symbol_ids,
&self.use_resolver,
&self.registry,
&mut self.code_graph,
&method_index,
);
}
}
}
pub fn get_symbols_in_files(&self, file_paths: &[WorkspaceFilePath]) -> Vec<SymbolId> {
let file_set: std::collections::HashSet<_> = file_paths.iter().collect();
let mut symbols = Vec::new();
for (id, _) in self.registry.iter() {
if let Some(span) = self.registry.span(id) {
if file_set.contains(&span.file) {
symbols.push(id);
}
}
}
symbols
}
pub fn rebuild_after_mutation(&mut self, modified_files: &[WorkspaceFilePath]) {
let affected_symbols = self.get_symbols_in_files(modified_files);
self.rebuild_after_mutation_by_symbols(&affected_symbols);
}
pub fn rebuild_after_mutation_by_symbols(&mut self, affected_ids: &[SymbolId]) {
if affected_ids.is_empty() {
return;
}
let crate_name = self
.files
.keys()
.next()
.map(|r| r.crate_name().to_string())
.unwrap_or_else(|| "unknown".to_string());
let mut file_set = std::collections::HashSet::new();
for &id in affected_ids {
if let Some(span) = self.registry.span(id) {
file_set.insert(span.file.clone());
}
}
let _modified_files: Vec<_> = file_set.into_iter().collect();
self.rebuild_edges_for_symbols(affected_ids);
self.typeflow_graph =
TypeFlowBuilderV2::build_from_ast_registry(&self.registry, &self.ast_registry);
self.dataflow_graph.clear_for_symbols(affected_ids);
DataFlowBuilderWorkspace::new(&self.registry, &self.files, &crate_name)
.build_incremental_by_symbols(
&mut self.dataflow_graph,
&self.ast_registry,
affected_ids,
);
self.detail_store
.rebuild_for_symbols(affected_ids, &self.ast_registry);
self.derive_index.rebuild_for_symbols(
affected_ids,
&self.ast_registry,
&self.code_graph,
&self.typeflow_graph,
&self.registry,
);
}
pub fn fork(&self) -> ExecutionContext<'_> {
ExecutionContext {
workspace_root: &self.workspace_root,
registry: &self.registry,
graph: &self.code_graph,
files: self.files.clone(), }
}
pub fn fork_rebuild(&self) -> Self {
let files: HashMap<WorkspaceFilePath, PureFile> = self
.files
.iter()
.map(|(path, arc)| (path.clone(), (**arc).clone()))
.collect();
Self::build_from_workspace_files(
files,
self.workspace_root.clone(),
AnalysisConfig::default(),
)
.expect("fork_rebuild: source generation failed")
}
pub fn fork_clone(&self) -> Self {
Self {
workspace_root: self.workspace_root.clone(),
registry: self.registry.clone(),
code_graph: self.code_graph.clone(),
typeflow_graph: self.typeflow_graph.clone(),
dataflow_graph: self.dataflow_graph.clone(),
detail_store: self.detail_store.clone(),
ast_registry: self.ast_registry.clone(),
files: self.files.clone(), original: self.original.clone(),
use_resolver: self.use_resolver.clone(),
#[cfg(feature = "literal-search")]
literal_index: None,
derive_index: self.derive_index.clone(),
}
}
pub fn symbol_count(&self) -> usize {
self.registry.len()
}
pub fn snapshot_symbols(&self, symbols: &[SymbolId]) -> ContextSnapshot {
let ast_items: HashMap<SymbolId, PureItem> = symbols
.iter()
.filter_map(|&id| self.ast_registry.get(id).map(|item| (id, item.clone())))
.collect();
ContextSnapshot { ast_items }
}
pub fn rollback(&mut self, snapshot: ContextSnapshot, affected_ids: &[SymbolId]) {
for (id, item) in snapshot.ast_items {
self.ast_registry.set(id, item);
}
if !affected_ids.is_empty() {
self.rebuild_after_mutation_by_symbols(affected_ids);
}
}
}
#[derive(Debug, Clone)]
pub struct ContextSnapshot {
pub ast_items: HashMap<SymbolId, PureItem>,
}
pub struct ExecutionContext<'a> {
pub workspace_root: &'a Path,
pub registry: &'a SymbolRegistry,
pub graph: &'a CodeGraphV2,
pub files: ImHashMap<WorkspaceFilePath, Arc<PureFile>>,
}
impl<'a> ExecutionContext<'a> {
pub fn file(&self, path: &WorkspaceFilePath) -> Option<&PureFile> {
self.files.get(path).map(|arc| arc.as_ref())
}
pub fn file_mut(&mut self, path: &WorkspaceFilePath) -> Option<&mut PureFile> {
self.files.get_mut(path).map(Arc::make_mut)
}
pub fn has_file(&self, path: &WorkspaceFilePath) -> bool {
self.files.contains_key(path)
}
pub fn file_count(&self) -> usize {
self.files.len()
}
}
fn pure_vis_to_visibility(pure_vis: &PureVis) -> Visibility {
match pure_vis {
PureVis::Public => Visibility::Public,
PureVis::Crate => Visibility::Crate,
PureVis::Super => Visibility::Super,
PureVis::Private => Visibility::Private,
PureVis::In(path) => {
SymbolPath::parse(path)
.map(|p| Visibility::Restricted(Box::new(p)))
.unwrap_or(Visibility::Private)
}
}
}
fn get_parent_path(path: &str) -> Option<String> {
let parts: Vec<&str> = path.rsplitn(2, "::").collect();
if parts.len() == 2 {
Some(parts[1].to_string())
} else {
None
}
}
fn strip_to_module_path(path: &str) -> String {
if let Some(impl_pos) = path.find("::<impl ") {
return path[..impl_pos].to_string();
}
path.to_string()
}
fn is_common_trait_method(method: &str) -> bool {
matches!(
method,
"new"
| "default"
| "fmt"
| "clone"
| "eq"
| "ne"
| "cmp"
| "partial_cmp"
| "hash"
| "from"
| "into"
| "try_from"
| "try_into"
| "as_ref"
| "as_mut"
| "deref"
| "deref_mut"
| "drop"
| "next"
| "into_iter"
| "iter"
| "len"
| "is_empty"
)
}
fn extract_receiver_type_hint(receiver: &PureExpr) -> Option<&str> {
match receiver {
PureExpr::Call { func, .. } => {
if let PureExpr::Path(path) = func.as_ref() {
let segments: Vec<&str> = path.rsplitn(2, "::").collect();
if segments.len() == 2 {
return Some(segments[1].rsplit("::").next().unwrap_or(segments[1]));
}
}
None
}
PureExpr::Struct { path, .. } => path.rsplit("::").next(),
_ => None,
}
}
fn extract_self_type_from_parent_path(parent_path: &str) -> Option<&str> {
if let Some(impl_start) = parent_path.rfind("::<impl ") {
let impl_segment = &parent_path[impl_start + 2..]; let inner = impl_segment.strip_prefix("<impl ")?.strip_suffix('>')?;
let self_ty = if let Some(pos) = inner.find(" for ") {
&inner[pos + 5..]
} else {
inner
};
let base = self_ty.split('<').next().unwrap_or(self_ty).trim();
if !base.is_empty() {
return Some(base);
}
}
parent_path.rsplit("::").next()
}
fn candidate_matches_type_hint(
candidate_id: SymbolId,
type_hint: &str,
registry: &SymbolRegistry,
) -> bool {
if let Some(path) = registry.path(candidate_id) {
for segment in path.segment_refs() {
if segment.is_impl() {
if let Some(self_ty) = segment.impl_self_ty() {
let base = self_ty.split('<').next().unwrap_or(self_ty).trim();
let base_name = base.rsplit("::").next().unwrap_or(base);
return base_name == type_hint;
}
}
}
let segments: Vec<&str> = path.segments().collect();
if segments.len() >= 2 {
let parent_name = segments[segments.len() - 2];
return parent_name == type_hint;
}
}
false
}
type MethodNameIndex = HashMap<String, Vec<SymbolId>>;
struct CallsBuildContext<'a> {
symbol_ids: &'a HashMap<String, SymbolId>,
use_resolver: &'a UseResolver,
registry: &'a SymbolRegistry,
graph: &'a mut CodeGraphV2,
method_index: &'a MethodNameIndex,
}
fn build_method_name_index(
symbol_ids: &HashMap<String, SymbolId>,
registry: &SymbolRegistry,
) -> MethodNameIndex {
let mut index: MethodNameIndex = HashMap::new();
for (path, &id) in symbol_ids {
let kind = registry.kind(id);
let is_callable = matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Method));
if !is_callable {
continue;
}
if let Some(method_name) = path.rsplit("::").next() {
index.entry(method_name.to_string()).or_default().push(id);
}
}
index
}
#[cfg(test)]
mod tests {
use super::*;
use ryo_symbol::{TestWorkspace, WorkspaceFilePath};
fn build_context_from_workspace(
workspace: &TestWorkspace,
crate_name: &str,
) -> AnalysisContext {
let files: HashMap<WorkspaceFilePath, PureFile> = workspace
.files_in_crate(crate_name)
.into_iter()
.filter_map(|path| {
let abs = path.to_absolute();
let content = std::fs::read_to_string(&abs).ok()?;
let file = PureFile::from_source(&content).ok()?;
Some((path, file))
})
.collect();
let workspace_root = Arc::from(workspace.workspace_root());
AnalysisContext::build_from_workspace_files(
files,
workspace_root,
AnalysisConfig::default(),
)
.expect("build_from_workspace_files failed in test helper")
}
#[test]
fn test_get_parent_path() {
assert_eq!(
get_parent_path("mylib::handlers::handle"),
Some("mylib::handlers".to_string())
);
assert_eq!(get_parent_path("mylib::foo"), Some("mylib".to_string()));
assert_eq!(get_parent_path("mylib"), None);
}
#[test]
fn test_empty_context() {
let workspace = TestWorkspace::builder()
.crate_with_source("test_crate", "src/lib.rs", "")
.build();
let ctx = build_context_from_workspace(&workspace, "test_crate");
assert_eq!(ctx.file_count(), 1);
assert!(ctx.code_graph.node_count() <= 1);
}
#[test]
fn test_context_with_files() {
let workspace = TestWorkspace::builder()
.crate_with_source("mylib", "src/lib.rs", "pub fn foo() {}")
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
assert_eq!(ctx.file_count(), 1);
let workspace_path = ctx.files.keys().next().expect("should have one file");
assert!(ctx.file(workspace_path).is_some());
assert!(ctx.original(workspace_path).is_some());
let foo_path = SymbolPath::parse("mylib::foo").unwrap();
assert!(
ctx.registry.lookup(&foo_path).is_some(),
"foo should be registered"
);
}
#[test]
fn test_fork_creates_independent_files() {
let workspace = TestWorkspace::builder()
.crate_with_source("mylib", "src/lib.rs", "pub fn foo() {}")
.build();
let original = build_context_from_workspace(&workspace, "mylib");
let workspace_path = original.files.keys().next().expect("file exists").clone();
let mut forked = original.fork();
forked.files.insert(
workspace_path.clone(),
Arc::new(PureFile::from_source("pub fn bar() {}").unwrap()),
);
let original_source = original.file(&workspace_path).unwrap().to_source().unwrap();
let forked_source = forked.file(&workspace_path).unwrap().to_source().unwrap();
assert!(original_source.contains("foo"), "Original should have foo");
assert!(forked_source.contains("bar"), "Forked should have bar");
assert!(
!original_source.contains("bar"),
"Original should not have bar"
);
}
#[test]
fn test_fork_shares_registry() {
let workspace = TestWorkspace::builder()
.crate_with_source("mylib", "src/lib.rs", "pub struct Foo {}")
.build();
let original = build_context_from_workspace(&workspace, "mylib");
let forked = original.fork();
assert!(std::ptr::eq(
&original.registry as *const _,
forked.registry as *const _
));
assert_eq!(original.registry.len(), forked.registry.len());
}
#[test]
fn test_fork_rebuild_creates_independent_context() {
let workspace = TestWorkspace::builder()
.crate_with_source("mylib", "src/lib.rs", "pub fn foo() {}")
.build();
let original = build_context_from_workspace(&workspace, "mylib");
let rebuilt = original.fork_rebuild();
assert!(!std::ptr::eq(
&original.registry as *const _,
&rebuilt.registry as *const _
));
assert_eq!(original.registry.len(), rebuilt.registry.len());
}
#[test]
fn test_execution_context_file_access() {
let workspace = TestWorkspace::builder()
.crate_with_source("mylib", "src/lib.rs", "pub fn test_fn() {}")
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let workspace_path = ctx.files.keys().next().expect("file exists").clone();
let exec_ctx = ctx.fork();
assert!(exec_ctx.has_file(&workspace_path));
assert_eq!(exec_ctx.file_count(), 1);
let file = exec_ctx.file(&workspace_path).unwrap();
assert!(file.to_source().unwrap().contains("test_fn"));
}
#[test]
fn test_commit_changes_add_symbol() {
use crate::SymbolKind;
use crate::{FileSpan, RegistryUpdate, RegistryUpdateBatch, SymbolPath};
let workspace = TestWorkspace::builder()
.crate_with_source("testcrate", "src/lib.rs", "")
.build();
let mut ctx = build_context_from_workspace(&workspace, "testcrate");
let mut batch = RegistryUpdateBatch::new();
let path = SymbolPath::parse("testcrate::NewStruct").unwrap();
let dummy_span = FileSpan::new(
WorkspaceFilePath::new_for_test("src/test.rs", "/project", "testcrate"),
0,
100,
);
batch.push(RegistryUpdate::Add {
path: path.clone(),
kind: SymbolKind::Struct,
span: dummy_span,
});
let initial_nodes = ctx.code_graph.node_count();
ctx.commit_changes(&batch);
let id = ctx.registry.lookup(&path);
assert!(id.is_some(), "Symbol should be registered");
assert_eq!(
ctx.code_graph.node_count(),
initial_nodes + 1,
"Graph should have one more node"
);
}
#[test]
fn test_commit_changes_remove_symbol() {
use crate::{RegistryUpdate, RegistryUpdateBatch, SymbolPath};
let workspace = TestWorkspace::builder()
.crate_with_source("mylib", "src/lib.rs", "pub struct Foo {}")
.build();
let mut ctx = build_context_from_workspace(&workspace, "mylib");
let foo_path = SymbolPath::parse("mylib::Foo").unwrap();
let foo_id = ctx.registry.lookup(&foo_path);
assert!(foo_id.is_some(), "Foo should exist initially");
let foo_id = foo_id.unwrap();
let initial_nodes = ctx.code_graph.node_count();
let mut batch = RegistryUpdateBatch::new();
batch.push(RegistryUpdate::Remove { id: foo_id });
ctx.commit_changes(&batch);
let foo_path_check = SymbolPath::parse("mylib::Foo").unwrap();
assert!(
ctx.registry.lookup(&foo_path_check).is_none(),
"Symbol should be removed from registry"
);
assert_eq!(
ctx.code_graph.node_count(),
initial_nodes - 1,
"Graph should have one fewer node"
);
}
#[test]
fn test_commit_changes_update_kind() {
use crate::SymbolKind;
use crate::{RegistryUpdate, RegistryUpdateBatch, SymbolPath};
let workspace = TestWorkspace::builder()
.crate_with_source("mylib", "src/lib.rs", "pub struct Foo {}")
.build();
let mut ctx = build_context_from_workspace(&workspace, "mylib");
let foo_path = SymbolPath::parse("mylib::Foo").unwrap();
let foo_id = ctx.registry.lookup(&foo_path).expect("Foo should exist");
assert_eq!(ctx.registry.kind(foo_id), Some(SymbolKind::Struct));
let initial_nodes = ctx.code_graph.node_count();
let mut batch = RegistryUpdateBatch::new();
batch.push(RegistryUpdate::UpdateKind {
id: foo_id,
new_kind: SymbolKind::Enum,
});
ctx.commit_changes(&batch);
assert_eq!(
ctx.registry.kind(foo_id),
Some(SymbolKind::Enum),
"Kind should be updated to Enum"
);
assert_eq!(
ctx.code_graph.node_count(),
initial_nodes,
"Node count should not change"
);
}
#[test]
fn test_commit_changes_batch_multiple_updates() {
use crate::SymbolKind;
use crate::{FileSpan, RegistryUpdate, RegistryUpdateBatch, SymbolPath};
let workspace = TestWorkspace::builder()
.crate_with_source("testcrate", "src/lib.rs", "")
.build();
let mut ctx = build_context_from_workspace(&workspace, "testcrate");
let mut batch = RegistryUpdateBatch::new();
for name in ["Alpha", "Beta", "Gamma"] {
let path = SymbolPath::parse(&format!("testcrate::{}", name)).unwrap();
let dummy_span = FileSpan::new(
WorkspaceFilePath::new_for_test("src/test.rs", "/project", "testcrate"),
0,
100,
);
batch.push(RegistryUpdate::Add {
path,
kind: SymbolKind::Struct,
span: dummy_span,
});
}
let initial_nodes = ctx.code_graph.node_count();
ctx.commit_changes(&batch);
assert_eq!(
ctx.code_graph.node_count(),
initial_nodes + 3,
"Graph should have 3 more nodes"
);
for name in ["Alpha", "Beta", "Gamma"] {
let path = SymbolPath::parse(&format!("testcrate::{}", name)).unwrap();
assert!(
ctx.registry.lookup(&path).is_some(),
"{} should be registered",
name
);
}
}
#[test]
fn test_implements_edge() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub trait MyTrait {
fn do_something(&self);
}
pub struct MyStruct {}
impl MyTrait for MyStruct {
fn do_something(&self) {}
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let trait_path = SymbolPath::parse("mylib::MyTrait").unwrap();
let trait_id = ctx.registry.lookup(&trait_path);
assert!(trait_id.is_some(), "Trait should be registered");
let struct_path = SymbolPath::parse("mylib::MyStruct").unwrap();
let struct_id = ctx.registry.lookup(&struct_path);
assert!(struct_id.is_some(), "Struct should be registered");
let has_implementors = ctx
.code_graph
.implementors_of(trait_id.unwrap())
.next()
.is_some();
assert!(
has_implementors,
"MyTrait should have at least one implementor"
);
}
#[test]
fn test_uses_edge_from_struct_field() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub struct Inner {}
pub struct Outer {
pub inner: Inner,
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let outer_path = SymbolPath::parse("mylib::Outer").unwrap();
let inner_path = SymbolPath::parse("mylib::Inner").unwrap();
let outer_id = ctx.registry.lookup(&outer_path);
let inner_id = ctx.registry.lookup(&inner_path);
assert!(outer_id.is_some(), "Outer should be registered");
assert!(inner_id.is_some(), "Inner should be registered");
let outer = outer_id.unwrap();
let is_user = ctx
.typeflow_graph
.type_users(inner_id.unwrap())
.any(|id| id == outer);
assert!(is_user, "Outer should use Inner");
}
#[test]
fn test_uses_edge_from_fn_params() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub struct Config {}
pub fn process(config: Config) {}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let fn_path = SymbolPath::parse("mylib::process").unwrap();
let config_path = SymbolPath::parse("mylib::Config").unwrap();
let fn_id = ctx.registry.lookup(&fn_path);
let config_id = ctx.registry.lookup(&config_path);
assert!(fn_id.is_some(), "Function should be registered");
assert!(config_id.is_some(), "Config should be registered");
let fn_sym = fn_id.unwrap();
let config_sym = config_id.unwrap();
let is_user = ctx
.typeflow_graph
.type_users(config_sym)
.any(|id| id == fn_sym);
assert!(is_user, "process should use Config");
}
#[test]
fn test_calls_edge() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub fn helper() {}
pub fn main_fn() {
helper();
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let main_path = SymbolPath::parse("mylib::main_fn").unwrap();
let helper_path = SymbolPath::parse("mylib::helper").unwrap();
let main_id = ctx.registry.lookup(&main_path);
let helper_id = ctx.registry.lookup(&helper_path);
assert!(main_id.is_some(), "main_fn should be registered");
assert!(helper_id.is_some(), "helper should be registered");
let main = main_id.unwrap();
let is_caller = ctx
.code_graph
.callers_of(helper_id.unwrap())
.any(|id| id == main);
assert!(is_caller, "main_fn should call helper");
}
#[test]
fn test_fork_clone_creates_independent_context() {
let workspace = TestWorkspace::builder()
.crate_with_source("mylib", "src/lib.rs", "pub fn foo() {}")
.build();
let original = build_context_from_workspace(&workspace, "mylib");
let cloned = original.fork_clone();
assert!(!std::ptr::eq(
&original.registry as *const _,
&cloned.registry as *const _
));
assert_eq!(original.registry.len(), cloned.registry.len());
assert_eq!(
original.code_graph.node_count(),
cloned.code_graph.node_count()
);
}
#[test]
fn test_fork_clone_is_faster_than_rebuild() {
use std::time::Instant;
let workspace = TestWorkspace::builder()
.crate_with_source("mylib", "src/lib.rs", "pub mod a; pub mod b;")
.crate_with_source("mylib", "src/a.rs", "pub struct A { x: i32 }")
.crate_with_source("mylib", "src/b.rs", "pub struct B { y: String }")
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let _ = ctx.fork_clone();
let _ = ctx.fork_rebuild();
let t0 = Instant::now();
for _ in 0..10 {
let _ = ctx.fork_clone();
}
let clone_time = t0.elapsed();
let t1 = Instant::now();
for _ in 0..10 {
let _ = ctx.fork_rebuild();
}
let rebuild_time = t1.elapsed();
eprintln!(
"fork_clone: {:?} avg, fork_rebuild: {:?} avg, speedup: {:.1}x",
clone_time / 10,
rebuild_time / 10,
rebuild_time.as_nanos() as f64 / clone_time.as_nanos() as f64
);
assert!(
clone_time < rebuild_time,
"fork_clone ({:?}) should be faster than fork_rebuild ({:?})",
clone_time,
rebuild_time
);
}
#[test]
fn test_uuid_persistence_save_and_load() {
use ryo_symbol::SymbolPath;
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let workspace_root: Arc<Path> = Arc::from(temp_dir.path());
std::fs::create_dir_all(temp_dir.path().join(".ryo")).unwrap();
let wfp = WorkspaceFilePath::new_for_test("src/lib.rs", temp_dir.path(), "test_crate");
let source = r#"pub struct TestStruct { pub field: i32 }"#;
let file = PureFile::from_source(source).expect("Failed to parse");
let mut files = HashMap::new();
files.insert(wfp.clone(), file);
let ctx1 = AnalysisContext::build_from_workspace_files(
files.clone(),
workspace_root.clone(),
AnalysisConfig::default(),
)
.unwrap();
let path = SymbolPath::parse("test_crate::TestStruct").unwrap();
let id1 = ctx1
.registry
.lookup(&path)
.expect("TestStruct should be registered");
let uuid1 = ctx1.registry.uuid(id1).expect("Should have UUID");
ctx1.save_uuid_mappings().expect("Failed to save");
let uuid_file = temp_dir.path().join(".ryo/uuid-mapping.json");
assert!(uuid_file.exists(), "UUID file should exist");
let mappings =
AnalysisContext::load_uuid_mappings(temp_dir.path()).expect("Should load mappings");
let config = AnalysisConfig::default().with_uuid_mappings(mappings);
let ctx2 =
AnalysisContext::build_from_workspace_files(files, workspace_root, config).unwrap();
let id2 = ctx2
.registry
.lookup(&path)
.expect("TestStruct should exist");
let uuid2 = ctx2.registry.uuid(id2).expect("Should have UUID");
assert_eq!(uuid1, uuid2, "UUID should be preserved across rebuilds");
}
#[test]
fn test_uuid_persistence_new_symbol_gets_new_uuid() {
use ryo_symbol::SymbolPath;
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let workspace_root: Arc<Path> = Arc::from(temp_dir.path());
std::fs::create_dir_all(temp_dir.path().join(".ryo")).unwrap();
let wfp = WorkspaceFilePath::new_for_test("src/lib.rs", temp_dir.path(), "test_crate");
let source1 = "pub struct First;";
let file1 = PureFile::from_source(source1).unwrap();
let mut files1 = HashMap::new();
files1.insert(wfp.clone(), file1);
let ctx1 = AnalysisContext::build_from_workspace_files(
files1,
workspace_root.clone(),
AnalysisConfig::default(),
)
.unwrap();
let first_path = SymbolPath::parse("test_crate::First").unwrap();
let first_id = ctx1.registry.lookup(&first_path).unwrap();
let first_uuid = ctx1.registry.uuid(first_id).unwrap();
ctx1.save_uuid_mappings().unwrap();
let source2 = "pub struct First;\npub struct Second;";
let file2 = PureFile::from_source(source2).unwrap();
let mut files2 = HashMap::new();
files2.insert(wfp, file2);
let mappings = AnalysisContext::load_uuid_mappings(temp_dir.path()).unwrap();
let config = AnalysisConfig::default().with_uuid_mappings(mappings);
let ctx2 =
AnalysisContext::build_from_workspace_files(files2, workspace_root, config).unwrap();
let first_id2 = ctx2.registry.lookup(&first_path).unwrap();
let first_uuid2 = ctx2.registry.uuid(first_id2).unwrap();
assert_eq!(first_uuid, first_uuid2, "First UUID preserved");
let second_path = SymbolPath::parse("test_crate::Second").unwrap();
let second_id = ctx2.registry.lookup(&second_path).unwrap();
let second_uuid = ctx2.registry.uuid(second_id).unwrap();
assert_ne!(first_uuid, second_uuid, "Second has different UUID");
}
#[test]
fn test_impl_methods_registered_directly_on_struct() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub struct TodoList {
items: Vec<String>,
}
impl TodoList {
pub fn new() -> Self {
Self { items: vec![] }
}
pub fn add(&mut self, item: String) {
self.items.push(item);
}
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let new_path = SymbolPath::parse("mylib::TodoList::new").unwrap();
let add_path = SymbolPath::parse("mylib::TodoList::add").unwrap();
assert!(
ctx.registry.lookup(&new_path).is_some(),
"new method should be registered as TodoList::new"
);
assert!(
ctx.registry.lookup(&add_path).is_some(),
"add method should be registered as TodoList::add"
);
let impl_blocks: Vec<_> = ctx
.registry
.iter()
.filter(|(_, path)| path.segment_refs().iter().any(|s| s.is_impl()))
.collect();
assert_eq!(
impl_blocks.len(),
1,
"impl block should be registered as <impl TodoList>"
);
assert!(
impl_blocks[0].1.to_string() == "mylib::<impl TodoList>",
"impl block path should be mylib::<impl TodoList>, found: {}",
impl_blocks[0].1
);
}
#[test]
fn test_multiple_impl_blocks_methods_merged_on_struct() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub struct TodoList;
impl TodoList {
pub fn new() -> Self { Self }
}
impl TodoList {
pub fn add(&mut self, item: String) {}
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let new_path = SymbolPath::parse("mylib::TodoList::new").unwrap();
let add_path = SymbolPath::parse("mylib::TodoList::add").unwrap();
assert!(
ctx.registry.lookup(&new_path).is_some(),
"new from first impl should be registered"
);
assert!(
ctx.registry.lookup(&add_path).is_some(),
"add from second impl should be registered"
);
let impl_blocks: Vec<_> = ctx
.registry
.iter()
.filter(|(_, path)| path.segment_refs().iter().any(|s| s.is_impl()))
.collect();
assert_eq!(
impl_blocks.len(),
1,
"merged impl block should be registered"
);
assert!(
impl_blocks[0].1.to_string() == "mylib::<impl TodoList>",
"impl block path should be mylib::<impl TodoList>"
);
}
#[test]
fn test_calls_edge_associated_fn_cross_module() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub mod types;
pub mod handler;
"#,
)
.crate_with_source(
"mylib",
"src/types.rs",
r#"
pub struct Router {}
impl Router {
pub fn new() -> Self { Router {} }
}
"#,
)
.crate_with_source(
"mylib",
"src/handler.rs",
r#"
use crate::types::Router;
pub fn setup() {
let _r = Router::new();
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let setup_path = SymbolPath::parse("mylib::handler::setup").unwrap();
let new_path = SymbolPath::parse("mylib::types::Router::new").unwrap();
let setup_id = ctx
.registry
.lookup(&setup_path)
.expect("setup should be registered");
let new_id = ctx
.registry
.lookup(&new_path)
.expect("Router::new should be registered");
let callees: Vec<_> = ctx.code_graph.callees_of(setup_id).collect();
assert!(
callees.contains(&new_id),
"setup should call Router::new, but callees = {:?}",
callees
);
}
#[test]
fn test_calls_edge_free_fn_cross_module_via_use() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub mod utils;
pub mod handler;
"#,
)
.crate_with_source(
"mylib",
"src/utils.rs",
r#"
pub fn helper() {}
"#,
)
.crate_with_source(
"mylib",
"src/handler.rs",
r#"
use crate::utils::helper;
pub fn process() {
helper();
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let process_path = SymbolPath::parse("mylib::handler::process").unwrap();
let helper_path = SymbolPath::parse("mylib::utils::helper").unwrap();
let process_id = ctx
.registry
.lookup(&process_path)
.expect("process should be registered");
let helper_id = ctx
.registry
.lookup(&helper_path)
.expect("helper should be registered");
let is_callee = ctx
.code_graph
.callees_of(process_id)
.any(|id| id == helper_id);
assert!(is_callee, "process should call helper via use import");
}
#[test]
fn test_calls_edge_associated_fn_new_not_filtered() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub struct Builder {}
impl Builder {
pub fn new() -> Self { Builder {} }
pub fn build(self) -> String { String::new() }
}
pub fn create() -> String {
let b = Builder::new();
b.build()
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let create_path = SymbolPath::parse("mylib::create").unwrap();
let new_path = SymbolPath::parse("mylib::Builder::new").unwrap();
let create_id = ctx
.registry
.lookup(&create_path)
.expect("create should be registered");
let new_id = ctx
.registry
.lookup(&new_path)
.expect("Builder::new should be registered");
let callees: Vec<_> = ctx.code_graph.callees_of(create_id).collect();
assert!(
callees.contains(&new_id),
"create should call Builder::new (associated fn, not filtered by is_common_trait_method), callees = {:?}",
callees
);
}
#[test]
fn test_calls_edge_qualified_path_call() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub mod utils {
pub fn do_work() {}
}
pub fn caller() {
utils::do_work();
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let caller_path = SymbolPath::parse("mylib::caller").unwrap();
let do_work_path = SymbolPath::parse("mylib::utils::do_work").unwrap();
let caller_id = ctx
.registry
.lookup(&caller_path)
.expect("caller should be registered");
let do_work_id = ctx
.registry
.lookup(&do_work_path)
.expect("do_work should be registered");
let is_callee = ctx
.code_graph
.callees_of(caller_id)
.any(|id| id == do_work_id);
assert!(
is_callee,
"caller should call utils::do_work via qualified path"
);
}
#[test]
fn test_calls_edge_method_common_name_single_candidate() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub struct Data {}
impl Data {
pub fn clone(&self) -> Self { Data {} }
}
pub fn process(d: Data) -> Data {
d.clone()
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let process_path = SymbolPath::parse("mylib::process").unwrap();
let clone_path = SymbolPath::parse("mylib::Data::clone").unwrap();
let process_id = ctx
.registry
.lookup(&process_path)
.expect("process should be registered");
let clone_id = ctx
.registry
.lookup(&clone_path)
.expect("Data::clone should be registered");
let callees: Vec<_> = ctx.code_graph.callees_of(process_id).collect();
assert!(
callees.contains(&clone_id),
"process should call Data::clone even though 'clone' is in common trait list (single candidate), callees = {:?}",
callees
);
}
#[test]
fn test_calls_edge_method_many_candidates_not_blocked() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub struct A {} impl A { pub fn handle(&self) {} }
pub struct B {} impl B { pub fn handle(&self) {} }
pub struct C {} impl C { pub fn handle(&self) {} }
pub struct D {} impl D { pub fn handle(&self) {} }
pub struct E {} impl E { pub fn handle(&self) {} }
pub struct F {} impl F { pub fn handle(&self) {} }
pub struct G {} impl G { pub fn handle(&self) {} }
pub struct H {} impl H { pub fn handle(&self) {} }
pub struct I {} impl I { pub fn handle(&self) {} }
pub struct J {} impl J { pub fn handle(&self) {} }
pub struct K {} impl K { pub fn handle(&self) {} }
pub struct L {} impl L { pub fn handle(&self) {} }
pub fn caller(a: A) {
a.handle();
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let caller_path = SymbolPath::parse("mylib::caller").unwrap();
let caller_id = ctx
.registry
.lookup(&caller_path)
.expect("caller should be registered");
let callees: Vec<_> = ctx.code_graph.callees_of(caller_id).collect();
assert!(
!callees.is_empty(),
"caller should have callees for 'handle' method (12 candidates should not be blocked), callees = {:?}",
callees
);
}
#[test]
fn test_calls_edge_trait_impl_method_has_callees() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub fn helper() -> i32 { 42 }
pub trait MyTrait {
fn do_work(&self) -> i32;
}
pub struct Foo;
impl MyTrait for Foo {
fn do_work(&self) -> i32 {
helper()
}
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let helper_path = SymbolPath::parse("mylib::helper").unwrap();
let helper_id = ctx
.registry
.lookup(&helper_path)
.expect("helper should be registered");
let method_path = SymbolPath::parse("mylib::<impl MyTrait for Foo>::do_work").unwrap();
let method_id = ctx
.registry
.lookup(&method_path)
.expect("trait impl method should be registered");
let callees: Vec<_> = ctx.code_graph.callees_of(method_id).collect();
assert!(
callees.contains(&helper_id),
"Trait impl method do_work should call helper(), but callees = {:?}",
callees
);
}
#[test]
fn test_calls_edge_trait_impl_method_call_inside_body() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub struct Data {
pub value: i32,
}
impl Data {
pub fn process(&self) -> i32 { self.value }
}
pub trait Transform {
fn transform(&self) -> i32;
}
impl Transform for Data {
fn transform(&self) -> i32 {
self.process()
}
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let transform_method_path =
SymbolPath::parse("mylib::<impl Transform for Data>::transform").unwrap();
let transform_id = ctx
.registry
.lookup(&transform_method_path)
.expect("transform should be registered");
let process_path = SymbolPath::parse("mylib::Data::process").unwrap();
let process_id = ctx
.registry
.lookup(&process_path)
.expect("Data::process should be registered");
let callees: Vec<_> = ctx.code_graph.callees_of(transform_id).collect();
assert!(
callees.contains(&process_id),
"Trait impl transform() should call self.process(), but callees = {:?}",
callees
);
}
#[test]
fn test_calls_edge_method_over_32_candidates_not_dropped() {
let mut source = String::new();
for i in 0..35 {
source.push_str(&format!(
"pub struct T{i} {{}}\nimpl T{i} {{ pub fn render(&self) -> i32 {{ {i} }} }}\n"
));
}
source.push_str(
r#"
pub fn caller(t: T0) -> i32 {
t.render()
}
"#,
);
let workspace = TestWorkspace::builder()
.crate_with_source("mylib", "src/lib.rs", &source)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let caller_path = SymbolPath::parse("mylib::caller").unwrap();
let caller_id = ctx
.registry
.lookup(&caller_path)
.expect("caller should be registered");
let callees: Vec<_> = ctx.code_graph.callees_of(caller_id).collect();
assert!(
!callees.is_empty(),
"caller should have callees for 'render' even with 35 candidates (>32 limit), \
but callees is empty — candidate limit silently drops all edges"
);
}
#[test]
fn test_method_index_excludes_non_callable_symbols() {
let source = r#"
pub mod response {
pub mod process {
pub fn run() -> i32 { 42 }
}
}
pub struct Engine;
impl Engine {
pub fn process(&self) -> i32 { 1 }
}
pub fn caller(e: Engine) -> i32 {
e.process()
}
"#;
let workspace = TestWorkspace::builder()
.crate_with_source("mylib", "src/lib.rs", source)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let caller_path = SymbolPath::parse("mylib::caller").unwrap();
let caller_id = ctx
.registry
.lookup(&caller_path)
.expect("caller should be registered");
let callees: Vec<_> = ctx.code_graph.callees_of(caller_id).collect();
let mod_path = SymbolPath::parse("mylib::response::process").unwrap();
let mod_id = ctx
.registry
.lookup(&mod_path)
.expect("nested module mylib::response::process should be registered");
let has_mod_edge = callees.contains(&mod_id);
assert!(
!has_mod_edge,
"method_index should not include module 'response::process' as a callee of caller(); \
only Function/Method symbols should be in the method_index"
);
assert!(
!callees.is_empty(),
"caller should have at least one callee (Engine::process)"
);
}
#[test]
fn test_method_call_receiver_type_hint_filters_candidates() {
let source = r#"
pub trait Render {
fn render(&self) -> String;
}
pub struct Html;
impl Render for Html {
fn render(&self) -> String { String::new() }
}
pub struct Json;
impl Json {
pub fn new() -> Self { Json }
}
impl Render for Json {
fn render(&self) -> String { String::new() }
}
pub struct Xml;
impl Render for Xml {
fn render(&self) -> String { String::new() }
}
pub fn caller() -> String {
Json::new().render()
}
"#;
let workspace = TestWorkspace::builder()
.crate_with_source("mylib", "src/lib.rs", source)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let caller_path = SymbolPath::parse("mylib::caller").unwrap();
let caller_id = ctx
.registry
.lookup(&caller_path)
.expect("caller should be registered");
let callees: Vec<_> = ctx.code_graph.callees_of(caller_id).collect();
let new_path = SymbolPath::parse("mylib::Json::new").unwrap();
let new_id = ctx.registry.lookup(&new_path);
if let Some(new_id) = new_id {
assert!(callees.contains(&new_id), "Json::new should be a callee");
}
let json_render = ctx
.registry
.lookup(&SymbolPath::parse("mylib::<impl Render for Json>::render").unwrap());
let html_render = ctx
.registry
.lookup(&SymbolPath::parse("mylib::<impl Render for Html>::render").unwrap());
let xml_render = ctx
.registry
.lookup(&SymbolPath::parse("mylib::<impl Render for Xml>::render").unwrap());
if let Some(json_id) = json_render {
assert!(
callees.contains(&json_id),
"Json's render should be a callee (receiver type hint: Json from Json::new())"
);
}
if let Some(html_id) = html_render {
assert!(
!callees.contains(&html_id),
"Html's render should NOT be a callee when receiver is Json::new(); \
receiver type hint should filter it out"
);
}
if let Some(xml_id) = xml_render {
assert!(
!callees.contains(&xml_id),
"Xml's render should NOT be a callee when receiver is Json::new(); \
receiver type hint should filter it out"
);
}
}
#[test]
fn test_associated_fn_call_in_trait_impl_resolved_via_imports() {
let source = r#"
pub mod types {
pub struct Body;
impl Body {
pub fn create() -> Self { Body }
}
}
use crate::types::Body;
pub trait Render {
fn render(&self) -> Body;
}
pub struct Page;
impl Render for Page {
fn render(&self) -> Body {
Body::create()
}
}
"#;
let workspace = TestWorkspace::builder()
.crate_with_source("mylib", "src/lib.rs", source)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let render_path = SymbolPath::parse("mylib::<impl Render for Page>::render").unwrap();
let render_id = ctx
.registry
.lookup(&render_path)
.expect("Page::render should be registered");
let callees: Vec<_> = ctx.code_graph.callees_of(render_id).collect();
let create_path = SymbolPath::parse("mylib::types::Body::create").unwrap();
let create_id = ctx
.registry
.lookup(&create_path)
.expect("Body::create should be registered");
assert!(
callees.contains(&create_id),
"Body::create() should be a callee of Page::render(); \
import resolution in trait impl methods must strip impl segments \
from parent_path before querying UseResolver"
);
}
#[test]
fn test_generic_impl_methods_registered_and_edges_built() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub struct Inner;
impl Inner {
pub fn create() -> Self { Inner }
}
pub struct Router<S> {
_marker: std::marker::PhantomData<S>,
}
impl<S> Router<S> {
pub fn new() -> Self {
let _inner = Inner::create();
Router { _marker: std::marker::PhantomData }
}
pub fn route(self, path: &str) -> Self {
self
}
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let new_path = SymbolPath::parse("mylib::Router::new").unwrap();
let route_path = SymbolPath::parse("mylib::Router::route").unwrap();
let new_id = ctx
.registry
.lookup(&new_path)
.expect("Router::new should be registered despite generic impl<S> Router<S>");
assert!(
ctx.registry.lookup(&route_path).is_some(),
"Router::route should be registered despite generic impl<S> Router<S>"
);
let impl_path_str = "mylib::<impl Router < S >>";
let impl_path = SymbolPath::parse(impl_path_str).unwrap();
assert!(
ctx.registry.lookup(&impl_path).is_some(),
"impl block <impl Router < S >> should be registered"
);
let callees: Vec<_> = ctx.code_graph.callees_of(new_id).collect();
let create_path = SymbolPath::parse("mylib::Inner::create").unwrap();
let create_id = ctx
.registry
.lookup(&create_path)
.expect("Inner::create should be registered");
assert!(
callees.contains(&create_id),
"Router::new must have Inner::create() as callee; \
build_edges_from_impl must strip generics from self_ty"
);
}
#[test]
fn test_external_trait_impl_callees_built() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub trait MyWrite {
fn write(&mut self, buf: &[u8]) -> usize;
}
pub fn helper() -> usize { 42 }
pub struct Writer;
impl MyWrite for Writer {
fn write(&mut self, buf: &[u8]) -> usize {
helper()
}
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let impl_path_str = "mylib::<impl MyWrite for Writer>";
let impl_path = SymbolPath::parse(impl_path_str).unwrap();
assert!(
ctx.registry.lookup(&impl_path).is_some(),
"impl block should be registered: {}",
impl_path_str,
);
let method_path = SymbolPath::parse(&format!("{}::write", impl_path_str)).unwrap();
let method_id = ctx
.registry
.lookup(&method_path)
.expect("Writer::write should be registered under trait impl path");
let callees: Vec<_> = ctx.code_graph.callees_of(method_id).collect();
let helper_path = SymbolPath::parse("mylib::helper").unwrap();
let helper_id = ctx
.registry
.lookup(&helper_path)
.expect("helper should be registered");
assert!(
callees.contains(&helper_id),
"trait impl method Writer::write must have helper() as callee; \
callees = {:?}",
callees
);
}
#[test]
fn test_external_trait_impl_with_generics_callees_built() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub trait MyWrite {
fn write(&mut self, buf: &[u8]) -> usize;
}
pub fn process_buf(buf: &[u8]) -> usize { buf.len() }
pub struct Writer<'a> {
_data: &'a [u8],
}
impl<'a> MyWrite for Writer<'a> {
fn write(&mut self, buf: &[u8]) -> usize {
process_buf(buf)
}
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let impl_path_str = "mylib::<impl MyWrite for Writer < 'a >>";
let impl_path = SymbolPath::parse(impl_path_str).unwrap();
let impl_registered = ctx.registry.lookup(&impl_path).is_some();
let method_path = SymbolPath::parse(&format!("{}::write", impl_path_str)).unwrap();
let method_id = ctx.registry.lookup(&method_path);
assert!(
impl_registered,
"impl block <impl MyWrite for Writer < 'a >> should be registered"
);
assert!(
method_id.is_some(),
"Writer::write should be registered under trait impl path"
);
if let Some(mid) = method_id {
let callees: Vec<_> = ctx.code_graph.callees_of(mid).collect();
let process_path = SymbolPath::parse("mylib::process_buf").unwrap();
let process_id = ctx
.registry
.lookup(&process_path)
.expect("process_buf should be registered");
assert!(
callees.contains(&process_id),
"trait impl method with generics must have process_buf() as callee; \
callees = {:?}",
callees
);
}
}
#[test]
fn test_struct_trait_implements_chain_via_impl() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub trait MyTrait {
fn do_something(&self);
}
pub trait AnotherTrait {
fn other(&self);
}
pub struct MyStruct;
impl MyTrait for MyStruct {
fn do_something(&self) {}
}
impl AnotherTrait for MyStruct {
fn other(&self) {}
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let impl_mytrait_path = SymbolPath::parse("mylib::<impl MyTrait for MyStruct>").unwrap();
let impl_another_path =
SymbolPath::parse("mylib::<impl AnotherTrait for MyStruct>").unwrap();
let mytrait_path = SymbolPath::parse("mylib::MyTrait").unwrap();
let another_path = SymbolPath::parse("mylib::AnotherTrait").unwrap();
let struct_path = SymbolPath::parse("mylib::MyStruct").unwrap();
let impl_mytrait_id = ctx
.registry
.lookup(&impl_mytrait_path)
.expect("impl MyTrait for MyStruct should be registered");
let impl_another_id = ctx
.registry
.lookup(&impl_another_path)
.expect("impl AnotherTrait for MyStruct should be registered");
let mytrait_id = ctx
.registry
.lookup(&mytrait_path)
.expect("MyTrait should be registered");
let another_id = ctx
.registry
.lookup(&another_path)
.expect("AnotherTrait should be registered");
let struct_id = ctx
.registry
.lookup(&struct_path)
.expect("MyStruct should be registered");
let impl1_targets: Vec<_> = ctx
.code_graph
.outgoing_edges(impl_mytrait_id)
.filter(|e| e.kind == CodeEdgeV2::Implements)
.map(|e| e.to)
.collect();
assert!(
impl1_targets.contains(&mytrait_id),
"impl MyTrait for MyStruct should have Implements edge to MyTrait"
);
let impl2_targets: Vec<_> = ctx
.code_graph
.outgoing_edges(impl_another_id)
.filter(|e| e.kind == CodeEdgeV2::Implements)
.map(|e| e.to)
.collect();
assert!(
impl2_targets.contains(&another_id),
"impl AnotherTrait for MyStruct should have Implements edge to AnotherTrait"
);
let struct_implements: Vec<_> = ctx
.code_graph
.outgoing_edges(struct_id)
.filter(|e| e.kind == CodeEdgeV2::Implements)
.collect();
assert!(
struct_implements.is_empty(),
"Struct should have no direct Implements edges; chain via Impl is needed"
);
let struct_name = struct_path.name();
let impl_ids_for_struct: Vec<_> = ctx
.registry
.iter_by_kind(SymbolKind::Impl)
.filter(|&id| {
ctx.registry
.resolve(id)
.and_then(|p| p.segment_refs().last())
.and_then(|seg| seg.impl_self_ty())
.map(|ty| ty.split('<').next().unwrap_or(ty).trim() == struct_name)
.unwrap_or(false)
})
.collect();
assert_eq!(
impl_ids_for_struct.len(),
2,
"MyStruct should have 2 impl blocks"
);
let mut trait_ids: Vec<_> = impl_ids_for_struct
.iter()
.flat_map(|&impl_id| {
ctx.code_graph
.outgoing_edges(impl_id)
.filter(|e| e.kind == CodeEdgeV2::Implements)
.map(|e| e.to)
.collect::<Vec<_>>()
})
.collect();
trait_ids.sort();
trait_ids.dedup();
assert!(
trait_ids.contains(&mytrait_id),
"Struct → Impl → Trait chain should reach MyTrait"
);
assert!(
trait_ids.contains(&another_id),
"Struct → Impl → Trait chain should reach AnotherTrait"
);
let implementors: Vec<_> = ctx.code_graph.implementors_of(mytrait_id).collect();
assert!(
implementors.contains(&impl_mytrait_id),
"MyTrait should have impl block as implementor"
);
for &impl_id in &implementors {
if let Some(impl_path) = ctx.registry.resolve(impl_id) {
if let Some(last_seg) = impl_path.segment_refs().last() {
if let Some(self_ty) = last_seg.impl_self_ty() {
let base = self_ty.split('<').next().unwrap_or(self_ty).trim();
let resolved_struct = ctx.registry.lookup_by_name(base);
assert!(
resolved_struct.is_some(),
"Impl self_ty '{}' should resolve to a registered struct",
base
);
assert_eq!(
resolved_struct.unwrap(),
struct_id,
"Impl self_ty should resolve to MyStruct"
);
}
}
}
}
}
#[test]
fn test_self_method_resolves_via_impl_type_hint() {
let workspace = TestWorkspace::builder()
.crate_with_source(
"mylib",
"src/lib.rs",
r#"
pub trait IntoResponse {
fn into_response(self) -> String;
}
pub struct Html {
content: String,
}
impl Html {
pub fn render(&self) -> String {
self.content.clone()
}
}
pub struct Json {
data: String,
}
impl Json {
pub fn render(&self) -> String {
self.data.clone()
}
}
impl IntoResponse for Html {
fn into_response(self) -> String {
self.render()
}
}
"#,
)
.build();
let ctx = build_context_from_workspace(&workspace, "mylib");
let into_response_path =
SymbolPath::parse("mylib::<impl IntoResponse for Html>::into_response").unwrap();
let html_render_path = SymbolPath::parse("mylib::Html::render").unwrap();
let json_render_path = SymbolPath::parse("mylib::Json::render").unwrap();
let into_response_id = ctx
.registry
.lookup(&into_response_path)
.expect("into_response should be registered");
let html_render_id = ctx
.registry
.lookup(&html_render_path)
.expect("Html::render should be registered");
let json_render_id = ctx
.registry
.lookup(&json_render_path)
.expect("Json::render should be registered");
let callees: Vec<_> = ctx.code_graph.callees_of(into_response_id).collect();
assert!(
callees.contains(&html_render_id),
"self.render() in Html's trait impl should resolve to Html::render; \
callees = {:?}",
callees
);
assert!(
!callees.contains(&json_render_id),
"self.render() in Html's trait impl should NOT resolve to Json::render; \
callees = {:?}",
callees
);
}
#[test]
fn test_extract_self_type_from_parent_path() {
assert_eq!(
extract_self_type_from_parent_path("mylib::<impl IntoResponse for Html>"),
Some("Html")
);
assert_eq!(
extract_self_type_from_parent_path("mylib::<impl io::Write for Writer < '_ >>"),
Some("Writer")
);
assert_eq!(
extract_self_type_from_parent_path("mylib::<impl Router < S >>"),
Some("Router")
);
assert_eq!(
extract_self_type_from_parent_path("mylib::Html"),
Some("Html")
);
}
}