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, TypeRefContext, Visibility,
};
use crate::lang::Language;
use crate::symbol::Descriptor;
#[cfg(feature = "sql")]
use super::emit_embedded_sql_refs;
use super::{
BindingRules, ExtractCtx, Extractor, MIN_REF_LEN, attach_reference_scopes, child_text,
collect_call_references, definition_bindings, field_text, import_bindings, make_symbol,
mark_receiver_qualifier_calls, mark_self_receiver_calls, member_descriptors, node_occurrence,
node_span, node_text, one_line_signature, push_binding, push_ref, push_scope,
push_typed_binding, simple_type_name,
};
const CALL_QUERY: &str = r#"
(call_expression
function: [
(identifier) @callee
(field_expression field: (field_identifier) @callee)
(scoped_identifier path: (_) @qualifier name: (identifier) @callee)
]
)
"#;
const ASSOCIATED_CALL_SUBJECT_QUERY: &str = r#"
(call_expression
function: (scoped_identifier path: (_) @subject name: (identifier)))
"#;
const SELF_CALL_QUERY: &str = r#"
(call_expression
function: (field_expression
value: (self)
field: (field_identifier) @callee))
"#;
const RECEIVER_CALL_QUERY: &str = r#"
(call_expression
function: (field_expression
value: (identifier) @receiver
field: (field_identifier) @callee))
"#;
const TYPE_QUERY: &str = r#"
(parameter type: (_) @parameter_ty)
(function_item return_type: (_) @return_ty)
(field_declaration type: (_) @field_ty)
(ordered_field_declaration_list type: (_) @field_ty)
"#;
pub struct RustExtractor;
impl Extractor for RustExtractor {
fn lang(&self) -> Language {
Language::Rust
}
fn extract_facts(&self, source: &str, file: &str) -> Result<FileFacts> {
self.extract_impl(source, file, None)
}
fn extract_facts_with_bindings(
&self,
source: &str,
file: &str,
rules: &BindingRules,
) -> Result<FileFacts> {
self.extract_impl(source, file, Some(rules))
}
}
impl RustExtractor {
fn extract_impl(
&self,
source: &str,
file: &str,
rules: Option<&BindingRules>,
) -> 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)?;
mark_self_receiver_calls(
&root,
&ts_language,
SELF_CALL_QUERY,
Language::Rust,
bytes,
&mut references,
None,
)?;
mark_receiver_qualifier_calls(
&root,
&ts_language,
RECEIVER_CALL_QUERY,
Language::Rust,
bytes,
&mut references,
)?;
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_associated_call_type_references(&root, &ts_language, bytes, file, &mut references)?;
collect_read_references(&root, bytes, file, &mut references);
collect_field_access_references(&root, bytes, file, &mut references);
collect_write_references(&root, bytes, file, &mut references);
#[cfg(feature = "sql")]
if let Some(rules) = rules {
collect_query_bindings(&root, bytes, file, rules, &mut references);
}
#[cfg(not(feature = "sql"))]
let _ = rules;
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() {
match last.as_str() {
"mod" => {
segs.pop();
}
"lib" | "main" if segs.len() == 1 => {
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: crate::symbol::MethodDisambiguator::empty(),
},
)
}
"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 type_name = impl_type_name(&child, bytes);
if child.child_by_field_name("trait").is_none() {
collect_impl_members(&child, ctx, namespaces, &type_name, &mut out);
}
continue;
}
_ => continue,
};
let visibility = 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::Trait {
collect_trait_members(&child, ctx, namespaces, &sym_name, &mut out);
}
if kind == SymbolKind::Struct {
collect_struct_fields(&child, ctx, namespaces, &sym_name, &mut out);
}
if kind == SymbolKind::Enum {
collect_enum_variants(&child, ctx, namespaces, &sym_name, &mut out);
}
}
out
}
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: crate::symbol::MethodDisambiguator::empty(),
},
)
}
"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: crate::symbol::MethodDisambiguator::empty(),
},
)
}
"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 collect_struct_fields(
struct_node: &Node,
ctx: &ExtractCtx,
namespaces: &[String],
struct_name: &str,
out: &mut Vec<Symbol>,
) {
let bytes = ctx.bytes;
let Some(body) = struct_node.child_by_field_name("body") else {
return;
};
for field in body.children(&mut body.walk()) {
if field.kind() != "field_declaration" {
continue;
}
let Some(name) = field_text(&field, "name", bytes) else {
continue;
};
let visibility = read_visibility(&field, bytes);
let descriptors =
member_descriptors(namespaces, struct_name, Descriptor::Term(name.clone()));
let signature = one_line_signature(node_text(&field, bytes), &['{']);
out.push(make_symbol(
ctx,
&field,
name,
SymbolKind::Field,
visibility,
descriptors,
signature,
));
}
}
fn collect_enum_variants(
enum_node: &Node,
ctx: &ExtractCtx,
namespaces: &[String],
enum_name: &str,
out: &mut Vec<Symbol>,
) {
let bytes = ctx.bytes;
let Some(body) = enum_node.child_by_field_name("body") else {
return;
};
for variant in body.children(&mut body.walk()) {
if variant.kind() != "enum_variant" {
continue;
}
let Some(name) = field_text(&variant, "name", bytes) else {
continue;
};
let visibility = read_visibility(&variant, bytes);
let descriptors = member_descriptors(namespaces, enum_name, Descriptor::Term(name.clone()));
let signature = one_line_signature(node_text(&variant, bytes), &['{']);
out.push(make_symbol(
ctx,
&variant,
name,
SymbolKind::Variant,
visibility,
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 {
node.child_by_field_name("type")
.map(|self_type| super::simple_type_name(node_text(&self_type, bytes), "::").to_owned())
.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,
);
if let Some(reference) = out.last_mut() {
reference.qualifier = Some(impl_type_name(node, bytes));
}
}
}
"trait_item" => {
let subject = child_text(node, "type_identifier", bytes);
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,
);
if let (Some(reference), Some(subject)) = (out.last_mut(), &subject) {
reference.qualifier = Some(subject.clone());
}
}
_ => {}
}
}
}
}
_ => {}
}
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" => {
let first = out.len();
if let Some(path_node) = node.child_by_field_name("path") {
collect_use_leaves(&path_node, bytes, file, out, module_id, prefix);
}
if let Some(alias) = node.child_by_field_name("alias") {
let alias = node_text(&alias, bytes).to_owned();
for reference in out[first..]
.iter_mut()
.filter(|reference| reference.role == RefRole::Import)
{
reference.imported_name = Some(reference.name.clone());
reference.name = alias.clone();
}
}
}
"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" {
let first = out.len();
if let Some(arg) = node.child_by_field_name("argument") {
collect_use_leaves(&arg, bytes, file, out, module_id, "");
}
if read_visibility(node, bytes) == Visibility::Public {
for reference in out[first..]
.iter_mut()
.filter(|reference| reference.role == RefRole::Import)
{
reference.is_reexport = true;
}
}
return;
}
for child in node.children(&mut node.walk()) {
collect_imports(&child, bytes, file, out, module_id);
}
}
fn collect_named_type_nodes(
node: &Node,
bytes: &[u8],
context: TypeRefContext,
file: &str,
out: &mut Vec<Reference>,
) {
match node.kind() {
"primitive_type" | "lifetime" => {}
"type_identifier" => out.push(Reference {
name: node_text(node, bytes).to_owned(),
occ: node_occurrence(node, file),
role: RefRole::TypeRef,
source_module: None,
from_path: None,
is_reexport: false,
imported_name: None,
qualifier: None,
scope: None,
type_ref_ctx: Some(context),
cross_artifact: false,
self_receiver: false,
}),
"scoped_type_identifier" => {
let Some(name_node) = node.child_by_field_name("name") else {
return;
};
out.push(Reference {
name: node_text(&name_node, bytes).to_owned(),
occ: node_occurrence(&name_node, file),
role: RefRole::TypeRef,
source_module: None,
from_path: None,
is_reexport: false,
imported_name: None,
qualifier: node
.child_by_field_name("path")
.map(|path| node_text(&path, bytes).to_owned()),
scope: None,
type_ref_ctx: Some(context),
cross_artifact: false,
self_receiver: false,
});
}
"generic_type" => {
if let Some(base) = node.child_by_field_name("type") {
collect_named_type_nodes(&base, bytes, context, file, out);
}
if let Some(args) = node.child_by_field_name("type_arguments") {
for child in args.named_children(&mut args.walk()) {
collect_named_type_nodes(&child, bytes, TypeRefContext::GenericArg, file, out);
}
}
}
_ => {
for child in node.named_children(&mut node.walk()) {
collect_named_type_nodes(&child, bytes, context, file, out);
}
}
}
}
fn collect_associated_call_type_references(
root: &Node,
ts_lang: &TsLanguage,
bytes: &[u8],
file: &str,
out: &mut Vec<Reference>,
) -> Result<()> {
let query =
Query::new(ts_lang, ASSOCIATED_CALL_SUBJECT_QUERY).map_err(|e| CodegraphError::Query {
lang: "rust".to_owned(),
msg: e.to_string(),
})?;
let subject_idx =
query
.capture_index_for_name("subject")
.ok_or_else(|| CodegraphError::Query {
lang: "rust".to_owned(),
msg: "missing @subject capture".to_owned(),
})?;
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&query, *root, bytes);
while let Some(m) = matches.next() {
for capture in m
.captures
.iter()
.filter(|capture| capture.index == subject_idx)
{
let written = node_text(&capture.node, bytes);
let name = super::simple_type_name(written, "::");
if name.len() < MIN_REF_LEN {
continue;
}
let qualifier = written
.rsplit_once("::")
.map(|(prefix, _)| prefix.to_owned());
out.push(Reference {
name: name.to_owned(),
occ: node_occurrence(&capture.node, file),
role: RefRole::TypeRef,
source_module: None,
from_path: None,
is_reexport: false,
imported_name: None,
qualifier,
scope: None,
type_ref_ctx: Some(TypeRefContext::Other),
cross_artifact: false,
self_receiver: false,
});
}
}
Ok(())
}
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 capture_context = |index| {
if Some(index) == query.capture_index_for_name("parameter_ty") {
Some(TypeRefContext::ParameterType)
} else if Some(index) == query.capture_index_for_name("return_ty") {
Some(TypeRefContext::ReturnType)
} else if Some(index) == query.capture_index_for_name("field_ty") {
Some(TypeRefContext::Field)
} else {
None
}
};
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&query, *root, bytes);
while let Some(m) = matches.next() {
for cap in m.captures {
if let Some(context) = capture_context(cap.index) {
collect_named_type_nodes(&cap.node, bytes, context, file, out);
}
}
}
let mut seen = std::collections::HashSet::new();
out.retain(|reference| {
reference.role != RefRole::TypeRef
|| seen.insert((
reference.occ.byte,
reference.name.clone(),
reference.type_ref_ctx,
))
});
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 collect_field_access_references(
node: &Node,
bytes: &[u8],
file: &str,
out: &mut Vec<Reference>,
) {
if matches!(node.kind(), "macro_definition" | "macro_invocation") {
return;
}
if node.kind() == "field_expression" {
if let Some(field) = node.child_by_field_name("field") {
if field.kind() == "field_identifier" && !is_method_call_callee(node) {
let name = node_text(&field, bytes);
if name.len() >= MIN_REF_LEN {
let (qualifier, self_receiver) = match node.child_by_field_name("value") {
Some(v) if v.kind() == "self" => (None, true),
Some(v) if v.kind() == "identifier" => {
(Some(node_text(&v, bytes).to_owned()), false)
}
_ => (None, false),
};
out.push(Reference {
name: name.to_owned(),
occ: node_occurrence(&field, file),
role: RefRole::Read,
source_module: None,
from_path: None,
is_reexport: false,
imported_name: None,
qualifier,
scope: None,
type_ref_ctx: None,
cross_artifact: false,
self_receiver,
});
}
}
}
}
for child in node.children(&mut node.walk()) {
collect_field_access_references(&child, bytes, file, out);
}
}
fn is_method_call_callee(field_expr: &Node) -> bool {
field_expr
.parent()
.filter(|p| p.kind() == "call_expression")
.and_then(|p| p.child_by_field_name("function"))
.as_ref()
== Some(field_expr)
}
#[cfg(feature = "sql")]
fn collect_query_bindings(
node: &Node,
bytes: &[u8],
file: &str,
rules: &BindingRules,
out: &mut Vec<Reference>,
) {
match node.kind() {
"call_expression" => {
if let Some(func) = node.child_by_field_name("function") {
if func.kind() == "scoped_identifier" {
let callee = node_text(&func, bytes);
for rule in rules.for_language(Language::Rust) {
if rule.construct != callee {
continue;
}
let Some(arguments) = node.child_by_field_name("arguments") else {
continue;
};
let Some(arg) = arguments
.named_children(&mut arguments.walk())
.nth(rule.sql_arg)
else {
continue;
};
emit_bound_sql_refs(&arg, bytes, file, out);
}
}
}
}
"macro_invocation" => {
if let Some(mac) = node.child_by_field_name("macro") {
let callee = node_text(&mac, bytes);
for rule in rules.for_language(Language::Rust) {
if rule.construct != callee {
continue;
}
let Some(token_tree) = node
.children(&mut node.walk())
.find(|c| c.kind() == "token_tree")
else {
continue;
};
let Some(arg) = nth_macro_string_arg(&token_tree, rule.sql_arg) else {
continue;
};
emit_bound_sql_refs(&arg, bytes, file, out);
}
}
}
_ => {}
}
for child in node.children(&mut node.walk()) {
collect_query_bindings(&child, bytes, file, rules, out);
}
}
#[cfg(feature = "sql")]
fn nth_macro_string_arg<'a>(token_tree: &Node<'a>, index: usize) -> Option<Node<'a>> {
let mut group = 0usize;
for child in token_tree.children(&mut token_tree.walk()) {
if child.kind() == "," {
group += 1;
continue;
}
if group == index && matches!(child.kind(), "string_literal" | "raw_string_literal") {
return Some(child);
}
}
None
}
#[cfg(feature = "sql")]
fn emit_bound_sql_refs(arg: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if !matches!(arg.kind(), "string_literal" | "raw_string_literal") {
return;
}
emit_embedded_sql_refs(arg, "string_content", 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();
let type_name = let_declaration_type_name(node, bytes);
push_typed_binding(out, name, intro, BindingKind::Local, scopes, type_name);
}
}
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 let_declaration_type_name(let_decl: &Node, bytes: &[u8]) -> Option<String> {
if let Some(type_node) = let_decl.child_by_field_name("type") {
return Some(simple_type_name(node_text(&type_node, bytes), "::").to_owned());
}
let value_node = let_decl.child_by_field_name("value")?;
match value_node.kind() {
"struct_expression" => {
let name_node = value_node.child_by_field_name("name")?;
Some(simple_type_name(node_text(&name_node, bytes), "::").to_owned())
}
"call_expression" => {
let function_node = value_node.child_by_field_name("function")?;
(function_node.kind() == "scoped_identifier")
.then(|| function_node.child_by_field_name("path"))
.flatten()
.map(|path_node| simple_type_name(node_text(&path_node, bytes), "::").to_owned())
}
_ => None,
}
}
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();
let type_name = child
.child_by_field_name("type")
.map(|t| simple_type_name(node_text(&t, bytes), "::").to_owned());
push_typed_binding(out, name, intro, BindingKind::Param, scopes, type_name);
}
}
}
"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_struct_fields_as_field_symbols() {
let src = r#"
pub struct Config { pub value: u32, name: String }
pub struct P(u32);
"#;
let facts = RustExtractor.extract(src, "src/cfg.rs").unwrap();
let value = facts
.symbols
.iter()
.find(|s| s.name == "value")
.expect("struct field `value`");
assert_eq!(value.kind, SymbolKind::Field);
assert!(value.id.to_scip_string().contains("Config#value."));
let name = facts
.symbols
.iter()
.find(|s| s.name == "name")
.expect("struct field `name`");
assert_eq!(name.kind, SymbolKind::Field);
assert!(name.id.to_scip_string().contains("Config#name."));
assert!(
!facts
.symbols
.iter()
.any(|s| s.kind == SymbolKind::Field && s.id.to_scip_string().contains("P#")),
"tuple-struct positional fields must not be emitted"
);
}
#[test]
fn extracts_enum_variants_as_variant_symbols() {
let src = "pub enum State { Idle, Running }";
let facts = RustExtractor.extract(src, "src/state.rs").unwrap();
let idle = facts
.symbols
.iter()
.find(|s| s.name == "Idle")
.expect("enum variant `Idle`");
assert_eq!(idle.kind, SymbolKind::Variant);
assert!(idle.id.to_scip_string().contains("State#Idle."));
let running = facts
.symbols
.iter()
.find(|s| s.name == "Running")
.expect("enum variant `Running`");
assert_eq!(running.kind, SymbolKind::Variant);
assert!(running.id.to_scip_string().contains("State#Running."));
}
#[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 impl_block_does_not_duplicate_its_type_definition() {
let facts = RustExtractor
.extract("pub struct Point; impl Point {}", "src/point.rs")
.unwrap();
let point_ids: Vec<_> = facts
.symbols
.iter()
.filter(|symbol| symbol.id.leaf_name() == Some("Point"))
.map(|symbol| symbol.id.clone())
.collect();
assert_eq!(point_ids.len(), 1, "impl containers must not mint symbols");
}
#[test]
fn multiple_impl_blocks_share_one_type_definition() {
let facts = RustExtractor
.extract(
"pub struct Point; impl Point { pub fn x(&self) {} } impl Point { pub fn y(&self) {} }",
"src/point.rs",
)
.unwrap();
let point_ids: Vec<_> = facts
.symbols
.iter()
.filter(|symbol| symbol.id.leaf_name() == Some("Point"))
.map(|symbol| symbol.id.clone())
.collect();
assert_eq!(point_ids.len(), 1, "impl containers must not mint symbols");
}
#[test]
fn generic_impl_members_use_the_declared_type_identity() {
let facts = RustExtractor
.extract(
"pub struct Wrapper<T>(T); impl<T> Wrapper<T> { pub fn get(&self) -> &T { &self.0 } }",
"src/wrapper.rs",
)
.unwrap();
let get = facts
.symbols
.iter()
.find(|symbol| symbol.name == "get")
.expect("impl member");
assert!(get.id.to_scip_string().contains("Wrapper#get()."));
assert!(!get.id.to_scip_string().contains("Wrapper<T>"));
}
#[test]
fn trait_and_inherent_impls_share_one_type_definition() {
let facts = RustExtractor
.extract(
"pub trait Draw {} pub struct Point; impl Draw for Point {} impl Point {}",
"src/point.rs",
)
.unwrap();
let point_ids: Vec<_> = facts
.symbols
.iter()
.filter(|symbol| symbol.id.leaf_name() == Some("Point"))
.map(|symbol| symbol.id.clone())
.collect();
assert_eq!(point_ids.len(), 1, "impl containers must not mint symbols");
assert!(facts.references.iter().any(|reference| {
reference.role == RefRole::IsImplementation && reference.name == "Draw"
}));
}
#[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_preserves_alias_and_source_leaf() {
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!["c"],
"expected ['c'], got {import_names:?}"
);
assert_eq!(
facts
.references
.iter()
.find(|reference| reference.role == RefRole::Import)
.and_then(|reference| reference.imported_name.as_deref()),
Some("b")
);
}
#[test]
fn qualified_reexport_metadata_is_confined_to_import_leaves() {
for src in [
"pub use inner::Thing as T;",
"pub use inner::deep;",
"pub use inner::deep::d;",
"pub use crate::inner::helper;",
] {
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
crate::validate_file_facts_with_context(
&facts,
crate::FileFactsValidationContext {
expected_file: "src/lib.rs",
expected_language: Language::Rust,
source_len: src.len(),
},
)
.unwrap();
assert!(
facts.references.iter().any(|reference| {
reference.role == RefRole::Import && reference.is_reexport
})
);
assert!(facts.references.iter().all(|reference| {
reference.role == RefRole::Import
|| (!reference.is_reexport && reference.imported_name.is_none())
}));
}
}
#[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 nested_crate_root_lib_and_main_have_distinct_module_ids() {
let lib = crate::extract::module_symbol(
Language::Rust,
&rust_namespaces("app/src/lib.rs"),
"app/src/lib.rs",
0,
);
let main = crate::extract::module_symbol(
Language::Rust,
&rust_namespaces("app/src/main.rs"),
"app/src/main.rs",
0,
);
assert_ne!(
lib.id.to_scip_string(),
main.id.to_scip_string(),
"nested lib.rs and main.rs must have distinct crate-root module ids"
);
}
#[test]
fn crate_root_lib_and_main_collapse_to_empty_namespace() {
assert!(rust_namespaces("src/lib.rs").is_empty());
assert!(rust_namespaces("src/main.rs").is_empty());
assert_eq!(rust_namespaces("src/util/lib.rs"), vec!["util", "lib"]);
}
#[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 field_expression_call_captures_receiver_as_qualifier() {
let src = "fn f() { let r: Repo = Repo; r.save(); }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let save_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Call && r.name == "save")
.collect();
assert_eq!(
save_refs.len(),
1,
"expected exactly one Call ref for 'save', got {save_refs:?}"
);
assert_eq!(save_refs[0].qualifier.as_deref(), Some("r"));
assert!(!save_refs[0].self_receiver);
}
#[test]
fn self_receiver_call_keeps_qualifier_none() {
let src = "impl Person { fn greet(&self) { self.hello(); } }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let hello_ref = facts
.references
.iter()
.find(|r| r.role == RefRole::Call && r.name == "hello")
.expect("expected a Call ref for 'hello'");
assert!(hello_ref.self_receiver);
assert_eq!(hello_ref.qualifier, None);
}
#[test]
fn let_with_type_annotation_sets_binding_type_name() {
let src = "fn f() { let r: Repo = Repo; r.save(); }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let r_binding = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Local && b.name == "r")
.expect("expected a Local binding for 'r'");
assert_eq!(r_binding.type_name.as_deref(), Some("Repo"));
}
#[test]
fn param_type_sets_binding_type_name() {
let src = "fn f(r: Repo) {}";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let r_binding = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Param && b.name == "r")
.expect("expected a Param binding for 'r'");
assert_eq!(r_binding.type_name.as_deref(), Some("Repo"));
}
#[test]
fn let_without_annotation_or_recognized_constructor_leaves_type_name_none() {
let src = "fn f() { let r = Repo; r.save(); }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let r_binding = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Local && b.name == "r")
.expect("expected a Local binding for 'r'");
assert_eq!(r_binding.type_name, None);
}
#[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_captures_receiver_as_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.as_deref(),
Some("obj"),
"method call via field_expression should capture the receiver `obj` as qualifier, got {:?}",
r.qualifier
);
assert!(
!r.self_receiver,
"a non-self receiver must not be marked self_receiver"
);
}
#[test]
fn self_receiver_method_call_is_marked_self_receiver() {
let src = "impl Person { fn caller(&self) { self.foo(); } }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let r = facts
.references
.iter()
.find(|r| r.role == RefRole::Call && r.name == "foo")
.expect("expected a Call ref for 'foo'");
assert!(
r.self_receiver,
"self.foo() should have self_receiver == true"
);
assert_eq!(
r.qualifier, None,
"self.foo() should still have qualifier == None, got {:?}",
r.qualifier
);
}
#[test]
fn non_self_method_call_is_not_marked_self_receiver() {
let src = "impl Person { fn caller(&self, x: Foo) { x.foo(); } }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let r = facts
.references
.iter()
.find(|r| r.role == RefRole::Call && r.name == "foo")
.expect("expected a Call ref for 'foo'");
assert!(
!r.self_receiver,
"x.foo() must not have self_receiver == true"
);
}
#[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_and_argument_are_captured() {
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"),
"expected generic argument in {names:?}"
);
assert_eq!(
type_refs(&facts)
.into_iter()
.find(|reference| reference.name == "Config")
.and_then(|reference| reference.type_ref_ctx),
Some(TypeRefContext::GenericArg)
);
}
#[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_compound_types_capture_all_named_components() {
let facts = RustExtractor
.extract(
"struct Holder { value: Option<Vec<Config>>, pair: (Left, Right) }",
"src/lib.rs",
)
.unwrap();
let references = type_refs(&facts);
for name in ["Option", "Vec", "Config", "Left", "Right"] {
assert!(
references.iter().any(|reference| reference.name == name),
"missing {name}"
);
}
assert_eq!(
references
.iter()
.find(|reference| reference.name == "Config")
.and_then(|reference| reference.type_ref_ctx),
Some(TypeRefContext::GenericArg)
);
}
#[test]
fn associated_call_subject_emits_type_reference() {
let src = "fn parse(node: &Node) { crate::params::FusionParams::extract(node); }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let reference = type_refs(&facts)
.into_iter()
.find(|reference| reference.name == "FusionParams")
.expect("associated call subject TypeRef");
assert_eq!(reference.qualifier.as_deref(), Some("crate::params"));
}
#[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_is_a_read_of_the_field() {
let src = "fn f(c: Config) -> i32 { c.value }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let field_reads: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "value")
.collect();
assert_eq!(
field_reads.len(),
1,
"expected exactly one Read ref for field 'value'; got: {field_reads:?}"
);
assert_eq!(
field_reads[0].qualifier.as_deref(),
Some("c"),
"field read 'value' must carry the receiver 'c' as qualifier"
);
assert!(
!field_reads[0].self_receiver,
"a bare-identifier receiver is not a self receiver"
);
}
#[test]
fn method_call_not_also_a_field_read() {
let src = "fn f(x: X) { x.foo(); }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let call_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Call && r.name == "foo")
.collect();
assert_eq!(
call_refs.len(),
1,
"expected exactly one Call ref for 'foo'; got: {call_refs:?}"
);
let read_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "foo")
.collect();
assert!(
read_refs.is_empty(),
"x.foo() must NOT also produce a Read ref for 'foo'; got: {read_refs:?}"
);
}
#[test]
fn self_field_read_marks_self_receiver() {
let src = "impl S { fn f(&self) -> i32 { self.value } }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let read = facts
.references
.iter()
.find(|r| r.role == RefRole::Read && r.name == "value")
.expect("expected a Read ref for 'value' from self.value");
assert!(
read.self_receiver,
"self.value must set self_receiver = true"
);
assert_eq!(
read.qualifier, None,
"self.value must clear qualifier (self is not a resolvable receiver)"
);
}
#[test]
fn chained_receiver_field_read_has_no_qualifier() {
let src = "fn f() -> i32 { a().field }";
let facts = RustExtractor.extract(src, "src/lib.rs").unwrap();
let read = facts
.references
.iter()
.find(|r| r.role == RefRole::Read && r.name == "field")
.expect("expected a Read ref for 'field' from a().field");
assert_eq!(
read.qualifier, None,
"a().field must leave qualifier = None (receiver is not a bare identifier)"
);
assert!(!read.self_receiver, "a().field is not a self receiver");
}
#[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_and_container_are_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.kind == SymbolKind::Impl),
"impl containers must not mint symbols"
);
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 ffi_exports_require_exact_unconditional_attributes() {
let cases = [
(
"#[doc = \"no_mangle\"]\npub extern \"C\" fn documented() {}",
"documented",
),
(
"#[other_no_mangle]\npub extern \"C\" fn lookalike() {}",
"lookalike",
),
(
"#[export_name_note = \"ffi_name\"]\npub extern \"C\" fn note() {}",
"note",
),
(
"#[cfg_attr(feature = \"ffi\", no_mangle)]\npub extern \"C\" fn conditional() {}",
"conditional",
),
(
"#[cfg_attr(feature = \"ffi\", doc = \"no_mangle\")]\npub extern \"C\" fn quoted() {}",
"quoted",
),
(
"#[cfg_attr(feature = \"ffi\", unrelated::no_mangle)]\npub extern \"C\" fn nested_unrelated() {}",
"nested_unrelated",
),
];
let incorrectly_exported: Vec<_> = cases
.iter()
.filter_map(|(source, name)| {
let facts = RustExtractor.extract(source, "src/ffi.rs").unwrap();
(!facts.ffi_exports.is_empty()).then_some(*name)
})
.collect();
assert!(
incorrectly_exported.is_empty(),
"only exact, unconditional supported attributes may create FFI facts; got {incorrectly_exported:?}"
);
}
#[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]).unwrap();
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<_>>()
);
}
#[cfg(feature = "sql")]
#[test]
fn sqlx_query_call_emits_cross_artifact_typeref() {
let src = r#"pub fn f() { sqlx::query("SELECT id FROM users"); }"#;
let facts = RustExtractor
.extract_with_bindings(src, "src/app.rs", &BindingRules::with_defaults())
.unwrap();
let found = facts
.references
.iter()
.find(|r| r.role == RefRole::TypeRef && r.name == "users" && r.cross_artifact);
let r = found.expect("expected a cross-artifact TypeRef reference named 'users'");
let select_byte = src.find("SELECT").expect("fixture contains SELECT");
assert!(
r.occ.byte >= select_byte,
"reference byte {} should point at/after 'SELECT' at {}",
r.occ.byte,
select_byte
);
}
#[cfg(feature = "sql")]
#[test]
fn sqlx_query_macro_emits_cross_artifact_typeref() {
let src = r#"pub fn f() { sqlx::query!("SELECT id FROM users"); }"#;
let facts = RustExtractor
.extract_with_bindings(src, "src/app.rs", &BindingRules::with_defaults())
.unwrap();
let found = facts
.references
.iter()
.find(|r| r.role == RefRole::TypeRef && r.name == "users" && r.cross_artifact);
let r = found.expect(
"expected a cross-artifact TypeRef reference named 'users' from the macro form",
);
let select_byte = src.find("SELECT").expect("fixture contains SELECT");
assert!(
r.occ.byte >= select_byte,
"reference byte {} should point at/after 'SELECT' at {}",
r.occ.byte,
select_byte
);
}
#[cfg(feature = "sql")]
#[test]
fn empty_binding_rules_yield_no_cross_artifact_reference() {
let src = r#"pub fn f() { sqlx::query("SELECT id FROM users"); }"#;
let file = "src/app.rs";
let with_empty_rules = RustExtractor
.extract_with_bindings(src, file, &BindingRules::empty())
.unwrap();
assert!(
!with_empty_rules.references.iter().any(|r| r.cross_artifact),
"an empty binding-rule registry must yield no cross-artifact references"
);
let plain = RustExtractor.extract(src, file).unwrap();
assert!(
!plain.references.iter().any(|r| r.cross_artifact),
"the plain extract() path must yield no cross-artifact references"
);
}
#[cfg(feature = "sql")]
#[test]
fn cross_artifact_query_binding_resolves_to_sql_table() {
use crate::extract::SqlExtractor;
use crate::graph::{Confidence, Provenance};
use crate::resolve::{Resolver, SymbolTableResolver};
let schema = SqlExtractor
.extract("CREATE TABLE users (id INT);", "db/schema.sql")
.unwrap();
let src = r#"pub fn f() { sqlx::query("SELECT id FROM users"); }"#;
let rust_file = RustExtractor
.extract_with_bindings(src, "src/app.rs", &BindingRules::with_defaults())
.unwrap();
let graph = SymbolTableResolver.resolve(&[schema, rust_file]).unwrap();
let edges_to_users: Vec<_> = graph
.edges
.iter()
.filter(|e| e.role == RefRole::TypeRef && e.to.to_scip_string().ends_with("users#"))
.collect();
assert_eq!(
edges_to_users.len(),
1,
"expected one Code→SQL TypeRef edge to 'users#', got: {:?}",
edges_to_users
.iter()
.map(|e| format!("{} -> {}", e.from.to_scip_string(), e.to.to_scip_string()))
.collect::<Vec<_>>()
);
let e = edges_to_users[0];
assert_eq!(
e.provenance,
Provenance::CrossArtifact,
"query-binding edge must carry Provenance::CrossArtifact, got {:?}",
e.provenance
);
assert_eq!(
e.confidence,
Confidence::NameOnly,
"query-binding edge must carry Confidence::NameOnly, got {:?}",
e.confidence
);
}
}