use kdo_core::Language;
use serde::{Deserialize, Serialize};
use std::path::Path;
use tracing::debug;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SignatureKind {
Function,
Struct,
Enum,
Trait,
TypeAlias,
Constant,
Impl,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Signature {
pub kind: SignatureKind,
pub text: String,
pub file: String,
pub line: usize,
}
pub fn extract_signatures(file_path: &Path, language: &Language) -> Vec<Signature> {
let content = match std::fs::read_to_string(file_path) {
Ok(c) => c,
Err(_) => return Vec::new(),
};
let file_str = file_path.to_string_lossy().to_string();
match language {
Language::Rust | Language::Anchor => extract_rust_signatures(&content, &file_str),
Language::TypeScript | Language::JavaScript => extract_ts_signatures(&content, &file_str),
Language::Python => extract_python_signatures(&content, &file_str),
Language::Go => extract_go_signatures(&content, &file_str),
}
}
fn extract_rust_signatures(source: &str, file: &str) -> Vec<Signature> {
let mut parser = tree_sitter::Parser::new();
let ts_lang = tree_sitter_rust::language();
if parser.set_language(&ts_lang).is_err() {
return fallback_rust_extract(source, file);
}
let tree = match parser.parse(source, None) {
Some(t) => t,
None => return fallback_rust_extract(source, file),
};
let mut sigs = Vec::new();
let root = tree.root_node();
let mut cursor = root.walk();
for node in root.children(&mut cursor) {
match node.kind() {
"function_item" => {
if let Some(sig) = extract_rust_fn_sig(source, &node, file) {
sigs.push(sig);
}
}
"struct_item" => {
if let Some(sig) = extract_rust_type_sig(source, &node, file, SignatureKind::Struct)
{
sigs.push(sig);
}
}
"enum_item" => {
if let Some(sig) = extract_rust_type_sig(source, &node, file, SignatureKind::Enum) {
sigs.push(sig);
}
}
"trait_item" => {
if let Some(sig) = extract_rust_type_sig(source, &node, file, SignatureKind::Trait)
{
sigs.push(sig);
}
}
"impl_item" => {
if let Some(sig) = extract_rust_impl_sig(source, &node, file) {
sigs.push(sig);
}
}
"type_item" => {
let text = node_text(source, &node);
sigs.push(Signature {
kind: SignatureKind::TypeAlias,
text,
file: file.to_string(),
line: node.start_position().row + 1,
});
}
"const_item" | "static_item" => {
if is_pub(source, &node) {
let text = node_text(source, &node);
sigs.push(Signature {
kind: SignatureKind::Constant,
text,
file: file.to_string(),
line: node.start_position().row + 1,
});
}
}
_ => {}
}
}
debug!(file = file, count = sigs.len(), "extracted Rust signatures");
sigs
}
fn extract_rust_fn_sig(
source: &str,
node: &tree_sitter::Node<'_>,
file: &str,
) -> Option<Signature> {
if !is_pub(source, node) {
return None;
}
let mut sig_end = node.end_byte();
let mut child_cursor = node.walk();
for child in node.children(&mut child_cursor) {
if child.kind() == "block" {
sig_end = child.start_byte();
break;
}
}
let text = source[node.start_byte()..sig_end].trim().to_string();
Some(Signature {
kind: SignatureKind::Function,
text,
file: file.to_string(),
line: node.start_position().row + 1,
})
}
fn extract_rust_type_sig(
source: &str,
node: &tree_sitter::Node<'_>,
file: &str,
kind: SignatureKind,
) -> Option<Signature> {
if !is_pub(source, node) {
return None;
}
let sig_end = node.end_byte();
let text = source[node.start_byte()..sig_end].trim().to_string();
Some(Signature {
kind,
text,
file: file.to_string(),
line: node.start_position().row + 1,
})
}
fn extract_rust_impl_sig(
source: &str,
node: &tree_sitter::Node<'_>,
file: &str,
) -> Option<Signature> {
let mut sig_end = node.end_byte();
let mut child_cursor = node.walk();
for child in node.children(&mut child_cursor) {
if child.kind() == "declaration_list" {
sig_end = child.start_byte();
break;
}
}
let text = source[node.start_byte()..sig_end].trim().to_string();
Some(Signature {
kind: SignatureKind::Impl,
text,
file: file.to_string(),
line: node.start_position().row + 1,
})
}
fn extract_ts_signatures(source: &str, file: &str) -> Vec<Signature> {
let mut parser = tree_sitter::Parser::new();
let ts_lang = tree_sitter_typescript::language_typescript();
if parser.set_language(&ts_lang).is_err() {
return fallback_ts_extract(source, file);
}
let tree = match parser.parse(source, None) {
Some(t) => t,
None => return fallback_ts_extract(source, file),
};
let mut sigs = Vec::new();
let root = tree.root_node();
let mut cursor = root.walk();
for node in root.children(&mut cursor) {
if node.kind() != "export_statement" {
continue;
}
let mut child_cursor = node.walk();
for child in node.children(&mut child_cursor) {
match child.kind() {
"function_declaration" | "function_signature" => {
let mut sig_end = child.end_byte();
let mut gc = child.walk();
for grandchild in child.children(&mut gc) {
if grandchild.kind() == "statement_block" {
sig_end = grandchild.start_byte();
break;
}
}
let text = format!("export {}", source[child.start_byte()..sig_end].trim());
sigs.push(Signature {
kind: SignatureKind::Function,
text,
file: file.to_string(),
line: child.start_position().row + 1,
});
}
"class_declaration" => {
let mut sig_end = child.end_byte();
let mut gc = child.walk();
for grandchild in child.children(&mut gc) {
if grandchild.kind() == "class_body" {
sig_end = grandchild.start_byte();
break;
}
}
let text = format!("export {}", source[child.start_byte()..sig_end].trim());
sigs.push(Signature {
kind: SignatureKind::Struct,
text,
file: file.to_string(),
line: child.start_position().row + 1,
});
}
"interface_declaration" => {
let text = format!("export {}", node_text(source, &child));
sigs.push(Signature {
kind: SignatureKind::Trait,
text,
file: file.to_string(),
line: child.start_position().row + 1,
});
}
"type_alias_declaration" => {
let text = format!("export {}", node_text(source, &child));
sigs.push(Signature {
kind: SignatureKind::TypeAlias,
text,
file: file.to_string(),
line: child.start_position().row + 1,
});
}
"lexical_declaration" => {
let text = format!("export {}", node_text(source, &child));
sigs.push(Signature {
kind: SignatureKind::Constant,
text,
file: file.to_string(),
line: child.start_position().row + 1,
});
}
_ => {}
}
}
}
debug!(file = file, count = sigs.len(), "extracted TS signatures");
sigs
}
fn extract_python_signatures(source: &str, file: &str) -> Vec<Signature> {
let mut parser = tree_sitter::Parser::new();
let py_lang = tree_sitter_python::language();
if parser.set_language(&py_lang).is_err() {
return fallback_python_extract(source, file);
}
let tree = match parser.parse(source, None) {
Some(t) => t,
None => return fallback_python_extract(source, file),
};
let mut sigs = Vec::new();
let root = tree.root_node();
let mut cursor = root.walk();
for node in root.children(&mut cursor) {
match node.kind() {
"function_definition" => {
let mut sig_end = node.end_byte();
let mut child_cursor = node.walk();
for child in node.children(&mut child_cursor) {
if child.kind() == "block" {
sig_end = child.start_byte();
break;
}
}
let text = source[node.start_byte()..sig_end].trim().to_string();
if !text.contains("def _") || text.contains("def __init__") {
sigs.push(Signature {
kind: SignatureKind::Function,
text,
file: file.to_string(),
line: node.start_position().row + 1,
});
}
}
"class_definition" => {
let mut sig_end = node.end_byte();
let mut child_cursor = node.walk();
for child in node.children(&mut child_cursor) {
if child.kind() == "block" {
sig_end = child.start_byte();
break;
}
}
let text = source[node.start_byte()..sig_end].trim().to_string();
sigs.push(Signature {
kind: SignatureKind::Struct,
text,
file: file.to_string(),
line: node.start_position().row + 1,
});
}
"expression_statement" => {
let text = node_text(source, &node);
if text.contains(':') && !text.starts_with('_') {
sigs.push(Signature {
kind: SignatureKind::Constant,
text,
file: file.to_string(),
line: node.start_position().row + 1,
});
}
}
_ => {}
}
}
debug!(
file = file,
count = sigs.len(),
"extracted Python signatures"
);
sigs
}
fn is_pub(source: &str, node: &tree_sitter::Node<'_>) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "visibility_modifier" {
let text = node_text(source, &child);
return text.starts_with("pub");
}
}
false
}
fn node_text(source: &str, node: &tree_sitter::Node<'_>) -> String {
source[node.start_byte()..node.end_byte()].to_string()
}
fn fallback_rust_extract(source: &str, file: &str) -> Vec<Signature> {
let mut sigs = Vec::new();
for (i, line) in source.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("pub fn ")
|| trimmed.starts_with("pub struct ")
|| trimmed.starts_with("pub enum ")
|| trimmed.starts_with("pub trait ")
{
let kind = if trimmed.starts_with("pub fn") {
SignatureKind::Function
} else if trimmed.starts_with("pub struct") {
SignatureKind::Struct
} else if trimmed.starts_with("pub enum") {
SignatureKind::Enum
} else {
SignatureKind::Trait
};
sigs.push(Signature {
kind,
text: trimmed.trim_end_matches('{').trim().to_string(),
file: file.to_string(),
line: i + 1,
});
}
}
sigs
}
fn fallback_ts_extract(source: &str, file: &str) -> Vec<Signature> {
let mut sigs = Vec::new();
for (i, line) in source.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("export function ")
|| trimmed.starts_with("export class ")
|| trimmed.starts_with("export interface ")
|| trimmed.starts_with("export type ")
|| trimmed.starts_with("export const ")
{
sigs.push(Signature {
kind: SignatureKind::Function,
text: trimmed.trim_end_matches('{').trim().to_string(),
file: file.to_string(),
line: i + 1,
});
}
}
sigs
}
fn fallback_python_extract(source: &str, file: &str) -> Vec<Signature> {
let mut sigs = Vec::new();
for (i, line) in source.lines().enumerate() {
let trimmed = line.trim();
if (trimmed.starts_with("def ") || trimmed.starts_with("class "))
&& !trimmed.starts_with("def _")
{
let kind = if trimmed.starts_with("def ") {
SignatureKind::Function
} else {
SignatureKind::Struct
};
sigs.push(Signature {
kind,
text: trimmed.trim_end_matches(':').trim().to_string(),
file: file.to_string(),
line: i + 1,
});
}
}
sigs
}
fn extract_go_signatures(source: &str, file: &str) -> Vec<Signature> {
let mut sigs = Vec::new();
for (i, line) in source.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("func ") {
let is_exported = trimmed
.trim_start_matches("func ")
.trim_start_matches('(') .chars()
.next()
.map(|c| c.is_uppercase())
.unwrap_or(false)
|| {
if let Some(close) = trimmed.find(')') {
trimmed[close..]
.trim_start_matches(')')
.trim()
.chars()
.next()
.map(|c| c.is_uppercase())
.unwrap_or(false)
} else {
false
}
};
if is_exported {
let sig = trimmed.trim_end_matches('{').trim().to_string();
sigs.push(Signature {
kind: SignatureKind::Function,
text: sig,
file: file.to_string(),
line: i + 1,
});
}
} else if trimmed.starts_with("type ") {
let rest = trimmed.trim_start_matches("type ").trim();
let first_char = rest.chars().next().unwrap_or(' ');
if first_char.is_uppercase() {
let kind = if rest.contains("interface") {
SignatureKind::Trait
} else if rest.contains("struct") {
SignatureKind::Struct
} else {
SignatureKind::Constant
};
sigs.push(Signature {
kind,
text: trimmed.trim_end_matches('{').trim().to_string(),
file: file.to_string(),
line: i + 1,
});
}
}
}
sigs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rust_extraction() {
let source = r#"
pub fn hello(name: &str) -> String {
format!("hello {name}")
}
fn private_fn() {}
pub struct Foo {
pub bar: u32,
}
pub enum Color {
Red,
Green,
Blue,
}
"#;
let sigs = extract_rust_signatures(source, "test.rs");
assert!(sigs.iter().any(|s| s.text.contains("pub fn hello")));
assert!(!sigs.iter().any(|s| s.text.contains("private_fn")));
assert!(sigs.iter().any(|s| s.text.contains("pub struct Foo")));
}
#[test]
fn test_python_extraction() {
let source = r#"
def hello(name: str) -> str:
return f"hello {name}"
def _private():
pass
class Greeter:
def __init__(self):
pass
"#;
let sigs = extract_python_signatures(source, "test.py");
assert!(sigs.iter().any(|s| s.text.contains("def hello")));
assert!(!sigs.iter().any(|s| s.text == "def _private():"));
assert!(sigs.iter().any(|s| s.text.contains("class Greeter")));
}
}