Skip to main content

ryo_suggest/lint/
mod.rs

1//! Lint rules implemented as Suggest patterns
2//!
3//! This module provides code quality checks that integrate with the Suggest framework.
4//! Lint rules detect violations and report them as SuggestOpportunity with `SuggestCategory::Lint`.
5//!
6//! # Architecture
7//!
8//! ```text
9//! ┌─────────────────────────────────────────────────────────────┐
10//! │  Lint as Suggest                                            │
11//! │  ─────────────────                                          │
12//! │  LintSuggest trait extends Suggest                          │
13//! │    ├─ code() → Rule code (e.g., "RL001")                    │
14//! │    ├─ default_severity() → LintSeverity                     │
15//! │    └─ detect() → SuggestOpportunity with Lint context       │
16//! └─────────────────────────────────────────────────────────────┘
17//! ```
18//!
19//! # Available Rules
20//!
21//! - [`RequireTestForMutation`] - Ensures mutation implementations have tests
22
23mod require_test_for_mutation;
24
25pub use require_test_for_mutation::RequireTestForMutation;
26
27use crate::{
28    LintSeverity, OpportunityContext, OpportunityId, Suggest, SuggestCategory, SuggestLocation,
29    SuggestOpportunity,
30};
31use ryo_analysis::SymbolId;
32
33/// Lint diagnostic details: suggestion, expected value, and actual value.
34pub struct LintDetails {
35    /// Fix suggestion text shown to the user
36    pub suggestion: Option<String>,
37    /// Expected code pattern
38    pub expected: Option<String>,
39    /// Actual code found
40    pub actual: Option<String>,
41}
42
43/// Extension trait for lint-specific functionality
44pub trait LintSuggest: Suggest {
45    /// Returns the rule code (e.g., "RL001")
46    fn code(&self) -> &'static str;
47
48    /// Returns the default severity for this lint rule
49    fn default_severity(&self) -> LintSeverity;
50
51    /// Helper to create a lint opportunity
52    fn create_lint_opportunity(
53        &self,
54        id: OpportunityId,
55        targets: Vec<SymbolId>,
56        location: SuggestLocation,
57        message: impl Into<String>,
58        details: LintDetails,
59    ) -> SuggestOpportunity {
60        SuggestOpportunity::new(
61            id,
62            targets,
63            location,
64            message,
65            1.0, // Lint violations have 100% confidence
66            OpportunityContext::Lint {
67                code: self.code().to_string(),
68                rule: self.name().to_string(),
69                severity: self.default_severity(),
70                suggestion: details.suggestion,
71                expected: details.expected,
72                actual: details.actual,
73            },
74        )
75    }
76}
77
78/// Helper to check if this Suggest is a lint rule
79pub fn is_lint_suggest(suggest: &dyn Suggest) -> bool {
80    suggest.category() == SuggestCategory::Lint
81}
82
83// ========== Output Formatters ==========
84
85/// Format a lint opportunity as Clippy-compatible output
86///
87/// Format: `file (symbol_path): SEVERITY [CODE] message`
88pub fn format_clippy(opp: &SuggestOpportunity) -> String {
89    match &opp.context {
90        OpportunityContext::Lint {
91            code,
92            severity,
93            suggestion,
94            ..
95        } => {
96            let mut output = format!(
97                "{} ({}): {} [{}] {}\n",
98                opp.location.file, opp.location.symbol_path, severity, code, opp.message
99            );
100
101            if let Some(sugg) = suggestion {
102                output.push_str(&format!("  = help: {}\n", sugg));
103            }
104
105            output
106        }
107        _ => format!("{}: {}\n", opp.location, opp.message),
108    }
109}
110
111/// Format multiple lint opportunities as Clippy-compatible output
112pub fn format_clippy_all(opportunities: &[SuggestOpportunity]) -> String {
113    let mut output = String::new();
114
115    for opp in opportunities {
116        output.push_str(&format_clippy(opp));
117    }
118
119    // Summary
120    let (errors, warnings, infos) = count_by_severity(opportunities);
121    output.push_str(&format!(
122        "\nFound {} error(s), {} warning(s), {} info(s)\n",
123        errors, warnings, infos
124    ));
125
126    output
127}
128
129/// Format a lint opportunity as JSON
130pub fn format_json(opp: &SuggestOpportunity) -> String {
131    serde_json::to_string(opp).unwrap_or_else(|_| "{}".to_string())
132}
133
134/// Format multiple lint opportunities as JSON array
135pub fn format_json_all(opportunities: &[SuggestOpportunity]) -> String {
136    serde_json::to_string_pretty(opportunities).unwrap_or_else(|_| "[]".to_string())
137}
138
139/// Count opportunities by severity
140pub fn count_by_severity(opportunities: &[SuggestOpportunity]) -> (usize, usize, usize) {
141    let mut errors = 0;
142    let mut warnings = 0;
143    let mut infos = 0;
144
145    for opp in opportunities {
146        if let OpportunityContext::Lint { severity, .. } = &opp.context {
147            match severity {
148                LintSeverity::Error => errors += 1,
149                LintSeverity::Warning => warnings += 1,
150                LintSeverity::Info => infos += 1,
151            }
152        }
153    }
154
155    (errors, warnings, infos)
156}
157
158/// Check if any opportunity is an error
159pub fn has_errors(opportunities: &[SuggestOpportunity]) -> bool {
160    opportunities.iter().any(|opp| {
161        matches!(
162            &opp.context,
163            OpportunityContext::Lint {
164                severity: LintSeverity::Error,
165                ..
166            }
167        )
168    })
169}
170
171/// Lint result summary
172#[derive(Debug, Clone, Default)]
173pub struct LintResult {
174    /// All violations found
175    pub violations: Vec<SuggestOpportunity>,
176    /// Number of files checked (if tracked)
177    pub files_checked: usize,
178}
179
180impl LintResult {
181    pub fn new() -> Self {
182        Self::default()
183    }
184
185    /// Check if there are any errors
186    pub fn has_errors(&self) -> bool {
187        has_errors(&self.violations)
188    }
189
190    /// Count by severity
191    pub fn count_by_severity(&self) -> (usize, usize, usize) {
192        count_by_severity(&self.violations)
193    }
194
195    /// Format as Clippy output
196    pub fn format_clippy(&self) -> String {
197        format_clippy_all(&self.violations)
198    }
199
200    /// Format as JSON
201    pub fn format_json(&self) -> String {
202        format_json_all(&self.violations)
203    }
204}