use crate::ir::ExtractResult;
use crate::model::{Edge, EdgeType, Graph};
use crate::resolve::calls::CallResolver;
use crate::resolve::dataflow::DataFlowResolver;
use crate::resolve::fqn::FqnGenerator;
use crate::resolve::imports::ImportResolver;
use crate::resolve::includes_graph::IncludesGraph;
use crate::resolve::symbol_table::{FileSymbolTable, ProjectSymbolTable, SymbolEntry};
use crate::resolve::type_resolver::TypeResolver;
#[cfg(all(feature = "lang-c", feature = "lang-rust"))]
use crate::resolve::cross_lang::FfiResolver;
const PRUNABLE_EDGE_TYPES: [EdgeType; 3] =
[EdgeType::Extends, EdgeType::Implements, EdgeType::UsesType];
fn prune_dangling_type_edges(graph: &mut Graph) -> usize {
let before = graph.edge_count();
let node_ids: std::collections::HashSet<String> = graph.nodes.keys().cloned().collect();
prune_dangling_type_edges_vec(&mut graph.edges, &node_ids);
before - graph.edge_count()
}
pub fn prune_dangling_type_edges_vec(
edges: &mut Vec<Edge>,
node_ids: &std::collections::HashSet<String>,
) -> usize {
let before = edges.len();
edges.retain(|edge| {
!PRUNABLE_EDGE_TYPES.contains(&edge.edge_type) || node_ids.contains(&edge.target)
});
before - edges.len()
}
#[must_use]
pub fn build_symbol_table(results: &[ExtractResult], project: &str) -> ProjectSymbolTable {
let mut table = ProjectSymbolTable::new();
for result in results {
let mut file_table = FileSymbolTable::new();
for node in &result.nodes {
let entity_name = &node.name;
let language = node.language.unwrap_or(result.language);
let fqn = if node.qualified_name.is_empty() {
FqnGenerator::generate(project, &result.file_path, entity_name, language, None)
} else {
node.qualified_name.clone()
};
let entry = SymbolEntry::new(
entity_name.clone(),
fqn,
node.label,
result.file_path.clone(),
project,
)
.with_language(language)
.with_exported(node.is_exported)
.with_signature_opt(node.signature.clone());
file_table.add(entry);
}
if !file_table.is_empty() {
table.add_file_table(&result.file_path, file_table);
}
}
table
}
pub fn resolve_all(
results: &[ExtractResult],
symbol_table: &ProjectSymbolTable,
project: &str,
graph: &mut Graph,
includes_graph: &IncludesGraph,
) -> Vec<Edge> {
let mut edges = Vec::new();
let call_resolver =
CallResolver::new(symbol_table, project).with_includes_graph(includes_graph.clone());
edges.extend(call_resolver.resolve_calls(results, graph));
let df_resolver = DataFlowResolver::new(symbol_table, project);
edges.extend(df_resolver.resolve_dataflows(results, graph));
#[cfg(all(feature = "lang-c", feature = "lang-rust"))]
{
let ffi_resolver = FfiResolver::new(symbol_table, project);
edges.extend(ffi_resolver.resolve_ffi(results, graph));
}
let import_resolver = ImportResolver::new(project);
edges.extend(import_resolver.resolve_imports(results, graph));
let type_resolver = TypeResolver::new(symbol_table);
edges.extend(type_resolver.resolve_types(results, graph));
prune_dangling_type_edges(graph);
edges
}
#[cfg(all(
test,
feature = "lang-c",
feature = "lang-python",
feature = "lang-rust",
feature = "lang-typescript"
))]
mod tests {
use super::*;
use crate::ir::{CallInfo, ImportInfo};
use crate::model::{EdgeType, Language, Node, NodeLabel};
fn make_node(name: &str, label: NodeLabel, language: Language) -> Node {
let mut node = Node::builder(label, name, String::new())
.language(language)
.build();
node.qualified_name.clear();
node
}
fn make_node_with_fqn(
name: &str,
label: NodeLabel,
language: Language,
file_path: &str,
project: &str,
disambiguator: Option<&str>,
) -> Node {
let qn = FqnGenerator::generate(project, file_path, name, language, disambiguator);
Node::builder(label, name, qn).language(language).build()
}
fn make_result(file_path: &str, language: Language, nodes: Vec<Node>) -> ExtractResult {
let mut result = ExtractResult::new(file_path, language);
result.nodes = nodes;
result
}
#[test]
fn build_from_empty_results() {
let table = build_symbol_table(&[], "proj");
assert_eq!(table.file_count(), 0);
assert_eq!(table.symbol_count(), 0);
}
#[test]
fn build_from_single_result_single_node() {
let node = make_node("parse", NodeLabel::Function, Language::Rust);
let result = make_result("src/main.rs", Language::Rust, vec![node]);
let table = build_symbol_table(&[result], "myproject");
assert_eq!(table.file_count(), 1);
assert_eq!(table.symbol_count(), 1);
let entries = table.lookup("parse");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].qn, "myproject.src.main.rs.parse");
assert_eq!(entries[0].file_path, "src/main.rs");
assert_eq!(entries[0].language, Some(Language::Rust));
}
#[test]
fn build_from_multiple_results() {
let r1 = make_result(
"src/main.rs",
Language::Rust,
vec![make_node("main", NodeLabel::Function, Language::Rust)],
);
let r2 = make_result(
"src/utils.rs",
Language::Rust,
vec![make_node("helper", NodeLabel::Function, Language::Rust)],
);
let table = build_symbol_table(&[r1, r2], "proj");
assert_eq!(table.file_count(), 2);
assert_eq!(table.symbol_count(), 2);
assert_eq!(table.lookup("main")[0].qn, "proj.src.main.rs.main");
assert_eq!(table.lookup("helper")[0].qn, "proj.src.utils.rs.helper");
}
#[test]
fn build_generates_correct_fqns() {
let r1 = make_result(
"src/deep/file.rs",
Language::Rust,
vec![make_node("foo", NodeLabel::Function, Language::Rust)],
);
let table = build_symbol_table(&[r1], "proj");
let entry = table.lookup_exact("foo").unwrap();
assert_eq!(entry.qn, "proj.src.deep.file.rs.foo");
}
#[test]
fn build_python_init_py_fqn() {
let r = make_result(
"src/pkg/__init__.py",
Language::Python,
vec![make_node("MyClass", NodeLabel::Class, Language::Python)],
);
let table = build_symbol_table(&[r], "proj");
let entry = table.lookup_exact("MyClass").unwrap();
assert_eq!(entry.qn, "proj.src.pkg.MyClass");
}
#[test]
fn build_c_header_fqn() {
let r = make_result(
"include/header.h",
Language::C,
vec![make_node("MY_DEFINE", NodeLabel::Const, Language::C)],
);
let table = build_symbol_table(&[r], "proj");
let entry = table.lookup_exact("MY_DEFINE").unwrap();
assert_eq!(entry.qn, "proj.include.header.h.MY_DEFINE");
}
#[cfg(feature = "lang-fortran")]
#[test]
fn build_fortran_module_fqn_via_generate_for_module() {
let fqn =
FqnGenerator::generate_for_module("proj", "src/mod.f90", "mymod", "my_func", None);
assert_eq!(fqn, "proj.src.mod.f90.mymod.my_func");
}
#[test]
fn build_symbols_added_to_correct_file_tables() {
let r1 = make_result(
"a.rs",
Language::Rust,
vec![make_node("foo", NodeLabel::Function, Language::Rust)],
);
let r2 = make_result(
"b.rs",
Language::Rust,
vec![make_node("bar", NodeLabel::Function, Language::Rust)],
);
let table = build_symbol_table(&[r1, r2], "proj");
assert_eq!(table.lookup_in_file("a.rs", "foo").len(), 1);
assert!(table.lookup_in_file("a.rs", "bar").is_empty());
assert_eq!(table.lookup_in_file("b.rs", "bar").len(), 1);
assert!(table.lookup_in_file("b.rs", "foo").is_empty());
}
#[test]
fn build_preserves_exported_flag() {
let node = Node::builder(NodeLabel::Function, "foo", "qn")
.language(Language::Rust)
.is_exported(true)
.build();
let result = make_result("src/main.rs", Language::Rust, vec![node]);
let table = build_symbol_table(&[result], "proj");
let entry = table.lookup_exact("foo").unwrap();
assert!(entry.is_exported);
}
#[test]
fn build_preserves_signature() {
let node = Node::builder(NodeLabel::Function, "foo", "qn")
.language(Language::Rust)
.signature("fn foo(x: i32) -> i32")
.build();
let result = make_result("src/main.rs", Language::Rust, vec![node]);
let table = build_symbol_table(&[result], "proj");
let entry = table.lookup_exact("foo").unwrap();
assert_eq!(entry.signature.as_deref(), Some("fn foo(x: i32) -> i32"));
}
#[test]
fn build_preserves_label() {
let node = make_node("MyClass", NodeLabel::Class, Language::Rust);
let result = make_result("src/main.rs", Language::Rust, vec![node]);
let table = build_symbol_table(&[result], "proj");
let entry = table.lookup_exact("MyClass").unwrap();
assert_eq!(entry.label, NodeLabel::Class);
}
#[test]
fn build_skips_empty_results() {
let r1 = make_result("a.rs", Language::Rust, vec![]);
let r2 = make_result(
"b.rs",
Language::Rust,
vec![make_node("foo", NodeLabel::Function, Language::Rust)],
);
let table = build_symbol_table(&[r1, r2], "proj");
assert_eq!(table.file_count(), 1);
assert_eq!(table.symbol_count(), 1);
}
#[test]
fn build_multiple_nodes_same_file() {
let r = make_result(
"src/main.rs",
Language::Rust,
vec![
make_node("foo", NodeLabel::Function, Language::Rust),
make_node("bar", NodeLabel::Function, Language::Rust),
make_node("MyClass", NodeLabel::Class, Language::Rust),
],
);
let table = build_symbol_table(&[r], "proj");
assert_eq!(table.symbol_count(), 3);
assert_eq!(table.file_count(), 1);
assert_eq!(table.lookup("foo").len(), 1);
assert_eq!(table.lookup("bar").len(), 1);
assert_eq!(table.lookup("MyClass").len(), 1);
}
#[test]
fn build_cross_file_lookup() {
let r1 = make_result(
"a.rs",
Language::Rust,
vec![make_node("foo", NodeLabel::Function, Language::Rust)],
);
let r2 = make_result(
"b.rs",
Language::Rust,
vec![make_node("foo", NodeLabel::Function, Language::Rust)],
);
let table = build_symbol_table(&[r1, r2], "proj");
let results = table.lookup("foo");
assert_eq!(results.len(), 2);
}
#[test]
fn build_uses_result_language_when_node_has_none() {
let mut node = make_node("foo", NodeLabel::Function, Language::Rust);
node.language = None;
let result = make_result("src/main.rs", Language::Rust, vec![node]);
let table = build_symbol_table(&[result], "proj");
let entry = table.lookup_exact("foo").unwrap();
assert_eq!(entry.language, Some(Language::Rust));
}
#[test]
fn build_ignores_imports_and_calls() {
let mut result = ExtractResult::new("src/main.rs", Language::Rust);
result
.nodes
.push(make_node("foo", NodeLabel::Function, Language::Rust));
result.imports.push(ImportInfo {
source_file: "std::io".to_string(),
imported_names: vec!["println".to_string()],
line: 1,
});
result.calls.push(CallInfo {
caller_qn: Some("foo".to_string()),
callee_name: "println".to_string(),
line: 3,
args: vec![],
});
let table = build_symbol_table(&[result], "proj");
assert_eq!(table.symbol_count(), 1);
assert_eq!(table.lookup("foo").len(), 1);
assert!(table.lookup("println").is_empty());
}
#[test]
fn build_typescript_fqn() {
let r = make_result(
"src/components/Button.tsx",
Language::TypeScript,
vec![make_node("Button", NodeLabel::Class, Language::TypeScript)],
);
let table = build_symbol_table(&[r], "proj");
let entry = table.lookup_exact("Button").unwrap();
assert_eq!(entry.qn, "proj.src.components.Button.tsx.Button");
}
#[test]
fn build_normalizes_dot_slash_path() {
let r = make_result(
"./src/main.rs",
Language::Rust,
vec![make_node("foo", NodeLabel::Function, Language::Rust)],
);
let table = build_symbol_table(&[r], "proj");
let entry = table.lookup_exact("foo").unwrap();
assert_eq!(entry.qn, "proj.src.main.rs.foo");
}
#[test]
fn build_exported_lookup_works() {
let node = Node::builder(NodeLabel::Function, "foo", "qn")
.language(Language::Rust)
.is_exported(true)
.build();
let result = make_result("src/main.rs", Language::Rust, vec![node]);
let table = build_symbol_table(&[result], "proj");
let exported = table.lookup_exported("foo");
assert_eq!(exported.len(), 1);
}
#[test]
fn build_all_symbols_returns_all() {
let r = make_result(
"src/main.rs",
Language::Rust,
vec![
make_node("foo", NodeLabel::Function, Language::Rust),
make_node("bar", NodeLabel::Function, Language::Rust),
],
);
let table = build_symbol_table(&[r], "proj");
let all = table.all_symbols();
assert_eq!(all.len(), 2);
}
#[test]
fn build_preserves_project_field() {
let node = make_node("foo", NodeLabel::Function, Language::Rust);
let result = make_result("src/main.rs", Language::Rust, vec![node]);
let table = build_symbol_table(&[result], "myproject");
let entry = table.lookup_exact("foo").unwrap();
assert_eq!(entry.project, "myproject");
}
#[test]
fn resolve_all_combines_calls_and_dataflows() {
let foo_qn = FqnGenerator::generate("proj", "a.rs", "foo", Language::Rust, None);
let bar_qn = FqnGenerator::generate("proj", "a.rs", "bar", Language::Rust, None);
let foo_node = Node::builder(NodeLabel::Function, "foo", foo_qn.clone())
.language(Language::Rust)
.file_path("a.rs")
.is_exported(true)
.build();
let bar_node = Node::builder(NodeLabel::Function, "bar", bar_qn.clone())
.language(Language::Rust)
.file_path("a.rs")
.is_exported(true)
.build();
let mut result = ExtractResult::new("a.rs", Language::Rust);
result.nodes = vec![foo_node, bar_node];
result.calls.push(CallInfo {
caller_qn: Some(foo_qn.clone()),
callee_name: "bar".to_string(),
line: 5,
args: vec![],
});
result.assignments.push(crate::parse::AssignInfo {
target_name: "x".to_string(),
source_name: "bar".to_string(),
line: 6,
is_return_assign: true,
});
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
for r in &results {
for node in &r.nodes {
let qn =
FqnGenerator::generate("proj", &r.file_path, &node.name, Language::Rust, None);
let mut g = node.clone();
g.id = qn.clone();
g.qualified_name = qn;
graph.add_node(g);
}
}
let edges = resolve_all(&results, &table, "proj", &mut graph, &IncludesGraph::new());
assert_eq!(edges.len(), 2);
assert_eq!(graph.edge_count(), 2);
let calls_count = edges
.iter()
.filter(|e| e.edge_type == crate::model::EdgeType::Calls)
.count();
let dataflows_count = edges
.iter()
.filter(|e| e.edge_type == crate::model::EdgeType::DataFlows)
.count();
assert_eq!(calls_count, 1);
assert_eq!(dataflows_count, 1);
let call_edge = edges
.iter()
.find(|e| e.edge_type == crate::model::EdgeType::Calls)
.unwrap();
assert_eq!(call_edge.source, foo_qn);
assert_eq!(call_edge.target, bar_qn);
}
#[test]
fn resolve_all_empty_results_returns_empty() {
let table = ProjectSymbolTable::new();
let mut graph = Graph::new();
let edges = resolve_all(&[], &table, "proj", &mut graph, &IncludesGraph::new());
assert!(edges.is_empty());
assert_eq!(graph.edge_count(), 0);
}
#[test]
fn resolve_all_adds_edges_to_graph() {
let foo_qn = FqnGenerator::generate("proj", "a.rs", "foo", Language::Rust, None);
let foo_node = Node::builder(NodeLabel::Function, "foo", foo_qn.clone())
.language(Language::Rust)
.file_path("a.rs")
.is_exported(true)
.build();
let mut result = ExtractResult::new("a.rs", Language::Rust);
result.nodes = vec![foo_node];
result.calls.push(CallInfo {
caller_qn: Some(foo_qn.clone()),
callee_name: "foo".to_string(),
line: 5,
args: vec![],
});
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
for r in &results {
for node in &r.nodes {
let qn =
FqnGenerator::generate("proj", &r.file_path, &node.name, Language::Rust, None);
let mut g = node.clone();
g.id = qn.clone();
g.qualified_name = qn;
graph.add_node(g);
}
}
resolve_all(&results, &table, "proj", &mut graph, &IncludesGraph::new());
assert_eq!(graph.edge_count(), 1);
let neighbors = graph.neighbors(&foo_qn, Some(crate::model::EdgeType::Calls));
assert_eq!(neighbors.len(), 1);
}
#[test]
fn build_symbol_table_uses_qualified_name_with_disambiguator() {
let node = make_node_with_fqn(
"my_test",
NodeLabel::Function,
Language::Rust,
"src/lib.rs",
"proj",
Some("tests"),
);
assert_eq!(
node.qualified_name, "proj.src.lib.rs.my_test#tests",
"test setup: qualified_name must include disambiguator"
);
let result = make_result("src/lib.rs", Language::Rust, vec![node]);
let table = build_symbol_table(&[result], "proj");
let entry = table.lookup_exact("my_test").unwrap();
assert_eq!(
entry.qn, "proj.src.lib.rs.my_test#tests",
"symbol table FQN must include disambiguator (matches node ID)"
);
}
#[test]
fn build_symbol_table_qualified_name_matches_node_id_for_deletion() {
let node = make_node_with_fqn(
"helper",
NodeLabel::Function,
Language::Rust,
"src/util.rs",
"proj",
Some("impl"),
);
let expected_id = node.qualified_name.clone();
let result = make_result("src/util.rs", Language::Rust, vec![node]);
let table = build_symbol_table(&[result], "proj");
let entry = table.lookup_exact("helper").unwrap();
assert_eq!(
entry.qn, expected_id,
"FQN must match node ID for edge cleanup to work"
);
}
#[test]
fn resolve_all_prunes_unresolvable_dangling_type_edges() {
let trait_qn = FqnGenerator::generate("proj", "a.rs", "MyTrait", Language::Rust, None);
let trait_node = Node::builder(NodeLabel::Trait, "MyTrait", trait_qn.clone())
.id(trait_qn.clone())
.language(Language::Rust)
.file_path("a.rs")
.is_exported(true)
.build();
let foo_qn = FqnGenerator::generate("proj", "a.rs", "Foo", Language::Rust, None);
let foo_node = Node::builder(NodeLabel::Struct, "Foo", foo_qn.clone())
.id(foo_qn.clone())
.language(Language::Rust)
.file_path("a.rs")
.is_exported(true)
.build();
let bar_qn = FqnGenerator::generate("proj", "b.rs", "Bar", Language::Rust, None);
let bar_node = Node::builder(NodeLabel::Struct, "Bar", bar_qn.clone())
.id(bar_qn.clone())
.language(Language::Rust)
.file_path("b.rs")
.is_exported(true)
.build();
let result_a = make_result(
"a.rs",
Language::Rust,
vec![trait_node.clone(), foo_node.clone()],
);
let result_b = make_result("b.rs", Language::Rust, vec![bar_node.clone()]);
let results = vec![result_a, result_b];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
graph.add_node(trait_node);
graph.add_node(foo_node);
graph.add_node(bar_node);
graph.add_edge(Edge::new(
&foo_qn,
"proj.a.rs.Display",
EdgeType::Implements,
"proj",
));
graph.add_edge(Edge::new(
&bar_qn,
"proj.b.rs.MyTrait",
EdgeType::Implements,
"proj",
));
assert_eq!(graph.edge_count(), 2, "precondition: 2 IMPLEMENTS edges");
resolve_all(&results, &table, "proj", &mut graph, &IncludesGraph::new());
let implements_edges: Vec<_> = graph
.edges
.iter()
.filter(|e| e.edge_type == EdgeType::Implements)
.collect();
assert_eq!(
implements_edges.len(),
1,
"dangling IMPLEMENTS edge should be pruned"
);
assert_eq!(
implements_edges[0].target, trait_qn,
"resolved edge target should be MyTrait"
);
}
#[test]
fn resolve_all_prunes_unresolvable_extends_and_uses_type() {
let struct_qn = FqnGenerator::generate("proj", "a.rs", "Foo", Language::Rust, None);
let struct_node = Node::builder(NodeLabel::Struct, "Foo", struct_qn.clone())
.id(struct_qn.clone())
.language(Language::Rust)
.file_path("a.rs")
.is_exported(true)
.build();
let result = make_result("a.rs", Language::Rust, vec![struct_node.clone()]);
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
graph.add_node(struct_node);
graph.add_edge(Edge::new(
&struct_qn,
"proj.a.rs.BaseClass",
EdgeType::Extends,
"proj",
));
graph.add_edge(Edge::new(
&struct_qn,
"proj.a.rs.ExternalType",
EdgeType::UsesType,
"proj",
));
assert_eq!(graph.edge_count(), 2, "precondition: 2 dangling type edges");
resolve_all(&results, &table, "proj", &mut graph, &IncludesGraph::new());
assert_eq!(
graph.edge_count(),
0,
"both dangling type edges should be pruned"
);
}
#[test]
fn resolve_all_keeps_non_type_edges_with_dangling_targets() {
let foo_qn = FqnGenerator::generate("proj", "a.rs", "foo", Language::Rust, None);
let foo_node = Node::builder(NodeLabel::Function, "foo", foo_qn.clone())
.id(foo_qn.clone())
.language(Language::Rust)
.file_path("a.rs")
.is_exported(true)
.build();
let result = make_result("a.rs", Language::Rust, vec![foo_node.clone()]);
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
graph.add_node(foo_node);
graph.add_edge(Edge::new(
&foo_qn,
"proj.a.rs.external_fn",
EdgeType::Calls,
"proj",
));
resolve_all(&results, &table, "proj", &mut graph, &IncludesGraph::new());
let calls_count = graph
.edges
.iter()
.filter(|e| e.edge_type == EdgeType::Calls)
.count();
assert_eq!(
calls_count, 1,
"CALLS edge with dangling target should NOT be pruned"
);
}
#[test]
fn prune_dangling_type_edges_vec_removes_dangling_type_edges() {
let mut edges = vec![
Edge::new(
"proj.a.rs.Foo",
"proj.a.rs.Display",
EdgeType::Implements,
"proj",
),
Edge::new(
"proj.a.rs.Bar",
"proj.a.rs.MyTrait",
EdgeType::Implements,
"proj",
),
Edge::new("proj.a.rs.foo", "proj.a.rs.bar", EdgeType::Calls, "proj"),
];
let mut node_ids = std::collections::HashSet::new();
node_ids.insert("proj.a.rs.MyTrait".to_string());
let pruned = prune_dangling_type_edges_vec(&mut edges, &node_ids);
assert_eq!(pruned, 1, "1 dangling IMPLEMENTS edge should be pruned");
assert_eq!(
edges.len(),
2,
"2 edges remain (resolvable Implements + Calls)"
);
let implements: Vec<_> = edges
.iter()
.filter(|e| e.edge_type == EdgeType::Implements)
.collect();
assert_eq!(implements.len(), 1);
assert_eq!(implements[0].target, "proj.a.rs.MyTrait");
}
#[test]
fn prune_dangling_type_edges_vec_keeps_extends_and_uses_type_when_resolved() {
let mut edges = vec![
Edge::new("a", "proj.a.rs.Base", EdgeType::Extends, "proj"), Edge::new("b", "proj.a.rs.Unknown", EdgeType::UsesType, "proj"), ];
let mut node_ids = std::collections::HashSet::new();
node_ids.insert("proj.a.rs.Base".to_string());
let pruned = prune_dangling_type_edges_vec(&mut edges, &node_ids);
assert_eq!(
pruned, 1,
"only the dangling UsesType edge should be pruned"
);
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].edge_type, EdgeType::Extends);
}
}