1use regex::Regex;
2
3use crate::analyzer::{CodeIssue, Severity};
4use crate::language::Language;
5use crate::treesitter::engine::ParsedFile;
6use crate::treesitter::query::collect_captures;
7use crate::treesitter::rule::TreeSitterRule;
8
9use super::base_rules::{
10 closure_depth, count_descendants_of_types, count_parameters, find_function_name, BLOCK_KINDS,
11 BLOCK_PARENT_TYPES,
12};
13
14pub(crate) struct DeepNestingRule;
19
20impl TreeSitterRule for DeepNestingRule {
21 fn name(&self) -> &'static str {
22 "deep-nesting"
23 }
24
25 fn supported_languages(&self) -> &'static [Language] {
26 crate::language::LANGUAGES_WITH_GRAMMAR
27 }
28
29 fn skips_test_files(&self) -> bool {
30 false
31 }
32
33 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
34 let mut issues = Vec::new();
35 let root = file.root_node();
36 check_nesting_recursive(file, root, 0, &mut issues);
37 issues
38 }
39}
40
41fn check_nesting_recursive(
42 file: &ParsedFile,
43 node: tree_sitter::Node,
44 depth: usize,
45 issues: &mut Vec<CodeIssue>,
46) {
47 let node_type = node.kind();
48 let is_block_parent = BLOCK_PARENT_TYPES.contains(&node_type);
49 let is_block_kind = BLOCK_KINDS.contains(&node_type);
50
51 let new_depth = if is_block_kind && depth > 0 {
52 if depth > 5 {
54 let messages = [
55 "Nesting deeper than Russian dolls, are you writing a maze?",
56 "Nesting so deep, trying to dig to the Earth's core?",
57 "Code nested like an onion, makes me want to cry",
58 "Nesting level exceeded! Consider refactoring",
59 "This nesting depth could apply for a Guinness World Record",
60 ];
61 let severity = if depth > 8 {
62 Severity::Nuclear
63 } else if depth > 6 {
64 Severity::Spicy
65 } else {
66 Severity::Mild
67 };
68 let pos = node.start_position();
69 issues.push(CodeIssue {
70 file_path: file.path.clone(),
71 line: pos.row + 1,
72 column: pos.column + 1,
73 rule_name: "deep-nesting".to_string(),
74 message: format!(
75 "{} (nesting depth: {})",
76 messages[issues.len() % messages.len()],
77 depth
78 ),
79 severity,
80 });
81 }
82 depth
83 } else if is_block_parent {
84 depth + 1
85 } else {
86 depth
87 };
88
89 let mut cursor = node.walk();
90 for child in node.children(&mut cursor) {
91 check_nesting_recursive(file, child, new_depth, issues);
92 }
93}
94
95fn function_query(lang: Language) -> &'static str {
96 match lang {
97 Language::Rust => "(function_item) @fn",
98 Language::Python => "(function_definition) @fn",
99 Language::JavaScript => "(function_declaration) @fn",
100 Language::C | Language::Cpp => "(function_definition) @fn",
101 _ => "(function_item) @fn",
102 }
103}
104
105pub(crate) struct LongFunctionRule;
110
111impl TreeSitterRule for LongFunctionRule {
112 fn name(&self) -> &'static str {
113 "long-function"
114 }
115
116 fn supported_languages(&self) -> &'static [Language] {
117 crate::language::LANGUAGES_WITH_GRAMMAR
118 }
119
120 fn skips_test_files(&self) -> bool {
121 false
122 }
123
124 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
125 let query = function_query(file.language);
126 let captures = match collect_captures(file, query) {
127 Ok(c) => c,
128 Err(_) => return vec![],
129 };
130
131 let is_test = file.path.to_string_lossy().contains("test");
132 let threshold: u32 = if is_test { 150 } else { 80 };
133 let content_bytes = file.content.as_bytes();
134 let mut issues = Vec::new();
135
136 for group in &captures {
137 if let Some(cap) = group.first() {
138 let node = cap.node;
139 let start_line = node.start_position().row as u32;
140 let end_line = node.end_position().row as u32;
141 let line_count = end_line.saturating_sub(start_line) + 1;
142
143 if line_count > threshold {
144 let func_name = find_function_name(node, content_bytes);
145 let messages = [
146 "Function '{}' has {} lines? This isn't a function, it's a novel!",
147 "'{}' function is {} lines long, consider splitting into smaller functions",
148 "Function '{}' is longer than my patience ({} lines), consider refactoring",
149 ];
150 let severity = if line_count > threshold * 2 {
151 Severity::Nuclear
152 } else if line_count > threshold + threshold / 2 {
153 Severity::Spicy
154 } else {
155 Severity::Mild
156 };
157 let pos = cap.node.start_position();
158 issues.push(CodeIssue {
159 file_path: file.path.clone(),
160 line: pos.row + 1,
161 column: pos.column + 1,
162 rule_name: "long-function".to_string(),
163 message: format!(
164 "{} ({} lines)",
165 messages[issues.len() % messages.len()].replace("'{}'", &func_name),
166 line_count
167 ),
168 severity,
169 });
170 }
171 }
172 }
173
174 issues
175 }
176}
177
178pub(crate) struct GodFunctionRule;
183
184impl TreeSitterRule for GodFunctionRule {
185 fn name(&self) -> &'static str {
186 "god-function"
187 }
188
189 fn supported_languages(&self) -> &'static [Language] {
190 crate::language::LANGUAGES_WITH_GRAMMAR
191 }
192
193 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
194 let query = function_query(file.language);
195 let captures = match collect_captures(file, query) {
196 Ok(c) => c,
197 Err(_) => return vec![],
198 };
199
200 let control_flow_types = [
201 "if_expression",
202 "for_expression",
203 "while_expression",
204 "match_expression",
205 "loop_expression",
206 ];
207
208 let content_bytes = file.content.as_bytes();
209 let mut issues = Vec::new();
210
211 for group in &captures {
212 if let Some(cap) = group.first() {
213 let node = cap.node;
214 let mut score: u32 = 0;
215
216 let start_line = node.start_position().row as u32;
218 let end_line = node.end_position().row as u32;
219 let line_count = end_line.saturating_sub(start_line) + 1;
220 if line_count > 50 {
221 score += (line_count - 50) / 10;
222 }
223
224 let param_count = count_parameters(node);
226 if param_count > 5 {
227 score += (param_count - 5) * 2;
228 }
229
230 score += count_descendants_of_types(node, &control_flow_types);
232
233 if score > 15 {
234 let func_name = find_function_name(node, content_bytes);
235 let messages = [
236 "Function '{}' does more things than I do in a day",
237 "Is '{}' a god function? Wants to control everything",
238 "Function '{}' is as complex as my love life",
239 "Function '{}' needs to be split - too bloated",
240 "Function '{}' violates single responsibility principle",
241 ];
242 let severity = if score > 25 {
243 Severity::Spicy
244 } else {
245 Severity::Mild
246 };
247 let pos = cap.node.start_position();
248 issues.push(CodeIssue {
249 file_path: file.path.clone(),
250 line: pos.row + 1,
251 column: pos.column + 1,
252 rule_name: "god-function".to_string(),
253 message: format!(
254 "{} (score: {})",
255 messages[issues.len() % messages.len()].replace("'{}'", &func_name),
256 score
257 ),
258 severity,
259 });
260 }
261 }
262 }
263
264 issues
265 }
266}
267
268pub(crate) struct ComplexClosureRule;
273
274impl TreeSitterRule for ComplexClosureRule {
275 fn name(&self) -> &'static str {
276 "complex-closure"
277 }
278
279 fn supported_languages(&self) -> &'static [Language] {
280 &[Language::Rust]
281 }
282
283 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
284 let captures = match collect_captures(file, "(closure_expression) @closure") {
285 Ok(c) => c,
286 Err(_) => return vec![],
287 };
288
289 let mut issues = Vec::new();
290
291 for group in &captures {
292 if let Some(cap) = group.first() {
293 let closure_node = cap.node;
294
295 let depth = closure_depth(closure_node);
297 if depth > 2 {
298 let messages = [
299 "Closures within closures? Are you writing Russian nesting dolls?",
300 "Nested closures are more complex than my relationships",
301 "These closures are nested like an onion - peel one layer, cry once",
302 "Closures too deeply nested - consider splitting into separate functions",
303 ];
304 let pos = closure_node.start_position();
305 issues.push(CodeIssue {
306 file_path: file.path.clone(),
307 line: pos.row + 1,
308 column: pos.column + 1,
309 rule_name: "complex-closure".to_string(),
310 message: format!(
311 "{} (depth: {})",
312 messages[issues.len() % messages.len()],
313 depth
314 ),
315 severity: Severity::Spicy,
316 });
317 }
318
319 let param_count = closure_node
321 .children(&mut closure_node.walk())
322 .filter(|c| c.kind() == "closure_parameters")
323 .map(|p| p.named_child_count() as u32)
324 .next()
325 .unwrap_or(0);
326
327 if param_count > 5 {
328 let messages = [
329 "This closure has more parameters than my excuses",
330 "Too many parameters for a closure - are you sure this isn't a function?",
331 "So many parameters - consider making this a proper function",
332 ];
333 let pos = closure_node.start_position();
334 issues.push(CodeIssue {
335 file_path: file.path.clone(),
336 line: pos.row + 1,
337 column: pos.column + 1,
338 rule_name: "complex-closure".to_string(),
339 message: messages[issues.len() % messages.len()].to_string(),
340 severity: Severity::Mild,
341 });
342 }
343 }
344 }
345
346 issues
347 }
348}
349
350pub(crate) fn variable_name_query(lang: Language) -> &'static str {
355 match lang {
356 Language::Rust => "(let_declaration pattern: (identifier) @name)",
357 Language::Python => "(assignment left: (identifier) @name)",
358 Language::JavaScript => "(variable_declarator name: (identifier) @name)",
359 _ => "(identifier) @name",
360 }
361}
362
363pub(crate) struct TerribleNamingRule;
364
365impl TreeSitterRule for TerribleNamingRule {
366 fn name(&self) -> &'static str {
367 "terrible-naming"
368 }
369
370 fn supported_languages(&self) -> &'static [Language] {
371 crate::language::LANGUAGES_WITH_GRAMMAR
372 }
373
374 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
375 let query = variable_name_query(file.language);
376 let captures = match collect_captures(file, query) {
377 Ok(c) => c,
378 Err(_) => return vec![],
379 };
380
381 let Ok(terrible_re) = Regex::new(
382 r"^(data|info|temp|tmp|val|value|thing|stuff|obj|object|manager|handler|helper|util|utils)(\d+)?$",
383 ) else {
384 tracing::error!("Failed to compile terrible naming regex");
385 return vec![];
386 };
387
388 let mut issues = Vec::new();
389
390 for group in &captures {
391 if let Some(cap) = group.first() {
392 let name = cap.text;
393 if terrible_re.is_match(&name.to_lowercase()) {
394 let msgs = [
395 format!(
396 "Variable '{}' - more abstract than my programming skills",
397 name
398 ),
399 format!(
400 "Variable '{}' - this name tells me you've given up on naming",
401 name
402 ),
403 format!(
404 "Variable '{}' - trying to make maintainers cry and quit?",
405 name
406 ),
407 format!(
408 "Variable '{}' - congrats on inventing the most meaningless identifier",
409 name
410 ),
411 format!(
412 "Variable '{}' - creativity level of naming a kid 'Child'",
413 name
414 ),
415 ];
416 let pos = cap.node.start_position();
417 issues.push(CodeIssue {
418 file_path: file.path.clone(),
419 line: pos.row + 1,
420 column: pos.column + 1,
421 rule_name: "terrible-naming".to_string(),
422 message: msgs[issues.len() % msgs.len()].clone(),
423 severity: Severity::Spicy,
424 });
425 }
426 }
427 }
428
429 issues
430 }
431}
432
433fn single_letter_query(lang: Language) -> &'static str {
438 match lang {
439 Language::Rust => {
440 r#"(let_declaration pattern: (identifier) @var (#match? @var "^[a-z]$"))"#
441 }
442 Language::Python => r#"(assignment left: (identifier) @var (#match? @var "^[a-z]$"))"#,
443 Language::JavaScript => {
444 r#"(variable_declarator name: (identifier) @var (#match? @var "^[a-z]$"))"#
445 }
446 Language::Go => {
447 r#"(short_variable_declaration left: (identifier) @var (#match? @var "^[a-z]$"))"#
448 }
449 Language::Java => {
450 r#"(variable_declarator name: (identifier) @var (#match? @var "^[a-z]$"))"#
451 }
452 Language::Ruby => r#"(assignment left: (identifier) @var (#match? @var "^[a-z]$"))"#,
453 Language::C | Language::Cpp => {
454 r#"(init_declarator declarator: (identifier) @var (#match? @var "^[a-z]$"))"#
455 }
456 _ => r#"(identifier) @var"#,
457 }
458}
459
460fn is_loop_counter(node: tree_sitter::Node) -> bool {
464 let mut current = node;
465 loop {
466 let kind = current.kind();
467 if matches!(kind, "for_expression" | "for_statement") {
468 if let Some(first) = current.named_child(0) {
471 let ns = node.start_byte();
472 let ne = node.end_byte();
473 let vs = first.start_byte();
474 let ve = first.end_byte();
475 if ns >= vs && ne <= ve {
476 return true; }
478 }
479 return false; }
481 if matches!(
482 kind,
483 "while_statement" | "while_expression" | "loop_expression" | "do_statement"
484 ) {
485 return false; }
487 match current.parent() {
488 Some(p) => current = p,
489 None => return false,
490 }
491 }
492}
493
494fn is_template_param(node: tree_sitter::Node) -> bool {
496 let mut parent = node.parent();
497 while let Some(p) = parent {
499 if p.kind() == "template_parameter_declaration" || p.kind() == "type_parameter" {
500 return true;
501 }
502 if matches!(
504 p.kind(),
505 "function_definition"
506 | "function_declaration"
507 | "class_specifier"
508 | "struct_specifier"
509 | "translation_unit"
510 ) {
511 break;
512 }
513 parent = p.parent();
514 }
515 false
516}
517
518pub(crate) struct SingleLetterTsRule;
519
520impl TreeSitterRule for SingleLetterTsRule {
521 fn name(&self) -> &'static str {
522 "single-letter-variable"
523 }
524
525 fn supported_languages(&self) -> &'static [Language] {
526 crate::language::LANGUAGES_WITH_GRAMMAR
527 }
528
529 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
530 let pattern = single_letter_query(file.language);
531 let captures = match collect_captures(file, pattern) {
532 Ok(c) => c,
533 Err(_) => return vec![],
534 };
535
536 let mut issues = Vec::new();
539
540 for group in &captures {
541 if let Some(cap) = group.first() {
542 let name = cap.text;
543 if name.len() != 1 {
545 continue;
546 }
547
548 if is_loop_counter(cap.node) {
551 continue;
552 }
553
554 if file.language == Language::Cpp && is_template_param(cap.node) {
556 continue;
557 }
558
559 let msgs = [
560 format!(
561 "Single-letter variable '{}'? Writing math formulas or torturing readers?",
562 name
563 ),
564 format!(
565 "Variable '{}'? Is this a name or did your keyboard break?",
566 name
567 ),
568 format!(
569 "Using '{}' as a variable name? You need a book on naming",
570 name
571 ),
572 format!(
573 "Single-letter variable '{}': harder to read than hieroglyphics",
574 name
575 ),
576 format!("Variable '{}' has about as much info as a period", name),
577 ];
578 let pos = cap.node.start_position();
579 issues.push(CodeIssue {
580 file_path: file.path.clone(),
581 line: pos.row + 1,
582 column: pos.column + 1,
583 rule_name: "single-letter-variable".to_string(),
584 message: msgs[issues.len() % msgs.len()].clone(),
585 severity: Severity::Mild,
586 });
587 }
588 }
589
590 issues
591 }
592}
593
594pub(crate) struct HungarianNotationTsRule;
599
600impl TreeSitterRule for HungarianNotationTsRule {
601 fn name(&self) -> &'static str {
602 "hungarian-notation"
603 }
604
605 fn supported_languages(&self) -> &'static [Language] {
606 crate::language::LANGUAGES_WITH_GRAMMAR
607 }
608
609 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
610 let captures = match collect_captures(file, "(identifier) @id") {
611 Ok(c) => c,
612 Err(_) => return vec![],
613 };
614
615 let prefixes = [
616 "str", "int", "bool", "float", "double", "char", "arr", "vec", "list", "map", "set",
617 ];
618 let scope_prefixes = ["g_", "m_", "s_", "p_"];
619
620 let mut issues = Vec::new();
621
622 for group in &captures {
623 if let Some(cap) = group.first() {
624 let name = cap.text;
625 let is_hungarian = scope_prefixes.iter().any(|p| name.starts_with(p))
626 || prefixes.iter().any(|&prefix| {
627 name.starts_with(prefix)
628 && name.len() > prefix.len()
629 && name
630 .chars()
631 .nth(prefix.len())
632 .is_some_and(|c| c.is_uppercase())
633 && !name[prefix.len()..].starts_with("ify")
634 && !name[prefix.len()..].starts_with("nal")
635 && !name[prefix.len()..].starts_with("ean")
636 });
637
638 if is_hungarian {
639 let messages = [
640 format!(
641 "'{}' uses Hungarian notation? This isn't the 1990s anymore",
642 name
643 ),
644 format!(
645 "Seeing '{}' makes me nostalgic for the dark ages of C++",
646 name
647 ),
648 format!(
649 "'{}' - Hungarian notation is as outdated as my haircut",
650 name
651 ),
652 format!(
653 "Hungarian notation '{}'? Rust's type system has got you covered",
654 name
655 ),
656 ];
657 let pos = cap.node.start_position();
658 issues.push(CodeIssue {
659 file_path: file.path.clone(),
660 line: pos.row + 1,
661 column: pos.column + 1,
662 rule_name: "hungarian-notation".to_string(),
663 message: messages[issues.len() % messages.len()].clone(),
664 severity: Severity::Mild,
665 });
666 }
667 }
668 }
669
670 issues
671 }
672}
673
674pub(crate) struct AbbreviationAbuseTsRule;
679
680impl TreeSitterRule for AbbreviationAbuseTsRule {
681 fn name(&self) -> &'static str {
682 "abbreviation-abuse"
683 }
684
685 fn supported_languages(&self) -> &'static [Language] {
686 crate::language::LANGUAGES_WITH_GRAMMAR
687 }
688
689 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
690 let captures = match collect_captures(file, "(identifier) @id") {
691 Ok(c) => c,
692 Err(_) => return vec![],
693 };
694
695 let bad_abbrevs: &[(&str, &str)] = &[
696 ("mgr", "manager"),
697 ("mngr", "manager"),
698 ("ctrl", "controller"),
699 ("hdlr", "handler"),
700 ("usr", "user"),
701 ("pwd", "password"),
702 ("prefs", "preferences"),
703 ("btn", "button"),
704 ("lbl", "label"),
705 ("pic", "picture"),
706 ("tbl", "table"),
707 ("col", "column"),
708 ("cnt", "count"),
709 ];
710
711 let mut issues = Vec::new();
712
713 for group in &captures {
714 if let Some(cap) = group.first() {
715 let name = cap.text;
716 let name_lower = name.to_lowercase();
717
718 for &(abbrev, full) in bad_abbrevs {
719 if name_lower == abbrev || name_lower.starts_with(&format!("{}_", abbrev)) {
720 let messages = [
721 format!("'{}' is too abbreviated, consider '{}'", name, full),
722 format!(
723 "Seeing '{}' makes me feel like I'm decoding, just use '{}'",
724 name, full
725 ),
726 format!(
727 "'{}' saves a few letters but kills readability, use '{}'",
728 name, full
729 ),
730 ];
731 let pos = cap.node.start_position();
732 issues.push(CodeIssue {
733 file_path: file.path.clone(),
734 line: pos.row + 1,
735 column: pos.column + 1,
736 rule_name: "abbreviation-abuse".to_string(),
737 message: messages[issues.len() % messages.len()].clone(),
738 severity: Severity::Mild,
739 });
740 break;
741 }
742 }
743 }
744 }
745
746 issues
747 }
748}
749
750fn print_debug_query(lang: Language) -> &'static str {
751 match lang {
752 Language::Rust => r#"(macro_invocation macro: (identifier) @m (#eq? @m "println"))"#,
753 Language::Python => r#"(call function: (identifier) @f (#eq? @f "print"))"#,
754 Language::JavaScript => {
755 r#"(call_expression function: (member_expression object: (identifier) @o property: (property_identifier) @p) (#eq? @o "console") (#eq? @p "log"))"#
756 }
757 _ => "",
758 }
759}
760
761pub(crate) struct PrintlnDebuggingRule;
762
763impl TreeSitterRule for PrintlnDebuggingRule {
764 fn name(&self) -> &'static str {
765 "println-debugging"
766 }
767
768 fn supported_languages(&self) -> &'static [Language] {
769 crate::language::LANGUAGES_WITH_GRAMMAR
770 }
771
772 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
773 let query = print_debug_query(file.language);
774 if query.is_empty() {
775 return vec![];
776 }
777 let captures = match collect_captures(file, query) {
778 Ok(c) => c,
779 Err(_) => return vec![],
780 };
781 let mut issues = Vec::new();
782 for group in &captures {
783 if let Some(cap) = group.first() {
784 let pos = cap.node.start_position();
785 issues.push(CodeIssue {
786 file_path: file.path.clone(),
787 line: pos.row + 1,
788 column: pos.column + 1,
789 rule_name: "println-debugging".to_string(),
790 message: format!("{} - use proper logging instead", cap.text),
791 severity: Severity::Spicy,
792 });
793 }
794 }
795 issues
796 }
797}
798
799fn number_literal_query(lang: Language) -> &'static str {
800 match lang {
801 Language::Rust => "(integer_literal) @num",
802 Language::Python => "(integer) @num",
803 Language::JavaScript | Language::TypeScript => "(number) @num",
804 Language::C | Language::Cpp => "(number_literal) @num",
805 _ => "",
806 }
807}
808
809pub(crate) struct MagicNumberRule;
810
811impl TreeSitterRule for MagicNumberRule {
812 fn name(&self) -> &'static str {
813 "magic-number"
814 }
815
816 fn supported_languages(&self) -> &'static [Language] {
817 crate::language::LANGUAGES_WITH_GRAMMAR
818 }
819
820 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
821 let query = number_literal_query(file.language);
822 if query.is_empty() {
823 return vec![];
824 }
825 let captures = match collect_captures(file, query) {
826 Ok(c) => c,
827 Err(_) => return vec![],
828 };
829 let named_parents: &[&str] = &[
831 "const_item",
832 "let_declaration",
833 "assignment",
834 "variable_declarator",
835 ];
836 let case_label_kinds: &[&str] = &["case", "switch_case", "case_statement"];
838 let common: &[&str] = &["0", "1", "-1", "2", "100", "0.0", "1.0", "10", "60", "24"];
839 let mut issues = Vec::new();
840 for group in &captures {
841 if let Some(cap) = group.first() {
842 let text = cap.text.trim();
843 let parent = cap.node.parent();
845 if parent.is_some_and(|p| named_parents.contains(&p.kind())) {
846 continue;
847 }
848 if parent.is_some_and(|p| case_label_kinds.contains(&p.kind())) {
850 continue;
851 }
852 if !common.contains(&text) && text.parse::<f64>().is_ok() {
853 let pos = cap.node.start_position();
854 issues.push(CodeIssue {
855 file_path: file.path.clone(),
856 line: pos.row + 1,
857 column: pos.column + 1,
858 rule_name: "magic-number".to_string(),
859 message: format!("Magic number '{}' - consider a named constant", text),
860 severity: Severity::Mild,
861 });
862 }
863 }
864 }
865 issues
866 }
867}