use std::sync::{Arc, OnceLock};
use tree_sitter::{Language, Query};
pub struct CallConfig {
pub language: Language,
pub query: Query,
}
pub struct LangConfig {
pub language: Language,
pub query: Query,
}
pub mod lsp_symbol_kind {
pub const FILE: u32 = 1;
pub const MODULE: u32 = 2;
pub const NAMESPACE: u32 = 3;
pub const PACKAGE: u32 = 4;
pub const CLASS: u32 = 5;
pub const METHOD: u32 = 6;
pub const PROPERTY: u32 = 7;
pub const FIELD: u32 = 8;
pub const CONSTRUCTOR: u32 = 9;
pub const ENUM: u32 = 10;
pub const INTERFACE: u32 = 11;
pub const FUNCTION: u32 = 12;
pub const VARIABLE: u32 = 13;
pub const CONSTANT: u32 = 14;
pub const STRING: u32 = 15;
pub const NUMBER: u32 = 16;
pub const BOOLEAN: u32 = 17;
pub const ARRAY: u32 = 18;
pub const OBJECT: u32 = 19;
pub const KEY: u32 = 20;
pub const NULL: u32 = 21;
pub const ENUM_MEMBER: u32 = 22;
pub const STRUCT: u32 = 23;
pub const EVENT: u32 = 24;
pub const OPERATOR: u32 = 25;
pub const TYPE_PARAMETER: u32 = 26;
}
#[must_use]
#[expect(
clippy::match_same_arms,
reason = "variable/assignment patterns are explicit for documentation completeness; \
they intentionally duplicate the wildcard fallback (VARIABLE=13) so the \
mapping table reads as a self-contained reference"
)]
pub fn lsp_symbol_kind_for_node_kind(node_kind: &str) -> u32 {
use lsp_symbol_kind as K;
match node_kind {
"function_item"
| "function_signature_item"
| "function_definition"
| "function_declaration" => K::FUNCTION,
"method_definition" | "method_declaration" => K::METHOD,
"constructor_declaration" => K::CONSTRUCTOR,
"struct_item" => K::STRUCT,
"enum_item" | "enum_declaration" | "enum_definition" => K::ENUM,
"enum_variant" => K::ENUM_MEMBER,
"trait_item"
| "interface_declaration"
| "trait_definition"
| "protocol_declaration"
| "interface_type" => K::INTERFACE,
"struct_type" => K::STRUCT,
"impl_item" | "class_definition" | "class_declaration" | "class_specifier"
| "object_definition" | "object_declaration" | "element" => K::CLASS,
"mod_item" | "module" | "mod_definition" | "atx_heading" => K::MODULE,
"namespace_definition" => K::NAMESPACE,
"const_item" | "static_item" | "local_attribute" => K::CONSTANT,
"type_item"
| "type_alias_declaration"
| "type_definition"
| "type_declaration"
| "typealias_declaration" => K::TYPE_PARAMETER,
"type_alias" => K::VARIABLE,
"field_declaration" => K::FIELD,
"variable_declarator"
| "variable_declaration"
| "assignment"
| "val_definition"
| "var_definition" => K::VARIABLE,
"property_declaration" | "decorated_definition" => K::PROPERTY,
"block" | "table" | "pair" => K::KEY,
"create_table" => K::STRUCT,
"cte" => K::VARIABLE,
"sql_file" => K::FILE,
"rdf_statements" => K::OBJECT,
"file" | "window" => K::FILE,
_ => K::VARIABLE,
}
}
#[must_use]
pub fn lsp_symbol_kind_for_decorated_definition(first_decorator_name: &str) -> u32 {
use lsp_symbol_kind as K;
match first_decorator_name {
"property" | "cached_property" => K::PROPERTY,
_ => K::FUNCTION,
}
}
fn first_decorator_ident<'src>(
node: &tree_sitter::Node<'_>,
source: &'src [u8],
) -> Option<&'src str> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "decorator" {
let mut inner = child.walk();
for inner_child in child.children(&mut inner) {
match inner_child.kind() {
"identifier" => {
let start = inner_child.start_byte();
let end = inner_child.end_byte();
return std::str::from_utf8(&source[start..end]).ok();
}
"attribute" | "call" => return None,
_ => {}
}
}
return None;
}
}
None
}
#[must_use]
pub fn lsp_symbol_kind_for_node(node: &tree_sitter::Node<'_>, source: &[u8]) -> u32 {
if node.kind() == "decorated_definition" {
let decorator = first_decorator_ident(node, source).unwrap_or("");
return lsp_symbol_kind_for_decorated_definition(decorator);
}
lsp_symbol_kind_for_node_kind(node.kind())
}
#[must_use]
pub fn is_rust_language(lang: &tree_sitter::Language) -> bool {
let rust_lang: tree_sitter::Language = tree_sitter_rust::LANGUAGE.into();
lang.abi_version() == rust_lang.abi_version()
&& lang.node_kind_count() == rust_lang.node_kind_count()
}
#[must_use]
pub fn is_python_language(lang: &tree_sitter::Language) -> bool {
let py_lang: tree_sitter::Language = tree_sitter_python::LANGUAGE.into();
lang.abi_version() == py_lang.abi_version()
&& lang.node_kind_count() == py_lang.node_kind_count()
}
#[must_use]
pub fn is_go_language(lang: &tree_sitter::Language) -> bool {
let go_lang: tree_sitter::Language = tree_sitter_go::LANGUAGE.into();
lang.abi_version() == go_lang.abi_version()
&& lang.node_kind_count() == go_lang.node_kind_count()
}
#[must_use]
pub fn is_hcl_language(lang: &tree_sitter::Language) -> bool {
let hcl_lang: tree_sitter::Language = tree_sitter_hcl::LANGUAGE.into();
lang.abi_version() == hcl_lang.abi_version()
&& lang.node_kind_count() == hcl_lang.node_kind_count()
}
#[must_use]
pub fn is_sql_language(lang: &tree_sitter::Language) -> bool {
let sql_lang: tree_sitter::Language = tree_sitter_sequel::LANGUAGE.into();
lang.abi_version() == sql_lang.abi_version()
&& lang.node_kind_count() == sql_lang.node_kind_count()
}
#[must_use]
pub fn derive_hcl_block_name(block_node: &tree_sitter::Node<'_>, source: &[u8]) -> String {
if block_node.kind() != "block" {
return String::new();
}
let mut labels: Vec<&str> = Vec::new();
let mut cursor = block_node.walk();
for child in block_node.children(&mut cursor) {
if child.kind() == "string_lit" {
let mut inner = child.walk();
for grandchild in child.children(&mut inner) {
if grandchild.kind() == "template_literal" {
let start = grandchild.start_byte();
let end = grandchild.end_byte();
if let Ok(text) = std::str::from_utf8(&source[start..end]) {
labels.push(text);
}
}
}
} else if child.kind() == "block_start" {
break;
}
}
match labels.len() {
0 => {
let mut cursor2 = block_node.walk();
for child in block_node.children(&mut cursor2) {
if child.kind() == "identifier" {
let start = child.start_byte();
let end = child.end_byte();
if let Ok(text) = std::str::from_utf8(&source[start..end]) {
return text.to_string();
}
}
}
String::new()
}
1 => labels[0].to_string(),
_ => {
labels.join(".")
}
}
}
#[must_use]
pub fn config_for_extension(ext: &str) -> Option<Arc<LangConfig>> {
static CACHE: OnceLock<std::collections::HashMap<&'static str, Arc<LangConfig>>> =
OnceLock::new();
let cache = CACHE.get_or_init(|| {
let mut m = std::collections::HashMap::new();
for &ext in &[
"rs", "py", "pyi", "js", "jsx", "ts", "tsx", "go", "java", "c", "h", "cpp", "cc",
"cxx", "hpp", "sh", "bash", "bats", "rb", "tf", "tfvars", "hcl", "kt", "kts", "swift",
"scala", "toml", "json", "yaml", "yml", "md", "xml", "rdf", "owl", "sql",
] {
if let Some(cfg) = compile_config(ext) {
m.insert(ext, Arc::new(cfg));
}
}
m
});
cache.get(ext).cloned()
}
#[expect(
clippy::too_many_lines,
reason = "one match arm per language — flat by design"
)]
fn compile_config(ext: &str) -> Option<LangConfig> {
let (lang, query_str): (Language, &str) = match ext {
"rs" => (
tree_sitter_rust::LANGUAGE.into(),
concat!(
"(function_item name: (identifier) @name) @def\n",
"(struct_item name: (type_identifier) @name) @def\n",
"(enum_item name: (type_identifier) @name) @def\n",
"(type_item name: (type_identifier) @name) @def\n",
"(field_declaration name: (field_identifier) @name) @def\n",
"(enum_variant name: (identifier) @name) @def\n",
"(impl_item type: (type_identifier) @name) @def\n",
"(trait_item name: (type_identifier) @name) @def\n",
"(const_item name: (identifier) @name) @def\n",
"(static_item name: (identifier) @name) @def\n",
"(mod_item name: (identifier) @name) @def",
),
),
"py" | "pyi" => (
tree_sitter_python::LANGUAGE.into(),
concat!(
"(decorated_definition (function_definition name: (identifier) @name)) @def\n",
"(function_definition name: (identifier) @name) @def\n",
"(class_definition name: (identifier) @name) @def\n",
"(assignment left: (identifier) @name) @def",
),
),
"js" | "jsx" => (
tree_sitter_javascript::LANGUAGE.into(),
concat!(
"(function_declaration name: (identifier) @name) @def\n",
"(method_definition name: (property_identifier) @name) @def\n",
"(class_declaration name: (identifier) @name) @def\n",
"(variable_declarator name: (identifier) @name) @def",
),
),
"ts" => (
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
concat!(
"(function_declaration name: (identifier) @name) @def\n",
"(method_definition name: (property_identifier) @name) @def\n",
"(class_declaration name: (type_identifier) @name) @def\n",
"(interface_declaration name: (type_identifier) @name) @def\n",
"(variable_declarator name: (identifier) @name) @def\n",
"(type_alias_declaration name: (type_identifier) @name) @def\n",
"(enum_declaration name: (identifier) @name) @def",
),
),
"tsx" => (
tree_sitter_typescript::LANGUAGE_TSX.into(),
concat!(
"(function_declaration name: (identifier) @name) @def\n",
"(method_definition name: (property_identifier) @name) @def\n",
"(class_declaration name: (type_identifier) @name) @def\n",
"(interface_declaration name: (type_identifier) @name) @def\n",
"(variable_declarator name: (identifier) @name) @def\n",
"(type_alias_declaration name: (type_identifier) @name) @def\n",
"(enum_declaration name: (identifier) @name) @def",
),
),
"go" => (
tree_sitter_go::LANGUAGE.into(),
concat!(
"(function_declaration name: (identifier) @name) @def\n",
"(method_declaration name: (field_identifier) @name) @def\n",
"(type_declaration (type_spec name: (type_identifier) @name type: (interface_type) @def))\n",
"(type_declaration (type_spec name: (type_identifier) @name type: (struct_type) @def))\n",
"(type_declaration (type_alias name: (type_identifier) @name) @def)\n",
"(const_spec name: (identifier) @name) @def",
),
),
"java" => (
tree_sitter_java::LANGUAGE.into(),
concat!(
"(method_declaration name: (identifier) @name) @def\n",
"(class_declaration name: (identifier) @name) @def\n",
"(interface_declaration name: (identifier) @name) @def\n",
"(field_declaration declarator: (variable_declarator name: (identifier) @name)) @def\n",
"(enum_constant name: (identifier) @name) @def\n",
"(enum_declaration name: (identifier) @name) @def\n",
"(constructor_declaration name: (identifier) @name) @def",
),
),
"c" | "h" => (
tree_sitter_c::LANGUAGE.into(),
concat!(
"(function_definition declarator: (function_declarator declarator: (identifier) @name)) @def\n",
"(declaration declarator: (init_declarator declarator: (identifier) @name)) @def\n",
"(struct_specifier name: (type_identifier) @name) @def\n",
"(enum_specifier name: (type_identifier) @name) @def\n",
"(type_definition declarator: (type_identifier) @name) @def",
),
),
"cpp" | "cc" | "cxx" | "hpp" => (
tree_sitter_cpp::LANGUAGE.into(),
concat!(
"(function_definition declarator: (function_declarator declarator: (identifier) @name)) @def\n",
"(class_specifier name: (type_identifier) @name) @def\n",
"(declaration declarator: (init_declarator declarator: (identifier) @name)) @def\n",
"(struct_specifier name: (type_identifier) @name) @def\n",
"(enum_specifier name: (type_identifier) @name) @def\n",
"(type_definition declarator: (type_identifier) @name) @def\n",
"(namespace_definition name: (namespace_identifier) @name) @def\n",
"(field_declaration declarator: (field_identifier) @name) @def",
),
),
"sh" | "bash" | "bats" => (
tree_sitter_bash::LANGUAGE.into(),
concat!(
"(function_definition name: (word) @name) @def\n",
"(variable_assignment name: (variable_name) @name) @def",
),
),
"rb" => (
tree_sitter_ruby::LANGUAGE.into(),
concat!(
"(method name: (identifier) @name) @def\n",
"(class name: (constant) @name) @def\n",
"(module name: (constant) @name) @def\n",
"(assignment left: (identifier) @name) @def\n",
"(assignment left: (constant) @name) @def",
),
),
"tf" | "tfvars" | "hcl" => (
tree_sitter_hcl::LANGUAGE.into(),
concat!(
"(block (string_lit (template_literal) @name) . (block_start)) @def\n",
"(block (identifier) @name . (block_start)) @def",
),
),
"kt" | "kts" => (
tree_sitter_kotlin_ng::LANGUAGE.into(),
concat!(
"(function_declaration name: (identifier) @name) @def\n",
"(class_declaration name: (identifier) @name) @def\n",
"(object_declaration name: (identifier) @name) @def\n",
"(property_declaration (identifier) @name) @def\n",
"(enum_entry (identifier) @name) @def",
),
),
"swift" => (
tree_sitter_swift::LANGUAGE.into(),
concat!(
"(function_declaration name: (simple_identifier) @name) @def\n",
"(class_declaration name: (type_identifier) @name) @def\n",
"(protocol_declaration name: (type_identifier) @name) @def\n",
"(property_declaration name: (pattern bound_identifier: (simple_identifier) @name)) @def\n",
"(typealias_declaration name: (type_identifier) @name) @def",
),
),
"scala" => (
tree_sitter_scala::LANGUAGE.into(),
concat!(
"(function_definition name: (identifier) @name) @def\n",
"(class_definition name: (identifier) @name) @def\n",
"(trait_definition name: (identifier) @name) @def\n",
"(object_definition name: (identifier) @name) @def\n",
"(val_definition pattern: (identifier) @name) @def\n",
"(var_definition pattern: (identifier) @name) @def\n",
"(type_definition name: (type_identifier) @name) @def",
),
),
"toml" => (
tree_sitter_toml_ng::LANGUAGE.into(),
concat!(
"(table (bare_key) @name) @def\n",
"(pair (bare_key) @name) @def",
),
),
"json" => (
tree_sitter_json::LANGUAGE.into(),
"(pair key: (string (string_content) @name)) @def",
),
"yaml" | "yml" => (
tree_sitter_yaml::LANGUAGE.into(),
"(block_mapping_pair key: (flow_node (plain_scalar (string_scalar) @name))) @def",
),
"md" => (
tree_sitter_md::LANGUAGE.into(),
"(atx_heading heading_content: (inline) @name) @def",
),
"xml" | "rdf" | "owl" => (
tree_sitter_xml::LANGUAGE_XML.into(),
concat!(
"(element (STag (Name) @name)) @def\n",
"(element (EmptyElemTag (Name) @name)) @def",
),
),
"sql" => (
tree_sitter_sequel::LANGUAGE.into(),
concat!(
"(create_table (object_reference name: (identifier) @name)) @def\n",
"(cte (identifier) @name) @def",
),
),
_ => return None,
};
let query = match Query::new(&lang, query_str) {
Ok(q) => q,
Err(e) => {
tracing::warn!(ext, %e, "tree-sitter query compilation failed — language may be ABI-incompatible");
return None;
}
};
Some(LangConfig {
language: lang,
query,
})
}
#[must_use]
pub fn call_query_for_extension(ext: &str) -> Option<Arc<CallConfig>> {
static CACHE: OnceLock<std::collections::HashMap<&'static str, Arc<CallConfig>>> =
OnceLock::new();
let cache = CACHE.get_or_init(|| {
let mut m = std::collections::HashMap::new();
for &ext in &[
"rs", "py", "pyi", "js", "jsx", "ts", "tsx", "go", "java", "c", "h", "cpp", "cc",
"cxx", "hpp", "sh", "bash", "bats", "rb", "tf", "tfvars", "hcl", "kt", "kts", "swift",
"scala", "sql",
] {
if let Some(cfg) = compile_call_config(ext) {
m.insert(ext, Arc::new(cfg));
}
}
m
});
cache.get(ext).cloned()
}
#[expect(
clippy::too_many_lines,
reason = "one match arm per language — flat by design"
)]
fn compile_call_config(ext: &str) -> Option<CallConfig> {
let (lang, query_str): (Language, &str) = match ext {
"rs" => (
tree_sitter_rust::LANGUAGE.into(),
concat!(
"(call_expression function: (identifier) @callee) @call\n",
"(call_expression function: (field_expression field: (field_identifier) @callee)) @call\n",
"(call_expression function: (scoped_identifier) @callee) @call",
),
),
"py" | "pyi" => (
tree_sitter_python::LANGUAGE.into(),
concat!(
"(call function: (identifier) @callee) @call\n",
"(call function: (attribute attribute: (identifier) @callee)) @call",
),
),
"js" | "jsx" => (
tree_sitter_javascript::LANGUAGE.into(),
concat!(
"(call_expression function: (identifier) @callee) @call\n",
"(call_expression function: (member_expression property: (property_identifier) @callee)) @call",
),
),
"ts" => (
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
concat!(
"(call_expression function: (identifier) @callee) @call\n",
"(call_expression function: (member_expression property: (property_identifier) @callee)) @call",
),
),
"tsx" => (
tree_sitter_typescript::LANGUAGE_TSX.into(),
concat!(
"(call_expression function: (identifier) @callee) @call\n",
"(call_expression function: (member_expression property: (property_identifier) @callee)) @call",
),
),
"go" => (
tree_sitter_go::LANGUAGE.into(),
concat!(
"(call_expression function: (identifier) @callee) @call\n",
"(call_expression function: (selector_expression field: (field_identifier) @callee)) @call",
),
),
"java" => (
tree_sitter_java::LANGUAGE.into(),
"(method_invocation name: (identifier) @callee) @call",
),
"c" | "h" => (
tree_sitter_c::LANGUAGE.into(),
concat!(
"(call_expression function: (identifier) @callee) @call\n",
"(call_expression function: (field_expression field: (field_identifier) @callee)) @call",
),
),
"cpp" | "cc" | "cxx" | "hpp" => (
tree_sitter_cpp::LANGUAGE.into(),
concat!(
"(call_expression function: (identifier) @callee) @call\n",
"(call_expression function: (field_expression field: (field_identifier) @callee)) @call",
),
),
"sh" | "bash" | "bats" => (
tree_sitter_bash::LANGUAGE.into(),
"(command name: (command_name (word) @callee)) @call",
),
"rb" => (
tree_sitter_ruby::LANGUAGE.into(),
"(call method: (identifier) @callee) @call",
),
"tf" | "tfvars" | "hcl" => (
tree_sitter_hcl::LANGUAGE.into(),
"(function_call (identifier) @callee) @call",
),
"kt" | "kts" => (
tree_sitter_kotlin_ng::LANGUAGE.into(),
"(call_expression (identifier) @callee) @call",
),
"swift" => (
tree_sitter_swift::LANGUAGE.into(),
"(call_expression (simple_identifier) @callee) @call",
),
"scala" => (
tree_sitter_scala::LANGUAGE.into(),
concat!(
"(call_expression function: (identifier) @callee) @call\n",
"(call_expression function: (field_expression field: (identifier) @callee)) @call",
),
),
"sql" => (
tree_sitter_sequel::LANGUAGE.into(),
concat!(
"(from (relation (object_reference name: (identifier) @callee))) @call\n",
"(join (relation (object_reference name: (identifier) @callee))) @call",
),
),
_ => return None,
};
let query = match Query::new(&lang, query_str) {
Ok(q) => q,
Err(e) => {
tracing::warn!(ext, %e, "tree-sitter call query compilation failed");
return None;
}
};
Some(CallConfig {
language: lang,
query,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rust_extension_resolves() {
assert!(config_for_extension("rs").is_some());
}
#[test]
fn python_extension_resolves() {
assert!(config_for_extension("py").is_some());
}
#[test]
fn python_stub_extension_resolves() {
assert!(config_for_extension("pyi").is_some());
}
#[test]
fn unknown_extension_returns_none() {
assert!(config_for_extension("xyz").is_none());
}
#[test]
fn all_supported_extensions() {
let exts = [
"rs", "py", "pyi", "js", "jsx", "ts", "tsx", "go", "java", "c", "h", "cpp", "cc",
"cxx", "hpp", "sh", "bash", "bats", "rb", "tf", "tfvars", "hcl", "kt", "kts", "swift",
"scala", "toml", "json", "yaml", "yml", "md", "xml", "rdf", "owl", "sql",
];
for ext in &exts {
assert!(config_for_extension(ext).is_some(), "failed for {ext}");
}
}
#[test]
fn turtle_family_uses_rdf_text_chunking_not_tree_sitter() {
for ext in ["ttl", "nt", "n3", "trig", "nq"] {
assert!(
config_for_extension(ext).is_none(),
"{ext} should be handled by RDF text chunking"
);
assert!(crate::chunk::is_rdf_text_extension(ext));
}
}
#[test]
fn all_call_query_extensions() {
let exts = [
"rs", "py", "pyi", "js", "jsx", "ts", "tsx", "go", "java", "c", "h", "cpp", "cc",
"cxx", "hpp", "sh", "bash", "bats", "rb", "tf", "tfvars", "hcl", "kt", "kts", "swift",
"scala", "sql",
];
for ext in &exts {
assert!(
call_query_for_extension(ext).is_some(),
"call query failed for {ext}"
);
}
}
#[test]
fn toml_has_no_call_query() {
assert!(call_query_for_extension("toml").is_none());
}
#[test]
fn test_scoped_identifier_call_query_captures_full_path() {
use streaming_iterator::StreamingIterator as _;
let source = "
fn caller() {
mod_a::foo();
std::io::stderr();
}
";
let call_cfg = call_query_for_extension("rs").expect("rs call config");
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&call_cfg.language)
.expect("set language");
let tree = parser.parse(source, None).expect("parse");
let mut cursor = tree_sitter::QueryCursor::new();
let mut matches = cursor.matches(&call_cfg.query, tree.root_node(), source.as_bytes());
let mut callees: Vec<String> = Vec::new();
while let Some(m) = matches.next() {
for cap in m.captures {
let name = &call_cfg.query.capture_names()[cap.index as usize];
if *name == "callee" {
let text = &source[cap.node.start_byte()..cap.node.end_byte()];
callees.push(text.to_string());
}
}
}
assert!(
callees.contains(&"mod_a::foo".to_string()),
"expected 'mod_a::foo' in callees, got: {callees:?}"
);
assert!(
!callees.contains(&"foo".to_string()),
"bare 'foo' must not appear for scoped call; got: {callees:?}"
);
}
#[test]
fn rust_node_kind_maps_to_lsp_symbol_kind_struct() {
assert_eq!(
lsp_symbol_kind_for_node_kind("struct_item"),
lsp_symbol_kind::STRUCT,
"struct_item must map to SymbolKind::Struct (23)"
);
}
#[test]
fn rust_node_kind_maps_to_lsp_symbol_kind_trait() {
assert_eq!(
lsp_symbol_kind_for_node_kind("trait_item"),
lsp_symbol_kind::INTERFACE,
"trait_item must map to SymbolKind::Interface (11)"
);
}
#[test]
fn rust_node_kind_maps_to_lsp_symbol_kind_enum() {
assert_eq!(
lsp_symbol_kind_for_node_kind("enum_item"),
lsp_symbol_kind::ENUM,
"enum_item must map to SymbolKind::Enum (10)"
);
}
#[test]
fn rust_node_kind_maps_to_lsp_symbol_kind_function() {
assert_eq!(
lsp_symbol_kind_for_node_kind("function_item"),
lsp_symbol_kind::FUNCTION,
"function_item must map to SymbolKind::Function (12)"
);
}
#[test]
fn rust_node_kind_maps_to_lsp_symbol_kind_module() {
assert_eq!(
lsp_symbol_kind_for_node_kind("mod_item"),
lsp_symbol_kind::MODULE,
"mod_item must map to SymbolKind::Module (2)"
);
}
#[test]
fn rust_node_kinds_map_to_non_variable_kinds() {
let cases: &[(&str, u32)] = &[
("impl_item", lsp_symbol_kind::CLASS),
("const_item", lsp_symbol_kind::CONSTANT),
("static_item", lsp_symbol_kind::CONSTANT),
("type_item", lsp_symbol_kind::TYPE_PARAMETER),
("field_declaration", lsp_symbol_kind::FIELD),
("enum_variant", lsp_symbol_kind::ENUM_MEMBER),
("function_signature_item", lsp_symbol_kind::FUNCTION),
];
for &(kind, expected) in cases {
assert_eq!(
lsp_symbol_kind_for_node_kind(kind),
expected,
"node kind '{kind}' should map to {expected}, got {}",
lsp_symbol_kind_for_node_kind(kind)
);
}
}
#[test]
fn unknown_node_kind_falls_back_to_variable() {
assert_eq!(
lsp_symbol_kind_for_node_kind("some_unknown_kind"),
lsp_symbol_kind::VARIABLE,
"unknown kind must fall back to Variable (13)"
);
}
#[test]
fn python_property_decorator_classifies_as_property_kind() {
assert_eq!(
lsp_symbol_kind_for_node_kind("decorated_definition"),
lsp_symbol_kind::PROPERTY,
"decorated_definition must map to SymbolKind::Property (22); baseline gave Variable (13)"
);
}
#[test]
fn python_property_query_captures_decorated_definition() {
use streaming_iterator::StreamingIterator as _;
let source = r"class MyModel:
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
def regular_method(self):
pass
";
let cfg = config_for_extension("py").expect("Python config must compile");
let mut parser = tree_sitter::Parser::new();
parser.set_language(&cfg.language).expect("set language");
let tree = parser.parse(source, None).expect("parse");
let mut cursor = tree_sitter::QueryCursor::new();
let mut matches = cursor.matches(&cfg.query, tree.root_node(), source.as_bytes());
let mut property_kind_found = false;
let mut property_name_found = false;
while let Some(m) = matches.next() {
let mut name = "";
let mut def_kind = "";
for cap in m.captures {
let cap_name = &cfg.query.capture_names()[cap.index as usize];
let text = &source[cap.node.start_byte()..cap.node.end_byte()];
if *cap_name == "name" {
name = text;
} else if *cap_name == "def" {
def_kind = cap.node.kind();
}
}
if def_kind == "decorated_definition" && name == "name" {
property_kind_found = true;
property_name_found = true;
}
}
assert!(
property_kind_found,
"Python query must capture decorated_definition for @property method; got none"
);
assert!(
property_name_found,
"Python query must capture 'name' as the method name inside @property definition"
);
}
#[test]
fn go_interface_type_classifies_as_interface_kind() {
assert_eq!(
lsp_symbol_kind_for_node_kind("interface_type"),
lsp_symbol_kind::INTERFACE,
"interface_type must map to SymbolKind::Interface (11); baseline gave TypeParameter (26)"
);
}
#[test]
fn go_struct_type_classifies_as_struct_kind() {
assert_eq!(
lsp_symbol_kind_for_node_kind("struct_type"),
lsp_symbol_kind::STRUCT,
"struct_type must map to SymbolKind::Struct (23); baseline gave TypeParameter (26)"
);
}
#[test]
fn go_interface_query_captures_interface_type() {
use streaming_iterator::StreamingIterator as _;
let source = r"package io
type Reader interface {
Read(p []byte) (n int, err error)
}
type MyStruct struct {
Name string
}
func NewReader() Reader {
return nil
}
";
let cfg = config_for_extension("go").expect("Go config must compile");
let mut parser = tree_sitter::Parser::new();
parser.set_language(&cfg.language).expect("set language");
let tree = parser.parse(source, None).expect("parse");
let mut cursor = tree_sitter::QueryCursor::new();
let mut matches = cursor.matches(&cfg.query, tree.root_node(), source.as_bytes());
let mut interface_found = false;
let mut struct_found = false;
let mut function_found = false;
while let Some(m) = matches.next() {
let mut name = "";
let mut def_kind = "";
for cap in m.captures {
let cap_name = &cfg.query.capture_names()[cap.index as usize];
let text = &source[cap.node.start_byte()..cap.node.end_byte()];
if *cap_name == "name" {
name = text;
} else if *cap_name == "def" {
def_kind = cap.node.kind();
}
}
if def_kind == "interface_type" && name == "Reader" {
interface_found = true;
}
if def_kind == "struct_type" && name == "MyStruct" {
struct_found = true;
}
if def_kind == "function_declaration" && name == "NewReader" {
function_found = true;
}
}
assert!(
interface_found,
"Go query must emit def_kind='interface_type' for 'type Reader interface {{ ... }}'"
);
assert!(
struct_found,
"Go query must emit def_kind='struct_type' for 'type MyStruct struct {{ ... }}'"
);
assert!(
function_found,
"Go query must emit def_kind='function_declaration' for 'func NewReader()'"
);
}
#[test]
fn hcl_resource_symbol_uses_type_dot_name() {
let source = br#"resource "aws_iam_role" "loader" {
assume_role_policy = "assume.json"
}
"#;
let lang: tree_sitter::Language = tree_sitter_hcl::LANGUAGE.into();
let mut parser = tree_sitter::Parser::new();
parser.set_language(&lang).expect("set HCL language");
let tree = parser.parse(source, None).expect("parse HCL");
let root = tree.root_node();
let body = root.child(0).expect("config_file has body");
#[expect(
clippy::cast_possible_truncation,
reason = "child_count() is a small usize; fits in u32"
)]
let block = (0..body.child_count())
.filter_map(|i| body.child(i as u32))
.find(|n| n.kind() == "block")
.expect("should have at least one block node");
let name = derive_hcl_block_name(&block, source);
assert_eq!(
name, "aws_iam_role.loader",
"derive_hcl_block_name must produce 'aws_iam_role.loader' for \
`resource \"aws_iam_role\" \"loader\"` block; got {name:?}"
);
}
#[test]
fn hcl_data_source_symbol_uses_type_dot_name() {
let source = br#"data "aws_s3_bucket" "main" {
bucket = "my-bucket"
}
"#;
let lang: tree_sitter::Language = tree_sitter_hcl::LANGUAGE.into();
let mut parser = tree_sitter::Parser::new();
parser.set_language(&lang).expect("set HCL language");
let tree = parser.parse(source, None).expect("parse HCL");
let root = tree.root_node();
let body = root.child(0).expect("config_file has body");
#[expect(
clippy::cast_possible_truncation,
reason = "child_count() returns a small usize; fits in u32"
)]
let block = (0..body.child_count())
.filter_map(|i| body.child(i as u32))
.find(|n| n.kind() == "block")
.expect("block node");
let name = derive_hcl_block_name(&block, source);
assert_eq!(
name, "aws_s3_bucket.main",
"derive_hcl_block_name must produce 'aws_s3_bucket.main'"
);
}
#[test]
fn hcl_query_captures_resource_name_not_keyword() {
use streaming_iterator::StreamingIterator as _;
let source = r#"resource "aws_iam_role" "loader" {
x = 1
}
variable "region" {
type = "string"
}
output "role_arn" {
value = "arn"
}
locals {
x = 1
}
"#;
let cfg = config_for_extension("tf").expect("HCL config must compile");
let mut parser = tree_sitter::Parser::new();
parser.set_language(&cfg.language).expect("set language");
let tree = parser.parse(source, None).expect("parse");
let mut cursor = tree_sitter::QueryCursor::new();
let mut matches = cursor.matches(&cfg.query, tree.root_node(), source.as_bytes());
let mut names: Vec<(String, String)> = Vec::new(); while let Some(m) = matches.next() {
let mut name = String::new();
let mut def_kind = String::new();
for cap in m.captures {
let cap_name = &cfg.query.capture_names()[cap.index as usize];
let text = &source[cap.node.start_byte()..cap.node.end_byte()];
if *cap_name == "name" {
name = text.to_string();
} else if *cap_name == "def" {
def_kind = cap.node.kind().to_string();
}
}
if !name.is_empty() {
names.push((name, def_kind));
}
}
let name_list: Vec<&str> = names.iter().map(|(n, _)| n.as_str()).collect();
assert!(
name_list.contains(&"loader"),
"HCL query must capture 'loader' (not the keyword 'resource') for resource block; got: {name_list:?}"
);
assert!(
!name_list.contains(&"resource"),
"HCL query must NOT capture the keyword 'resource' as a symbol name; got: {name_list:?}"
);
assert!(
name_list.contains(&"region"),
"HCL query must capture 'region' for variable block; got: {name_list:?}"
);
assert!(
name_list.contains(&"role_arn"),
"HCL query must capture 'role_arn' for output block; got: {name_list:?}"
);
assert!(
name_list.contains(&"locals"),
"HCL query must capture 'locals' for locals block; got: {name_list:?}"
);
}
#[test]
fn test_python_class_definition_kind_5() {
use streaming_iterator::StreamingIterator as _;
let source = r"class Foo:
pass
";
let cfg = config_for_extension("py").expect("Python config must compile");
let mut parser = tree_sitter::Parser::new();
parser.set_language(&cfg.language).expect("set language");
let tree = parser.parse(source, None).expect("parse");
let mut cursor = tree_sitter::QueryCursor::new();
let mut matches = cursor.matches(&cfg.query, tree.root_node(), source.as_bytes());
let mut class_kind_found = false;
while let Some(m) = matches.next() {
for cap in m.captures {
let cap_name = &cfg.query.capture_names()[cap.index as usize];
if *cap_name == "def" {
let def_kind = cap.node.kind();
if def_kind == "class_definition" {
let lsp_kind = lsp_symbol_kind_for_node_kind(def_kind);
assert_eq!(
lsp_kind,
lsp_symbol_kind::CLASS,
"class_definition must map to SymbolKind::Class (5); got {lsp_kind}"
);
class_kind_found = true;
}
}
}
}
assert!(
class_kind_found,
"Python query must emit def_kind='class_definition' for 'class Foo:' pattern"
);
}
#[test]
fn test_go_type_alias_kind_21() {
use streaming_iterator::StreamingIterator as _;
let source = r"package main
type Foo = Bar
type Reader interface {
Read(p []byte) (n int, err error)
}
";
let cfg = config_for_extension("go").expect("Go config must compile");
let mut parser = tree_sitter::Parser::new();
parser.set_language(&cfg.language).expect("set language");
let tree = parser.parse(source, None).expect("parse");
let mut cursor = tree_sitter::QueryCursor::new();
let mut matches = cursor.matches(&cfg.query, tree.root_node(), source.as_bytes());
let mut alias_kind_found = false;
while let Some(m) = matches.next() {
let mut name = "";
let mut def_kind = "";
for cap in m.captures {
let cap_name = &cfg.query.capture_names()[cap.index as usize];
let text = &source[cap.node.start_byte()..cap.node.end_byte()];
if *cap_name == "name" {
name = text;
} else if *cap_name == "def" {
def_kind = cap.node.kind();
}
}
if def_kind == "type_alias" && name == "Foo" {
let lsp_kind = lsp_symbol_kind_for_node_kind(def_kind);
assert_eq!(
lsp_kind,
lsp_symbol_kind::VARIABLE,
"type_alias must map to SymbolKind::Variable (13) not TypeParameter (26); got {lsp_kind}"
);
alias_kind_found = true;
}
}
assert!(
alias_kind_found,
"Go query must emit def_kind='type_alias' for 'type Foo = Bar' pattern"
);
}
#[test]
fn test_go_type_alias_distinct_from_type_parameter() {
use streaming_iterator::StreamingIterator as _;
let source = r"package main
type Foo = Bar
func generic[T any](x T) {
}
";
let cfg = config_for_extension("go").expect("Go config must compile");
let mut parser = tree_sitter::Parser::new();
parser.set_language(&cfg.language).expect("set language");
let tree = parser.parse(source, None).expect("parse");
let mut cursor = tree_sitter::QueryCursor::new();
let mut matches = cursor.matches(&cfg.query, tree.root_node(), source.as_bytes());
let mut alias_found = false;
let mut alias_kind = 0u32;
while let Some(m) = matches.next() {
let mut name = "";
let mut def_kind = "";
for cap in m.captures {
let cap_name = &cfg.query.capture_names()[cap.index as usize];
let text = &source[cap.node.start_byte()..cap.node.end_byte()];
if *cap_name == "name" {
name = text;
} else if *cap_name == "def" {
def_kind = cap.node.kind();
}
}
if def_kind == "type_alias" && name == "Foo" {
alias_kind = lsp_symbol_kind_for_node_kind(def_kind);
alias_found = true;
}
}
assert!(
alias_found,
"Go query must emit 'type Foo = Bar' as type_alias; got none"
);
assert_eq!(
alias_kind,
lsp_symbol_kind::VARIABLE,
"type_alias 'Foo' must be kind=13 (Variable), got {alias_kind}"
);
}
}