use std::path::{Path, PathBuf};
use streaming_iterator::StreamingIterator;
use tree_sitter::{Node, Parser, Query, QueryCursor};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EntryPointKind {
Main,
LibraryExport,
Test,
Ffi,
ProcMacro,
Init,
BuildScript,
FrameworkDispatched,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntryPoint {
pub name: String,
pub kind: EntryPointKind,
pub file_path: PathBuf,
pub line: u32,
}
pub trait EntryPointDetector {
fn detect(&self, source: &str, file_path: &Path) -> Vec<EntryPoint>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct RustEntryDetector;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RustFileRole {
LibOrMod,
Example,
Bench,
Other,
}
fn rust_file_role(file_path: &Path) -> RustFileRole {
if matches!(
file_path.file_name().and_then(|s| s.to_str()),
Some("lib.rs" | "mod.rs")
) {
return RustFileRole::LibOrMod;
}
for component in file_path.components() {
match component.as_os_str().to_str() {
Some("examples") => return RustFileRole::Example,
Some("benches") => return RustFileRole::Bench,
_ => {}
}
}
RustFileRole::Other
}
impl EntryPointDetector for RustEntryDetector {
fn detect(&self, source: &str, file_path: &Path) -> Vec<EntryPoint> {
let mut entries = Vec::new();
let Some(tree) = parse_with(source, &tree_sitter_rust::LANGUAGE.into()) else {
return entries;
};
let root = tree.root_node();
let bytes = source.as_bytes();
if file_path.file_name().and_then(|s| s.to_str()) == Some("build.rs") {
entries.push(EntryPoint {
name: "build.rs".to_string(),
kind: EntryPointKind::BuildScript,
file_path: file_path.to_path_buf(),
line: 1,
});
}
let role = rust_file_role(file_path);
if role == RustFileRole::LibOrMod {
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
if child.kind() == "use_declaration" && rust_use_is_pub(&child, bytes) {
collect_rust_pub_use_entries(&child, bytes, file_path, &mut entries);
}
}
}
visit_rust_tool_router_impls(&root, bytes, file_path, &mut entries);
visit_rust_node(&root, bytes, file_path, role, &mut entries);
entries
}
}
fn visit_rust_node(
node: &Node<'_>,
bytes: &[u8],
file_path: &Path,
role: RustFileRole,
out: &mut Vec<EntryPoint>,
) {
if node.kind() == "function_item" {
rust_classify_function(node, bytes, file_path, role, out);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
visit_rust_node(&child, bytes, file_path, role, out);
}
}
fn rust_classify_function(
node: &Node<'_>,
bytes: &[u8],
file_path: &Path,
role: RustFileRole,
out: &mut Vec<EntryPoint>,
) {
let name_node = node.child_by_field_name("name");
let Some(name_node) = name_node else { return };
let Ok(name) = std::str::from_utf8(&bytes[name_node.start_byte()..name_node.end_byte()]) else {
return;
};
let line = u32::try_from(node.start_position().row + 1).unwrap_or(u32::MAX);
let attrs = collect_preceding_rust_attrs(node, bytes);
if attrs.iter().any(|a| {
a.starts_with("proc_macro_derive")
|| a.starts_with("proc_macro_attribute")
|| a == "proc_macro"
|| a.starts_with("proc_macro(")
}) {
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::ProcMacro,
file_path: file_path.to_path_buf(),
line,
});
}
if attrs.iter().any(|a| a == "test" || a == "bench") {
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::Test,
file_path: file_path.to_path_buf(),
line,
});
}
let function_text =
std::str::from_utf8(&bytes[node.start_byte()..node.end_byte()]).unwrap_or("");
let has_extern_c =
rust_function_has_extern_c(node, bytes) || function_text.contains("extern \"C\"");
if attrs.iter().any(|a| a == "no_mangle") || has_extern_c {
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::Ffi,
file_path: file_path.to_path_buf(),
line,
});
}
if attrs.iter().any(|a| a == "tool" || a.starts_with("tool(")) {
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::FrameworkDispatched,
file_path: file_path.to_path_buf(),
line,
});
}
if name == "main" {
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::Main,
file_path: file_path.to_path_buf(),
line,
});
}
if role == RustFileRole::LibOrMod && rust_function_is_pub(node, bytes) {
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
}
if role == RustFileRole::Bench
&& rust_function_is_pub(node, bytes)
&& is_top_level_in_source(node)
{
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::Main,
file_path: file_path.to_path_buf(),
line,
});
}
}
fn is_top_level_in_source(node: &Node<'_>) -> bool {
node.parent().is_some_and(|p| p.kind() == "source_file")
}
fn collect_preceding_rust_attrs(node: &Node<'_>, bytes: &[u8]) -> Vec<String> {
let mut attrs = Vec::new();
let mut prev = node.prev_sibling();
while let Some(p) = prev {
if p.kind() == "attribute_item" || p.kind() == "inner_attribute_item" {
let mut cursor = p.walk();
let mut attr_text: Option<String> = None;
for child in p.children(&mut cursor) {
if child.kind() == "attribute"
&& let Ok(text) =
std::str::from_utf8(&bytes[child.start_byte()..child.end_byte()])
{
attr_text = Some(text.to_string());
}
}
if let Some(t) = attr_text {
attrs.push(t);
}
prev = p.prev_sibling();
} else if p.kind().starts_with("line_comment") || p.kind().starts_with("block_comment") {
prev = p.prev_sibling();
} else {
break;
}
}
attrs
}
fn rust_function_is_pub(node: &Node<'_>, bytes: &[u8]) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "visibility_modifier"
&& let Ok(text) = std::str::from_utf8(&bytes[child.start_byte()..child.end_byte()])
{
return text.starts_with("pub");
}
}
false
}
fn rust_use_is_pub(node: &Node<'_>, bytes: &[u8]) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "visibility_modifier"
&& let Ok(text) = std::str::from_utf8(&bytes[child.start_byte()..child.end_byte()])
{
return text.starts_with("pub");
}
}
false
}
fn collect_rust_pub_use_entries(
use_decl: &Node<'_>,
bytes: &[u8],
file_path: &Path,
out: &mut Vec<EntryPoint>,
) {
let line = u32::try_from(use_decl.start_position().row + 1).unwrap_or(u32::MAX);
let argument = use_decl.child_by_field_name("argument").or_else(|| {
let mut cursor = use_decl.walk();
let mut found: Option<Node<'_>> = None;
for child in use_decl.children(&mut cursor) {
match child.kind() {
"scoped_identifier" | "scoped_use_list" | "use_list" | "use_as_clause"
| "use_wildcard" | "identifier" => {
found = Some(child);
break;
}
_ => {}
}
}
found
});
let Some(argument) = argument else { return };
rust_collect_use_tree(&argument, bytes, file_path, line, out);
}
fn rust_collect_use_tree(
node: &Node<'_>,
bytes: &[u8],
file_path: &Path,
line: u32,
out: &mut Vec<EntryPoint>,
) {
match node.kind() {
"use_wildcard" => {
if let Ok(text) = std::str::from_utf8(&bytes[node.start_byte()..node.end_byte()]) {
let trimmed = text.trim();
let normalised = trimmed.replace(char::is_whitespace, "");
out.push(EntryPoint {
name: normalised,
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
}
}
"use_list" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if matches!(child.kind(), "," | "{" | "}") {
continue;
}
rust_collect_use_tree(&child, bytes, file_path, line, out);
}
}
"scoped_use_list" => {
let list = node.child_by_field_name("list").or_else(|| {
let mut cursor = node.walk();
node.children(&mut cursor).find(|c| c.kind() == "use_list")
});
if let Some(list) = list {
rust_collect_use_tree(&list, bytes, file_path, line, out);
}
}
"use_as_clause" => {
let alias = node.child_by_field_name("alias");
if let Some(alias) = alias
&& let Ok(text) = std::str::from_utf8(&bytes[alias.start_byte()..alias.end_byte()])
{
out.push(EntryPoint {
name: text.to_string(),
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
}
}
"scoped_identifier" => {
let name = node.child_by_field_name("name");
if let Some(name) = name
&& let Ok(text) = std::str::from_utf8(&bytes[name.start_byte()..name.end_byte()])
{
out.push(EntryPoint {
name: text.to_string(),
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
}
}
"identifier" => {
if let Ok(text) = std::str::from_utf8(&bytes[node.start_byte()..node.end_byte()]) {
out.push(EntryPoint {
name: text.to_string(),
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
}
}
_ => {}
}
}
fn visit_rust_tool_router_impls(
node: &Node<'_>,
bytes: &[u8],
file_path: &Path,
out: &mut Vec<EntryPoint>,
) {
if node.kind() == "impl_item" {
let attrs = collect_preceding_rust_attrs(node, bytes);
if attrs
.iter()
.any(|a| a == "tool_router" || a.starts_with("tool_router("))
{
if let Some(body) = node.child_by_field_name("body") {
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
if child.kind() != "function_item" {
continue;
}
let Some(name_node) = child.child_by_field_name("name") else {
continue;
};
let Ok(name) =
std::str::from_utf8(&bytes[name_node.start_byte()..name_node.end_byte()])
else {
continue;
};
let line = u32::try_from(child.start_position().row + 1).unwrap_or(u32::MAX);
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::FrameworkDispatched,
file_path: file_path.to_path_buf(),
line,
});
}
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
visit_rust_tool_router_impls(&child, bytes, file_path, out);
}
}
fn rust_function_has_extern_c(node: &Node<'_>, bytes: &[u8]) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() != "function_modifiers" {
continue;
}
let mut inner = child.walk();
for grandchild in child.children(&mut inner) {
if grandchild.kind() == "extern_modifier"
&& let Ok(text) =
std::str::from_utf8(&bytes[grandchild.start_byte()..grandchild.end_byte()])
&& text.contains("\"C\"")
{
return true;
}
}
}
false
}
#[derive(Debug, Default, Clone, Copy)]
pub struct PythonEntryDetector;
impl EntryPointDetector for PythonEntryDetector {
#[expect(
clippy::too_many_lines,
reason = "two-pass detection: first-pass collects top-level fn names and CLI decorators, second-pass emits entries; helper functions keep individual pieces readable"
)]
fn detect(&self, source: &str, file_path: &Path) -> Vec<EntryPoint> {
let mut entries = Vec::new();
let Some(tree) = parse_with(source, &tree_sitter_python::LANGUAGE.into()) else {
return entries;
};
let root = tree.root_node();
let bytes = source.as_bytes();
let is_test_file = python_is_test_file(file_path);
let mut toplevel_fns: Vec<(String, u32)> = Vec::new();
let mut click_decorated: Vec<(String, u32)> = Vec::new();
{
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
match child.kind() {
"function_definition" => {
if let Some(name_node) = child.child_by_field_name("name")
&& let Ok(name) = std::str::from_utf8(
&bytes[name_node.start_byte()..name_node.end_byte()],
)
{
let line =
u32::try_from(child.start_position().row + 1).unwrap_or(u32::MAX);
toplevel_fns.push((name.to_string(), line));
}
}
"decorated_definition" => {
if let Some(fn_node) = child.child_by_field_name("definition")
&& fn_node.kind() == "function_definition"
&& let Some(name_node) = fn_node.child_by_field_name("name")
&& let Ok(name) = std::str::from_utf8(
&bytes[name_node.start_byte()..name_node.end_byte()],
)
{
let line =
u32::try_from(fn_node.start_position().row + 1).unwrap_or(u32::MAX);
toplevel_fns.push((name.to_string(), line));
if python_has_cli_command_decorator(&child, bytes) {
click_decorated.push((name.to_string(), line));
}
}
}
_ => {}
}
}
}
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
match child.kind() {
"if_statement" if python_is_dunder_main_block(&child, bytes) => {
let line = u32::try_from(child.start_position().row + 1).unwrap_or(u32::MAX);
entries.push(EntryPoint {
name: "__main__".to_string(),
kind: EntryPointKind::Main,
file_path: file_path.to_path_buf(),
line,
});
if let Some(body) = child.child_by_field_name("consequence") {
for called in python_direct_calls_in_block(&body, bytes) {
let fn_line = toplevel_fns
.iter()
.find(|(n, _)| n == &called)
.map(|(_, l)| *l)
.unwrap_or(line);
entries.push(EntryPoint {
name: called,
kind: EntryPointKind::Main,
file_path: file_path.to_path_buf(),
line: fn_line,
});
}
}
}
"expression_statement" => {
if let Some(names) = python_extract_dunder_all(&child, bytes) {
let line =
u32::try_from(child.start_position().row + 1).unwrap_or(u32::MAX);
for n in names {
entries.push(EntryPoint {
name: n,
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
}
}
}
"function_definition" | "decorated_definition" => {
let fn_node = if child.kind() == "decorated_definition" {
child.child_by_field_name("definition")
} else {
Some(child)
};
if let Some(fn_node) = fn_node
&& fn_node.kind() == "function_definition"
&& let Some(name_node) = fn_node.child_by_field_name("name")
&& let Ok(name) = std::str::from_utf8(
&bytes[name_node.start_byte()..name_node.end_byte()],
)
&& is_test_file
&& name.starts_with("test_")
{
let line =
u32::try_from(fn_node.start_position().row + 1).unwrap_or(u32::MAX);
entries.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::Test,
file_path: file_path.to_path_buf(),
line,
});
}
}
_ => {}
}
}
for (name, line) in click_decorated {
entries.push(EntryPoint {
name,
kind: EntryPointKind::Main,
file_path: file_path.to_path_buf(),
line,
});
}
entries
}
}
fn python_is_test_file(file_path: &Path) -> bool {
let Some(file_name) = file_path.file_name().and_then(|s| s.to_str()) else {
return false;
};
let is_py = Path::new(file_name)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("py"));
if !is_py {
return false;
}
let stem = Path::new(file_name)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("");
if stem.starts_with("test_") || stem.ends_with("_test") {
return true;
}
file_path
.components()
.any(|c| c.as_os_str() == std::ffi::OsStr::new("tests"))
}
fn python_is_dunder_main_block(node: &Node<'_>, bytes: &[u8]) -> bool {
let cond = node.child_by_field_name("condition");
let Some(cond) = cond else { return false };
let Ok(text) = std::str::from_utf8(&bytes[cond.start_byte()..cond.end_byte()]) else {
return false;
};
let normalized = text.replace(' ', "");
normalized.contains("__name__==\"__main__\"")
|| normalized.contains("__name__=='__main__'")
|| normalized.contains("\"__main__\"==__name__")
|| normalized.contains("'__main__'==__name__")
}
fn python_extract_dunder_all(node: &Node<'_>, bytes: &[u8]) -> Option<Vec<String>> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "assignment" {
let left = child.child_by_field_name("left")?;
let right = child.child_by_field_name("right")?;
let left_text = std::str::from_utf8(&bytes[left.start_byte()..left.end_byte()]).ok()?;
if left_text.trim() != "__all__" {
return None;
}
let mut names = Vec::new();
let mut inner = right.walk();
for grandchild in right.children(&mut inner) {
if grandchild.kind() != "string" {
continue;
}
let mut sc = grandchild.walk();
let mut content_text: Option<String> = None;
for sg in grandchild.children(&mut sc) {
if sg.kind() == "string_content"
&& let Ok(t) = std::str::from_utf8(&bytes[sg.start_byte()..sg.end_byte()])
{
content_text = Some(t.to_string());
}
}
if let Some(t) = content_text {
names.push(t);
} else if let Ok(raw) =
std::str::from_utf8(&bytes[grandchild.start_byte()..grandchild.end_byte()])
{
let trimmed = raw.trim_matches(|c| c == '"' || c == '\'');
names.push(trimmed.to_string());
}
}
return Some(names);
}
}
None
}
fn python_direct_calls_in_block(block: &Node<'_>, bytes: &[u8]) -> Vec<String> {
let mut names = Vec::new();
let mut cursor = block.walk();
for stmt in block.children(&mut cursor) {
if stmt.kind() != "expression_statement" {
continue;
}
let mut sc = stmt.walk();
for expr in stmt.children(&mut sc) {
if expr.kind() != "call" {
continue;
}
if let Some(func_node) = expr.child_by_field_name("function")
&& func_node.kind() == "identifier"
&& let Ok(name) =
std::str::from_utf8(&bytes[func_node.start_byte()..func_node.end_byte()])
&& !name.is_empty()
{
names.push(name.to_string());
}
}
}
names
}
fn python_has_cli_command_decorator(decorated_def: &Node<'_>, bytes: &[u8]) -> bool {
let mut cursor = decorated_def.walk();
for child in decorated_def.children(&mut cursor) {
if child.kind() != "decorator" {
continue;
}
let mut dc = child.walk();
for inner in child.children(&mut dc) {
match inner.kind() {
"call" => {
if let Some(func) = inner.child_by_field_name("function")
&& func.kind() == "attribute"
&& let Some(prop) = func.child_by_field_name("attribute")
&& let Ok(prop_text) =
std::str::from_utf8(&bytes[prop.start_byte()..prop.end_byte()])
&& prop_text == "command"
{
return true;
}
}
"attribute" => {
if let Some(prop) = inner.child_by_field_name("attribute")
&& let Ok(prop_text) =
std::str::from_utf8(&bytes[prop.start_byte()..prop.end_byte()])
&& prop_text == "command"
{
return true;
}
}
_ => {}
}
}
}
false
}
#[derive(Debug, Default, Clone, Copy)]
pub struct GoEntryDetector;
impl EntryPointDetector for GoEntryDetector {
fn detect(&self, source: &str, file_path: &Path) -> Vec<EntryPoint> {
let mut entries = Vec::new();
let Some(tree) = parse_with(source, &tree_sitter_go::LANGUAGE.into()) else {
return entries;
};
let root = tree.root_node();
let bytes = source.as_bytes();
let package_name = go_package_name(&root, bytes).unwrap_or_default();
let is_main_package = package_name == "main";
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
match child.kind() {
"function_declaration" => {
if let Some(name_node) = child.child_by_field_name("name")
&& let Ok(name) = std::str::from_utf8(
&bytes[name_node.start_byte()..name_node.end_byte()],
)
{
let line =
u32::try_from(child.start_position().row + 1).unwrap_or(u32::MAX);
go_classify(name, line, is_main_package, file_path, &mut entries);
}
}
"method_declaration" => {
if let Some(name_node) = child.child_by_field_name("name")
&& let Ok(name) = std::str::from_utf8(
&bytes[name_node.start_byte()..name_node.end_byte()],
)
&& !is_main_package
&& go_is_exported(name)
{
let line =
u32::try_from(child.start_position().row + 1).unwrap_or(u32::MAX);
entries.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
}
}
_ => {}
}
}
entries
}
}
fn go_package_name(root: &Node<'_>, bytes: &[u8]) -> Option<String> {
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
if child.kind() != "package_clause" {
continue;
}
let mut inner = child.walk();
for grandchild in child.children(&mut inner) {
if grandchild.kind() == "package_identifier"
&& let Ok(text) =
std::str::from_utf8(&bytes[grandchild.start_byte()..grandchild.end_byte()])
{
return Some(text.to_string());
}
}
}
None
}
fn go_classify(
name: &str,
line: u32,
is_main_package: bool,
file_path: &Path,
out: &mut Vec<EntryPoint>,
) {
if name == "main" && is_main_package {
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::Main,
file_path: file_path.to_path_buf(),
line,
});
return;
}
if name == "init" {
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::Init,
file_path: file_path.to_path_buf(),
line,
});
return;
}
if name.starts_with("Test")
|| name.starts_with("Benchmark")
|| name.starts_with("Example")
|| name.starts_with("Fuzz")
{
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::Test,
file_path: file_path.to_path_buf(),
line,
});
return;
}
if !is_main_package && go_is_exported(name) {
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
}
}
fn go_is_exported(name: &str) -> bool {
name.chars().next().is_some_and(|c| c.is_ascii_uppercase())
}
#[derive(Debug, Default, Clone, Copy)]
pub struct CEntryDetector;
impl EntryPointDetector for CEntryDetector {
fn detect(&self, source: &str, file_path: &Path) -> Vec<EntryPoint> {
let mut entries = Vec::new();
let Some(tree) = parse_with(source, &tree_sitter_c::LANGUAGE.into()) else {
return entries;
};
let root = tree.root_node();
let bytes = source.as_bytes();
c_collect_cgo_exports(source, file_path, &mut entries);
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
if child.kind() == "function_definition" {
c_classify_function(&child, bytes, file_path, &mut entries);
}
}
entries
}
}
fn c_classify_function(
node: &tree_sitter::Node<'_>,
bytes: &[u8],
file_path: &Path,
out: &mut Vec<EntryPoint>,
) {
let line = u32::try_from(node.start_position().row + 1).unwrap_or(u32::MAX);
let Some(declarator) = node.child_by_field_name("declarator") else {
return;
};
let Some(name) = c_resolve_function_name(&declarator, bytes) else {
return;
};
let has_constructor_attr = c_has_constructor_attribute(node, bytes);
if name == "main" {
out.push(EntryPoint {
name: name.clone(),
kind: EntryPointKind::Main,
file_path: file_path.to_path_buf(),
line,
});
}
if has_constructor_attr {
out.push(EntryPoint {
name,
kind: EntryPointKind::Init,
file_path: file_path.to_path_buf(),
line,
});
}
}
fn c_resolve_function_name(declarator: &tree_sitter::Node<'_>, bytes: &[u8]) -> Option<String> {
match declarator.kind() {
"function_declarator" => {
let inner = declarator.child_by_field_name("declarator")?;
c_resolve_function_name(&inner, bytes)
}
"pointer_declarator" => {
let inner = declarator.child_by_field_name("declarator")?;
c_resolve_function_name(&inner, bytes)
}
"identifier" => {
let text =
std::str::from_utf8(&bytes[declarator.start_byte()..declarator.end_byte()]).ok()?;
Some(text.to_string())
}
_ => None,
}
}
fn c_has_constructor_attribute(node: &tree_sitter::Node<'_>, bytes: &[u8]) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() != "attribute_specifier" {
continue;
}
let mut inner = child.walk();
for grandchild in child.children(&mut inner) {
if grandchild.kind() != "argument_list" {
continue;
}
let mut arg_cur = grandchild.walk();
for arg in grandchild.children(&mut arg_cur) {
if arg.kind() == "identifier"
&& std::str::from_utf8(&bytes[arg.start_byte()..arg.end_byte()])
.is_ok_and(|t| t == "constructor")
{
return true;
}
}
}
}
false
}
fn c_collect_cgo_exports(source: &str, file_path: &Path, out: &mut Vec<EntryPoint>) {
for (i, line) in source.lines().enumerate() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("//export ") {
let name = rest.trim();
if !name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_') {
let line_num = u32::try_from(i + 1).unwrap_or(u32::MAX);
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::Ffi,
file_path: file_path.to_path_buf(),
line: line_num,
});
}
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct JsEntryDetector;
impl EntryPointDetector for JsEntryDetector {
fn detect(&self, source: &str, file_path: &Path) -> Vec<EntryPoint> {
let mut entries = Vec::new();
let Some(tree) = parse_with(source, &tree_sitter_javascript::LANGUAGE.into()) else {
return entries;
};
let root = tree.root_node();
let bytes = source.as_bytes();
let is_test_file = js_is_test_file(file_path);
let module_exports_alias = js_find_module_exports_alias(&root, bytes);
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
match child.kind() {
"export_statement" => {
js_classify_export(&child, bytes, file_path, &mut entries);
}
"expression_statement" => {
js_classify_expression_statement(
&child,
bytes,
file_path,
is_test_file,
module_exports_alias.as_deref(),
&mut entries,
);
}
_ => {}
}
}
entries
}
}
fn js_is_test_file(file_path: &Path) -> bool {
let Some(file_name) = file_path.file_name().and_then(|s| s.to_str()) else {
return false;
};
let stem_lower = file_name.to_ascii_lowercase();
if stem_lower.contains(".test.") || stem_lower.contains(".spec.") {
return true;
}
file_path
.components()
.any(|c| c.as_os_str() == std::ffi::OsStr::new("__tests__"))
}
fn js_find_module_exports_alias(root: &tree_sitter::Node<'_>, bytes: &[u8]) -> Option<String> {
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
if child.kind() != "variable_declaration" {
continue;
}
let mut vc = child.walk();
for decl in child.children(&mut vc) {
if decl.kind() != "variable_declarator" {
continue;
}
let Some(name_node) = decl.child_by_field_name("name") else {
continue;
};
let Ok(alias) =
std::str::from_utf8(&bytes[name_node.start_byte()..name_node.end_byte()])
else {
continue;
};
let Some(value) = decl.child_by_field_name("value") else {
continue;
};
if js_assignment_reaches_module_exports(&value, bytes) {
return Some(alias.to_string());
}
}
}
None
}
fn js_assignment_reaches_module_exports(node: &tree_sitter::Node<'_>, bytes: &[u8]) -> bool {
if node.kind() != "assignment_expression" {
return false;
}
if let Some(left) = node.child_by_field_name("left")
&& left.kind() == "member_expression"
&& let (Some(obj), Some(prop)) = (
left.child_by_field_name("object"),
left.child_by_field_name("property"),
)
{
let obj_text = std::str::from_utf8(&bytes[obj.start_byte()..obj.end_byte()]).unwrap_or("");
let prop_text =
std::str::from_utf8(&bytes[prop.start_byte()..prop.end_byte()]).unwrap_or("");
if obj_text == "module" && prop_text == "exports" {
return true;
}
}
node.child_by_field_name("right")
.is_some_and(|right| js_assignment_reaches_module_exports(&right, bytes))
}
fn js_classify_export(
node: &tree_sitter::Node<'_>,
bytes: &[u8],
file_path: &Path,
out: &mut Vec<EntryPoint>,
) {
let line = u32::try_from(node.start_position().row + 1).unwrap_or(u32::MAX);
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"function_declaration" => {
if let Some(name_node) = child.child_by_field_name("name")
&& let Ok(name) =
std::str::from_utf8(&bytes[name_node.start_byte()..name_node.end_byte()])
{
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
}
}
"class_declaration" => {
if let Some(name_node) = child.child_by_field_name("name")
&& let Ok(name) =
std::str::from_utf8(&bytes[name_node.start_byte()..name_node.end_byte()])
{
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
}
}
"lexical_declaration" => {
js_collect_lexical_exports(&child, bytes, file_path, line, out);
}
_ => {}
}
}
}
fn js_collect_lexical_exports(
node: &tree_sitter::Node<'_>,
bytes: &[u8],
file_path: &Path,
line: u32,
out: &mut Vec<EntryPoint>,
) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() != "variable_declarator" {
continue;
}
let Some(name_node) = child.child_by_field_name("name") else {
continue;
};
let Ok(name) = std::str::from_utf8(&bytes[name_node.start_byte()..name_node.end_byte()])
else {
continue;
};
if child
.child_by_field_name("value")
.is_some_and(|v| matches!(v.kind(), "arrow_function" | "function_expression"))
{
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
}
}
}
fn js_classify_expression_statement(
node: &tree_sitter::Node<'_>,
bytes: &[u8],
file_path: &Path,
is_test_file: bool,
module_exports_alias: Option<&str>,
out: &mut Vec<EntryPoint>,
) {
let line = u32::try_from(node.start_position().row + 1).unwrap_or(u32::MAX);
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"assignment_expression" => {
js_classify_assignment(&child, bytes, file_path, line, module_exports_alias, out);
}
"call_expression" if is_test_file => {
js_classify_test_call(&child, bytes, file_path, line, out);
}
_ => {}
}
}
}
fn js_classify_assignment(
node: &tree_sitter::Node<'_>,
bytes: &[u8],
file_path: &Path,
line: u32,
module_exports_alias: Option<&str>,
out: &mut Vec<EntryPoint>,
) {
let Some(left) = node.child_by_field_name("left") else {
return;
};
if left.kind() == "identifier" {
let Ok(left_name) = std::str::from_utf8(&bytes[left.start_byte()..left.end_byte()]) else {
return;
};
if left_name == "exports"
&& let Some(right) = node.child_by_field_name("right")
{
if right.kind() == "assignment_expression" {
js_classify_assignment(&right, bytes, file_path, line, module_exports_alias, out);
} else {
js_emit_identifier_as_export(&right, bytes, file_path, line, out);
}
}
return;
}
if left.kind() != "member_expression" {
return;
}
let Some(obj_node) = left.child_by_field_name("object") else {
return;
};
let Some(prop_node) = left.child_by_field_name("property") else {
return;
};
let Ok(obj) = std::str::from_utf8(&bytes[obj_node.start_byte()..obj_node.end_byte()]) else {
return;
};
let Ok(prop) = std::str::from_utf8(&bytes[prop_node.start_byte()..prop_node.end_byte()]) else {
return;
};
if obj == "module" && prop == "exports" {
out.push(EntryPoint {
name: "module.exports".to_string(),
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
} else if obj == "exports" {
out.push(EntryPoint {
name: prop.to_string(),
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
} else if module_exports_alias.is_some_and(|alias| alias == obj) {
let is_fn = node.child_by_field_name("right").is_some_and(|v| {
matches!(
v.kind(),
"function_expression" | "arrow_function" | "function_declaration"
)
});
if is_fn {
out.push(EntryPoint {
name: prop.to_string(),
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
}
}
}
fn js_emit_identifier_as_export(
node: &tree_sitter::Node<'_>,
bytes: &[u8],
file_path: &Path,
line: u32,
out: &mut Vec<EntryPoint>,
) {
if node.kind() == "identifier"
&& let Ok(name) = std::str::from_utf8(&bytes[node.start_byte()..node.end_byte()])
&& !name.is_empty()
{
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
}
}
fn js_classify_test_call(
node: &tree_sitter::Node<'_>,
bytes: &[u8],
file_path: &Path,
line: u32,
out: &mut Vec<EntryPoint>,
) {
let Some(func_node) = node.child_by_field_name("function") else {
return;
};
let Ok(func_name) = std::str::from_utf8(&bytes[func_node.start_byte()..func_node.end_byte()])
else {
return;
};
if !matches!(func_name, "test" | "it" | "describe") {
return;
}
let entry_name = node
.child_by_field_name("arguments")
.and_then(|args| {
let mut c = args.walk();
args.children(&mut c).find(|ch| ch.kind() == "string")
})
.and_then(|s| {
let mut c = s.walk();
s.children(&mut c).find(|ch| ch.kind() == "string_fragment")
})
.and_then(|frag| {
std::str::from_utf8(&bytes[frag.start_byte()..frag.end_byte()])
.ok()
.map(ToString::to_string)
})
.unwrap_or_else(|| func_name.to_string());
out.push(EntryPoint {
name: entry_name,
kind: EntryPointKind::Test,
file_path: file_path.to_path_buf(),
line,
});
}
#[derive(Debug, Default, Clone, Copy)]
pub struct JavaEntryDetector;
impl EntryPointDetector for JavaEntryDetector {
fn detect(&self, source: &str, file_path: &Path) -> Vec<EntryPoint> {
let mut entries = Vec::new();
let Some(tree) = parse_with(source, &tree_sitter_java::LANGUAGE.into()) else {
return entries;
};
let root = tree.root_node();
let bytes = source.as_bytes();
visit_java_node(&root, bytes, file_path, &mut entries);
entries
}
}
fn visit_java_node(node: &Node<'_>, bytes: &[u8], file_path: &Path, out: &mut Vec<EntryPoint>) {
match node.kind() {
"class_declaration" | "interface_declaration" | "enum_declaration" => {
java_classify_class(node, bytes, file_path, out);
}
"method_declaration" => {
java_classify_method(node, bytes, file_path, out);
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
visit_java_node(&child, bytes, file_path, out);
}
}
const JAVA_FRAMEWORK_CLASS_ANNOTATIONS: &[&str] = &[
"SpringBootApplication",
"SpringBootTest",
"EnableAutoConfiguration",
];
const JAVA_STEREOTYPE_CLASS_ANNOTATIONS: &[&str] = &[
"Component",
"Service",
"Repository",
"Controller",
"RestController",
"Configuration",
"AutoConfiguration",
"ConfigurationProperties",
];
const JAVA_TEST_METHOD_ANNOTATIONS: &[&str] = &[
"Test",
"ParameterizedTest",
"RepeatedTest",
"TestFactory",
"TestTemplate",
"BeforeEach",
"AfterEach",
"BeforeAll",
"AfterAll",
];
fn java_classify_class(node: &Node<'_>, bytes: &[u8], file_path: &Path, out: &mut Vec<EntryPoint>) {
let Some(name_node) = node.child_by_field_name("name") else {
return;
};
let Ok(name) = std::str::from_utf8(&bytes[name_node.start_byte()..name_node.end_byte()]) else {
return;
};
let line = u32::try_from(node.start_position().row + 1).unwrap_or(u32::MAX);
let annotations = java_collect_annotation_names(node, bytes);
if annotations
.iter()
.any(|a| JAVA_FRAMEWORK_CLASS_ANNOTATIONS.contains(&a.as_str()))
{
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::FrameworkDispatched,
file_path: file_path.to_path_buf(),
line,
});
}
if annotations
.iter()
.any(|a| JAVA_STEREOTYPE_CLASS_ANNOTATIONS.contains(&a.as_str()))
{
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
}
}
fn java_classify_method(
node: &Node<'_>,
bytes: &[u8],
file_path: &Path,
out: &mut Vec<EntryPoint>,
) {
let Some(name_node) = node.child_by_field_name("name") else {
return;
};
let Ok(name) = std::str::from_utf8(&bytes[name_node.start_byte()..name_node.end_byte()]) else {
return;
};
let line = u32::try_from(node.start_position().row + 1).unwrap_or(u32::MAX);
let annotations = java_collect_annotation_names(node, bytes);
if name == "main" && java_method_is_public_static(node, bytes) {
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::Main,
file_path: file_path.to_path_buf(),
line,
});
}
if annotations
.iter()
.any(|a| JAVA_TEST_METHOD_ANNOTATIONS.contains(&a.as_str()))
{
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::Test,
file_path: file_path.to_path_buf(),
line,
});
}
if annotations.iter().any(|a| a == "Bean") {
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
}
}
fn java_collect_annotation_names(node: &Node<'_>, bytes: &[u8]) -> Vec<String> {
let mut names = Vec::new();
let Some(modifiers) = java_find_modifiers_child(node) else {
return names;
};
let mut cursor = modifiers.walk();
for child in modifiers.children(&mut cursor) {
match child.kind() {
"marker_annotation" | "annotation" => {
if let Some(name_node) = child.child_by_field_name("name")
&& let Some(ident) = java_annotation_trailing_identifier(&name_node, bytes)
{
names.push(ident);
}
}
_ => {}
}
}
names
}
fn java_find_modifiers_child<'tree>(node: &Node<'tree>) -> Option<Node<'tree>> {
let mut cursor = node.walk();
node.children(&mut cursor)
.find(|&child| child.kind() == "modifiers")
}
fn java_annotation_trailing_identifier(name_node: &Node<'_>, bytes: &[u8]) -> Option<String> {
match name_node.kind() {
"identifier" => std::str::from_utf8(&bytes[name_node.start_byte()..name_node.end_byte()])
.ok()
.map(ToString::to_string),
"scoped_identifier" => {
if let Some(name) = name_node.child_by_field_name("name") {
return std::str::from_utf8(&bytes[name.start_byte()..name.end_byte()])
.ok()
.map(ToString::to_string);
}
let mut last: Option<String> = None;
let mut cursor = name_node.walk();
for child in name_node.children(&mut cursor) {
if child.kind() == "identifier"
&& let Ok(text) =
std::str::from_utf8(&bytes[child.start_byte()..child.end_byte()])
{
last = Some(text.to_string());
}
}
last
}
_ => None,
}
}
fn java_method_is_public_static(node: &Node<'_>, bytes: &[u8]) -> bool {
let Some(modifiers) = java_find_modifiers_child(node) else {
return false;
};
let text =
std::str::from_utf8(&bytes[modifiers.start_byte()..modifiers.end_byte()]).unwrap_or("");
text.contains("public") && text.contains("static")
}
#[derive(Debug, Default, Clone, Copy)]
pub struct KotlinEntryDetector;
impl EntryPointDetector for KotlinEntryDetector {
fn detect(&self, source: &str, file_path: &Path) -> Vec<EntryPoint> {
let mut entries = Vec::new();
let Some(tree) = parse_with(source, &tree_sitter_kotlin_ng::LANGUAGE.into()) else {
return entries;
};
let root = tree.root_node();
let bytes = source.as_bytes();
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
if child.kind() == "function_declaration" {
kotlin_classify_top_level_function(&child, bytes, file_path, &mut entries);
}
}
visit_kotlin_node(&root, bytes, file_path, &mut entries);
entries
}
}
const KOTLIN_FRAMEWORK_CLASS_ANNOTATIONS: &[&str] = &[
"SpringBootApplication",
"SpringBootTest",
"EnableAutoConfiguration",
];
const KOTLIN_STEREOTYPE_CLASS_ANNOTATIONS: &[&str] = &[
"Component",
"Service",
"Repository",
"Controller",
"RestController",
"Configuration",
"AutoConfiguration",
"ConfigurationProperties",
];
const KOTLIN_TEST_FUNCTION_ANNOTATIONS: &[&str] = &[
"Test",
"ParameterizedTest",
"RepeatedTest",
"TestFactory",
"TestTemplate",
];
fn visit_kotlin_node(node: &Node<'_>, bytes: &[u8], file_path: &Path, out: &mut Vec<EntryPoint>) {
match node.kind() {
"class_declaration" | "object_declaration" => {
kotlin_classify_class(node, bytes, file_path, out);
}
"function_declaration" => {
kotlin_classify_function_annotations(node, bytes, file_path, out);
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
visit_kotlin_node(&child, bytes, file_path, out);
}
}
fn kotlin_classify_top_level_function(
node: &Node<'_>,
bytes: &[u8],
file_path: &Path,
out: &mut Vec<EntryPoint>,
) {
let Some(name_node) = node.child_by_field_name("name") else {
return;
};
let Ok(name) = std::str::from_utf8(&bytes[name_node.start_byte()..name_node.end_byte()]) else {
return;
};
let line = u32::try_from(node.start_position().row + 1).unwrap_or(u32::MAX);
if name == "main" {
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::Main,
file_path: file_path.to_path_buf(),
line,
});
}
}
fn kotlin_classify_function_annotations(
node: &Node<'_>,
bytes: &[u8],
file_path: &Path,
out: &mut Vec<EntryPoint>,
) {
let Some(name_node) = node.child_by_field_name("name") else {
return;
};
let Ok(name) = std::str::from_utf8(&bytes[name_node.start_byte()..name_node.end_byte()]) else {
return;
};
let line = u32::try_from(node.start_position().row + 1).unwrap_or(u32::MAX);
let annotations = kotlin_collect_annotation_names(node, bytes);
if annotations
.iter()
.any(|a| KOTLIN_TEST_FUNCTION_ANNOTATIONS.contains(&a.as_str()))
{
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::Test,
file_path: file_path.to_path_buf(),
line,
});
}
}
fn kotlin_classify_class(
node: &Node<'_>,
bytes: &[u8],
file_path: &Path,
out: &mut Vec<EntryPoint>,
) {
let Some(name_node) = node.child_by_field_name("name") else {
return;
};
let Ok(name) = std::str::from_utf8(&bytes[name_node.start_byte()..name_node.end_byte()]) else {
return;
};
let line = u32::try_from(node.start_position().row + 1).unwrap_or(u32::MAX);
let annotations = kotlin_collect_annotation_names(node, bytes);
if annotations
.iter()
.any(|a| KOTLIN_FRAMEWORK_CLASS_ANNOTATIONS.contains(&a.as_str()))
{
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::FrameworkDispatched,
file_path: file_path.to_path_buf(),
line,
});
}
if annotations
.iter()
.any(|a| KOTLIN_STEREOTYPE_CLASS_ANNOTATIONS.contains(&a.as_str()))
{
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::LibraryExport,
file_path: file_path.to_path_buf(),
line,
});
}
if (name.ends_with("Test") || name.ends_with("Spec")) && name.len() > 4 {
out.push(EntryPoint {
name: name.to_string(),
kind: EntryPointKind::Test,
file_path: file_path.to_path_buf(),
line,
});
}
}
fn kotlin_collect_annotation_names(node: &Node<'_>, bytes: &[u8]) -> Vec<String> {
let mut names = Vec::new();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"modifiers" | "modifier_list" => {
kotlin_collect_annotations_in(&child, bytes, &mut names);
}
"annotation" | "single_annotation" => {
if let Some(ident) = kotlin_annotation_identifier(&child, bytes) {
names.push(ident);
}
}
_ => {}
}
}
names
}
fn kotlin_collect_annotations_in(node: &Node<'_>, bytes: &[u8], out: &mut Vec<String>) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"annotation" | "single_annotation" => {
if let Some(ident) = kotlin_annotation_identifier(&child, bytes) {
out.push(ident);
}
}
_ => {}
}
}
}
fn kotlin_annotation_identifier(node: &Node<'_>, bytes: &[u8]) -> Option<String> {
let mut last: Option<String> = None;
let mut stack: Vec<Node<'_>> = vec![*node];
while let Some(n) = stack.pop() {
if matches!(n.kind(), "identifier" | "simple_identifier")
&& let Ok(text) = std::str::from_utf8(&bytes[n.start_byte()..n.end_byte()])
{
if !text.is_empty() && text != "@" {
last = Some(text.to_string());
}
}
let mut cursor = n.walk();
for child in n.children(&mut cursor) {
stack.push(child);
}
}
last
}
#[must_use]
pub fn detector_for(language: &str) -> Option<Box<dyn EntryPointDetector>> {
match language {
"rust" | "rs" => Some(Box::new(RustEntryDetector)),
"python" | "py" | "pyi" => Some(Box::new(PythonEntryDetector)),
"go" => Some(Box::new(GoEntryDetector)),
"c" | "h" => Some(Box::new(CEntryDetector)),
"javascript" | "js" | "jsx" | "typescript" | "ts" | "tsx" => {
Some(Box::new(JsEntryDetector))
}
"java" => Some(Box::new(JavaEntryDetector)),
"kotlin" | "kt" | "kts" => Some(Box::new(KotlinEntryDetector)),
_ => None,
}
}
#[must_use]
pub fn summarize_entry_point_kinds<S: std::hash::BuildHasher>(
counts: &std::collections::HashMap<EntryPointKind, usize, S>,
) -> Vec<String> {
let mut summary: Vec<String> = counts
.iter()
.map(|(kind, count)| format!("{count} {label}", label = label_for_kind(*kind)))
.collect();
summary.sort();
summary
}
#[must_use]
pub fn label_for_kind(kind: EntryPointKind) -> &'static str {
match kind {
EntryPointKind::Main => "main",
EntryPointKind::LibraryExport => "library exports",
EntryPointKind::Test => "tests",
EntryPointKind::Ffi => "FFI",
EntryPointKind::ProcMacro => "proc-macros",
EntryPointKind::Init => "init functions",
EntryPointKind::BuildScript => "build scripts",
EntryPointKind::FrameworkDispatched => "framework-dispatched (MCP tools)",
}
}
fn parse_with(source: &str, language: &tree_sitter::Language) -> Option<tree_sitter::Tree> {
let mut parser = Parser::new();
parser.set_language(language).ok()?;
parser.parse(source, None)
}
#[allow(dead_code)]
pub(crate) fn query_match_lines(
source: &str,
language: &tree_sitter::Language,
query: &Query,
) -> Vec<u32> {
let mut lines = Vec::new();
let Some(tree) = parse_with(source, language) else {
return lines;
};
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(query, tree.root_node(), source.as_bytes());
while let Some(m) = matches.next() {
for cap in m.captures {
let line = u32::try_from(cap.node.start_position().row + 1).unwrap_or(u32::MAX);
lines.push(line);
}
}
lines
}