1use 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
12pub struct TypeScriptParser;
14
15impl Default for TypeScriptParser {
16 fn default() -> Self {
17 Self::new()
18 }
19}
20
21impl TypeScriptParser {
22 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 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 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 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 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}