use std::path::Path;
use serde::Serialize;
use super::{Language, child_text_by_kind, child_text_by_kinds, parse_source};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SymbolKind {
Function,
Struct,
Enum,
Trait,
Impl,
Class,
Method,
Const,
Type,
Interface,
Module,
}
impl std::fmt::Display for SymbolKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Function => "fn",
Self::Struct => "struct",
Self::Enum => "enum",
Self::Trait => "trait",
Self::Impl => "impl",
Self::Class => "class",
Self::Method => "method",
Self::Const => "const",
Self::Type => "type",
Self::Interface => "interface",
Self::Module => "mod",
};
f.write_str(s)
}
}
impl SymbolKind {
pub fn from_str_loose(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"fn" | "func" | "function" => Some(Self::Function),
"struct" => Some(Self::Struct),
"enum" => Some(Self::Enum),
"trait" => Some(Self::Trait),
"impl" => Some(Self::Impl),
"class" => Some(Self::Class),
"method" => Some(Self::Method),
"const" | "constant" => Some(Self::Const),
"type" => Some(Self::Type),
"interface" => Some(Self::Interface),
"mod" | "module" => Some(Self::Module),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct SymbolDef {
pub name: String,
pub kind: SymbolKind,
pub start_line: usize,
pub end_line: usize,
pub signature: String,
pub children: Vec<SymbolDef>,
pub depth: usize,
}
pub fn extract_symbols(source: &str, lang: Language) -> Vec<SymbolDef> {
let Some((tree, _)) = parse_source(source, lang) else {
return Vec::new();
};
let mut symbols = Vec::new();
let mut cursor = tree.walk();
visit_node(&mut cursor, source, lang, 0, &mut symbols);
if lang == Language::Go {
group_go_receiver_methods(&mut symbols);
}
symbols
}
pub fn extract_symbols_from_file(path: &Path, lang_hint: Option<Language>) -> Vec<SymbolDef> {
let lang = lang_hint.unwrap_or_else(|| Language::from_path(path));
if !lang.has_grammar() {
return Vec::new();
}
let Ok(source) = std::fs::read_to_string(path) else {
return Vec::new();
};
extract_symbols(&source, lang)
}
pub fn find_symbol<'a>(symbols: &'a [SymbolDef], name: &str) -> Option<&'a SymbolDef> {
if let Some((parent, rest)) = name.split_once("::") {
for sym in symbols {
if sym.name == parent
&& let Some(found) = find_symbol(&sym.children, rest)
{
return Some(found);
}
}
None
} else {
for sym in symbols {
if sym.name == name {
return Some(sym);
}
}
for sym in symbols {
if let Some(found) = find_symbol(&sym.children, name) {
return Some(found);
}
}
None
}
}
#[derive(Debug, Clone)]
pub struct FunctionSpan {
pub full_range: std::ops::Range<usize>,
pub signature_range: std::ops::Range<usize>,
pub signature_text: String,
pub name: String,
pub start_line: usize,
pub signature_end_line: usize,
}
pub fn find_function_span(
source: &str,
function_name: &str,
lang: Language,
) -> Option<FunctionSpan> {
let (tree, _) = parse_source(source, lang)?;
let root = tree.root_node();
let fn_node = find_function_node(root, source, function_name)?;
let start = fn_node.start_byte();
let end = fn_node.end_byte();
let sig_end = find_body_start(fn_node).unwrap_or(end);
let signature_text = source[start..sig_end].trim_end().to_string();
let start_line = fn_node.start_position().row + 1;
let sig_end_line = source[..sig_end].matches('\n').count() + 1;
Some(FunctionSpan {
full_range: start..end,
signature_range: start..sig_end,
signature_text,
name: function_name.to_string(),
start_line,
signature_end_line: sig_end_line,
})
}
const FUNCTION_NODE_KINDS: &[&str] = &[
"function_item", "function_definition", "function_declaration", "method_declaration", "method_definition", "constructor_declaration", ];
const BODY_NODE_KINDS: &[&str] = &[
"block", "statement_block", "compound_statement", ];
fn find_function_node<'a>(
node: tree_sitter_lib::Node<'a>,
source: &str,
name: &str,
) -> Option<tree_sitter_lib::Node<'a>> {
if FUNCTION_NODE_KINDS.contains(&node.kind()) && function_node_has_name(node, source, name) {
return Some(node);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if let Some(found) = find_function_node(child, source, name) {
return Some(found);
}
}
None
}
fn function_node_has_name(node: tree_sitter_lib::Node, source: &str, name: &str) -> bool {
let name_kinds = &[
"identifier",
"name",
"property_identifier",
"field_identifier",
"word",
];
if child_text_by_kinds(node, name_kinds, source) == Some(name) {
return true;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind().contains("declarator") {
if check_declarator_for_name(child, name_kinds, source, name) {
return true;
}
let mut inner = child.walk();
for grandchild in child.children(&mut inner) {
if grandchild.kind().contains("declarator") || grandchild.kind() == "identifier" {
if grandchild.kind() == "identifier" {
if grandchild.utf8_text(source.as_bytes()).ok() == Some(name) {
return true;
}
} else if check_declarator_for_name(grandchild, name_kinds, source, name) {
return true;
}
}
}
}
}
false
}
fn check_declarator_for_name(
node: tree_sitter_lib::Node,
name_kinds: &[&str],
source: &str,
name: &str,
) -> bool {
if child_text_by_kinds(node, name_kinds, source) == Some(name) {
return true;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if (child.kind() == "qualified_identifier" || child.kind() == "scoped_identifier")
&& qualified_identifier_has_name(child, name_kinds, source, name)
{
return true;
}
}
false
}
fn qualified_identifier_has_name(
node: tree_sitter_lib::Node,
name_kinds: &[&str],
source: &str,
name: &str,
) -> bool {
innermost_qualified_name(node, name_kinds, source) == Some(name)
}
fn find_body_start(fn_node: tree_sitter_lib::Node) -> Option<usize> {
let mut cursor = fn_node.walk();
for child in fn_node.children(&mut cursor) {
if BODY_NODE_KINDS.contains(&child.kind()) {
return Some(child.start_byte());
}
}
None
}
pub fn replace_function_signature(source: &str, old_name: &str, new_sig: &str) -> Option<String> {
let (tree, _) = parse_source(source, Language::Rust)?;
let root = tree.root_node();
fn find_fn<'a>(
node: tree_sitter_lib::Node<'a>,
source: &str,
old_name: &str,
) -> Option<tree_sitter_lib::Node<'a>> {
if node.kind() == "function_item"
&& let Some(id) = child_text_by_kind(node, "identifier", source)
&& id == old_name
{
return Some(node);
}
let mut c = node.walk();
for child in node.children(&mut c) {
if let Some(found) = find_fn(child, source, old_name) {
return Some(found);
}
}
None
}
let fn_node = find_fn(root, source, old_name)?;
let sig_end = find_body_start(fn_node).unwrap_or(fn_node.end_byte());
let start = fn_node.start_byte();
let before = &source[..start];
let after = &source[sig_end..];
Some(format!("{}{}{}", before, new_sig, after))
}
#[derive(Debug, Clone, Default)]
pub struct FunctionSigEdit {
pub visibility: Option<String>,
pub parameters: Option<String>,
pub return_type: Option<String>,
}
pub fn rewrite_function_signature(
source: &str,
old_name: &str,
edit: &FunctionSigEdit,
lang: Language,
) -> Option<String> {
if lang == Language::Rust {
return rewrite_rust_sig(source, old_name, edit);
}
rewrite_sig_generic(source, old_name, edit, lang)
}
fn rewrite_rust_sig(source: &str, old_name: &str, edit: &FunctionSigEdit) -> Option<String> {
let (tree, _) = parse_source(source, Language::Rust)?;
let root = tree.root_node();
let fn_node = find_fn_for_rewrite(root, source, old_name)?;
let vis = edit.visibility.as_deref().unwrap_or_else(|| {
child_text_by_kind(fn_node, "visibility_modifier", source).unwrap_or("")
});
let params = edit
.parameters
.as_deref()
.unwrap_or_else(|| child_text_by_kind(fn_node, "parameters", source).unwrap_or("()"));
let ret = edit
.return_type
.as_deref()
.unwrap_or_else(|| extract_return_type(fn_node, source).unwrap_or(""));
let qualifiers = extract_fn_qualifiers(fn_node, source);
let vis_part = if vis.is_empty() {
String::new()
} else {
format!("{} ", vis)
};
let qual_part = if qualifiers.is_empty() {
String::new()
} else {
format!("{} ", qualifiers)
};
let ret_part = if ret.is_empty() {
String::new()
} else {
format!(" {}", ret)
};
let new_sig = format!(
"{}{}fn {}{}{}",
vis_part, qual_part, old_name, params, ret_part
);
let sig_end = find_body_start(fn_node).unwrap_or(fn_node.end_byte());
let start = fn_node.start_byte();
let before = &source[..start];
let after = &source[sig_end..];
Some(format!("{}{}{}", before, new_sig, after))
}
const PARAM_NODE_KINDS: &[&str] = &[
"parameters", "formal_parameters", "parameter_list", ];
fn rewrite_sig_generic(
source: &str,
old_name: &str,
edit: &FunctionSigEdit,
lang: Language,
) -> Option<String> {
let (tree, _) = parse_source(source, lang)?;
let root = tree.root_node();
let fn_node = find_function_node(root, source, old_name)?;
let sig_end = find_body_start(fn_node).unwrap_or(fn_node.end_byte());
let sig_start = fn_node.start_byte();
let mut edits: Vec<(std::ops::Range<usize>, String)> = Vec::new();
if let Some(new_vis) = &edit.visibility {
if let Some(node) = find_modifier_node(fn_node, lang) {
let end = node.end_byte();
let ws_end = if source.as_bytes().get(end) == Some(&b' ') {
end + 1
} else {
end
};
if new_vis.is_empty() {
edits.push((node.start_byte()..ws_end, String::new()));
} else {
edits.push((node.start_byte()..end, new_vis.clone()));
}
} else if !new_vis.is_empty() {
edits.push((sig_start..sig_start, format!("{} ", new_vis)));
}
}
if let Some(new_params) = &edit.parameters {
let params_node = if fn_node.kind() == "method_declaration" {
fn_node
.child_by_field_name("parameters")
.or_else(|| find_nth_child_of_kinds(fn_node, PARAM_NODE_KINDS, 1))
} else {
find_first_child_of_kinds(fn_node, PARAM_NODE_KINDS)
};
if let Some(node) = params_node {
edits.push((node.start_byte()..node.end_byte(), new_params.clone()));
}
}
if let Some(new_ret) = &edit.return_type {
if let Some(range) = find_return_type_range(fn_node, lang, sig_end) {
if new_ret.is_empty() {
let trimmed_start = source[..range.start].trim_end().len();
edits.push((trimmed_start..range.end, String::new()));
} else {
edits.push((range, new_ret.clone()));
}
} else if !new_ret.is_empty() {
let insert_pos = if fn_node.kind() == "method_declaration" {
fn_node
.child_by_field_name("parameters")
.or_else(|| find_nth_child_of_kinds(fn_node, PARAM_NODE_KINDS, 1))
} else {
find_first_child_of_kinds(fn_node, PARAM_NODE_KINDS)
}
.map(|n| n.end_byte())
.unwrap_or(sig_end);
edits.push((insert_pos..insert_pos, format!(" {}", new_ret)));
}
}
edits.sort_by_key(|e| std::cmp::Reverse(e.0.start));
let mut result = source.to_string();
for (range, text) in edits {
result.replace_range(range, &text);
}
Some(result)
}
fn find_first_child_of_kinds<'a>(
node: tree_sitter_lib::Node<'a>,
kinds: &[&str],
) -> Option<tree_sitter_lib::Node<'a>> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if kinds.contains(&child.kind()) {
return Some(child);
}
if child.kind().contains("declarator") {
let mut inner = child.walk();
for grandchild in child.children(&mut inner) {
if kinds.contains(&grandchild.kind()) {
return Some(grandchild);
}
}
}
}
None
}
fn find_nth_child_of_kinds<'a>(
node: tree_sitter_lib::Node<'a>,
kinds: &[&str],
n: usize,
) -> Option<tree_sitter_lib::Node<'a>> {
let mut cursor = node.walk();
let mut count = 0;
for child in node.children(&mut cursor) {
if kinds.contains(&child.kind()) {
if count == n {
return Some(child);
}
count += 1;
}
}
None
}
fn find_modifier_node(
fn_node: tree_sitter_lib::Node,
lang: Language,
) -> Option<tree_sitter_lib::Node> {
match lang {
Language::Java => find_first_child_of_kinds(fn_node, &["modifiers"]),
_ => find_first_child_of_kinds(fn_node, &["visibility_modifier", "visibility"]),
}
}
fn find_return_type_range(
fn_node: tree_sitter_lib::Node,
lang: Language,
sig_end: usize,
) -> Option<std::ops::Range<usize>> {
match lang {
Language::Python => {
let mut cursor = fn_node.walk();
let mut arrow_start = None;
for child in fn_node.children(&mut cursor) {
if child.kind() == "->" {
arrow_start = Some(child.start_byte());
continue;
}
if let Some(a) = arrow_start
&& child.kind() == "type"
{
return Some(a..child.end_byte());
}
}
None
}
Language::Go => {
fn_node
.child_by_field_name("result")
.map(|n| n.start_byte()..n.end_byte())
}
Language::TypeScript | Language::JavaScript => {
find_first_child_of_kinds(fn_node, &["return_type", "type_annotation"])
.map(|n| n.start_byte()..n.end_byte())
}
Language::Java => {
let name_start = fn_node
.child_by_field_name("name")
.map(|n| n.start_byte())
.unwrap_or(sig_end);
let type_kinds = [
"void_type",
"type_identifier",
"generic_type",
"integral_type",
"boolean_type",
"floating_point_type",
"array_type",
"scoped_type_identifier",
];
let mut cursor = fn_node.walk();
for child in fn_node.children(&mut cursor) {
if child.start_byte() < name_start && type_kinds.contains(&child.kind()) {
return Some(child.start_byte()..child.end_byte());
}
}
None
}
Language::C | Language::Cpp => {
let decl_start =
find_first_child_of_kinds(fn_node, &["declarator", "function_declarator"])
.map(|n| n.start_byte())
.unwrap_or(sig_end);
let type_kinds = [
"primitive_type",
"type_identifier",
"sized_type_specifier",
"struct_specifier",
"enum_specifier",
"union_specifier",
"template_type",
];
let mut cursor = fn_node.walk();
for child in fn_node.children(&mut cursor) {
if child.start_byte() < decl_start && type_kinds.contains(&child.kind()) {
return Some(child.start_byte()..child.end_byte());
}
}
None
}
_ => None,
}
}
fn extract_return_type<'a>(fn_node: tree_sitter_lib::Node, source: &'a str) -> Option<&'a str> {
let mut cursor = fn_node.walk();
let mut arrow_start = None;
for child in fn_node.children(&mut cursor) {
if child.kind() == "->" {
arrow_start = Some(child.start_byte());
continue;
}
if let Some(a) = arrow_start {
return Some(source[a..child.end_byte()].trim_end());
}
}
None
}
fn extract_fn_qualifiers(fn_node: tree_sitter_lib::Node, source: &str) -> String {
let node_text = &source[fn_node.start_byte()..fn_node.end_byte()];
let fn_pos = match node_text.find("fn ") {
Some(pos) => pos,
None => return String::new(),
};
let prefix = &node_text[..fn_pos];
let vis = child_text_by_kind(fn_node, "visibility_modifier", source)
.or_else(|| child_text_by_kind(fn_node, "visibility", source));
let quals = if let Some(v) = vis {
let after_vis = prefix.strip_prefix(v).unwrap_or(prefix);
after_vis.trim()
} else {
prefix.trim()
};
quals.to_string()
}
fn find_fn_for_rewrite<'a>(
node: tree_sitter_lib::Node<'a>,
source: &str,
old_name: &str,
) -> Option<tree_sitter_lib::Node<'a>> {
if node.kind() == "function_item"
&& let Some(id) = child_text_by_kind(node, "identifier", source)
&& id == old_name
{
return Some(node);
}
let mut c = node.walk();
for child in node.children(&mut c) {
if let Some(found) = find_fn_for_rewrite(child, source, old_name) {
return Some(found);
}
}
None
}
fn visit_node(
cursor: &mut tree_sitter_lib::TreeCursor,
source: &str,
language: Language,
depth: usize,
symbols: &mut Vec<SymbolDef>,
) {
let node = cursor.node();
if let Some(mut sym) = try_extract_symbol(node, source, language, depth) {
if cursor.goto_first_child() {
loop {
visit_node(cursor, source, language, depth + 1, &mut sym.children);
if !cursor.goto_next_sibling() {
break;
}
}
cursor.goto_parent();
}
symbols.push(sym);
} else if cursor.goto_first_child() {
loop {
visit_node(cursor, source, language, depth, symbols);
if !cursor.goto_next_sibling() {
break;
}
}
cursor.goto_parent();
}
}
fn try_extract_symbol(
node: tree_sitter_lib::Node,
source: &str,
language: Language,
depth: usize,
) -> Option<SymbolDef> {
let (kind, name) = match language {
Language::Rust => extract_rust(node, source)?,
Language::Python => extract_python(node, source)?,
Language::TypeScript | Language::JavaScript => extract_ts_js(node, source, language)?,
Language::Go => extract_go(node, source)?,
Language::Hcl => extract_hcl(node, source)?,
Language::Protobuf => extract_proto(node, source)?,
Language::Shell => extract_bash(node, source)?,
Language::Ruby => extract_ruby(node, source)?,
_ => extract_generic(node, source)?,
};
let start_line = node.start_position().row + 1;
let end_line = node.end_position().row + 1;
let signature = node_signature(node, source);
Some(SymbolDef {
name,
kind,
start_line,
end_line,
signature,
children: Vec::new(),
depth,
})
}
fn node_signature(node: tree_sitter_lib::Node, source: &str) -> String {
let start = node.start_byte();
let mut end = node.end_byte().min(start + 200);
while end > start && !source.is_char_boundary(end) {
end -= 1;
}
let raw = &source[start..end];
let mut depth: i32 = 0;
let mut body_brace = None;
for (i, ch) in raw.char_indices() {
match ch {
'(' | '[' => depth += 1,
')' | ']' => depth = (depth - 1).max(0),
'{' if depth == 0 => {
body_brace = Some(i);
break;
}
_ => {}
}
}
let sig = match body_brace {
Some(brace) => raw[..brace].trim(),
None => raw.lines().next().unwrap_or(raw).trim(),
};
sig.to_string()
}
fn extract_rust(node: tree_sitter_lib::Node, source: &str) -> Option<(SymbolKind, String)> {
match node.kind() {
"function_item" => {
let name = child_text_by_kind(node, "identifier", source)?;
Some((SymbolKind::Function, name.to_string()))
}
"struct_item" => {
let name = child_text_by_kind(node, "type_identifier", source)?;
Some((SymbolKind::Struct, name.to_string()))
}
"enum_item" => {
let name = child_text_by_kind(node, "type_identifier", source)?;
Some((SymbolKind::Enum, name.to_string()))
}
"trait_item" => {
let name = child_text_by_kind(node, "type_identifier", source)?;
Some((SymbolKind::Trait, name.to_string()))
}
"impl_item" => {
let type_node = node.child_by_field_name("type")?;
let name = type_node.utf8_text(source.as_bytes()).ok()?;
Some((SymbolKind::Impl, name.to_string()))
}
"const_item" => {
let name = child_text_by_kind(node, "identifier", source)?;
Some((SymbolKind::Const, name.to_string()))
}
"type_item" => {
let name = child_text_by_kind(node, "type_identifier", source)?;
Some((SymbolKind::Type, name.to_string()))
}
"mod_item" => {
let name = child_text_by_kind(node, "identifier", source)?;
Some((SymbolKind::Module, name.to_string()))
}
_ => None,
}
}
fn extract_python(node: tree_sitter_lib::Node, source: &str) -> Option<(SymbolKind, String)> {
match node.kind() {
"function_definition" => {
let name = child_text_by_kind(node, "identifier", source)?;
let kind = {
let mut ancestor = node.parent();
let mut is_method = false;
while let Some(a) = ancestor {
if a.kind() == "class_definition" {
is_method = true;
break;
}
ancestor = a.parent();
}
if is_method {
SymbolKind::Method
} else {
SymbolKind::Function
}
};
Some((kind, name.to_string()))
}
"class_definition" => {
let name = child_text_by_kind(node, "identifier", source)?;
Some((SymbolKind::Class, name.to_string()))
}
_ => None,
}
}
fn extract_ts_js(
node: tree_sitter_lib::Node,
source: &str,
language: Language,
) -> Option<(SymbolKind, String)> {
match node.kind() {
"function_declaration" => {
let name = child_text_by_kind(node, "identifier", source)?;
Some((SymbolKind::Function, name.to_string()))
}
"class_declaration" => {
let name = child_text_by_kinds(node, &["type_identifier", "identifier"], source)?;
Some((SymbolKind::Class, name.to_string()))
}
"method_definition" => {
let name = child_text_by_kind(node, "property_identifier", source)?;
Some((SymbolKind::Method, name.to_string()))
}
"interface_declaration" if language == Language::TypeScript => {
let name = child_text_by_kinds(node, &["type_identifier", "identifier"], source)?;
Some((SymbolKind::Interface, name.to_string()))
}
"enum_declaration" if language == Language::TypeScript => {
let name = child_text_by_kinds(node, &["type_identifier", "identifier"], source)?;
Some((SymbolKind::Enum, name.to_string()))
}
"variable_declarator" => {
if let Some(parent) = node.parent()
&& parent.kind() == "lexical_declaration"
&& let Some(first) = parent.child(0)
&& first.utf8_text(source.as_bytes()).ok() == Some("const")
{
let name = child_text_by_kind(node, "identifier", source)?;
Some((SymbolKind::Const, name.to_string()))
} else {
None
}
}
_ => None,
}
}
fn extract_go(node: tree_sitter_lib::Node, source: &str) -> Option<(SymbolKind, String)> {
match node.kind() {
"function_declaration" => {
let name = child_text_by_kind(node, "identifier", source)?;
Some((SymbolKind::Function, name.to_string()))
}
"method_declaration" => {
let name = child_text_by_kinds(node, &["field_identifier", "identifier"], source)?;
Some((SymbolKind::Method, name.to_string()))
}
"type_spec" => {
let name = child_text_by_kinds(node, &["type_identifier", "identifier"], source)?;
let mut inner = node.walk();
for grandchild in node.children(&mut inner) {
match grandchild.kind() {
"struct_type" => {
return Some((SymbolKind::Struct, name.to_string()));
}
"interface_type" => {
return Some((SymbolKind::Interface, name.to_string()));
}
_ => {}
}
}
Some((SymbolKind::Type, name.to_string()))
}
"type_alias" => {
let name = child_text_by_kinds(node, &["type_identifier", "identifier"], source)?;
Some((SymbolKind::Type, name.to_string()))
}
_ => None,
}
}
fn group_go_receiver_methods(symbols: &mut Vec<SymbolDef>) {
let mut to_move: Vec<(usize, String)> = Vec::new();
for (i, sym) in symbols.iter().enumerate() {
if sym.kind == SymbolKind::Method
&& let Some(receiver_type) = parse_go_receiver_type(&sym.signature)
{
to_move.push((i, receiver_type));
}
}
for (idx, receiver_type) in to_move.into_iter().rev() {
if let Some(parent_idx) = symbols.iter().position(|s| {
matches!(
s.kind,
SymbolKind::Struct | SymbolKind::Type | SymbolKind::Interface
) && s.name == receiver_type
}) {
let mut method = symbols.remove(idx);
let adj = if idx < parent_idx {
parent_idx - 1
} else {
parent_idx
};
method.depth = symbols[adj].depth + 1;
symbols[adj].children.push(method);
}
}
}
fn parse_go_receiver_type(signature: &str) -> Option<String> {
let after_func = signature.strip_prefix("func ")?.trim_start();
let receiver_part = after_func.strip_prefix('(')?;
let end_paren = receiver_part.find(')')?;
let receiver = receiver_part[..end_paren].trim();
if receiver.is_empty() {
return None;
}
let type_part = receiver.split_whitespace().next_back()?;
let type_name = type_part.strip_prefix('*').unwrap_or(type_part);
let type_name = type_name.split('[').next().unwrap_or(type_name);
if type_name.is_empty() {
return None;
}
Some(type_name.to_string())
}
fn extract_hcl(node: tree_sitter_lib::Node, source: &str) -> Option<(SymbolKind, String)> {
if node.kind() != "block" {
return None;
}
let block_type = child_text_by_kind(node, "identifier", source)?;
let mut labels = Vec::new();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "string_lit"
&& let Ok(text) = child.utf8_text(source.as_bytes())
{
labels.push(text.trim_matches('"').to_string());
}
}
let (kind, name) = match block_type {
"resource" | "data" => {
let name = if labels.len() >= 2 {
format!("{}.{}", labels[0], labels[1])
} else if !labels.is_empty() {
labels[0].clone()
} else {
return None;
};
(SymbolKind::Struct, name)
}
"variable" | "output" => {
let name = labels.first()?.clone();
(SymbolKind::Const, name)
}
"module" | "provider" => {
let name = labels
.first()
.cloned()
.unwrap_or_else(|| block_type.to_string());
(SymbolKind::Module, name)
}
"locals" | "terraform" => (SymbolKind::Module, block_type.to_string()),
_ => return None,
};
Some((kind, name))
}
fn extract_proto(node: tree_sitter_lib::Node, source: &str) -> Option<(SymbolKind, String)> {
match node.kind() {
"message" => {
let name = child_text_by_kinds(node, &["message_name", "identifier"], source)?;
Some((SymbolKind::Struct, name.to_string()))
}
"enum" => {
let name = child_text_by_kinds(node, &["enum_name", "identifier"], source)?;
Some((SymbolKind::Enum, name.to_string()))
}
"service" => {
let name = child_text_by_kinds(node, &["service_name", "identifier"], source)?;
Some((SymbolKind::Interface, name.to_string()))
}
"rpc" => {
let name = child_text_by_kinds(node, &["rpc_name", "identifier"], source)?;
Some((SymbolKind::Method, name.to_string()))
}
_ => None,
}
}
fn extract_bash(node: tree_sitter_lib::Node, source: &str) -> Option<(SymbolKind, String)> {
if node.kind() != "function_definition" {
return None;
}
let name = child_text_by_kinds(node, &["word", "name", "identifier"], source)?;
Some((SymbolKind::Function, name.to_string()))
}
fn extract_ruby(node: tree_sitter_lib::Node, source: &str) -> Option<(SymbolKind, String)> {
match node.kind() {
"class" => {
let name = child_text_by_kinds(node, &["constant", "scope_resolution"], source)?;
Some((SymbolKind::Class, name.to_string()))
}
"module" => {
let name = child_text_by_kinds(node, &["constant", "scope_resolution"], source)?;
Some((SymbolKind::Module, name.to_string()))
}
"method" | "singleton_method" => {
let name = child_text_by_kinds(node, &["identifier", "setter", "operator"], source)?;
Some((SymbolKind::Method, name.to_string()))
}
_ => None,
}
}
const GENERIC_NAME_KINDS: &[&str] = &[
"identifier",
"name",
"type_identifier",
"property_identifier",
"field_identifier",
"simple_identifier",
];
const GENERIC_FN_NAME_KINDS: &[&str] = &[
"identifier",
"name",
"property_identifier",
"field_identifier",
"simple_identifier",
];
fn extract_generic(node: tree_sitter_lib::Node, source: &str) -> Option<(SymbolKind, String)> {
let kind = node.kind();
let is_function_kind = matches!(
kind,
"function_item"
| "function_definition"
| "function_declaration"
| "method_definition"
| "method_declaration"
);
let symbol_kind = match kind {
"function_item" | "function_definition" | "function_declaration" => SymbolKind::Function,
"method_definition" | "method_declaration" => SymbolKind::Method,
"class_definition" | "class_declaration" | "class_specifier" => SymbolKind::Class,
"interface_declaration" => SymbolKind::Interface,
"struct_item" | "struct_declaration" | "struct_specifier" => SymbolKind::Struct,
"enum_item" | "enum_declaration" | "enum_specifier" => SymbolKind::Enum,
"type_declaration" | "type_item" | "type_alias_declaration" => SymbolKind::Type,
"module_declaration" | "mod_item" | "namespace_declaration" | "namespace_definition" => {
SymbolKind::Module
}
"trait_item" | "trait_declaration" | "protocol_declaration" => SymbolKind::Trait,
_ => return None,
};
if is_function_kind {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if (child.kind().contains("declarator") || child.kind() == "name")
&& let Some(name) = find_name_in_declarator_tree(child, GENERIC_NAME_KINDS, source)
{
return Some((symbol_kind, name.to_string()));
}
}
if let Some(name) = child_text_by_kinds(node, GENERIC_FN_NAME_KINDS, source) {
return Some((symbol_kind, name.to_string()));
}
} else {
if let Some(name) = child_text_by_kinds(node, GENERIC_NAME_KINDS, source) {
return Some((symbol_kind, name.to_string()));
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if (child.kind().contains("declarator") || child.kind() == "name")
&& let Some(name) = find_name_in_declarator_tree(child, GENERIC_NAME_KINDS, source)
{
return Some((symbol_kind, name.to_string()));
}
}
}
None
}
fn find_name_in_declarator_tree<'a>(
node: tree_sitter_lib::Node<'a>,
name_kinds: &[&str],
source: &'a str,
) -> Option<&'a str> {
if let Some(name) = child_text_by_kinds(node, name_kinds, source) {
return Some(name);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if (child.kind() == "qualified_identifier" || child.kind() == "scoped_identifier")
&& let Some(name) = innermost_qualified_name(child, name_kinds, source)
{
return Some(name);
}
if child.kind().contains("declarator")
&& let Some(name) = find_name_in_declarator_tree(child, name_kinds, source)
{
return Some(name);
}
}
None
}
fn innermost_qualified_name<'a>(
node: tree_sitter_lib::Node<'a>,
name_kinds: &[&str],
source: &'a str,
) -> Option<&'a str> {
if let Some(name) = child_text_by_kinds(node, name_kinds, source) {
return Some(name);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if (child.kind() == "qualified_identifier" || child.kind() == "scoped_identifier")
&& let Some(name) = innermost_qualified_name(child, name_kinds, source)
{
return Some(name);
}
}
None
}
pub fn full_symbol_span(source: &str, sym: &SymbolDef, lang: Language) -> (usize, usize) {
let lines: Vec<&str> = source.lines().collect();
let sym_start_0 = sym.start_line.saturating_sub(1); if sym_start_0 == 0 {
return (sym.start_line, sym.end_line);
}
let mut first_line_0 = sym_start_0;
let mut i = sym_start_0;
while i > 0 {
i -= 1;
let trimmed = lines[i].trim();
if trimmed.is_empty() {
if i > 0 && is_annotation_line(lines[i - 1].trim(), lang) {
first_line_0 = i;
continue;
}
break;
}
if is_annotation_line(trimmed, lang) {
first_line_0 = i;
} else if lang == Language::Rust {
if let Some(attr_start) = find_rust_multiline_attr_start(&lines, i) {
first_line_0 = attr_start;
i = attr_start;
continue;
}
break;
} else if lang == Language::Python
|| lang == Language::TypeScript
|| lang == Language::JavaScript
|| lang == Language::Java
|| lang == Language::Kotlin
{
if let Some(dec_start) = find_multiline_decorator_start(&lines, i) {
first_line_0 = dec_start;
i = dec_start;
continue;
}
break;
} else {
break;
}
}
(first_line_0 + 1, sym.end_line) }
pub fn check_no_overlapping_spans(spans: &[(usize, usize)], names: &[&str]) -> anyhow::Result<()> {
if spans.len() < 2 {
return Ok(());
}
let mut indexed: Vec<(usize, usize, usize)> = spans
.iter()
.enumerate()
.map(|(i, &(s, e))| (s, e, i))
.collect();
indexed.sort_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)));
let mut overlaps: Vec<String> = Vec::new();
for window in indexed.windows(2) {
let (s1, e1, i1) = window[0];
let (s2, e2, i2) = window[1];
if s2 < e1 {
let n1 = names.get(i1).copied().unwrap_or("?");
let n2 = names.get(i2).copied().unwrap_or("?");
overlaps.push(format!(
"'{}' (lines {}-{}) and '{}' (lines {}-{})",
n1,
s1 + 1,
e1,
n2,
s2 + 1,
e2
));
}
}
if overlaps.is_empty() {
Ok(())
} else {
anyhow::bail!(
"overlapping symbol spans would corrupt content: {}",
overlaps.join("; ")
)
}
}
fn is_annotation_line(trimmed: &str, lang: Language) -> bool {
match lang {
Language::Rust => {
trimmed.starts_with("#[")
|| trimmed.starts_with("///")
|| trimmed.starts_with("/**")
|| trimmed.starts_with("* ")
|| trimmed == "*/"
|| trimmed == "*"
}
Language::Python => trimmed.starts_with('@'),
Language::TypeScript | Language::JavaScript => {
trimmed.starts_with('@')
|| trimmed.starts_with("/**")
|| trimmed.starts_with("* ")
|| trimmed == "*/"
|| trimmed == "*"
}
Language::Java | Language::Kotlin => {
trimmed.starts_with('@')
|| trimmed.starts_with("/**")
|| trimmed.starts_with("* ")
|| trimmed == "*/"
|| trimmed == "*"
}
Language::Go => {
trimmed.starts_with("//")
}
_ => false,
}
}
fn find_rust_multiline_attr_start(lines: &[&str], from_0: usize) -> Option<usize> {
let mut depth: i32 = 0;
for j in (0..=from_0).rev() {
let trimmed = lines[j].trim();
for ch in trimmed.chars() {
if ch == ']' {
depth += 1;
} else if ch == '[' {
depth -= 1;
}
}
if trimmed.starts_with("#[") && depth <= 0 {
return Some(j);
}
if from_0 - j > 20 {
break;
}
}
None
}
fn find_multiline_decorator_start(lines: &[&str], from_0: usize) -> Option<usize> {
let mut depth: i32 = 0;
for j in (0..=from_0).rev() {
let trimmed = lines[j].trim();
for ch in trimmed.chars() {
if ch == ')' {
depth += 1;
} else if ch == '(' {
depth -= 1;
}
}
if trimmed.starts_with('@') && depth <= 0 {
return Some(j);
}
if from_0 - j > 20 {
break;
}
}
None
}
pub(crate) fn compute_line_byte_offsets(source: &str) -> Vec<usize> {
let mut offsets = vec![0usize];
let bytes = source.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\n' {
offsets.push(i + 1);
i += 1;
} else if bytes[i] == b'\r' && i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
offsets.push(i + 2);
i += 2;
} else if bytes[i] == b'\r' {
offsets.push(i + 1);
i += 1;
} else {
i += 1;
}
}
offsets
}
pub fn extract_symbol_text<'a>(source: &'a str, sym: &SymbolDef, lang: Language) -> &'a str {
let (full_start, full_end) = full_symbol_span(source, sym, lang);
let lines: Vec<&str> = source.lines().collect();
let start_0 = full_start.saturating_sub(1);
let end_0 = full_end.min(lines.len());
let line_offsets = compute_line_byte_offsets(source);
let byte_start = if start_0 < line_offsets.len() {
line_offsets[start_0]
} else {
source.len()
};
let byte_end = if end_0 < line_offsets.len() {
line_offsets[end_0]
} else {
source.len()
};
&source[byte_start..byte_end]
}
pub fn parse_kind_filter(kind_arg: &Option<String>) -> Vec<SymbolKind> {
match kind_arg {
Some(s) => s
.split(',')
.filter_map(|k| SymbolKind::from_str_loose(k.trim()))
.collect(),
None => Vec::new(),
}
}
pub fn filter_symbols<'a>(
symbols: &'a [SymbolDef],
kind_filter: &[SymbolKind],
) -> Vec<&'a SymbolDef> {
if kind_filter.is_empty() {
return symbols.iter().collect();
}
symbols
.iter()
.filter(|s| kind_filter.contains(&s.kind))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_no_overlapping_spans_ok() {
let spans = vec![(0, 3), (4, 7), (8, 10)];
let names = vec!["a", "b", "c"];
check_no_overlapping_spans(&spans, &names).unwrap();
}
#[test]
fn check_no_overlapping_spans_adjacent_ok() {
let spans = vec![(0, 3), (3, 6), (6, 9)];
let names = vec!["a", "b", "c"];
check_no_overlapping_spans(&spans, &names).unwrap();
}
#[test]
fn check_no_overlapping_spans_detects_overlap() {
let spans = vec![(0, 5), (3, 8)];
let names = vec!["foo", "bar"];
let err = check_no_overlapping_spans(&spans, &names).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("overlapping"), "error: {msg}");
assert!(
msg.contains("foo"),
"error should mention first symbol: {msg}"
);
assert!(
msg.contains("bar"),
"error should mention second symbol: {msg}"
);
}
#[test]
fn check_no_overlapping_spans_detects_containment() {
let spans = vec![(0, 10), (2, 5)];
let names = vec!["outer", "inner"];
let err = check_no_overlapping_spans(&spans, &names).unwrap_err();
assert!(err.to_string().contains("overlapping"));
}
#[test]
fn check_no_overlapping_spans_single_ok() {
let spans = vec![(0, 5)];
let names = vec!["only"];
check_no_overlapping_spans(&spans, &names).unwrap();
}
#[test]
fn check_no_overlapping_spans_empty_ok() {
check_no_overlapping_spans(&[], &[]).unwrap();
}
#[test]
fn extract_rust_symbols() {
let source = r#"
struct Foo {
x: i32,
}
fn bar() -> i32 {
42
}
impl Foo {
fn baz(&self) -> i32 {
self.x
}
}
"#;
let symbols = extract_symbols(source, Language::Rust);
let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"Foo"));
assert!(names.contains(&"bar"));
let impl_sym = symbols.iter().find(|s| s.kind == SymbolKind::Impl).unwrap();
assert_eq!(impl_sym.name, "Foo");
assert_eq!(impl_sym.children.len(), 1);
assert_eq!(impl_sym.children[0].name, "baz");
}
#[test]
fn rust_impl_trait_extracts_type_not_trait() {
let source = r#"
use std::fmt;
struct Foo;
impl fmt::Display for Foo {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Foo")
}
}
"#;
let symbols = extract_symbols(source, Language::Rust);
let impl_sym = symbols.iter().find(|s| s.kind == SymbolKind::Impl).unwrap();
assert_eq!(
impl_sym.name, "Foo",
"impl target should be Foo, not the trait"
);
}
#[test]
fn rust_impl_without_trait() {
let source = "struct Bar;\nimpl Bar { fn go(&self) {} }\n";
let symbols = extract_symbols(source, Language::Rust);
let impl_sym = symbols.iter().find(|s| s.kind == SymbolKind::Impl).unwrap();
assert_eq!(impl_sym.name, "Bar");
}
#[test]
fn extract_python_symbols() {
let source = r#"
class MyClass:
def method(self):
pass
def standalone():
pass
"#;
let symbols = extract_symbols(source, Language::Python);
let class = symbols.iter().find(|s| s.name == "MyClass").unwrap();
assert_eq!(class.kind, SymbolKind::Class);
assert_eq!(class.children.len(), 1);
assert_eq!(class.children[0].name, "method");
assert_eq!(class.children[0].kind, SymbolKind::Method);
let func = symbols.iter().find(|s| s.name == "standalone").unwrap();
assert_eq!(func.kind, SymbolKind::Function);
}
#[test]
fn extract_python_decorated_method() {
let source = "class Foo:\n @staticmethod\n def bar():\n pass\n";
let symbols = extract_symbols(source, Language::Python);
let class = symbols.iter().find(|s| s.name == "Foo").unwrap();
assert_eq!(class.children.len(), 1);
assert_eq!(class.children[0].name, "bar");
assert_eq!(
class.children[0].kind,
SymbolKind::Method,
"decorated method should be classified as Method, not Function"
);
}
#[test]
fn extract_go_symbols() {
let source = r#"
package main
func main() {
fmt.Println("hello")
}
type Config struct {
Host string
}
"#;
let symbols = extract_symbols(source, Language::Go);
let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"main"));
assert!(names.contains(&"Config"));
}
#[test]
fn go_grouped_type_declaration() {
let source = "package main\n\ntype (\n\tPoint struct{ X, Y int }\n\tReader interface{ Read([]byte) (int, error) }\n\tAlias = int\n)\n";
let symbols = extract_symbols(source, Language::Go);
let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"Point"), "missing Point, got {:?}", names);
assert!(names.contains(&"Reader"), "missing Reader, got {:?}", names);
assert!(names.contains(&"Alias"), "missing Alias, got {:?}", names);
let point = symbols.iter().find(|s| s.name == "Point").unwrap();
assert_eq!(point.kind, SymbolKind::Struct);
let reader = symbols.iter().find(|s| s.name == "Reader").unwrap();
assert_eq!(reader.kind, SymbolKind::Interface);
}
#[test]
fn find_symbol_qualified() {
let source = r#"
impl Server {
fn start(&self) {}
fn stop(&self) {}
}
"#;
let symbols = extract_symbols(source, Language::Rust);
let found = find_symbol(&symbols, "Server::start").expect("should find Server::start");
assert_eq!(found.name, "start");
}
#[test]
fn find_symbol_unqualified_searches_children() {
let source = r#"
impl Server {
fn start(&self) {}
}
"#;
let symbols = extract_symbols(source, Language::Rust);
find_symbol(&symbols, "start").expect("should find 'start' via unqualified search");
}
#[test]
fn find_symbol_deeply_nested() {
let inner = SymbolDef {
name: "deep_method".into(),
kind: SymbolKind::Method,
start_line: 3,
end_line: 4,
signature: "fn deep_method()".into(),
children: Vec::new(),
depth: 2,
};
let mid = SymbolDef {
name: "MidStruct".into(),
kind: SymbolKind::Struct,
start_line: 2,
end_line: 5,
signature: "struct MidStruct".into(),
children: vec![inner],
depth: 1,
};
let outer = SymbolDef {
name: "outer_mod".into(),
kind: SymbolKind::Module,
start_line: 1,
end_line: 6,
signature: "mod outer_mod".into(),
children: vec![mid],
depth: 0,
};
let symbols = vec![outer];
find_symbol(&symbols, "deep_method").expect("should find deeply nested symbol");
find_symbol(&symbols, "outer_mod::MidStruct").expect("should find qualified nested symbol");
}
#[test]
fn find_symbol_qualified_skips_struct_to_impl() {
let source = r#"
struct Point {
x: f64,
y: f64,
}
impl Point {
fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
}
"#;
let symbols = extract_symbols(source, Language::Rust);
let found = find_symbol(&symbols, "Point::new").expect("should find Point::new via impl");
assert_eq!(found.name, "new");
}
#[test]
fn symbol_kind_from_str() {
assert_eq!(
SymbolKind::from_str_loose("function"),
Some(SymbolKind::Function)
);
assert_eq!(SymbolKind::from_str_loose("fn"), Some(SymbolKind::Function));
assert_eq!(
SymbolKind::from_str_loose("struct"),
Some(SymbolKind::Struct)
);
assert_eq!(SymbolKind::from_str_loose("CONST"), Some(SymbolKind::Const));
assert_eq!(SymbolKind::from_str_loose("unknown"), None);
}
#[test]
fn unknown_language_returns_empty() {
assert!(extract_symbols("anything", Language::Unknown).is_empty());
}
#[test]
fn signature_truncates_at_brace() {
let source = "fn hello(x: i32) {\n x + 1\n}\n";
let symbols = extract_symbols(source, Language::Rust);
assert_eq!(symbols[0].signature, "fn hello(x: i32)");
}
#[test]
fn replace_function_signature_basic() {
let src = "fn old(a: i32) -> i32 { a }\nfn other() {}";
let res = replace_function_signature(src, "old", "pub fn new(b: u32) -> u32");
let out = res.expect("replace_function_signature should succeed for matching name");
assert!(out.contains("pub fn new(b: u32) -> u32"));
assert!(out.contains("fn other"));
assert!(!out.contains("fn old"));
assert!(
out.contains("{ a }"),
"function body should be preserved: {out}"
);
}
#[test]
fn extract_typescript_symbols() {
let source = r#"
class Foo {
greet(name: string): string {
return `Hello, ${name}`;
}
farewell(): void {
console.log("bye");
}
}
function bar(x: number): number {
return x * 2;
}
interface Baz {
id: number;
name: string;
}
enum Status {
Active,
Inactive,
Pending,
}
const MAX_RETRIES = 5;
"#;
let symbols = extract_symbols(source, Language::TypeScript);
let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"Foo"), "should find class Foo");
assert!(names.contains(&"bar"), "should find function bar");
assert!(names.contains(&"Baz"), "should find interface Baz");
assert!(names.contains(&"Status"), "should find enum Status");
assert!(
names.contains(&"MAX_RETRIES"),
"should find const MAX_RETRIES"
);
let class_foo = symbols.iter().find(|s| s.name == "Foo").unwrap();
assert_eq!(class_foo.kind, SymbolKind::Class);
let child_names: Vec<&str> = class_foo.children.iter().map(|c| c.name.as_str()).collect();
assert!(
child_names.contains(&"greet"),
"Foo should contain method greet"
);
assert!(
child_names.contains(&"farewell"),
"Foo should contain method farewell"
);
let iface = symbols.iter().find(|s| s.name == "Baz").unwrap();
assert_eq!(iface.kind, SymbolKind::Interface);
let status = symbols.iter().find(|s| s.name == "Status").unwrap();
assert_eq!(status.kind, SymbolKind::Enum);
}
#[test]
fn extract_typescript_multi_declarator_const() {
let source = "const a = 1, b = 2, c = 3;\n";
let symbols = extract_symbols(source, Language::TypeScript);
let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"a"), "should find const a: {names:?}");
assert!(names.contains(&"b"), "should find const b: {names:?}");
assert!(names.contains(&"c"), "should find const c: {names:?}");
assert_eq!(
symbols.len(),
3,
"exactly 3 symbols for 3 declarators: {names:?}"
);
}
#[test]
fn extract_typescript_let_var_not_extracted() {
let source = "let x = 1, y = 2;\nvar z = 3;\n";
let symbols = extract_symbols(source, Language::TypeScript);
assert!(
symbols.is_empty(),
"let/var should not produce symbols: {:?}",
symbols.iter().map(|s| &s.name).collect::<Vec<_>>()
);
}
#[test]
fn extract_java_symbols() {
let source = r#"
public class Foo {
private int count;
public void bar() {
System.out.println("hello");
}
public int getCount() {
return count;
}
}
interface Baz {
void process();
String getName();
}
enum Status {
ACTIVE,
INACTIVE,
PENDING
}
"#;
let symbols = extract_symbols(source, Language::Java);
let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"Foo"), "should find class Foo");
assert!(names.contains(&"Baz"), "should find interface Baz");
assert!(names.contains(&"Status"), "should find enum Status");
let class_foo = symbols.iter().find(|s| s.name == "Foo").unwrap();
assert_eq!(class_foo.kind, SymbolKind::Class);
let child_names: Vec<&str> = class_foo.children.iter().map(|c| c.name.as_str()).collect();
assert!(
child_names.contains(&"bar"),
"Foo should contain method bar"
);
assert!(
child_names.contains(&"getCount"),
"Foo should contain method getCount"
);
let iface = symbols.iter().find(|s| s.name == "Baz").unwrap();
assert_eq!(iface.kind, SymbolKind::Interface);
let status = symbols.iter().find(|s| s.name == "Status").unwrap();
assert_eq!(status.kind, SymbolKind::Enum);
}
#[test]
fn extract_c_symbols() {
let source = r#"
#include <stdio.h>
void foo(int x) {
printf("%d\n", x);
}
int calculate(int a, int b) {
return a + b;
}
struct Bar {
int x;
int y;
char name[64];
};
enum Color {
RED,
GREEN,
BLUE
};
"#;
let symbols = extract_symbols(source, Language::C);
let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"foo"), "should find function foo");
assert!(
names.contains(&"calculate"),
"should find function calculate"
);
assert!(names.contains(&"Bar"), "should find struct Bar");
assert!(names.contains(&"Color"), "should find enum Color");
let foo_sym = symbols.iter().find(|s| s.name == "foo").unwrap();
assert_eq!(foo_sym.kind, SymbolKind::Function);
let bar_sym = symbols.iter().find(|s| s.name == "Bar").unwrap();
assert_eq!(bar_sym.kind, SymbolKind::Struct);
let color_sym = symbols.iter().find(|s| s.name == "Color").unwrap();
assert_eq!(color_sym.kind, SymbolKind::Enum);
}
#[test]
fn extract_c_pointer_returning_functions() {
let source = r#"
int *pointer_func(void) { return 0; }
void *void_ptr_func(void) { return 0; }
char *string_func(void) { return "hello"; }
typedef struct Foo { int x; } Foo;
Foo *foo_create(void) { return 0; }
"#;
let symbols = extract_symbols(source, Language::C);
let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
assert!(
names.contains(&"pointer_func"),
"should find int* function: {names:?}"
);
assert!(
names.contains(&"void_ptr_func"),
"should find void* function: {names:?}"
);
assert!(
names.contains(&"string_func"),
"should find char* function: {names:?}"
);
assert!(
names.contains(&"foo_create"),
"should find Foo* function (not mislabeled as 'Foo'): {names:?}"
);
for fname in &["pointer_func", "void_ptr_func", "string_func", "foo_create"] {
let sym = symbols.iter().find(|s| s.name == *fname).unwrap();
assert_eq!(
sym.kind,
SymbolKind::Function,
"{fname} should be Function, got {:?}",
sym.kind
);
}
}
#[test]
fn extract_cpp_symbols() {
let source = r#"
#include <iostream>
#include <string>
class Engine {
public:
void start() {
std::cout << "started" << std::endl;
}
int getSpeed() const {
return speed;
}
private:
int speed;
};
namespace utils {
int helper(int x) {
return x + 1;
}
}
struct Point {
double x;
double y;
};
"#;
let symbols = extract_symbols(source, Language::Cpp);
let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"Engine"), "should find class Engine");
assert!(names.contains(&"Point"), "should find struct Point");
let engine = symbols.iter().find(|s| s.name == "Engine").unwrap();
assert_eq!(engine.kind, SymbolKind::Class);
}
#[test]
fn extract_cpp_qualified_method() {
let source = "void MyClass::process(int x) {\n // body\n}\n";
let symbols = extract_symbols(source, Language::Cpp);
let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
assert!(
names.contains(&"process"),
"should find qualified method process, got: {names:?}"
);
}
#[test]
fn rewrite_function_signature_structured() {
let src = "fn old(a: i32) -> i32 { a }\nfn other() {}";
let edit = FunctionSigEdit {
visibility: Some("pub(crate)".to_string()),
parameters: Some("(x: u32, y: &str)".to_string()),
return_type: Some("-> String".to_string()),
};
let res = rewrite_function_signature(src, "old", &edit, Language::Rust);
let out = res.expect("rewrite_function_signature should succeed for matching name");
assert!(out.contains("pub(crate) fn old(x: u32, y: &str) -> String"));
assert!(out.contains("fn other"));
assert!(!out.contains("fn old(a: i32)"));
}
#[test]
fn rewrite_function_signature_preserves_async() {
let src = "pub async fn process(data: &[u8]) -> Result<()> { Ok(()) }";
let edit = FunctionSigEdit {
visibility: None,
parameters: Some("(input: &str)".to_string()),
return_type: None,
};
let res = rewrite_function_signature(src, "process", &edit, Language::Rust);
let out = res.expect("rewrite should succeed");
assert!(
out.contains("pub async fn process(input: &str) -> Result<()>"),
"async qualifier should be preserved: {out}"
);
}
#[test]
fn rewrite_function_signature_preserves_unsafe() {
let src = "pub unsafe fn dangerous(ptr: *const u8) {}";
let edit = FunctionSigEdit {
visibility: None,
parameters: Some("(ptr: *mut u8)".to_string()),
return_type: None,
};
let res = rewrite_function_signature(src, "dangerous", &edit, Language::Rust);
let out = res.expect("rewrite should succeed");
assert!(
out.contains("pub unsafe fn dangerous(ptr: *mut u8)"),
"unsafe qualifier should be preserved: {out}"
);
}
#[test]
fn rewrite_python_params() {
let src = "def process(data: list) -> dict:\n return {}\n";
let edit = FunctionSigEdit {
parameters: Some("(data: list, validate: bool = True)".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "process", &edit, Language::Python).unwrap();
assert!(
out.contains("def process(data: list, validate: bool = True) -> dict:"),
"should replace params: {out}"
);
assert!(out.contains("return {}"), "body preserved");
}
#[test]
fn rewrite_python_return_type() {
let src = "def process(data: list) -> dict:\n return {}\n";
let edit = FunctionSigEdit {
return_type: Some("-> list".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "process", &edit, Language::Python).unwrap();
assert!(
out.contains("def process(data: list) -> list:"),
"should replace return type: {out}"
);
}
#[test]
fn rewrite_python_add_return_type() {
let src = "def process(data):\n pass\n";
let edit = FunctionSigEdit {
return_type: Some("-> dict".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "process", &edit, Language::Python).unwrap();
assert!(out.contains("-> dict"), "should add return type: {out}");
}
#[test]
fn rewrite_python_async_preserved() {
let src = "async def fetch(url: str) -> bytes:\n pass\n";
let edit = FunctionSigEdit {
parameters: Some("(url: str, timeout: int = 30)".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "fetch", &edit, Language::Python).unwrap();
assert!(
out.contains("async def fetch(url: str, timeout: int = 30)"),
"async should be preserved: {out}"
);
}
#[test]
fn rewrite_typescript_params() {
let src = "function fetchData(url: string): Promise<Response> {\n return fetch(url);\n}\n";
let edit = FunctionSigEdit {
parameters: Some("(url: string, options?: RequestInit)".to_string()),
..Default::default()
};
let out =
rewrite_function_signature(src, "fetchData", &edit, Language::TypeScript).unwrap();
assert!(
out.contains("function fetchData(url: string, options?: RequestInit)"),
"should replace params: {out}"
);
assert!(out.contains("return fetch(url)"), "body preserved");
}
#[test]
fn rewrite_typescript_return_type() {
let src = "function fetchData(url: string): Promise<Response> {\n return fetch(url);\n}\n";
let edit = FunctionSigEdit {
return_type: Some(": Promise<Result[]>".to_string()),
..Default::default()
};
let out =
rewrite_function_signature(src, "fetchData", &edit, Language::TypeScript).unwrap();
assert!(
out.contains(": Promise<Result[]>"),
"should replace return type: {out}"
);
}
#[test]
fn rewrite_go_function_params() {
let src =
"func HandleRequest(w http.ResponseWriter, r *http.Request) error {\n\treturn nil\n}\n";
let edit = FunctionSigEdit {
parameters: Some(
"(ctx context.Context, w http.ResponseWriter, r *http.Request)".to_string(),
),
..Default::default()
};
let out = rewrite_function_signature(src, "HandleRequest", &edit, Language::Go).unwrap();
assert!(
out.contains(
"func HandleRequest(ctx context.Context, w http.ResponseWriter, r *http.Request)"
),
"should replace params: {out}"
);
assert!(out.contains("return nil"), "body preserved");
}
#[test]
fn rewrite_go_function_return_type() {
let src =
"func HandleRequest(w http.ResponseWriter, r *http.Request) error {\n\treturn nil\n}\n";
let edit = FunctionSigEdit {
return_type: Some("(*Response, error)".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "HandleRequest", &edit, Language::Go).unwrap();
assert!(
out.contains("(*Response, error)"),
"should replace return type: {out}"
);
}
#[test]
fn rewrite_go_method_params() {
let src = "func (s *Server) HandleRequest(w http.ResponseWriter, r *http.Request) error {\n\treturn nil\n}\n";
let edit = FunctionSigEdit {
parameters: Some("(ctx context.Context, r *http.Request)".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "HandleRequest", &edit, Language::Go).unwrap();
assert!(
out.contains("func (s *Server) HandleRequest(ctx context.Context, r *http.Request)"),
"should replace method params without touching receiver: {out}"
);
}
#[test]
fn rewrite_java_params() {
let src = "public class Foo {\n public void processEvent(Event e) {\n // body\n }\n}\n";
let edit = FunctionSigEdit {
parameters: Some("(Event e, Config config)".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "processEvent", &edit, Language::Java).unwrap();
assert!(
out.contains("processEvent(Event e, Config config)"),
"should replace params: {out}"
);
}
#[test]
fn rewrite_java_return_type() {
let src = "public class Foo {\n public void processEvent(Event e) {\n // body\n }\n}\n";
let edit = FunctionSigEdit {
return_type: Some("Response".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "processEvent", &edit, Language::Java).unwrap();
assert!(
out.contains("public Response processEvent"),
"should replace return type: {out}"
);
}
#[test]
fn rewrite_java_visibility() {
let src = "public class Foo {\n public void processEvent(Event e) {\n // body\n }\n}\n";
let edit = FunctionSigEdit {
visibility: Some("protected".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "processEvent", &edit, Language::Java).unwrap();
assert!(
out.contains("protected void processEvent"),
"should replace visibility: {out}"
);
}
#[test]
fn rewrite_c_params() {
let src = "int process(const char *input) {\n return 0;\n}\n";
let edit = FunctionSigEdit {
parameters: Some("(const char *input, size_t len, int flags)".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "process", &edit, Language::C).unwrap();
assert!(
out.contains("int process(const char *input, size_t len, int flags)"),
"should replace params: {out}"
);
assert!(out.contains("return 0"), "body preserved");
}
#[test]
fn rewrite_c_return_type() {
let src = "int process(const char *input) {\n return 0;\n}\n";
let edit = FunctionSigEdit {
return_type: Some("void".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "process", &edit, Language::C).unwrap();
assert!(
out.contains("void process(const char *input)"),
"should replace return type: {out}"
);
}
#[test]
fn rewrite_cpp_params() {
let src = "void MyClass::process(int x) {\n // body\n}\n";
let edit = FunctionSigEdit {
parameters: Some("(int x, double y)".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "process", &edit, Language::Cpp).unwrap();
assert!(
out.contains("void MyClass::process(int x, double y)"),
"should replace C++ method params: {out}"
);
}
#[test]
fn rewrite_unsupported_language_returns_none() {
let src = "whatever";
let edit = FunctionSigEdit::default();
assert!(rewrite_function_signature(src, "foo", &edit, Language::Unknown).is_none());
}
#[test]
fn rewrite_function_not_found_returns_none() {
let src = "def process(data):\n pass\n";
let edit = FunctionSigEdit {
parameters: Some("(x)".to_string()),
..Default::default()
};
assert!(rewrite_function_signature(src, "nonexistent", &edit, Language::Python).is_none());
}
#[test]
fn full_span_no_attributes() {
let source = "fn foo() {\n 42\n}\n";
let symbols = extract_symbols(source, Language::Rust);
let sym = &symbols[0];
let (start, end) = full_symbol_span(source, sym, Language::Rust);
assert_eq!(start, sym.start_line);
assert_eq!(end, sym.end_line);
}
#[test]
fn full_span_single_attribute() {
let source = "#[test]\nfn foo() {\n 42\n}\n";
let symbols = extract_symbols(source, Language::Rust);
let sym = &symbols[0];
assert_eq!(sym.start_line, 2); let (start, end) = full_symbol_span(source, sym, Language::Rust);
assert_eq!(start, 1); assert_eq!(end, sym.end_line);
}
#[test]
fn full_span_stacked_attributes() {
let source = "#[test]\n#[cfg(unix)]\nfn foo() {}\n";
let symbols = extract_symbols(source, Language::Rust);
let sym = &symbols[0];
let (start, _) = full_symbol_span(source, sym, Language::Rust);
assert_eq!(start, 1); }
#[test]
fn full_span_doc_comment() {
let source = "/// This is a doc comment.\n/// Second line.\nfn foo() {}\n";
let symbols = extract_symbols(source, Language::Rust);
let sym = &symbols[0];
let (start, _) = full_symbol_span(source, sym, Language::Rust);
assert_eq!(start, 1);
}
#[test]
fn full_span_mixed_attrs_and_docs() {
let source = "/// A doc comment.\n#[test]\n#[cfg(unix)]\nfn foo() {}\n";
let symbols = extract_symbols(source, Language::Rust);
let sym = &symbols[0];
let (start, _) = full_symbol_span(source, sym, Language::Rust);
assert_eq!(start, 1);
}
#[test]
fn full_span_python_decorator() {
let source = "@staticmethod\ndef foo():\n pass\n";
let symbols = extract_symbols(source, Language::Python);
let sym = &symbols[0];
let (start, _) = full_symbol_span(source, sym, Language::Python);
assert_eq!(start, 1);
}
#[test]
fn full_span_python_multiline_decorator() {
let source = "\
@decorator(
arg1,
arg2
)
def foo():
pass
";
let symbols = extract_symbols(source, Language::Python);
let sym = symbols.iter().find(|s| s.name == "foo").unwrap();
let (start, _) = full_symbol_span(source, sym, Language::Python);
assert_eq!(
start, 1,
"multiline @decorator(...) should be included in foo's span"
);
}
#[test]
fn full_span_python_stacked_multiline_decorator() {
let source = "\
@first_decorator
@second_decorator(
option=True
)
def bar():
pass
";
let symbols = extract_symbols(source, Language::Python);
let sym = symbols.iter().find(|s| s.name == "bar").unwrap();
let (start, _) = full_symbol_span(source, sym, Language::Python);
assert_eq!(
start, 1,
"both decorators (including multiline) should be included"
);
}
#[test]
fn full_span_excludes_inner_doc_comments() {
let source = "//! Module-level doc.\nstruct Config {}\n";
let symbols = extract_symbols(source, Language::Rust);
let sym = symbols.iter().find(|s| s.name == "Config").unwrap();
let (start, _) = full_symbol_span(source, sym, Language::Rust);
assert_eq!(
start, 2,
"inner doc comment //! should not be included in Config's span"
);
}
#[test]
fn full_span_stops_at_unrelated_code() {
let source = "fn bar() {}\n\n#[test]\nfn foo() {}\n";
let symbols = extract_symbols(source, Language::Rust);
let foo = symbols.iter().find(|s| s.name == "foo").unwrap();
let (start, _) = full_symbol_span(source, foo, Language::Rust);
assert_eq!(start, 3); }
#[test]
fn full_span_multiline_rust_attribute() {
let source = "\
#[cfg_attr(
feature = \"serde\",
derive(Serialize, Deserialize)
)]
struct Config {
name: String,
}
";
let symbols = extract_symbols(source, Language::Rust);
let sym = symbols.iter().find(|s| s.name == "Config").unwrap();
let (start, _) = full_symbol_span(source, sym, Language::Rust);
assert_eq!(
start, 1,
"multiline #[cfg_attr(...)] should be included in Config's span"
);
}
#[test]
fn full_span_multiline_attribute_with_stacked_single() {
let source = "\
#[derive(Debug)]
#[cfg_attr(
feature = \"serde\",
derive(Serialize)
)]
pub struct Foo {}
";
let symbols = extract_symbols(source, Language::Rust);
let sym = symbols.iter().find(|s| s.name == "Foo").unwrap();
let (start, _) = full_symbol_span(source, sym, Language::Rust);
assert_eq!(
start, 1,
"both #[derive(Debug)] and the multiline #[cfg_attr] should be included"
);
}
#[test]
fn extract_symbol_text_basic() {
let source = "#[test]\nfn foo() {\n 42\n}\n\nfn bar() {}\n";
let symbols = extract_symbols(source, Language::Rust);
let foo = symbols.iter().find(|s| s.name == "foo").unwrap();
let text = extract_symbol_text(source, foo, Language::Rust);
assert!(text.contains("#[test]"));
assert!(text.contains("fn foo()"));
assert!(!text.contains("fn bar"));
}
#[test]
fn extract_symbol_text_mixed_line_endings() {
let source = "fn first() {}\r\nfn second() {\n 42\n}\n";
let symbols = extract_symbols(source, Language::Rust);
let second = symbols.iter().find(|s| s.name == "second").unwrap();
let text = extract_symbol_text(source, second, Language::Rust);
assert!(
text.contains("fn second()"),
"should contain fn second(): {text:?}"
);
assert!(
!text.contains("fn first()"),
"should not contain fn first(): {text:?}"
);
}
#[test]
fn find_function_span_rust() {
let source = "fn hello(x: i32) -> String {\n x.to_string()\n}\n";
let span = find_function_span(source, "hello", Language::Rust).unwrap();
assert_eq!(span.name, "hello");
assert!(span.signature_text.contains("fn hello(x: i32) -> String"));
assert!(!span.signature_text.contains("x.to_string()"));
assert_eq!(span.start_line, 1);
}
#[test]
fn find_function_span_python() {
let source = "class Foo:\n def process(self, data: list) -> dict:\n return {}\n";
let span = find_function_span(source, "process", Language::Python).unwrap();
assert_eq!(span.name, "process");
assert!(span.signature_text.contains("def process"));
assert!(span.signature_text.contains("-> dict"));
}
#[test]
fn find_function_span_typescript() {
let source = "export async function fetchData(url: string): Promise<Response> {\n return fetch(url);\n}\n";
let span = find_function_span(source, "fetchData", Language::TypeScript).unwrap();
assert_eq!(span.name, "fetchData");
assert!(span.signature_text.contains("function fetchData"));
}
#[test]
fn find_function_span_go_function() {
let source =
"func HandleRequest(w http.ResponseWriter, r *http.Request) error {\n\treturn nil\n}\n";
let span = find_function_span(source, "HandleRequest", Language::Go).unwrap();
assert_eq!(span.name, "HandleRequest");
assert!(span.signature_text.contains("func HandleRequest"));
}
#[test]
fn find_function_span_go_method() {
let source = "func (s *Server) HandleRequest(w http.ResponseWriter, r *http.Request) error {\n\treturn nil\n}\n";
let span = find_function_span(source, "HandleRequest", Language::Go).unwrap();
assert_eq!(span.name, "HandleRequest");
assert!(
span.signature_text
.contains("func (s *Server) HandleRequest")
);
}
#[test]
fn find_function_span_java() {
let source = "public class Foo {\n public void processEvent(Event e) {\n // ...\n }\n}\n";
let span = find_function_span(source, "processEvent", Language::Java).unwrap();
assert_eq!(span.name, "processEvent");
assert!(span.signature_text.contains("public void processEvent"));
}
#[test]
fn find_function_span_not_found() {
let source = "fn hello() {}\n";
let result = find_function_span(source, "nonexistent", Language::Rust);
assert!(result.is_none());
}
#[test]
fn find_function_span_no_grammar() {
let source = "whatever";
let result = find_function_span(source, "foo", Language::Unknown);
assert!(result.is_none());
}
#[test]
fn find_function_span_multiline_python() {
let source = "def long_function(\n param1: str,\n param2: int,\n param3: bool = False,\n) -> dict:\n return {}\n";
let span = find_function_span(source, "long_function", Language::Python).unwrap();
assert_eq!(span.name, "long_function");
assert!(span.signature_text.contains("param1: str"));
assert!(span.signature_text.contains("-> dict"));
assert_eq!(span.start_line, 1);
assert!(span.signature_end_line >= 5);
}
#[test]
fn find_function_span_can_splice_replacement() {
let source = "fn old_name(x: i32) -> bool {\n true\n}\n";
let span = find_function_span(source, "old_name", Language::Rust).unwrap();
let new_sig = "fn new_name(x: i32, y: bool) -> bool ";
let mut result = String::new();
result.push_str(&source[..span.signature_range.start]);
result.push_str(new_sig);
result.push_str(&source[span.signature_range.end..]);
assert!(result.contains("fn new_name(x: i32, y: bool) -> bool"));
assert!(result.contains("true")); }
#[test]
fn find_function_span_cpp() {
let source = "void process(int x, double y) {\n // body\n}\n";
let span = find_function_span(source, "process", Language::Cpp).unwrap();
assert_eq!(span.name, "process");
assert!(
span.signature_text
.contains("void process(int x, double y)")
);
assert!(!span.signature_text.contains("// body"));
}
#[test]
fn find_function_span_cpp_qualified_name() {
let source = "void MyClass::process(int x) {\n // body\n}\n";
let span = find_function_span(source, "process", Language::Cpp).unwrap();
assert_eq!(span.name, "process");
assert!(span.signature_text.contains("void MyClass::process(int x)"));
}
#[test]
fn find_function_span_cpp_nested_namespace_qualified() {
let source = "int ns::MyClass::deep() {\n return 0;\n}\n";
let span = find_function_span(source, "deep", Language::Cpp).unwrap();
assert_eq!(span.name, "deep");
assert!(span.signature_text.contains("ns::MyClass::deep()"));
}
#[test]
fn find_function_span_cpp_triple_nested_namespace() {
let source = "int outer::inner::MyClass::compute(int x) {\n return x;\n}\n";
let span = find_function_span(source, "compute", Language::Cpp).unwrap();
assert_eq!(span.name, "compute");
assert!(
span.signature_text
.contains("outer::inner::MyClass::compute(int x)")
);
}
#[test]
fn find_function_span_cpp_qualified_no_false_positive() {
let source = "void MyClass::alpha(int x) {\n}\nvoid MyClass::beta() {\n}\n";
let span = find_function_span(source, "beta", Language::Cpp).unwrap();
assert_eq!(span.name, "beta");
assert!(span.signature_text.contains("beta"));
assert!(!span.signature_text.contains("alpha"));
}
#[test]
fn find_function_span_c() {
let source = "int main(int argc, char *argv[]) {\n return 0;\n}\n";
let span = find_function_span(source, "main", Language::C).unwrap();
assert_eq!(span.name, "main");
assert!(span.signature_text.contains("int main"));
assert!(!span.signature_text.contains("return 0"));
}
#[test]
fn extract_symbol_text_crlf() {
let source = "fn first() {}\r\nfn second() {\r\n 42\r\n}\r\n";
let symbols = extract_symbols(source, Language::Rust);
let second = symbols.iter().find(|s| s.name == "second").unwrap();
let text = extract_symbol_text(source, second, Language::Rust);
assert!(
text.contains("fn second()"),
"extracted text should contain 'fn second()', got: {:?}",
text
);
assert!(
!text.contains("fn first()"),
"extracted text should not contain 'fn first()', got: {:?}",
text
);
}
#[test]
fn node_signature_skips_destructuring_brace() {
let source = "function foo({a, b}) {\n return a + b;\n}\n";
let syms = extract_symbols(source, Language::JavaScript);
let foo = syms
.iter()
.find(|s| s.name == "foo")
.expect("foo not found");
assert!(
foo.signature.contains("{a, b}"),
"signature should include destructured params: {:?}",
foo.signature
);
}
#[test]
fn go_receiver_methods_grouped_under_struct() {
let source = "\
package main
type Server struct {
\tHost string
}
func NewServer() *Server { return nil }
func (s *Server) Start() error { return nil }
func (s *Server) Stop() {}
";
let syms = extract_symbols(source, Language::Go);
let server = syms.iter().find(|s| s.name == "Server").unwrap();
assert_eq!(server.kind, SymbolKind::Struct);
assert_eq!(
server.children.len(),
2,
"Server should have 2 method children, got: {:?}",
server.children.iter().map(|c| &c.name).collect::<Vec<_>>()
);
assert!(server.children.iter().any(|c| c.name == "Start"));
assert!(server.children.iter().any(|c| c.name == "Stop"));
assert!(
syms.iter()
.any(|s| s.name == "NewServer" && s.kind == SymbolKind::Function)
);
}
#[test]
fn go_qualified_name_lookup_works() {
let source = "\
package main
type Dog struct{}
func (d *Dog) Speak() string { return \"woof\" }
type Cat struct{}
func (c *Cat) Speak() string { return \"meow\" }
";
let syms = extract_symbols(source, Language::Go);
let dog_speak = find_symbol(&syms, "Dog::Speak").expect("Dog::Speak should be found");
assert!(dog_speak.signature.contains("Dog"));
let cat_speak = find_symbol(&syms, "Cat::Speak").expect("Cat::Speak should be found");
assert!(cat_speak.signature.contains("Cat"));
}
#[test]
fn go_value_receiver_grouped() {
let source = "\
package main
type Counter struct{ n int }
func (c Counter) Count() int { return c.n }
";
let syms = extract_symbols(source, Language::Go);
let counter = syms.iter().find(|s| s.name == "Counter").unwrap();
assert_eq!(counter.children.len(), 1);
assert_eq!(counter.children[0].name, "Count");
}
#[test]
fn go_method_without_matching_struct_stays_top_level() {
let source = "\
package main
func (e *External) DoWork() {}
";
let syms = extract_symbols(source, Language::Go);
assert!(
syms.iter()
.any(|s| s.name == "DoWork" && s.kind == SymbolKind::Method),
"orphan method should stay top-level"
);
}
#[test]
fn parse_go_receiver_type_cases() {
assert_eq!(
parse_go_receiver_type("func (s *Server) Start()"),
Some("Server".to_string())
);
assert_eq!(
parse_go_receiver_type("func (s Server) Start()"),
Some("Server".to_string())
);
assert_eq!(
parse_go_receiver_type("func (*Server) Method()"),
Some("Server".to_string())
);
assert_eq!(parse_go_receiver_type("func main()"), None);
assert_eq!(parse_go_receiver_type("func NewServer() *Server"), None);
}
}