garbage_code_hunter/language/adapter/
ts.rs1use 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 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}