1use crate::analyzer::{CodeIssue, Severity};
2use crate::language::Language;
3use crate::treesitter::engine::ParsedFile;
4use crate::treesitter::query::collect_captures;
5use crate::treesitter::rule::TreeSitterRule;
6
7use super::complex_rules::variable_name_query;
8
9pub(crate) struct MeaninglessRule;
11
12impl TreeSitterRule for MeaninglessRule {
13 fn name(&self) -> &'static str {
14 "meaningless-naming"
15 }
16
17 fn supported_languages(&self) -> &'static [Language] {
18 crate::language::LANGUAGES_WITH_GRAMMAR
19 }
20
21 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
22 let query = variable_name_query(file.language);
23 let captures = match collect_captures(file, query) {
24 Ok(c) => c,
25 Err(_) => return vec![],
26 };
27
28 let meaningless: &[&str] = &[
29 "foo", "bar", "baz", "qux", "quux", "quuz", "aaa", "bbb", "ccc", "ddd", "eee", "xxx",
30 "yyy", "zzz", "test1", "test2", "test3",
31 ];
32
33 let mut issues = Vec::new();
34
35 for group in &captures {
36 if let Some(cap) = group.first() {
37 let name = cap.text.to_lowercase();
38 let chars: Vec<char> = name.chars().collect();
39 let is_repeating = chars.len() >= 3 && chars.iter().all(|c| *c == chars[0]);
40 let is_meaningless = meaningless.contains(&name.as_str()) || is_repeating;
41 if is_meaningless {
42 let msgs = [
43 format!(
44 "Variable '{}'? Did you fall asleep on the keyboard?",
45 cap.text
46 ),
47 format!("'{}'? Naming is hard, but this is just sad", cap.text),
48 format!(
49 "A variable named '{}'? I've seen better names in random tests",
50 cap.text
51 ),
52 format!(
53 "'{}' is not a real variable name, it's a cry for help",
54 cap.text
55 ),
56 format!(
57 "Congratulations on naming a variable '{}' — truly innovative",
58 cap.text
59 ),
60 ];
61 let pos = cap.node.start_position();
62 let severity = if matches!(name.as_str(), "foo" | "bar" | "baz") {
63 Severity::Spicy
64 } else {
65 Severity::Mild
66 };
67 issues.push(CodeIssue {
68 file_path: file.path.clone(),
69 line: pos.row + 1,
70 column: pos.column + 1,
71 rule_name: "meaningless-naming".to_string(),
72 message: msgs[issues.len() % msgs.len()].clone(),
73 severity,
74 });
75 }
76 }
77 }
78 issues
79 }
80}
81
82pub(crate) struct CommentedCodeRule;
84
85impl TreeSitterRule for CommentedCodeRule {
86 fn name(&self) -> &'static str {
87 "commented-code"
88 }
89
90 fn supported_languages(&self) -> &'static [Language] {
91 crate::language::LANGUAGES_WITH_GRAMMAR
92 }
93
94 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
95 let mut issues = Vec::new();
96 let mut block_start = 0;
97 let mut block_size = 0;
98
99 let line_comment = file.language.line_comment();
100
101 for (line_num, line) in file.content.lines().enumerate() {
102 let trimmed = line.trim();
103 if trimmed.starts_with(line_comment) {
104 let comment_text = trimmed.strip_prefix(line_comment).unwrap_or("").trim();
105 if is_likely_code(comment_text) {
106 if block_size == 0 {
107 block_start = line_num + 1;
108 }
109 block_size += 1;
110 } else if block_size > 0 {
111 emit_comment_block(&mut issues, file, block_start, block_size);
112 block_size = 0;
113 }
114 } else if !trimmed.is_empty() && block_size > 0 {
115 emit_comment_block(&mut issues, file, block_start, block_size);
116 block_size = 0;
117 }
118 }
119 if block_size > 0 {
120 emit_comment_block(&mut issues, file, block_start, block_size);
121 }
122 issues
123 }
124}
125
126fn is_likely_code(text: &str) -> bool {
127 let code_patterns = [
128 "fn ", "if ", "else", "for ", "while ", "match ", "struct ", "enum ", "impl ", "let ",
129 "return ", "use ", "mod ", "break", "continue", "{", "}", "(", ")", "[", "]", ";", "=",
130 "==", "!=", "&&", "||", "->", "::",
131 ];
132 let keywords = [
133 "pub", "const", "static", "mut", "ref", "move", "async", "await", "unsafe", "extern",
134 "crate", "def", "class", "import", "from", "lambda", "function", "var", "let", "const",
135 "if", "else",
136 ];
137 let pattern_count = code_patterns.iter().filter(|p| text.contains(*p)).count();
138 let keyword_count = keywords.iter().filter(|k| text.contains(*k)).count();
139 pattern_count >= 2 || keyword_count >= 1
140}
141
142fn emit_comment_block(
143 issues: &mut Vec<CodeIssue>,
144 file: &ParsedFile,
145 start_line: usize,
146 size: usize,
147) {
148 if size < 3 {
149 return;
150 }
151 let msgs = [
152 format!(
153 "{} lines of commented-out code — commit or delete, don't hoard",
154 size
155 ),
156 format!(
157 "Found {} lines of dead comment code. Git exists for a reason",
158 size
159 ),
160 format!(
161 "Commenting out code is like keeping an ex's photos. Let it go ({} lines)",
162 size
163 ),
164 ];
165 let severity = if size > 10 {
166 Severity::Spicy
167 } else {
168 Severity::Mild
169 };
170 issues.push(CodeIssue {
171 file_path: file.path.clone(),
172 line: start_line,
173 column: 1,
174 rule_name: "commented-code".to_string(),
175 message: msgs[issues.len() % msgs.len()].clone(),
176 severity,
177 });
178}
179
180pub(crate) struct DeadCodeRule;
182
183impl TreeSitterRule for DeadCodeRule {
184 fn name(&self) -> &'static str {
185 "dead-code"
186 }
187
188 fn supported_languages(&self) -> &'static [Language] {
189 &[Language::Rust]
190 }
191
192 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
193 let mut issues = Vec::new();
194 let mut dead_start: Option<usize> = None;
195 let mut reported = false;
196
197 for (line_num, line) in file.content.lines().enumerate() {
198 let trimmed = line.trim();
199
200 if is_terminator(trimmed) {
201 dead_start = Some(line_num + 2);
202 reported = false;
203 continue;
204 }
205
206 if let Some(start) = dead_start {
207 if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("/*") {
208 continue;
209 }
210 if trimmed == "}" || trimmed.starts_with("} else") {
211 dead_start = None;
212 continue;
213 }
214 if !reported && line_num + 1 >= start {
215 let msgs = [
216 "Dead code detected — this code never executes, like my健身计划",
217 "Unreachable code! Return already happened, this is just decoration",
218 "Dead code walking... nothing after 'return' ever runs",
219 ];
220 issues.push(CodeIssue {
221 file_path: file.path.clone(),
222 line: line_num + 1,
223 column: 1,
224 rule_name: "dead-code".to_string(),
225 message: msgs[issues.len() % msgs.len()].to_string(),
226 severity: Severity::Mild,
227 });
228 reported = true;
229 }
230 }
231 }
232 issues
233 }
234}
235
236fn is_terminator(line: &str) -> bool {
237 let trimmed = line.trim();
238 matches!(
239 trimmed,
240 "return;" | "break;" | "continue;" | "unreachable!()" | "unreachable!();"
241 ) || (trimmed.starts_with("return ") && trimmed.ends_with(';'))
242 || (trimmed.starts_with("panic!(") && trimmed.ends_with(';'))
243 || (trimmed.starts_with("unreachable!(") && trimmed.ends_with(')'))
244}
245
246pub(crate) struct TodoCommentRule;
248
249impl TreeSitterRule for TodoCommentRule {
250 fn name(&self) -> &'static str {
251 "todo-comment"
252 }
253
254 fn supported_languages(&self) -> &'static [Language] {
255 crate::language::LANGUAGES_WITH_GRAMMAR
256 }
257
258 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
259 let mut issues = Vec::new();
260 let mut total_todos = 0;
261
262 for pattern in &[
264 "(macro_invocation macro: (identifier) @m (#eq? @m \"todo\"))",
265 "(macro_invocation macro: (identifier) @m (#eq? @m \"unimplemented\"))",
266 "(macro_invocation macro: (identifier) @m (#eq? @m \"unreachable\"))",
267 ] {
268 if let Ok(caps) = collect_captures(file, pattern) {
269 total_todos += caps.iter().map(|c| c.len()).sum::<usize>();
270 }
271 }
272
273 let line_comment = file.language.line_comment();
274 for (line_num, line) in file.content.lines().enumerate() {
275 let trimmed = line.trim();
276 if let Some(pos) = trimmed.find(line_comment) {
277 let comment = trimmed[pos + line_comment.len()..].trim();
278 let upper = comment.to_uppercase();
279
280 let has_todo = upper.starts_with("TODO") || upper.contains(" TODO ");
281 let has_fixme = upper.starts_with("FIXME") || upper.contains(" FIXME ");
282 let has_bug = upper.starts_with("BUG") || upper.contains(" BUG ");
283 let has_hack = upper.starts_with("HACK") || upper.contains(" HACK ");
284
285 if has_todo {
286 total_todos += 1;
287 }
288 if has_fixme {
289 issues.push(CodeIssue {
290 file_path: file.path.clone(),
291 line: line_num + 1,
292 column: pos + 1,
293 rule_name: "todo-fixme".to_string(),
294 message: format!("FIXME: {}", comment.trim()),
295 severity: Severity::Mild,
296 });
297 }
298 if has_bug {
299 issues.push(CodeIssue {
300 file_path: file.path.clone(),
301 line: line_num + 1,
302 column: pos + 1,
303 rule_name: "todo-bug".to_string(),
304 message: format!("BUG: {}", comment.trim()),
305 severity: Severity::Spicy,
306 });
307 }
308 if has_hack {
309 issues.push(CodeIssue {
310 file_path: file.path.clone(),
311 line: line_num + 1,
312 column: pos + 1,
313 rule_name: "todo-hack".to_string(),
314 message: format!("HACK: {}", comment.trim()),
315 severity: Severity::Mild,
316 });
317 }
318 }
319 }
320
321 if total_todos > 3 {
322 let sev = if total_todos > 10 {
323 Severity::Spicy
324 } else {
325 Severity::Mild
326 };
327 let msgs = [
328 format!(
329 "Found {} TODO markers — your backlog must be terrifying",
330 total_todos
331 ),
332 format!(
333 "{} TODOs left in code. Future you will hate present you",
334 total_todos
335 ),
336 format!("{} unfinished tasks. Ship now, fix later?", total_todos),
337 ];
338 issues.push(CodeIssue {
339 file_path: file.path.clone(),
340 line: 1,
341 column: 1,
342 rule_name: "todo-comment".to_string(),
343 message: msgs[issues.len() % msgs.len()].clone(),
344 severity: sev,
345 });
346 }
347
348 issues
349 }
350}
351
352pub(crate) struct DuplicateImportsRule;
354
355impl TreeSitterRule for DuplicateImportsRule {
356 fn name(&self) -> &'static str {
357 "duplicate-imports"
358 }
359
360 fn supported_languages(&self) -> &'static [Language] {
361 &[Language::Rust]
362 }
363
364 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
365 let mut seen = std::collections::HashSet::new();
366 let mut issues = Vec::new();
367 let mut first_use_line = None;
368
369 for (line_num, line) in file.content.lines().enumerate() {
370 let trimmed = line.trim();
371 if trimmed.starts_with("use ") {
372 if first_use_line.is_none() {
373 first_use_line = Some(line_num + 1);
374 }
375 if !seen.insert(trimmed.to_string()) {
376 let msgs = [
377 format!(
378 "Duplicate import '{}' — reading comprehension matters",
379 trimmed
380 ),
381 format!(
382 "Importing '{}' twice doesn't make it more imported",
383 trimmed
384 ),
385 format!("You already imported '{}' once. That was enough", trimmed),
386 ];
387 issues.push(CodeIssue {
388 file_path: file.path.clone(),
389 line: line_num + 1,
390 column: 1,
391 rule_name: "duplicate-imports".to_string(),
392 message: msgs[issues.len() % msgs.len()].clone(),
393 severity: Severity::Mild,
394 });
395 }
396 }
397 }
398 issues
399 }
400}
401
402pub(crate) struct FileTooLongRule;
404
405impl TreeSitterRule for FileTooLongRule {
406 fn name(&self) -> &'static str {
407 "file-too-long"
408 }
409
410 fn supported_languages(&self) -> &'static [Language] {
411 crate::language::LANGUAGES_WITH_GRAMMAR
412 }
413
414 fn check(&self, file: &ParsedFile) -> Vec<CodeIssue> {
415 let line_count = file.content.lines().count();
416 let is_test = file.path.to_string_lossy().contains("test");
417 let threshold = if is_test { 2000 } else { 1000 };
418
419 if line_count > threshold {
420 let msgs = [
421 format!(
422 "{} lines! Is this a file or a novel? Split it up",
423 line_count
424 ),
425 format!(
426 "This file has {} lines. Your editor is judging you",
427 line_count
428 ),
429 format!(
430 "{} lines in one file — that's not 'modular', that's a disaster",
431 line_count
432 ),
433 ];
434 let severity = if line_count > 2000 {
435 Severity::Nuclear
436 } else if line_count > 1500 {
437 Severity::Spicy
438 } else {
439 Severity::Mild
440 };
441 vec![CodeIssue {
442 file_path: file.path.clone(),
443 line: 1,
444 column: 1,
445 rule_name: "file-too-long".to_string(),
446 message: msgs[0].clone(),
447 severity,
448 }]
449 } else {
450 vec![]
451 }
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use crate::treesitter::TreeSitterEngine;
459 use std::path::Path;
460
461 fn parse_rust(code: &str) -> ParsedFile {
462 let engine = TreeSitterEngine::new();
463 engine
464 .parse_file(Path::new("test.rs"), code)
465 .expect("Should parse")
466 }
467
468 #[test]
469 fn test_meaningless_names_detected() {
470 let file = parse_rust("fn main() { let foo = 1; let aaa = 2; let xxx = 3; }");
471 let rule = MeaninglessRule;
472 let issues = rule.check(&file);
473 assert!(issues.len() >= 3, "Should detect foo, aaa, xxx");
474 }
475
476 #[test]
477 fn test_meaningful_names_clean() {
478 let file = parse_rust("fn main() { let user_count = 1; let max_retries = 3; }");
479 let rule = MeaninglessRule;
480 let issues = rule.check(&file);
481 assert!(issues.is_empty(), "Good names should not trigger");
482 }
483
484 #[test]
485 fn test_commented_code_detected() {
486 let file = parse_rust(
487 r#"
488fn main() {
489 // let x = foo();
490 // let y = bar();
491 // let z = baz();
492 println!("real");
493}
494"#,
495 );
496 let rule = CommentedCodeRule;
497 let issues = rule.check(&file);
498 assert!(!issues.is_empty(), "Should detect commented-out code");
499 }
500
501 #[test]
502 fn test_todo_comment_detected() {
503 let file = parse_rust(
504 r#"
505fn main() {
506 // TODO: implement this
507 // FIXME: this is broken
508 // TODO: also this
509 // TODO: and one more
510 // XXX: cleanup required
511 todo!();
512}
513"#,
514 );
515 let rule = TodoCommentRule;
516 let issues = rule.check(&file);
517 let rule_names: Vec<_> = issues.iter().map(|i| i.rule_name.as_str()).collect();
518 assert!(rule_names.contains(&"todo-fixme"), "Should detect FIXME");
519 assert!(rule_names.contains(&"todo-comment"), "Should detect TODO");
520 }
521
522 #[test]
523 fn test_dead_code_detected() {
524 let file = parse_rust(
525 r#"
526fn main() {
527 return;
528 let x = 1;
529}
530"#,
531 );
532 let rule = DeadCodeRule;
533 let issues = rule.check(&file);
534 assert!(!issues.is_empty(), "Should detect dead code after return");
535 }
536
537 #[test]
538 fn test_duplicate_imports_detected() {
539 let file = parse_rust(
540 r#"
541use std::collections::HashMap;
542use std::collections::HashMap;
543fn main() {}
544"#,
545 );
546 let rule = DuplicateImportsRule;
547 let issues = rule.check(&file);
548 assert!(!issues.is_empty(), "Should detect duplicate import");
549 }
550}