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}