use tree_sitter::Node;
use crate::model::{Edge, EdgeType, Language, Node as ModelNode, NodeLabel};
use crate::resolve::FqnGenerator;
use super::dedupe_qn;
use super::error::{ParseError, Result};
use super::extractor::{ExtractResult, Extractor, ImportInfo};
use super::parser_factory::ParserFactory;
pub struct HaskellExtractor {
_priv: (),
}
impl HaskellExtractor {
#[must_use]
pub const fn new() -> Self {
Self { _priv: () }
}
}
impl Default for HaskellExtractor {
fn default() -> Self {
Self::new()
}
}
impl Extractor for HaskellExtractor {
fn language(&self) -> Language {
Language::Haskell
}
fn extract(&self, source: &str, file_path: &str, project: &str) -> Result<ExtractResult> {
let mut result = ExtractResult::new(file_path, Language::Haskell);
let mut parser = ParserFactory::create_parser(Language::Haskell)?;
let tree = parser
.parse(source, None)
.ok_or_else(|| ParseError::ParseFailed {
file_path: file_path.to_string(),
})?;
let root = tree.root_node();
let ctx = VisitContext {
file_path,
project,
current_parent: None,
};
for i in 0..root.named_child_count() as u32 {
if let Some(child) = root.named_child(i) {
visit_node(child, source, &ctx, &mut result);
}
}
Ok(result)
}
}
struct VisitContext<'a> {
file_path: &'a str,
project: &'a str,
current_parent: Option<&'a str>,
}
fn visit_node(node: Node, source: &str, ctx: &VisitContext<'_>, result: &mut ExtractResult) {
match node.kind() {
"function" => {
extract_function(node, source, ctx, result);
let child_ctx = VisitContext {
file_path: ctx.file_path,
project: ctx.project,
current_parent: ctx.current_parent,
};
visit_children(node, source, &child_ctx, result);
}
"signature" => {
extract_type_signature(node, source, ctx, result);
}
"data_type" => {
extract_data_type(node, source, ctx, result);
}
"newtype" => {
extract_new_type(node, source, ctx, result);
}
"type_synomym" => {
extract_type_alias(node, source, ctx, result);
}
"module" => {
extract_module(node, source, ctx, result);
visit_children(node, source, ctx, result);
}
"import" => {
extract_import(node, source, result);
}
_ => {
visit_children(node, source, ctx, result);
}
}
}
fn visit_children(node: Node, source: &str, ctx: &VisitContext<'_>, result: &mut ExtractResult) {
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
visit_node(child, source, ctx, result);
}
}
}
fn extract_function(node: Node, source: &str, ctx: &VisitContext<'_>, result: &mut ExtractResult) {
let Some(name) = function_name(node, source) else {
return;
};
let qn = dedupe_qn(
make_qn(ctx.file_path, &name, ctx.project, None),
node.start_position().row as u32 + 1,
result,
);
let signature = node_text(node, source)
.map(signature_first_line)
.map(String::from);
let mut builder = ModelNode::builder(NodeLabel::Function, name, qn)
.file_path(ctx.file_path)
.start_line(node.start_position().row as u32 + 1)
.end_line(node.end_position().row as u32 + 1)
.language(Language::Haskell)
.project(ctx.project)
.is_global(true)
.is_exported(true);
if let Some(sig) = signature {
builder = builder.signature(sig);
}
let model_node = builder.build();
add_definition_edges(ctx.file_path, ctx.project, &model_node, result);
result.push_node(model_node);
}
fn extract_type_signature(
node: Node,
source: &str,
ctx: &VisitContext<'_>,
result: &mut ExtractResult,
) {
let Some(name) = function_name(node, source) else {
return;
};
let qn = dedupe_qn(
make_qn(ctx.file_path, &name, ctx.project, None),
node.start_position().row as u32 + 1,
result,
);
let signature = node_text(node, source)
.map(signature_first_line)
.map(String::from);
let mut builder = ModelNode::builder(NodeLabel::TypeAlias, name, qn)
.file_path(ctx.file_path)
.start_line(node.start_position().row as u32 + 1)
.end_line(node.end_position().row as u32 + 1)
.language(Language::Haskell)
.project(ctx.project)
.is_global(true)
.is_exported(true);
if let Some(sig) = signature {
builder = builder.signature(sig);
}
let model_node = builder.build();
add_definition_edges(ctx.file_path, ctx.project, &model_node, result);
result.push_node(model_node);
}
fn extract_data_type(node: Node, source: &str, ctx: &VisitContext<'_>, result: &mut ExtractResult) {
let Some(name) = haskell_type_name(node, source) else {
return;
};
let qn = dedupe_qn(
make_qn(ctx.file_path, &name, ctx.project, None),
node.start_position().row as u32 + 1,
result,
);
let signature = node_text(node, source)
.map(signature_first_line)
.map(String::from);
let mut builder = ModelNode::builder(NodeLabel::Struct, name, qn)
.file_path(ctx.file_path)
.start_line(node.start_position().row as u32 + 1)
.end_line(node.end_position().row as u32 + 1)
.language(Language::Haskell)
.project(ctx.project)
.is_global(true)
.is_exported(true);
if let Some(sig) = signature {
builder = builder.signature(sig);
}
let model_node = builder.build();
add_definition_edges(ctx.file_path, ctx.project, &model_node, result);
result.push_node(model_node);
}
fn extract_new_type(node: Node, source: &str, ctx: &VisitContext<'_>, result: &mut ExtractResult) {
let Some(name) = haskell_type_name(node, source) else {
return;
};
let qn = dedupe_qn(
make_qn(ctx.file_path, &name, ctx.project, None),
node.start_position().row as u32 + 1,
result,
);
let model_node = ModelNode::builder(NodeLabel::TypeAlias, name, qn)
.file_path(ctx.file_path)
.start_line(node.start_position().row as u32 + 1)
.end_line(node.end_position().row as u32 + 1)
.language(Language::Haskell)
.project(ctx.project)
.is_global(true)
.is_exported(true)
.build();
add_definition_edges(ctx.file_path, ctx.project, &model_node, result);
result.push_node(model_node);
}
fn extract_type_alias(
node: Node,
source: &str,
ctx: &VisitContext<'_>,
result: &mut ExtractResult,
) {
let Some(name) = haskell_type_name(node, source) else {
return;
};
let qn = dedupe_qn(
make_qn(ctx.file_path, &name, ctx.project, None),
node.start_position().row as u32 + 1,
result,
);
let model_node = ModelNode::builder(NodeLabel::TypeAlias, name, qn)
.file_path(ctx.file_path)
.start_line(node.start_position().row as u32 + 1)
.end_line(node.end_position().row as u32 + 1)
.language(Language::Haskell)
.project(ctx.project)
.is_global(true)
.is_exported(true)
.build();
add_definition_edges(ctx.file_path, ctx.project, &model_node, result);
result.push_node(model_node);
}
fn extract_module(node: Node, source: &str, ctx: &VisitContext<'_>, result: &mut ExtractResult) {
let Some(name) = module_name(node, source) else {
return;
};
let qn = dedupe_qn(
make_qn(ctx.file_path, &name, ctx.project, None),
node.start_position().row as u32 + 1,
result,
);
let model_node = ModelNode::builder(NodeLabel::Module, name, qn)
.file_path(ctx.file_path)
.start_line(node.start_position().row as u32 + 1)
.end_line(node.end_position().row as u32 + 1)
.language(Language::Haskell)
.project(ctx.project)
.is_global(true)
.is_exported(true)
.build();
add_definition_edges(ctx.file_path, ctx.project, &model_node, result);
result.push_node(model_node);
}
fn extract_import(node: Node, source: &str, result: &mut ExtractResult) {
let line = node.start_position().row as u32 + 1;
if let Some(module) = import_module_name(node, source) {
result.imports.push(ImportInfo {
source_file: module,
imported_names: Vec::new(),
line,
});
}
}
fn function_name(node: Node, source: &str) -> Option<String> {
if let Some(name_node) = node.child_by_field_name("name") {
return node_text(name_node, source).map(String::from);
}
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
if child.kind() == "variable" {
return node_text(child, source).map(String::from);
}
for j in 0..child.named_child_count() as u32 {
if let Some(grandchild) = child.named_child(j) {
if grandchild.kind() == "variable" {
return node_text(grandchild, source).map(String::from);
}
}
}
}
}
None
}
fn haskell_type_name(node: Node, source: &str) -> Option<String> {
if let Some(name_node) = node.child_by_field_name("name") {
return node_text(name_node, source).map(String::from);
}
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
if let Some(name) = find_name_in_child(child, source) {
return Some(name);
}
}
}
None
}
fn find_name_in_child(node: Node, source: &str) -> Option<String> {
let kind = node.kind();
if kind == "type_constructor" || kind == "constructor" || kind == "name" {
return node_text(node, source).map(String::from);
}
if let Some(name_node) = node.child_by_field_name("name") {
return node_text(name_node, source).map(String::from);
}
for i in 0..node.named_child_count() as u32 {
if let Some(grandchild) = node.named_child(i) {
let gk = grandchild.kind();
if gk == "type_constructor" || gk == "constructor" || gk == "name" {
return node_text(grandchild, source).map(String::from);
}
}
}
None
}
fn module_name(node: Node, source: &str) -> Option<String> {
if let Some(name_node) = node.child_by_field_name("name") {
return node_text(name_node, source).map(String::from);
}
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
let kind = child.kind();
if kind == "module_id" || kind == "module_name" || kind == "identifier" {
return node_text(child, source).map(String::from);
}
}
}
None
}
fn import_module_name(node: Node, source: &str) -> Option<String> {
if let Some(module_node) = node.child_by_field_name("module") {
return node_text(module_node, source).map(String::from);
}
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
let kind = child.kind();
if kind == "module_name" || kind == "module_id" || kind == "identifier" {
return node_text(child, source).map(String::from);
}
}
}
None
}
fn node_text<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
node.utf8_text(source.as_bytes()).ok()
}
fn signature_first_line(text: &str) -> &str {
text.lines().next().unwrap_or(text)
}
fn make_qn(file_path: &str, name: &str, project: &str, parent: Option<&str>) -> String {
FqnGenerator::generate(project, file_path, name, Language::Haskell, parent)
}
fn add_definition_edges(
file_path: &str,
project: &str,
node: &ModelNode,
result: &mut ExtractResult,
) {
result.edges.push(Edge::new(
file_path.to_string(),
node.id.clone(),
EdgeType::Defines,
project,
));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::NodeLabel;
fn extract(source: &str) -> ExtractResult {
let ext = HaskellExtractor::new();
ext.extract(source, "test.hs", "proj")
.expect("extraction should succeed")
}
#[test]
fn language_returns_haskell() {
assert_eq!(HaskellExtractor::new().language(), Language::Haskell);
}
#[test]
fn default_creates_extractor() {
let ext = HaskellExtractor::default();
assert_eq!(ext.language(), Language::Haskell);
}
#[test]
fn extracts_function() {
let result = extract("foo x = x + 1\n");
let funcs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Function)
.collect();
assert!(
!funcs.is_empty(),
"should extract at least 1 function: {:?}",
result.nodes
);
assert_eq!(funcs[0].name, "foo");
assert_eq!(funcs[0].language, Some(Language::Haskell));
assert_eq!(funcs[0].project, "proj");
assert_eq!(funcs[0].file_path.as_deref(), Some("test.hs"));
assert!(funcs[0].is_global);
}
#[test]
fn extracts_type_signature() {
let result = extract("foo :: Int -> Int\n");
let sigs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::TypeAlias)
.collect();
assert!(
!sigs.is_empty(),
"should extract type signature: {:?}",
result.nodes
);
assert_eq!(sigs[0].name, "foo");
}
#[test]
fn extracts_data_type() {
let result = extract("data Foo = FooCon\n");
let structs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Struct)
.collect();
assert!(
!structs.is_empty(),
"should extract data type: {:?}",
result.nodes
);
assert_eq!(structs[0].name, "Foo");
}
#[test]
fn extracts_new_type() {
let result = extract("newtype Foo = Foo Int\n");
let aliases: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::TypeAlias && n.name == "Foo")
.collect();
assert!(
!aliases.is_empty(),
"should extract newtype: {:?}",
result.nodes
);
}
#[test]
fn extracts_type_alias() {
let result = extract("type Foo = Int\n");
let aliases: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::TypeAlias && n.name == "Foo")
.collect();
assert!(
!aliases.is_empty(),
"should extract type alias: {:?}",
result.nodes
);
}
#[test]
fn extracts_module() {
let result = extract("module Foo where\n");
let modules: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Module)
.collect();
assert!(
!modules.is_empty(),
"should extract module: {:?}",
result.nodes
);
assert_eq!(modules[0].name, "Foo");
}
#[test]
fn extracts_import() {
let result = extract("import Data.List\n");
assert!(
!result.imports.is_empty(),
"should extract import: {:?}",
result.imports
);
assert_eq!(result.imports[0].source_file, "Data.List");
}
#[test]
fn empty_source_returns_empty_result() {
let result = extract("");
assert!(result.is_empty());
}
#[test]
fn result_language_is_haskell() {
let result = extract("foo x = x\n");
assert_eq!(result.language, Language::Haskell);
assert_eq!(result.file_path, "test.hs");
}
#[test]
fn creates_defines_edges() {
let result = extract("foo x = x\n");
let defines_count = result
.edges
.iter()
.filter(|e| e.edge_type == EdgeType::Defines)
.count();
let node_count = result.nodes.len();
assert!(
defines_count >= node_count,
"should create DEFINES edges for nodes: {defines_count} vs {node_count}"
);
}
#[test]
fn qualified_name_uses_file_path_and_name() {
let result = extract("foo x = x\n");
let foo = result.nodes.iter().find(|n| n.name == "foo").unwrap();
assert_eq!(foo.qualified_name, "proj.test.hs.foo");
}
#[test]
fn function_has_signature() {
let result = extract("foo x = x + 1\n");
let foo = result.nodes.iter().find(|n| n.name == "foo").unwrap();
assert!(foo.signature.is_some(), "function should have a signature");
assert!(foo.signature.as_deref().unwrap().contains("foo"));
}
#[test]
fn comment_only_source_returns_empty_result() {
let result = extract("-- just a comment\n");
assert!(
result.is_empty(),
"comment-only file should produce no nodes"
);
}
#[test]
fn module_with_body_extracts_inner_definitions() {
let src = "module Foo where\nfoo x = x\nbar y = y\n";
let result = extract(src);
let funcs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Function)
.collect();
assert!(
funcs.iter().any(|f| f.name == "foo"),
"should extract foo inside module: {:?}",
funcs.iter().map(|f| &f.name).collect::<Vec<_>>()
);
assert!(
funcs.iter().any(|f| f.name == "bar"),
"should extract bar inside module: {:?}",
funcs.iter().map(|f| &f.name).collect::<Vec<_>>()
);
let modules: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Module)
.collect();
assert_eq!(modules.len(), 1, "should still extract the Module node");
}
#[test]
fn multiple_imports_extracted() {
let src = "import Data.List\nimport Data.Maybe\nimport Control.Monad\n";
let result = extract(src);
assert_eq!(result.imports.len(), 3, "should extract 3 imports");
let sources: Vec<_> = result
.imports
.iter()
.map(|i| i.source_file.as_str())
.collect();
assert!(sources.contains(&"Data.List"));
assert!(sources.contains(&"Data.Maybe"));
assert!(sources.contains(&"Control.Monad"));
}
#[test]
fn signature_and_function_with_same_name_create_two_nodes() {
let src = "foo :: Int -> Int\nfoo x = x + 1\n";
let result = extract(src);
let foo_nodes: Vec<_> = result.nodes.iter().filter(|n| n.name == "foo").collect();
assert_eq!(
foo_nodes.len(),
2,
"signature and function with same name should produce 2 nodes: {:?}",
foo_nodes.iter().map(|n| n.label).collect::<Vec<_>>()
);
assert!(
foo_nodes.iter().any(|n| n.label == NodeLabel::TypeAlias),
"should have a TypeAlias node for the signature"
);
assert!(
foo_nodes.iter().any(|n| n.label == NodeLabel::Function),
"should have a Function node for the definition"
);
}
#[test]
fn data_type_with_multiple_constructors() {
let src = "data Color = Red | Green | Blue\n";
let result = extract(src);
let structs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Struct)
.collect();
assert_eq!(structs.len(), 1);
assert_eq!(structs[0].name, "Color");
assert!(
structs[0].signature.is_some(),
"data type should have signature"
);
}
#[test]
fn import_with_qualified_module_name() {
let src = "import qualified Data.Map as M\n";
let result = extract(src);
assert!(
result
.imports
.iter()
.any(|i| i.source_file.contains("Data.Map")),
"should extract qualified import: {:?}",
result.imports
);
}
#[test]
fn combined_definitions_in_module() {
let src = "module Stack where\n\nimport Data.List (intercalate)\n\ndata Stack a = Stack [a]\n\npush :: a -> Stack a -> Stack a\npush x (Stack xs) = Stack (x : xs)\n\ntype Item = Int\n";
let result = extract(src);
assert!(
result
.nodes
.iter()
.any(|n| n.label == NodeLabel::Module && n.name == "Stack"),
"should extract Module node"
);
assert!(
result
.nodes
.iter()
.any(|n| n.label == NodeLabel::Struct && n.name == "Stack"),
"should extract Struct node for data Stack"
);
assert!(
result
.nodes
.iter()
.any(|n| n.label == NodeLabel::Function && n.name == "push"),
"should extract Function node for push"
);
assert!(
result
.nodes
.iter()
.any(|n| n.label == NodeLabel::TypeAlias && n.name == "push"),
"should extract TypeAlias node for push signature"
);
assert!(
result
.nodes
.iter()
.any(|n| n.label == NodeLabel::TypeAlias && n.name == "Item"),
"should extract TypeAlias node for type Item"
);
assert!(!result.imports.is_empty(), "should extract import");
}
#[test]
fn newtype_without_extractable_name_does_not_panic() {
let result = extract("newtype\n");
let _ = result;
}
#[test]
fn type_alias_with_complex_rhs() {
let src = "type Pair a = (a, a)\n";
let result = extract(src);
let aliases: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::TypeAlias && n.name == "Pair")
.collect();
assert_eq!(aliases.len(), 1);
}
#[test]
fn module_node_has_correct_line_numbers() {
let src = "module Foo where\n";
let result = extract(src);
let module = result
.nodes
.iter()
.find(|n| n.label == NodeLabel::Module)
.expect("module should exist");
assert_eq!(module.start_line, Some(1));
assert!(module.end_line.is_some());
assert!(module.end_line.unwrap() >= module.start_line.unwrap());
}
fn parse_source(source: &str) -> tree_sitter::Tree {
let mut parser =
crate::parse::parser_factory::ParserFactory::create_parser(Language::Haskell)
.expect("parser");
parser.parse(source, None).expect("parse")
}
fn find_first_by_kind<'a>(
node: tree_sitter::Node<'a>,
kind: &str,
) -> Option<tree_sitter::Node<'a>> {
if node.kind() == kind {
return Some(node);
}
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
if let Some(found) = find_first_by_kind(child, kind) {
return Some(found);
}
}
}
None
}
#[test]
fn module_name_strategy1_on_node_with_name_field() {
let src = "foo x = x + 1\n";
let tree = parse_source(src);
let root = tree.root_node();
if let Some(func) = find_first_by_kind(root, "function") {
let name = module_name(func, src);
if let Some(n) = name {
assert!(
n.contains("foo"),
"module_name on function via name field should return foo: {n}"
);
}
}
}
#[test]
fn module_name_returns_none_for_node_without_name_like_children() {
let src = "module Foo where\n";
let tree = parse_source(src);
let root = tree.root_node();
let name = module_name(root, src);
let _ = name;
}
#[test]
fn import_module_name_strategy2_on_module_node() {
let src = "module Foo where\n";
let tree = parse_source(src);
let root = tree.root_node();
if let Some(module) = find_first_by_kind(root, "module") {
let name = import_module_name(module, src);
if let Some(n) = name {
assert!(
n.contains("Foo"),
"import_module_name on module via Strategy 2 should return Foo: {n}"
);
}
}
}
#[test]
fn import_module_name_returns_none_for_node_without_module_children() {
let src = "foo x = x\n";
let tree = parse_source(src);
let root = tree.root_node();
let name = import_module_name(root, src);
let _ = name;
}
#[test]
fn function_name_strategy2_on_node_with_variable_child() {
let src = "foo x = x + 1\n";
let tree = parse_source(src);
let root = tree.root_node();
fn walk_and_call(node: tree_sitter::Node, src: &str, results: &mut Vec<Option<String>>) {
let has_name = node.child_by_field_name("name").is_some();
let has_variable_child = (0..node.named_child_count() as u32).any(|i| {
node.named_child(i)
.map(|c| c.kind() == "variable")
.unwrap_or(false)
});
if !has_name && has_variable_child {
results.push(function_name(node, src));
}
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
walk_and_call(child, src, results);
}
}
}
let mut results = Vec::new();
walk_and_call(root, src, &mut results);
for ref name in results.iter().flatten() {
assert!(
!name.is_empty(),
"function_name Strategy 2 should return non-empty name: {name}"
);
}
}
#[test]
fn function_name_returns_none_for_root_node() {
let src = "foo x = x\n";
let tree = parse_source(src);
let root = tree.root_node();
let name = function_name(root, src);
let _ = name;
}
#[test]
fn haskell_type_name_strategy2_on_data_type_without_name_field() {
let src = "data Foo = FooCon\n";
let tree = parse_source(src);
let root = tree.root_node();
if let Some(dt) = find_first_by_kind(root, "data_type") {
let name = haskell_type_name(dt, src);
if let Some(n) = name {
assert!(
n.contains("Foo"),
"haskell_type_name on data_type should return Foo: {n}"
);
}
}
}
#[test]
fn haskell_type_name_returns_none_for_root_node() {
let src = "data Foo = FooCon\n";
let tree = parse_source(src);
let root = tree.root_node();
let name = haskell_type_name(root, src);
let _ = name;
}
#[test]
fn find_name_in_child_finds_type_constructor() {
let src = "data Foo = FooCon\n";
let tree = parse_source(src);
let root = tree.root_node();
fn walk_and_call(node: tree_sitter::Node, src: &str, results: &mut Vec<Option<String>>) {
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
let kind = child.kind();
if kind == "type_constructor" || kind == "constructor" || kind == "name" {
results.push(find_name_in_child(child, src));
}
walk_and_call(child, src, results);
}
}
}
let mut results = Vec::new();
walk_and_call(root, src, &mut results);
assert!(
results.iter().any(|r| r.is_some()),
"find_name_in_child should find at least one name: {results:?}"
);
}
#[test]
fn find_name_in_child_uses_name_field_if_present() {
let src = "foo x = x + 1\n";
let tree = parse_source(src);
let root = tree.root_node();
if let Some(func) = find_first_by_kind(root, "function") {
let name = find_name_in_child(func, src);
if let Some(n) = name {
assert!(
n.contains("foo"),
"find_name_in_child via name field should return foo: {n}"
);
}
}
}
#[test]
fn find_name_in_child_returns_none_for_node_without_name_like_children() {
let src = "module Foo where\n";
let tree = parse_source(src);
let root = tree.root_node();
let name = find_name_in_child(root, src);
let _ = name;
}
}