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