|
1 | 1 | use clippy_utils::diagnostics::span_lint_and_then;
|
2 |
| -use rustc_ast::ast; |
3 |
| -use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexSet}; |
| 2 | +use rustc_ast::ast::{self, Inline, ItemKind, ModKind}; |
| 3 | +use rustc_data_structures::fx::FxHashSet; |
4 | 4 | use rustc_lint::{EarlyContext, EarlyLintPass, Level, LintContext};
|
5 | 5 | use rustc_session::impl_lint_pass;
|
6 | 6 | use rustc_span::def_id::LOCAL_CRATE;
|
7 | 7 | use rustc_span::{FileName, SourceFile, Span, SyntaxContext};
|
8 |
| -use std::ffi::OsStr; |
9 |
| -use std::path::{Component, Path}; |
| 8 | +use std::path::{Path, PathBuf}; |
10 | 9 |
|
11 | 10 | declare_clippy_lint! {
|
12 | 11 | /// ### What it does
|
@@ -67,100 +66,95 @@ declare_clippy_lint! {
|
67 | 66 | "checks that module layout is consistent"
|
68 | 67 | }
|
69 | 68 |
|
70 |
| -pub struct ModStyle; |
71 |
| - |
72 | 69 | impl_lint_pass!(ModStyle => [MOD_MODULE_FILES, SELF_NAMED_MODULE_FILES]);
|
73 | 70 |
|
| 71 | +#[derive(Default)] |
| 72 | +pub struct ModStyle { |
| 73 | + mod_folders: FxHashSet<PathBuf>, |
| 74 | + working_dir: Option<PathBuf>, |
| 75 | +} |
| 76 | + |
74 | 77 | impl EarlyLintPass for ModStyle {
|
75 | 78 | fn check_crate(&mut self, cx: &EarlyContext<'_>, _: &ast::Crate) {
|
76 | 79 | if cx.builder.lint_level(MOD_MODULE_FILES).level == Level::Allow
|
77 | 80 | && cx.builder.lint_level(SELF_NAMED_MODULE_FILES).level == Level::Allow
|
78 | 81 | {
|
79 | 82 | return;
|
80 | 83 | }
|
81 |
| - |
82 |
| - let files = cx.sess().source_map().files(); |
83 |
| - |
84 |
| - let Some(trim_to_src) = cx.sess().opts.working_dir.local_path() else { |
| 84 | + let Some(working_dir) = cx.sess().opts.working_dir.local_path() else { |
85 | 85 | return;
|
86 | 86 | };
|
87 |
| - |
88 |
| - // `folder_segments` is all unique folder path segments `path/to/foo.rs` gives |
89 |
| - // `[path, to]` but not foo |
90 |
| - let mut folder_segments = FxIndexSet::default(); |
91 |
| - // `mod_folders` is all the unique folder names that contain a mod.rs file |
92 |
| - let mut mod_folders = FxHashSet::default(); |
93 |
| - // `file_map` maps file names to the full path including the file name |
94 |
| - // `{ foo => path/to/foo.rs, .. } |
95 |
| - let mut file_map = FxHashMap::default(); |
96 |
| - for file in files.iter() { |
97 |
| - if let FileName::Real(name) = &file.name |
98 |
| - && let Some(lp) = name.local_path() |
99 |
| - && file.cnum == LOCAL_CRATE |
100 |
| - { |
101 |
| - // [#8887](https://github.com/rust-lang/rust-clippy/issues/8887) |
102 |
| - // Only check files in the current crate. |
103 |
| - // Fix false positive that crate dependency in workspace sub directory |
104 |
| - // is checked unintentionally. |
105 |
| - let path = if lp.is_relative() { |
106 |
| - lp |
107 |
| - } else if let Ok(relative) = lp.strip_prefix(trim_to_src) { |
108 |
| - relative |
| 87 | + self.mod_folders = cx |
| 88 | + .sess() |
| 89 | + .source_map() |
| 90 | + .files() |
| 91 | + .iter() |
| 92 | + .filter_map(|file| { |
| 93 | + let path = trim_file_path_to(file, working_dir)?; |
| 94 | + if path.extension()?.eq("rs") { |
| 95 | + check_mod_module(cx, path, file); |
| 96 | + let mut mod_folder = path.to_path_buf(); |
| 97 | + mod_folder.pop(); |
| 98 | + Some(mod_folder) |
109 | 99 | } else {
|
110 |
| - continue; |
111 |
| - }; |
112 |
| - |
113 |
| - if let Some(stem) = path.file_stem() { |
114 |
| - file_map.insert(stem, (file, path)); |
| 100 | + None |
115 | 101 | }
|
116 |
| - process_paths_for_mod_files(path, &mut folder_segments, &mut mod_folders); |
117 |
| - check_self_named_mod_exists(cx, path, file); |
118 |
| - } |
| 102 | + }) |
| 103 | + .collect(); |
| 104 | + self.working_dir = Some(working_dir.to_path_buf()); |
| 105 | + } |
| 106 | + |
| 107 | + fn check_item(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) { |
| 108 | + if let ItemKind::Mod(.., mod_kind) = &item.kind |
| 109 | + && let ModKind::Loaded(_, inline, mod_spans, _) = mod_kind |
| 110 | + && matches!(inline, Inline::No) |
| 111 | + && let mod_file = cx.sess().source_map().lookup_source_file(mod_spans.inner_span.lo()) |
| 112 | + && let Some(working_dir) = self.working_dir.as_ref() |
| 113 | + && let Some(mod_path) = trim_file_path_to(&mod_file, working_dir.as_path()) |
| 114 | + { |
| 115 | + check_self_named_module(cx, mod_path, &mod_file, &self.mod_folders); |
119 | 116 | }
|
| 117 | + } |
| 118 | +} |
120 | 119 |
|
121 |
| - for folder in &folder_segments { |
122 |
| - if !mod_folders.contains(folder) |
123 |
| - && let Some((file, path)) = file_map.get(folder) |
124 |
| - { |
125 |
| - span_lint_and_then( |
126 |
| - cx, |
127 |
| - SELF_NAMED_MODULE_FILES, |
128 |
| - Span::new(file.start_pos, file.start_pos, SyntaxContext::root(), None), |
129 |
| - format!("`mod.rs` files are required, found `{}`", path.display()), |
130 |
| - |diag| { |
131 |
| - let mut correct = path.to_path_buf(); |
132 |
| - correct.pop(); |
133 |
| - correct.push(folder); |
134 |
| - correct.push("mod.rs"); |
135 |
| - diag.help(format!("move `{}` to `{}`", path.display(), correct.display(),)); |
136 |
| - }, |
137 |
| - ); |
138 |
| - } |
| 120 | +fn trim_file_path_to<'a>(file: &'a SourceFile, prefix: &'a Path) -> Option<&'a Path> { |
| 121 | + if let FileName::Real(name) = &file.name |
| 122 | + && let Some(mut path) = name.local_path() |
| 123 | + && file.cnum == LOCAL_CRATE |
| 124 | + { |
| 125 | + if !path.is_relative() { |
| 126 | + path = path.strip_prefix(prefix).ok()?; |
139 | 127 | }
|
| 128 | + Some(path) |
| 129 | + } else { |
| 130 | + None |
140 | 131 | }
|
141 | 132 | }
|
142 | 133 |
|
143 |
| -/// For each `path` we add each folder component to `folder_segments` and if the file name |
144 |
| -/// is `mod.rs` we add it's parent folder to `mod_folders`. |
145 |
| -fn process_paths_for_mod_files<'a>( |
146 |
| - path: &'a Path, |
147 |
| - folder_segments: &mut FxIndexSet<&'a OsStr>, |
148 |
| - mod_folders: &mut FxHashSet<&'a OsStr>, |
149 |
| -) { |
150 |
| - let mut comp = path.components().rev().peekable(); |
151 |
| - let _: Option<_> = comp.next(); |
152 |
| - if path.ends_with("mod.rs") { |
153 |
| - mod_folders.insert(comp.peek().map(|c| c.as_os_str()).unwrap_or_default()); |
| 134 | +fn check_self_named_module(cx: &EarlyContext<'_>, path: &Path, file: &SourceFile, mod_folders: &FxHashSet<PathBuf>) { |
| 135 | + if !path.ends_with("mod.rs") |
| 136 | + && let mod_folder = path.with_extension("") |
| 137 | + && mod_folders.contains(&mod_folder) |
| 138 | + { |
| 139 | + span_lint_and_then( |
| 140 | + cx, |
| 141 | + SELF_NAMED_MODULE_FILES, |
| 142 | + Span::new(file.start_pos, file.start_pos, SyntaxContext::root(), None), |
| 143 | + format!("`mod.rs` files are required, found `{}`", path.display()), |
| 144 | + |diag| { |
| 145 | + let mut mod_file = mod_folder.to_path_buf(); |
| 146 | + mod_file.push("mod.rs"); |
| 147 | + |
| 148 | + diag.help(format!("move `{}` to `{}`", path.display(), mod_file.display())); |
| 149 | + }, |
| 150 | + ); |
154 | 151 | }
|
155 |
| - let folders = comp.filter_map(|c| if let Component::Normal(s) = c { Some(s) } else { None }); |
156 |
| - folder_segments.extend(folders); |
157 | 152 | }
|
158 | 153 |
|
159 |
| -/// Checks every path for the presence of `mod.rs` files and emits the lint if found. |
160 | 154 | /// We should not emit a lint for test modules in the presence of `mod.rs`.
|
161 | 155 | /// Using `mod.rs` in integration tests is a [common pattern](https://doc.rust-lang.org/book/ch11-03-test-organization.html#submodules-in-integration-test)
|
162 | 156 | /// for code-sharing between tests.
|
163 |
| -fn check_self_named_mod_exists(cx: &EarlyContext<'_>, path: &Path, file: &SourceFile) { |
| 157 | +fn check_mod_module(cx: &EarlyContext<'_>, path: &Path, file: &SourceFile) { |
164 | 158 | if path.ends_with("mod.rs") && !path.starts_with("tests") {
|
165 | 159 | span_lint_and_then(
|
166 | 160 | cx,
|
|
0 commit comments