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