Skip to content

Commit 65f4098

Browse files
authored
refactor module_style (#15469)
Previous approach roughly works as follows: For each file, it marks every directory segment of its relative path to crate's working directory as a potential module. If segment `foo` exists, `foo.rs` exists and `foo/mod.rs` does not exist, the lint will trigger. The main issue with that is it ignores all the prefixes. #14697 also explained a bit how it works. This PR refactored previous approach with `check_item` and `check_item_post`. We also skip mod tagged with `#[path = ...]`. Fixes #14697 Fixes #10271 Fixes #11916 changelog: [`self_named_module_files`]: don't lint mod tagged with path attribute changelog: [`mod_module_files`]: don't lint mod tagged with path attribute
2 parents 5e02a4e + b3e056f commit 65f4098

File tree

22 files changed

+167
-85
lines changed

22 files changed

+167
-85
lines changed

clippy_lints/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
671671
store.register_late_pass(|_| Box::new(from_str_radix_10::FromStrRadix10));
672672
store.register_late_pass(move |_| Box::new(if_then_some_else_none::IfThenSomeElseNone::new(conf)));
673673
store.register_late_pass(|_| Box::new(bool_assert_comparison::BoolAssertComparison));
674-
store.register_early_pass(move || Box::new(module_style::ModStyle));
674+
store.register_early_pass(move || Box::new(module_style::ModStyle::default()));
675675
store.register_late_pass(|_| Box::<unused_async::UnusedAsync>::default());
676676
store.register_late_pass(move |tcx| Box::new(disallowed_types::DisallowedTypes::new(tcx, conf)));
677677
store.register_late_pass(move |tcx| Box::new(missing_enforced_import_rename::ImportRename::new(tcx, conf)));

clippy_lints/src/module_style.rs

Lines changed: 79 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
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};
43
use rustc_lint::{EarlyContext, EarlyLintPass, Level, LintContext};
54
use rustc_session::impl_lint_pass;
65
use rustc_span::def_id::LOCAL_CRATE;
7-
use rustc_span::{FileName, SourceFile, Span, SyntaxContext};
8-
use std::ffi::OsStr;
9-
use std::path::{Component, Path};
6+
use rustc_span::{FileName, SourceFile, Span, SyntaxContext, sym};
7+
use std::path::{Path, PathBuf};
8+
use std::sync::Arc;
109

1110
declare_clippy_lint! {
1211
/// ### What it does
@@ -60,107 +59,97 @@ declare_clippy_lint! {
6059
/// mod.rs
6160
/// lib.rs
6261
/// ```
63-
6462
#[clippy::version = "1.57.0"]
6563
pub SELF_NAMED_MODULE_FILES,
6664
restriction,
6765
"checks that module layout is consistent"
6866
}
6967

70-
pub struct ModStyle;
71-
7268
impl_lint_pass!(ModStyle => [MOD_MODULE_FILES, SELF_NAMED_MODULE_FILES]);
7369

70+
pub struct ModState {
71+
contains_external: bool,
72+
has_path_attr: bool,
73+
mod_file: Arc<SourceFile>,
74+
}
75+
76+
#[derive(Default)]
77+
pub struct ModStyle {
78+
working_dir: Option<PathBuf>,
79+
module_stack: Vec<ModState>,
80+
}
81+
7482
impl EarlyLintPass for ModStyle {
7583
fn check_crate(&mut self, cx: &EarlyContext<'_>, _: &ast::Crate) {
84+
self.working_dir = cx.sess().opts.working_dir.local_path().map(Path::to_path_buf);
85+
}
86+
87+
fn check_item(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) {
7688
if cx.builder.lint_level(MOD_MODULE_FILES).level == Level::Allow
7789
&& cx.builder.lint_level(SELF_NAMED_MODULE_FILES).level == Level::Allow
7890
{
7991
return;
8092
}
93+
if let ItemKind::Mod(.., ModKind::Loaded(_, Inline::No { .. }, mod_spans, ..)) = &item.kind {
94+
let has_path_attr = item.attrs.iter().any(|attr| attr.has_name(sym::path));
95+
if !has_path_attr && let Some(current) = self.module_stack.last_mut() {
96+
current.contains_external = true;
97+
}
98+
let mod_file = cx.sess().source_map().lookup_source_file(mod_spans.inner_span.lo());
99+
self.module_stack.push(ModState {
100+
contains_external: false,
101+
has_path_attr,
102+
mod_file,
103+
});
104+
}
105+
}
81106

82-
let files = cx.sess().source_map().files();
83-
84-
let Some(trim_to_src) = cx.sess().opts.working_dir.local_path() else {
107+
fn check_item_post(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) {
108+
if cx.builder.lint_level(MOD_MODULE_FILES).level == Level::Allow
109+
&& cx.builder.lint_level(SELF_NAMED_MODULE_FILES).level == Level::Allow
110+
{
85111
return;
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
109-
} else {
110-
continue;
111-
};
112-
113-
if let Some(stem) = path.file_stem() {
114-
file_map.insert(stem, (file, path));
115-
}
116-
process_paths_for_mod_files(path, &mut folder_segments, &mut mod_folders);
117-
check_self_named_mod_exists(cx, path, file);
118-
}
119112
}
120113

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-
);
114+
if let ItemKind::Mod(.., ModKind::Loaded(_, Inline::No { .. }, ..)) = &item.kind
115+
&& let Some(current) = self.module_stack.pop()
116+
&& !current.has_path_attr
117+
{
118+
let Some(path) = self
119+
.working_dir
120+
.as_ref()
121+
.and_then(|src| try_trim_file_path_prefix(&current.mod_file, src))
122+
else {
123+
return;
124+
};
125+
if current.contains_external {
126+
check_self_named_module(cx, path, &current.mod_file);
138127
}
128+
check_mod_module(cx, path, &current.mod_file);
139129
}
140130
}
141131
}
142132

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());
133+
fn check_self_named_module(cx: &EarlyContext<'_>, path: &Path, file: &SourceFile) {
134+
if !path.ends_with("mod.rs") {
135+
let mut mod_folder = path.with_extension("");
136+
span_lint_and_then(
137+
cx,
138+
SELF_NAMED_MODULE_FILES,
139+
Span::new(file.start_pos, file.start_pos, SyntaxContext::root(), None),
140+
format!("`mod.rs` files are required, found `{}`", path.display()),
141+
|diag| {
142+
mod_folder.push("mod.rs");
143+
diag.help(format!("move `{}` to `{}`", path.display(), mod_folder.display()));
144+
},
145+
);
154146
}
155-
let folders = comp.filter_map(|c| if let Component::Normal(s) = c { Some(s) } else { None });
156-
folder_segments.extend(folders);
157147
}
158148

