cargo_quality/analyzers/
format_args.rs1use masterror::AppResult;
5use syn::{ExprMacro, File, Macro, spanned::Spanned};
6
7use crate::analyzer::{AnalysisResult, Analyzer, Fix, Issue};
8
9pub struct FormatArgsAnalyzer;
11
12impl FormatArgsAnalyzer {
13 #[inline]
14 pub fn new() -> Self {
15 Self
16 }
17
18 fn analyze_format_macro(mac: &Macro) -> Option<Issue> {
19 let tokens = &mac.tokens;
20 let token_str = tokens.to_string();
21
22 if token_str.contains("{}") {
23 let placeholder_count = token_str.matches("{}").count();
24
25 if placeholder_count >= 3 {
26 let span = mac.span();
27 let start = span.start();
28
29 return Some(Issue {
30 line: start.line,
31 column: start.column,
32 message: format!(
33 "Use named format arguments for better readability ({} placeholders)",
34 placeholder_count
35 ),
36 fix: Fix::None
37 });
38 }
39 }
40
41 None
42 }
43}
44
45impl Analyzer for FormatArgsAnalyzer {
46 fn name(&self) -> &'static str {
47 "format_args"
48 }
49
50 fn analyze(&self, ast: &File, _content: &str) -> AppResult<AnalysisResult> {
51 let mut visitor = FormatVisitor {
52 issues: Vec::new()
53 };
54 syn::visit::visit_file(&mut visitor, ast);
55
56 let fixable_count = visitor.issues.len();
57
58 Ok(AnalysisResult {
59 issues: visitor.issues,
60 fixable_count
61 })
62 }
63
64 fn fix(&self, _ast: &mut File) -> AppResult<usize> {
65 Ok(0)
66 }
67}
68
69struct FormatVisitor {
70 issues: Vec<Issue>
71}
72
73impl<'ast> syn::visit::Visit<'ast> for FormatVisitor {
74 fn visit_expr_macro(&mut self, node: &'ast ExprMacro) {
75 self.check_macro(&node.mac);
76 syn::visit::visit_expr_macro(self, node);
77 }
78
79 fn visit_stmt_macro(&mut self, node: &'ast syn::StmtMacro) {
80 self.check_macro(&node.mac);
81 syn::visit::visit_stmt_macro(self, node);
82 }
83}
84
85impl FormatVisitor {
86 fn check_macro(&mut self, mac: &Macro) {
87 let path = &mac.path;
88
89 if (path.is_ident("format")
90 || path.is_ident("println")
91 || path.is_ident("print")
92 || path.is_ident("write")
93 || path.is_ident("writeln"))
94 && let Some(issue) = FormatArgsAnalyzer::analyze_format_macro(mac)
95 {
96 self.issues.push(issue);
97 }
98 }
99}
100
101impl Default for FormatArgsAnalyzer {
102 fn default() -> Self {
103 Self::new()
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use syn::parse_quote;
110
111 use super::*;
112
113 #[test]
114 fn test_analyzer_name() {
115 let analyzer = FormatArgsAnalyzer::new();
116 assert_eq!(analyzer.name(), "format_args");
117 }
118
119 #[test]
120 fn test_detect_positional_args() {
121 let analyzer = FormatArgsAnalyzer::new();
122 let code: File = parse_quote! {
123 fn main() {
124 println!("Values: {} {} {}", 1, 2, 3);
125 }
126 };
127
128 let result = analyzer.analyze(&code, "").unwrap();
129 assert!(!result.issues.is_empty());
130 }
131
132 #[test]
133 fn test_ignore_simple_positional() {
134 let analyzer = FormatArgsAnalyzer::new();
135 let code: File = parse_quote! {
136 fn main() {
137 println!("Value: {}", 42);
138 println!("Two: {} {}", 1, 2);
139 }
140 };
141
142 let result = analyzer.analyze(&code, "").unwrap();
143 assert_eq!(result.issues.len(), 0);
144 }
145
146 #[test]
147 fn test_ignore_named_args() {
148 let analyzer = FormatArgsAnalyzer::new();
149 let code: File = parse_quote! {
150 fn main() {
151 let name = "World";
152 println!("Hello {name}");
153 }
154 };
155
156 let result = analyzer.analyze(&code, "").unwrap();
157 assert_eq!(result.issues.len(), 0);
158 }
159
160 #[test]
161 fn test_detect_format_macro() {
162 let analyzer = FormatArgsAnalyzer::new();
163 let code: File = parse_quote! {
164 fn main() {
165 let msg = format!("Values: {} {} {}", 1, 2, 3);
166 }
167 };
168
169 let result = analyzer.analyze(&code, "").unwrap();
170 assert!(!result.issues.is_empty());
171 }
172
173 #[test]
174 fn test_detect_print_macro() {
175 let analyzer = FormatArgsAnalyzer::new();
176 let code: File = parse_quote! {
177 fn main() {
178 print!("Values: {} {} {}", 1, 2, 3);
179 }
180 };
181
182 let result = analyzer.analyze(&code, "").unwrap();
183 assert!(!result.issues.is_empty());
184 }
185
186 #[test]
187 fn test_detect_write_macro() {
188 let analyzer = FormatArgsAnalyzer::new();
189 let code: File = parse_quote! {
190 fn main() {
191 use std::io::Write;
192 let mut buf = Vec::new();
193 write!(&mut buf, "Values: {} {} {}", 1, 2, 3).unwrap();
194 }
195 };
196
197 let result = analyzer.analyze(&code, "").unwrap();
198 assert!(!result.issues.is_empty());
199 }
200
201 #[test]
202 fn test_detect_writeln_macro() {
203 let analyzer = FormatArgsAnalyzer::new();
204 let code: File = parse_quote! {
205 fn main() {
206 use std::io::Write;
207 let mut buf = Vec::new();
208 writeln!(&mut buf, "Values: {} {} {}", 1, 2, 3).unwrap();
209 }
210 };
211
212 let result = analyzer.analyze(&code, "").unwrap();
213 assert!(!result.issues.is_empty());
214 }
215
216 #[test]
217 fn test_fix_returns_zero() {
218 let analyzer = FormatArgsAnalyzer::new();
219 let mut code: File = parse_quote! {
220 fn main() {
221 println!("Hello {} {} {}", 1, 2, 3);
222 }
223 };
224
225 let fixed = analyzer.fix(&mut code).unwrap();
226 assert_eq!(fixed, 0);
227 }
228
229 #[test]
230 fn test_default_implementation() {
231 let analyzer = FormatArgsAnalyzer;
232 assert_eq!(analyzer.name(), "format_args");
233 }
234
235 #[test]
236 fn test_format_without_args() {
237 let analyzer = FormatArgsAnalyzer::new();
238 let code: File = parse_quote! {
239 fn main() {
240 println!("Hello world");
241 }
242 };
243
244 let result = analyzer.analyze(&code, "").unwrap();
245 assert_eq!(result.issues.len(), 0);
246 }
247}