1use std::{
25 collections::{HashMap, HashSet},
26 ops::Range
27};
28
29use masterror::AppResult;
30use syn::{ExprPath, File, Item, ItemUse, Path, UseTree, spanned::Spanned, visit::Visit};
31
32use crate::{
33 analyzer::{AnalysisResult, Analyzer, Fix, ImportEdit, Issue, Suggestion, TextEdit},
34 fixer::import_insertion_offset
35};
36
37pub struct PathImportAnalyzer;
55
56impl PathImportAnalyzer {
57 #[inline]
59 pub fn new() -> Self {
60 Self
61 }
62
63 fn should_extract_to_import(path: &Path) -> bool {
77 if path.segments.len() < 2 {
78 return false;
79 }
80
81 let first_segment = match path.segments.first() {
82 Some(seg) => seg,
83 None => return false
84 };
85
86 let first_name = first_segment.ident.to_string();
87
88 let first_char = match first_name.chars().next() {
89 Some(c) => c,
90 None => return false
91 };
92
93 if first_char.is_uppercase() {
94 return false;
95 }
96
97 let last_segment = match path.segments.last() {
98 Some(seg) => seg,
99 None => return false
100 };
101
102 let last_name = last_segment.ident.to_string();
103
104 if Self::is_screaming_snake_case(&last_name) {
105 return false;
106 }
107
108 let last_first_char = match last_name.chars().next() {
109 Some(c) => c,
110 None => return false
111 };
112
113 if last_first_char.is_uppercase() {
114 return false;
115 }
116
117 if path.segments.len() >= 2 {
118 let second_to_last = path.segments.iter().rev().nth(1);
119 if let Some(seg) = second_to_last {
120 let seg_name = seg.ident.to_string();
121 if let Some(c) = seg_name.chars().next()
122 && c.is_uppercase()
123 {
124 return false;
125 }
126 }
127 }
128
129 Self::is_extractable_root(&first_name)
130 }
131
132 fn is_screaming_snake_case(s: &str) -> bool {
142 s.chars()
143 .all(|c| c.is_uppercase() || c == '_' || c.is_numeric())
144 }
145
146 fn is_extractable_root(name: &str) -> bool {
160 matches!(name, "std" | "core" | "alloc" | "crate")
161 }
162}
163
164impl Analyzer for PathImportAnalyzer {
165 fn name(&self) -> &'static str {
166 "path_import"
167 }
168
169 fn analyze(&self, ast: &File, _content: &str) -> AppResult<AnalysisResult> {
170 let mut visitor = PathVisitor {
171 issues: Vec::new()
172 };
173 visitor.visit_file(ast);
174
175 let fixable_count = visitor.issues.len();
176
177 Ok(AnalysisResult {
178 issues: visitor.issues,
179 fixable_count
180 })
181 }
182
183 fn suggestions(&self, ast: &File, content: &str) -> AppResult<Vec<Suggestion>> {
184 let root = ModuleScope::build(&ast.items, Some(import_insertion_offset(content)));
185
186 let mut suggestions = Vec::new();
187 root.collect_suggestions(&HashSet::new(), false, &mut suggestions);
188
189 Ok(suggestions)
190 }
191}
192
193fn path_to_string(path: &Path) -> String {
203 path.segments
204 .iter()
205 .map(|segment| segment.ident.to_string())
206 .collect::<Vec<_>>()
207 .join("::")
208}
209
210fn item_bound_name(item: &Item) -> Option<String> {
220 match item {
221 Item::Const(item) => Some(item.ident.to_string()),
222 Item::Enum(item) => Some(item.ident.to_string()),
223 Item::ExternCrate(item) => Some(
224 item.rename
225 .as_ref()
226 .map_or_else(|| item.ident.to_string(), |(_, rename)| rename.to_string())
227 ),
228 Item::Fn(item) => Some(item.sig.ident.to_string()),
229 Item::Macro(item) => item.ident.as_ref().map(|ident| ident.to_string()),
230 Item::Mod(item) => Some(item.ident.to_string()),
231 Item::Static(item) => Some(item.ident.to_string()),
232 Item::Struct(item) => Some(item.ident.to_string()),
233 Item::Trait(item) => Some(item.ident.to_string()),
234 Item::TraitAlias(item) => Some(item.ident.to_string()),
235 Item::Type(item) => Some(item.ident.to_string()),
236 Item::Union(item) => Some(item.ident.to_string()),
237 _ => None
238 }
239}
240
241struct Candidate {
243 path: String,
245 ident: String,
247 range: Range<usize>
249}
250
251#[derive(Default)]
257struct SymbolTable {
258 bound: HashSet<String>,
260 bare_idents: HashSet<String>,
262 has_super_glob: bool,
264 has_foreign_glob: bool
266}
267
268impl SymbolTable {
269 fn record_use(&mut self, item: &ItemUse) {
275 let mut prefix = Vec::new();
276 self.record_use_tree(&item.tree, &mut prefix);
277 }
278
279 fn record_use_tree(&mut self, tree: &UseTree, prefix: &mut Vec<String>) {
290 match tree {
291 UseTree::Path(path) => {
292 prefix.push(path.ident.to_string());
293 self.record_use_tree(&path.tree, prefix);
294 prefix.pop();
295 }
296 UseTree::Name(name) => {
297 let ident = name.ident.to_string();
298 if ident == "self" {
299 if let Some(parent) = prefix.last() {
300 self.bound.insert(parent.clone());
301 }
302 } else {
303 self.bound.insert(ident);
304 }
305 }
306 UseTree::Rename(rename) => {
307 self.bound.insert(rename.rename.to_string());
308 }
309 UseTree::Glob(_) => {
310 if prefix.len() == 1 && prefix[0] == "super" {
311 self.has_super_glob = true;
312 } else {
313 self.has_foreign_glob = true;
314 }
315 }
316 UseTree::Group(group) => {
317 for tree in &group.items {
318 self.record_use_tree(tree, prefix);
319 }
320 }
321 }
322 }
323}
324
325struct ModuleScope {
327 symbols: SymbolTable,
329 insert_offset: Option<usize>,
331 candidates: Vec<Candidate>,
333 children: Vec<ModuleScope>
335}
336
337impl ModuleScope {
338 fn build(items: &[Item], insert_offset: Option<usize>) -> Self {
353 let mut scope = Self {
354 symbols: SymbolTable::default(),
355 insert_offset,
356 candidates: Vec::new(),
357 children: Vec::new()
358 };
359
360 for item in items {
361 if let Item::Mod(module) = item {
362 scope.symbols.bound.insert(module.ident.to_string());
363 if let Some((_, child_items)) = &module.content {
364 let child_offset = child_items
365 .first()
366 .map(|first| first.span().byte_range().start);
367 scope.children.push(Self::build(child_items, child_offset));
368 }
369 continue;
370 }
371
372 let mut collector = BodyCollector {
373 scope: &mut scope
374 };
375 collector.visit_item(item);
376 }
377
378 scope
379 }
380
381 fn ambiguous_idents(&self) -> HashSet<String> {
390 let mut sources: HashMap<&str, &str> = HashMap::new();
391 let mut ambiguous = HashSet::new();
392
393 for candidate in &self.candidates {
394 match sources.get(candidate.ident.as_str()) {
395 Some(path) if *path != candidate.path.as_str() => {
396 ambiguous.insert(candidate.ident.clone());
397 }
398 Some(_) => {}
399 None => {
400 sources.insert(&candidate.ident, &candidate.path);
401 }
402 }
403 }
404
405 ambiguous
406 }
407
408 fn descendant_bare_conflict(&self, name: &str) -> bool {
423 self.children.iter().any(|child| {
424 child.symbols.has_super_glob
425 && ((child.symbols.bare_idents.contains(name)
426 && !child.symbols.bound.contains(name))
427 || child.descendant_bare_conflict(name))
428 })
429 }
430
431 fn collect_suggestions(
445 &self,
446 inherited_bound: &HashSet<String>,
447 inherited_foreign_glob: bool,
448 suggestions: &mut Vec<Suggestion>
449 ) {
450 let foreign_glob = self.symbols.has_foreign_glob
451 || (self.symbols.has_super_glob && inherited_foreign_glob);
452
453 let mut visible = self.symbols.bound.clone();
454 if self.symbols.has_super_glob {
455 visible.extend(inherited_bound.iter().cloned());
456 }
457
458 if !foreign_glob && let Some(offset) = self.insert_offset {
459 let ambiguous = self.ambiguous_idents();
460
461 for candidate in &self.candidates {
462 if ambiguous.contains(&candidate.ident)
463 || visible.contains(&candidate.ident)
464 || self.descendant_bare_conflict(&candidate.ident)
465 {
466 continue;
467 }
468
469 suggestions.push(Suggestion {
470 edit: TextEdit {
471 range: candidate.range.clone(),
472 replacement: String::new()
473 },
474 import: Some(ImportEdit {
475 offset,
476 statement: format!("use {};", candidate.path)
477 })
478 });
479 }
480 }
481
482 for child in &self.children {
483 child.collect_suggestions(&visible, foreign_glob, suggestions);
484 }
485 }
486}
487
488struct BodyCollector<'scope> {
495 scope: &'scope mut ModuleScope
497}
498
499impl<'scope, 'ast> Visit<'ast> for BodyCollector<'scope> {
500 fn visit_item(&mut self, node: &'ast Item) {
501 match node {
502 Item::Mod(_) => {}
503 Item::Use(item) => {
504 self.scope.symbols.record_use(item);
505 }
506 other => {
507 if let Some(name) = item_bound_name(other) {
508 self.scope.symbols.bound.insert(name);
509 }
510 syn::visit::visit_item(self, node);
511 }
512 }
513 }
514
515 fn visit_expr_path(&mut self, node: &'ast ExprPath) {
516 if node.qself.is_none() {
517 if node.path.segments.len() == 1 {
518 if let Some(only) = node.path.segments.first() {
519 self.scope
520 .symbols
521 .bare_idents
522 .insert(only.ident.to_string());
523 }
524 } else if PathImportAnalyzer::should_extract_to_import(&node.path)
525 && let Some(last) = node.path.segments.last()
526 {
527 let path_start = node.path.span().byte_range().start;
528 let last_start = last.ident.span().byte_range().start;
529
530 if last_start > path_start {
531 self.scope.candidates.push(Candidate {
532 path: path_to_string(&node.path),
533 ident: last.ident.to_string(),
534 range: path_start..last_start
535 });
536 }
537 }
538 }
539
540 syn::visit::visit_expr_path(self, node);
541 }
542
543 fn visit_pat_ident(&mut self, node: &'ast syn::PatIdent) {
544 self.scope.symbols.bound.insert(node.ident.to_string());
545 syn::visit::visit_pat_ident(self, node);
546 }
547}
548
549struct PathVisitor {
550 issues: Vec<Issue>
551}
552
553impl PathVisitor {
554 fn check_path(&mut self, path: &Path) {
555 if PathImportAnalyzer::should_extract_to_import(path) {
556 let span = path.span();
557 let start = span.start();
558
559 let path_str = path_to_string(path);
560
561 let function_name = path
562 .segments
563 .last()
564 .map(|s| s.ident.to_string())
565 .unwrap_or_default();
566
567 self.issues.push(Issue::new(
568 start.line,
569 start.column,
570 format!("Use import instead of path: {}", path_str),
571 Fix::WithImport {
572 import: format!("use {};", path_str),
573 pattern: path_str.clone(),
574 replacement: function_name
575 }
576 ));
577 }
578 }
579}
580
581impl<'ast> Visit<'ast> for PathVisitor {
582 fn visit_expr_path(&mut self, node: &'ast ExprPath) {
583 self.check_path(&node.path);
584 syn::visit::visit_expr_path(self, node);
585 }
586}
587
588impl Default for PathImportAnalyzer {
589 fn default() -> Self {
590 Self::new()
591 }
592}
593
594#[cfg(test)]
595mod tests {
596 use syn::parse_quote;
597
598 use super::*;
599
600 #[test]
601 fn test_analyzer_name() {
602 let analyzer = PathImportAnalyzer::new();
603 assert_eq!(analyzer.name(), "path_import");
604 }
605
606 #[test]
607 fn test_detect_path_separator() {
608 let analyzer = PathImportAnalyzer::new();
609 let code: File = parse_quote! {
610 fn main() {
611 let content = std::fs::read_to_string("file.txt");
612 }
613 };
614
615 let result = analyzer.analyze(&code, "").unwrap();
616 assert!(!result.issues.is_empty());
617 }
618
619 #[test]
620 fn test_ignore_enum_variants() {
621 let analyzer = PathImportAnalyzer::new();
622 let code: File = parse_quote! {
623 fn main() {
624 let err = AppError::NotFound;
625 }
626 };
627
628 let result = analyzer.analyze(&code, "").unwrap();
629 assert_eq!(result.issues.len(), 0);
630 }
631
632 #[test]
633 fn test_detect_stdlib_free_functions() {
634 let analyzer = PathImportAnalyzer::new();
635 let code: File = parse_quote! {
636 fn main() {
637 let content = std::fs::read_to_string("file.txt");
638 let result = std::io::stdin();
639 let data = core::mem::size_of::<u32>();
640 }
641 };
642
643 let result = analyzer.analyze(&code, "").unwrap();
644 assert_eq!(result.issues.len(), 3);
645 }
646
647 #[test]
648 fn test_ignore_associated_functions() {
649 let analyzer = PathImportAnalyzer::new();
650 let code: File = parse_quote! {
651 fn main() {
652 let v = Vec::new();
653 let s = String::from("hello");
654 let p = PathBuf::from("/path");
655 let m = std::collections::HashMap::new();
656 }
657 };
658
659 let result = analyzer.analyze(&code, "").unwrap();
660 assert_eq!(result.issues.len(), 0);
661 }
662
663 #[test]
664 fn test_ignore_option_result_variants() {
665 let analyzer = PathImportAnalyzer::new();
666 let code: File = parse_quote! {
667 fn main() {
668 let x = Option::Some(42);
669 let y = Option::None;
670 let ok = Result::Ok(1);
671 let err = Result::Err("error");
672 }
673 };
674
675 let result = analyzer.analyze(&code, "").unwrap();
676 assert_eq!(result.issues.len(), 0);
677 }
678
679 #[test]
680 fn test_ignore_associated_constants() {
681 let analyzer = PathImportAnalyzer::new();
682 let code: File = parse_quote! {
683 fn main() {
684 let max = u32::MAX;
685 let min = i64::MIN;
686 let pi = f64::consts::PI;
687 }
688 };
689
690 let result = analyzer.analyze(&code, "").unwrap();
691 assert_eq!(result.issues.len(), 0);
692 }
693
694 #[test]
695 fn test_ignore_relative_module_paths() {
696 let analyzer = PathImportAnalyzer::new();
697 let code: File = parse_quote! {
698 fn main() {
699 let data = helpers::io::load("file");
700 let more = my_mod::sub::func();
701 }
702 };
703
704 let result = analyzer.analyze(&code, "").unwrap();
705 assert_eq!(result.issues.len(), 0);
706 }
707
708 #[test]
709 fn test_ignore_super_and_self_paths() {
710 let analyzer = PathImportAnalyzer::new();
711 let code: File = parse_quote! {
712 mod inner {
713 fn f() {
714 super::helpers::run();
715 self::local::call();
716 }
717 }
718 };
719
720 let result = analyzer.analyze(&code, "").unwrap();
721 assert_eq!(result.issues.len(), 0);
722 }
723
724 #[test]
725 fn test_detect_crate_rooted_paths() {
726 let analyzer = PathImportAnalyzer::new();
727 let code: File = parse_quote! {
728 fn main() {
729 crate::util::helper();
730 }
731 };
732
733 let result = analyzer.analyze(&code, "").unwrap();
734 assert_eq!(result.issues.len(), 1);
735 }
736
737 #[test]
738 fn test_mixed_scenarios() {
739 let analyzer = PathImportAnalyzer::new();
740 let code: File = parse_quote! {
741 fn main() {
742 let content = std::fs::read_to_string("file.txt");
743 let v = Vec::new();
744 let opt = Option::Some(42);
745 let max = u32::MAX;
746 }
747 };
748
749 let result = analyzer.analyze(&code, "").unwrap();
750 assert_eq!(result.issues.len(), 1);
751 }
752
753 fn apply_fix(content: &str) -> (usize, String) {
754 let analyzer = PathImportAnalyzer::new();
755 let ast = syn::parse_file(content).unwrap();
756 let suggestions = analyzer.suggestions(&ast, content).unwrap();
757 let fixed = suggestions.len();
758 let output = crate::fixer::apply_suggestions(content, &suggestions);
759 (fixed, output)
760 }
761
762 #[test]
763 fn test_fix_rewrites_path_and_adds_import() {
764 let content = "fn main() {\n let content = std::fs::read_to_string(\"file.txt\");\n}\n";
765 let (fixed, output) = apply_fix(content);
766
767 assert_eq!(fixed, 1);
768 assert!(output.contains("use std::fs::read_to_string;"));
769 assert!(output.contains("read_to_string(\"file.txt\")"));
770 assert!(!output.contains("std::fs::read_to_string("));
771 }
772
773 #[test]
774 fn test_fix_preserves_comments_and_blank_lines() {
775 let content = "// top comment\nfn main() {\n // inline note\n let x = std::fs::read_to_string(\"f\");\n\n let _ = x;\n}\n";
776 let (fixed, output) = apply_fix(content);
777
778 assert_eq!(fixed, 1);
779 assert!(output.contains("// top comment"), "top comment preserved");
780 assert!(
781 output.contains("// inline note"),
782 "inline comment preserved"
783 );
784 assert!(
785 output.contains("\n\n let _ = x;"),
786 "blank line preserved"
787 );
788 assert!(output.contains("use std::fs::read_to_string;"));
789 assert!(output.contains("let x = read_to_string(\"f\");"));
790 }
791
792 #[test]
793 fn test_fix_returns_zero_without_issues() {
794 let content = "fn main() {\n let v = Vec::new();\n}\n";
795 let (fixed, output) = apply_fix(content);
796
797 assert_eq!(fixed, 0);
798 assert_eq!(output, content);
799 }
800
801 #[test]
802 fn test_fix_dedups_repeated_import() {
803 let content = "fn main() {\n let a = std::fs::read_to_string(\"a\");\n let b = std::fs::read_to_string(\"b\");\n}\n";
804 let (fixed, output) = apply_fix(content);
805
806 assert_eq!(fixed, 2);
807 assert_eq!(output.matches("use std::fs::read_to_string;").count(), 1);
808 }
809
810 #[test]
811 fn test_fix_skips_short_name_collision() {
812 let content = "fn main() {\n let a = std::fs::read(\"x\");\n let b = crate::helpers::read(\"y\");\n}\n";
813 let (fixed, output) = apply_fix(content);
814
815 assert_eq!(fixed, 0);
816 assert!(output.contains("std::fs::read(\"x\")"));
817 assert!(output.contains("crate::helpers::read(\"y\")"));
818 assert!(!output.contains("use std::fs::read;"));
819 assert!(!output.contains("use crate::helpers::read;"));
820 }
821
822 #[test]
823 fn test_fix_same_path_repeated_is_not_collision() {
824 let content = "fn main() {\n let a = std::fs::read(\"x\");\n let b = std::fs::read(\"y\");\n}\n";
825 let (fixed, output) = apply_fix(content);
826
827 assert_eq!(fixed, 2);
828 assert_eq!(output.matches("use std::fs::read;").count(), 1);
829 }
830
831 #[test]
832 fn test_fix_preserves_generic_arguments() {
833 let content = "fn main() {\n let size = core::mem::size_of::<u32>();\n}\n";
834 let (fixed, output) = apply_fix(content);
835
836 assert_eq!(fixed, 1);
837 assert!(output.contains("use core::mem::size_of;"));
838 assert!(output.contains("size_of::<u32>()"));
839 }
840
841 #[test]
842 fn test_fix_skips_name_bound_by_existing_import() {
843 let content =
844 "use crate::util::read;\n\nfn main() {\n let a = std::fs::read(\"x\");\n}\n";
845 let (fixed, output) = apply_fix(content);
846
847 assert_eq!(fixed, 0);
848 assert_eq!(output, content);
849 }
850
851 #[test]
852 fn test_fix_skips_already_imported_path() {
853 let content = "use std::fs::read;\n\nfn main() {\n let a = std::fs::read(\"x\");\n}\n";
854 let (fixed, output) = apply_fix(content);
855
856 assert_eq!(fixed, 0);
857 assert_eq!(output, content);
858 }
859
860 #[test]
861 fn test_fix_skips_name_bound_by_local_fn() {
862 let content = "fn read(path: &str) -> &str {\n path\n}\n\nfn main() {\n let a = std::fs::read(\"x\");\n}\n";
863 let (fixed, output) = apply_fix(content);
864
865 assert_eq!(fixed, 0);
866 assert_eq!(output, content);
867 }
868
869 #[test]
870 fn test_fix_skips_name_bound_by_local_binding() {
871 let content = "fn main() {\n let read = 1;\n let a = std::fs::read(\"x\");\n let _ = read;\n}\n";
872 let (fixed, output) = apply_fix(content);
873
874 assert_eq!(fixed, 0);
875 assert_eq!(output, content);
876 }
877
878 #[test]
879 fn test_fix_skips_module_with_foreign_glob() {
880 let content = "use helpers::*;\n\nfn main() {\n let a = std::fs::read(\"x\");\n}\n";
881 let (fixed, output) = apply_fix(content);
882
883 assert_eq!(fixed, 0);
884 assert_eq!(output, content);
885 }
886
887 #[test]
888 fn test_fix_inserts_import_into_nested_module() {
889 let content = "fn top() {\n let a = std::fs::read_to_string(\"a\");\n}\n\nmod inner {\n fn f() {\n let b = std::fs::read_to_string(\"b\");\n }\n}\n";
890 let (fixed, output) = apply_fix(content);
891
892 assert_eq!(fixed, 2);
893 assert_eq!(output.matches("use std::fs::read_to_string;").count(), 2);
894 assert!(!output.contains("std::fs::read_to_string("));
895 let import_pos = output.find("mod inner").unwrap();
896 assert!(
897 output[import_pos..].contains("use std::fs::read_to_string;"),
898 "nested module receives its own import"
899 );
900 }
901
902 #[test]
903 fn test_fix_respects_names_inherited_via_super_glob() {
904 let content = "fn read() {}\n\nmod inner {\n use super::*;\n\n fn f() {\n let x = std::fs::read(\"f\");\n }\n}\n";
905 let (fixed, output) = apply_fix(content);
906
907 assert_eq!(fixed, 0);
908 assert_eq!(output, content);
909 }
910
911 #[test]
912 fn test_fix_skips_when_descendant_uses_name_bare() {
913 let content = "fn parent_call() {\n let a = std::fs::read(\"f\");\n}\n\nmod inner {\n use super::*;\n\n fn g() {\n read(\"x\");\n }\n}\n";
914 let (fixed, output) = apply_fix(content);
915
916 assert_eq!(fixed, 0);
917 assert_eq!(output, content);
918 }
919
920 #[test]
921 fn test_fix_allows_test_module_with_super_glob() {
922 let content = "fn top() {\n let a = std::fs::read_to_string(\"a\");\n}\n\nmod tests {\n use super::*;\n\n fn t() {\n let b = std::fs::read_to_string(\"b\");\n }\n}\n";
923 let (fixed, output) = apply_fix(content);
924
925 assert_eq!(fixed, 2);
926 assert!(!output.contains("std::fs::read_to_string("));
927 }
928
929 #[test]
930 fn test_fix_crate_rooted_path() {
931 let content = "fn main() {\n crate::util::helper();\n}\n";
932 let (fixed, output) = apply_fix(content);
933
934 assert_eq!(fixed, 1);
935 assert!(output.contains("use crate::util::helper;"));
936 assert!(output.contains(" helper();"));
937 }
938
939 #[test]
940 fn test_default_implementation() {
941 let analyzer = PathImportAnalyzer;
942 assert_eq!(analyzer.name(), "path_import");
943 }
944
945 #[test]
946 fn test_single_segment_path() {
947 let analyzer = PathImportAnalyzer::new();
948 let code: File = parse_quote! {
949 fn main() {
950 println!("test");
951 }
952 };
953
954 let result = analyzer.analyze(&code, "").unwrap();
955 assert_eq!(result.issues.len(), 0);
956 }
957
958 #[test]
959 fn test_core_module_functions() {
960 let analyzer = PathImportAnalyzer::new();
961 let code: File = parse_quote! {
962 fn main() {
963 let size = core::mem::size_of::<u32>();
964 }
965 };
966
967 let result = analyzer.analyze(&code, "").unwrap();
968 assert!(!result.issues.is_empty());
969 }
970
971 #[test]
972 fn test_alloc_module_functions() {
973 let analyzer = PathImportAnalyzer::new();
974 let code: File = parse_quote! {
975 fn main() {
976 let data = alloc::format::format(format_args!("test"));
977 }
978 };
979
980 let result = analyzer.analyze(&code, "").unwrap();
981 assert!(!result.issues.is_empty());
982 }
983
984 #[test]
985 fn test_two_segment_path() {
986 let analyzer = PathImportAnalyzer::new();
987 let code: File = parse_quote! {
988 fn main() {
989 let x = fs::read("file");
990 }
991 };
992
993 let result = analyzer.analyze(&code, "").unwrap();
994 assert_eq!(result.issues.len(), 0);
995 }
996
997 #[test]
998 fn test_screaming_snake_case_constant() {
999 let analyzer = PathImportAnalyzer::new();
1000 let code: File = parse_quote! {
1001 fn main() {
1002 let x = std::u32::MAX_VALUE;
1003 }
1004 };
1005
1006 let result = analyzer.analyze(&code, "").unwrap();
1007 assert_eq!(result.issues.len(), 0);
1008 }
1009
1010 #[test]
1011 fn test_result_fixable_count() {
1012 let analyzer = PathImportAnalyzer::new();
1013 let code: File = parse_quote! {
1014 fn main() {
1015 let a = std::fs::read_to_string("f");
1016 let b = std::io::stdin();
1017 }
1018 };
1019
1020 let result = analyzer.analyze(&code, "").unwrap();
1021 assert_eq!(result.fixable_count, result.issues.len());
1022 }
1023
1024 #[test]
1025 fn test_issue_format() {
1026 let analyzer = PathImportAnalyzer::new();
1027 let code: File = parse_quote! {
1028 fn main() {
1029 let x = std::fs::read("file");
1030 }
1031 };
1032
1033 let result = analyzer.analyze(&code, "").unwrap();
1034 assert!(!result.issues.is_empty());
1035 let issue = &result.issues[0];
1036 assert!(
1037 issue
1038 .diagnostic
1039 .message
1040 .contains("Use import instead of path")
1041 );
1042 assert!(issue.fix.is_available());
1043 if let Some((import, pattern, replacement)) = issue.fix.as_import() {
1044 assert!(import.contains("use"));
1045 assert_eq!(pattern, "std::fs::read");
1046 assert_eq!(replacement, "read");
1047 } else {
1048 panic!("Expected Fix::WithImport");
1049 }
1050 }
1051}