159-
/// Checks every path for the presence of `mod.rs` files and emits the lint if found.
160149
/// We should not emit a lint for test modules in the presence of `mod.rs`.
161150
/// 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)
162151
/// for code-sharing between tests.
163-
fn check_self_named_mod_exists(cx: &EarlyContext<'_>, path: &Path, file: &SourceFile) {
152+
fn check_mod_module(cx: &EarlyContext<'_>, path: &Path, file: &SourceFile) {
164153
if path.ends_with("mod.rs") && !path.starts_with("tests") {
165154
span_lint_and_then(
166155
cx,
@@ -177,3 +166,17 @@ fn check_self_named_mod_exists(cx: &EarlyContext<'_>, path: &Path, file: &Source
177166
);
178167
}
179168
}
169+
170+
fn try_trim_file_path_prefix<'a>(file: &'a SourceFile, prefix: &'a Path) -> Option<&'a Path> {
171+
if let FileName::Real(name) = &file.name
172+
&& let Some(mut path) = name.local_path()
173+
&& file.cnum == LOCAL_CRATE
174+
{
175+
if !path.is_relative() {
176+
path = path.strip_prefix(prefix).ok()?;
177+
}
178+
Some(path)
179+
} else {
180+
None
181+
}
182+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
error: `mod.rs` files are required, found `src/foo.rs`
2+
--> src/foo.rs:1:1
3+
|
4+
1 | pub mod bar;
5+
| ^
6+
|
7+
= help: move `src/foo.rs` to `src/foo/mod.rs`
8+
= note: `-D clippy::self-named-module-files` implied by `-D warnings`
9+
= help: to override `-D warnings` add `#[allow(clippy::self_named_module_files)]`
10+
11+
error: could not compile `duplicated-mod-names-14697` (lib) due to 1 previous error
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Should trigger when multiple mods with the same name exist and not all of them follow self-named convention.
2+
# See issue #14697.
3+
[package]
4+
name = "duplicated-mod-names-14697"
5+
version = "0.1.0"
6+
edition = "2024"
7+
publish = false
8+
9+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
10+
11+
[dependencies]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod bar;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#![warn(clippy::self_named_module_files)]
2+
3+
pub mod foo;
4+
pub mod other;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod foo;
Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
error: `mod.rs` files are required, found `src/bad/inner.rs`
2-
--> src/bad/inner.rs:1:1
1+
error: `mod.rs` files are required, found `src/bad/inner/stuff.rs`
2+
--> src/bad/inner/stuff.rs:1:1
33
|
4-
1 | pub mod stuff;
4+
1 | pub mod most;
55
| ^
66
|
7-
= help: move `src/bad/inner.rs` to `src/bad/inner/mod.rs`
7+
= help: move `src/bad/inner/stuff.rs` to `src/bad/inner/stuff/mod.rs`
88
= note: `-D clippy::self-named-module-files` implied by `-D warnings`
99
= help: to override `-D warnings` add `#[allow(clippy::self_named_module_files)]`
1010

11-
error: `mod.rs` files are required, found `src/bad/inner/stuff.rs`
12-
--> src/bad/inner/stuff.rs:1:1
11+
error: `mod.rs` files are required, found `src/bad/inner.rs`
12+
--> src/bad/inner.rs:1:1
1313
|
14-
1 | pub mod most;
14+
1 | pub mod stuff;
1515
| ^
1616
|
17-
= help: move `src/bad/inner/stuff.rs` to `src/bad/inner/stuff/mod.rs`
17+
= help: move `src/bad/inner.rs` to `src/bad/inner/mod.rs`
1818

1919
error: could not compile `fail-mod` (bin "fail-mod") due to 2 previous errors

0 commit comments

Comments
 (0)