garbage_code_hunter/language/adapter/
js.rs1use 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 "(throw_statement) @pc_throw",
17 "[(function_declaration name: (identifier) @ex_name) (method_definition name: (property_identifier) @ex_name)] @ex_fn",
19 "(variable_declarator name: (identifier) @nv_var)",
21 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 "[(function_declaration parameters: (formal_parameters) @ep_params) (arrow_function parameters: (formal_parameters) @ep_params) (method_definition parameters: (formal_parameters) @ep_params)]",
26 "(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 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}