use tree_sitter::{Language as TsLanguage, Node, Parser, Query, QueryCursor, StreamingIterator};
use crate::error::{CodegraphError, Result};
use crate::graph::types::{
Binding, BindingKind, ByteSpan, EntryPoint, FfiAbi, FfiExport, FileFacts, RefRole, Reference,
Scope, ScopeId, ScopeKind, Symbol, SymbolKind, Visibility,
};
use crate::lang::Language;
use crate::symbol::Descriptor;
use super::{
ExtractCtx, Extractor, MIN_REF_LEN, attach_reference_scopes, child_text,
collect_call_references, definition_bindings, import_bindings, make_symbol, node_occurrence,
node_span, node_text, one_line_signature, push_binding, push_ref, push_scope,
};
const CALL_QUERY: &str = r#"
(call_expression
function: [
(identifier) @callee
(field_expression field: (field_identifier) @callee)
(scoped_identifier path: (_) @qualifier name: (identifier) @callee)
]
)
"#;
const TYPE_QUERY: &str = r#"
(parameter type: (_) @ty)
(function_item return_type: (_) @ty)
(field_declaration type: (_) @ty)
(ordered_field_declaration_list type: (_) @ty)
"#;
pub struct RustExtractor;
impl Extractor for RustExtractor {
fn lang(&self) -> Language {
Language::Rust
}
fn extract(&self, source: &str, file: &str) -> Result<FileFacts> {
let ts_language = crate::grammar::rust();
let mut parser = Parser::new();
parser
.set_language(&ts_language)
.map_err(|_| CodegraphError::Parse {
path: file.to_owned(),
})?;
let tree = parser
.parse(source, None)
.ok_or_else(|| CodegraphError::Parse {
path: file.to_owned(),
})?;
let root = tree.root_node();
let bytes = source.as_bytes();
let namespaces = rust_namespaces(file);
let ctx = ExtractCtx {
bytes,
file,
lang: Language::Rust,
};
let defs = collect_symbols(&root, &ctx, &namespaces);
let def_bindings = definition_bindings(&defs);
let ffi_exports = collect_ffi_exports(&root, bytes, &defs);
let mut symbols = defs;
let mod_sym = super::module_symbol(Language::Rust, &namespaces, file, source.len());
let module_id = mod_sym.id.to_scip_string();
symbols.push(mod_sym);
let mut references =
collect_call_references(&root, &ts_language, CALL_QUERY, Language::Rust, bytes, file)?;
collect_inheritance(&root, bytes, file, &mut references);
collect_module_decl_refs(&root, bytes, file, &mut references);
collect_imports(&root, bytes, file, &mut references, &module_id);
collect_type_references(&root, &ts_language, bytes, file, &mut references)?;
collect_read_references(&root, bytes, file, &mut references);
collect_write_references(&root, bytes, file, &mut references);
let scopes = collect_scopes(&root, source.len());
attach_reference_scopes(&mut references, &scopes);
let mut bindings = collect_bindings(&root, bytes, &scopes);
bindings.extend(def_bindings);
bindings.extend(import_bindings(&references, &scopes));
Ok(FileFacts {
file: file.to_owned(),
lang: Language::Rust.as_str().to_owned(),
symbols,
references,
scopes,
bindings,
ffi_exports,
})
}
}
fn rust_namespaces(file: &str) -> Vec<String> {
let p = file.strip_suffix(".rs").unwrap_or(file);
let p = p.strip_prefix("src/").unwrap_or(p);
let mut segs: Vec<String> = p
.split('/')
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect();
if let Some(last) = segs.last() {
if matches!(last.as_str(), "mod" | "lib" | "main") {
segs.pop();
}
}
segs
}
fn collect_symbols(root: &Node, ctx: &ExtractCtx, namespaces: &[String]) -> Vec<Symbol> {
let bytes = ctx.bytes;
let mut out = Vec::new();
for child in root.children(&mut root.walk()) {
let (kind, leaf) = match child.kind() {
"function_item" => {
let Some(name) = child_text(&child, "identifier", bytes) else {
continue;
};
(
SymbolKind::Function,
Descriptor::Method {
name: name.clone(),
disambiguator: String::new(),
},
)
}
"struct_item" => {
let Some(name) = child_text(&child, "type_identifier", bytes) else {
continue;
};
(SymbolKind::Struct, Descriptor::Type(name))
}
"enum_item" => {
let Some(name) = child_text(&child, "type_identifier", bytes) else {
continue;
};
(SymbolKind::Enum, Descriptor::Type(name))
}
"trait_item" => {
let Some(name) = child_text(&child, "type_identifier", bytes) else {
continue;
};
(SymbolKind::Trait, Descriptor::Type(name))
}
"type_item" => {
let Some(name) = child_text(&child, "type_identifier", bytes) else {
continue;
};
(SymbolKind::TypeAlias, Descriptor::Type(name))
}
"const_item" => {
let Some(name) = child_text(&child, "identifier", bytes) else {
continue;
};
(SymbolKind::Const, Descriptor::Term(name))
}
"static_item" => {
let Some(name) = child_text(&child, "identifier", bytes) else {
continue;
};
(SymbolKind::Static, Descriptor::Term(name))
}
"mod_item" => {
if child.child_by_field_name("body").is_none() {
continue;
}
let Some(name) = child_text(&child, "identifier", bytes) else {
continue;
};
(SymbolKind::Module, Descriptor::Namespace(name))
}
"impl_item" => {
let name = impl_type_name(&child, bytes);
(SymbolKind::Impl, Descriptor::Type(name))
}
_ => continue,
};
let visibility = if kind == SymbolKind::Impl {
Visibility::Public
} else {
read_visibility(&child, bytes)
};
let sym_name = leaf.name().to_owned();
let mut descriptors: Vec<Descriptor> = namespaces
.iter()
.cloned()
.map(Descriptor::Namespace)
.collect();
descriptors.push(leaf);
let signature = one_line_signature(node_text(&child, bytes), &['{']);
out.push(make_symbol(
ctx,
&child,
sym_name.clone(),
kind,
visibility,
descriptors,
signature,
));
if kind == SymbolKind::Function {
if let Some(sym) = out.last_mut() {
sym.entry_points = entry_points_for_rust(&sym_name, &child, bytes);
}
}
if kind == SymbolKind::Impl && child.child_by_field_name("trait").is_none() {
collect_impl_members(&child, ctx, namespaces, &sym_name, &mut out);
}
if kind == SymbolKind::Trait {
collect_trait_members(&child, ctx, namespaces, &sym_name, &mut out);
}
}
out
}
fn member_descriptors(namespaces: &[String], type_name: &str, leaf: Descriptor) -> Vec<Descriptor> {
let mut descriptors: Vec<Descriptor> = namespaces
.iter()
.cloned()
.map(Descriptor::Namespace)
.collect();
descriptors.push(Descriptor::Type(type_name.to_owned()));
descriptors.push(leaf);
descriptors
}
fn collect_impl_members(
impl_node: &Node,
ctx: &ExtractCtx,
namespaces: &[String],
type_name: &str,
out: &mut Vec<Symbol>,
) {
let bytes = ctx.bytes;
let Some(body) = impl_node.child_by_field_name("body") else {
return;
};
for member in body.children(&mut body.walk()) {
let (kind, leaf) = match member.kind() {
"function_item" => {
let Some(name) = child_text(&member, "identifier", bytes) else {
continue;
};
(
SymbolKind::Method,
Descriptor::Method {
name,
disambiguator: String::new(),
},
)
}
"const_item" => {
let Some(name) = child_text(&member, "identifier", bytes) else {
continue;
};
(SymbolKind::Const, Descriptor::Term(name))
}
_ => continue,
};
let visibility = read_visibility(&member, bytes);
let member_name = leaf.name().to_owned();
let descriptors = member_descriptors(namespaces, type_name, leaf);
let signature = one_line_signature(node_text(&member, bytes), &['{']);
out.push(make_symbol(
ctx,
&member,
member_name,
kind,
visibility,
descriptors,
signature,
));
}
}
fn collect_trait_members(
trait_node: &Node,
ctx: &ExtractCtx,
namespaces: &[String],
trait_name: &str,
out: &mut Vec<Symbol>,
) {
let bytes = ctx.bytes;
let Some(body) = trait_node.child_by_field_name("body") else {
return;
};
for member in body.children(&mut body.walk()) {
let (kind, leaf) = match member.kind() {
"function_signature_item" | "function_item" => {
let Some(name) = child_text(&member, "identifier", bytes) else {
continue;
};
(
SymbolKind::Method,
Descriptor::Method {
name,
disambiguator: String::new(),
},
)
}
"const_item" => {
let Some(name) = child_text(&member, "identifier", bytes) else {
continue;
};
(SymbolKind::Const, Descriptor::Term(name))
}
_ => continue,
};
let member_name = leaf.name().to_owned();
let descriptors = member_descriptors(namespaces, trait_name, leaf);
let signature = one_line_signature(node_text(&member, bytes), &['{']);
out.push(make_symbol(
ctx,
&member,
member_name,
kind,
Visibility::Public,
descriptors,
signature,
));
}
}
fn read_visibility(node: &Node, bytes: &[u8]) -> Visibility {
let modifier = node
.children(&mut node.walk())
.find(|c| c.kind() == "visibility_modifier")
.map(|c| node_text(&c, bytes));
match modifier {
Some("pub") => Visibility::Public,
Some(text) if text.starts_with("pub(") => Visibility::Internal,
_ => Visibility::Private,
}
}
const RUST_ROUTE_ATTRS: &[&str] = &[
"get", "post", "put", "delete", "patch", "head", "options", "route", "connect", "trace",
];
fn entry_points_for_rust(fn_name: &str, func: &Node, bytes: &[u8]) -> Vec<EntryPoint> {
let mut markers: Vec<EntryPoint> = Vec::new();
if fn_name == "main" {
markers.push(EntryPoint::Main);
}
let mut sib = func.prev_sibling();
while let Some(node) = sib {
if node.kind() != "attribute_item" {
break;
}
if let Some(attr) = node.named_children(&mut node.walk()).next() {
if attr.kind() == "attribute" {
if let Some(path_node) = attr.named_children(&mut attr.walk()).next() {
let terminal = match path_node.kind() {
"identifier" => node_text(&path_node, bytes),
"scoped_identifier" => path_node
.child_by_field_name("name")
.map_or("", |n| node_text(&n, bytes)),
_ => "",
};
if !terminal.is_empty() && RUST_ROUTE_ATTRS.contains(&terminal) {
markers.push(EntryPoint::HttpRoute(terminal.to_owned()));
}
}
}
}
sib = node.prev_sibling();
}
let main_count = markers
.iter()
.take_while(|m| matches!(m, EntryPoint::Main))
.count();
markers[main_count..].reverse();
markers
}
fn collect_ffi_exports(root: &Node, bytes: &[u8], defs: &[Symbol]) -> Vec<FfiExport> {
let mut out = Vec::new();
for child in root.children(&mut root.walk()) {
if child.kind() != "function_item" {
continue;
}
let Some(sym) = defs
.iter()
.find(|s| s.kind == SymbolKind::Function && s.span.start == child.start_byte())
else {
continue; };
for (abi, export_name) in fn_ffi_exports(&child, bytes, &sym.name) {
out.push(FfiExport {
symbol: sym.id.clone(),
abi,
export_name,
});
}
}
out
}
fn fn_ffi_exports(func: &Node, bytes: &[u8], fn_name: &str) -> Vec<(FfiAbi, String)> {
let mut attr_texts: Vec<&str> = Vec::new();
let mut sib = func.prev_sibling();
while let Some(node) = sib {
if node.kind() != "attribute_item" {
break;
}
attr_texts.push(node_text(&node, bytes));
sib = node.prev_sibling();
}
crate::ffi::rust_exports(&attr_texts, fn_name)
}
fn impl_type_name(node: &Node, bytes: &[u8]) -> String {
let mut name: Option<String> = None;
for child in node.children(&mut node.walk()) {
match child.kind() {
"type_identifier" | "generic_type" | "scoped_type_identifier" => {
name = Some(node_text(&child, bytes).to_owned());
}
"declaration_list" => break,
_ => {}
}
}
name.unwrap_or_else(|| "impl".to_owned())
}
fn collect_inheritance(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
match node.kind() {
"impl_item" => {
if let Some(trait_node) = node.child_by_field_name("trait") {
super::push_ref(
out,
super::simple_type_name(node_text(&trait_node, bytes), "::"),
&trait_node,
file,
RefRole::IsImplementation,
);
}
}
"trait_item" => {
if let Some(bounds) = node.child_by_field_name("bounds") {
for child in bounds.children(&mut bounds.walk()) {
match child.kind() {
"type_identifier" | "generic_type" | "scoped_type_identifier" => {
super::push_ref(
out,
super::simple_type_name(node_text(&child, bytes), "::"),
&child,
file,
RefRole::IsImplementation,
);
}
_ => {}
}
}
}
}
_ => {}
}
for child in node.children(&mut node.walk()) {
collect_inheritance(&child, bytes, file, out);
}
}
fn collect_module_decl_refs(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if node.kind() == "mod_item" {
if let Some(name_node) = node.child_by_field_name("name") {
push_ref(
out,
node_text(&name_node, bytes),
&name_node,
file,
RefRole::ModuleRef,
);
}
}
for child in node.children(&mut node.walk()) {
collect_module_decl_refs(&child, bytes, file, out);
}
}
fn push_path_module_refs(path: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
match path.kind() {
"scoped_identifier" => {
if let Some(inner) = path.child_by_field_name("path") {
push_path_module_refs(&inner, bytes, file, out);
}
let Some(seg) = path.child_by_field_name("name") else {
return;
};
let name = node_text(&seg, bytes);
if matches!(name, "crate" | "self" | "super" | "Self") {
return;
}
push_ref(out, name, &seg, file, RefRole::ModuleRef);
}
"identifier" => {
push_ref(out, node_text(path, bytes), path, file, RefRole::ModuleRef);
}
"crate" if is_crate_root(file) => {
push_ref(out, &self_module_name(file), path, file, RefRole::ModuleRef);
}
_ => {}
}
}
fn is_crate_root(file: &str) -> bool {
let base = file.rsplit('/').next().unwrap_or(file);
matches!(base, "lib.rs" | "main.rs" | "mod.rs")
}
fn self_module_name(file: &str) -> String {
super::module_name(&rust_namespaces(file), file)
}
fn collect_use_leaves(
node: &Node,
bytes: &[u8],
file: &str,
out: &mut Vec<Reference>,
module_id: &str,
prefix: &str,
) {
match node.kind() {
"identifier" => {
super::push_import_ref(
out,
super::node_text(node, bytes),
node,
file,
module_id,
prefix,
);
}
"scoped_identifier" => {
let path_node = node.child_by_field_name("path");
let from_path = path_node
.as_ref()
.map_or("", |n| super::node_text(n, bytes));
if let Some(ref pn) = path_node {
push_path_module_refs(pn, bytes, file, out);
}
if let Some(name_node) = node.child_by_field_name("name") {
super::push_import_ref(
out,
super::node_text(&name_node, bytes),
&name_node,
file,
module_id,
from_path,
);
}
}
"use_as_clause" => {
if let Some(path_node) = node.child_by_field_name("path") {
collect_use_leaves(&path_node, bytes, file, out, module_id, prefix);
}
}
"scoped_use_list" => {
let new_prefix = node
.child_by_field_name("path")
.map_or("", |n| super::node_text(&n, bytes));
if let Some(list_node) = node.child_by_field_name("list") {
collect_use_leaves(&list_node, bytes, file, out, module_id, new_prefix);
}
}
"use_list" => {
for child in node.named_children(&mut node.walk()) {
collect_use_leaves(&child, bytes, file, out, module_id, prefix);
}
}
_ => {}
}
}
fn collect_imports(
node: &Node,
bytes: &[u8],
file: &str,
out: &mut Vec<Reference>,
module_id: &str,
) {
if node.kind() == "use_declaration" {
if let Some(arg) = node.child_by_field_name("argument") {
collect_use_leaves(&arg, bytes, file, out, module_id, "");
}
return;
}
for child in node.children(&mut node.walk()) {
collect_imports(&child, bytes, file, out, module_id);
}
}
fn base_type_name(node: &Node, bytes: &[u8]) -> Option<(String, Option<String>)> {
match node.kind() {
"type_identifier" => Some((node_text(node, bytes).to_owned(), None)),
"primitive_type" => None,
"scoped_type_identifier" => {
let name = node
.child_by_field_name("name")
.map(|n| node_text(&n, bytes).to_owned())?;
let qual = node
.child_by_field_name("path")
.map(|n| node_text(&n, bytes).to_owned());
Some((name, qual))
}
"generic_type" => node
.child_by_field_name("type")
.and_then(|t| base_type_name(&t, bytes)),
"reference_type" => node
.child_by_field_name("type")
.and_then(|t| base_type_name(&t, bytes)),
_ => None,
}
}
fn collect_type_references(
root: &Node,
ts_lang: &TsLanguage,
bytes: &[u8],
file: &str,
out: &mut Vec<Reference>,
) -> Result<()> {
let query = Query::new(ts_lang, TYPE_QUERY).map_err(|e| CodegraphError::Query {
lang: "rust".to_owned(),
msg: e.to_string(),
})?;
let ty_idx = query
.capture_index_for_name("ty")
.ok_or_else(|| CodegraphError::Query {
lang: "rust".to_owned(),
msg: "missing @ty capture".to_owned(),
})?;
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&query, *root, bytes);
while let Some(m) = matches.next() {
for cap in m.captures.iter().filter(|c| c.index == ty_idx) {
if let Some((name, qualifier)) = base_type_name(&cap.node, bytes) {
out.push(Reference {
name,
occ: node_occurrence(&cap.node, file),
role: RefRole::TypeRef,
source_module: None,
from_path: None,
qualifier,
scope: None, type_ref_ctx: None,
});
}
}
}
Ok(())
}
fn is_non_read_position(node: &Node) -> bool {
let parent = match node.parent() {
Some(p) => p,
None => return true, };
match parent.kind() {
"call_expression" => parent.child_by_field_name("function").as_ref() == Some(node),
"function_item" | "struct_item" | "enum_item" | "const_item" | "static_item"
| "mod_item" | "trait_item" | "type_item" => {
parent.child_by_field_name("name").as_ref() == Some(node)
}
"let_declaration" => parent.child_by_field_name("pattern").as_ref() == Some(node),
"parameter" => parent.child_by_field_name("pattern").as_ref() == Some(node),
"closure_parameters" => true,
"mut_pattern" | "ref_pattern" => true,
"scoped_identifier" => true,
"use_declaration" | "use_list" | "scoped_use_list" | "use_as_clause" => true,
"assignment_expression" | "compound_assignment_expr" => {
parent.child_by_field_name("left").as_ref() == Some(node)
}
_ => false,
}
}
fn collect_read_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if matches!(node.kind(), "macro_definition" | "macro_invocation") {
return;
}
if node.kind() == "identifier" {
let name = node_text(node, bytes);
if name.len() >= MIN_REF_LEN && !is_non_read_position(node) {
push_ref(out, name, node, file, RefRole::Read);
}
return;
}
for child in node.children(&mut node.walk()) {
collect_read_references(&child, bytes, file, out);
}
}
fn collect_write_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if matches!(node.kind(), "macro_definition" | "macro_invocation") {
return;
}
if matches!(
node.kind(),
"assignment_expression" | "compound_assignment_expr"
) {
if let Some(lhs) = node.child_by_field_name("left") {
if lhs.kind() == "identifier" {
let name = node_text(&lhs, bytes);
if name.len() >= MIN_REF_LEN {
push_ref(out, name, &lhs, file, RefRole::Write);
}
}
}
}
for child in node.children(&mut node.walk()) {
collect_write_references(&child, bytes, file, out);
}
}
fn scope_dfs(node: &Node, parent_id: ScopeId, scopes: &mut Vec<Scope>) {
match node.kind() {
"function_item" | "closure_expression" => {
let fn_id = push_scope(
scopes,
Some(parent_id),
node_span(node),
ScopeKind::Function,
);
if let Some(body) = node.child_by_field_name("body") {
for child in body.children(&mut body.walk()) {
scope_dfs(&child, fn_id, scopes);
}
} else {
for child in node.children(&mut node.walk()) {
scope_dfs(&child, fn_id, scopes);
}
}
}
"mod_item" | "impl_item" | "trait_item" | "struct_item" | "enum_item" => {
if let Some(body) = node.child_by_field_name("body") {
let kind = if node.kind() == "mod_item" {
ScopeKind::Module
} else {
ScopeKind::Type
};
let body_id = push_scope(scopes, Some(parent_id), node_span(&body), kind);
for child in body.children(&mut body.walk()) {
scope_dfs(&child, body_id, scopes);
}
} else {
for child in node.children(&mut node.walk()) {
scope_dfs(&child, parent_id, scopes);
}
}
}
"block" => {
let block_id = push_scope(scopes, Some(parent_id), node_span(node), ScopeKind::Block);
for child in node.children(&mut node.walk()) {
scope_dfs(&child, block_id, scopes);
}
}
"macro_definition" | "macro_invocation" => {}
_ => {
for child in node.children(&mut node.walk()) {
scope_dfs(&child, parent_id, scopes);
}
}
}
}
fn collect_scopes(root: &Node, source_len: usize) -> Vec<Scope> {
let mut scopes = Vec::new();
push_scope(
&mut scopes,
None,
ByteSpan {
start: 0,
end: source_len,
},
ScopeKind::Module,
);
for child in root.children(&mut root.walk()) {
scope_dfs(&child, 0, &mut scopes);
}
scopes
}
fn resolve_pattern_ident<'tree>(pattern: &Node<'tree>) -> Option<Node<'tree>> {
match pattern.kind() {
"identifier" => Some(*pattern),
"mut_pattern" | "ref_pattern" => {
pattern
.named_children(&mut pattern.walk())
.find(|c| c.kind() == "identifier")
}
_ => None,
}
}
fn collect_bindings(root: &Node, bytes: &[u8], scopes: &[Scope]) -> Vec<Binding> {
let mut out = Vec::new();
collect_bindings_dfs(root, bytes, scopes, &mut out);
out
}
fn collect_bindings_dfs(node: &Node, bytes: &[u8], scopes: &[Scope], out: &mut Vec<Binding>) {
match node.kind() {
"function_item" | "closure_expression" => {
if let Some(params_node) = node.child_by_field_name("parameters") {
collect_params(¶ms_node, bytes, scopes, out);
}
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
"let_declaration" => {
if let Some(pattern_node) = node.child_by_field_name("pattern") {
if let Some(ident_node) = resolve_pattern_ident(&pattern_node) {
let intro = ident_node.start_byte();
let name = node_text(&ident_node, bytes).to_owned();
push_binding(out, name, intro, BindingKind::Local, scopes);
}
}
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
_ => {
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
}
}
fn collect_params(params_node: &Node, bytes: &[u8], scopes: &[Scope], out: &mut Vec<Binding>) {
for child in params_node.named_children(&mut params_node.walk()) {
match child.kind() {
"parameter" => {
if let Some(pattern_node) = child.child_by_field_name("pattern") {
if pattern_node.kind() == "self" {
let intro = pattern_node.start_byte();
push_binding(out, "self".to_owned(), intro, BindingKind::Param, scopes);
} else if let Some(ident_node) = resolve_pattern_ident(&pattern_node) {
let intro = ident_node.start_byte();
let name = node_text(&ident_node, bytes).to_owned();
push_binding(out, name, intro, BindingKind::Param, scopes);
}
}
}
"self_parameter" => {
if let Some(self_node) = child
.named_children(&mut child.walk())
.find(|c| c.kind() == "self")
{
let intro = self_node.start_byte();
push_binding(out, "self".to_owned(), intro, BindingKind::Param, scopes);
}
}
"identifier" => {
let intro = child.start_byte();
let name = node_text(&child, bytes).to_owned();
push_binding(out, name, intro, BindingKind::Param, scopes);
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::types::BindingTarget;
#[test]
fn extracts_defs_with_scip_ids() {
let src = r#"
pub fn validate_token(tok: &str) -> bool { helper() }
fn private_helper() {}
pub struct Config { pub value: u32 }
"#;
let facts = RustExtractor.extract(src, "src/auth/session.rs").unwrap();
let names: Vec<&str> = facts.symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"validate_token"));
assert!(names.contains(&"Config"));
assert!(names.contains(&"private_helper"));
let vt = facts
.symbols
.iter()
.find(|s| s.name == "validate_token")
.unwrap();
assert_eq!(
vt.id.to_scip_string(),
"codegraph . . . auth/session/validate_token()."
);
assert_eq!(vt.kind, SymbolKind::Function);
assert_eq!(vt.visibility, Visibility::Public);
let ph = facts
.symbols
.iter()
.find(|s| s.name == "private_helper")
.unwrap();
assert_eq!(ph.visibility, Visibility::Private);
}
#[test]
fn extracts_call_references() {
let src = "pub fn main() { validate_token(\"t\"); helper(); }";
let facts = RustExtractor.extract(src, "src/main.rs").unwrap();
let names: Vec<&str> = facts.references.iter().map(|r| r.name.as_str()).collect();
assert!(names.contains(&"validate_token"));
assert!(names.contains(&"helper"));
}
#[test]
fn trait_impl_emits_inherit_ref_and_inherent_impl_does_not() {
let src_trait_impl = r#"
use std::fmt;
pub struct Point;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Ok(()) }
}
"#;
let facts = RustExtractor
.extract(src_trait_impl, "src/point.rs")
.unwrap();
let inherit_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::IsImplementation)
.map(|r| r.name.as_str())
.collect();
assert!(
inherit_names.contains(&"Display"),
"expected 'Display' in {inherit_names:?}"
);
let src_inherent = "pub struct Point; impl Point { pub fn new() -> Self { Point } }";
let facts2 = RustExtractor.extract(src_inherent, "src/point.rs").unwrap();
let inherit2: Vec<&str> = facts2
.references
.iter()
.filter(|r| r.role == RefRole::IsImplementation)
.map(|r| r.name.as_str())
.collect();
assert!(
inherit2.is_empty(),
"expected no Inherit refs, got {inherit2:?}"
);
}
#[test]
fn supertrait_bounds_emit_inherit_refs() {
let src = "pub trait Foo: Bar + Baz {}";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let inherit_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::IsImplementation)
.map(|r| r.name.as_str())
.collect();
assert!(
inherit_names.contains(&"Bar"),
"expected 'Bar' in {inherit_names:?}"
);
assert!(
inherit_names.contains(&"Baz"),
"expected 'Baz' in {inherit_names:?}"
);
}
#[test]
fn scoped_trait_path_emits_leaf_name() {
let src = r#"
pub struct Point;
impl std::fmt::Display for Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Ok(()) }
}
"#;
let facts = RustExtractor.extract(src, "src/point.rs").unwrap();
let inherit_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::IsImplementation)
.map(|r| r.name.as_str())
.collect();
assert!(
inherit_names.contains(&"Display"),
"expected 'Display' in {inherit_names:?}"
);
}
#[test]
fn import_scoped_identifier_emits_leaf() {
let src = "use a::b::Config;";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let import_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert_eq!(
import_names,
vec!["Config"],
"expected ['Config'], got {import_names:?}"
);
}
#[test]
fn import_use_list_emits_all_leaves() {
let src = "use std::collections::{HashMap, HashSet};";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let mut import_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
import_names.sort_unstable();
assert_eq!(
import_names,
vec!["HashMap", "HashSet"],
"expected ['HashMap', 'HashSet'], got {import_names:?}"
);
}
#[test]
fn import_use_as_clause_emits_real_leaf_not_alias() {
let src = "use a::b as c;";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let import_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert_eq!(
import_names,
vec!["b"],
"expected ['b'], got {import_names:?}"
);
}
#[test]
fn import_wildcard_emits_nothing() {
let src = "use a::*;";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let import_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert!(
import_names.is_empty(),
"expected no Import refs, got {import_names:?}"
);
}
#[test]
fn import_simple_scoped_path_emits_leaf() {
let src = "use std::io::Result;";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let import_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert_eq!(
import_names,
vec!["Result"],
"expected ['Result'], got {import_names:?}"
);
}
#[test]
fn import_refs_carry_source_module() {
let src = "use std::io::Result;";
let file = "src/net/client.rs";
let facts = RustExtractor.extract(src, file).unwrap();
let namespaces = rust_namespaces(file);
let expected_module_id =
crate::extract::module_symbol(Language::Rust, &namespaces, file, src.len())
.id
.to_scip_string();
let import_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.collect();
assert!(!import_refs.is_empty(), "expected at least one Import ref");
for r in &import_refs {
assert_eq!(
r.source_module,
Some(expected_module_id.clone()),
"Import ref '{}' should carry source_module = {:?}",
r.name,
expected_module_id
);
}
}
#[test]
fn import_scoped_identifier_carries_from_path() {
let src = "use std::io::Result;";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let r = facts
.references
.iter()
.find(|r| r.role == RefRole::Import && r.name == "Result")
.expect("expected Import ref for 'Result'");
assert_eq!(
r.from_path,
Some("std::io".to_owned()),
"from_path should be 'std::io', got {:?}",
r.from_path
);
}
#[test]
fn import_use_list_leaves_carry_prefix_as_from_path() {
let src = "use std::collections::{HashMap, BTreeMap};";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let import_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.collect();
assert_eq!(
import_refs.len(),
2,
"expected 2 Import refs, got {:?}",
import_refs.iter().map(|r| &r.name).collect::<Vec<_>>()
);
for r in &import_refs {
assert_eq!(
r.from_path,
Some("std::collections".to_owned()),
"from_path for '{}' should be 'std::collections', got {:?}",
r.name,
r.from_path
);
}
}
#[test]
fn mod_declaration_emits_module_ref() {
let src = "mod util;\npub fn run() {}";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let module_refs: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::ModuleRef)
.map(|r| r.name.as_str())
.collect();
assert_eq!(
module_refs,
vec!["util"],
"expected ['util'], got {module_refs:?}"
);
}
#[test]
fn use_path_segment_emits_module_ref_and_keeps_import_leaf() {
let src = "use crate::alpha::helper;";
let facts = RustExtractor.extract(src, "src/foo.rs").unwrap();
let module_refs: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::ModuleRef)
.map(|r| r.name.as_str())
.collect();
assert_eq!(
module_refs,
vec!["alpha"],
"expected ModuleRef ['alpha'], got {module_refs:?}"
);
let import_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert_eq!(
import_names,
vec!["helper"],
"expected Import ['helper'], got {import_names:?}"
);
}
#[test]
fn use_path_anchor_emits_no_module_ref() {
let src = "use crate::helper;";
let facts = RustExtractor.extract(src, "src/foo.rs").unwrap();
let module_refs: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::ModuleRef)
.map(|r| r.name.as_str())
.collect();
assert!(
module_refs.is_empty(),
"expected no ModuleRef for an anchor, got {module_refs:?}"
);
let import_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert_eq!(
import_names,
vec!["helper"],
"expected Import ['helper'], got {import_names:?}"
);
}
#[test]
fn crate_anchor_in_root_file_emits_self_module_ref() {
let src = "use crate::alpha::helper;";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let module_refs: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::ModuleRef)
.map(|r| r.name.as_str())
.collect();
let mut sorted = module_refs.clone();
sorted.sort_unstable();
assert_eq!(
sorted,
vec!["alpha", "lib"],
"expected ModuleRefs == {{lib, alpha}}, got {module_refs:?}"
);
let import_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert_eq!(
import_names,
vec!["helper"],
"expected Import ['helper'], got {import_names:?}"
);
}
#[test]
fn crate_anchor_root_file_single_segment() {
let src = "use crate::helper;";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let module_refs: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::ModuleRef)
.map(|r| r.name.as_str())
.collect();
assert_eq!(
module_refs,
vec!["lib"],
"expected ModuleRef ['lib'], got {module_refs:?}"
);
let import_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert_eq!(
import_names,
vec!["helper"],
"expected Import ['helper'], got {import_names:?}"
);
}
#[test]
fn deep_use_path_emits_every_module_segment() {
let src = "use a::b::c::Thing;";
let facts = RustExtractor.extract(src, "src/foo.rs").unwrap();
let mut module_refs: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::ModuleRef)
.map(|r| r.name.as_str())
.collect();
module_refs.sort_unstable();
assert_eq!(
module_refs,
vec!["a", "b", "c"],
"expected ModuleRefs ['a','b','c'], got {module_refs:?}"
);
let import_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert_eq!(
import_names,
vec!["Thing"],
"expected Import ['Thing'], got {import_names:?}"
);
}
#[test]
fn crate_anchor_in_non_root_file_skipped() {
let src = "use crate::alpha::helper;";
let facts = RustExtractor.extract(src, "src/foo.rs").unwrap();
let module_refs: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::ModuleRef)
.map(|r| r.name.as_str())
.collect();
assert_eq!(
module_refs,
vec!["alpha"],
"expected only ModuleRef ['alpha'] (crate anchor skipped), got {module_refs:?}"
);
let import_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert_eq!(
import_names,
vec!["helper"],
"expected Import ['helper'], got {import_names:?}"
);
}
#[test]
fn scope_fn_with_call_has_function_scope_and_ref_attaches_to_it() {
let src = "pub fn greet() { helper(); }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
assert!(!facts.scopes.is_empty(), "scopes must not be empty");
assert_eq!(
facts.scopes[0].kind,
ScopeKind::Module,
"scopes[0] must be Module"
);
assert_eq!(facts.scopes[0].parent, None, "root scope has no parent");
let fn_scope_pos = facts
.scopes
.iter()
.position(|s| s.kind == ScopeKind::Function)
.expect("expected a Function scope");
let helper_ref = facts
.references
.iter()
.find(|r| r.role == RefRole::Call && r.name == "helper")
.expect("expected a Call ref for 'helper'");
assert_eq!(
helper_ref.scope,
Some(fn_scope_pos),
"helper call should be attributed to the Function scope ({}), got {:?}",
fn_scope_pos,
helper_ref.scope
);
}
#[test]
fn nested_block_scope_parent_chains_correctly() {
let src = "fn outer() { { inner_call(); } }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let fn_scope_id = facts
.scopes
.iter()
.position(|s| s.kind == ScopeKind::Function)
.expect("expected a Function scope");
let block_scope_id = facts
.scopes
.iter()
.position(|s| s.kind == ScopeKind::Block)
.expect("expected a Block scope");
assert_eq!(
facts.scopes[block_scope_id].parent,
Some(fn_scope_id),
"Block scope parent should be the Function scope"
);
let inner_ref = facts
.references
.iter()
.find(|r| r.role == RefRole::Call && r.name == "inner_call")
.expect("expected a Call ref for 'inner_call'");
assert_eq!(
inner_ref.scope,
Some(block_scope_id),
"inner_call should attribute to the Block scope ({}), got {:?}",
block_scope_id,
inner_ref.scope
);
}
#[test]
fn empty_source_produces_exactly_one_root_scope() {
let ts_language = crate::grammar::rust();
let mut parser = tree_sitter::Parser::new();
parser.set_language(&ts_language).unwrap();
let tree = parser.parse("", None).unwrap();
let root = tree.root_node();
let scopes = collect_scopes(&root, 0);
assert_eq!(
scopes.len(),
1,
"empty source should produce exactly one scope"
);
assert_eq!(scopes[0].kind, ScopeKind::Module);
assert_eq!(scopes[0].parent, None);
}
#[test]
fn fn_params_emit_param_bindings() {
let src = "fn f(a: u32, b: u32) { }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let fn_scope_id = facts
.scopes
.iter()
.position(|s| s.kind == ScopeKind::Function)
.expect("expected a Function scope");
let mut param_names: Vec<(&str, ScopeId)> = facts
.bindings
.iter()
.filter(|b| b.kind == BindingKind::Param)
.map(|b| (b.name.as_str(), b.scope))
.collect();
param_names.sort_by_key(|(n, _)| *n);
assert_eq!(
param_names,
vec![("a", fn_scope_id), ("b", fn_scope_id)],
"expected Param bindings for a and b in the Function scope, got {param_names:?}"
);
for b in facts
.bindings
.iter()
.filter(|b| b.kind == BindingKind::Param)
{
assert_eq!(
b.target,
BindingTarget::Local,
"param binding target must be Local"
);
}
}
#[test]
fn self_parameter_emits_param_binding() {
let src = "pub struct S; impl S { fn m(&self) {} }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let self_binding = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Param && b.name == "self")
.expect("expected a Param binding named 'self'");
assert_eq!(self_binding.target, BindingTarget::Local);
assert_eq!(
facts.scopes[self_binding.scope].kind,
ScopeKind::Function,
"self binding should be in a Function scope"
);
}
#[test]
fn let_binding_emits_local_binding() {
let src = "fn f() { let x = 1; }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let fn_scope_id = facts
.scopes
.iter()
.position(|s| s.kind == ScopeKind::Function)
.expect("expected a Function scope");
let x_binding = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Local && b.name == "x")
.expect("expected a Local binding for 'x'");
assert_eq!(
x_binding.scope, fn_scope_id,
"x should be in the Function scope"
);
assert_eq!(x_binding.target, BindingTarget::Local);
let expected_intro = src.find('x').expect("'x' not in src");
assert_eq!(
x_binding.intro, expected_intro,
"intro should point at the 'x' token"
);
}
#[test]
fn shadowing_produces_two_local_bindings_with_different_intros() {
let src = "fn f() { let x = 1; let x = 2; }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let x_bindings: Vec<_> = facts
.bindings
.iter()
.filter(|b| b.kind == BindingKind::Local && b.name == "x")
.collect();
assert_eq!(
x_bindings.len(),
2,
"expected exactly two Local bindings for 'x', got {}",
x_bindings.len()
);
assert_ne!(
x_bindings[0].intro, x_bindings[1].intro,
"shadowed bindings must have different intro offsets"
);
}
#[test]
fn nested_block_let_binding_attributes_to_inner_block_scope() {
let src = "fn f() { { let y = 1; } }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let block_scope_id = facts
.scopes
.iter()
.position(|s| s.kind == ScopeKind::Block)
.expect("expected a Block scope");
let y_binding = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Local && b.name == "y")
.expect("expected a Local binding for 'y'");
assert_eq!(
y_binding.scope, block_scope_id,
"y should be attributed to the inner Block scope ({}), got {}",
block_scope_id, y_binding.scope
);
}
#[test]
fn impl_block_with_method_nests_type_then_function_scope() {
let src = "pub struct Foo; impl Foo { pub fn bar(&self) { call(); } }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let type_scope_id = facts
.scopes
.iter()
.position(|s| s.kind == ScopeKind::Type)
.expect("expected a Type scope for the impl body");
let fn_scope_id = facts
.scopes
.iter()
.position(|s| s.kind == ScopeKind::Function)
.expect("expected a Function scope for the method");
assert_eq!(
facts.scopes[type_scope_id].parent,
Some(0),
"impl body Type scope parent should be root (0)"
);
assert_eq!(
facts.scopes[fn_scope_id].parent,
Some(type_scope_id),
"method Function scope parent should be the Type scope"
);
let call_ref = facts
.references
.iter()
.find(|r| r.role == RefRole::Call && r.name == "call")
.expect("expected a Call ref for 'call'");
assert_eq!(
call_ref.scope,
Some(fn_scope_id),
"call() should attribute to the Function scope ({}), got {:?}",
fn_scope_id,
call_ref.scope
);
}
#[test]
fn pub_fn_emits_definition_binding() {
let src = "pub fn foo() {}";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let b = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Definition && b.name == "foo")
.expect("expected a Definition binding named 'foo'");
assert_eq!(b.scope, 0, "top-level def must bind in scope 0");
assert!(
matches!(b.target, BindingTarget::Def(_)),
"Definition binding target must be Def(_), got {:?}",
b.target
);
}
#[test]
fn pub_struct_emits_definition_binding_in_root_scope() {
let src = "pub struct Bar {}";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let b = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Definition && b.name == "Bar")
.expect("expected a Definition binding named 'Bar'");
assert_eq!(b.scope, 0, "struct def must bind in root scope 0");
assert!(
matches!(b.target, BindingTarget::Def(_)),
"Definition binding target must be Def(_)"
);
}
#[test]
fn use_stmt_emits_import_binding() {
let src = "use std::io::Result;";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let b = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Import && b.name == "Result")
.expect("expected an Import binding named 'Result'");
assert_eq!(
b.target,
BindingTarget::Import("std::io".to_owned()),
"import binding target should be Import(\"std::io\"), got {:?}",
b.target
);
}
#[test]
fn module_file_symbol_does_not_produce_definition_binding() {
let src = "pub fn foo() {}";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let def_bindings: Vec<_> = facts
.bindings
.iter()
.filter(|b| b.kind == BindingKind::Definition)
.collect();
assert_eq!(
def_bindings.len(),
1,
"expected exactly one Definition binding, got {}: {:?}",
def_bindings.len(),
def_bindings.iter().map(|b| &b.name).collect::<Vec<_>>()
);
assert_eq!(
def_bindings[0].name, "foo",
"the sole Definition binding must be 'foo', not the module stem"
);
}
#[test]
fn qualified_call_single_segment_captures_qualifier() {
let src = "pub fn caller() { mod_a::process(); }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let r = facts
.references
.iter()
.find(|r| r.role == RefRole::Call && r.name == "process")
.expect("expected a Call ref for 'process'");
assert_eq!(
r.qualifier,
Some("mod_a".to_owned()),
"qualifier should be 'mod_a', got {:?}",
r.qualifier
);
}
#[test]
fn qualified_call_nested_segments_captures_full_qualifier() {
let src = "pub fn caller() { a::b::process(); }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let r = facts
.references
.iter()
.find(|r| r.role == RefRole::Call && r.name == "process")
.expect("expected a Call ref for 'process'");
assert_eq!(
r.qualifier,
Some("a::b".to_owned()),
"qualifier should be 'a::b', got {:?}",
r.qualifier
);
}
#[test]
fn unqualified_call_has_no_qualifier() {
let src = "pub fn caller() { helper(); }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let r = facts
.references
.iter()
.find(|r| r.role == RefRole::Call && r.name == "helper")
.expect("expected a Call ref for 'helper'");
assert_eq!(
r.qualifier, None,
"unqualified call should have qualifier == None, got {:?}",
r.qualifier
);
}
#[test]
fn method_call_via_field_expression_has_no_qualifier() {
let src = "pub fn caller(obj: Foo) { obj.method(); }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let r = facts
.references
.iter()
.find(|r| r.role == RefRole::Call && r.name == "method")
.expect("expected a Call ref for 'method'");
assert_eq!(
r.qualifier, None,
"method call via field_expression should have qualifier == None, got {:?}",
r.qualifier
);
}
#[test]
fn combined_def_and_use_emit_both_kinds_and_locals_still_work() {
let src = "use std::io::Result;\npub fn foo(x: u32) { let y = 1; }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
assert!(
facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Definition && b.name == "foo"),
"expected a Definition binding for 'foo'"
);
assert!(
facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Import && b.name == "Result"),
"expected an Import binding for 'Result'"
);
assert!(
facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Param && b.name == "x"),
"expected a Param binding for 'x' (regression check)"
);
assert!(
facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Local && b.name == "y"),
"expected a Local binding for 'y' (regression check)"
);
}
fn type_refs(facts: &crate::graph::FileFacts) -> Vec<&Reference> {
facts
.references
.iter()
.filter(|r| r.role == RefRole::TypeRef)
.collect()
}
#[test]
fn typeref_param_and_return_types_captured() {
let src = "fn validate(cfg: Config) -> Outcome {}";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let names: Vec<&str> = type_refs(&facts).iter().map(|r| r.name.as_str()).collect();
assert!(
names.contains(&"Config"),
"expected TypeRef for 'Config' in {names:?}"
);
assert!(
names.contains(&"Outcome"),
"expected TypeRef for 'Outcome' in {names:?}"
);
for r in type_refs(&facts) {
assert_eq!(
r.role,
RefRole::TypeRef,
"role should be TypeRef, got {:?}",
r.role
);
}
}
#[test]
fn typeref_struct_field_type_captured() {
let src = "struct Holder { item: Widget }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let names: Vec<&str> = type_refs(&facts).iter().map(|r| r.name.as_str()).collect();
assert!(
names.contains(&"Widget"),
"expected TypeRef for 'Widget' in {names:?}"
);
}
#[test]
fn typeref_generic_base_type_captured_inner_deferred() {
let src = "fn f(v: Vec<Config>) {}";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let names: Vec<&str> = type_refs(&facts).iter().map(|r| r.name.as_str()).collect();
assert!(
names.contains(&"Vec"),
"expected TypeRef for 'Vec' (base generic) in {names:?}"
);
assert!(
!names.contains(&"Config"),
"Config inside generic args should NOT be captured in v1 (got {names:?})"
);
}
#[test]
fn typeref_scoped_type_emits_leaf_and_qualifier() {
let src = "fn f(r: std::io::Result) {}";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let r = type_refs(&facts)
.into_iter()
.find(|r| r.name == "Result")
.expect("expected a TypeRef ref named 'Result'");
assert_eq!(
r.qualifier,
Some("std::io".to_owned()),
"qualifier should be 'std::io', got {:?}",
r.qualifier
);
}
#[test]
fn typeref_reference_type_descends_through_borrow() {
let src = "fn f(c: &Config) {}";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let names: Vec<&str> = type_refs(&facts).iter().map(|r| r.name.as_str()).collect();
assert!(
names.contains(&"Config"),
"expected TypeRef for 'Config' through '&' in {names:?}"
);
}
#[test]
fn typeref_primitive_type_not_captured() {
let src = "fn f(n: u32, b: bool) -> i64 { 0 }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let names: Vec<&str> = type_refs(&facts).iter().map(|r| r.name.as_str()).collect();
assert!(
!names.contains(&"u32"),
"primitive 'u32' should NOT be captured as TypeRef (got {names:?})"
);
assert!(
!names.contains(&"bool"),
"primitive 'bool' should NOT be captured as TypeRef (got {names:?})"
);
assert!(
!names.contains(&"i64"),
"primitive 'i64' should NOT be captured as TypeRef (got {names:?})"
);
}
#[test]
fn typeref_empty_fn_no_types_emits_no_typeref() {
let src = "fn f() {}";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let trefs = type_refs(&facts);
assert!(
trefs.is_empty(),
"fn with no types should produce no TypeRef refs, got {:?}",
trefs.iter().map(|r| &r.name).collect::<Vec<_>>()
);
}
#[test]
fn read_ref_emitted_for_tail_use_not_declaration() {
let src = "fn f() -> i32 { let base = 1; base }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let read_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "base")
.collect();
assert!(
!read_refs.is_empty(),
"expected at least one Read ref for 'base', got none"
);
let decl_offset = src.find("let base").unwrap() + "let ".len();
for r in &read_refs {
assert!(
r.occ.byte > decl_offset,
"Read ref for 'base' at byte {} should be after declaration offset {}",
r.occ.byte,
decl_offset
);
}
}
#[test]
fn write_ref_emitted_for_assignment_not_let() {
let src = "fn f() { let mut cnt = 0; cnt = 5; }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let write_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Write && r.name == "cnt")
.collect();
assert!(
!write_refs.is_empty(),
"expected at least one Write ref for 'cnt', got none — all refs: {:?}",
facts
.references
.iter()
.map(|r| (&r.name, r.role))
.collect::<Vec<_>>()
);
let assign_offset = src.find("cnt = 5").unwrap();
for r in &write_refs {
assert!(
r.occ.byte >= assign_offset,
"Write ref for 'cnt' at byte {} should be at/after the assignment at {}",
r.occ.byte,
assign_offset
);
}
}
#[test]
fn compound_assignment_emits_write_ref() {
let src = "fn f() { let mut num = 0; num += 1; }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let write_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Write && r.name == "num")
.collect();
assert!(
!write_refs.is_empty(),
"expected a Write ref for 'num' from compound assignment, got none — all refs: {:?}",
facts
.references
.iter()
.map(|r| (&r.name, r.role))
.collect::<Vec<_>>()
);
}
#[test]
fn call_not_also_read() {
let src = "fn f() { helper(); }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let call_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Call && r.name == "helper")
.collect();
assert!(!call_refs.is_empty(), "expected a Call ref for 'helper'");
let read_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "helper")
.collect();
assert!(
read_refs.is_empty(),
"helper() must NOT produce a Read ref; got: {read_refs:?}"
);
}
#[test]
fn field_access_not_a_read_of_field() {
let src = "fn f(c: C) -> i32 { c.field }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let field_reads: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "field")
.collect();
assert!(
field_reads.is_empty(),
"field 'field' in field_expression must NOT be a Read ref; got: {field_reads:?}"
);
}
#[test]
fn assoc_fn_symbol_emitted_for_pub_new() {
let src = "pub struct Foo; impl Foo { pub fn new() -> Self { Foo } }";
let facts = RustExtractor.extract(src, "src/foo.rs").unwrap();
let sym = facts
.symbols
.iter()
.find(|s| s.name == "new" && s.kind == SymbolKind::Method)
.expect("expected a Method symbol named 'new'");
assert!(
sym.id.to_scip_string().ends_with("Foo#new()."),
"SCIP string should end with 'Foo#new().', got: {}",
sym.id.to_scip_string()
);
}
#[test]
fn assoc_const_symbol_emitted_for_pub_const() {
let src = "pub struct Foo; impl Foo { pub const MAX: u32 = 3; }";
let facts = RustExtractor.extract(src, "src/foo.rs").unwrap();
let sym = facts
.symbols
.iter()
.find(|s| s.name == "MAX" && s.kind == SymbolKind::Const)
.expect("expected a Const symbol named 'MAX'");
assert!(
sym.id.to_scip_string().ends_with("Foo#MAX."),
"SCIP string should end with 'Foo#MAX.', got: {}",
sym.id.to_scip_string()
);
}
#[test]
fn method_with_self_is_emitted() {
let src = "pub struct Foo; impl Foo { pub fn run(&self) {} }";
let facts = RustExtractor.extract(src, "src/foo.rs").unwrap();
let sym = facts
.symbols
.iter()
.find(|s| s.name == "run" && s.kind == SymbolKind::Method)
.expect("expected a Method symbol named 'run'");
assert!(
sym.id.to_scip_string().ends_with("Foo#run()."),
"SCIP string should end with 'Foo#run().', got: {}",
sym.id.to_scip_string()
);
}
#[test]
fn non_pub_member_emitted_with_private_visibility() {
let src = "pub struct Foo; impl Foo { fn secret(&self) {} }";
let facts = RustExtractor.extract(src, "src/foo.rs").unwrap();
let secret = facts
.symbols
.iter()
.find(|s| s.name == "secret")
.expect("private method 'secret' must now be emitted as a symbol");
assert_eq!(
secret.visibility,
Visibility::Private,
"private method 'secret' must have Visibility::Private, got {:?}",
secret.visibility
);
assert_eq!(secret.kind, SymbolKind::Method);
}
#[test]
fn trait_impl_members_excluded() {
let src = r#"pub struct Point;
impl std::fmt::Display for Point {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { Ok(()) }
}"#;
let facts = RustExtractor.extract(src, "src/foo.rs").unwrap();
assert!(
facts
.symbols
.iter()
.any(|s| s.name == "Point" && s.kind == SymbolKind::Impl),
"Impl block symbol for 'Point' should still be emitted"
);
let fmt_under_point: Vec<_> = facts
.symbols
.iter()
.filter(|s| s.name == "fmt" && s.id.to_scip_string().contains("Point#"))
.collect();
assert!(
fmt_under_point.is_empty(),
"trait-impl method 'fmt' must NOT be emitted under Point#, got: {:?}",
fmt_under_point
.iter()
.map(|s| s.id.to_scip_string())
.collect::<Vec<_>>()
);
}
#[test]
fn pub_fn_has_public_visibility() {
let src = "pub fn f() {}";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let sym = facts
.symbols
.iter()
.find(|s| s.name == "f" && s.kind == SymbolKind::Function)
.expect("expected a Function symbol named 'f'");
assert_eq!(
sym.visibility,
Visibility::Public,
"pub fn should have Visibility::Public, got {:?}",
sym.visibility
);
}
#[test]
fn private_fn_has_private_visibility() {
let src = "fn g() {}";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let sym = facts
.symbols
.iter()
.find(|s| s.name == "g" && s.kind == SymbolKind::Function)
.expect("expected a Function symbol named 'g'");
assert_eq!(
sym.visibility,
Visibility::Private,
"fn with no modifier should have Visibility::Private, got {:?}",
sym.visibility
);
}
#[test]
fn pub_crate_fn_has_internal_visibility() {
let src = "pub(crate) fn h() {}";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let sym = facts
.symbols
.iter()
.find(|s| s.name == "h" && s.kind == SymbolKind::Function)
.expect("expected a Function symbol named 'h'");
assert_eq!(
sym.visibility,
Visibility::Internal,
"pub(crate) fn should have Visibility::Internal, got {:?}",
sym.visibility
);
}
#[test]
fn private_impl_method_emitted_with_private_visibility() {
let src = "pub struct Bar; impl Bar { fn inner(&self) {} }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let sym = facts
.symbols
.iter()
.find(|s| s.name == "inner" && s.kind == SymbolKind::Method)
.expect("expected a Method symbol named 'inner'");
assert_eq!(
sym.visibility,
Visibility::Private,
"private impl method should have Visibility::Private, got {:?}",
sym.visibility
);
}
fn sym_by_name(facts: &crate::graph::types::FileFacts, name: &str) -> Symbol {
facts
.symbols
.iter()
.find(|s| s.name == name)
.unwrap_or_else(|| {
panic!(
"symbol '{name}' not found; symbols: {:?}",
facts
.symbols
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
)
})
.clone()
}
fn ep_str(eps: &[EntryPoint]) -> String {
eps.iter()
.map(|ep| match ep {
EntryPoint::Main => "Main".to_owned(),
EntryPoint::HttpRoute(m) => format!("HttpRoute({m})"),
})
.collect::<Vec<_>>()
.join(", ")
}
#[test]
fn rust_entry_point_get_route() {
let src = "#[get(\"/\")]\npub fn index() -> String { String::new() }";
let facts = RustExtractor.extract(src, "src/routes.rs").unwrap();
let sym = sym_by_name(&facts, "index");
assert_eq!(
sym.entry_points.len(),
1,
"expected exactly 1 entry point, got [{}]",
ep_str(&sym.entry_points)
);
assert!(
matches!(&sym.entry_points[0], EntryPoint::HttpRoute(m) if m == "get"),
"expected HttpRoute(\"get\"), got [{}]",
ep_str(&sym.entry_points)
);
}
#[test]
fn rust_entry_point_post_route() {
let src = "#[post(\"/users\")]\npub fn create() {}";
let facts = RustExtractor.extract(src, "src/routes.rs").unwrap();
let sym = sym_by_name(&facts, "create");
assert_eq!(
sym.entry_points.len(),
1,
"expected exactly 1 entry point, got [{}]",
ep_str(&sym.entry_points)
);
assert!(
matches!(&sym.entry_points[0], EntryPoint::HttpRoute(m) if m == "post"),
"expected HttpRoute(\"post\"), got [{}]",
ep_str(&sym.entry_points)
);
}
#[test]
fn rust_entry_point_non_route_attr_ignored() {
let src = "#[derive(Debug)]\n#[inline]\npub fn helper() {}";
let facts = RustExtractor.extract(src, "src/util.rs").unwrap();
let sym = sym_by_name(&facts, "helper");
assert!(
sym.entry_points.is_empty(),
"non-route attributes must not produce entry points; got [{}]",
ep_str(&sym.entry_points)
);
}
#[test]
fn rust_entry_point_main_fn_name() {
let src = "pub fn main() {}";
let facts = RustExtractor.extract(src, "src/main.rs").unwrap();
let sym = sym_by_name(&facts, "main");
assert_eq!(
sym.entry_points.len(),
1,
"expected exactly 1 entry point, got [{}]",
ep_str(&sym.entry_points)
);
assert!(
matches!(&sym.entry_points[0], EntryPoint::Main),
"expected Main, got [{}]",
ep_str(&sym.entry_points)
);
}
#[test]
fn rust_entry_point_plain_fn_empty() {
let src = "pub fn process() {}";
let facts = RustExtractor.extract(src, "src/util.rs").unwrap();
let sym = sym_by_name(&facts, "process");
assert!(
sym.entry_points.is_empty(),
"plain function must have no entry points; got [{}]",
ep_str(&sym.entry_points)
);
}
#[test]
fn rust_entry_point_qualified_path_terminal() {
let src = "#[actix_web::get(\"/\")]\npub fn scoped() {}";
let facts = RustExtractor.extract(src, "src/routes.rs").unwrap();
let sym = sym_by_name(&facts, "scoped");
assert_eq!(
sym.entry_points.len(),
1,
"expected exactly 1 entry point, got [{}]",
ep_str(&sym.entry_points)
);
assert!(
matches!(&sym.entry_points[0], EntryPoint::HttpRoute(m) if m == "get"),
"expected HttpRoute(\"get\"), got [{}]",
ep_str(&sym.entry_points)
);
}
#[test]
fn rust_entry_point_tokio_main_not_a_route() {
let src = "#[tokio::main]\nasync fn main() {}";
let facts = RustExtractor.extract(src, "src/main.rs").unwrap();
let sym = sym_by_name(&facts, "main");
assert_eq!(
sym.entry_points.len(),
1,
"expected exactly 1 entry point (Main), got [{}]",
ep_str(&sym.entry_points)
);
assert!(
matches!(&sym.entry_points[0], EntryPoint::Main),
"expected Main, got [{}]",
ep_str(&sym.entry_points)
);
}
#[test]
fn cross_file_assoc_fn_call_resolves_to_impl_member() {
use crate::resolve::{Resolver, SymbolTableResolver};
let point = RustExtractor
.extract(
"pub struct Point; impl Point { pub fn new() -> Self { Point } }",
"src/point.rs",
)
.unwrap();
let main = RustExtractor
.extract("pub fn run() { let _ = Point::new(); }", "src/main.rs")
.unwrap();
let graph = SymbolTableResolver.resolve(&[point, main]);
let call_to_new: Vec<_> = graph
.edges
.iter()
.filter(|e| e.role == RefRole::Call && e.to.to_scip_string().ends_with("Point#new()."))
.collect();
assert_eq!(
call_to_new.len(),
1,
"expected exactly one Call edge to Point#new().(), got: {:?}",
call_to_new
.iter()
.map(|e| format!("{} -> {}", e.from.to_scip_string(), e.to.to_scip_string()))
.collect::<Vec<_>>()
);
}
}