mod go;
mod python;
mod rust;
mod solidity;
mod typescript;
use std::path::Path;
use tree_sitter::{Node, Query, QueryCursor};
use crate::db::{Edge, EdgeKind, ParseResult, Symbol, SymbolKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Language {
Rust,
TypeScript,
Tsx,
JavaScript,
Jsx,
Python,
Go,
Solidity,
Yaml,
Unknown,
}
impl Language {
pub fn from_path(path: &Path) -> Self {
match path.extension().and_then(|e| e.to_str()) {
Some("rs") => Language::Rust,
Some("ts") => Language::TypeScript,
Some("tsx") => Language::Tsx,
Some("js") | Some("mjs") | Some("cjs") => Language::JavaScript,
Some("jsx") => Language::Jsx,
Some("py") | Some("pyi") => Language::Python,
Some("go") => Language::Go,
Some("sol") => Language::Solidity,
Some("yaml") | Some("yml") => Language::Yaml,
_ => Language::Unknown,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Language::Rust => "rust",
Language::TypeScript => "typescript",
Language::Tsx => "tsx",
Language::JavaScript => "javascript",
Language::Jsx => "jsx",
Language::Python => "python",
Language::Go => "go",
Language::Solidity => "solidity",
Language::Yaml => "yaml",
Language::Unknown => "unknown",
}
}
}
pub struct CodeParser {
go_parser: go::GoParser,
python_parser: python::PythonParser,
rust_parser: rust::RustParser,
solidity_parser: solidity::SolidityParser,
typescript_parser: typescript::TypeScriptParser,
}
impl CodeParser {
pub fn new() -> Self {
Self {
go_parser: go::GoParser::new(),
python_parser: python::PythonParser::new(),
rust_parser: rust::RustParser::new(),
solidity_parser: solidity::SolidityParser::new(),
typescript_parser: typescript::TypeScriptParser::new(),
}
}
pub fn parse(&mut self, path: &Path, source: &str) -> Option<ParseResult> {
let language = Language::from_path(path);
let file_path = path.to_string_lossy().to_string();
match language {
Language::Rust => self.rust_parser.parse(&file_path, source),
Language::Solidity => self.solidity_parser.parse(&file_path, source),
Language::TypeScript => {
self.typescript_parser
.parse(&file_path, source, typescript::JsVariant::TypeScript)
}
Language::Tsx => {
self.typescript_parser
.parse(&file_path, source, typescript::JsVariant::Tsx)
}
Language::JavaScript => {
self.typescript_parser
.parse(&file_path, source, typescript::JsVariant::JavaScript)
}
Language::Jsx => {
self.typescript_parser
.parse(&file_path, source, typescript::JsVariant::Jsx)
}
Language::Python => self.python_parser.parse(&file_path, source),
Language::Go => self.go_parser.parse(&file_path, source),
_ => {
Some(ParseResult {
file_path,
language: language.as_str().to_string(),
symbols: Vec::new(),
edges: Vec::new(),
module: None,
})
}
}
}
pub fn is_supported(&self, path: &Path) -> bool {
Self::is_supported_static(path)
}
pub fn is_supported_static(path: &Path) -> bool {
matches!(
Language::from_path(path),
Language::Rust
| Language::Solidity
| Language::TypeScript
| Language::Tsx
| Language::JavaScript
| Language::Jsx
| Language::Python
| Language::Go )
}
}
impl Default for CodeParser {
fn default() -> Self {
Self::new()
}
}
pub fn extract_brief(docstring: &str) -> Option<String> {
let trimmed = docstring.trim();
if trimmed.is_empty() {
return None;
}
let first_line = trimmed.lines().next()?;
let brief = first_line.trim();
if brief.ends_with('.') {
return Some(brief.to_string());
}
if let Some(idx) = brief.find(". ") {
return Some(brief[..=idx].to_string());
}
Some(brief.to_string())
}
pub fn truncate_context(s: &str, max_len: usize) -> String {
let s = s.trim();
if s.len() <= max_len {
s.to_string()
} else {
let target = max_len.saturating_sub(3);
let mut end = target;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
format!("{}...", &s[..end])
}
}
#[allow(dead_code)]
pub fn get_context_snippet(source: &str, line: usize, col: usize) -> Option<String> {
let lines: Vec<&str> = source.lines().collect();
if line == 0 || line > lines.len() {
return None;
}
let target_line = lines[line - 1];
let mut start = col.saturating_sub(20);
while start > 0 && !target_line.is_char_boundary(start) {
start -= 1;
}
let mut end = (col + 60).min(target_line.len());
while end < target_line.len() && !target_line.is_char_boundary(end) {
end += 1;
}
let snippet = &target_line[start..end];
Some(snippet.trim().to_string())
}
pub struct SymbolKindMapping {
pub prefix: &'static str,
pub kind: SymbolKind,
}
impl SymbolKindMapping {
pub const fn new(prefix: &'static str, kind: SymbolKind) -> Self {
Self { prefix, kind }
}
}
pub fn find_symbol_kind(capture_name: &str, mappings: &[SymbolKindMapping]) -> Option<SymbolKind> {
if !capture_name.ends_with(".name") {
return None;
}
let prefix = capture_name.trim_end_matches(".name");
mappings.iter().find(|m| m.prefix == prefix).map(|m| m.kind)
}
pub fn is_def_capture(capture_name: &str) -> bool {
capture_name.ends_with(".def")
}
pub struct CallCapturePatterns {
pub name_patterns: &'static [&'static str],
pub expr_patterns: &'static [&'static str],
}
impl CallCapturePatterns {
pub const STANDARD: CallCapturePatterns = CallCapturePatterns {
name_patterns: &["call.name", "method_call.name"],
expr_patterns: &["call.expr", "method_call.expr"],
};
pub const RUST: CallCapturePatterns = CallCapturePatterns {
name_patterns: &["call.name", "method_call.name", "scoped_call.name"],
expr_patterns: &["call.expr", "method_call.expr", "scoped_call.expr"],
};
pub const TYPESCRIPT: CallCapturePatterns = CallCapturePatterns {
name_patterns: &["call.name", "method_call.name", "new.name"],
expr_patterns: &["call.expr", "method_call.expr", "new.expr"],
};
}
pub fn extract_call_edges(
query: &Query,
root: &Node,
source: &str,
symbols: &[Symbol],
edges: &mut Vec<Edge>,
patterns: &CallCapturePatterns,
) {
let func_ranges: Vec<_> = symbols
.iter()
.filter(|s| matches!(s.kind, SymbolKind::Function | SymbolKind::Method))
.map(|s| (s.line_start, s.line_end, s.id.clone()))
.collect();
let mut cursor = QueryCursor::new();
let matches = cursor.matches(query, *root, source.as_bytes());
for m in matches {
let mut call_name: Option<&str> = None;
let mut call_node: Option<Node> = None;
for capture in m.captures {
let capture_name = &query.capture_names()[capture.index as usize];
let node = capture.node;
let text = node.utf8_text(source.as_bytes()).unwrap_or("");
if patterns.name_patterns.contains(&capture_name.as_str()) {
call_name = Some(text);
} else if patterns.expr_patterns.contains(&capture_name.as_str()) {
call_node = Some(node);
}
}
if let (Some(name), Some(node)) = (call_name, call_node) {
let line = node.start_position().row as u32 + 1;
let col = node.start_position().column as u32;
let source_id = func_ranges
.iter()
.find(|(start, end, _)| line >= *start && line <= *end)
.map(|(_, _, id)| id.clone());
if let Some(source_id) = source_id {
let context = node
.utf8_text(source.as_bytes())
.ok()
.map(|s| truncate_context(s, 80));
let target_id = if let Some(ctx) = &context {
symbols
.iter()
.find(|s| {
s.name == name
&& s.qualified_name
.as_ref()
.map(|qn| ctx.contains(qn))
.unwrap_or(false)
})
.map(|s| s.id.clone())
} else {
None
};
edges.push(Edge {
source_id,
target_id,
target_name: name.to_string(),
kind: EdgeKind::Calls,
line: Some(line),
col: Some(col),
context,
});
}
}
}
}
pub fn extract_module_name(file_path: &str, index_names: &[&str]) -> Option<String> {
let path = std::path::Path::new(file_path);
let stem = path.file_stem()?.to_str()?;
if index_names.contains(&stem) {
path.parent()?.file_name()?.to_str().map(String::from)
} else {
Some(stem.to_string())
}
}
pub fn parse_block_doc_comment(text: &str) -> String {
let content = text
.trim_start_matches("/**")
.trim_start_matches("/*!")
.trim_end_matches("*/");
content
.lines()
.map(|l| l.trim().trim_start_matches('*').trim())
.filter(|l| !l.is_empty())
.collect::<Vec<_>>()
.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_language_detection() {
assert_eq!(Language::from_path(Path::new("main.rs")), Language::Rust);
assert_eq!(
Language::from_path(Path::new("app.ts")),
Language::TypeScript
);
assert_eq!(Language::from_path(Path::new("App.tsx")), Language::Tsx);
assert_eq!(
Language::from_path(Path::new("script.js")),
Language::JavaScript
);
assert_eq!(Language::from_path(Path::new("Button.jsx")), Language::Jsx);
assert_eq!(Language::from_path(Path::new("main.py")), Language::Python);
assert_eq!(Language::from_path(Path::new("main.go")), Language::Go);
assert_eq!(
Language::from_path(Path::new("Token.sol")),
Language::Solidity
);
assert_eq!(
Language::from_path(Path::new("config.yaml")),
Language::Yaml
);
assert_eq!(Language::from_path(Path::new("ci.yml")), Language::Yaml);
assert_eq!(
Language::from_path(Path::new("data.json")),
Language::Unknown
);
}
#[test]
fn test_extract_brief() {
assert_eq!(
extract_brief("This is a brief.\nMore details here."),
Some("This is a brief.".to_string())
);
assert_eq!(
extract_brief("Single line"),
Some("Single line".to_string())
);
assert_eq!(extract_brief(""), None);
}
#[test]
fn test_truncate_context_ascii() {
assert_eq!(truncate_context("hello", 10), "hello");
assert_eq!(truncate_context("hello", 5), "hello");
assert_eq!(truncate_context("hello world", 8), "hello...");
assert_eq!(truncate_context(" hello world ", 8), "hello...");
}
#[test]
fn test_truncate_context_unicode() {
let box_line = "┌────────────────────────────────────────────────────────┐";
let result = truncate_context(box_line, 20);
assert!(result.ends_with("..."));
assert!(result.len() <= 20);
let emoji_str = "Hello 🎉🎊🎁 World";
let result = truncate_context(emoji_str, 12);
assert!(result.ends_with("..."));
let mixed = "console.log(\"├──────┤\")";
let result = truncate_context(mixed, 15);
assert!(result.ends_with("..."));
let chinese = "你好世界这是一个测试";
let result = truncate_context(chinese, 10);
assert!(result.ends_with("..."));
}
#[test]
fn test_truncate_context_edge_cases() {
assert_eq!(truncate_context("hello", 3), "...");
assert_eq!(truncate_context("hi", 3), "hi");
assert_eq!(truncate_context("", 10), "");
assert_eq!(truncate_context(" ", 10), "");
}
#[test]
fn test_get_context_snippet_unicode() {
let source = "line1\nconsole.log(\"┌────────────────────┐\")\nline3";
let result = get_context_snippet(source, 2, 15);
assert!(result.is_some());
}
}