cargo_quality/analyzers/
format_args.rs1use masterror::AppResult;
5use proc_macro2::TokenTree;
6use syn::{ExprMacro, File, LitStr, Macro, spanned::Spanned};
7
8use crate::analyzer::{AnalysisResult, Analyzer, Fix, Issue};
9
10pub struct FormatArgsAnalyzer;
12
13impl FormatArgsAnalyzer {
14 #[inline]
15 pub fn new() -> Self {
16 Self
17 }
18
19 fn analyze_format_macro(mac: &Macro) -> Option<Issue> {
20 let format = Self::extract_format_string(mac)?;
21 let placeholder_count = Self::count_positional_placeholders(&format);
22
23 if placeholder_count >= 3 {
24 let span = mac.span();
25 let start = span.start();
26
27 return Some(Issue {
28 line: start.line,
29 column: start.column,
30 message: format!(
31 "Use named format arguments for better readability ({} placeholders)",
32 placeholder_count
33 ),
34 fix: Fix::None
35 });
36 }
37
38 None
39 }
40
41 fn extract_format_string(mac: &Macro) -> Option<String> {
55 for token in mac.tokens.clone() {
56 if let TokenTree::Literal(literal) = token
57 && let Ok(lit_str) = syn::parse_str::<LitStr>(&literal.to_string())
58 {
59 return Some(lit_str.value());
60 }
61 }
62
63 None
64 }
65
66 fn count_positional_placeholders(format: &str) -> usize {
80 let bytes = format.as_bytes();
81 let mut count = 0;
82 let mut index = 0;
83
84 while index < bytes.len() {
85 match bytes[index] {
86 b'{' => {
87 if bytes.get(index + 1) == Some(&b'{') {
88 index += 2;
89 continue;
90 }
91
92 let name_start = index + 1;
93 let mut name_end = name_start;
94 while name_end < bytes.len()
95 && bytes[name_end] != b'}'
96 && bytes[name_end] != b':'
97 {
98 name_end += 1;
99 }
100
101 let name = &format[name_start..name_end];
102 if name.is_empty() || name.bytes().all(|b| b.is_ascii_digit()) {
103 count += 1;
104 }
105
106 while index < bytes.len() && bytes[index] != b'}' {
107 index += 1;
108 }
109 index += 1;
110 }
111 b'}' => {
112 index += if bytes.get(index + 1) == Some(&b'}') {
113 2
114 } else {
115 1
116 };
117 }
118 _ => index += 1
119 }
120 }
121
122 count
123 }
124}
125
126impl Analyzer for FormatArgsAnalyzer {
127 fn name(&self) -> &'static str {
128 "format_args"
129 }
130
131 fn analyze(&self, ast: &File, _content: &str) -> AppResult<AnalysisResult> {
132 let mut visitor = FormatVisitor {
133 issues: Vec::new()
134 };
135 syn::visit::visit_file(&mut visitor, ast);
136
137 Ok(AnalysisResult {
138 issues: visitor.issues,
139 fixable_count: 0
140 })
141 }
142
143 fn fix(&self, _ast: &mut File) -> AppResult<usize> {
144 Ok(0)
145 }
146}
147
148struct FormatVisitor {
149 issues: Vec<Issue>
150}
151
152impl<'ast> syn::visit::Visit<'ast> for FormatVisitor {
153 fn visit_expr_macro(&mut self, node: &'ast ExprMacro) {
154 self.check_macro(&node.mac);
155 syn::visit::visit_expr_macro(self, node);
156 }
157
158 fn visit_stmt_macro(&mut self, node: &'ast syn::StmtMacro) {
159 self.check_macro(&node.mac);
160 syn::visit::visit_stmt_macro(self, node);
161 }
162}
163
164impl FormatVisitor {
165 fn check_macro(&mut self, mac: &Macro) {
166 let path = &mac.path;
167
168 if (path.is_ident("format")
169 || path.is_ident("println")
170 || path.is_ident("print")
171 || path.is_ident("write")
172 || path.is_ident("writeln"))
173 && let Some(issue) = FormatArgsAnalyzer::analyze_format_macro(mac)
174 {
175 self.issues.push(issue);
176 }
177 }
178}
179
180impl Default for FormatArgsAnalyzer {
181 fn default() -> Self {
182 Self::new()
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use syn::parse_quote;
189
190 use super::*;
191
192 #[test]
193 fn test_analyzer_name() {
194 let analyzer = FormatArgsAnalyzer::new();
195 assert_eq!(analyzer.name(), "format_args");
196 }
197
198 #[test]
199 fn test_detect_positional_args() {
200 let analyzer = FormatArgsAnalyzer::new();
201 let code: File = parse_quote! {
202 fn main() {
203 println!("Values: {} {} {}", 1, 2, 3);
204 }
205 };
206
207 let result = analyzer.analyze(&code, "").unwrap();
208 assert!(!result.issues.is_empty());
209 }
210
211 #[test]
212 fn test_advisory_only_not_fixable() {
213 let analyzer = FormatArgsAnalyzer::new();
214 let code: File = parse_quote! {
215 fn main() {
216 println!("Values: {} {} {}", 1, 2, 3);
217 }
218 };
219
220 let result = analyzer.analyze(&code, "").unwrap();
221 assert!(!result.issues.is_empty());
222 assert_eq!(result.fixable_count, 0);
223 assert!(!result.issues[0].fix.is_available());
224 }
225
226 #[test]
227 fn test_ignore_simple_positional() {
228 let analyzer = FormatArgsAnalyzer::new();
229 let code: File = parse_quote! {
230 fn main() {
231 println!("Value: {}", 42);
232 println!("Two: {} {}", 1, 2);
233 }
234 };
235
236 let result = analyzer.analyze(&code, "").unwrap();
237 assert_eq!(result.issues.len(), 0);
238 }
239
240 #[test]
241 fn test_ignore_named_args() {
242 let analyzer = FormatArgsAnalyzer::new();
243 let code: File = parse_quote! {
244 fn main() {
245 let name = "World";
246 println!("Hello {name}");
247 }
248 };
249
250 let result = analyzer.analyze(&code, "").unwrap();
251 assert_eq!(result.issues.len(), 0);
252 }
253
254 #[test]
255 fn test_detect_format_macro() {
256 let analyzer = FormatArgsAnalyzer::new();
257 let code: File = parse_quote! {
258 fn main() {
259 let msg = format!("Values: {} {} {}", 1, 2, 3);
260 }
261 };
262
263 let result = analyzer.analyze(&code, "").unwrap();
264 assert!(!result.issues.is_empty());
265 }
266
267 #[test]
268 fn test_detect_print_macro() {
269 let analyzer = FormatArgsAnalyzer::new();
270 let code: File = parse_quote! {
271 fn main() {
272 print!("Values: {} {} {}", 1, 2, 3);
273 }
274 };
275
276 let result = analyzer.analyze(&code, "").unwrap();
277 assert!(!result.issues.is_empty());
278 }
279
280 #[test]
281 fn test_detect_write_macro() {
282 let analyzer = FormatArgsAnalyzer::new();
283 let code: File = parse_quote! {
284 fn main() {
285 use std::io::Write;
286 let mut buf = Vec::new();
287 write!(&mut buf, "Values: {} {} {}", 1, 2, 3).unwrap();
288 }
289 };
290
291 let result = analyzer.analyze(&code, "").unwrap();
292 assert!(!result.issues.is_empty());
293 }
294
295 #[test]
296 fn test_detect_writeln_macro() {
297 let analyzer = FormatArgsAnalyzer::new();
298 let code: File = parse_quote! {
299 fn main() {
300 use std::io::Write;
301 let mut buf = Vec::new();
302 writeln!(&mut buf, "Values: {} {} {}", 1, 2, 3).unwrap();
303 }
304 };
305
306 let result = analyzer.analyze(&code, "").unwrap();
307 assert!(!result.issues.is_empty());
308 }
309
310 #[test]
311 fn test_fix_returns_zero() {
312 let analyzer = FormatArgsAnalyzer::new();
313 let mut code: File = parse_quote! {
314 fn main() {
315 println!("Hello {} {} {}", 1, 2, 3);
316 }
317 };
318
319 let fixed = analyzer.fix(&mut code).unwrap();
320 assert_eq!(fixed, 0);
321 }
322
323 #[test]
324 fn test_default_implementation() {
325 let analyzer = FormatArgsAnalyzer;
326 assert_eq!(analyzer.name(), "format_args");
327 }
328
329 #[test]
330 fn test_format_without_args() {
331 let analyzer = FormatArgsAnalyzer::new();
332 let code: File = parse_quote! {
333 fn main() {
334 println!("Hello world");
335 }
336 };
337
338 let result = analyzer.analyze(&code, "").unwrap();
339 assert_eq!(result.issues.len(), 0);
340 }
341
342 #[test]
343 fn test_count_debug_and_spec_placeholders() {
344 let analyzer = FormatArgsAnalyzer::new();
345 let code: File = parse_quote! {
346 fn main() {
347 println!("{} {} {:?}", a, b, c);
348 }
349 };
350
351 let result = analyzer.analyze(&code, "").unwrap();
352 assert_eq!(result.issues.len(), 1);
353 assert!(result.issues[0].message.contains("3 placeholders"));
354 }
355
356 #[test]
357 fn test_ignore_named_placeholders_over_threshold() {
358 let analyzer = FormatArgsAnalyzer::new();
359 let code: File = parse_quote! {
360 fn main() {
361 println!("{a} {b} {c}", a = 1, b = 2, c = 3);
362 }
363 };
364
365 let result = analyzer.analyze(&code, "").unwrap();
366 assert_eq!(result.issues.len(), 0);
367 }
368
369 #[test]
370 fn test_escaped_braces_not_counted() {
371 let analyzer = FormatArgsAnalyzer::new();
372 let code: File = parse_quote! {
373 fn main() {
374 println!("{{}} {{}} {{}} {}", x);
375 }
376 };
377
378 let result = analyzer.analyze(&code, "").unwrap();
379 assert_eq!(result.issues.len(), 0);
380 }
381
382 #[test]
383 fn test_indexed_placeholders_counted() {
384 let analyzer = FormatArgsAnalyzer::new();
385 let code: File = parse_quote! {
386 fn main() {
387 println!("{0} {1} {2}", a, b, c);
388 }
389 };
390
391 let result = analyzer.analyze(&code, "").unwrap();
392 assert_eq!(result.issues.len(), 1);
393 }
394
395 #[test]
396 fn test_count_positional_placeholders_helper() {
397 assert_eq!(
398 FormatArgsAnalyzer::count_positional_placeholders("{} {} {:?}"),
399 3
400 );
401 assert_eq!(
402 FormatArgsAnalyzer::count_positional_placeholders("{a} {b} {c}"),
403 0
404 );
405 assert_eq!(
406 FormatArgsAnalyzer::count_positional_placeholders("{{}} literal {}"),
407 1
408 );
409 assert_eq!(
410 FormatArgsAnalyzer::count_positional_placeholders("{0} {1:>8} {2:.2}"),
411 3
412 );
413 }
414}