Skip to main content

mir_extractor/rules/
code_quality.rs

1//! Code quality and miscellaneous rules.
2//!
3//! Rules detecting code quality issues:
4//! - Crate-wide allow lint (RUSTCOLA049)
5//! - Misordered assert_eq arguments (RUSTCOLA050)
6//! - Try operator on io::Result (RUSTCOLA051)
7//! - Local RefCell patterns (RUSTCOLA052)
8//! - Unnecessary borrow_mut (RUSTCOLA057)
9//! - Dead stores in arrays (RUSTCOLA068)
10//! - Overscoped allow attributes (RUSTCOLA072)
11//! - Commented-out code (RUSTCOLA092)
12
13#![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
22// ============================================================================
23// RUSTCOLA049: Crate-Wide Allow Attribute
24// ============================================================================
25
26/// Detects crate-wide #![allow(...)] attributes that disable lints.
27pub 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
122// ============================================================================
123// RUSTCOLA050: Misordered assert_eq Arguments
124// ============================================================================
125
126/// Detects assert_eq! calls where literal appears as first argument instead of second.
127pub 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            // Check for promoted constant in FIRST position (_3)
159            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
213// ============================================================================
214// RUSTCOLA051: Try Operator on io::Result
215// ============================================================================
216
217/// Detects use of ? operator on io::Result without additional context.
218pub 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
307// ============================================================================
308// RUSTCOLA052: Local RefCell Usage
309// ============================================================================
310
311/// Detects RefCell used for purely local mutable state.
312pub 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
405// ============================================================================
406// RUSTCOLA057: Unnecessary borrow_mut()
407// ============================================================================
408
409/// Detects borrow_mut() on RefCell where borrow() would suffice.
410pub 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        // Check for actual mutation patterns
448        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
519// ============================================================================
520// RUSTCOLA068: Dead Store in Array
521// ============================================================================
522
523/// Detects array elements written but never read before being overwritten.
524pub 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        // Check for array passed to function
576        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        // Exclude writes (left side of assignment)
593        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; // Only report first overwrite
724                    }
725                }
726            }
727        }
728
729        findings
730    }
731}
732
733// ============================================================================
734// RUSTCOLA072: Overscoped Allow Attributes
735// ============================================================================
736
737/// Detects crate-level #![allow(...)] that suppresses security-relevant lints.
738pub 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    /// Check if this attribute path is a security-relevant lint
761    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
871// ============================================================================
872// RUSTCOLA092: Commented-Out Code Detection
873// ============================================================================
874
875/// Detects commented-out code that should be removed.
876pub 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    /// Check if a comment line looks like commented-out code
899    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
1098// ============================================================================
1099// RUSTCOLA123: Unwrap/Expect in Hot Paths Rule
1100// ============================================================================
1101
1102use crate::rules::utils::filter_entry;
1103use std::ffi::OsStr;
1104use walkdir::WalkDir;
1105
1106/// Detects panic-prone code in performance-critical paths.
1107/// Panics in hot paths can cause cascading failures under load.
1108pub 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    /// Hot path indicators
1135    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    /// Panic-prone patterns
1157    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                // Skip comments
1232                if trimmed.starts_with("//") {
1233                    continue;
1234                }
1235
1236                // Check for hot path entry
1237                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                // Track brace depth
1247                brace_depth += trimmed.chars().filter(|&c| c == '{').count() as i32;
1248                brace_depth -= trimmed.chars().filter(|&c| c == '}').count() as i32;
1249
1250                // Check if we've exited the hot path
1251                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                // Check for panic patterns in hot path
1257                if in_hot_path {
1258                    for (pattern, advice) in Self::panic_patterns() {
1259                        if trimmed.contains(pattern) {
1260                            // Filter out false positives
1261                            // Skip comments
1262                            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                            // For indexing, be more specific
1273                            if *pattern == "[" {
1274                                // Skip if it's a slice pattern, array type, or already uses .get()
1275                                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; // One finding per line
1304                        }
1305                    }
1306                }
1307            }
1308        }
1309
1310        findings
1311    }
1312}
1313
1314// ============================================================================
1315// Registration
1316// ============================================================================
1317
1318/// Register all code quality rules with the rule engine.
1319pub 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}