cargo_quality/analyzers/
path_import.rs1use masterror::AppResult;
14use syn::{
15 ExprMethodCall, ExprPath, File, Path,
16 spanned::Spanned,
17 visit::Visit,
18 visit_mut::{self, VisitMut}
19};
20
21use crate::analyzer::{AnalysisResult, Analyzer, Fix, Issue};
22
23pub struct PathImportAnalyzer;
41
42impl PathImportAnalyzer {
43 #[inline]
45 pub fn new() -> Self {
46 Self
47 }
48
49 fn should_extract_to_import(path: &Path) -> bool {
61 if path.segments.len() < 2 {
62 return false;
63 }
64
65 let first_segment = match path.segments.first() {
66 Some(seg) => seg,
67 None => return false
68 };
69
70 let first_name = first_segment.ident.to_string();
71
72 let first_char = match first_name.chars().next() {
73 Some(c) => c,
74 None => return false
75 };
76
77 if first_char.is_uppercase() {
78 return false;
79 }
80
81 let last_segment = match path.segments.last() {
82 Some(seg) => seg,
83 None => return false
84 };
85
86 let last_name = last_segment.ident.to_string();
87
88 if Self::is_screaming_snake_case(&last_name) {
89 return false;
90 }
91
92 let last_first_char = match last_name.chars().next() {
93 Some(c) => c,
94 None => return false
95 };
96
97 if last_first_char.is_uppercase() {
98 return false;
99 }
100
101 if path.segments.len() >= 2 {
102 let second_to_last = path.segments.iter().rev().nth(1);
103 if let Some(seg) = second_to_last {
104 let seg_name = seg.ident.to_string();
105 if let Some(c) = seg_name.chars().next()
106 && c.is_uppercase()
107 {
108 return false;
109 }
110 }
111 }
112
113 if Self::is_stdlib_root(&first_name) {
114 return true;
115 }
116
117 if path.segments.len() >= 3 && first_char.is_lowercase() {
118 return true;
119 }
120
121 false
122 }
123
124 fn is_screaming_snake_case(s: &str) -> bool {
134 s.chars()
135 .all(|c| c.is_uppercase() || c == '_' || c.is_numeric())
136 }
137
138 fn is_stdlib_root(name: &str) -> bool {
148 matches!(name, "std" | "core" | "alloc")
149 }
150}
151
152impl Analyzer for PathImportAnalyzer {
153 fn name(&self) -> &'static str {
154 "path_import"
155 }
156
157 fn analyze(&self, ast: &File, _content: &str) -> AppResult<AnalysisResult> {
158 let mut visitor = PathVisitor {
159 issues: Vec::new()
160 };
161 visitor.visit_file(ast);
162
163 let fixable_count = visitor.issues.len();
164
165 Ok(AnalysisResult {
166 issues: visitor.issues,
167 fixable_count
168 })
169 }
170
171 fn fix(&self, ast: &mut File) -> AppResult<usize> {
172 let mut fixer = PathFixer {
173 fixed_count: 0
174 };
175 fixer.visit_file_mut(ast);
176 Ok(fixer.fixed_count)
177 }
178}
179
180struct PathVisitor {
181 issues: Vec<Issue>
182}
183
184impl PathVisitor {
185 fn check_path(&mut self, path: &Path) {
186 if PathImportAnalyzer::should_extract_to_import(path) {
187 let span = path.span();
188 let start = span.start();
189
190 let path_str = path
191 .segments
192 .iter()
193 .map(|s| s.ident.to_string())
194 .collect::<Vec<_>>()
195 .join("::");
196
197 let function_name = path
198 .segments
199 .last()
200 .map(|s| s.ident.to_string())
201 .unwrap_or_default();
202
203 self.issues.push(Issue {
204 line: start.line,
205 column: start.column,
206 message: format!("Use import instead of path: {}", path_str),
207 fix: Fix::WithImport {
208 import: format!("use {};", path_str),
209 pattern: path_str.clone(),
210 replacement: function_name
211 }
212 });
213 }
214 }
215}
216
217impl<'ast> syn::visit::Visit<'ast> for PathVisitor {
218 fn visit_expr_path(&mut self, node: &'ast ExprPath) {
219 self.check_path(&node.path);
220 syn::visit::visit_expr_path(self, node);
221 }
222}
223
224struct PathFixer {
225 fixed_count: usize
226}
227
228impl VisitMut for PathFixer {
229 fn visit_expr_method_call_mut(&mut self, node: &mut ExprMethodCall) {
230 visit_mut::visit_expr_method_call_mut(self, node);
231 }
232}
233
234impl Default for PathImportAnalyzer {
235 fn default() -> Self {
236 Self::new()
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use syn::parse_quote;
243
244 use super::*;
245
246 #[test]
247 fn test_analyzer_name() {
248 let analyzer = PathImportAnalyzer::new();
249 assert_eq!(analyzer.name(), "path_import");
250 }
251
252 #[test]
253 fn test_detect_path_separator() {
254 let analyzer = PathImportAnalyzer::new();
255 let code: File = parse_quote! {
256 fn main() {
257 let content = std::fs::read_to_string("file.txt");
258 }
259 };
260
261 let result = analyzer.analyze(&code, "").unwrap();
262 assert!(!result.issues.is_empty());
263 }
264
265 #[test]
266 fn test_ignore_enum_variants() {
267 let analyzer = PathImportAnalyzer::new();
268 let code: File = parse_quote! {
269 fn main() {
270 let err = AppError::NotFound;
271 }
272 };
273
274 let result = analyzer.analyze(&code, "").unwrap();
275 assert_eq!(result.issues.len(), 0);
276 }
277
278 #[test]
279 fn test_detect_stdlib_free_functions() {
280 let analyzer = PathImportAnalyzer::new();
281 let code: File = parse_quote! {
282 fn main() {
283 let content = std::fs::read_to_string("file.txt");
284 let result = std::io::stdin();
285 let data = core::mem::size_of::<u32>();
286 }
287 };
288
289 let result = analyzer.analyze(&code, "").unwrap();
290 assert_eq!(result.issues.len(), 3);
291 }
292
293 #[test]
294 fn test_ignore_associated_functions() {
295 let analyzer = PathImportAnalyzer::new();
296 let code: File = parse_quote! {
297 fn main() {
298 let v = Vec::new();
299 let s = String::from("hello");
300 let p = PathBuf::from("/path");
301 let m = std::collections::HashMap::new();
302 }
303 };
304
305 let result = analyzer.analyze(&code, "").unwrap();
306 assert_eq!(result.issues.len(), 0);
307 }
308
309 #[test]
310 fn test_ignore_option_result_variants() {
311 let analyzer = PathImportAnalyzer::new();
312 let code: File = parse_quote! {
313 fn main() {
314 let x = Option::Some(42);
315 let y = Option::None;
316 let ok = Result::Ok(1);
317 let err = Result::Err("error");
318 }
319 };
320
321 let result = analyzer.analyze(&code, "").unwrap();
322 assert_eq!(result.issues.len(), 0);
323 }
324
325 #[test]
326 fn test_ignore_associated_constants() {
327 let analyzer = PathImportAnalyzer::new();
328 let code: File = parse_quote! {
329 fn main() {
330 let max = u32::MAX;
331 let min = i64::MIN;
332 let pi = f64::consts::PI;
333 }
334 };
335
336 let result = analyzer.analyze(&code, "").unwrap();
337 assert_eq!(result.issues.len(), 0);
338 }
339
340 #[test]
341 fn test_detect_module_paths_3plus_segments() {
342 let analyzer = PathImportAnalyzer::new();
343 let code: File = parse_quote! {
344 fn main() {
345 let content = std::fs::read("file");
346 let data = std::io::stdin();
347 }
348 };
349
350 let result = analyzer.analyze(&code, "").unwrap();
351 assert_eq!(result.issues.len(), 2);
352 }
353
354 #[test]
355 fn test_mixed_scenarios() {
356 let analyzer = PathImportAnalyzer::new();
357 let code: File = parse_quote! {
358 fn main() {
359 let content = std::fs::read_to_string("file.txt");
360 let v = Vec::new();
361 let opt = Option::Some(42);
362 let max = u32::MAX;
363 }
364 };
365
366 let result = analyzer.analyze(&code, "").unwrap();
367 assert_eq!(result.issues.len(), 1);
368 }
369
370 #[test]
371 fn test_fix_returns_zero() {
372 let analyzer = PathImportAnalyzer::new();
373 let mut code: File = parse_quote! {
374 fn main() {
375 let content = std::fs::read_to_string("file.txt");
376 }
377 };
378
379 let fixed = analyzer.fix(&mut code).unwrap();
380 assert_eq!(fixed, 0);
381 }
382
383 #[test]
384 fn test_default_implementation() {
385 let analyzer = PathImportAnalyzer;
386 assert_eq!(analyzer.name(), "path_import");
387 }
388
389 #[test]
390 fn test_single_segment_path() {
391 let analyzer = PathImportAnalyzer::new();
392 let code: File = parse_quote! {
393 fn main() {
394 println!("test");
395 }
396 };
397
398 let result = analyzer.analyze(&code, "").unwrap();
399 assert_eq!(result.issues.len(), 0);
400 }
401
402 #[test]
403 fn test_core_module_functions() {
404 let analyzer = PathImportAnalyzer::new();
405 let code: File = parse_quote! {
406 fn main() {
407 let size = core::mem::size_of::<u32>();
408 }
409 };
410
411 let result = analyzer.analyze(&code, "").unwrap();
412 assert!(!result.issues.is_empty());
413 }
414
415 #[test]
416 fn test_alloc_module_functions() {
417 let analyzer = PathImportAnalyzer::new();
418 let code: File = parse_quote! {
419 fn main() {
420 let data = alloc::format::format(format_args!("test"));
421 }
422 };
423
424 let result = analyzer.analyze(&code, "").unwrap();
425 assert!(!result.issues.is_empty());
426 }
427
428 #[test]
429 fn test_two_segment_path() {
430 let analyzer = PathImportAnalyzer::new();
431 let code: File = parse_quote! {
432 fn main() {
433 let x = fs::read("file");
434 }
435 };
436
437 let result = analyzer.analyze(&code, "").unwrap();
438 assert_eq!(result.issues.len(), 0);
439 }
440
441 #[test]
442 fn test_screaming_snake_case_constant() {
443 let analyzer = PathImportAnalyzer::new();
444 let code: File = parse_quote! {
445 fn main() {
446 let x = some::module::MAX_VALUE;
447 }
448 };
449
450 let result = analyzer.analyze(&code, "").unwrap();
451 assert_eq!(result.issues.len(), 0);
452 }
453
454 #[test]
455 fn test_path_with_generics() {
456 let analyzer = PathImportAnalyzer::new();
457 let code: File = parse_quote! {
458 fn main() {
459 let content = std::fs::read_to_string("file.txt");
460 let data = std::io::stdin();
461 }
462 };
463
464 let result = analyzer.analyze(&code, "").unwrap();
465 assert!(!result.issues.is_empty());
466 }
467
468 #[test]
469 fn test_result_fixable_count() {
470 let analyzer = PathImportAnalyzer::new();
471 let code: File = parse_quote! {
472 fn main() {
473 let a = std::fs::read_to_string("f");
474 let b = std::io::stdin();
475 }
476 };
477
478 let result = analyzer.analyze(&code, "").unwrap();
479 assert_eq!(result.fixable_count, result.issues.len());
480 }
481
482 #[test]
483 fn test_issue_format() {
484 let analyzer = PathImportAnalyzer::new();
485 let code: File = parse_quote! {
486 fn main() {
487 let x = std::fs::read("file");
488 }
489 };
490
491 let result = analyzer.analyze(&code, "").unwrap();
492 assert!(!result.issues.is_empty());
493 let issue = &result.issues[0];
494 assert!(issue.message.contains("Use import instead of path"));
495 assert!(issue.fix.is_available());
496 if let Some((import, pattern, replacement)) = issue.fix.as_import() {
497 assert!(import.contains("use"));
498 assert_eq!(pattern, "std::fs::read");
499 assert_eq!(replacement, "read");
500 } else {
501 panic!("Expected Fix::WithImport");
502 }
503 }
504}