use crate::languages::LanguageParser;
use crate::node::{CodeNode, NodeKind, Visibility};
use tree_sitter::{Language, Node, Tree};
pub struct DartParser;
impl LanguageParser for DartParser {
fn language(&self) -> Language {
tree_sitter_dart::language()
}
fn extensions(&self) -> &[&str] {
&["dart"]
}
fn extract_nodes(&self, tree: &Tree, source: &str, file_path: &str) -> Vec<CodeNode> {
let mut nodes = Vec::new();
let root = tree.root_node();
extract_from_node(&root, source, file_path, &mut nodes, None);
nodes
}
}
fn extract_from_node(
node: &Node,
source: &str,
file_path: &str,
nodes: &mut Vec<CodeNode>,
context: Option<&str>,
) {
let kind = node.kind();
match kind {
"class_definition" => {
if let Some(code_node) = extract_class(node, source, file_path) {
let class_name = code_node.name.clone();
nodes.push(code_node);
if let Some(body) = node.child_by_field_name("body") {
for i in 0..body.child_count() {
if let Some(child) = body.child(i) {
extract_from_node(&child, source, file_path, nodes, Some(&class_name));
}
}
}
return;
}
}
"mixin_declaration" => {
if let Some(code_node) = extract_mixin(node, source, file_path) {
let mixin_name = code_node.name.clone();
nodes.push(code_node);
if let Some(body) = node.child_by_field_name("body") {
for i in 0..body.child_count() {
if let Some(child) = body.child(i) {
extract_from_node(&child, source, file_path, nodes, Some(&mixin_name));
}
}
}
return;
}
}
"extension_declaration" => {
if let Some(code_node) = extract_extension(node, source, file_path) {
let ext_name = code_node.name.clone();
nodes.push(code_node);
if let Some(body) = node.child_by_field_name("body") {
for i in 0..body.child_count() {
if let Some(child) = body.child(i) {
extract_from_node(&child, source, file_path, nodes, Some(&ext_name));
}
}
}
return;
}
}
"enum_declaration" => {
if let Some(code_node) = extract_enum(node, source, file_path) {
nodes.push(code_node);
}
}
"function_signature" | "function_definition" => {
if let Some(code_node) = extract_function(node, source, file_path, context) {
nodes.push(code_node);
}
}
"method_signature" | "method_definition" => {
if let Some(code_node) = extract_method(node, source, file_path, context) {
nodes.push(code_node);
}
}
"constructor_signature" => {
if let Some(code_node) = extract_constructor(node, source, file_path, context) {
nodes.push(code_node);
}
}
"getter_signature" | "setter_signature" => {
if let Some(code_node) = extract_accessor(node, source, file_path, context) {
nodes.push(code_node);
}
}
"import_or_export" | "import_specification" => {
if let Some(code_node) = extract_import(node, source, file_path) {
nodes.push(code_node);
}
}
"library_name" => {
if let Some(code_node) = extract_library(node, source, file_path) {
nodes.push(code_node);
}
}
"top_level_definition" => {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "initialized_variable_definition"
|| child.kind() == "static_final_declaration"
{
if let Some(code_node) = extract_variable(&child, source, file_path) {
nodes.push(code_node);
}
}
}
}
}
_ => {}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
extract_from_node(&child, source, file_path, nodes, context);
}
}
}
fn extract_class(node: &Node, source: &str, file_path: &str) -> Option<CodeNode> {
let name_node = node.child_by_field_name("name")?;
let name = get_text(&name_node, source);
let visibility = detect_visibility(&name);
Some(
CodeNode::new(&name, &name, NodeKind::Class, file_path)
.with_lines(
node.start_position().row as u32 + 1,
node.end_position().row as u32 + 1,
)
.with_bytes(node.start_byte() as u32, node.end_byte() as u32)
.with_column(name_node.start_position().column as u32)
.with_visibility(visibility),
)
}
fn extract_mixin(node: &Node, source: &str, file_path: &str) -> Option<CodeNode> {
let name_node = node.child_by_field_name("name")?;
let name = get_text(&name_node, source);
let visibility = detect_visibility(&name);
Some(
CodeNode::new(&name, &name, NodeKind::Interface, file_path)
.with_lines(
node.start_position().row as u32 + 1,
node.end_position().row as u32 + 1,
)
.with_bytes(node.start_byte() as u32, node.end_byte() as u32)
.with_column(name_node.start_position().column as u32)
.with_visibility(visibility),
)
}
fn extract_extension(node: &Node, source: &str, file_path: &str) -> Option<CodeNode> {
let name = node
.child_by_field_name("name")
.map(|n| get_text(&n, source))
.unwrap_or_else(|| "<anonymous>".to_string());
Some(
CodeNode::new(&name, &name, NodeKind::TypeAlias, file_path)
.with_lines(
node.start_position().row as u32 + 1,
node.end_position().row as u32 + 1,
)
.with_bytes(node.start_byte() as u32, node.end_byte() as u32)
.with_visibility(Visibility::Public),
)
}
fn extract_enum(node: &Node, source: &str, file_path: &str) -> Option<CodeNode> {
let name_node = node.child_by_field_name("name")?;
let name = get_text(&name_node, source);
let visibility = detect_visibility(&name);
Some(
CodeNode::new(&name, &name, NodeKind::Enum, file_path)
.with_lines(
node.start_position().row as u32 + 1,
node.end_position().row as u32 + 1,
)
.with_bytes(node.start_byte() as u32, node.end_byte() as u32)
.with_column(name_node.start_position().column as u32)
.with_visibility(visibility),
)
}
fn extract_function(
node: &Node,
source: &str,
file_path: &str,
context: Option<&str>,
) -> Option<CodeNode> {
let name_node = node.child_by_field_name("name")?;
let name = get_text(&name_node, source);
if context.is_some() {
return None;
}
let visibility = detect_visibility(&name);
let signature = build_function_signature(node, source, &name);
let references = extract_call_references(node, source);
Some(
CodeNode::new(&name, &name, NodeKind::Function, file_path)
.with_lines(
node.start_position().row as u32 + 1,
node.end_position().row as u32 + 1,
)
.with_bytes(node.start_byte() as u32, node.end_byte() as u32)
.with_column(name_node.start_position().column as u32)
.with_signature(signature)
.with_visibility(visibility)
.with_references(references),
)
}
fn extract_method(
node: &Node,
source: &str,
file_path: &str,
context: Option<&str>,
) -> Option<CodeNode> {
let name_node = node.child_by_field_name("name")?;
let name = get_text(&name_node, source);
let qualified_name = match context {
Some(ctx) => format!("{}.{}", ctx, name),
None => name.clone(),
};
let visibility = detect_visibility(&name);
let signature = build_function_signature(node, source, &name);
let references = extract_call_references(node, source);
Some(
CodeNode::new(&name, &qualified_name, NodeKind::Method, file_path)
.with_lines(
node.start_position().row as u32 + 1,
node.end_position().row as u32 + 1,
)
.with_bytes(node.start_byte() as u32, node.end_byte() as u32)
.with_column(name_node.start_position().column as u32)
.with_signature(signature)
.with_visibility(visibility)
.with_references(references),
)
}
fn extract_constructor(
node: &Node,
source: &str,
file_path: &str,
context: Option<&str>,
) -> Option<CodeNode> {
let name = find_constructor_name(node, source)?;
let qualified_name = match context {
Some(ctx) => format!("{}.{}", ctx, name),
None => name.clone(),
};
let visibility = detect_visibility(&name);
Some(
CodeNode::new(&name, &qualified_name, NodeKind::Constructor, file_path)
.with_lines(
node.start_position().row as u32 + 1,
node.end_position().row as u32 + 1,
)
.with_bytes(node.start_byte() as u32, node.end_byte() as u32)
.with_visibility(visibility),
)
}
fn extract_accessor(
node: &Node,
source: &str,
file_path: &str,
context: Option<&str>,
) -> Option<CodeNode> {
let name_node = node.child_by_field_name("name")?;
let name = get_text(&name_node, source);
let qualified_name = match context {
Some(ctx) => format!("{}.{}", ctx, name),
None => name.clone(),
};
let visibility = detect_visibility(&name);
Some(
CodeNode::new(&name, &qualified_name, NodeKind::Method, file_path)
.with_lines(
node.start_position().row as u32 + 1,
node.end_position().row as u32 + 1,
)
.with_bytes(node.start_byte() as u32, node.end_byte() as u32)
.with_column(name_node.start_position().column as u32)
.with_visibility(visibility),
)
}
fn extract_import(node: &Node, source: &str, file_path: &str) -> Option<CodeNode> {
let text = get_text(node, source);
if let Some(start) = text.find('\'').or_else(|| text.find('"')) {
if let Some(end) = text[start + 1..]
.find('\'')
.or_else(|| text[start + 1..].find('"'))
{
let import_path = &text[start + 1..start + 1 + end];
return Some(
CodeNode::new(import_path, import_path, NodeKind::Import, file_path)
.with_lines(
node.start_position().row as u32 + 1,
node.end_position().row as u32 + 1,
)
.with_bytes(node.start_byte() as u32, node.end_byte() as u32),
);
}
}
None
}
fn extract_library(node: &Node, source: &str, file_path: &str) -> Option<CodeNode> {
let text = get_text(node, source);
let name = text
.trim_start_matches("library ")
.trim_end_matches(';')
.trim();
Some(
CodeNode::new(name, name, NodeKind::Module, file_path)
.with_lines(
node.start_position().row as u32 + 1,
node.end_position().row as u32 + 1,
)
.with_bytes(node.start_byte() as u32, node.end_byte() as u32),
)
}
fn extract_variable(node: &Node, source: &str, file_path: &str) -> Option<CodeNode> {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "identifier" {
let name = get_text(&child, source);
let visibility = detect_visibility(&name);
return Some(
CodeNode::new(&name, &name, NodeKind::Variable, file_path)
.with_lines(
node.start_position().row as u32 + 1,
node.end_position().row as u32 + 1,
)
.with_bytes(node.start_byte() as u32, node.end_byte() as u32)
.with_visibility(visibility),
);
}
}
}
None
}
fn get_text(node: &Node, source: &str) -> String {
source[node.byte_range()].to_string()
}
fn find_constructor_name(node: &Node, source: &str) -> Option<String> {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "identifier" {
return Some(get_text(&child, source));
}
}
}
None
}
fn detect_visibility(name: &str) -> Visibility {
if name.starts_with('_') {
Visibility::Private
} else {
Visibility::Public
}
}
fn build_function_signature(node: &Node, source: &str, name: &str) -> String {
let return_type = node
.child_by_field_name("return_type")
.map(|n| get_text(&n, source))
.unwrap_or_else(|| "void".to_string());
let params = node
.child_by_field_name("parameters")
.map(|n| get_text(&n, source))
.unwrap_or_else(|| "()".to_string());
format!("{} {}{}", return_type, name, params)
}
fn extract_call_references(node: &Node, source: &str) -> Vec<String> {
let mut refs = Vec::new();
collect_calls(node, source, &mut refs);
refs.sort();
refs.dedup();
refs
}
fn collect_calls(node: &Node, source: &str, refs: &mut Vec<String>) {
if node.kind() == "call_expression" || node.kind() == "selector" {
if let Some(func_node) = node.child_by_field_name("function") {
let call_name = get_text(&func_node, source);
refs.push(call_name);
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
collect_calls(&child, source, refs);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_class() {
let source = r#"
class MyClass {
void hello() {
print('Hello');
}
}
"#;
let parser = DartParser;
let mut ts_parser = tree_sitter::Parser::new();
ts_parser.set_language(&parser.language()).unwrap();
let tree = ts_parser.parse(source, None).unwrap();
let nodes = parser.extract_nodes(&tree, source, "my_class.dart");
assert!(nodes
.iter()
.any(|n| n.name == "MyClass" && matches!(n.kind, NodeKind::Class)));
}
#[test]
fn test_visibility_detection() {
let source = r#"
class Example {
void publicMethod() {}
void _privateMethod() {}
}
"#;
let parser = DartParser;
let mut ts_parser = tree_sitter::Parser::new();
ts_parser.set_language(&parser.language()).unwrap();
let tree = ts_parser.parse(source, None).unwrap();
let nodes = parser.extract_nodes(&tree, source, "example.dart");
let example = nodes.iter().find(|n| n.name == "Example");
assert!(example.is_some());
assert!(matches!(example.unwrap().visibility, Visibility::Public));
}
#[test]
fn test_parse_enum() {
let source = r#"
enum Color { red, green, blue }
"#;
let parser = DartParser;
let mut ts_parser = tree_sitter::Parser::new();
ts_parser.set_language(&parser.language()).unwrap();
let tree = ts_parser.parse(source, None).unwrap();
let nodes = parser.extract_nodes(&tree, source, "color.dart");
assert!(nodes
.iter()
.any(|n| n.name == "Color" && matches!(n.kind, NodeKind::Enum)));
}
}