use tree_sitter::{Parser, Query, QueryCursor};
use crate::parser::{EdgeDef, EdgeKind, LanguageParser, NodeDef, NodeKind, ParseResult};
use crate::walker::SourceFile;
pub struct RustParser {
language: tree_sitter::Language,
}
impl RustParser {
pub fn new() -> Self {
Self {
language: tree_sitter_rust::language(),
}
}
}
impl Default for RustParser {
fn default() -> Self {
Self::new()
}
}
impl LanguageParser for RustParser {
fn extensions(&self) -> &[&str] {
&["rs"]
}
fn extract(&self, file: &SourceFile) -> anyhow::Result<ParseResult> {
let mut parser = Parser::new();
parser.set_language(&self.language)?;
let tree = parser
.parse(&file.content, None)
.ok_or_else(|| anyhow::anyhow!("failed to parse {}", file.relative_path))?;
let source_bytes = file.content.as_bytes();
let root = tree.root_node();
let mut nodes = Vec::new();
let mut edges = Vec::new();
let fp = format!("file:{}", file.relative_path);
if let Ok(query) = Query::new(
&self.language,
"(function_item name: (identifier) @name) @fn",
) {
let mut cursor = QueryCursor::new();
for m in cursor.matches(&query, root, source_bytes) {
let Some(name_capture) = m
.captures
.iter()
.find(|c| query.capture_names()[c.index as usize] == "name")
else {
continue;
};
let name = node_text(name_capture.node, source_bytes);
let start = name_capture.node.start_position();
let body_end = m
.captures
.iter()
.find(|c| query.capture_names()[c.index as usize] == "fn")
.map(|c| c.node.end_position())
.unwrap_or_else(|| name_capture.node.end_position());
let id = format!("fn:{}:{}", file.relative_path, name);
nodes.push(NodeDef {
id: id.clone(),
kind: NodeKind::Function,
name,
path: file.relative_path.clone(),
line_start: start.row as u32 + 1,
line_end: body_end.row as u32 + 1,
..Default::default()
});
edges.push(EdgeDef {
src: fp.clone(),
dst: id,
kind: EdgeKind::Exports,
..Default::default()
});
}
}
if let Ok(query) = Query::new(
&self.language,
"(struct_item name: (type_identifier) @name) @s",
) {
extract_type_nodes(
&mut nodes,
&mut edges,
&fp,
file,
&query,
root,
source_bytes,
NodeKind::Class,
"cls",
);
}
if let Ok(query) = Query::new(
&self.language,
"(enum_item name: (type_identifier) @name) @e",
) {
extract_type_nodes(
&mut nodes,
&mut edges,
&fp,
file,
&query,
root,
source_bytes,
NodeKind::Class,
"cls",
);
}
if let Ok(query) = Query::new(
&self.language,
"(trait_item name: (type_identifier) @name) @t",
) {
extract_type_nodes(
&mut nodes,
&mut edges,
&fp,
file,
&query,
root,
source_bytes,
NodeKind::Class,
"cls",
);
}
if let Ok(query) = Query::new(
&self.language,
"(impl_item type: (type_identifier) @type body: (_) @body)",
) {
let mut cursor = QueryCursor::new();
for m in cursor.matches(&query, root, source_bytes) {
if let Some(type_cap) = m
.captures
.iter()
.find(|c| query.capture_names()[c.index as usize] == "type")
{
let type_name = node_text(type_cap.node, source_bytes);
edges.push(EdgeDef {
src: fp.clone(),
dst: format!("cls:{}:{}", file.relative_path, type_name),
kind: EdgeKind::Exports,
..Default::default()
});
}
}
}
if let Ok(query) = Query::new(
&self.language,
"(use_declaration argument: (scoped_identifier path: (_) @path name: (_)?))",
) {
let mut cursor = QueryCursor::new();
for m in cursor.matches(&query, root, source_bytes) {
if let Some(path_cap) = m
.captures
.iter()
.find(|c| query.capture_names()[c.index as usize] == "path")
{
let full_path = node_text(path_cap.node, source_bytes);
let import_path = if full_path.starts_with("crate::") {
format!(
"src/{}.rs",
full_path.trim_start_matches("crate::").replace("::", "/")
)
} else {
continue;
};
edges.push(EdgeDef {
src: fp.clone(),
dst: format!("file:{}", import_path),
kind: EdgeKind::Imports,
..Default::default()
});
}
}
}
if let Ok(query) = Query::new(
&self.language,
"(use_declaration argument: (identifier) @name)",
) {
let mut cursor = QueryCursor::new();
for m in cursor.matches(&query, root, source_bytes) {
if let Some(name_cap) = m
.captures
.iter()
.find(|c| query.capture_names()[c.index as usize] == "name")
{
let mod_name = node_text(name_cap.node, source_bytes);
let import_path = mod_name;
edges.push(EdgeDef {
src: fp.clone(),
dst: format!("file:{}.rs", import_path),
kind: EdgeKind::Imports,
..Default::default()
});
}
}
}
mark_pub_exported(&mut nodes, root, source_bytes);
Ok(ParseResult {
nodes,
edges,
..Default::default()
})
}
}
fn is_pub_item(node: tree_sitter::Node, source_bytes: &[u8]) -> bool {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "visibility_modifier" {
let text = node_text(child, source_bytes);
if text == "pub" || text.starts_with("pub(") {
return true;
}
}
}
}
false
}
fn mark_pub_exported(
nodes: &mut Vec<crate::parser::NodeDef>,
root: tree_sitter::Node,
source_bytes: &[u8],
) {
walk_pub(nodes, root, source_bytes);
}
fn walk_pub(nodes: &mut Vec<crate::parser::NodeDef>, node: tree_sitter::Node, source_bytes: &[u8]) {
let kind = node.kind();
if matches!(
kind,
"function_item" | "struct_item" | "enum_item" | "trait_item" | "type_item"
) && is_pub_item(node, source_bytes)
{
if let Some(name_node) = node.child_by_field_name("name") {
let item_name = node_text(name_node, source_bytes);
for n in nodes.iter_mut() {
if n.name == item_name {
n.metadata = serde_json::json!({"exported": true});
}
}
}
}
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
walk_pub(nodes, cursor.node(), source_bytes);
if !cursor.goto_next_sibling() {
break;
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn extract_type_nodes(
nodes: &mut Vec<NodeDef>,
edges: &mut Vec<EdgeDef>,
file_id: &str,
file: &SourceFile,
query: &Query,
root: tree_sitter::Node,
source_bytes: &[u8],
kind: NodeKind,
prefix: &str,
) {
let mut cursor = QueryCursor::new();
for m in cursor.matches(query, root, source_bytes) {
let Some(name_capture) = m
.captures
.iter()
.find(|c| query.capture_names()[c.index as usize] == "name")
else {
continue;
};
let name = node_text(name_capture.node, source_bytes);
let start = name_capture.node.start_position();
let body_end = m
.captures
.iter()
.find(|c| query.capture_names()[c.index as usize] != "name")
.map(|c| c.node.end_position())
.unwrap_or_else(|| name_capture.node.end_position());
let id = format!("{}:{}:{}", prefix, file.relative_path, name);
nodes.push(NodeDef {
id: id.clone(),
kind: kind.clone(),
name,
path: file.relative_path.clone(),
line_start: start.row as u32 + 1,
line_end: body_end.row as u32 + 1,
..Default::default()
});
edges.push(EdgeDef {
src: file_id.to_string(),
dst: id,
kind: EdgeKind::Exports,
..Default::default()
});
}
}
fn node_text(node: tree_sitter::Node, source: &[u8]) -> String {
node.utf8_text(source).unwrap_or("").to_string()
}