use std::path::Path;
use serde::Serialize;
use super::{Language, 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();
super::symbol_extract::visit_node(&mut cursor, source, lang, 0, &mut symbols);
if lang == Language::Go {
super::symbol_extract::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
}
}
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 {
Err(anyhow::Error::new(crate::exit::InvalidInputError {
msg: format!(
"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;