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 });
483 }
484 }
485 None
486}
487
488fn count_comment_lines(node: Node) -> usize {
490 let mut count = 0;
491 let mut cursor = node.walk();
492 for child in node.children(&mut cursor) {
493 if child.kind() == "comment" {
494 count += child.end_position().row - child.start_position().row + 1;
495 } else if child.child_count() > 0 {
496 count += count_comment_lines(child);
497 }
498 }
499 count
500}
501
502fn collect_this_fields(body: Option<Node>, src: &[u8]) -> Vec<String> {
505 let Some(body) = body else { return vec![] };
506 let mut refs = Vec::new();
507 collect_this_refs(body, src, &mut refs);
508 refs.sort();
509 refs.dedup();
510 refs
511}
512
513fn collect_this_refs(node: Node, src: &[u8], refs: &mut Vec<String>) {
514 if node.kind() == "member_expression"
515 && let Some(obj) = node.child_by_field_name("object")
516 && node_text(obj, src) == "this"
517 && let Some(prop) = node.child_by_field_name("property")
518 {
519 refs.push(node_text(prop, src).to_string());
520 }
521 let mut cursor = node.walk();
522 for child in node.children(&mut cursor) {
523 collect_this_refs(child, src, refs);
524 }
525}
526
527fn is_accessor_name(name: &str) -> bool {
529 let lower = name.to_lowercase();
530 lower.starts_with("get") || lower.starts_with("set") || lower.starts_with("is")
531}
532
533fn extract_parent_name(node: Node, src: &[u8]) -> Option<String> {
536 let mut cursor = node.walk();
537 for child in node.children(&mut cursor) {
538 if child.kind() == "class_heritage" {
539 let mut inner = child.walk();
540 for c in child.children(&mut inner) {
541 if c.kind() == "extends_clause" {
542 let mut ec = c.walk();
544 for e in c.children(&mut ec) {
545 if e.kind() == "identifier" || e.kind() == "type_identifier" {
546 return Some(node_text(e, src).to_string());
547 }
548 }
549 }
550 }
551 }
552 }
553 None
554}
555
556fn collect_null_checks_ts(body: Option<Node>, src: &[u8]) -> Vec<String> {
558 let Some(body) = body else { return vec![] };
559 let mut fields = Vec::new();
560 walk_null_checks_ts(body, src, &mut fields);
561 fields.sort();
562 fields.dedup();
563 fields
564}
565
566fn walk_null_checks_ts(node: Node, src: &[u8], fields: &mut Vec<String>) {
567 if node.kind() == "binary_expression"
568 && let text = node_text(node, src)
569 && (text.contains("null") || text.contains("undefined"))
570 && let Some(left) = node.child_by_field_name("left")
571 && let ltext = node_text(left, src)
572 && let Some(f) = ltext.strip_prefix("this.")
573 {
574 fields.push(f.to_string());
575 }
576 let mut cursor = node.walk();
577 for child in node.children(&mut cursor) {
578 walk_null_checks_ts(child, src, fields);
579 }
580}
581
582fn extract_switch_target_ts(body: Option<Node>, src: &[u8]) -> Option<String> {
584 let body = body?;
585 find_switch_target_ts(body, src)
586}
587
588fn find_switch_target_ts(node: Node, src: &[u8]) -> Option<String> {
589 if node.kind() == "switch_statement"
590 && let Some(value) = node.child_by_field_name("value")
591 {
592 return Some(node_text(value, src).to_string());
593 }
594 let mut cursor = node.walk();
595 for child in node.children(&mut cursor) {
596 if let Some(t) = find_switch_target_ts(child, src) {
597 return Some(t);
598 }
599 }
600 None
601}
602
603fn count_optional_params_ts(node: Node, src: &[u8]) -> usize {
605 let Some(params) = node.child_by_field_name("parameters") else {
606 return 0;
607 };
608 let mut count = 0;
609 let mut cursor = params.walk();
610 for child in params.children(&mut cursor) {
611 let text = node_text(child, src);
612 if text.contains('?') || child.child_by_field_name("value").is_some() {
613 count += 1;
614 }
615 }
616 count
617}
618
619fn is_callback_collection_type_ts(field_node: Node, src: &[u8]) -> bool {
622 let Some(ty) = field_node.child_by_field_name("type") else {
623 if let Some(init) = field_node.child_by_field_name("value") {
625 let text = node_text(init, src);
626 return text == "[]" || text.contains("new Array");
627 }
628 return false;
629 };
630 let text = node_text(ty, src);
631 (text.contains("Function") && (text.contains("[]") || text.contains("Array<")))
633 || (text.contains("=>") && text.contains("[]"))
634 || text.contains("Array<(")
635}
636
637fn has_iterate_and_call_ts(body: Node, src: &[u8], cb_fields: &[String]) -> bool {
640 if cb_fields.is_empty() {
641 return false;
642 }
643 let mut cursor = body.walk();
644 for child in body.children(&mut cursor) {
645 if child.kind() == "method_definition"
646 && let Some(fn_body) = child.child_by_field_name("body")
647 {
648 for field in cb_fields {
649 let this_field = format!("this.{field}");
650 if walk_for_iterate_call_ts(fn_body, src, &this_field) {
651 return true;
652 }
653 }
654 }
655 }
656 false
657}
658
659fn walk_for_iterate_call_ts(node: Node, src: &[u8], this_field: &str) -> bool {
660 if node.kind() == "for_in_statement"
662 && node_text(node, src).contains(this_field)
663 && let Some(loop_body) = node.child_by_field_name("body")
664 && has_call_expression_ts(loop_body)
665 {
666 return true;
667 }
668 if node.kind() == "call_expression" || node.kind() == "expression_statement" {
670 let text = node_text(node, src);
671 if text.contains(this_field) && text.contains("forEach") {
672 return true;
673 }
674 }
675 let mut cursor = node.walk();
676 for child in node.children(&mut cursor) {
677 if walk_for_iterate_call_ts(child, src, this_field) {
678 return true;
679 }
680 }
681 false
682}
683
684fn collect_comments(root: Node, src: &[u8]) -> Vec<cha_core::CommentInfo> {
685 let mut comments = Vec::new();
686 let mut cursor = root.walk();
687 visit_all(root, &mut cursor, &mut |n| {
688 if n.kind().contains("comment") {
689 comments.push(cha_core::CommentInfo {
690 text: node_text(n, src).to_string(),
691 line: n.start_position().row + 1,
692 });
693 }
694 });
695 comments
696}
697
698fn has_call_expression_ts(node: Node) -> bool {
699 if node.kind() == "call_expression" {
700 return true;
701 }
702 let mut cursor = node.walk();
703 for child in node.children(&mut cursor) {
704 if has_call_expression_ts(child) {
705 return true;
706 }
707 }
708 false
709}
710
711fn cognitive_complexity_ts(node: tree_sitter::Node) -> usize {
712 let mut score = 0;
713 cc_walk_ts(node, 0, &mut score);
714 score
715}
716
717fn cc_walk_ts(node: tree_sitter::Node, nesting: usize, score: &mut usize) {
718 match node.kind() {
719 "if_statement" => {
720 *score += 1 + nesting;
721 cc_children_ts(node, nesting + 1, score);
722 return;
723 }
724 "for_statement" | "for_in_statement" | "while_statement" | "do_statement" => {
725 *score += 1 + nesting;
726 cc_children_ts(node, nesting + 1, score);
727 return;
728 }
729 "switch_statement" => {
730 *score += 1 + nesting;
731 cc_children_ts(node, nesting + 1, score);
732 return;
733 }
734 "else_clause" => {
735 *score += 1;
736 }
737 "binary_expression" => {
738 if let Some(op) = node.child_by_field_name("operator")
739 && (op.kind() == "&&" || op.kind() == "||")
740 {
741 *score += 1;
742 }
743 }
744 "catch_clause" => {
745 *score += 1 + nesting;
746 cc_children_ts(node, nesting + 1, score);
747 return;
748 }
749 "arrow_function" | "function_expression" => {
750 cc_children_ts(node, nesting + 1, score);
751 return;
752 }
753 _ => {}
754 }
755 cc_children_ts(node, nesting, score);
756}
757
758fn cc_children_ts(node: tree_sitter::Node, nesting: usize, score: &mut usize) {
759 let mut cursor = node.walk();
760 for child in node.children(&mut cursor) {
761 cc_walk_ts(child, nesting, score);
762 }
763}
764
765fn collect_calls_ts(body: Option<tree_sitter::Node>, src: &[u8]) -> Vec<String> {
766 let Some(body) = body else { return Vec::new() };
767 let mut calls = Vec::new();
768 let mut cursor = body.walk();
769 visit_all(body, &mut cursor, &mut |n| {
770 if n.kind() == "call_expression"
771 && let Some(func) = n.child(0)
772 {
773 let name = node_text(func, src).to_string();
774 if !calls.contains(&name) {
775 calls.push(name);
776 }
777 }
778 });
779 calls
780}
781
782fn visit_all<F: FnMut(Node)>(node: Node, cursor: &mut tree_sitter::TreeCursor, f: &mut F) {
783 f(node);
784 if cursor.goto_first_child() {
785 loop {
786 let child_node = cursor.node();
787 let mut child_cursor = child_node.walk();
788 visit_all(child_node, &mut child_cursor, f);
789 if !cursor.goto_next_sibling() {
790 break;
791 }
792 }
793 cursor.goto_parent();
794 }
795}