Skip to content

Commit f30bdb9

Browse files
authored
new module style lint: inline_modules (#16732)
*[View all comments](https://triagebot.infra.rust-lang.org/gh-comments/rust-lang/rust-clippy/pull/16732)* fixes: #15966 add new module style restriction lint that checks for use of inline modules, with an exception for test module. changelog: new lint: [`inline_modules`]
2 parents 33b28c6 + 6a16a8f commit f30bdb9

11 files changed

Lines changed: 277 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6804,6 +6804,7 @@ Released 2018-09-13
68046804
[`inline_asm_x86_att_syntax`]: https://rust-lang.github.io/rust-clippy/master/index.html#inline_asm_x86_att_syntax
68056805
[`inline_asm_x86_intel_syntax`]: https://rust-lang.github.io/rust-clippy/master/index.html#inline_asm_x86_intel_syntax
68066806
[`inline_fn_without_body`]: https://rust-lang.github.io/rust-clippy/master/index.html#inline_fn_without_body
6807+
[`inline_modules`]: https://rust-lang.github.io/rust-clippy/master/index.html#inline_modules
68076808
[`inspect_for_each`]: https://rust-lang.github.io/rust-clippy/master/index.html#inspect_for_each
68086809
[`int_plus_one`]: https://rust-lang.github.io/rust-clippy/master/index.html#int_plus_one
68096810
[`integer_arithmetic`]: https://rust-lang.github.io/rust-clippy/master/index.html#integer_arithmetic

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
537537
crate::missing_trait_methods::MISSING_TRAIT_METHODS_INFO,
538538
crate::mixed_read_write_in_expression::DIVERGING_SUB_EXPRESSION_INFO,
539539
crate::mixed_read_write_in_expression::MIXED_READ_WRITE_IN_EXPRESSION_INFO,
540+
crate::module_style::INLINE_MODULES_INFO,
540541
crate::module_style::MOD_MODULE_FILES_INFO,
541542
crate::module_style::SELF_NAMED_MODULE_FILES_INFO,
542543
crate::multi_assignments::MULTI_ASSIGNMENTS_INFO,

clippy_lints/src/module_style.rs

Lines changed: 140 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,46 @@
1+
use clippy_utils::ast_utils::is_cfg_test;
12
use clippy_utils::diagnostics::span_lint_and_then;
3+
use clippy_utils::source::snippet;
24
use rustc_ast::ast::{self, Inline, ItemKind, ModKind};
35
use rustc_lint::{EarlyContext, EarlyLintPass, Level, LintContext};
46
use rustc_session::impl_lint_pass;
57
use rustc_span::def_id::LOCAL_CRATE;
6-
use rustc_span::{FileName, SourceFile, Span, SyntaxContext, sym};
8+
use rustc_span::{FileName, Ident, SourceFile, Span, SyntaxContext, sym};
79
use std::path::{Component, Path, PathBuf};
810
use std::sync::Arc;
911

12+
declare_clippy_lint! {
13+
/// ### What it does
14+
/// Checks that module layout does not use inline modules.
15+
/// Inline test modules (anything annotated with `#[cfg(test)]`) are not linted.
16+
///
17+
/// ### Why restrict this?
18+
/// Having multiple module layout styles in a project can be confusing.
19+
///
20+
/// ### Known problems
21+
/// The lint currently doesn't lint inline modules whose parent module is annotated
22+
/// with the `#[path]` attribute.
23+
///
24+
/// ### Example
25+
/// ```ignore
26+
/// // in `src/lib.rs`
27+
/// mod foo {
28+
/// /* module contents */
29+
/// }
30+
/// ```
31+
/// Use instead:
32+
/// ```ignore
33+
/// // in `src/lib.rs`
34+
/// mod foo;
35+
/// // in `src/foo.rs` (or `src/foo/mod.rs`)
36+
/// /* module contents */
37+
/// ```
38+
#[clippy::version = "1.96.0"]
39+
pub INLINE_MODULES,
40+
restriction,
41+
"checks that module layout does not use inline modules"
42+
}
43+
1044
declare_clippy_lint! {
1145
/// ### What it does
1246
/// Checks that module layout uses only self named module files; bans `mod.rs` files.
@@ -65,18 +99,36 @@ declare_clippy_lint! {
6599
"checks that module layout is consistent"
66100
}
67101

68-
impl_lint_pass!(ModStyle => [MOD_MODULE_FILES, SELF_NAMED_MODULE_FILES]);
102+
impl_lint_pass!(ModStyle => [
103+
INLINE_MODULES,
104+
MOD_MODULE_FILES,
105+
SELF_NAMED_MODULE_FILES,
106+
]);
69107

70108
pub struct ModState {
109+
mod_file: Arc<SourceFile>,
110+
mod_ident: Ident,
111+
path_from_working_dir: Option<PathBuf>,
71112
contains_external: bool,
72113
has_path_attr: bool,
73-
mod_file: Arc<SourceFile>,
114+
is_cfg_test: bool,
74115
}
75116

76117
#[derive(Default)]
77118
pub struct ModStyle {
78119
working_dir: Option<PathBuf>,
79-
module_stack: Vec<ModState>,
120+
regular_mod_stack: Vec<ModState>,
121+
inline_mod_stack: Vec<ModState>,
122+
}
123+
124+
impl ModStyle {
125+
fn inside_cfg_test_inline_mod(&self) -> bool {
126+
self.inline_mod_stack.last().is_some_and(|last| last.is_cfg_test)
127+
}
128+
129+
fn get_relative_path_from_working_dir(&self, file: &SourceFile) -> Option<PathBuf> {
130+
try_trim_file_path_prefix(file, self.working_dir.as_ref()?).map(Path::to_path_buf)
131+
}
80132
}
81133

82134
impl EarlyLintPass for ModStyle {
@@ -87,45 +139,83 @@ impl EarlyLintPass for ModStyle {
87139
fn check_item(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) {
88140
if cx.builder.lint_level(MOD_MODULE_FILES).level == Level::Allow
89141
&& cx.builder.lint_level(SELF_NAMED_MODULE_FILES).level == Level::Allow
142+
&& cx.builder.lint_level(INLINE_MODULES).level == Level::Allow
90143
{
91144
return;
92145
}
93-
if let ItemKind::Mod(.., ModKind::Loaded(_, Inline::No { .. }, mod_spans, ..)) = &item.kind {
146+
if let ItemKind::Mod(_, mod_ident, ModKind::Loaded(_, inline_or_not, mod_spans, ..)) = &item.kind {
94147
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-
}
98148
let mod_file = cx.sess().source_map().lookup_source_file(mod_spans.inner_span.lo());
99-
self.module_stack.push(ModState {
149+
let path_from_working_dir = self.get_relative_path_from_working_dir(&mod_file);
150+
let current = ModState {
151+
mod_file,
152+
mod_ident: *mod_ident,
153+
path_from_working_dir,
100154
contains_external: false,
101155
has_path_attr,
102-
mod_file,
103-
});
156+
is_cfg_test: self.inside_cfg_test_inline_mod() || is_cfg_test(item),
157+
};
158+
match inline_or_not {
159+
Inline::Yes => {
160+
if !current.is_cfg_test
161+
&& !item.span.from_expansion()
162+
&& self.regular_mod_stack.last().is_none_or(|last| !last.has_path_attr)
163+
&& let Some(path) = &current.path_from_working_dir
164+
{
165+
let opt_extra_mod_dir = self.regular_mod_stack.last().and_then(|last| {
166+
if last.path_from_working_dir.as_ref()?.ends_with("mod.rs") {
167+
None
168+
} else {
169+
Some(&last.mod_ident)
170+
}
171+
});
172+
check_inline_module(
173+
cx,
174+
path,
175+
*mod_ident,
176+
item.span,
177+
opt_extra_mod_dir
178+
.into_iter()
179+
.chain(self.inline_mod_stack.iter().map(|state| &state.mod_ident)),
180+
);
181+
}
182+
self.inline_mod_stack.push(current);
183+
},
184+
Inline::No { .. } => {
185+
if !has_path_attr && let Some(last) = self.regular_mod_stack.last_mut() {
186+
last.contains_external = true;
187+
}
188+
self.regular_mod_stack.push(current);
189+
},
190+
}
104191
}
105192
}
106193

107194
fn check_item_post(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) {
108195
if cx.builder.lint_level(MOD_MODULE_FILES).level == Level::Allow
109196
&& cx.builder.lint_level(SELF_NAMED_MODULE_FILES).level == Level::Allow
197+
&& cx.builder.lint_level(INLINE_MODULES).level == Level::Allow
110198
{
111199
return;
112200
}
113201

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);
202+
if let ItemKind::Mod(.., ModKind::Loaded(_, inline_or_not, ..)) = &item.kind {
203+
match inline_or_not {
204+
Inline::Yes => {
205+
self.inline_mod_stack.pop();
206+
},
207+
Inline::No { .. } => {
208+
if let Some(current) = self.regular_mod_stack.pop()
209+
&& let Some(path) = &current.path_from_working_dir
210+
&& !current.has_path_attr
211+
{
212+
if current.contains_external {
213+
check_self_named_module(cx, path, &current.mod_file);
214+
}
215+
check_mod_module(cx, path, &current.mod_file);
216+
}
217+
},
127218
}
128-
check_mod_module(cx, path, &current.mod_file);
129219
}
130220
}
131221
}
@@ -173,6 +263,31 @@ fn check_mod_module(cx: &EarlyContext<'_>, path: &Path, file: &SourceFile) {
173263
}
174264
}
175265

266+
fn check_inline_module<'a>(
267+
cx: &EarlyContext<'_>,
268+
path: &Path,
269+
mod_ident: Ident,
270+
mod_span: Span,
271+
ancestor_mods: impl Iterator<Item = &'a Ident>,
272+
) {
273+
let Some(parent) = path.parent() else { return };
274+
275+
span_lint_and_then(cx, INLINE_MODULES, mod_span, "inline module found", |diag| {
276+
let mut mod_folder = parent.to_path_buf();
277+
mod_folder.extend(ancestor_mods.map(Ident::as_str));
278+
let mod_name = mod_ident.as_str();
279+
280+
let mod_file = mod_folder.join(mod_name).join("mod.rs");
281+
let self_named_mod_file = mod_folder.join(format!("{mod_name}.rs"));
282+
let outlined_mod = snippet(cx, mod_span.with_hi(mod_ident.span.hi()), "");
283+
diag.help(format!(
284+
"move the contents of the module to `{}` or `{}`, and replace this with `{outlined_mod};`",
285+
self_named_mod_file.display(),
286+
mod_file.display(),
287+
));
288+
});
289+
}
290+
176291
fn try_trim_file_path_prefix<'a>(file: &'a SourceFile, prefix: &'a Path) -> Option<&'a Path> {
177292
if let FileName::Real(name) = &file.name
178293
&& let Some(mut path) = name.local_path()

clippy_utils/src/ast_utils/mod.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
66

77
use crate::{both, over};
8-
use rustc_ast::{self as ast, *};
8+
use rustc_ast::{self as ast, HasAttrs, *};
9+
use rustc_span::sym;
910
use rustc_span::symbol::Ident;
1011
use std::mem;
1112

@@ -1044,3 +1045,17 @@ pub fn eq_delim_args(l: &DelimArgs, r: &DelimArgs) -> bool {
10441045
&& l.tokens.len() == r.tokens.len()
10451046
&& l.tokens.iter().zip(r.tokens.iter()).all(|(a, b)| a.eq_unspanned(b))
10461047
}
1048+
1049+
/// Checks whether `#[cfg(test)]` is directly applied to `item`.
1050+
pub fn is_cfg_test(item: &impl HasAttrs) -> bool {
1051+
item.attrs().iter().any(|attr| {
1052+
if attr.has_name(sym::cfg)
1053+
&& let Some(item_list) = attr.meta_item_list()
1054+
&& item_list.iter().any(|item| item.has_name(sym::test))
1055+
{
1056+
true
1057+
} else {
1058+
false
1059+
}
1060+
})
1061+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
error: inline module found
2+
--> src/other.rs:1:1
3+
|
4+
1 | mod foo {}
5+
| ^^^^^^^^^^
6+
|
7+
= help: move the contents of the module to `src/other/foo.rs` or `src/other/foo/mod.rs`, and replace this with `mod foo;`
8+
= note: `-D clippy::inline-modules` implied by `-D warnings`
9+
= help: to override `-D warnings` add `#[allow(clippy::inline_modules)]`
10+
11+
error: inline module found
12+
--> src/qux/foo.rs:1:1
13+
|
14+
1 | mod bar {}
15+
| ^^^^^^^^^^
16+
|
17+
= help: move the contents of the module to `src/qux/foo/bar.rs` or `src/qux/foo/bar/mod.rs`, and replace this with `mod bar;`
18+
19+
error: inline module found
20+
--> src/lib.rs:9:1
21+
|
22+
9 | / pub mod test_nested_inline_mods {
23+
10 | | mod bar {
24+
11 | | mod baz {}
25+
12 | | }
26+
13 | | }
27+
| |_^
28+
|
29+
= help: move the contents of the module to `src/test_nested_inline_mods.rs` or `src/test_nested_inline_mods/mod.rs`, and replace this with `pub mod test_nested_inline_mods;`
30+
31+
error: inline module found
32+
--> src/lib.rs:10:5
33+
|
34+
10 | / mod bar {
35+
11 | | mod baz {}
36+
12 | | }
37+
| |_____^
38+
|
39+
= help: move the contents of the module to `src/test_nested_inline_mods/bar.rs` or `src/test_nested_inline_mods/bar/mod.rs`, and replace this with `mod bar;`
40+
41+
error: inline module found
42+
--> src/lib.rs:11:9
43+
|
44+
11 | mod baz {}
45+
| ^^^^^^^^^^
46+
|
47+
= help: move the contents of the module to `src/test_nested_inline_mods/bar/baz.rs` or `src/test_nested_inline_mods/bar/baz/mod.rs`, and replace this with `mod baz;`
48+
49+
error: inline module found
50+
--> src/lib.rs:20:1
51+
|
52+
20 | / mod partially_escaped_test_mod {
53+
21 | | #[cfg(test)]
54+
22 | | mod tests {
55+
23 | | mod bar {}
56+
24 | | }
57+
25 | | mod baz {}
58+
26 | | }
59+
| |_^
60+
|
61+
= help: move the contents of the module to `src/partially_escaped_test_mod.rs` or `src/partially_escaped_test_mod/mod.rs`, and replace this with `mod partially_escaped_test_mod;`
62+
63+
error: inline module found
64+
--> src/lib.rs:25:5
65+
|
66+
25 | mod baz {}
67+
| ^^^^^^^^^^
68+
|
69+
= help: move the contents of the module to `src/partially_escaped_test_mod/baz.rs` or `src/partially_escaped_test_mod/baz/mod.rs`, and replace this with `mod baz;`
70+
71+
error: could not compile `inline-mod` (lib) due to 7 previous errors
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "inline-mod"
3+
version = "0.1.0"
4+
edition = "2024"
5+
publish = false
6+
7+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8+
9+
[dependencies]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/// Lint shouldn't fire because parent mod has a path attribute.
2+
mod foo {}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#![warn(clippy::inline_modules)]
2+
3+
pub mod other;
4+
#[path = "qux/mod.rs"]
5+
pub mod something;
6+
#[path = "foo.rs"]
7+
pub mod stuff;
8+
9+
pub mod test_nested_inline_mods {
10+
mod bar {
11+
mod baz {}
12+
}
13+
}
14+
15+
#[cfg(test)]
16+
mod escaped_test_mod {
17+
mod bar {}
18+
}
19+
20+
mod partially_escaped_test_mod {
21+
#[cfg(test)]
22+
mod tests {
23+
mod bar {}
24+
}
25+
mod baz {}
26+
}
27+
28+
macro_rules! inline_mod_from_expansion {
29+
() => {
30+
mod _foo {}
31+
};
32+
}
33+
34+
inline_mod_from_expansion!();
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mod foo {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mod bar {}

0 commit comments

Comments
 (0)