1use super::{
4 count_block_ancestors, count_nested_blocks, count_params, is_boolean_or_null,
5 is_common_safe_number, is_inside_declaration, is_repeating_chars, max_scope_depth,
6 FunctionNode, LanguageAdapter,
7};
8use crate::language::Language;
9use crate::treesitter::engine::ParsedFile;
10use crate::treesitter::query::QueryCapture;
11use regex::Regex;
12use std::sync::LazyLock;
13
14fn cfg_test_ranges(content: &str) -> Vec<(usize, usize)> {
17 let mut ranges = Vec::new();
18 let mut search_from = 0;
19 while let Some(attr_pos) = content[search_from..].find("#[cfg(test)]") {
20 let attr_start = search_from + attr_pos;
21 let after_attr = attr_start + "#[cfg(test)]".len();
22 if let Some(brace_offset) = content[after_attr..].find('{') {
23 let open_brace = after_attr + brace_offset;
24 let mut depth = 1i32;
25 let mut j = open_brace + 1;
26 for ch in content[open_brace + 1..].chars() {
27 match ch {
28 '{' => depth += 1,
29 '}' => depth -= 1,
30 _ => {}
31 }
32 j += ch.len_utf8();
33 if depth == 0 {
34 break;
35 }
36 }
37 if depth == 0 {
38 ranges.push((open_brace, j));
39 }
40 search_from = j;
41 } else {
42 search_from = after_attr;
43 }
44 }
45 ranges
46}
47
48const RUST_PATTERNS: &[&str] = &[
49 "(field_expression field: (field_identifier) @pc_method (#eq? @pc_method \"unwrap\"))",
50 "(macro_invocation macro: (identifier) @pc_m)",
51 "(function_item name: (identifier) @ex_name) @ex_fn",
52 "(let_declaration pattern: (identifier) @nv_var (#match? @nv_var \"^[a-z]$\"))",
53 "(let_declaration pattern: (identifier) @nv_name)",
54 "(identifier) @nv_id",
55 "(macro_invocation macro: (identifier) @dp_name (#match? @dp_name \"^(println|dbg|eprintln|eprint|todo|unimplemented)$\"))",
56 "(function_item parameters: (parameters) @ep_params)",
57 "(unsafe_block) @ub_unsafe",
58 "(integer_literal) @mn_num",
59];
60
61pub struct RustAdapter;
62
63impl LanguageAdapter for RustAdapter {
64 fn language(&self) -> Language {
65 Language::Rust
66 }
67
68 fn query_patterns(&self) -> &[&str] {
69 RUST_PATTERNS
70 }
71
72 fn count_panic_calls(&self, file: &ParsedFile) -> usize {
73 self.count_panic_from_batch(file, &self.batch_captures(file))
74 }
75
76 fn extract_functions(&self, file: &ParsedFile) -> Vec<FunctionNode> {
77 self.extract_functions_from_batch(file, &self.batch_captures(file))
78 }
79
80 fn max_nesting_depth(&self, file: &ParsedFile) -> usize {
81 max_scope_depth(file.root_node(), 0)
82 }
83
84 fn count_naming_violations(&self, file: &ParsedFile) -> usize {
85 self.count_naming_from_batch(file, &self.batch_captures(file))
86 }
87
88 fn count_deeply_nested_blocks(&self, file: &ParsedFile) -> usize {
89 let threshold = 5;
90 let mut count = 0;
91 count_nested_blocks(file.root_node(), 0, threshold, &mut count);
92 count
93 }
94
95 fn count_debug_calls(&self, file: &ParsedFile) -> usize {
96 self.count_debug_from_batch(file, &self.batch_captures(file))
97 }
98
99 fn count_excessive_params(&self, file: &ParsedFile, threshold: usize) -> usize {
100 self.count_excessive_from_batch_with(file, &self.batch_captures(file), threshold)
101 }
102
103 fn count_unsafe_blocks(&self, file: &ParsedFile) -> usize {
104 self.count_unsafe_from_batch(file, &self.batch_captures(file))
105 }
106
107 fn count_magic_numbers(&self, file: &ParsedFile) -> usize {
108 self.count_magic_from_batch(file, &self.batch_captures(file))
109 }
110
111 fn count_dead_code(&self, file: &ParsedFile) -> usize {
112 let mut count = 0;
113 let mut dead_start: Option<usize> = None;
114 for (line_num, line) in file.content.lines().enumerate() {
115 let trimmed = line.trim();
116 if matches!(
117 trimmed,
118 "return;" | "break;" | "continue;" | "unreachable!()" | "unreachable!();"
119 ) || (trimmed.starts_with("return ") && trimmed.ends_with(';'))
120 || (trimmed.starts_with("panic!(") && trimmed.ends_with(';'))
121 || (trimmed.starts_with("unreachable!(") && trimmed.ends_with(')'))
122 {
123 dead_start = Some(line_num + 2);
124 continue;
125 }
126 if let Some(start) = dead_start {
127 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("/*") {
128 continue;
129 }
130 if trimmed == "}"
131 || trimmed.starts_with("} else")
132 || trimmed.starts_with("} else if")
133 {
134 dead_start = None;
135 continue;
136 }
137 if line_num + 1 >= start {
138 count += 1;
139 dead_start = None;
140 }
141 }
142 }
143 count
144 }
145
146 fn count_duplicate_imports(&self, file: &ParsedFile) -> usize {
147 super::count_duplicate_imports_with(file, &["use "])
148 }
149
150 fn count_panic_from_batch<'a>(
151 &self,
152 file: &ParsedFile,
153 batch: &[Vec<QueryCapture<'a>>],
154 ) -> usize {
155 let test_ranges = cfg_test_ranges(&file.content);
156 let mut count = 0;
157 for m in batch {
158 for c in m {
159 if (c.name == "pc_method" && c.text == "unwrap")
160 || (c.name == "pc_m"
161 && matches!(c.text, "panic" | "assert" | "assert_eq" | "assert_ne"))
162 {
163 let byte_offset = c.node.start_byte();
164 if test_ranges
165 .iter()
166 .any(|&(s, e)| byte_offset >= s && byte_offset < e)
167 {
168 continue;
169 }
170 count += 1;
171 }
172 }
173 }
174 count
175 }
176
177 fn extract_functions_from_batch<'a>(
178 &self,
179 _file: &ParsedFile,
180 batch: &[Vec<QueryCapture<'a>>],
181 ) -> Vec<FunctionNode> {
182 let mut functions = Vec::new();
183 for m in batch {
184 let has_ex = m.iter().any(|c| c.name.starts_with("ex_"));
185 if !has_ex {
186 continue;
187 }
188 let mut name = String::new();
189 let mut start_line = 0usize;
190 let mut end_line = 0usize;
191 for c in m {
192 match c.name.as_str() {
193 "ex_name" => name = c.text.to_string(),
194 "ex_fn" => {
195 start_line = c.node.start_position().row + 1;
196 end_line = c.node.end_position().row + 1;
197 }
198 _ => {}
199 }
200 }
201 if !name.is_empty() {
202 let nesting_depth = count_block_ancestors(m);
203 functions.push(FunctionNode {
204 name,
205 start_line,
206 end_line,
207 nesting_depth,
208 });
209 }
210 }
211 functions
212 }
213
214 fn count_naming_from_batch<'a>(
215 &self,
216 _file: &ParsedFile,
217 batch: &[Vec<QueryCapture<'a>>],
218 ) -> usize {
219 let mut count = 0usize;
220 let idiomatic_single: &[&str] = &["i", "j", "k", "n", "c", "e", "x", "t", "f"];
221
222 static TERRIBLE_RE: LazyLock<Option<Regex>> = LazyLock::new(|| {
223 Regex::new(
224 r"^(data|info|temp|tmp|val|value|thing|stuff|obj|object|manager|handler|helper|util|utils)(\d+)?$",
225 ).ok()
226 });
227 let terrible_re = TERRIBLE_RE.as_ref();
228 let meaningless: &[&str] = &[
229 "foo", "bar", "baz", "qux", "quux", "quuz", "aaa", "bbb", "ccc", "ddd", "eee", "xxx",
230 "yyy", "zzz", "test1", "test2", "test3",
231 ];
232
233 let hungarian_prefixes: &[&str] = &[
234 "str", "int", "bool", "float", "double", "char", "arr", "vec", "list", "map", "set",
235 ];
236 let scope_prefixes: &[&str] = &["g_", "m_", "s_", "p_"];
237 let domain_prefixes: &[&str] = &[
238 "ctx", "req", "res", "err", "db", "kv", "fs", "io", "api", "http", "html", "ssh",
239 "tls", "uid", "uri", "url",
240 ];
241 let bad_abbrevs: &[&str] = &[
242 "mgr", "mngr", "ctrl", "hdlr", "usr", "pwd", "prefs", "btn", "lbl", "pic", "tbl",
243 "col", "cnt",
244 ];
245
246 for m in batch {
247 for c in m {
248 match c.name.as_str() {
249 "nv_var" if !idiomatic_single.contains(&c.text) => {
250 count += 1;
251 }
252 "nv_name" => {
253 let name = c.text;
254 let name_lower = name.to_lowercase();
255 if let Some(re) = terrible_re {
256 if re.is_match(&name_lower) {
257 count += 1;
258 continue;
259 }
260 }
261 if meaningless.contains(&name) || is_repeating_chars(name) {
262 count += 1;
263 }
264 }
265 "nv_id" => {
266 if count > 2000 {
267 continue;
268 }
269 let name = c.text;
270 let name_lower = name.to_lowercase();
271 if domain_prefixes.iter().any(|p| name_lower.starts_with(p)) {
272 continue;
273 }
274 if scope_prefixes.iter().any(|p| name_lower.starts_with(p))
275 || hungarian_prefixes.iter().any(|p| {
276 name_lower.starts_with(p)
277 && name.len() > p.len()
278 && name.as_bytes()[p.len()].is_ascii_uppercase()
279 })
280 {
281 count += 1;
282 continue;
283 }
284 if bad_abbrevs
285 .iter()
286 .any(|a| name_lower == *a || name_lower.starts_with(&format!("{}_", a)))
287 {
288 count += 1;
289 }
290 }
291 _ => {}
292 }
293 }
294 }
295 count
296 }
297
298 fn count_debug_from_batch<'a>(
299 &self,
300 file: &ParsedFile,
301 batch: &[Vec<QueryCapture<'a>>],
302 ) -> usize {
303 let test_ranges = cfg_test_ranges(&file.content);
304 batch
305 .iter()
306 .filter(|m| {
307 m.iter().any(|c| {
308 if c.name != "dp_name" {
309 return false;
310 }
311 let byte_offset = c.node.start_byte();
312 !test_ranges
313 .iter()
314 .any(|&(s, e)| byte_offset >= s && byte_offset < e)
315 })
316 })
317 .count()
318 }
319
320 fn count_excessive_from_batch<'a>(
321 &self,
322 _file: &ParsedFile,
323 batch: &[Vec<QueryCapture<'a>>],
324 ) -> usize {
325 self.count_excessive_from_batch_with(_file, batch, 5)
326 }
327
328 fn count_unsafe_from_batch<'a>(
329 &self,
330 _file: &ParsedFile,
331 batch: &[Vec<QueryCapture<'a>>],
332 ) -> usize {
333 batch
334 .iter()
335 .filter(|m| m.iter().any(|c| c.name == "ub_unsafe"))
336 .count()
337 }
338
339 fn count_magic_from_batch<'a>(
340 &self,
341 _file: &ParsedFile,
342 batch: &[Vec<QueryCapture<'a>>],
343 ) -> usize {
344 let mut count = 0;
345 for m in batch {
346 for c in m {
347 if c.name == "mn_num" && !is_inside_declaration(c.node) {
348 let text = c.text;
349 if text != "0"
350 && text != "1"
351 && text != "-1"
352 && !is_common_safe_number(text)
353 && !is_boolean_or_null(text)
354 {
355 count += 1;
356 }
357 }
358 }
359 }
360 count
361 }
362}
363
364impl RustAdapter {
365 fn count_excessive_from_batch_with<'a>(
366 &self,
367 _file: &ParsedFile,
368 batch: &[Vec<QueryCapture<'a>>],
369 threshold: usize,
370 ) -> usize {
371 let mut count = 0;
372 for m in batch {
373 for c in m {
374 if c.name == "ep_params" && count_params(c.text) > threshold {
375 count += 1;
376 }
377 }
378 }
379 count
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::super::parse_code;
386 use super::*;
387
388 fn parse_rust(code: &str) -> ParsedFile {
389 parse_code(code, "test.rs").expect("parse")
390 }
391
392 #[test]
393 fn test_rust_count_panic_unwrap_only() {
394 let code = "fn main() { let x = foo().unwrap(); let y = bar().expect(\"msg\"); }";
395 let file = parse_rust(code);
396 let adapter = RustAdapter;
397 assert_eq!(adapter.count_panic_calls(&file), 1);
398 }
399
400 #[test]
401 fn test_rust_count_panic_macro() {
402 let code = "fn main() { panic!(\"boom\"); }";
403 let file = parse_rust(code);
404 let adapter = RustAdapter;
405 assert_eq!(adapter.count_panic_calls(&file), 1);
406 }
407
408 #[test]
409 fn test_rust_count_panic_clean() {
410 let code = "fn main() { let x = 42; }";
411 let file = parse_rust(code);
412 let adapter = RustAdapter;
413 assert_eq!(adapter.count_panic_calls(&file), 0);
414 }
415
416 #[test]
417 fn test_rust_extract_functions() {
418 let code = r#"
419fn foo() {}
420fn bar(x: i32) -> i32 { x + 1 }
421"#;
422 let file = parse_rust(code);
423 let adapter = RustAdapter;
424 let fns = adapter.extract_functions(&file);
425 assert_eq!(fns.len(), 2, "should find 2 functions");
426 assert_eq!(fns[0].name, "foo");
427 assert_eq!(fns[1].name, "bar");
428 assert!(fns[0].start_line < fns[1].start_line, "foo before bar");
429 }
430
431 #[test]
432 fn test_rust_max_nesting_depth_flat() {
433 let code = "fn main() { let x = 1; }";
434 let file = parse_rust(code);
435 let adapter = RustAdapter;
436 assert_eq!(adapter.max_nesting_depth(&file), 1);
437 }
438
439 #[test]
440 fn test_rust_max_nesting_depth_nested() {
441 let code = r#"
442fn main() {
443 if true {
444 for i in 0..10 {
445 let x = i;
446 }
447 }
448}
449"#;
450 let file = parse_rust(code);
451 let adapter = RustAdapter;
452 let depth = adapter.max_nesting_depth(&file);
453 assert!(
454 depth >= 2,
455 "nested if+for should have depth >= 2, got {depth}"
456 );
457 }
458
459 #[test]
460 fn test_rust_max_nesting_depth_empty() {
461 let code = "";
462 let file = parse_rust(code);
463 let adapter = RustAdapter;
464 assert_eq!(adapter.max_nesting_depth(&file), 0);
465 }
466
467 #[test]
468 fn test_naming_single_letter() {
469 let code = "fn main() { let a = 1; let bb = 2; }";
470 let file = parse_rust(code);
471 let adapter = RustAdapter;
472 assert_eq!(adapter.count_naming_violations(&file), 1);
473 }
474
475 #[test]
476 fn test_naming_terrible() {
477 let code = "fn main() { let data = 1; let manager = 2; }";
478 let file = parse_rust(code);
479 let adapter = RustAdapter;
480 assert_eq!(adapter.count_naming_violations(&file), 2);
481 }
482
483 #[test]
484 fn test_naming_meaningless() {
485 let code = "fn main() { let foo = 1; let aaa = 2; }";
486 let file = parse_rust(code);
487 let adapter = RustAdapter;
488 assert_eq!(adapter.count_naming_violations(&file), 2);
489 }
490
491 #[test]
492 fn test_naming_hungarian() {
493 let code = "fn main() { let strName = \"hello\"; let g_count = 0; }";
494 let file = parse_rust(code);
495 let adapter = RustAdapter;
496 assert_eq!(adapter.count_naming_violations(&file), 2);
497 }
498
499 #[test]
500 fn test_naming_hungarian_exempts_domain_prefixes() {
501 let code = "fn main() { let ctxUser = 1; let dbQuery = 2; let kvStore = 3; }";
502 let file = parse_rust(code);
503 let adapter = RustAdapter;
504 assert_eq!(adapter.count_naming_violations(&file), 0);
505 }
506
507 #[test]
508 fn test_naming_abbreviation() {
509 let code = "fn main() { let mgr = \"boss\"; let btn_submit = true; }";
510 let file = parse_rust(code);
511 let adapter = RustAdapter;
512 assert_eq!(adapter.count_naming_violations(&file), 2);
513 }
514
515 #[test]
516 fn test_naming_clean() {
517 let code = "fn main() { let user_name = \"alice\"; let item_count = 42; }";
518 let file = parse_rust(code);
519 let adapter = RustAdapter;
520 assert_eq!(adapter.count_naming_violations(&file), 0);
521 }
522
523 #[test]
524 fn test_rust_count_unsafe_blocks() {
525 let code = r#"
526fn main() {
527 unsafe {
528 let p = 42 as *const i32;
529 }
530 unsafe {
531 let _ = 0usize;
532 }
533}
534"#;
535 let file = parse_rust(code);
536 let adapter = RustAdapter;
537 assert_eq!(adapter.count_unsafe_blocks(&file), 2);
538 }
539
540 #[test]
541 fn test_rust_count_unsafe_blocks_clean() {
542 let code = "fn main() { let x = 42; }";
543 let file = parse_rust(code);
544 let adapter = RustAdapter;
545 assert_eq!(adapter.count_unsafe_blocks(&file), 0);
546 }
547
548 #[test]
549 fn test_rust_count_magic_numbers() {
550 let code = r#"
551fn main() {
552 let x = 1;
553 foo(42);
554 bar(100);
555}
556"#;
557 let file = parse_rust(code);
558 let adapter = RustAdapter;
559 assert_eq!(adapter.count_magic_numbers(&file), 2);
560 }
561
562 #[test]
563 fn test_rust_count_magic_numbers_const_ok() {
564 let code = r#"
565const MAX: i32 = 100;
566fn main() {
567 let x = MAX;
568}
569"#;
570 let file = parse_rust(code);
571 let adapter = RustAdapter;
572 assert_eq!(adapter.count_magic_numbers(&file), 0);
573 }
574
575 #[test]
576 fn test_rust_count_magic_numbers_skips_trivial() {
577 let code = r#"
578fn main() {
579 let x = 0;
580 let y = x + 1;
581}
582"#;
583 let file = parse_rust(code);
584 let adapter = RustAdapter;
585 assert_eq!(adapter.count_magic_numbers(&file), 0);
586 }
587
588 #[test]
589 fn test_rust_compute_all() {
590 let code = r#"
591fn main() {
592 let x = foo().unwrap();
593 panic!("boom");
594 println!("debug");
595 unsafe { let p = 42 as *const i32; }
596 foo(100);
597}
598"#;
599 let file = parse_rust(code);
600 let adapter = RustAdapter;
601 let counts = adapter.compute_all(&file);
602 assert!(counts.panic_calls >= 2);
603 assert!(counts.debug_calls >= 1);
604 assert!(counts.unsafe_blocks >= 1);
605 assert!(counts.magic_numbers >= 1);
606 }
607}