Skip to main content

agentic_codebase/parse/
typescript.rs

1//! TypeScript and JavaScript parsing using tree-sitter.
2//!
3//! Extracts functions, classes, interfaces, type aliases, imports, methods.
4
5use std::path::Path;
6
7use crate::types::{AcbResult, CodeUnitType, Language, Visibility};
8
9use super::treesitter::{get_node_text, node_to_span};
10use super::{LanguageParser, RawCodeUnit, RawReference, ReferenceKind};
11
12/// TypeScript and JavaScript language parser.
13pub struct TypeScriptParser;
14
15impl Default for TypeScriptParser {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21impl TypeScriptParser {
22    /// Create a new TypeScript parser.
23    pub fn new() -> Self {
24        Self
25    }
26
27    fn detect_language(file_path: &Path) -> Language {
28        match file_path.extension().and_then(|e| e.to_str()) {
29            Some("ts") | Some("tsx") => Language::TypeScript,
30            Some("js") | Some("jsx") | Some("mjs") | Some("cjs") => Language::JavaScript,
31            _ => Language::TypeScript,
32        }
33    }
34
35    #[allow(clippy::too_many_arguments)]
36    fn extract_from_node(
37        &self,
38        node: tree_sitter::Node,
39        source: &str,
40        file_path: &Path,
41        units: &mut Vec<RawCodeUnit>,
42        next_id: &mut u64,
43        parent_qname: &str,
44        lang: Language,
45    ) {
46        let mut cursor = node.walk();
47        for child in node.children(&mut cursor) {
48            match child.kind() {
49                "function_declaration" => {
50                    if let Some(unit) =
51                        self.extract_function(child, source, file_path, parent_qname, next_id, lang)
52                    {
53                        units.push(unit);
54                    }
55                }
56                "class_declaration" => {
57                    if let Some(unit) =
58                        self.extract_class(child, source, file_path, parent_qname, next_id, lang)
59                    {
60                        let qname = unit.qualified_name.clone();
61                        units.push(unit);
62                        if let Some(body) = child.child_by_field_name("body") {
63                            self.extract_from_node(
64                                body, source, file_path, units, next_id, &qname, lang,
65                            );
66                        }
67                    }
68                }
69                "interface_declaration" => {
70                    if let Some(unit) = self.extract_interface(
71                        child,
72                        source,
73                        file_path,
74                        parent_qname,
75                        next_id,
76                        lang,
77                    ) {
78                        units.push(unit);
79                    }
80                }
81                "type_alias_declaration" => {
82                    if let Some(unit) = self.extract_type_alias(
83                        child,
84                        source,
85                        file_path,
86                        parent_qname,
87                        next_id,
88                        lang,
89                    ) {
90                        units.push(unit);
91                    }
92                }
93                "import_statement" => {
94                    if let Some(unit) =
95                        self.extract_import(child, source, file_path, parent_qname, next_id, lang)
96                    {
97                        units.push(unit);
98                    }
99                }
100                "export_statement" => {
101                    // Look inside export for the actual declaration
102                    self.extract_from_node(
103                        child,
104                        source,
105                        file_path,
106                        units,
107                        next_id,
108                        parent_qname,
109                        lang,
110                    );
111                }
112                "method_definition" => {
113                    if let Some(unit) =
114                        self.extract_method(child, source, file_path, parent_qname, next_id, lang)
115                    {
116                        units.push(unit);
117                    }
118                }
119                "lexical_declaration" | "variable_declaration" => {
120                    // Check for arrow function assignments: const foo = () => {}
121                    self.extract_arrow_functions(
122                        child,
123                        source,
124                        file_path,
125                        units,
126                        next_id,
127                        parent_qname,
128                        lang,
129                    );
130                }
131                _ => {}
132            }
133        }
134    }
135
136    fn extract_function(
137        &self,
138        node: tree_sitter::Node,
139        source: &str,
140        file_path: &Path,
141        parent_qname: &str,
142        next_id: &mut u64,
143        lang: Language,
144    ) -> Option<RawCodeUnit> {
145        let name_node = node.child_by_field_name("name")?;
146        let name = get_node_text(name_node, source).to_string();
147        let qname = ts_qname(parent_qname, &name);
148        let span = node_to_span(node);
149
150        let sig = node
151            .child_by_field_name("parameters")
152            .map(|p| get_node_text(p, source).to_string());
153        let is_async = get_node_text(node, source)
154            .trim_start()
155            .starts_with("async ");
156
157        let id = *next_id;
158        *next_id += 1;
159
160        let mut unit = RawCodeUnit::new(
161            CodeUnitType::Function,
162            lang,
163            name,
164            file_path.to_path_buf(),
165            span,
166        );
167        unit.temp_id = id;
168        unit.qualified_name = qname;
169        unit.signature = sig;
170        unit.is_async = is_async;
171        unit.visibility = Visibility::Public;
172
173        Some(unit)
174    }
175
176    fn extract_class(
177        &self,
178        node: tree_sitter::Node,
179        source: &str,
180        file_path: &Path,
181        parent_qname: &str,
182        next_id: &mut u64,
183        lang: Language,
184    ) -> Option<RawCodeUnit> {
185        let name_node = node.child_by_field_name("name")?;
186        let name = get_node_text(name_node, source).to_string();
187        let qname = ts_qname(parent_qname, &name);
188        let span = node_to_span(node);
189
190        let id = *next_id;
191        *next_id += 1;
192
193        let mut unit = RawCodeUnit::new(
194            CodeUnitType::Type,
195            lang,
196            name,
197            file_path.to_path_buf(),
198            span,
199        );
200        unit.temp_id = id;
201        unit.qualified_name = qname;
202        unit.visibility = Visibility::Public;
203
204        // Extract heritage (extends, implements)
205        let mut c = node.walk();
206        for child in node.children(&mut c) {
207            if child.kind() == "class_heritage" {
208                let heritage_text = get_node_text(child, source);
209                if heritage_text.contains("extends") || heritage_text.contains("implements") {
210                    unit.references.push(RawReference {
211                        name: heritage_text.trim().to_string(),
212                        kind: ReferenceKind::Inherit,
213                        span: node_to_span(child),
214                    });
215                }
216            }
217        }
218
219        Some(unit)
220    }
221
222    fn extract_interface(
223        &self,
224        node: tree_sitter::Node,
225        source: &str,
226        file_path: &Path,
227        parent_qname: &str,
228        next_id: &mut u64,
229        lang: Language,
230    ) -> Option<RawCodeUnit> {
231        let name_node = node.child_by_field_name("name")?;
232        let name = get_node_text(name_node, source).to_string();
233        let qname = ts_qname(parent_qname, &name);
234        let span = node_to_span(node);
235
236        let id = *next_id;
237        *next_id += 1;
238
239        let mut unit = RawCodeUnit::new(
240            CodeUnitType::Trait,
241            lang,
242            name,
243            file_path.to_path_buf(),
244            span,
245        );
246        unit.temp_id = id;
247        unit.qualified_name = qname;
248        unit.visibility = Visibility::Public;
249
250        Some(unit)
251    }
252
253    fn extract_type_alias(
254        &self,
255        node: tree_sitter::Node,
256        source: &str,
257        file_path: &Path,
258        parent_qname: &str,
259        next_id: &mut u64,
260        lang: Language,
261    ) -> Option<RawCodeUnit> {
262        let name_node = node.child_by_field_name("name")?;
263        let name = get_node_text(name_node, source).to_string();
264        let qname = ts_qname(parent_qname, &name);
265        let span = node_to_span(node);
266
267        let id = *next_id;
268        *next_id += 1;
269
270        let mut unit = RawCodeUnit::new(
271            CodeUnitType::Type,
272            lang,
273            name,
274            file_path.to_path_buf(),
275            span,
276        );
277        unit.temp_id = id;
278        unit.qualified_name = qname;
279        unit.visibility = Visibility::Public;
280
281        Some(unit)
282    }
283
284    fn extract_import(
285        &self,
286        node: tree_sitter::Node,
287        source: &str,
288        file_path: &Path,
289        parent_qname: &str,
290        next_id: &mut u64,
291        lang: Language,
292    ) -> Option<RawCodeUnit> {
293        let text = get_node_text(node, source).to_string();
294        let span = node_to_span(node);
295
296        // Extract module name from import statement
297        let import_name = text
298            .split("from")
299            .last()
300            .unwrap_or(&text)
301            .trim()
302            .trim_matches(|c: char| c == '\'' || c == '"' || c == ';')
303            .to_string();
304
305        let id = *next_id;
306        *next_id += 1;
307
308        let mut unit = RawCodeUnit::new(
309            CodeUnitType::Import,
310            lang,
311            import_name.clone(),
312            file_path.to_path_buf(),
313            span,
314        );
315        unit.temp_id = id;
316        unit.qualified_name = ts_qname(parent_qname, &import_name);
317        unit.references.push(RawReference {
318            name: import_name,
319            kind: ReferenceKind::Import,
320            span,
321        });
322
323        Some(unit)
324    }
325
326    fn extract_method(
327        &self,
328        node: tree_sitter::Node,
329        source: &str,
330        file_path: &Path,
331        parent_qname: &str,
332        next_id: &mut u64,
333        lang: Language,
334    ) -> Option<RawCodeUnit> {
335        let name_node = node.child_by_field_name("name")?;
336        let name = get_node_text(name_node, source).to_string();
337        let qname = ts_qname(parent_qname, &name);
338        let span = node_to_span(node);
339
340        let is_async = get_node_text(node, source)
341            .trim_start()
342            .starts_with("async ");
343
344        let id = *next_id;
345        *next_id += 1;
346
347        let mut unit = RawCodeUnit::new(
348            CodeUnitType::Function,
349            lang,
350            name,
351            file_path.to_path_buf(),
352            span,
353        );
354        unit.temp_id = id;
355        unit.qualified_name = qname;
356        unit.is_async = is_async;
357        unit.visibility = Visibility::Public;
358
359        Some(unit)
360    }
361
362    #[allow(clippy::too_many_arguments)]
363    fn extract_arrow_functions(
364        &self,
365        node: tree_sitter::Node,
366        source: &str,
367        file_path: &Path,
368        units: &mut Vec<RawCodeUnit>,
369        next_id: &mut u64,
370        parent_qname: &str,
371        lang: Language,
372    ) {
373        let mut cursor = node.walk();
374        for child in node.children(&mut cursor) {
375            if child.kind() == "variable_declarator" {
376                let name_node = child.child_by_field_name("name");
377                let value_node = child.child_by_field_name("value");
378                if let (Some(name_n), Some(val_n)) = (name_node, value_node) {
379                    if val_n.kind() == "arrow_function" {
380                        let name = get_node_text(name_n, source).to_string();
381                        let qname = ts_qname(parent_qname, &name);
382                        let span = node_to_span(child);
383
384                        let id = *next_id;
385                        *next_id += 1;
386
387                        let mut unit = RawCodeUnit::new(
388                            CodeUnitType::Function,
389                            lang,
390                            name,
391                            file_path.to_path_buf(),
392                            span,
393                        );
394                        unit.temp_id = id;
395                        unit.qualified_name = qname;
396                        unit.visibility = Visibility::Public;
397                        units.push(unit);
398                    }
399                }
400            }
401        }
402    }
403}
404
405impl LanguageParser for TypeScriptParser {
406    fn extract_units(
407        &self,
408        tree: &tree_sitter::Tree,
409        source: &str,
410        file_path: &Path,
411    ) -> AcbResult<Vec<RawCodeUnit>> {
412        let lang = Self::detect_language(file_path);
413        let mut units = Vec::new();
414        let mut next_id = 0u64;
415
416        let module_name = file_path
417            .file_stem()
418            .and_then(|s| s.to_str())
419            .unwrap_or("unknown")
420            .to_string();
421
422        let root_span = node_to_span(tree.root_node());
423        let mut module_unit = RawCodeUnit::new(
424            CodeUnitType::Module,
425            lang,
426            module_name.clone(),
427            file_path.to_path_buf(),
428            root_span,
429        );
430        module_unit.temp_id = next_id;
431        module_unit.qualified_name = module_name.clone();
432        next_id += 1;
433        units.push(module_unit);
434
435        self.extract_from_node(
436            tree.root_node(),
437            source,
438            file_path,
439            &mut units,
440            &mut next_id,
441            &module_name,
442            lang,
443        );
444
445        Ok(units)
446    }
447
448    fn is_test_file(&self, path: &Path, source: &str) -> bool {
449        let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
450        name.ends_with(".test.ts")
451            || name.ends_with(".test.tsx")
452            || name.ends_with(".spec.ts")
453            || name.ends_with(".spec.tsx")
454            || name.ends_with(".test.js")
455            || name.ends_with(".spec.js")
456            || path.components().any(|c| {
457                let s = c.as_os_str().to_str().unwrap_or("");
458                s == "__tests__" || s == "tests" || s == "test"
459            })
460            || source.contains("describe(")
461            || source.contains("it(")
462    }
463}
464
465fn ts_qname(parent: &str, name: &str) -> String {
466    if parent.is_empty() {
467        name.to_string()
468    } else {
469        format!("{}.{}", parent, name)
470    }
471}