1use std::collections::hash_map::DefaultHasher;
2use std::hash::{Hash, Hasher};
3
4use cha_core::{ClassInfo, FunctionInfo, ImportInfo, SourceFile, SourceModel};
5use tree_sitter::{Node, Parser};
6
7use crate::LanguageParser;
8
9pub struct TypeScriptParser;
10
11impl LanguageParser for TypeScriptParser {
12 fn language_name(&self) -> &str {
13 "typescript"
14 }
15
16 fn parse(&self, file: &SourceFile) -> Option<SourceModel> {
17 let mut parser = Parser::new();
18 parser
19 .set_language(&tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into())
20 .ok()?;
21 let tree = parser.parse(&file.content, None)?;
22 let root = tree.root_node();
23 let src = file.content.as_bytes();
24
25 let mut ctx = ParseContext::new(src);
26 ctx.collect_nodes(root, false);
27
28 Some(SourceModel {
29 language: "typescript".into(),
30 total_lines: file.line_count(),
31 functions: ctx.col.functions,
32 classes: ctx.col.classes,
33 imports: ctx.col.imports,
34 comments: collect_comments(root, src),
35 type_aliases: vec![], })
37 }
38}
39
40struct Collector {
42 functions: Vec<FunctionInfo>,
43 classes: Vec<ClassInfo>,
44 imports: Vec<ImportInfo>,
45}
46
47struct ParseContext<'a> {
49 src: &'a [u8],
50 col: Collector,
51}
52
53impl<'a> ParseContext<'a> {
54 fn new(src: &'a [u8]) -> Self {
55 Self {
56 src,
57 col: Collector {
58 functions: Vec::new(),
59 classes: Vec::new(),
60 imports: Vec::new(),
61 },
62 }
63 }
64
65 fn collect_nodes(&mut self, node: Node, exported: bool) {
66 let mut cursor = node.walk();
67 for child in node.children(&mut cursor) {
68 self.collect_single_node(child, exported);
69 }
70 }
71
72 fn collect_single_node(&mut self, child: Node, exported: bool) {
73 match child.kind() {
74 "export_statement" => self.collect_nodes(child, true),
75 "function_declaration" | "method_definition" => self.push_function(child, exported),
76 "lexical_declaration" | "variable_declaration" => {
77 extract_arrow_functions(child, self.src, exported, &mut self.col.functions);
78 self.collect_nodes(child, exported);
79 }
80 "class_declaration" => self.push_class(child, exported),
81 "import_statement" => self.push_import(child),
82 _ => self.collect_nodes(child, false),
83 }
84 }
85
86 fn push_function(&mut self, node: Node, exported: bool) {
87 if let Some(mut f) = extract_function(node, self.src) {
88 f.is_exported = exported;
89 self.col.functions.push(f);
90 }
91 }
92
93 fn push_class(&mut self, node: Node, exported: bool) {
94 if let Some(mut c) = extract_class(node, self.src) {
95 c.is_exported = exported;
96 self.col.classes.push(c);
97 }
98 }
99
100 fn push_import(&mut self, node: Node) {
101 if let Some(i) = extract_import(node, self.src) {
102 self.col.imports.push(i);
103 }
104 }
105}
106
107fn node_text<'a>(node: Node, src: &'a [u8]) -> &'a str {
108 node.utf8_text(src).unwrap_or("")
109}
110
111fn hash_ast_structure(node: Node) -> u64 {
113 let mut hasher = DefaultHasher::new();
114 walk_hash(node, &mut hasher);
115 hasher.finish()
116}
117
118fn walk_hash(node: Node, hasher: &mut DefaultHasher) {
119 node.kind().hash(hasher);
120 let mut cursor = node.walk();
121 for child in node.children(&mut cursor) {
122 walk_hash(child, hasher);
123 }
124}
125
126fn extract_function(node: Node, src: &[u8]) -> Option<FunctionInfo> {
127 let name_node = node.child_by_field_name("name")?;
128 let name = node_text(name_node, src).to_string();
129 let start_line = node.start_position().row + 1;
130 let end_line = node.end_position().row + 1;
131 let body = node.child_by_field_name("body");
132 let body_hash = body.map(hash_ast_structure);
133 let parameter_count = count_parameters(node);
134 let parameter_types = extract_param_types(node, src);
135 let chain_depth = body.map(max_chain_depth).unwrap_or(0);
136 let switch_arms = body.map(count_switch_arms).unwrap_or(0);
137 let external_refs = body
138 .map(|b| collect_external_refs(b, src))
139 .unwrap_or_default();
140 let is_delegating = body.map(|b| check_delegating(b, src)).unwrap_or(false);
141 Some(FunctionInfo {
142 name,
143 start_line,
144 end_line,
145 line_count: end_line - start_line + 1,
146 complexity: count_complexity(node),
147 body_hash,
148 is_exported: false,
149 parameter_count,
150 parameter_types,
151 chain_depth,
152 switch_arms,
153 external_refs,
154 is_delegating,
155 comment_lines: count_comment_lines(node),
156 referenced_fields: collect_this_fields(body, src),
157 null_check_fields: collect_null_checks_ts(body, src),
158 switch_dispatch_target: extract_switch_target_ts(body, src),
159 optional_param_count: count_optional_params_ts(node, src),
160 called_functions: collect_calls_ts(body, src),
161 cognitive_complexity: body.map(cognitive_complexity_ts).unwrap_or(0),
162 })
163}
164
165fn extract_arrow_functions(
166 node: Node,
167 src: &[u8],
168 exported: bool,
169 functions: &mut Vec<FunctionInfo>,
170) {
171 let mut cursor = node.walk();
172 for child in node.children(&mut cursor) {
173 if child.kind() == "variable_declarator"
174 && let Some(f) = try_extract_arrow(child, node, src, exported)
175 {
176 functions.push(f);
177 }
178 }
179}
180
181fn try_extract_arrow(child: Node, decl: Node, src: &[u8], exported: bool) -> Option<FunctionInfo> {
183 let name = child
184 .child_by_field_name("name")
185 .map(|n| node_text(n, src).to_string())?;
186 let value = child.child_by_field_name("value")?;
187 if value.kind() != "arrow_function" {
188 return None;
189 }
190 let start_line = decl.start_position().row + 1;
191 let end_line = decl.end_position().row + 1;
192 let body = value.child_by_field_name("body");
193 let body_hash = body.map(hash_ast_structure);
194 Some(FunctionInfo {
195 name,
196 start_line,
197 end_line,
198 line_count: end_line - start_line + 1,
199 complexity: count_complexity(value),
200 body_hash,
201 is_exported: exported,
202 parameter_count: count_parameters(value),
203 parameter_types: extract_param_types(value, src),
204 chain_depth: body.map(max_chain_depth).unwrap_or(0),
205 switch_arms: body.map(count_switch_arms).unwrap_or(0),
206 external_refs: body
207 .map(|b| collect_external_refs(b, src))
208 .unwrap_or_default(),
209 is_delegating: body.map(|b| check_delegating(b, src)).unwrap_or(false),
210 comment_lines: count_comment_lines(value),
211 referenced_fields: collect_this_fields(body, src),
212 null_check_fields: collect_null_checks_ts(body, src),
213 switch_dispatch_target: extract_switch_target_ts(body, src),
214 optional_param_count: count_optional_params_ts(value, src),
215 called_functions: collect_calls_ts(Some(value), src),
216 cognitive_complexity: cognitive_complexity_ts(value),
217 })
218}
219
220fn extract_class(node: Node, src: &[u8]) -> Option<ClassInfo> {
221 let name_node = node.child_by_field_name("name")?;
222 let name = node_text(name_node, src).to_string();
223 let start_line = node.start_position().row + 1;
224 let end_line = node.end_position().row + 1;
225 let body = node.child_by_field_name("body")?;
226 let (methods, delegating, fields, has_behavior, cb_fields) = scan_class_body(body, src);
227 let is_interface =
228 node.kind() == "interface_declaration" || node.kind() == "abstract_class_declaration";
229 let has_listener_field = !cb_fields.is_empty();
230 let has_notify_method = has_iterate_and_call_ts(body, src, &cb_fields);
231
232 Some(ClassInfo {
233 name,
234 start_line,
235 end_line,
236 method_count: methods,
237 line_count: end_line - start_line + 1,
238 is_exported: false,
239 delegating_method_count: delegating,
240 field_count: fields.len(),
241 field_names: fields,
242 field_types: Vec::new(),
243 has_behavior,
244 is_interface,
245 parent_name: extract_parent_name(node, src),
246 override_count: 0,
247 self_call_count: 0,
248 has_listener_field,
249 has_notify_method,
250 })
251}
252
253fn scan_class_body(body: Node, src: &[u8]) -> (usize, usize, Vec<String>, bool, Vec<String>) {
255 let mut methods = 0;
256 let mut delegating = 0;
257 let mut fields = Vec::new();
258 let mut callback_fields = Vec::new();
259 let mut has_behavior = false;
260 let mut cursor = body.walk();
261 for child in body.children(&mut cursor) {
262 match child.kind() {
263 "method_definition" => {
264 let (is_behavior, is_delegating) = classify_method(child, src);
265 methods += 1;
266 has_behavior |= is_behavior;
267 delegating += usize::from(is_delegating);
268 }
269 "public_field_definition" | "property_definition" => {
270 if let Some(n) = child.child_by_field_name("name") {
271 let name = node_text(n, src).to_string();
272 if is_callback_collection_type_ts(child, src) {
273 callback_fields.push(name.clone());
274 }
275 fields.push(name);
276 }
277 }
278 _ => {}
279 }
280 }
281 (methods, delegating, fields, has_behavior, callback_fields)
282}
283
284fn classify_method(node: Node, src: &[u8]) -> (bool, bool) {
286 let mname = node
287 .child_by_field_name("name")
288 .map(|n| node_text(n, src))
289 .unwrap_or("");
290 let is_behavior = !is_accessor_name(mname) && mname != "constructor";
291 let is_delegating = node
292 .child_by_field_name("body")
293 .is_some_and(|b| check_delegating(b, src));
294 (is_behavior, is_delegating)
295}
296
297fn count_parameters(node: Node) -> usize {
298 let params = match node.child_by_field_name("parameters") {
299 Some(p) => p,
300 None => return 0,
301 };
302 let mut cursor = params.walk();
303 params
304 .children(&mut cursor)
305 .filter(|c| {
306 matches!(
307 c.kind(),
308 "required_parameter" | "optional_parameter" | "rest_parameter"
309 )
310 })
311 .count()
312}
313
314fn extract_param_types(node: Node, src: &[u8]) -> Vec<String> {
315 let params = match node.child_by_field_name("parameters") {
316 Some(p) => p,
317 None => return vec![],
318 };
319 let mut types = Vec::new();
320 let mut cursor = params.walk();
321 for child in params.children(&mut cursor) {
322 if let Some(ann) = child.child_by_field_name("type") {
323 types.push(node_text(ann, src).to_string());
324 }
325 }
326 types.sort();
327 types
328}
329
330fn max_chain_depth(node: Node) -> usize {
331 let mut max = 0;
332 walk_chain_depth(node, &mut max);
333 max
334}
335
336fn walk_chain_depth(node: Node, max: &mut usize) {
337 if node.kind() == "member_expression" {
338 let depth = measure_chain(node);
339 if depth > *max {
340 *max = depth;
341 }
342 }
343 let mut cursor = node.walk();
344 for child in node.children(&mut cursor) {
345 walk_chain_depth(child, max);
346 }
347}
348
349fn measure_chain(node: Node) -> usize {
351 let mut depth = 0;
352 let mut current = node;
353 while current.kind() == "member_expression" {
354 depth += 1;
355 if let Some(obj) = current.child_by_field_name("object") {
356 current = obj;
357 } else {
358 break;
359 }
360 }
361 depth
362}
363
364fn count_switch_arms(node: Node) -> usize {
365 let mut count = 0;
366 walk_switch_arms(node, &mut count);
367 count
368}
369
370fn walk_switch_arms(node: Node, count: &mut usize) {
371 if node.kind() == "switch_case" || node.kind() == "switch_default" {
372 *count += 1;
373 }
374 let mut cursor = node.walk();
375 for child in node.children(&mut cursor) {
376 walk_switch_arms(child, count);
377 }
378}
379
380fn collect_external_refs(node: Node, src: &[u8]) -> Vec<String> {
381 let mut refs = Vec::new();
382 walk_external_refs(node, src, &mut refs);
383 refs.sort();
384 refs.dedup();
385 refs
386}
387
388fn member_chain_root(node: Node) -> Node {
389 let mut current = node;
390 while current.kind() == "member_expression" {
391 match current.child_by_field_name("object") {
392 Some(child) => current = child,
393 None => break,
394 }
395 }
396 current
397}
398
399fn walk_external_refs(node: Node, src: &[u8], refs: &mut Vec<String>) {
400 if node.kind() == "member_expression" {
401 let root = member_chain_root(node);
402 let text = node_text(root, src);
403 if text != "this" && text != "self" && !text.is_empty() {
404 refs.push(text.to_string());
405 }
406 }
407 let mut cursor = node.walk();
408 for child in node.children(&mut cursor) {
409 walk_external_refs(child, src, refs);
410 }
411}
412
413fn single_stmt(body: Node) -> Option<Node> {
414 let mut cursor = body.walk();
415 let stmts: Vec<_> = body
416 .children(&mut cursor)
417 .filter(|c| c.kind() != "{" && c.kind() != "}")
418 .collect();
419 (stmts.len() == 1).then(|| stmts[0])
420}
421
422fn is_external_call(node: Node, src: &[u8]) -> bool {
423 node.kind() == "call_expression"
424 && node.child_by_field_name("function").is_some_and(|func| {
425 func.kind() == "member_expression"
426 && func
427 .child_by_field_name("object")
428 .is_some_and(|obj| node_text(obj, src) != "this")
429 })
430}
431
432fn check_delegating(body: Node, src: &[u8]) -> bool {
433 let Some(stmt) = single_stmt(body) else {
434 return false;
435 };
436 let expr = match stmt.kind() {
437 "return_statement" => stmt.child(1).unwrap_or(stmt),
438 "expression_statement" => stmt.child(0).unwrap_or(stmt),
439 _ => stmt,
440 };
441 is_external_call(expr, src)
442}
443
444fn count_complexity(node: Node) -> usize {
445 let mut complexity = 1;
446 walk_complexity(node, &mut complexity);
447 complexity
448}
449
450fn walk_complexity(node: Node, count: &mut usize) {
451 match node.kind() {
452 "if_statement" | "else_clause" | "for_statement" | "for_in_statement"
453 | "while_statement" | "do_statement" | "switch_case" | "catch_clause"
454 | "ternary_expression" => {
455 *count += 1;
456 }
457 "binary_expression" => {
458 let mut cursor = node.walk();
459 for child in node.children(&mut cursor) {
460 if child.kind() == "&&" || child.kind() == "||" {
461 *count += 1;
462 }
463 }
464 }
465 _ => {}
466 }
467 let mut cursor = node.walk();
468 for child in node.children(&mut cursor) {
469 walk_complexity(child, count);
470 }
471}
472
473fn extract_import(node: Node, src: &[u8]) -> Option<ImportInfo> {
474 let mut cursor = node.walk();
475 for child in node.children(&mut cursor) {
476 if child.kind() == "string" {
477 let raw = node_text(child, src);
478 let source = raw.trim_matches(|c| c == '\'' || c == '"').to_string();
479 return Some(ImportInfo {
480 source,
481 line: node.start_position().row + 1,
482 ..Default::default()
483 });
484 }
485 }
486 None
487}
488
489fn count_comment_lines(node: Node) -> usize {
491 let mut count = 0;
492 let mut cursor = node.walk();
493 for child in node.children(&mut cursor) {
494 if child.kind() == "comment" {
495 count += child.end_position().row - child.start_position().row + 1;
496 } else if child.child_count() > 0 {
497 count += count_comment_lines(child);
498 }
499 }
500 count
501}
502
503fn collect_this_fields(body: Option<Node>, src: &[u8]) -> Vec<String> {
506 let Some(body) = body else { return vec![] };
507 let mut refs = Vec::new();
508 collect_this_refs(body, src, &mut refs);
509 refs.sort();
510 refs.dedup();
511 refs
512}
513
514fn collect_this_refs(node: Node, src: &[u8], refs: &mut Vec<String>) {
515 if node.kind() == "member_expression"
516 && let Some(obj) = node.child_by_field_name("object")
517 && node_text(obj, src) == "this"
518 && let Some(prop) = node.child_by_field_name("property")
519 {
520 refs.push(node_text(prop, src).to_string());
521 }
522 let mut cursor = node.walk();
523 for child in node.children(&mut cursor) {
524 collect_this_refs(child, src, refs);
525 }
526}
527
528fn is_accessor_name(name: &str) -> bool {
530 let lower = name.to_lowercase();
531 lower.starts_with("get") || lower.starts_with("set") || lower.starts_with("is")
532}
533
534fn extract_parent_name(node: Node, src: &[u8]) -> Option<String> {
537 let mut cursor = node.walk();
538 for child in node.children(&mut cursor) {
539 if child.kind() == "class_heritage" {
540 let mut inner = child.walk();
541 for c in child.children(&mut inner) {
542 if c.kind() == "extends_clause" {
543 let mut ec = c.walk();
545 for e in c.children(&mut ec) {
546 if e.kind() == "identifier" || e.kind() == "type_identifier" {
547 return Some(node_text(e, src).to_string());
548 }
549 }
550 }
551 }
552 }
553 }
554 None
555}
556
557fn collect_null_checks_ts(body: Option<Node>, src: &[u8]) -> Vec<String> {
559 let Some(body) = body else { return vec![] };
560 let mut fields = Vec::new();
561 walk_null_checks_ts(body, src, &mut fields);
562 fields.sort();
563 fields.dedup();
564 fields
565}
566
567fn walk_null_checks_ts(node: Node, src: &[u8], fields: &mut Vec<String>) {
568 if node.kind() == "binary_expression"
569 && let text = node_text(node, src)
570 && (text.contains("null") || text.contains("undefined"))
571 && let Some(left) = node.child_by_field_name("left")
572 && let ltext = node_text(left, src)
573 && let Some(f) = ltext.strip_prefix("this.")
574 {
575 fields.push(f.to_string());
576 }
577 let mut cursor = node.walk();
578 for child in node.children(&mut cursor) {
579 walk_null_checks_ts(child, src, fields);
580 }
581}
582
583fn extract_switch_target_ts(body: Option<Node>, src: &[u8]) -> Option<String> {
585 let body = body?;
586 find_switch_target_ts(body, src)
587}
588
589fn find_switch_target_ts(node: Node, src: &[u8]) -> Option<String> {
590 if node.kind() == "switch_statement"
591 && let Some(value) = node.child_by_field_name("value")
592 {
593 return Some(node_text(value, src).to_string());
594 }
595 let mut cursor = node.walk();
596 for child in node.children(&mut cursor) {
597 if let Some(t) = find_switch_target_ts(child, src) {
598 return Some(t);
599 }
600 }
601 None
602}
603
604fn count_optional_params_ts(node: Node, src: &[u8]) -> usize {
606 let Some(params) = node.child_by_field_name("parameters") else {
607 return 0;
608 };
609 let mut count = 0;
610 let mut cursor = params.walk();
611 for child in params.children(&mut cursor) {
612 let text = node_text(child, src);
613 if text.contains('?') || child.child_by_field_name("value").is_some() {
614 count += 1;
615 }
616 }
617 count
618}
619
620fn is_callback_collection_type_ts(field_node: Node, src: &[u8]) -> bool {
623 let Some(ty) = field_node.child_by_field_name("type") else {
624 if let Some(init) = field_node.child_by_field_name("value") {
626 let text = node_text(init, src);
627 return text == "[]" || text.contains("new Array");
628 }
629 return false;
630 };
631 let text = node_text(ty, src);
632 (text.contains("Function") && (text.contains("[]") || text.contains("Array<")))
634 || (text.contains("=>") && text.contains("[]"))
635 || text.contains("Array<(")
636}
637
638fn has_iterate_and_call_ts(body: Node, src: &[u8], cb_fields: &[String]) -> bool {
641 if cb_fields.is_empty() {
642 return false;
643 }
644 let mut cursor = body.walk();
645 for child in body.children(&mut cursor) {
646 if child.kind() == "method_definition"
647 && let Some(fn_body) = child.child_by_field_name("body")
648 {
649 for field in cb_fields {
650 let this_field = format!("this.{field}");
651 if walk_for_iterate_call_ts(fn_body, src, &this_field) {
652 return true;
653 }
654 }
655 }
656 }
657 false
658}
659
660fn walk_for_iterate_call_ts(node: Node, src: &[u8], this_field: &str) -> bool {
661 if node.kind() == "for_in_statement"
663 && node_text(node, src).contains(this_field)
664 && let Some(loop_body) = node.child_by_field_name("body")
665 && has_call_expression_ts(loop_body)
666 {
667 return true;
668 }
669 if node.kind() == "call_expression" || node.kind() == "expression_statement" {
671 let text = node_text(node, src);
672 if text.contains(this_field) && text.contains("forEach") {
673 return true;
674 }
675 }
676 let mut cursor = node.walk();
677 for child in node.children(&mut cursor) {
678 if walk_for_iterate_call_ts(child, src, this_field) {
679 return true;
680 }
681 }
682 false
683}
684
685fn collect_comments(root: Node, src: &[u8]) -> Vec<cha_core::CommentInfo> {
686 let mut comments = Vec::new();
687 let mut cursor = root.walk();
688 visit_all(root, &mut cursor, &mut |n| {
689 if n.kind().contains("comment") {
690 comments.push(cha_core::CommentInfo {
691 text: node_text(n, src).to_string(),
692 line: n.start_position().row + 1,
693 });
694 }
695 });
696 comments
697}
698
699fn has_call_expression_ts(node: Node) -> bool {
700 if node.kind() == "call_expression" {
701 return true;
702 }
703 let mut cursor = node.walk();
704 for child in node.children(&mut cursor) {
705 if has_call_expression_ts(child) {
706 return true;
707 }
708 }
709 false
710}
711
712fn cognitive_complexity_ts(node: tree_sitter::Node) -> usize {
713 let mut score = 0;
714 cc_walk_ts(node, 0, &mut score);
715 score
716}
717
718fn cc_walk_ts(node: tree_sitter::Node, nesting: usize, score: &mut usize) {
719 match node.kind() {
720 "if_statement" => {
721 *score += 1 + nesting;
722 cc_children_ts(node, nesting + 1, score);
723 return;
724 }
725 "for_statement" | "for_in_statement" | "while_statement" | "do_statement" => {
726 *score += 1 + nesting;
727 cc_children_ts(node, nesting + 1, score);
728 return;
729 }
730 "switch_statement" => {
731 *score += 1 + nesting;
732 cc_children_ts(node, nesting + 1, score);
733 return;
734 }
735 "else_clause" => {
736 *score += 1;
737 }
738 "binary_expression" => {
739 if let Some(op) = node.child_by_field_name("operator")
740 && (op.kind() == "&&" || op.kind() == "||")
741 {
742 *score += 1;
743 }
744 }
745 "catch_clause" => {
746 *score += 1 + nesting;
747 cc_children_ts(node, nesting + 1, score);
748 return;
749 }
750 "arrow_function" | "function_expression" => {
751 cc_children_ts(node, nesting + 1, score);
752 return;
753 }
754 _ => {}
755 }
756 cc_children_ts(node, nesting, score);
757}
758
759fn cc_children_ts(node: tree_sitter::Node, nesting: usize, score: &mut usize) {
760 let mut cursor = node.walk();
761 for child in node.children(&mut cursor) {
762 cc_walk_ts(child, nesting, score);
763 }
764}
765
766fn collect_calls_ts(body: Option<tree_sitter::Node>, src: &[u8]) -> Vec<String> {
767 let Some(body) = body else { return Vec::new() };
768 let mut calls = Vec::new();
769 let mut cursor = body.walk();
770 visit_all(body, &mut cursor, &mut |n| {
771 if n.kind() == "call_expression"
772 && let Some(func) = n.child(0)
773 {
774 let name = node_text(func, src).to_string();
775 if !calls.contains(&name) {
776 calls.push(name);
777 }
778 }
779 });
780 calls
781}
782
783fn visit_all<F: FnMut(Node)>(node: Node, cursor: &mut tree_sitter::TreeCursor, f: &mut F) {
784 f(node);
785 if cursor.goto_first_child() {
786 loop {
787 let child_node = cursor.node();
788 let mut child_cursor = child_node.walk();
789 visit_all(child_node, &mut child_cursor, f);
790 if !cursor.goto_next_sibling() {
791 break;
792 }
793 }
794 cursor.goto_parent();
795 }
796}