use clippy_utils::diagnostics::span_lint_hir_and_then;
use rustc_ast::visit::{self, Visitor};
use rustc_ast::{Block, Item, ItemKind, ModKind, Stmt, StmtKind};
use rustc_errors::Applicability;
use rustc_hir::HirId;
use rustc_lint::{LateContext, LateLintPass, LintStore};
use rustc_session::{declare_tool_lint, impl_lint_pass};
use rustc_span::Span;
use crate::common::{DefaultState, resolved_state};
use crate::enclosing_hir::find_enclosing_hir_ids;
use crate::module_reparse::for_each_module_file;
mod combined;
mod forbid;
mod render;
declare_tool_lint! {
#[cfg_attr(
dylint_lib = "perfectionist",
expect(
perfectionist::bare_identifier_reference,
reason = "the style names `forbid` / `combined` resolve to this rule's \
submodules, but this rustdoc is rendered to the docs site where \
intra-doc links don't apply"
)
)]
pub perfectionist::SELF_IMPORT,
Warn,
"module imported through `self` against the project's configured `self`-import style",
report_in_external_macro: false
}
const CONFIG_KEY: &str = "perfectionist::self_import";
pub(crate) const DEFAULT_STATE: DefaultState = DefaultState::Inactive;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
enum Style {
Forbid,
Combined,
}
#[derive(Debug, serde::Deserialize)]
#[serde(deny_unknown_fields, rename_all = "snake_case")]
struct Config {
#[cfg_attr(
dylint_lib = "perfectionist",
expect(
perfectionist::bare_identifier_reference,
reason = "the style names `forbid` / `combined` resolve to this rule's \
submodules, but this field doc is rendered to the docs site where \
intra-doc links don't apply"
)
)]
style: Style,
}
pub struct SelfImport {
style: Style,
}
impl_lint_pass!(SelfImport => [SELF_IMPORT]);
pub fn register_lint(lint_store: &mut LintStore) {
lint_store.register_lints(&[SELF_IMPORT]);
}
pub fn register_pass(lint_store: &mut LintStore) {
if let DefaultState::Inactive = resolved_state("self_import", DEFAULT_STATE) {
return;
}
let config = dylint_linting::config::<Config>(CONFIG_KEY)
.unwrap_or_else(|error| {
panic!(
"perfectionist::self_import: invalid `[perfectionist::self_import]` \
configuration: {error}",
)
})
.unwrap_or_else(|| {
panic!(
"perfectionist::self_import is enabled but `style` is not set; add \
`style = \"forbid\"` or `style = \"combined\"` under \
`[perfectionist::self_import]` in dylint.toml",
)
});
lint_store.register_late_pass(move |_| {
Box::new(SelfImport {
style: config.style,
})
});
}
pub(super) struct Pending {
pub(super) anchor: Span,
pub(super) span: Span,
pub(super) message: &'static str,
pub(super) fix: Fix,
}
pub(super) enum Fix {
Replace {
label: &'static str,
replacement: String,
note: Option<&'static str>,
},
Multipart {
label: &'static str,
parts: Vec<(Span, String)>,
applicability: Applicability,
},
}
impl<'tcx> LateLintPass<'tcx> for SelfImport {
fn check_crate(&mut self, cx: &LateContext<'tcx>) {
let style = self.style;
let mut violations: Vec<Pending> = Vec::new();
for_each_module_file(cx, |krate| {
let mut walker = SelfImportWalker {
cx,
style,
violations: &mut violations,
};
walker.scan_items(krate.items.iter().map(|item| Some(&**item)));
visit::walk_crate(&mut walker, krate);
});
if violations.is_empty() {
return;
}
let anchors: Vec<Span> = violations.iter().map(|pending| pending.anchor).collect();
let hir_ids = find_enclosing_hir_ids(cx.tcx, &anchors);
for (pending, hir_id) in violations.into_iter().zip(hir_ids) {
emit_pending(cx, hir_id, pending);
}
}
}
fn emit_pending(cx: &LateContext<'_>, hir_id: HirId, pending: Pending) {
let Pending {
span, message, fix, ..
} = pending;
span_lint_hir_and_then(
cx,
SELF_IMPORT,
hir_id,
span,
message,
|diagnostic| match fix {
Fix::Replace {
label,
replacement,
note,
} => {
if let Some(note) = note {
diagnostic.note(note);
}
diagnostic.span_suggestion(span, label, replacement, Applicability::MaybeIncorrect);
}
Fix::Multipart {
label,
parts,
applicability,
} => {
diagnostic.multipart_suggestion(label, parts, applicability);
}
},
);
}
struct SelfImportWalker<'a, 'b, 'tcx> {
cx: &'a LateContext<'tcx>,
style: Style,
violations: &'b mut Vec<Pending>,
}
impl SelfImportWalker<'_, '_, '_> {
fn scan_items<'ast>(&mut self, entries: impl Iterator<Item = Option<&'ast Item>> + Clone) {
if let Style::Combined = self.style {
combined::scan(self.cx, entries.clone(), self.violations);
}
if let Style::Forbid = self.style {
for item in entries.flatten() {
if let ItemKind::Use(tree) = &item.kind
&& !item.span.from_expansion()
{
forbid::check_use_item(self.cx, item, tree, self.violations);
}
}
}
}
}
impl<'ast> Visitor<'ast> for SelfImportWalker<'_, '_, '_> {
fn visit_item(&mut self, item: &'ast Item) {
if let ItemKind::Mod(_, _, ModKind::Loaded(items, ..)) = &item.kind {
self.scan_items(items.iter().map(|item| Some(&**item)));
}
visit::walk_item(self, item);
}
fn visit_block(&mut self, block: &'ast Block) {
self.scan_items(block.stmts.iter().map(stmt_item));
visit::walk_block(self, block);
}
}
fn stmt_item(stmt: &Stmt) -> Option<&Item> {
match &stmt.kind {
StmtKind::Item(item) => Some(item),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn style_values_deserialize() {
assert_eq!(
toml::from_str::<Config>(r#"style = "forbid""#)
.unwrap()
.style,
Style::Forbid,
);
assert_eq!(
toml::from_str::<Config>(r#"style = "combined""#)
.unwrap()
.style,
Style::Combined,
);
}
#[test]
fn missing_style_is_an_error() {
assert!(toml::from_str::<Config>("").is_err());
}
#[test]
fn unknown_style_is_rejected() {
assert!(toml::from_str::<Config>(r#"style = "preserve""#).is_err());
}
}