Skip to main content

garbage_code_hunter/language/adapter/
js.rs

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