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