| use clippy_utils::diagnostics::span_lint_and_then; | |
| use rustc_ast::ast::{self, Inline, ItemKind, ModKind}; | |
| use rustc_lint::{EarlyContext, EarlyLintPass, Level, LintContext}; | |
| use rustc_session::impl_lint_pass; | |
| use rustc_span::def_id::LOCAL_CRATE; | |
| use rustc_span::{FileName, SourceFile, Span, SyntaxContext, sym}; | |
| use std::path::{Component, Path, PathBuf}; | |
| use std::sync::Arc; | |
| declare_clippy_lint! { | |
| /// ### What it does | |
| /// Checks that module layout uses only self named module files; bans `mod.rs` files. | |
| /// | |
| /// ### Why restrict this? | |
| /// Having multiple module layout styles in a project can be confusing. | |
| /// | |
| /// ### Example | |
| /// ```text | |
| /// src/ | |
| /// stuff/ | |
| /// stuff_files.rs | |
| /// mod.rs | |
| /// lib.rs | |
| /// ``` | |
| /// Use instead: | |
| /// ```text | |
| /// src/ | |
| /// stuff/ | |
| /// stuff_files.rs | |
| /// stuff.rs | |
| /// lib.rs | |
| /// ``` | |
| #[clippy::version = "1.57.0"] | |
| pub MOD_MODULE_FILES, | |
| restriction, | |
| "checks that module layout is consistent" | |
| } | |
| declare_clippy_lint! { | |
| /// ### What it does | |
| /// Checks that module layout uses only `mod.rs` files. | |
| /// | |
| /// ### Why restrict this? | |
| /// Having multiple module layout styles in a project can be confusing. | |
| /// | |
| /// ### Example | |
| /// ```text | |
| /// src/ | |
| /// stuff/ | |
| /// stuff_files.rs | |
| /// stuff.rs | |
| /// lib.rs | |
| /// ``` | |
| /// Use instead: | |
| /// ```text | |
| /// src/ | |
| /// stuff/ | |
| /// stuff_files.rs | |
| /// mod.rs | |
| /// lib.rs | |
| /// ``` | |
| #[clippy::version = "1.57.0"] | |
| pub SELF_NAMED_MODULE_FILES, | |
| restriction, | |
| "checks that module layout is consistent" | |
| } | |
| impl_lint_pass!(ModStyle => [MOD_MODULE_FILES, SELF_NAMED_MODULE_FILES]); | |
| pub struct ModState { | |
| contains_external: bool, | |
| has_path_attr: bool, | |
| mod_file: Arc<SourceFile>, | |
| } | |
| #[derive(Default)] | |
| pub struct ModStyle { | |
| working_dir: Option<PathBuf>, | |
| module_stack: Vec<ModState>, | |
| } | |
| impl EarlyLintPass for ModStyle { | |
| fn check_crate(&mut self, cx: &EarlyContext<'_>, _: &ast::Crate) { | |
| self.working_dir = cx.sess().source_map().working_dir().local_path().map(Path::to_path_buf); | |
| } | |
| fn check_item(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) { | |
| if cx.builder.lint_level(MOD_MODULE_FILES).level == Level::Allow | |
| && cx.builder.lint_level(SELF_NAMED_MODULE_FILES).level == Level::Allow | |
| { | |
| return; | |
| } | |
| if let ItemKind::Mod(.., ModKind::Loaded(_, Inline::No { .. }, mod_spans, ..)) = &item.kind { | |
| let has_path_attr = item.attrs.iter().any(|attr| attr.has_name(sym::path)); | |
| if !has_path_attr && let Some(current) = self.module_stack.last_mut() { | |
| current.contains_external = true; | |
| } | |
| let mod_file = cx.sess().source_map().lookup_source_file(mod_spans.inner_span.lo()); | |
| self.module_stack.push(ModState { | |
| contains_external: false, | |
| has_path_attr, | |
| mod_file, | |
| }); | |
| } | |
| } | |
| fn check_item_post(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) { | |
| if cx.builder.lint_level(MOD_MODULE_FILES).level == Level::Allow | |
| && cx.builder.lint_level(SELF_NAMED_MODULE_FILES).level == Level::Allow | |
| { | |
| return; | |
| } | |
| if let ItemKind::Mod(.., ModKind::Loaded(_, Inline::No { .. }, ..)) = &item.kind | |
| && let Some(current) = self.module_stack.pop() | |
| && !current.has_path_attr | |
| { | |
| let Some(path) = self | |
| .working_dir | |
| .as_ref() | |
| .and_then(|src| try_trim_file_path_prefix(¤t.mod_file, src)) | |
| else { | |
| return; | |
| }; | |
| if current.contains_external { | |
| check_self_named_module(cx, path, ¤t.mod_file); | |
| } | |
| check_mod_module(cx, path, ¤t.mod_file); | |
| } | |
| } | |
| } | |
| fn check_self_named_module(cx: &EarlyContext<'_>, path: &Path, file: &SourceFile) { | |
| if !path.ends_with("mod.rs") { | |
| let mut mod_folder = path.with_extension(""); | |
| span_lint_and_then( | |
| cx, | |
| SELF_NAMED_MODULE_FILES, | |
| Span::new(file.start_pos, file.start_pos, SyntaxContext::root(), None), | |
| format!("`mod.rs` files are required, found `{}`", path.display()), | |
| |diag| { | |
| mod_folder.push("mod.rs"); | |
| diag.help(format!("move `{}` to `{}`", path.display(), mod_folder.display())); | |
| }, | |
| ); | |
| } | |
| } | |
| /// We should not emit a lint for test modules in the presence of `mod.rs`. | |
| /// 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) | |
| /// for code-sharing between tests. | |
| fn check_mod_module(cx: &EarlyContext<'_>, path: &Path, file: &SourceFile) { | |
| if path.ends_with("mod.rs") | |
| && !path | |
| .components() | |
| .filter_map(|c| if let Component::Normal(d) = c { Some(d) } else { None }) | |
| .take_while(|&c| c != "src") | |
| .any(|c| c == "tests") | |
| { | |
| span_lint_and_then( | |
| cx, | |
| MOD_MODULE_FILES, | |
| Span::new(file.start_pos, file.start_pos, SyntaxContext::root(), None), | |
| format!("`mod.rs` files are not allowed, found `{}`", path.display()), | |
| |diag| { | |
| let mut mod_file = path.to_path_buf(); | |
| mod_file.pop(); | |
| mod_file.set_extension("rs"); | |
| diag.help(format!("move `{}` to `{}`", path.display(), mod_file.display())); | |
| }, | |
| ); | |
| } | |
| } | |
| fn try_trim_file_path_prefix<'a>(file: &'a SourceFile, prefix: &'a Path) -> Option<&'a Path> { | |
| if let FileName::Real(name) = &file.name | |
| && let Some(mut path) = name.local_path() | |
| && file.cnum == LOCAL_CRATE | |
| { | |
| if !path.is_relative() { | |
| path = path.strip_prefix(prefix).ok()?; | |
| } | |
| Some(path) | |
| } else { | |
| None | |
| } | |
| } |