Skip to main content

cargo_quality/analyzers/
format_args.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use masterror::AppResult;
5use syn::{ExprMacro, File, Macro, spanned::Spanned};
6
7use crate::analyzer::{AnalysisResult, Analyzer, Fix, Issue};
8
9/// Analyzer for format macro arguments
10pub 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}