1mod go;
7mod python;
8mod rust;
9mod solidity;
10mod typescript;
11
12use std::path::Path;
13
14use tree_sitter::{Node, Query, QueryCursor};
15
16use crate::db::{Edge, EdgeKind, ParseResult, Symbol, SymbolKind};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Language {
21 Rust,
22 TypeScript,
23 Tsx,
24 JavaScript,
25 Jsx,
26 Python,
27 Go,
28 Solidity,
29 Yaml,
30 Unknown,
31}
32
33impl Language {
34 pub fn from_path(path: &Path) -> Self {
36 match path.extension().and_then(|e| e.to_str()) {
37 Some("rs") => Language::Rust,
38 Some("ts") => Language::TypeScript,
39 Some("tsx") => Language::Tsx,
40 Some("js") | Some("mjs") | Some("cjs") => Language::JavaScript,
41 Some("jsx") => Language::Jsx,
42 Some("py") | Some("pyi") => Language::Python,
43 Some("go") => Language::Go,
44 Some("sol") => Language::Solidity,
45 Some("yaml") | Some("yml") => Language::Yaml,
46 _ => Language::Unknown,
47 }
48 }
49
50 pub fn as_str(&self) -> &'static str {
52 match self {
53 Language::Rust => "rust",
54 Language::TypeScript => "typescript",
55 Language::Tsx => "tsx",
56 Language::JavaScript => "javascript",
57 Language::Jsx => "jsx",
58 Language::Python => "python",
59 Language::Go => "go",
60 Language::Solidity => "solidity",
61 Language::Yaml => "yaml",
62 Language::Unknown => "unknown",
63 }
64 }
65}
66
67pub struct CodeParser {
69 go_parser: go::GoParser,
70 python_parser: python::PythonParser,
71 rust_parser: rust::RustParser,
72 solidity_parser: solidity::SolidityParser,
73 typescript_parser: typescript::TypeScriptParser,
74}
75
76impl CodeParser {
77 pub fn new() -> Self {
79 Self {
80 go_parser: go::GoParser::new(),
81 python_parser: python::PythonParser::new(),
82 rust_parser: rust::RustParser::new(),
83 solidity_parser: solidity::SolidityParser::new(),
84 typescript_parser: typescript::TypeScriptParser::new(),
85 }
86 }
87
88 pub fn parse(&mut self, path: &Path, source: &str) -> Option<ParseResult> {
90 let language = Language::from_path(path);
91 let file_path = path.to_string_lossy().to_string();
92
93 match language {
94 Language::Rust => self.rust_parser.parse(&file_path, source),
95 Language::Solidity => self.solidity_parser.parse(&file_path, source),
96 Language::TypeScript => {
97 self.typescript_parser
98 .parse(&file_path, source, typescript::JsVariant::TypeScript)
99 }
100 Language::Tsx => {
101 self.typescript_parser
102 .parse(&file_path, source, typescript::JsVariant::Tsx)
103 }
104 Language::JavaScript => {
105 self.typescript_parser
106 .parse(&file_path, source, typescript::JsVariant::JavaScript)
107 }
108 Language::Jsx => {
109 self.typescript_parser
110 .parse(&file_path, source, typescript::JsVariant::Jsx)
111 }
112 Language::Python => self.python_parser.parse(&file_path, source),
113 Language::Go => self.go_parser.parse(&file_path, source),
114 _ => {
115 Some(ParseResult {
117 file_path,
118 language: language.as_str().to_string(),
119 symbols: Vec::new(),
120 edges: Vec::new(),
121 module: None,
122 })
123 }
124 }
125 }
126
127 pub fn is_supported(&self, path: &Path) -> bool {
130 Self::is_supported_static(path)
131 }
132
133 pub fn is_supported_static(path: &Path) -> bool {
135 matches!(
136 Language::from_path(path),
137 Language::Rust
138 | Language::Solidity
139 | Language::TypeScript
140 | Language::Tsx
141 | Language::JavaScript
142 | Language::Jsx
143 | Language::Python
144 | Language::Go )
146 }
147}
148
149impl Default for CodeParser {
150 fn default() -> Self {
151 Self::new()
152 }
153}
154
155pub fn extract_brief(docstring: &str) -> Option<String> {
157 let trimmed = docstring.trim();
158 if trimmed.is_empty() {
159 return None;
160 }
161
162 let first_line = trimmed.lines().next()?;
164 let brief = first_line.trim();
165
166 if brief.ends_with('.') {
168 return Some(brief.to_string());
169 }
170
171 if let Some(idx) = brief.find(". ") {
173 return Some(brief[..=idx].to_string());
174 }
175
176 Some(brief.to_string())
177}
178
179pub fn truncate_context(s: &str, max_len: usize) -> String {
181 let s = s.trim();
182 if s.len() <= max_len {
183 s.to_string()
184 } else {
185 let target = max_len.saturating_sub(3);
187 let mut end = target;
188 while end > 0 && !s.is_char_boundary(end) {
189 end -= 1;
190 }
191 format!("{}...", &s[..end])
192 }
193}
194
195#[allow(dead_code)]
197pub fn get_context_snippet(source: &str, line: usize, col: usize) -> Option<String> {
198 let lines: Vec<&str> = source.lines().collect();
199 if line == 0 || line > lines.len() {
200 return None;
201 }
202
203 let target_line = lines[line - 1];
204
205 let mut start = col.saturating_sub(20);
208 while start > 0 && !target_line.is_char_boundary(start) {
209 start -= 1;
210 }
211 let mut end = (col + 60).min(target_line.len());
212 while end < target_line.len() && !target_line.is_char_boundary(end) {
213 end += 1;
214 }
215
216 let snippet = &target_line[start..end];
217 Some(snippet.trim().to_string())
218}
219
220pub struct SymbolKindMapping {
225 pub prefix: &'static str,
227 pub kind: SymbolKind,
229}
230
231impl SymbolKindMapping {
232 pub const fn new(prefix: &'static str, kind: SymbolKind) -> Self {
234 Self { prefix, kind }
235 }
236}
237
238pub fn find_symbol_kind(capture_name: &str, mappings: &[SymbolKindMapping]) -> Option<SymbolKind> {
243 if !capture_name.ends_with(".name") {
244 return None;
245 }
246 let prefix = capture_name.trim_end_matches(".name");
247 mappings.iter().find(|m| m.prefix == prefix).map(|m| m.kind)
248}
249
250pub fn is_def_capture(capture_name: &str) -> bool {
252 capture_name.ends_with(".def")
253}
254
255pub struct CallCapturePatterns {
258 pub name_patterns: &'static [&'static str],
260 pub expr_patterns: &'static [&'static str],
262}
263
264impl CallCapturePatterns {
265 pub const STANDARD: CallCapturePatterns = CallCapturePatterns {
267 name_patterns: &["call.name", "method_call.name"],
268 expr_patterns: &["call.expr", "method_call.expr"],
269 };
270
271 pub const RUST: CallCapturePatterns = CallCapturePatterns {
273 name_patterns: &["call.name", "method_call.name", "scoped_call.name"],
274 expr_patterns: &["call.expr", "method_call.expr", "scoped_call.expr"],
275 };
276
277 pub const TYPESCRIPT: CallCapturePatterns = CallCapturePatterns {
279 name_patterns: &["call.name", "method_call.name", "new.name"],
280 expr_patterns: &["call.expr", "method_call.expr", "new.expr"],
281 };
282}
283
284pub fn extract_call_edges(
290 query: &Query,
291 root: &Node,
292 source: &str,
293 symbols: &[Symbol],
294 edges: &mut Vec<Edge>,
295 patterns: &CallCapturePatterns,
296) {
297 let func_ranges: Vec<_> = symbols
299 .iter()
300 .filter(|s| matches!(s.kind, SymbolKind::Function | SymbolKind::Method))
301 .map(|s| (s.line_start, s.line_end, s.id.clone()))
302 .collect();
303
304 let mut cursor = QueryCursor::new();
305 let matches = cursor.matches(query, *root, source.as_bytes());
306
307 for m in matches {
308 let mut call_name: Option<&str> = None;
309 let mut call_node: Option<Node> = None;
310
311 for capture in m.captures {
312 let capture_name = &query.capture_names()[capture.index as usize];
313 let node = capture.node;
314 let text = node.utf8_text(source.as_bytes()).unwrap_or("");
315
316 if patterns.name_patterns.contains(&capture_name.as_str()) {
317 call_name = Some(text);
318 } else if patterns.expr_patterns.contains(&capture_name.as_str()) {
319 call_node = Some(node);
320 }
321 }
322
323 if let (Some(name), Some(node)) = (call_name, call_node) {
324 let line = node.start_position().row as u32 + 1;
325 let col = node.start_position().column as u32;
326
327 let source_id = func_ranges
329 .iter()
330 .find(|(start, end, _)| line >= *start && line <= *end)
331 .map(|(_, _, id)| id.clone());
332
333 if let Some(source_id) = source_id {
334 let context = node
335 .utf8_text(source.as_bytes())
336 .ok()
337 .map(|s| truncate_context(s, 80));
338
339 let target_id = if let Some(ctx) = &context {
348 symbols
349 .iter()
350 .find(|s| {
351 s.name == name
352 && s.qualified_name
353 .as_ref()
354 .map(|qn| ctx.contains(qn))
355 .unwrap_or(false)
356 })
357 .map(|s| s.id.clone())
358 } else {
359 None
360 };
361
362 edges.push(Edge {
363 source_id,
364 target_id,
365 target_name: name.to_string(),
366 kind: EdgeKind::Calls,
367 line: Some(line),
368 col: Some(col),
369 context,
370 });
371 }
372 }
373 }
374}
375
376pub fn extract_module_name(file_path: &str, index_names: &[&str]) -> Option<String> {
382 let path = std::path::Path::new(file_path);
383 let stem = path.file_stem()?.to_str()?;
384
385 if index_names.contains(&stem) {
386 path.parent()?.file_name()?.to_str().map(String::from)
388 } else {
389 Some(stem.to_string())
390 }
391}
392
393pub fn parse_block_doc_comment(text: &str) -> String {
398 let content = text
400 .trim_start_matches("/**")
401 .trim_start_matches("/*!")
402 .trim_end_matches("*/");
403
404 content
406 .lines()
407 .map(|l| l.trim().trim_start_matches('*').trim())
408 .filter(|l| !l.is_empty())
409 .collect::<Vec<_>>()
410 .join("\n")
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416
417 #[test]
418 fn test_language_detection() {
419 assert_eq!(Language::from_path(Path::new("main.rs")), Language::Rust);
420 assert_eq!(
421 Language::from_path(Path::new("app.ts")),
422 Language::TypeScript
423 );
424 assert_eq!(Language::from_path(Path::new("App.tsx")), Language::Tsx);
425 assert_eq!(
426 Language::from_path(Path::new("script.js")),
427 Language::JavaScript
428 );
429 assert_eq!(Language::from_path(Path::new("Button.jsx")), Language::Jsx);
430 assert_eq!(Language::from_path(Path::new("main.py")), Language::Python);
431 assert_eq!(Language::from_path(Path::new("main.go")), Language::Go);
432 assert_eq!(
433 Language::from_path(Path::new("Token.sol")),
434 Language::Solidity
435 );
436 assert_eq!(
437 Language::from_path(Path::new("config.yaml")),
438 Language::Yaml
439 );
440 assert_eq!(Language::from_path(Path::new("ci.yml")), Language::Yaml);
441 assert_eq!(
442 Language::from_path(Path::new("data.json")),
443 Language::Unknown
444 );
445 }
446
447 #[test]
448 fn test_extract_brief() {
449 assert_eq!(
451 extract_brief("This is a brief.\nMore details here."),
452 Some("This is a brief.".to_string())
453 );
454 assert_eq!(
455 extract_brief("Single line"),
456 Some("Single line".to_string())
457 );
458 assert_eq!(extract_brief(""), None);
459 }
460
461 #[test]
462 fn test_truncate_context_ascii() {
463 assert_eq!(truncate_context("hello", 10), "hello");
465
466 assert_eq!(truncate_context("hello", 5), "hello");
468
469 assert_eq!(truncate_context("hello world", 8), "hello...");
471
472 assert_eq!(truncate_context(" hello world ", 8), "hello...");
474 }
475
476 #[test]
477 fn test_truncate_context_unicode() {
478 let box_line = "┌────────────────────────────────────────────────────────┐";
480
481 let result = truncate_context(box_line, 20);
483 assert!(result.ends_with("..."));
484 assert!(result.len() <= 20);
485
486 let emoji_str = "Hello 🎉🎊🎁 World";
488 let result = truncate_context(emoji_str, 12);
489 assert!(result.ends_with("..."));
490
491 let mixed = "console.log(\"├──────┤\")";
493 let result = truncate_context(mixed, 15);
494 assert!(result.ends_with("..."));
495
496 let chinese = "你好世界这是一个测试";
498 let result = truncate_context(chinese, 10);
499 assert!(result.ends_with("..."));
500 }
501
502 #[test]
503 fn test_truncate_context_edge_cases() {
504 assert_eq!(truncate_context("hello", 3), "...");
506 assert_eq!(truncate_context("hi", 3), "hi");
507
508 assert_eq!(truncate_context("", 10), "");
510
511 assert_eq!(truncate_context(" ", 10), "");
513 }
514
515 #[test]
516 fn test_get_context_snippet_unicode() {
517 let source = "line1\nconsole.log(\"┌────────────────────┐\")\nline3";
519
520 let result = get_context_snippet(source, 2, 15);
522 assert!(result.is_some());
523 }
524}