1#![allow(dead_code)]
14
15use crate::{
16 Confidence, Exploitability, Finding, MirFunction, MirPackage, Rule, RuleMetadata, RuleOrigin,
17 Severity, SourceFile,
18};
19use std::collections::HashMap;
20use std::path::Path;
21
22pub struct CrateWideAllowRule {
28 metadata: RuleMetadata,
29}
30
31impl CrateWideAllowRule {
32 pub fn new() -> Self {
33 Self {
34 metadata: RuleMetadata {
35 id: "RUSTCOLA049".to_string(),
36 name: "crate-wide-allow".to_string(),
37 short_description: "Crate-wide allow attribute disables lints".to_string(),
38 full_description: "Detects crate-level #![allow(...)] attributes that disable \
39 lints for the entire crate. This reduces security coverage. Use more \
40 targeted #[allow(...)] on specific items instead."
41 .to_string(),
42 help_uri: None,
43 default_severity: Severity::Low,
44 origin: RuleOrigin::BuiltIn,
45 cwe_ids: Vec::new(),
46 fix_suggestion: None,
47 exploitability: Exploitability::default(),
48 },
49 }
50 }
51
52 fn has_crate_wide_allow(line: &str) -> bool {
53 line.trim().starts_with("#![allow")
54 }
55
56 fn extract_allowed_lints(line: &str) -> Vec<String> {
57 if let Some(start) = line.find("#![allow(") {
58 if let Some(end) = line[start..].find(')') {
59 let content = &line[start + 9..start + end];
60 return content
61 .split(',')
62 .map(|s| s.trim().to_string())
63 .filter(|s| !s.is_empty())
64 .collect();
65 }
66 }
67 Vec::new()
68 }
69}
70
71impl Rule for CrateWideAllowRule {
72 fn metadata(&self) -> &RuleMetadata {
73 &self.metadata
74 }
75
76 fn evaluate(
77 &self,
78 package: &MirPackage,
79 _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
80 ) -> Vec<Finding> {
81 let mut findings = Vec::new();
82 let mut reported = false;
83
84 for function in &package.functions {
85 for line in &function.body {
86 if Self::has_crate_wide_allow(line) && !reported {
87 let lints = Self::extract_allowed_lints(line);
88 let lint_list = if lints.is_empty() {
89 "unknown lints".to_string()
90 } else {
91 lints.join(", ")
92 };
93
94 findings.push(Finding {
95 rule_id: self.metadata.id.clone(),
96 rule_name: self.metadata.name.clone(),
97 severity: self.metadata.default_severity,
98 message: format!(
99 "Crate-wide #![allow(...)] disables lints for entire crate: {}. \
100 Consider item-level #[allow(...)] for more targeted suppression.",
101 lint_list
102 ),
103 function: function.name.clone(),
104 function_signature: function.signature.clone(),
105 evidence: vec![line.clone()],
106 span: function.span.clone(),
107 ..Default::default()
108 });
109 reported = true;
110 break;
111 }
112 }
113 if reported {
114 break;
115 }
116 }
117
118 findings
119 }
120}
121
122pub struct MisorderedAssertEqRule {
128 metadata: RuleMetadata,
129}
130
131impl MisorderedAssertEqRule {
132 pub fn new() -> Self {
133 Self {
134 metadata: RuleMetadata {
135 id: "RUSTCOLA050".to_string(),
136 name: "misordered-assert-eq".to_string(),
137 short_description: "assert_eq arguments may be misordered".to_string(),
138 full_description: "Detects assert_eq! calls where a literal or constant appears \
139 as the first argument instead of the second. Convention is assert_eq!(actual, expected) \
140 so error messages show 'expected X but got Y' correctly.".to_string(),
141 help_uri: None,
142 default_severity: Severity::Low,
143 origin: RuleOrigin::BuiltIn,
144 cwe_ids: Vec::new(),
145 fix_suggestion: None,
146 exploitability: Exploitability::default(),
147 },
148 }
149 }
150
151 fn looks_like_misordered_assert(&self, function: &MirFunction) -> bool {
152 let mut has_misordered_promoted = false;
153 let mut has_assert_failed = false;
154
155 for line in &function.body {
156 let trimmed = line.trim();
157
158 if trimmed.starts_with("_3 = const") && trimmed.contains("::promoted[") {
160 has_misordered_promoted = true;
161 }
162
163 if trimmed.contains("assert_failed") {
164 has_assert_failed = true;
165 }
166 }
167
168 has_misordered_promoted && has_assert_failed
169 }
170}
171
172impl Rule for MisorderedAssertEqRule {
173 fn metadata(&self) -> &RuleMetadata {
174 &self.metadata
175 }
176
177 fn evaluate(
178 &self,
179 package: &MirPackage,
180 _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
181 ) -> Vec<Finding> {
182 let mut findings = Vec::new();
183
184 for function in &package.functions {
185 if self.looks_like_misordered_assert(function) {
186 let mut evidence = vec![];
187 for line in &function.body {
188 if line.contains("::promoted[") || line.contains("assert_failed") {
189 evidence.push(line.clone());
190 }
191 }
192
193 findings.push(Finding {
194 rule_id: self.metadata.id.clone(),
195 rule_name: self.metadata.name.clone(),
196 severity: self.metadata.default_severity,
197 message: "assert_eq! may have misordered arguments. Convention is \
198 assert_eq!(actual, expected) where 'expected' is typically a literal."
199 .to_string(),
200 function: function.name.clone(),
201 function_signature: function.signature.clone(),
202 evidence,
203 span: function.span.clone(),
204 ..Default::default()
205 });
206 }
207 }
208
209 findings
210 }
211}
212
213pub struct TryIoResultRule {
219 metadata: RuleMetadata,
220}
221
222impl TryIoResultRule {
223 pub fn new() -> Self {
224 Self {
225 metadata: RuleMetadata {
226 id: "RUSTCOLA051".to_string(),
227 name: "try-io-result".to_string(),
228 short_description: "Try operator (?) used on io::Result".to_string(),
229 full_description: "Detects use of the ? operator on std::io::Result, which can \
230 obscure IO errors. Prefer explicit error handling with .map_err() to add context.".to_string(),
231 help_uri: None,
232 default_severity: Severity::Low,
233 origin: RuleOrigin::BuiltIn,
234 cwe_ids: Vec::new(),
235 fix_suggestion: None,
236 exploitability: Exploitability::default(),
237 },
238 }
239 }
240
241 fn looks_like_io_result_try(&self, function: &MirFunction) -> bool {
242 let mut has_io_error_type = false;
243 let mut has_discriminant_check = false;
244
245 if function.signature.contains("std::io::Error") || function.signature.contains("io::Error")
246 {
247 has_io_error_type = true;
248 }
249
250 for line in &function.body {
251 if line.contains("discriminant(") {
252 has_discriminant_check = true;
253 break;
254 }
255 }
256
257 has_io_error_type && has_discriminant_check
258 }
259}
260
261impl Rule for TryIoResultRule {
262 fn metadata(&self) -> &RuleMetadata {
263 &self.metadata
264 }
265
266 fn evaluate(
267 &self,
268 package: &MirPackage,
269 _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
270 ) -> Vec<Finding> {
271 let mut findings = Vec::new();
272
273 for function in &package.functions {
274 if self.looks_like_io_result_try(function) {
275 let mut evidence = vec![];
276 for line in &function.body {
277 if line.to_lowercase().contains("io::error")
278 || line.to_lowercase().contains("discriminant")
279 {
280 evidence.push(line.clone());
281 if evidence.len() >= 3 {
282 break;
283 }
284 }
285 }
286
287 findings.push(Finding {
288 rule_id: self.metadata.id.clone(),
289 rule_name: self.metadata.name.clone(),
290 severity: self.metadata.default_severity,
291 message: "Using ? operator on io::Result may lose error context. \
292 Consider using .map_err() to add file paths or operation details."
293 .to_string(),
294 function: function.name.clone(),
295 function_signature: function.signature.clone(),
296 evidence,
297 span: function.span.clone(),
298 ..Default::default()
299 });
300 }
301 }
302
303 findings
304 }
305}
306
307pub struct LocalRefCellRule {
313 metadata: RuleMetadata,
314}
315
316impl LocalRefCellRule {
317 pub fn new() -> Self {
318 Self {
319 metadata: RuleMetadata {
320 id: "RUSTCOLA052".to_string(),
321 name: "local-ref-cell".to_string(),
322 short_description: "RefCell used for local mutable state".to_string(),
323 full_description: "Detects RefCell<T> used for purely local mutable state where \
324 a regular mutable variable would suffice. RefCell adds runtime borrow \
325 checking overhead and panic risk."
326 .to_string(),
327 help_uri: None,
328 default_severity: Severity::Low,
329 origin: RuleOrigin::BuiltIn,
330 cwe_ids: Vec::new(),
331 fix_suggestion: None,
332 exploitability: Exploitability::default(),
333 },
334 }
335 }
336
337 fn looks_like_local_refcell(&self, function: &MirFunction) -> bool {
338 let mut has_refcell_new = false;
339 let mut has_borrow_mut = false;
340
341 for line in &function.body {
342 let lower = line.to_lowercase();
343
344 if lower.contains("refcell") && lower.contains("::new") {
345 has_refcell_new = true;
346 }
347
348 if lower.contains("borrow_mut")
349 || (lower.contains("borrow(") && !lower.contains("borrow_mut"))
350 {
351 has_borrow_mut = true;
352 }
353 }
354
355 has_refcell_new && has_borrow_mut
356 }
357}
358
359impl Rule for LocalRefCellRule {
360 fn metadata(&self) -> &RuleMetadata {
361 &self.metadata
362 }
363
364 fn evaluate(
365 &self,
366 package: &MirPackage,
367 _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
368 ) -> Vec<Finding> {
369 let mut findings = Vec::new();
370
371 for function in &package.functions {
372 if self.looks_like_local_refcell(function) {
373 let mut evidence = vec![];
374 for line in &function.body {
375 if line.to_lowercase().contains("refcell")
376 || line.to_lowercase().contains("borrow")
377 {
378 evidence.push(line.clone());
379 if evidence.len() >= 3 {
380 break;
381 }
382 }
383 }
384
385 findings.push(Finding {
386 rule_id: self.metadata.id.clone(),
387 rule_name: self.metadata.name.clone(),
388 severity: self.metadata.default_severity,
389 message: "RefCell used for local mutable state. Consider using a regular \
390 mutable variable. RefCell adds runtime overhead and panic risk."
391 .to_string(),
392 function: function.name.clone(),
393 function_signature: function.signature.clone(),
394 evidence,
395 span: function.span.clone(),
396 ..Default::default()
397 });
398 }
399 }
400
401 findings
402 }
403}
404
405pub struct UnnecessaryBorrowMutRule {
411 metadata: RuleMetadata,
412}
413
414impl UnnecessaryBorrowMutRule {
415 pub fn new() -> Self {
416 Self {
417 metadata: RuleMetadata {
418 id: "RUSTCOLA057".to_string(),
419 name: "unnecessary-borrow-mut".to_string(),
420 short_description: "Unnecessary borrow_mut() on RefCell".to_string(),
421 full_description: "Detects RefCell::borrow_mut() calls where the mutable borrow \
422 is never actually used for mutation. Using borrow_mut() when borrow() suffices \
423 creates unnecessary runtime overhead and increases panic risk."
424 .to_string(),
425 help_uri: None,
426 default_severity: Severity::Low,
427 origin: RuleOrigin::BuiltIn,
428 cwe_ids: Vec::new(),
429 fix_suggestion: None,
430 exploitability: Exploitability::default(),
431 },
432 }
433 }
434
435 fn looks_like_unnecessary_borrow_mut(&self, function: &MirFunction) -> bool {
436 let body_str = function.body.join("\n");
437
438 if function.name.contains("::new") {
439 return false;
440 }
441
442 let has_borrow_mut = body_str.contains("RefCell") && body_str.contains("borrow_mut");
443 if !has_borrow_mut {
444 return false;
445 }
446
447 let mutation_methods = [
449 "::push(",
450 "::insert(",
451 "::remove(",
452 "::clear(",
453 "::extend(",
454 "::swap(",
455 "::sort(",
456 "::reverse(",
457 "::drain(",
458 "::append(",
459 "::pop(",
460 "::entry(",
461 "::get_mut(",
462 ];
463 let has_mutation_method = mutation_methods.iter().any(|m| body_str.contains(m));
464 let has_refmut_deref_mut = body_str.contains("RefMut") && body_str.contains("DerefMut");
465 let has_index_mut = body_str.contains("IndexMut") || body_str.contains("index_mut");
466
467 let has_mutation = has_mutation_method || has_refmut_deref_mut || has_index_mut;
468
469 has_borrow_mut && !has_mutation
470 }
471}
472
473impl Rule for UnnecessaryBorrowMutRule {
474 fn metadata(&self) -> &RuleMetadata {
475 &self.metadata
476 }
477
478 fn evaluate(
479 &self,
480 package: &MirPackage,
481 _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
482 ) -> Vec<Finding> {
483 let mut findings = Vec::new();
484
485 for function in &package.functions {
486 if self.looks_like_unnecessary_borrow_mut(function) {
487 let mut evidence = Vec::new();
488
489 for line in &function.body {
490 if line.contains("borrow_mut") || line.contains("RefCell") {
491 evidence.push(line.trim().to_string());
492 if evidence.len() >= 5 {
493 break;
494 }
495 }
496 }
497
498 findings.push(Finding {
499 rule_id: self.metadata.id.clone(),
500 rule_name: self.metadata.name.clone(),
501 severity: self.metadata.default_severity,
502 message:
503 "RefCell::borrow_mut() called but mutable borrow may not be necessary. \
504 If only read (not modified), use borrow() instead."
505 .to_string(),
506 function: function.name.clone(),
507 function_signature: function.signature.clone(),
508 evidence,
509 span: function.span.clone(),
510 ..Default::default()
511 });
512 }
513 }
514
515 findings
516 }
517}
518
519pub struct DeadStoreArrayRule {
525 metadata: RuleMetadata,
526}
527
528impl DeadStoreArrayRule {
529 pub fn new() -> Self {
530 Self {
531 metadata: RuleMetadata {
532 id: "RUSTCOLA068".to_string(),
533 name: "dead-store-array".to_string(),
534 short_description: "Dead store in array".to_string(),
535 full_description: "Detects array elements that are written but never read before \
536 being overwritten or going out of scope. Dead stores can indicate logic errors \
537 or wasted computation."
538 .to_string(),
539 help_uri: None,
540 default_severity: Severity::Low,
541 origin: RuleOrigin::BuiltIn,
542 cwe_ids: Vec::new(),
543 fix_suggestion: None,
544 exploitability: Exploitability::default(),
545 },
546 }
547 }
548
549 fn is_array_write(line: &str) -> Option<(&str, &str)> {
550 let trimmed = line.trim();
551
552 if let Some(eq_pos) = trimmed.find(" = ") {
553 let left_side = trimmed[..eq_pos].trim();
554
555 if let Some(bracket_start) = left_side.find('[') {
556 if let Some(bracket_end) = left_side.find(']') {
557 if bracket_start < bracket_end && bracket_end == left_side.len() - 1 {
558 let var = left_side[..bracket_start].trim();
559 let index = left_side[bracket_start + 1..bracket_end].trim();
560
561 if var.starts_with('_') && !index.is_empty() {
562 return Some((var, index));
563 }
564 }
565 }
566 }
567 }
568
569 None
570 }
571
572 fn is_array_read(line: &str, var: &str) -> bool {
573 let trimmed = line.trim();
574
575 if trimmed.contains("(copy ") || trimmed.contains("(&") || trimmed.contains("(move ") {
577 let patterns = [
578 format!("(copy {})", var),
579 format!("(&{})", var),
580 format!("(move {})", var),
581 ];
582 if patterns.iter().any(|p| trimmed.contains(p)) {
583 return true;
584 }
585 }
586
587 let pattern = format!("{}[", var);
588 if !trimmed.contains(&pattern) {
589 return false;
590 }
591
592 if let Some(eq_pos) = trimmed.find(" = ") {
594 let left_side = trimmed[..eq_pos].trim();
595 if left_side.contains(&pattern) {
596 return false;
597 }
598 if trimmed[eq_pos + 3..].contains(&pattern) {
599 return true;
600 }
601 }
602
603 trimmed.contains(&pattern)
604 }
605}
606
607impl Rule for DeadStoreArrayRule {
608 fn metadata(&self) -> &RuleMetadata {
609 &self.metadata
610 }
611
612 fn evaluate(
613 &self,
614 package: &MirPackage,
615 _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
616 ) -> Vec<Finding> {
617 if package.crate_name == "mir-extractor" {
618 return Vec::new();
619 }
620
621 let mut findings = Vec::new();
622
623 for function in &package.functions {
624 if function.signature.contains("-> [") && function.signature.contains("; ") {
625 continue;
626 }
627 if function.signature.contains("&mut [") {
628 continue;
629 }
630
631 let mut const_values: HashMap<String, String> = HashMap::new();
632
633 for line in &function.body {
634 let trimmed = line.trim();
635 if let Some(eq_pos) = trimmed.find(" = const ") {
636 let left = trimmed[..eq_pos].trim();
637 let right = trimmed[eq_pos + 9..].trim();
638 if let Some(semicolon) = right.find(';') {
639 let value = right[..semicolon].trim();
640 const_values.insert(left.to_string(), value.to_string());
641 }
642 }
643 }
644
645 let mut all_writes: Vec<(usize, String, String, String)> = Vec::new();
646
647 for (line_idx, line) in function.body.iter().enumerate() {
648 let trimmed = line.trim();
649 if let Some((var, index)) = Self::is_array_write(trimmed) {
650 let resolved_index = const_values
651 .get(index)
652 .unwrap_or(&index.to_string())
653 .clone();
654 all_writes.push((line_idx, var.to_string(), resolved_index, line.clone()));
655 }
656 }
657
658 for (i, (write_line_idx, write_var, write_resolved_idx, write_line)) in
659 all_writes.iter().enumerate()
660 {
661 let key = format!("{}[{}]", write_var, write_resolved_idx);
662
663 for (
664 j,
665 (overwrite_line_idx, overwrite_var, overwrite_resolved_idx, overwrite_line),
666 ) in all_writes.iter().enumerate()
667 {
668 if j <= i {
669 continue;
670 }
671
672 let overwrite_key = format!("{}[{}]", overwrite_var, overwrite_resolved_idx);
673 if key != overwrite_key {
674 continue;
675 }
676
677 let mut has_read_between = false;
678 for (between_idx, between_line) in function.body.iter().enumerate() {
679 if between_idx <= *write_line_idx || between_idx >= *overwrite_line_idx {
680 continue;
681 }
682
683 let trimmed = between_line.trim();
684 if trimmed.starts_with("bb")
685 || trimmed.starts_with("goto")
686 || trimmed.starts_with("assert")
687 || trimmed.starts_with("switchInt")
688 || trimmed.starts_with("return")
689 {
690 continue;
691 }
692
693 if Self::is_array_read(trimmed, write_var) {
694 has_read_between = true;
695 break;
696 }
697 }
698
699 if !has_read_between {
700 findings.push(Finding {
701 rule_id: self.metadata.id.clone(),
702 rule_name: self.metadata.name.clone(),
703 severity: self.metadata.default_severity,
704 message: format!(
705 "Dead store: array element {} written but overwritten without read in `{}`",
706 key, function.name
707 ),
708 function: function.name.clone(),
709 function_signature: function.signature.clone(),
710 evidence: vec![
711 format!("Line {}: {}", write_line_idx, write_line.trim()),
712 format!("Line {}: {} (overwrites)", overwrite_line_idx, overwrite_line.trim()),
713 ],
714 span: function.span.clone(),
715 confidence: Confidence::Medium,
716 cwe_ids: Vec::new(),
717 fix_suggestion: None,
718 code_snippet: None,
719 exploitability: Exploitability::default(),
720 exploitability_score: Exploitability::default().score(),
721 ..Default::default()
722 });
723 break; }
725 }
726 }
727 }
728
729 findings
730 }
731}
732
733pub struct OverscopedAllowRule {
739 metadata: RuleMetadata,
740}
741
742impl OverscopedAllowRule {
743 pub fn new() -> Self {
744 Self {
745 metadata: RuleMetadata {
746 id: "RUSTCOLA072".to_string(),
747 name: "overscoped-allow".to_string(),
748 short_description: "Crate-wide allow attribute suppresses security lints".to_string(),
749 full_description: "Detects #![allow(...)] attributes at crate level that suppress warnings across the entire crate. Such broad suppression can hide security issues that should be addressed. Prefer module-level or item-level allows that target specific warnings in specific contexts.".to_string(),
750 help_uri: None,
751 default_severity: Severity::Medium,
752 origin: RuleOrigin::BuiltIn,
753 cwe_ids: Vec::new(),
754 fix_suggestion: None,
755 exploitability: Exploitability::default(),
756 },
757 }
758 }
759
760 fn is_security_relevant_lint(path: &str) -> bool {
762 matches!(
763 path,
764 "warnings"
765 | "unsafe_code"
766 | "unused_must_use"
767 | "dead_code"
768 | "deprecated"
769 | "non_snake_case"
770 | "non_camel_case_types"
771 | "clippy::all"
772 | "clippy::pedantic"
773 | "clippy::restriction"
774 | "clippy::unwrap_used"
775 | "clippy::expect_used"
776 | "clippy::panic"
777 | "clippy::indexing_slicing"
778 | "clippy::mem_forget"
779 | "clippy::cast_ptr_alignment"
780 | "clippy::integer_arithmetic"
781 )
782 }
783}
784
785impl Rule for OverscopedAllowRule {
786 fn metadata(&self) -> &RuleMetadata {
787 &self.metadata
788 }
789
790 fn evaluate(
791 &self,
792 package: &MirPackage,
793 _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
794 ) -> Vec<Finding> {
795 let mut findings = Vec::new();
796
797 let crate_root = Path::new(&package.crate_root);
798 let sources = match SourceFile::collect_crate_sources(crate_root) {
799 Ok(s) => s,
800 Err(_) => return findings,
801 };
802
803 for source in sources {
804 let syntax_tree = match syn::parse_file(&source.content) {
805 Ok(tree) => tree,
806 Err(_) => continue,
807 };
808
809 for attr in &syntax_tree.attrs {
810 match attr.style {
811 syn::AttrStyle::Inner(_) => {}
812 syn::AttrStyle::Outer => continue,
813 }
814
815 if !attr.path().is_ident("allow") {
816 continue;
817 }
818
819 if let syn::Meta::List(meta_list) = &attr.meta {
820 let nested = match meta_list.parse_args_with(
821 syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
822 ) {
823 Ok(n) => n,
824 Err(_) => continue,
825 };
826
827 for meta in nested {
828 if let syn::Meta::Path(path) = meta {
829 let lint_name = path
830 .segments
831 .iter()
832 .map(|s| s.ident.to_string())
833 .collect::<Vec<_>>()
834 .join("::");
835
836 if Self::is_security_relevant_lint(&lint_name) {
837 let relative_path = source
838 .path
839 .strip_prefix(crate_root)
840 .unwrap_or(&source.path)
841 .display()
842 .to_string();
843
844 findings.push(Finding {
845 rule_id: self.metadata.id.clone(),
846 rule_name: self.metadata.name.clone(),
847 severity: self.metadata.default_severity,
848 message: format!(
849 "Crate-level #![allow({})] in {} suppresses warnings across entire crate. \
850 Consider module-level or item-level suppression instead.",
851 lint_name,
852 relative_path
853 ),
854 function: relative_path,
855 function_signature: String::new(),
856 evidence: vec![format!("#![allow({})]", lint_name)],
857 span: None,
858 ..Default::default()
859 });
860 }
861 }
862 }
863 }
864 }
865 }
866
867 findings
868 }
869}
870
871pub struct CommentedOutCodeRule {
877 metadata: RuleMetadata,
878}
879
880impl CommentedOutCodeRule {
881 pub fn new() -> Self {
882 Self {
883 metadata: RuleMetadata {
884 id: "RUSTCOLA092".to_string(),
885 name: "commented-out-code".to_string(),
886 short_description: "Commented-out code detected".to_string(),
887 full_description: "Detects commented-out code that should be removed to maintain clean, analyzable codebases. Commented-out code creates maintenance burden, confuses readers about actual functionality, and should be removed in favor of version control for historical reference.".to_string(),
888 help_uri: None,
889 default_severity: Severity::Low,
890 origin: RuleOrigin::BuiltIn,
891 cwe_ids: Vec::new(),
892 fix_suggestion: None,
893 exploitability: Exploitability::default(),
894 },
895 }
896 }
897
898 fn looks_like_commented_code(line: &str) -> bool {
900 let trimmed = line.trim();
901
902 if !trimmed.starts_with("//") {
903 return false;
904 }
905
906 let content = trimmed.trim_start_matches('/').trim();
907
908 if content.is_empty() {
909 return false;
910 }
911
912 if trimmed.starts_with("///") || trimmed.starts_with("//!") {
913 return false;
914 }
915
916 let lowercase = content.to_lowercase();
917 if lowercase.starts_with("todo:")
918 || lowercase.starts_with("fixme:")
919 || lowercase.starts_with("note:")
920 || lowercase.starts_with("hack:")
921 || lowercase.starts_with("xxx:")
922 || lowercase.starts_with("see:")
923 || lowercase.starts_with("example")
924 || lowercase.starts_with("usage:")
925 || lowercase.contains("http://")
926 || lowercase.contains("https://")
927 || content.starts_with('=')
928 || content.starts_with('|')
929 || content.starts_with('-')
930 || content
931 .chars()
932 .all(|c| c == '=' || c == '-' || c.is_whitespace())
933 {
934 return false;
935 }
936
937 let code_keywords = [
938 "pub fn",
939 "fn ",
940 "let ",
941 "let mut",
942 "struct ",
943 "enum ",
944 "impl ",
945 "use ",
946 "mod ",
947 "trait ",
948 "const ",
949 "static ",
950 "match ",
951 "if ",
952 "for ",
953 "while ",
954 "loop ",
955 "return ",
956 "self.",
957 "println!",
958 "format!",
959 "=> ",
960 ".unwrap()",
961 ".expect(",
962 "Vec<",
963 "HashMap<",
964 "Option<",
965 "Result<",
966 ];
967
968 for keyword in &code_keywords {
969 if content.contains(keyword) {
970 return true;
971 }
972 }
973
974 if content.contains(" = ") && !content.ends_with(':') {
975 if !lowercase.contains("means")
976 && !lowercase.contains("where")
977 && !lowercase.contains("when")
978 && !lowercase.contains("if ")
979 {
980 return true;
981 }
982 }
983
984 let has_semicolon = content.ends_with(';');
985 let has_braces = content.contains('{') || content.contains('}');
986 let has_brackets = content.contains('[') || content.contains(']');
987
988 if has_semicolon || (has_braces && has_brackets) {
989 if !lowercase.starts_with("this ")
990 && !lowercase.starts_with("the ")
991 && !lowercase.starts_with("a ")
992 && !lowercase.starts_with("an ")
993 {
994 return true;
995 }
996 }
997
998 false
999 }
1000}
1001
1002impl Rule for CommentedOutCodeRule {
1003 fn metadata(&self) -> &RuleMetadata {
1004 &self.metadata
1005 }
1006
1007 fn evaluate(
1008 &self,
1009 package: &MirPackage,
1010 _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
1011 ) -> Vec<Finding> {
1012 let mut findings = Vec::new();
1013
1014 let crate_root = Path::new(&package.crate_root);
1015 let sources = match SourceFile::collect_crate_sources(crate_root) {
1016 Ok(s) => s,
1017 Err(_) => return findings,
1018 };
1019
1020 for source in sources {
1021 let mut evidence = Vec::new();
1022 let mut consecutive_code_lines = 0;
1023 let mut first_code_line_num = 0;
1024
1025 for (line_num, line) in source.content.lines().enumerate() {
1026 if Self::looks_like_commented_code(line) {
1027 if consecutive_code_lines == 0 {
1028 first_code_line_num = line_num + 1;
1029 }
1030 consecutive_code_lines += 1;
1031
1032 if evidence.len() < 3 {
1033 evidence.push(format!("Line {}: {}", line_num + 1, line.trim()));
1034 }
1035 } else {
1036 if consecutive_code_lines >= 2 {
1037 let relative_path = source
1038 .path
1039 .strip_prefix(crate_root)
1040 .unwrap_or(&source.path)
1041 .display()
1042 .to_string();
1043
1044 findings.push(Finding {
1045 rule_id: self.metadata.id.clone(),
1046 rule_name: self.metadata.name.clone(),
1047 severity: self.metadata.default_severity,
1048 message: format!(
1049 "Commented-out code detected in {} starting at line {} ({} consecutive lines)",
1050 relative_path,
1051 first_code_line_num,
1052 consecutive_code_lines
1053 ),
1054 function: relative_path.clone(),
1055 function_signature: String::new(),
1056 evidence: evidence.clone(),
1057 span: None,
1058 ..Default::default()
1059 });
1060
1061 evidence.clear();
1062 }
1063 consecutive_code_lines = 0;
1064 }
1065 }
1066
1067 if consecutive_code_lines >= 2 {
1068 let relative_path = source
1069 .path
1070 .strip_prefix(crate_root)
1071 .unwrap_or(&source.path)
1072 .display()
1073 .to_string();
1074
1075 findings.push(Finding {
1076 rule_id: self.metadata.id.clone(),
1077 rule_name: self.metadata.name.clone(),
1078 severity: self.metadata.default_severity,
1079 message: format!(
1080 "Commented-out code detected in {} starting at line {} ({} consecutive lines)",
1081 relative_path,
1082 first_code_line_num,
1083 consecutive_code_lines
1084 ),
1085 function: relative_path,
1086 function_signature: String::new(),
1087 evidence,
1088 span: None,
1089 ..Default::default()
1090 });
1091 }
1092 }
1093
1094 findings
1095 }
1096}
1097
1098use crate::rules::utils::filter_entry;
1103use std::ffi::OsStr;
1104use walkdir::WalkDir;
1105
1106pub struct UnwrapInHotPathRule {
1109 metadata: RuleMetadata,
1110}
1111
1112impl UnwrapInHotPathRule {
1113 pub fn new() -> Self {
1114 Self {
1115 metadata: RuleMetadata {
1116 id: "RUSTCOLA123".to_string(),
1117 name: "unwrap-in-hot-path".to_string(),
1118 short_description: "Panic-prone code in performance-critical path".to_string(),
1119 full_description: "Detects unwrap(), expect(), and indexing operations in \
1120 performance-critical code paths like loops, iterators, async poll functions, \
1121 and request handlers. Panics in these paths can cause cascading failures. \
1122 Use Result propagation, .get(), or pattern matching instead."
1123 .to_string(),
1124 help_uri: None,
1125 default_severity: Severity::Medium,
1126 origin: RuleOrigin::BuiltIn,
1127 cwe_ids: Vec::new(),
1128 fix_suggestion: None,
1129 exploitability: Exploitability::default(),
1130 },
1131 }
1132 }
1133
1134 fn hot_path_indicators() -> &'static [&'static str] {
1136 &[
1137 "for ",
1138 "while ",
1139 "loop {",
1140 ".iter()",
1141 ".map(",
1142 ".filter(",
1143 ".fold(",
1144 ".for_each(",
1145 "fn poll(",
1146 "impl Future",
1147 "impl Stream",
1148 "async fn handle",
1149 "fn handle_request",
1150 "fn process",
1151 "#[inline]",
1152 "#[hot]",
1153 ]
1154 }
1155
1156 fn panic_patterns() -> &'static [(&'static str, &'static str)] {
1158 &[
1159 (
1160 ".unwrap()",
1161 "Use ? operator, .unwrap_or(), .unwrap_or_else(), or pattern match",
1162 ),
1163 (
1164 ".expect(",
1165 "Use ? operator, .unwrap_or(), .unwrap_or_else(), or pattern match",
1166 ),
1167 ("[", "Use .get() for safe indexing"),
1168 ]
1169 }
1170}
1171
1172impl Rule for UnwrapInHotPathRule {
1173 fn metadata(&self) -> &RuleMetadata {
1174 &self.metadata
1175 }
1176
1177 fn evaluate(
1178 &self,
1179 package: &MirPackage,
1180 _inter_analysis: Option<&crate::interprocedural::InterProceduralAnalysis>,
1181 ) -> Vec<Finding> {
1182 if package.crate_name == "mir-extractor" {
1183 return Vec::new();
1184 }
1185
1186 let mut findings = Vec::new();
1187 let crate_root = Path::new(&package.crate_root);
1188
1189 if !crate_root.exists() {
1190 return findings;
1191 }
1192
1193 for entry in WalkDir::new(crate_root)
1194 .into_iter()
1195 .filter_entry(|e| filter_entry(e))
1196 {
1197 let entry = match entry {
1198 Ok(e) => e,
1199 Err(_) => continue,
1200 };
1201
1202 if !entry.file_type().is_file() {
1203 continue;
1204 }
1205
1206 let path = entry.path();
1207 if path.extension() != Some(OsStr::new("rs")) {
1208 continue;
1209 }
1210
1211 let rel_path = path
1212 .strip_prefix(crate_root)
1213 .unwrap_or(path)
1214 .to_string_lossy()
1215 .replace('\\', "/");
1216
1217 let content = match std::fs::read_to_string(path) {
1218 Ok(c) => c,
1219 Err(_) => continue,
1220 };
1221
1222 let lines: Vec<&str> = content.lines().collect();
1223 let mut in_hot_path = false;
1224 let mut hot_path_type = String::new();
1225 let mut brace_depth = 0;
1226 let mut hot_path_start_depth = 0;
1227
1228 for (idx, line) in lines.iter().enumerate() {
1229 let trimmed = line.trim();
1230
1231 if trimmed.starts_with("//") {
1233 continue;
1234 }
1235
1236 for indicator in Self::hot_path_indicators() {
1238 if trimmed.contains(indicator) {
1239 in_hot_path = true;
1240 hot_path_start_depth = brace_depth;
1241 hot_path_type = (*indicator).to_string();
1242 break;
1243 }
1244 }
1245
1246 brace_depth += trimmed.chars().filter(|&c| c == '{').count() as i32;
1248 brace_depth -= trimmed.chars().filter(|&c| c == '}').count() as i32;
1249
1250 if in_hot_path && brace_depth <= hot_path_start_depth && trimmed.contains('}') {
1252 in_hot_path = false;
1253 hot_path_type.clear();
1254 }
1255
1256 if in_hot_path {
1258 for (pattern, advice) in Self::panic_patterns() {
1259 if trimmed.contains(pattern) {
1260 if let Some(comment_pos) = trimmed.find("//") {
1263 if trimmed
1264 .find(pattern)
1265 .map(|p| p > comment_pos)
1266 .unwrap_or(false)
1267 {
1268 continue;
1269 }
1270 }
1271
1272 if *pattern == "[" {
1274 if trimmed.contains(".get(")
1276 || trimmed.contains("[..]")
1277 || trimmed.contains(": [")
1278 || trimmed.contains("-> [")
1279 || trimmed.contains("Vec<")
1280 || !trimmed.contains("]")
1281 {
1282 continue;
1283 }
1284 }
1285
1286 let location = format!("{}:{}", rel_path, idx + 1);
1287 findings.push(Finding {
1288 rule_id: self.metadata.id.clone(),
1289 rule_name: self.metadata.name.clone(),
1290 severity: self.metadata.default_severity,
1291 message: format!(
1292 "Panic-prone code '{}' in hot path ({}). {}",
1293 pattern.trim_end_matches('('),
1294 hot_path_type,
1295 advice
1296 ),
1297 function: location,
1298 function_signature: String::new(),
1299 evidence: vec![trimmed.to_string()],
1300 span: None,
1301 ..Default::default()
1302 });
1303 break; }
1305 }
1306 }
1307 }
1308 }
1309
1310 findings
1311 }
1312}
1313
1314pub fn register_code_quality_rules(engine: &mut crate::RuleEngine) {
1320 engine.register_rule(Box::new(CrateWideAllowRule::new()));
1321 engine.register_rule(Box::new(MisorderedAssertEqRule::new()));
1322 engine.register_rule(Box::new(TryIoResultRule::new()));
1323 engine.register_rule(Box::new(LocalRefCellRule::new()));
1324 engine.register_rule(Box::new(UnnecessaryBorrowMutRule::new()));
1325 engine.register_rule(Box::new(DeadStoreArrayRule::new()));
1326 engine.register_rule(Box::new(OverscopedAllowRule::new()));
1327 engine.register_rule(Box::new(CommentedOutCodeRule::new()));
1328 engine.register_rule(Box::new(UnwrapInHotPathRule::new()));
1329}