1use std::path::Path;
2use syn::{
3 visit::Visit, Expr, ExprForLoop, ExprMatch, ExprMethodCall, File, Pat, PatIdent, Type, TypePath,
4};
5
6use crate::analyzer::{CodeIssue, RoastLevel, Severity};
7use crate::rules::Rule;
8use crate::utils::get_position;
9
10pub struct StringAbuseRule;
12
13impl Rule for StringAbuseRule {
14 fn name(&self) -> &'static str {
15 "string-abuse"
16 }
17
18 fn check(
19 &self,
20 file_path: &Path,
21 syntax_tree: &File,
22 content: &str,
23 lang: &str,
24 ) -> Vec<CodeIssue> {
25 let mut visitor = StringAbuseVisitor::new(file_path.to_path_buf(), lang);
26 visitor.visit_file(syntax_tree);
27
28 let string_new_count = content.matches("String::new()").count();
30 let string_from_count = content.matches("String::from(").count();
31 let to_string_count = content.matches(".to_string()").count();
32
33 if string_new_count + string_from_count + to_string_count > 5 {
34 visitor.add_excessive_string_conversion_issue(
35 string_new_count + string_from_count + to_string_count,
36 );
37 }
38
39 visitor.issues
40 }
41}
42
43pub struct VecAbuseRule;
45
46impl Rule for VecAbuseRule {
47 fn name(&self) -> &'static str {
48 "vec-abuse"
49 }
50
51 fn check(
52 &self,
53 file_path: &Path,
54 syntax_tree: &File,
55 content: &str,
56 lang: &str,
57 ) -> Vec<CodeIssue> {
58 let mut visitor = VecAbuseVisitor::new(file_path.to_path_buf(), lang);
59 visitor.visit_file(syntax_tree);
60
61 let vec_new_count = content.matches("Vec::new()").count();
63 let _vec_with_capacity_count = content.matches("Vec::with_capacity(").count();
64 let _vec_macro_count = content.matches("vec![").count();
65
66 if vec_new_count > 3 {
67 visitor.add_excessive_vec_allocation_issue(vec_new_count);
68 }
69
70 visitor.issues
71 }
72}
73
74pub struct IteratorAbuseRule;
76
77impl Rule for IteratorAbuseRule {
78 fn name(&self) -> &'static str {
79 "iterator-abuse"
80 }
81
82 fn check(
83 &self,
84 file_path: &Path,
85 syntax_tree: &File,
86 _content: &str,
87 lang: &str,
88 ) -> Vec<CodeIssue> {
89 let mut visitor = IteratorAbuseVisitor::new(file_path.to_path_buf(), lang);
90 visitor.visit_file(syntax_tree);
91 visitor.issues
92 }
93}
94
95pub struct MatchAbuseRule;
97
98impl Rule for MatchAbuseRule {
99 fn name(&self) -> &'static str {
100 "match-abuse"
101 }
102
103 fn check(
104 &self,
105 file_path: &Path,
106 syntax_tree: &File,
107 _content: &str,
108 lang: &str,
109 ) -> Vec<CodeIssue> {
110 let mut visitor = MatchAbuseVisitor::new(file_path.to_path_buf(), lang);
111 visitor.visit_file(syntax_tree);
112 visitor.issues
113 }
114}
115
116struct StringAbuseVisitor {
121 file_path: std::path::PathBuf,
122 issues: Vec<CodeIssue>,
123 lang: String,
124}
125
126impl StringAbuseVisitor {
127 fn new(file_path: std::path::PathBuf, lang: &str) -> Self {
128 Self {
129 file_path,
130 issues: Vec::new(),
131 lang: lang.to_string(),
132 }
133 }
134
135 fn add_excessive_string_conversion_issue(&mut self, count: usize) {
136 let messages = if self.lang == "zh-CN" {
137 vec![
138 format!("{} 次 String 转换?你是在开字符串工厂吗?", count),
139 format!("这么多 String 分配,内存都要哭了",),
140 format!("{} 个 String 转换,考虑用 &str 吧", count),
141 format!("String 用得比我换衣服还频繁",),
142 format!("到处都是 String::from(),性能在哭泣",),
143 ]
144 } else {
145 vec![
146 format!(
147 "{} String conversions? Are you running a string factory?",
148 count
149 ),
150 format!("So many String allocations, memory is crying"),
151 format!("{} String conversions - consider using &str", count),
152 format!("You use String more than I change clothes"),
153 format!("String::from() everywhere - performance is weeping"),
154 ]
155 };
156
157 self.issues.push(CodeIssue {
158 file_path: self.file_path.clone(),
159 line: 1,
160 column: 1,
161 rule_name: "string-abuse".to_string(),
162 message: messages[count % messages.len()].clone(),
163 severity: Severity::Spicy,
164 roast_level: RoastLevel::Sarcastic,
165 });
166 }
167
168 fn check_string_parameter(&mut self, ty: &Type) {
169 if let Type::Path(TypePath { path, .. }) = ty {
170 if let Some(segment) = path.segments.last() {
171 if segment.ident == "String" {
172 let messages = if self.lang == "zh-CN" {
173 vec![
174 "参数用 String?考虑用 &str 吧",
175 "String 参数会强制调用者分配内存",
176 "这个 String 参数设计得不太优雅",
177 "用 &str 参数会更灵活",
178 ]
179 } else {
180 vec![
181 "String parameter? Consider using &str",
182 "String parameter forces caller to allocate",
183 "This String parameter design isn't elegant",
184 "&str parameter would be more flexible",
185 ]
186 };
187
188 let (line, column) = get_position(ty);
189 self.issues.push(CodeIssue {
190 file_path: self.file_path.clone(),
191 line,
192 column,
193 rule_name: "string-abuse".to_string(),
194 message: messages[self.issues.len() % messages.len()].to_string(),
195 severity: Severity::Mild,
196 roast_level: RoastLevel::Gentle,
197 });
198 }
199 }
200 }
201 }
202}
203
204impl<'ast> Visit<'ast> for StringAbuseVisitor {
205 fn visit_expr_method_call(&mut self, method_call: &'ast ExprMethodCall) {
206 if method_call.method == "to_string" {
207 let messages = if self.lang == "zh-CN" {
208 vec![
209 "又见 to_string(),真的需要分配内存吗?",
210 "to_string() 调用,考虑是否必要",
211 "这个 to_string() 可能可以避免",
212 "to_string() 会分配内存,确定需要吗?",
213 ]
214 } else {
215 vec![
216 "Another to_string() - do you really need to allocate?",
217 "to_string() call - consider if necessary",
218 "This to_string() might be avoidable",
219 "to_string() allocates memory - sure you need it?",
220 ]
221 };
222
223 let (line, column) = get_position(method_call);
224 self.issues.push(CodeIssue {
225 file_path: self.file_path.clone(),
226 line,
227 column,
228 rule_name: "string-abuse".to_string(),
229 message: messages[self.issues.len() % messages.len()].to_string(),
230 severity: Severity::Mild,
231 roast_level: RoastLevel::Gentle,
232 });
233 }
234 syn::visit::visit_expr_method_call(self, method_call);
235 }
236
237 fn visit_type(&mut self, ty: &'ast Type) {
238 self.check_string_parameter(ty);
239 syn::visit::visit_type(self, ty);
240 }
241}
242
243struct VecAbuseVisitor {
248 file_path: std::path::PathBuf,
249 issues: Vec<CodeIssue>,
250 lang: String,
251}
252
253impl VecAbuseVisitor {
254 fn new(file_path: std::path::PathBuf, lang: &str) -> Self {
255 Self {
256 file_path,
257 issues: Vec::new(),
258 lang: lang.to_string(),
259 }
260 }
261
262 fn add_excessive_vec_allocation_issue(&mut self, count: usize) {
263 let messages = if self.lang == "zh-CN" {
264 vec![
265 format!("{} 个 Vec::new()?你在收集什么?", count),
266 format!("这么多 Vec 分配,考虑用数组或切片",),
267 format!("{} 次 Vec 分配,内存分配器很忙", count),
268 format!("Vec 用得这么多,确定都需要动态数组吗?",),
269 ]
270 } else {
271 vec![
272 format!("{} Vec::new()s? What are you collecting?", count),
273 format!("So many Vec allocations - consider arrays or slices"),
274 format!("{} Vec allocations - memory allocator is busy", count),
275 format!("So many Vecs - sure you need all these dynamic arrays?"),
276 ]
277 };
278
279 self.issues.push(CodeIssue {
280 file_path: self.file_path.clone(),
281 line: 1,
282 column: 1,
283 rule_name: "vec-abuse".to_string(),
284 message: messages[count % messages.len()].clone(),
285 severity: Severity::Mild,
286 roast_level: RoastLevel::Gentle,
287 });
288 }
289}
290
291impl<'ast> Visit<'ast> for VecAbuseVisitor {
292 fn visit_expr_method_call(&mut self, method_call: &'ast ExprMethodCall) {
293 if let Expr::Path(path_expr) = &*method_call.receiver {
295 if let Some(segment) = path_expr.path.segments.last() {
296 if segment.ident == "Vec" && method_call.method == "new" {
297 let messages = if self.lang == "zh-CN" {
298 vec![
299 "Vec::new() 出现,确定需要动态数组吗?",
300 "又一个 Vec::new(),考虑用数组",
301 "Vec::new() 会分配堆内存",
302 "这个 Vec::new() 可能可以优化",
303 ]
304 } else {
305 vec![
306 "Vec::new() spotted - sure you need a dynamic array?",
307 "Another Vec::new() - consider using an array",
308 "Vec::new() allocates heap memory",
309 "This Vec::new() might be optimizable",
310 ]
311 };
312
313 let (line, column) = get_position(method_call);
314 self.issues.push(CodeIssue {
315 file_path: self.file_path.clone(),
316 line,
317 column,
318 rule_name: "vec-abuse".to_string(),
319 message: messages[self.issues.len() % messages.len()].to_string(),
320 severity: Severity::Mild,
321 roast_level: RoastLevel::Gentle,
322 });
323 }
324 }
325 }
326 syn::visit::visit_expr_method_call(self, method_call);
327 }
328}
329
330struct IteratorAbuseVisitor {
335 file_path: std::path::PathBuf,
336 issues: Vec<CodeIssue>,
337 lang: String,
338}
339
340impl IteratorAbuseVisitor {
341 fn new(file_path: std::path::PathBuf, lang: &str) -> Self {
342 Self {
343 file_path,
344 issues: Vec::new(),
345 lang: lang.to_string(),
346 }
347 }
348
349 fn check_simple_for_loop(&mut self, for_loop: &ExprForLoop) {
350 if let Pat::Ident(PatIdent { ident, .. }) = for_loop.pat.as_ref() {
352 let _var_name = ident.to_string();
353
354 let loop_body = format!("{:?}", for_loop.body);
356
357 if loop_body.lines().count() < 5 {
359 let messages = if self.lang == "zh-CN" {
360 vec![
361 format!("简单的 for 循环,考虑用迭代器链"),
362 format!("这个循环可以用 .iter().for_each() 替代"),
363 format!("迭代器比传统循环更 Rust 风格"),
364 format!("考虑用函数式编程风格重写这个循环"),
365 ]
366 } else {
367 vec![
368 format!("Simple for loop - consider using iterator chains"),
369 format!("This loop could use .iter().for_each() instead"),
370 format!("Iterators are more idiomatic than traditional loops"),
371 format!("Consider rewriting this loop in functional style"),
372 ]
373 };
374
375 let (line, column) = get_position(for_loop);
376 self.issues.push(CodeIssue {
377 file_path: self.file_path.clone(),
378 line,
379 column,
380 rule_name: "iterator-abuse".to_string(),
381 message: messages[self.issues.len() % messages.len()].to_string(),
382 severity: Severity::Mild,
383 roast_level: RoastLevel::Gentle,
384 });
385 }
386 }
387 }
388}
389
390impl<'ast> Visit<'ast> for IteratorAbuseVisitor {
391 fn visit_expr_for_loop(&mut self, for_loop: &'ast ExprForLoop) {
392 self.check_simple_for_loop(for_loop);
393 syn::visit::visit_expr_for_loop(self, for_loop);
394 }
395}
396
397struct MatchAbuseVisitor {
402 file_path: std::path::PathBuf,
403 issues: Vec<CodeIssue>,
404 lang: String,
405}
406
407impl MatchAbuseVisitor {
408 fn new(file_path: std::path::PathBuf, lang: &str) -> Self {
409 Self {
410 file_path,
411 issues: Vec::new(),
412 lang: lang.to_string(),
413 }
414 }
415
416 fn check_simple_match(&mut self, match_expr: &ExprMatch) {
417 let arms_count = match_expr.arms.len();
418
419 if arms_count == 2 {
421 let match_str = format!("{match_expr:?}");
422
423 if match_str.contains("Some") && match_str.contains("None") {
425 let messages = if self.lang == "zh-CN" {
426 vec![
427 "简单的 Option match,考虑用 if let",
428 "这个 match 可以用 if let Some() 替代",
429 "Option 的二分支 match 不如 if let 简洁",
430 "if let 比 match 更适合这种情况",
431 ]
432 } else {
433 vec![
434 "Simple Option match - consider using if let",
435 "This match could use if let Some() instead",
436 "Two-arm Option match is less concise than if let",
437 "if let is more suitable for this case than match",
438 ]
439 };
440
441 let (line, column) = get_position(match_expr);
442 self.issues.push(CodeIssue {
443 file_path: self.file_path.clone(),
444 line,
445 column,
446 rule_name: "match-abuse".to_string(),
447 message: messages[self.issues.len() % messages.len()].to_string(),
448 severity: Severity::Mild,
449 roast_level: RoastLevel::Gentle,
450 });
451 }
452
453 if match_str.contains("Ok") && match_str.contains("Err") {
454 let messages = if self.lang == "zh-CN" {
455 vec![
456 "简单的 Result match,考虑用 if let",
457 "这个 match 可以用 if let Ok() 替代",
458 "Result 的二分支 match 可以简化",
459 "或者考虑用 ? 操作符",
460 ]
461 } else {
462 vec![
463 "Simple Result match - consider using if let",
464 "This match could use if let Ok() instead",
465 "Two-arm Result match can be simplified",
466 "Or consider using the ? operator",
467 ]
468 };
469
470 let (line, column) = get_position(match_expr);
471 self.issues.push(CodeIssue {
472 file_path: self.file_path.clone(),
473 line,
474 column,
475 rule_name: "match-abuse".to_string(),
476 message: messages[self.issues.len() % messages.len()].to_string(),
477 severity: Severity::Mild,
478 roast_level: RoastLevel::Gentle,
479 });
480 }
481 }
482 }
483}
484
485impl<'ast> Visit<'ast> for MatchAbuseVisitor {
486 fn visit_expr_match(&mut self, match_expr: &'ast ExprMatch) {
487 self.check_simple_match(match_expr);
488 syn::visit::visit_expr_match(self, match_expr);
489 }
490}