Skip to main content

garbage_code_hunter/language/adapter/
ts.rs

1//! TSAdapter — TypeScript language adapter.
2
3use super::{
4    count_block_ancestors, count_dead_code_with, count_duplicate_imports_with, count_nested_blocks,
5    count_params, is_boolean_or_null, is_common_safe_number, is_inside_declaration,
6    is_repeating_chars, FunctionNode, LanguageAdapter, MEANINGLESS_NAMES,
7};
8use crate::language::Language;
9use crate::treesitter::engine::ParsedFile;
10use crate::treesitter::query::QueryCapture;
11use regex::Regex;
12use std::sync::LazyLock;
13
14const TS_PATTERNS: &[&str] = &[
15    "(throw_statement) @pc_throw",
16    "[(function_declaration name: (identifier) @ex_name) (method_definition name: (property_identifier) @ex_name)] @ex_fn",
17    "(variable_declarator name: (identifier) @nv_var)",
18    "(call_expression function: (member_expression property: (property_identifier) @dp_method) (#match? @dp_method \"^(log|debug|warn|error|info|trace)$\"))",
19    "(debugger_statement) @dp_debug",
20    "[(function_declaration parameters: (formal_parameters) @ep_params) (arrow_function parameters: (formal_parameters) @ep_params) (method_definition parameters: (formal_parameters) @ep_params)]",
21    "(number) @mn_num",
22    "(predefined_type) @ts_type",
23    "(type_alias_declaration value: (object_type) @ts_alias)",
24    "(enum_declaration) @ts_enum",
25];
26
27pub struct TSAdapter;
28
29impl LanguageAdapter for TSAdapter {
30    fn language(&self) -> Language {
31        Language::TypeScript
32    }
33
34    fn query_patterns(&self) -> &[&str] {
35        TS_PATTERNS
36    }
37
38    fn count_panic_calls(&self, file: &ParsedFile) -> usize {
39        self.count_panic_from_batch(file, &self.batch_captures(file))
40    }
41
42    fn extract_functions(&self, file: &ParsedFile) -> Vec<FunctionNode> {
43        self.extract_functions_from_batch(file, &self.batch_captures(file))
44    }
45
46    fn max_nesting_depth(&self, file: &ParsedFile) -> usize {
47        fn ts_scope_depth(node: tree_sitter::Node, depth: usize) -> usize {
48            let mut max = depth;
49            for i in 0..node.child_count() {
50                if let Some(child) = node.child(i as u32) {
51                    let child_depth = match child.kind() {
52                        "block" => depth + 1,
53                        _ => depth,
54                    };
55                    max = max.max(ts_scope_depth(child, child_depth));
56                }
57            }
58            max
59        }
60        ts_scope_depth(file.root_node(), 0)
61    }
62
63    fn count_naming_violations(&self, file: &ParsedFile) -> usize {
64        self.count_naming_from_batch(file, &self.batch_captures(file))
65    }
66
67    fn count_deeply_nested_blocks(&self, file: &ParsedFile) -> usize {
68        let mut count = 0;
69        count_nested_blocks(file.root_node(), 0, 5, &mut count);
70        count
71    }
72
73    fn count_debug_calls(&self, file: &ParsedFile) -> usize {
74        self.count_debug_from_batch(file, &self.batch_captures(file))
75    }
76
77    fn count_excessive_params(&self, file: &ParsedFile, _threshold: usize) -> usize {
78        self.count_excessive_from_batch(file, &self.batch_captures(file))
79    }
80
81    fn count_magic_numbers(&self, file: &ParsedFile) -> usize {
82        self.count_magic_from_batch(file, &self.batch_captures(file))
83    }
84
85    fn count_dead_code(&self, file: &ParsedFile) -> usize {
86        count_dead_code_with(
87            file,
88            &["return;", "break;", "continue;", "throw;"],
89            &["return ", "throw ", "process.exit("],
90            "//",
91        )
92    }
93
94    fn count_duplicate_imports(&self, file: &ParsedFile) -> usize {
95        count_duplicate_imports_with(file, &["import "])
96    }
97
98    fn count_ts_issues(&self, file: &ParsedFile) -> usize {
99        self.count_ts_from_batch(file, &self.batch_captures(file))
100    }
101
102    // -- _from_batch overrides --
103
104    fn count_panic_from_batch<'a>(
105        &self,
106        _file: &ParsedFile,
107        batch: &[Vec<QueryCapture<'a>>],
108    ) -> usize {
109        batch
110            .iter()
111            .filter(|m| m.iter().any(|c| c.name == "pc_throw"))
112            .count()
113    }
114
115    fn extract_functions_from_batch<'a>(
116        &self,
117        _file: &ParsedFile,
118        batch: &[Vec<QueryCapture<'a>>],
119    ) -> Vec<FunctionNode> {
120        let mut functions = Vec::new();
121        for m in batch {
122            let has_ex = m.iter().any(|c| c.name.starts_with("ex_"));
123            if !has_ex {
124                continue;
125            }
126            let mut name = String::new();
127            let mut start_line = 0usize;
128            let mut end_line = 0usize;
129            for c in m {
130                match c.name.as_str() {
131                    "ex_name" => name = c.text.to_string(),
132                    "ex_fn" => {
133                        start_line = c.node.start_position().row + 1;
134                        end_line = c.node.end_position().row + 1;
135                    }
136                    _ => {}
137                }
138            }
139            if !name.is_empty() {
140                let nesting_depth = count_block_ancestors(m);
141                functions.push(FunctionNode {
142                    name,
143                    start_line,
144                    end_line,
145                    nesting_depth,
146                });
147            }
148        }
149        functions
150    }
151
152    fn count_naming_from_batch<'a>(
153        &self,
154        _file: &ParsedFile,
155        batch: &[Vec<QueryCapture<'a>>],
156    ) -> usize {
157        let mut count = 0usize;
158        static TERRIBLE_RE: LazyLock<Option<Regex>> = LazyLock::new(|| {
159            Regex::new(r"^(data|info|temp|tmp|val|value|thing|stuff|obj|object|manager|handler|helper|util|utils)(\d+)?$").ok()
160        });
161        let terrible_re = TERRIBLE_RE.as_ref();
162        let idiomatic_single: &[&str] = &["i", "j", "k", "e", "x", "y"];
163
164        for m in batch {
165            for c in m {
166                if c.name == "nv_var" {
167                    let name = c.text;
168                    if name.len() == 1 && name.chars().all(|ch| ch.is_ascii_lowercase()) {
169                        if !idiomatic_single.contains(&name) {
170                            count += 1;
171                        }
172                        continue;
173                    }
174                    if let Some(re) = terrible_re {
175                        if re.is_match(&name.to_lowercase()) {
176                            count += 1;
177                            continue;
178                        }
179                    }
180                    if MEANINGLESS_NAMES.contains(&name) || is_repeating_chars(name) {
181                        count += 1;
182                    }
183                }
184            }
185        }
186        count
187    }
188
189    fn count_debug_from_batch<'a>(
190        &self,
191        _file: &ParsedFile,
192        batch: &[Vec<QueryCapture<'a>>],
193    ) -> usize {
194        batch
195            .iter()
196            .filter(|m| {
197                m.iter()
198                    .any(|c| c.name == "dp_method" || c.name == "dp_debug")
199            })
200            .count()
201    }
202
203    fn count_excessive_from_batch<'a>(
204        &self,
205        _file: &ParsedFile,
206        batch: &[Vec<QueryCapture<'a>>],
207    ) -> usize {
208        let mut count = 0;
209        for m in batch {
210            for c in m {
211                if c.name == "ep_params" && count_params(c.text) > 5 {
212                    count += 1;
213                }
214            }
215        }
216        count
217    }
218
219    fn count_magic_from_batch<'a>(
220        &self,
221        _file: &ParsedFile,
222        batch: &[Vec<QueryCapture<'a>>],
223    ) -> usize {
224        let mut count = 0;
225        for m in batch {
226            for c in m {
227                if c.name == "mn_num" && !is_inside_declaration(c.node) {
228                    let text = c.text;
229                    if text != "0"
230                        && text != "1"
231                        && text != "-1"
232                        && !is_common_safe_number(text)
233                        && !is_boolean_or_null(text)
234                    {
235                        count += 1;
236                    }
237                }
238            }
239        }
240        count
241    }
242
243    fn count_ts_from_batch<'a>(&self, file: &ParsedFile, batch: &[Vec<QueryCapture<'a>>]) -> usize {
244        let mut count = 0;
245        for m in batch {
246            for c in m {
247                match c.name.as_str() {
248                    "ts_type" if c.text.trim() == "any" => {
249                        count += 1;
250                    }
251                    "ts_alias" | "ts_enum" => {
252                        count += 1;
253                    }
254                    _ => {}
255                }
256            }
257        }
258        for line in file.content.lines() {
259            let t = line.trim();
260            if t.starts_with("//") {
261                if t.contains("@ts-ignore") || t.contains("@ts-expect-error") {
262                    count += 1;
263                }
264                continue;
265            }
266            if t.starts_with("/*") || t.starts_with("*") {
267                continue;
268            }
269            if t.contains("require(") || t.contains("require (") {
270                count += 1;
271            }
272        }
273        count
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::super::parse_code;
280    use super::*;
281
282    fn parse_ts(code: &str) -> ParsedFile {
283        parse_code(code, "test.ts").expect("parse")
284    }
285
286    #[test]
287    fn test_ts_count_panic_throw() {
288        let code = r#"
289function main(): void {
290    throw new Error("boom");
291    throw "bang";
292}
293"#;
294        let file = parse_ts(code);
295        let adapter = TSAdapter;
296        assert_eq!(adapter.count_panic_calls(&file), 2);
297    }
298
299    #[test]
300    fn test_ts_count_panic_clean() {
301        let code = "function main(): void { return 42; }";
302        let file = parse_ts(code);
303        let adapter = TSAdapter;
304        assert_eq!(adapter.count_panic_calls(&file), 0);
305    }
306
307    #[test]
308    fn test_ts_extract_functions() {
309        let code = r#"
310function foo(): void {}
311function bar(x: number): number { return x; }
312const obj = { baz(): void {} };
313"#;
314        let file = parse_ts(code);
315        let adapter = TSAdapter;
316        let fns = adapter.extract_functions(&file);
317        assert_eq!(fns.len(), 3);
318        assert_eq!(fns[0].name, "foo");
319        assert_eq!(fns[1].name, "bar");
320        assert_eq!(fns[2].name, "baz");
321    }
322
323    #[test]
324    fn test_ts_naming_single_letter() {
325        let code = r#"
326function main(): void {
327    let a = 1;
328    const b = 2;
329}
330"#;
331        let file = parse_ts(code);
332        let adapter = TSAdapter;
333        assert_eq!(adapter.count_naming_violations(&file), 2);
334    }
335
336    #[test]
337    fn test_ts_debug_console_log() {
338        let code = r#"
339console.log("hello");
340console.error("bad");
341debugger;
342"#;
343        let file = parse_ts(code);
344        let adapter = TSAdapter;
345        assert_eq!(adapter.count_debug_calls(&file), 3);
346    }
347
348    #[test]
349    fn test_ts_excessive_params() {
350        let code = "function process(a: number, b: number, c: number, d: number, e: number, f: number): void {}\n";
351        let file = parse_ts(code);
352        let adapter = TSAdapter;
353        assert_eq!(adapter.count_excessive_params(&file, 5), 1);
354    }
355
356    #[test]
357    fn test_ts_magic_numbers() {
358        let code = r#"
359function main(): void {
360    foo(41);
361    bar(100);
362}
363"#;
364        let file = parse_ts(code);
365        let adapter = TSAdapter;
366        assert_eq!(adapter.count_magic_numbers(&file), 2);
367    }
368
369    #[test]
370    fn test_ts_magic_numbers_skips_trivial() {
371        let code = "function main(): void { foo(0); bar(1); }\n";
372        let file = parse_ts(code);
373        let adapter = TSAdapter;
374        assert_eq!(adapter.count_magic_numbers(&file), 0);
375    }
376
377    #[test]
378    fn test_ts_issues_ts_ignore() {
379        let code = "// @ts-ignore\nconst x: number = 'hello';\n";
380        let file = parse_ts(code);
381        let adapter = TSAdapter;
382        assert_eq!(adapter.count_ts_issues(&file), 1);
383    }
384
385    #[test]
386    fn test_ts_issues_ts_expect_error() {
387        let code = "// @ts-expect-error\nconst x = (null as any).foo;\n";
388        let file = parse_ts(code);
389        let adapter = TSAdapter;
390        assert_eq!(adapter.count_ts_issues(&file), 2);
391    }
392
393    #[test]
394    fn test_ts_issues_require() {
395        let code = "const fs = require('fs');\n";
396        let file = parse_ts(code);
397        let adapter = TSAdapter;
398        assert_eq!(adapter.count_ts_issues(&file), 1);
399    }
400
401    #[test]
402    fn test_ts_issues_clean() {
403        let code = "const x: number = 42;\n";
404        let file = parse_ts(code);
405        let adapter = TSAdapter;
406        assert_eq!(adapter.count_ts_issues(&file), 0);
407    }
408
409    #[test]
410    fn test_ts_issues_any_type() {
411        let code = "function foo(x: any): void {}\n";
412        let file = parse_ts(code);
413        let adapter = TSAdapter;
414        assert_eq!(adapter.count_ts_issues(&file), 1);
415    }
416
417    #[test]
418    fn test_ts_issues_enum() {
419        let code = "enum Color { Red, Green, Blue }\n";
420        let file = parse_ts(code);
421        let adapter = TSAdapter;
422        assert_eq!(adapter.count_ts_issues(&file), 1);
423    }
424
425    #[test]
426    fn test_ts_issues_object_alias() {
427        let code = "type Point = { x: number; y: number };\n";
428        let file = parse_ts(code);
429        let adapter = TSAdapter;
430        assert_eq!(adapter.count_ts_issues(&file), 1);
431    }
432
433    #[test]
434    fn test_ts_dead_code_after_throw() {
435        let code = r#"
436function foo(): void {
437    throw new Error("bad");
438    console.log("dead");
439}
440"#;
441        let file = parse_ts(code);
442        let adapter = TSAdapter;
443        assert_eq!(adapter.count_dead_code(&file), 1);
444    }
445}