use tree_sitter::Node;
use crate::core::{Kind, Symbol};
use crate::lang::{Ctx, LanguagePlugin, extract_with, qualify};
const LANGUAGE: &str = "rust";
pub(crate) struct Rust;
impl LanguagePlugin for Rust {
fn language(&self) -> &'static str {
LANGUAGE
}
fn extensions(&self) -> &[&str] {
&["rs"]
}
fn extract(&self, file: &str, source: &str) -> Vec<Symbol> {
extract_with(
LANGUAGE,
tree_sitter_rust::LANGUAGE.into(),
file,
source,
|ctx, root, out| walk(ctx, root, None, out),
)
}
}
fn walk(ctx: &Ctx, node: Node, parent: Option<&str>, out: &mut Vec<Symbol>) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"function_item" | "function_signature_item" => {
if let Some(name) = ctx.field_text(child, "name") {
let kind = if has_self(child) {
Kind::Method
} else {
Kind::Function
};
push(ctx, out, &name, kind, child, parent);
}
}
"struct_item" | "union_item" => {
if let Some(name) = ctx.field_text(child, "name") {
push(ctx, out, &name, Kind::Struct, child, parent);
}
}
"enum_item" => {
if let Some(name) = ctx.field_text(child, "name") {
push(ctx, out, &name, Kind::Enum, child, parent);
variants(ctx, child, &qualify(parent, &name, "::"), out);
}
}
"type_item" if !in_impl_or_trait(child) => {
if let Some(name) = ctx.field_text(child, "name") {
push(ctx, out, &name, Kind::Type, child, parent);
}
}
"macro_definition" => {
if let Some(name) = ctx.field_text(child, "name") {
let mut s = ctx.symbol(&name, Kind::Macro, child, parent);
s.visibility = Some(if exported(ctx, child) {
"public"
} else {
"crate"
});
out.push(s);
}
}
"macro_invocation" => {
if let Some(body) = braced_body(child) {
ctx.walk_fragment(&tree_sitter_rust::LANGUAGE.into(), body, |ctx, root| {
walk(ctx, root, parent, out)
});
}
}
"const_item" | "static_item" => {
if let Some(name) = ctx.field_text(child, "name") {
push(ctx, out, &name, Kind::Constant, child, parent);
}
}
"trait_item" => {
if let Some(name) = ctx.field_text(child, "name") {
push(ctx, out, &name, Kind::Trait, child, parent);
let qualified = qualify(parent, &name, "::");
walk(ctx, child, Some(&qualified), out);
}
}
"mod_item" => {
if child.child_by_field_name("body").is_some()
&& let Some(name) = ctx.field_text(child, "name")
{
push(ctx, out, &name, Kind::Module, child, parent);
let qualified = qualify(parent, &name, "::");
walk(ctx, child, Some(&qualified), out);
}
}
"impl_item" => {
let ty = ctx.field_text(child, "type").map(|t| base_type(&t));
let qualified = match &ty {
Some(t) => qualify(parent, t, "::"),
None => parent.map(str::to_string).unwrap_or_default(),
};
let p = if qualified.is_empty() {
None
} else {
Some(qualified.as_str())
};
walk(ctx, child, p, out);
}
_ => walk(ctx, child, parent, out),
}
}
}
fn variants(ctx: &Ctx, node: Node, qualified: &str, out: &mut Vec<Symbol>) {
let Some(body) = node.child_by_field_name("body") else {
return;
};
let vis = visibility(ctx, node);
let mut cursor = body.walk();
for v in body.named_children(&mut cursor) {
if v.kind() == "enum_variant"
&& let Some(name) = ctx.field_text(v, "name")
{
let mut s = ctx.symbol(&name, Kind::Variant, v, Some(qualified));
s.visibility = Some(vis);
out.push(s);
}
}
}
fn in_impl_or_trait(node: Node) -> bool {
node.parent()
.and_then(|list| list.parent())
.is_some_and(|owner| matches!(owner.kind(), "impl_item" | "trait_item"))
}
fn exported(ctx: &Ctx, node: Node) -> bool {
let mut prev = node.prev_sibling();
while let Some(p) = prev.filter(|p| matches!(p.kind(), "attribute_item" | "line_comment")) {
if ctx.node_text(p).is_some_and(|t| t.contains("macro_export")) {
return true;
}
prev = p.prev_sibling();
}
false
}
fn braced_body(node: Node) -> Option<(usize, usize, usize)> {
let mut cursor = node.walk();
let tt = node
.children(&mut cursor)
.find(|c| c.kind() == "token_tree")?;
let open = tt.child(0)?;
(open.kind() == "{").then(|| (open.end_byte(), tt.end_byte() - 1, tt.start_position().row))
}
fn push(ctx: &Ctx, out: &mut Vec<Symbol>, name: &str, kind: Kind, node: Node, p: Option<&str>) {
let mut s = ctx.symbol(name, kind, node, p);
s.visibility = item_visibility(ctx, node);
out.push(s);
}
fn item_visibility(ctx: &Ctx, node: Node) -> Option<&'static str> {
let owner = node.parent().and_then(|list| list.parent());
match owner {
Some(t) if t.kind() == "trait_item" => Some(visibility(ctx, t)),
Some(i) if i.kind() == "impl_item" && i.child_by_field_name("trait").is_some() => None,
_ => Some(visibility(ctx, node)),
}
}
fn visibility(ctx: &Ctx, node: Node) -> &'static str {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "visibility_modifier" {
let text = ctx.node_text(child).unwrap_or_default();
return if text.contains('(') {
"crate"
} else {
"public"
};
}
}
"private"
}
fn has_self(node: Node) -> bool {
node.child_by_field_name("parameters")
.is_some_and(|params| {
let mut cursor = params.walk();
params
.children(&mut cursor)
.any(|p| p.kind() == "self_parameter")
})
}
fn base_type(ty: &str) -> String {
let head = ty.split('<').next().unwrap_or(ty).trim();
head.rsplit("::").next().unwrap_or(head).trim().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lang::testing::find;
fn extract(source: &str) -> Vec<Symbol> {
Rust.extract("test.rs", source)
}
#[test]
fn extracts_types_functions_and_impl_methods() {
let src = r#"
pub struct Widget {
size: u32,
}
pub enum Color {
Red,
Green,
}
pub trait Render {
fn render(&self) -> String;
}
impl Widget {
pub fn new() -> Self {
Widget { size: 0 }
}
}
pub fn build() -> Widget {
Widget::new()
}
"#;
let syms = extract(src);
let widget = find(&syms, "Widget");
assert_eq!(widget.kind, Kind::Struct);
assert_eq!(widget.parent, None);
assert_eq!(find(&syms, "Color").kind, Kind::Enum);
assert_eq!(find(&syms, "Render").kind, Kind::Trait);
let build = find(&syms, "build");
assert_eq!(build.kind, Kind::Function);
assert_eq!(build.parent, None);
let new = find(&syms, "new");
assert_eq!(new.kind, Kind::Function);
assert_eq!(new.parent.as_deref(), Some("Widget"));
let render = find(&syms, "render");
assert_eq!(render.kind, Kind::Method);
assert_eq!(render.parent.as_deref(), Some("Render"));
assert_eq!(render.visibility, Some("public"));
assert_eq!(widget.language, "rust");
}
#[test]
fn qualifies_through_modules_and_generic_impls() {
let src = r#"
mod outer {
pub struct Store<T> {
inner: T,
}
impl<T> Store<T> {
pub fn get(&self) -> &T {
&self.inner
}
}
}
"#;
let syms = extract(src);
assert_eq!(find(&syms, "outer").kind, Kind::Module);
assert_eq!(find(&syms, "Store").parent.as_deref(), Some("outer"));
assert_eq!(find(&syms, "get").parent.as_deref(), Some("outer::Store"));
}
#[test]
fn bare_module_declarations_are_not_indexed() {
let syms = extract("mod search;\nmod handler { pub fn run() {} }\n");
assert!(
!syms.iter().any(|s| s.name == "search"),
"bare `mod search;` should be skipped: {syms:?}"
);
assert_eq!(find(&syms, "handler").kind, Kind::Module);
assert_eq!(find(&syms, "run").kind, Kind::Function);
}
#[test]
fn empty_and_unparseable_yield_no_symbols() {
assert!(extract("").is_empty());
assert!(extract("// just a comment\n").is_empty());
}
#[test]
fn consts_and_statics_are_constants() {
let src = r#"
pub const MAX: u32 = 10;
static NAME: &str = "x";
pub struct Widget;
impl Widget {
pub const DEFAULT: u32 = 1;
}
"#;
let syms = extract(src);
let max = find(&syms, "MAX");
assert_eq!(max.kind, Kind::Constant);
assert_eq!(max.visibility, Some("public"));
assert_eq!(find(&syms, "NAME").visibility, Some("private"));
assert_eq!(find(&syms, "DEFAULT").parent.as_deref(), Some("Widget"));
}
#[test]
fn variants_aliases_and_macros_are_definitions() {
let src = r#"
pub enum Shape {
Circle,
Square { side: u32 },
}
pub type Result<T> = std::result::Result<T, Error>;
#[macro_export]
macro_rules! shout {
() => {};
}
macro_rules! helper {
() => {};
}
impl Iterator for Walker {
type Item = u32;
fn next(&mut self) -> Option<u32> { None }
}
"#;
let syms = extract(src);
assert_eq!(find(&syms, "next").visibility, None);
let square = find(&syms, "Square");
assert_eq!(square.kind, Kind::Variant);
assert_eq!(square.parent.as_deref(), Some("Shape"));
assert_eq!(square.visibility, Some("public"));
assert_eq!(find(&syms, "Result").kind, Kind::Type);
let shout = find(&syms, "shout");
assert_eq!(shout.kind, Kind::Macro);
assert_eq!(shout.visibility, Some("public"));
assert_eq!(find(&syms, "helper").visibility, Some("crate"));
assert!(!syms.iter().any(|s| s.name == "Item"), "{syms:?}");
}
#[test]
fn items_inside_a_braced_macro_call_keep_their_lines() {
let src = r#"
cfg_rt! {
/// docs
pub struct JoinHandle<T> {
raw: T,
}
cfg_net! {
pub fn spawn() {}
}
}
impl Runtime {
cfg_rt! {
pub fn block_on(&self) {}
}
}
not_items! { a => b, c }
"#;
let syms = extract(src);
let handle = find(&syms, "JoinHandle");
assert_eq!(
(handle.kind, handle.line, handle.end_line),
(Kind::Struct, 4, 6)
);
assert_eq!(handle.visibility, Some("public"));
assert_eq!(find(&syms, "spawn").line, 9);
let block_on = find(&syms, "block_on");
assert_eq!(block_on.parent.as_deref(), Some("Runtime"));
assert_eq!(block_on.line, 15);
assert_eq!(syms.len(), 3, "{syms:?}");
}
#[test]
fn visibility_reflects_the_pub_modifier() {
let src = "pub fn open() {}\npub(crate) fn shared() {}\nfn helper() {}\n";
let syms = extract(src);
assert_eq!(find(&syms, "open").visibility, Some("public"));
assert_eq!(find(&syms, "shared").visibility, Some("crate"));
assert_eq!(find(&syms, "helper").visibility, Some("private"));
}
}