Skip to main content

cargo_quality/
analyzer.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4//! Core analyzer trait and types for code quality analysis.
5//!
6//! This module defines the fundamental abstractions for building code
7//! analyzers:
8//! - `Analyzer` trait that all analyzers must implement
9//! - `Issue` struct representing detected problems
10//! - `AnalysisResult` struct containing analysis outcomes
11
12use masterror::AppResult;
13use syn::File;
14
15/// Type of fix that can be applied to resolve an issue.
16///
17/// Represents different kinds of automatic fixes that analyzers can provide.
18///
19/// # Examples
20///
21/// ```
22/// use cargo_quality::analyzer::Fix;
23///
24/// let simple_fix = Fix::Simple("let x = 42;".to_string());
25/// assert!(simple_fix.is_available());
26/// assert_eq!(simple_fix.as_simple(), Some("let x = 42;"));
27///
28/// let import_fix = Fix::WithImport {
29///     import:      "use std::fs::read;".to_string(),
30///     pattern:     "std::fs::read".to_string(),
31///     replacement: "read".to_string()
32/// };
33/// assert!(import_fix.is_available());
34/// assert_eq!(
35///     import_fix.as_import(),
36///     Some(("use std::fs::read;", "std::fs::read", "read"))
37/// );
38/// ```
39#[derive(Debug, Clone, PartialEq)]
40pub enum Fix {
41    /// No automatic fix available
42    None,
43
44    /// Simple line replacement
45    ///
46    /// Replace the entire line with the provided string.
47    ///
48    /// Note: Reserved for future analyzers that need simple line replacements.
49    #[allow(dead_code)]
50    Simple(String),
51
52    /// Fix requiring import addition
53    ///
54    /// Adds an import statement and replaces the line.
55    WithImport {
56        /// Import statement to add (e.g., "use std::fs::read_to_string;")
57        import:      String,
58        /// Pattern to find in original line (e.g., "std::fs::read_to_string")
59        pattern:     String,
60        /// Replacement for the pattern (e.g., "read_to_string")
61        replacement: String
62    }
63}
64
65impl Fix {
66    /// Checks if fix is available.
67    ///
68    /// # Returns
69    ///
70    /// `true` if fix can be applied automatically
71    #[inline]
72    pub fn is_available(&self) -> bool {
73        !matches!(self, Fix::None)
74    }
75
76    /// Returns simple replacement string if available.
77    ///
78    /// # Returns
79    ///
80    /// Option<&str> - Replacement string for simple fixes
81    #[inline]
82    pub fn as_simple(&self) -> Option<&str> {
83        match self {
84            Fix::Simple(s) => Some(s.as_str()),
85            _ => None
86        }
87    }
88
89    /// Returns import, pattern, and replacement for import-based fixes.
90    ///
91    /// # Returns
92    ///
93    /// Option<(&str, &str, &str)> - (import, pattern, replacement) tuple
94    #[inline]
95    pub fn as_import(&self) -> Option<(&str, &str, &str)> {
96        match self {
97            Fix::WithImport {
98                import,
99                pattern,
100                replacement
101            } => Some((import.as_str(), pattern.as_str(), replacement.as_str())),
102            _ => None
103        }
104    }
105}
106
107/// Analysis issue found in code.
108///
109/// Represents a single quality issue detected by an analyzer, including
110/// its location, description, and optional fix.
111///
112/// # Examples
113///
114/// ```
115/// # use cargo_quality::analyzer::{Issue, Fix};
116/// let issue = Issue {
117///     line:    42,
118///     column:  15,
119///     message: "Use import instead of path".to_string(),
120///     fix:     Fix::WithImport {
121///         import:      "use std::fs::read_to_string;".to_string(),
122///         pattern:     "std::fs::read_to_string".to_string(),
123///         replacement: "read_to_string".to_string()
124///     }
125/// };
126/// assert_eq!(issue.line, 42);
127/// assert!(issue.fix.is_available());
128/// ```
129#[derive(Debug, Clone, PartialEq)]
130pub struct Issue {
131    /// Line number where issue was found
132    pub line:    usize,
133    /// Column number
134    pub column:  usize,
135    /// Issue description
136    pub message: String,
137    /// Automatic fix
138    pub fix:     Fix
139}
140
141/// Result of code analysis.
142///
143/// Contains all issues found during analysis and count of fixable issues.
144///
145/// # Examples
146///
147/// ```
148/// use cargo_quality::analyzer::AnalysisResult;
149///
150/// let result = AnalysisResult {
151///     issues:        vec![],
152///     fixable_count: 0
153/// };
154/// assert_eq!(result.issues.len(), 0);
155/// ```
156#[derive(Debug, Default)]
157pub struct AnalysisResult {
158    /// Issues found
159    pub issues:        Vec<Issue>,
160    /// Number of fixable issues
161    pub fixable_count: usize
162}
163
164/// Trait for code analyzers.
165///
166/// Implement this trait to create custom quality analyzers. Each analyzer
167/// must provide a unique name, analysis logic, and optional fix capability.
168///
169/// # Examples
170///
171/// ```
172/// use cargo_quality::analyzer::{AnalysisResult, Analyzer};
173/// use masterror::AppResult;
174/// use syn::File;
175///
176/// struct MyAnalyzer;
177///
178/// impl Analyzer for MyAnalyzer {
179///     fn name(&self) -> &'static str {
180///         "my_analyzer"
181///     }
182///
183///     fn analyze(&self, ast: &File, content: &str) -> AppResult<AnalysisResult> {
184///         Ok(AnalysisResult::default())
185///     }
186///
187///     fn fix(&self, ast: &mut File) -> AppResult<usize> {
188///         Ok(0)
189///     }
190/// }
191/// ```
192pub trait Analyzer {
193    /// Returns unique analyzer identifier.
194    ///
195    /// Used for reporting and configuration. Must be lowercase snake_case.
196    fn name(&self) -> &'static str;
197
198    /// Analyze Rust syntax tree for quality issues.
199    ///
200    /// # Arguments
201    ///
202    /// * `ast` - Parsed Rust syntax tree to analyze
203    /// * `content` - Source code content for analyzers that need raw text
204    ///
205    /// # Returns
206    ///
207    /// `AppResult<AnalysisResult>` - Analysis results or error
208    fn analyze(&self, ast: &File, content: &str) -> AppResult<AnalysisResult>;
209
210    /// Apply automatic fixes to syntax tree.
211    ///
212    /// Modifies the AST in-place to fix detected issues.
213    ///
214    /// # Arguments
215    ///
216    /// * `ast` - Mutable syntax tree to fix
217    ///
218    /// # Returns
219    ///
220    /// `AppResult<usize>` - Number of fixes applied or error
221    fn fix(&self, ast: &mut File) -> AppResult<usize>;
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_fix_none() {
230        let fix = Fix::None;
231        assert!(!fix.is_available());
232        assert!(fix.as_simple().is_none());
233        assert!(fix.as_import().is_none());
234    }
235
236    #[test]
237    fn test_fix_simple() {
238        let fix = Fix::Simple("replacement".to_string());
239        assert!(fix.is_available());
240        assert_eq!(fix.as_simple(), Some("replacement"));
241        assert!(fix.as_import().is_none());
242    }
243
244    #[test]
245    fn test_fix_with_import() {
246        let fix = Fix::WithImport {
247            import:      "use std::fs::read;".to_string(),
248            pattern:     "std::fs::read".to_string(),
249            replacement: "read".to_string()
250        };
251        assert!(fix.is_available());
252        assert!(fix.as_simple().is_none());
253        assert_eq!(
254            fix.as_import(),
255            Some(("use std::fs::read;", "std::fs::read", "read"))
256        );
257    }
258
259    #[test]
260    fn test_issue_creation() {
261        let issue = Issue {
262            line:    42,
263            column:  10,
264            message: "Test issue".to_string(),
265            fix:     Fix::Simple("Fix suggestion".to_string())
266        };
267
268        assert_eq!(issue.line, 42);
269        assert_eq!(issue.column, 10);
270        assert!(issue.fix.is_available());
271    }
272
273    #[test]
274    fn test_analysis_result_default() {
275        let result = AnalysisResult::default();
276        assert_eq!(result.issues.len(), 0);
277        assert_eq!(result.fixable_count, 0);
278    }
279}