539 lines
22 KiB
Rust
539 lines
22 KiB
Rust
use clippy_config::Conf;
|
|
use clippy_config::types::{
|
|
SourceItemOrderingCategory, SourceItemOrderingModuleItemGroupings, SourceItemOrderingModuleItemKind,
|
|
SourceItemOrderingTraitAssocItemKind, SourceItemOrderingTraitAssocItemKinds,
|
|
SourceItemOrderingWithinModuleItemGroupings,
|
|
};
|
|
use clippy_utils::diagnostics::span_lint_and_note;
|
|
use rustc_hir::{
|
|
AssocItemKind, FieldDef, HirId, ImplItemRef, IsAuto, Item, ItemKind, Mod, QPath, TraitItemRef, TyKind, Variant,
|
|
VariantData,
|
|
};
|
|
use rustc_lint::{LateContext, LateLintPass, LintContext};
|
|
use rustc_session::impl_lint_pass;
|
|
|
|
declare_clippy_lint! {
|
|
/// ### What it does
|
|
///
|
|
/// Confirms that items are sorted in source files as per configuration.
|
|
///
|
|
/// ### Why restrict this?
|
|
///
|
|
/// Keeping a consistent ordering throughout the codebase helps with working
|
|
/// as a team, and possibly improves maintainability of the codebase. The
|
|
/// idea is that by defining a consistent and enforceable rule for how
|
|
/// source files are structured, less time will be wasted during reviews on
|
|
/// a topic that is (under most circumstances) not relevant to the logic
|
|
/// implemented in the code. Sometimes this will be referred to as
|
|
/// "bikeshedding".
|
|
///
|
|
/// ### Default Ordering and Configuration
|
|
///
|
|
/// As there is no generally applicable rule, and each project may have
|
|
/// different requirements, the lint can be configured with high
|
|
/// granularity. The configuration is split into two stages:
|
|
///
|
|
/// 1. Which item kinds that should have an internal order enforced.
|
|
/// 2. Individual ordering rules per item kind.
|
|
///
|
|
/// The item kinds that can be linted are:
|
|
/// - Module (with customized groupings, alphabetical within - configurable)
|
|
/// - Trait (with customized order of associated items, alphabetical within)
|
|
/// - Enum, Impl, Struct (purely alphabetical)
|
|
///
|
|
/// #### Module Item Order
|
|
///
|
|
/// Due to the large variation of items within modules, the ordering can be
|
|
/// configured on a very granular level. Item kinds can be grouped together
|
|
/// arbitrarily, items within groups will be ordered alphabetically. The
|
|
/// following table shows the default groupings:
|
|
///
|
|
/// | Group | Item Kinds |
|
|
/// |--------------------|----------------------|
|
|
/// | `modules` | "mod", "foreign_mod" |
|
|
/// | `use` | "use" |
|
|
/// | `macros` | "macro" |
|
|
/// | `global_asm` | "global_asm" |
|
|
/// | `UPPER_SNAKE_CASE` | "static", "const" |
|
|
/// | `PascalCase` | "ty_alias", "opaque_ty", "enum", "struct", "union", "trait", "trait_alias", "impl" |
|
|
/// | `lower_snake_case` | "fn" |
|
|
///
|
|
/// The groups' names are arbitrary and can be changed to suit the
|
|
/// conventions that should be enforced for a specific project.
|
|
///
|
|
/// All item kinds must be accounted for to create an enforceable linting
|
|
/// rule set. Following are some example configurations that may be useful.
|
|
///
|
|
/// Example: *module inclusions and use statements to be at the top*
|
|
///
|
|
/// ```toml
|
|
/// module-item-order-groupings = [
|
|
/// [ "modules", [ "extern_crate", "mod", "foreign_mod" ], ],
|
|
/// [ "use", [ "use", ], ],
|
|
/// [ "everything_else", [ "macro", "global_asm", "static", "const", "ty_alias", "enum", "struct", "union", "trait", "trait_alias", "impl", "fn", ], ],
|
|
/// ]
|
|
/// ```
|
|
///
|
|
/// Example: *only consts and statics should be alphabetically ordered*
|
|
///
|
|
/// It is also possible to configure a selection of module item groups that
|
|
/// should be ordered alphabetically. This may be useful if for example
|
|
/// statics and consts should be ordered, but the rest should be left open.
|
|
///
|
|
/// ```toml
|
|
/// module-items-ordered-within-groupings = ["UPPER_SNAKE_CASE"]
|
|
/// ```
|
|
///
|
|
/// ### Known Problems
|
|
///
|
|
/// #### Performance Impact
|
|
///
|
|
/// Keep in mind, that ordering source code alphabetically can lead to
|
|
/// reduced performance in cases where the most commonly used enum variant
|
|
/// isn't the first entry anymore, and similar optimizations that can reduce
|
|
/// branch misses, cache locality and such. Either don't use this lint if
|
|
/// that's relevant, or disable the lint in modules or items specifically
|
|
/// where it matters. Other solutions can be to use profile guided
|
|
/// optimization (PGO), post-link optimization (e.g. using BOLT for LLVM),
|
|
/// or other advanced optimization methods. A good starting point to dig
|
|
/// into optimization is [cargo-pgo][cargo-pgo].
|
|
///
|
|
/// #### Lints on a Contains basis
|
|
///
|
|
/// The lint can be disabled only on a "contains" basis, but not per element
|
|
/// within a "container", e.g. the lint works per-module, per-struct,
|
|
/// per-enum, etc. but not for "don't order this particular enum variant".
|
|
///
|
|
/// #### Module documentation
|
|
///
|
|
/// Module level rustdoc comments are not part of the resulting syntax tree
|
|
/// and as such cannot be linted from within `check_mod`. Instead, the
|
|
/// `rustdoc::missing_documentation` lint may be used.
|
|
///
|
|
/// #### Module Tests
|
|
///
|
|
/// This lint does not implement detection of module tests (or other feature
|
|
/// dependent elements for that matter). To lint the location of mod tests,
|
|
/// the lint `items_after_test_module` can be used instead.
|
|
///
|
|
/// ### Example
|
|
///
|
|
/// ```no_run
|
|
/// trait TraitUnordered {
|
|
/// const A: bool;
|
|
/// const C: bool;
|
|
/// const B: bool;
|
|
///
|
|
/// type SomeType;
|
|
///
|
|
/// fn a();
|
|
/// fn c();
|
|
/// fn b();
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Use instead:
|
|
/// ```no_run
|
|
/// trait TraitOrdered {
|
|
/// const A: bool;
|
|
/// const B: bool;
|
|
/// const C: bool;
|
|
///
|
|
/// type SomeType;
|
|
///
|
|
/// fn a();
|
|
/// fn b();
|
|
/// fn c();
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// [cargo-pgo]: https://github.com/Kobzol/cargo-pgo/blob/main/README.md
|
|
///
|
|
#[clippy::version = "1.84.0"]
|
|
pub ARBITRARY_SOURCE_ITEM_ORDERING,
|
|
restriction,
|
|
"arbitrary source item ordering"
|
|
}
|
|
|
|
impl_lint_pass!(ArbitrarySourceItemOrdering => [ARBITRARY_SOURCE_ITEM_ORDERING]);
|
|
|
|
#[derive(Debug)]
|
|
#[allow(clippy::struct_excessive_bools)] // Bools are cached feature flags.
|
|
pub struct ArbitrarySourceItemOrdering {
|
|
assoc_types_order: SourceItemOrderingTraitAssocItemKinds,
|
|
enable_ordering_for_enum: bool,
|
|
enable_ordering_for_impl: bool,
|
|
enable_ordering_for_module: bool,
|
|
enable_ordering_for_struct: bool,
|
|
enable_ordering_for_trait: bool,
|
|
module_item_order_groupings: SourceItemOrderingModuleItemGroupings,
|
|
module_items_ordered_within_groupings: SourceItemOrderingWithinModuleItemGroupings,
|
|
}
|
|
|
|
impl ArbitrarySourceItemOrdering {
|
|
pub fn new(conf: &'static Conf) -> Self {
|
|
#[allow(clippy::enum_glob_use)] // Very local glob use for legibility.
|
|
use SourceItemOrderingCategory::*;
|
|
Self {
|
|
assoc_types_order: conf.trait_assoc_item_kinds_order.clone(),
|
|
enable_ordering_for_enum: conf.source_item_ordering.contains(&Enum),
|
|
enable_ordering_for_impl: conf.source_item_ordering.contains(&Impl),
|
|
enable_ordering_for_module: conf.source_item_ordering.contains(&Module),
|
|
enable_ordering_for_struct: conf.source_item_ordering.contains(&Struct),
|
|
enable_ordering_for_trait: conf.source_item_ordering.contains(&Trait),
|
|
module_item_order_groupings: conf.module_item_order_groupings.clone(),
|
|
module_items_ordered_within_groupings: conf.module_items_ordered_within_groupings.clone(),
|
|
}
|
|
}
|
|
|
|
/// Produces a linting warning for incorrectly ordered impl items.
|
|
fn lint_impl_item<T: LintContext>(&self, cx: &T, item: &ImplItemRef, before_item: &ImplItemRef) {
|
|
span_lint_and_note(
|
|
cx,
|
|
ARBITRARY_SOURCE_ITEM_ORDERING,
|
|
item.span,
|
|
format!(
|
|
"incorrect ordering of impl items (defined order: {:?})",
|
|
self.assoc_types_order
|
|
),
|
|
Some(before_item.span),
|
|
format!("should be placed before `{}`", before_item.ident.as_str(),),
|
|
);
|
|
}
|
|
|
|
/// Produces a linting warning for incorrectly ordered item members.
|
|
fn lint_member_name<T: LintContext>(cx: &T, ident: &rustc_span::Ident, before_ident: &rustc_span::Ident) {
|
|
span_lint_and_note(
|
|
cx,
|
|
ARBITRARY_SOURCE_ITEM_ORDERING,
|
|
ident.span,
|
|
"incorrect ordering of items (must be alphabetically ordered)",
|
|
Some(before_ident.span),
|
|
format!("should be placed before `{}`", before_ident.as_str(),),
|
|
);
|
|
}
|
|
|
|
fn lint_member_item<T: LintContext>(cx: &T, item: &Item<'_>, before_item: &Item<'_>, msg: &'static str) {
|
|
let span = if let Some(ident) = item.kind.ident() {
|
|
ident.span
|
|
} else {
|
|
item.span
|
|
};
|
|
|
|
let (before_span, note) = if let Some(ident) = before_item.kind.ident() {
|
|
(ident.span, format!("should be placed before `{}`", ident.as_str(),))
|
|
} else {
|
|
(
|
|
before_item.span,
|
|
"should be placed before the following item".to_owned(),
|
|
)
|
|
};
|
|
|
|
// This catches false positives where generated code gets linted.
|
|
if span == before_span {
|
|
return;
|
|
}
|
|
|
|
span_lint_and_note(cx, ARBITRARY_SOURCE_ITEM_ORDERING, span, msg, Some(before_span), note);
|
|
}
|
|
|
|
/// Produces a linting warning for incorrectly ordered trait items.
|
|
fn lint_trait_item<T: LintContext>(&self, cx: &T, item: &TraitItemRef, before_item: &TraitItemRef) {
|
|
span_lint_and_note(
|
|
cx,
|
|
ARBITRARY_SOURCE_ITEM_ORDERING,
|
|
item.span,
|
|
format!(
|
|
"incorrect ordering of trait items (defined order: {:?})",
|
|
self.assoc_types_order
|
|
),
|
|
Some(before_item.span),
|
|
format!("should be placed before `{}`", before_item.ident.as_str(),),
|
|
);
|
|
}
|
|
}
|
|
|
|
impl<'tcx> LateLintPass<'tcx> for ArbitrarySourceItemOrdering {
|
|
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
|
|
match &item.kind {
|
|
ItemKind::Enum(_, enum_def, _generics) if self.enable_ordering_for_enum => {
|
|
let mut cur_v: Option<&Variant<'_>> = None;
|
|
for variant in enum_def.variants {
|
|
if variant.span.in_external_macro(cx.sess().source_map()) {
|
|
continue;
|
|
}
|
|
|
|
if let Some(cur_v) = cur_v {
|
|
if cur_v.ident.name.as_str() > variant.ident.name.as_str() && cur_v.span != variant.span {
|
|
Self::lint_member_name(cx, &variant.ident, &cur_v.ident);
|
|
}
|
|
}
|
|
cur_v = Some(variant);
|
|
}
|
|
},
|
|
ItemKind::Struct(_, VariantData::Struct { fields, .. }, _generics) if self.enable_ordering_for_struct => {
|
|
let mut cur_f: Option<&FieldDef<'_>> = None;
|
|
for field in *fields {
|
|
if field.span.in_external_macro(cx.sess().source_map()) {
|
|
continue;
|
|
}
|
|
|
|
if let Some(cur_f) = cur_f {
|
|
if cur_f.ident.name.as_str() > field.ident.name.as_str() && cur_f.span != field.span {
|
|
Self::lint_member_name(cx, &field.ident, &cur_f.ident);
|
|
}
|
|
}
|
|
cur_f = Some(field);
|
|
}
|
|
},
|
|
ItemKind::Trait(is_auto, _safety, _ident, _generics, _generic_bounds, item_ref)
|
|
if self.enable_ordering_for_trait && *is_auto == IsAuto::No =>
|
|
{
|
|
let mut cur_t: Option<&TraitItemRef> = None;
|
|
|
|
for item in *item_ref {
|
|
if item.span.in_external_macro(cx.sess().source_map()) {
|
|
continue;
|
|
}
|
|
|
|
if let Some(cur_t) = cur_t {
|
|
let cur_t_kind = convert_assoc_item_kind(cur_t.kind);
|
|
let cur_t_kind_index = self.assoc_types_order.index_of(&cur_t_kind);
|
|
let item_kind = convert_assoc_item_kind(item.kind);
|
|
let item_kind_index = self.assoc_types_order.index_of(&item_kind);
|
|
|
|
if cur_t_kind == item_kind && cur_t.ident.name.as_str() > item.ident.name.as_str() {
|
|
Self::lint_member_name(cx, &item.ident, &cur_t.ident);
|
|
} else if cur_t_kind_index > item_kind_index {
|
|
self.lint_trait_item(cx, item, cur_t);
|
|
}
|
|
}
|
|
cur_t = Some(item);
|
|
}
|
|
},
|
|
ItemKind::Impl(trait_impl) if self.enable_ordering_for_impl => {
|
|
let mut cur_t: Option<&ImplItemRef> = None;
|
|
|
|
for item in trait_impl.items {
|
|
if item.span.in_external_macro(cx.sess().source_map()) {
|
|
continue;
|
|
}
|
|
|
|
if let Some(cur_t) = cur_t {
|
|
let cur_t_kind = convert_assoc_item_kind(cur_t.kind);
|
|
let cur_t_kind_index = self.assoc_types_order.index_of(&cur_t_kind);
|
|
let item_kind = convert_assoc_item_kind(item.kind);
|
|
let item_kind_index = self.assoc_types_order.index_of(&item_kind);
|
|
|
|
if cur_t_kind == item_kind && cur_t.ident.name.as_str() > item.ident.name.as_str() {
|
|
Self::lint_member_name(cx, &item.ident, &cur_t.ident);
|
|
} else if cur_t_kind_index > item_kind_index {
|
|
self.lint_impl_item(cx, item, cur_t);
|
|
}
|
|
}
|
|
cur_t = Some(item);
|
|
}
|
|
},
|
|
_ => {}, // Catch-all for `ItemKinds` that don't have fields.
|
|
}
|
|
}
|
|
|
|
fn check_mod(&mut self, cx: &LateContext<'tcx>, module: &'tcx Mod<'tcx>, _: HirId) {
|
|
struct CurItem<'a> {
|
|
item: &'a Item<'a>,
|
|
order: usize,
|
|
name: String,
|
|
}
|
|
let mut cur_t: Option<CurItem<'_>> = None;
|
|
|
|
if !self.enable_ordering_for_module {
|
|
return;
|
|
}
|
|
|
|
let items = module.item_ids.iter().map(|&id| cx.tcx.hir_item(id));
|
|
|
|
// Iterates over the items within a module.
|
|
//
|
|
// As of 2023-05-09, the Rust compiler will hold the entries in the same
|
|
// order as they appear in the source code, which is convenient for us,
|
|
// as no sorting by source map/line of code has to be applied.
|
|
//
|
|
for item in items {
|
|
if item.span.in_external_macro(cx.sess().source_map()) {
|
|
continue;
|
|
}
|
|
|
|
let ident = if let Some(ident) = item.kind.ident() {
|
|
ident
|
|
} else if let ItemKind::Impl(_) = item.kind
|
|
&& !get_item_name(item).is_empty()
|
|
{
|
|
rustc_span::Ident::empty() // FIXME: a bit strange, is there a better way to do it?
|
|
} else {
|
|
continue;
|
|
};
|
|
|
|
if ident.name.as_str().starts_with('_') {
|
|
// Filters out unnamed macro-like impls for various derives,
|
|
// e.g. serde::Serialize or num_derive::FromPrimitive.
|
|
continue;
|
|
}
|
|
|
|
if ident.name == rustc_span::sym::std && item.span.is_dummy() {
|
|
if let ItemKind::ExternCrate(None, _) = item.kind {
|
|
// Filters the auto-included Rust standard library.
|
|
continue;
|
|
}
|
|
println!("Unknown item: {item:?}");
|
|
}
|
|
|
|
let item_kind = convert_module_item_kind(&item.kind);
|
|
let grouping_name = self.module_item_order_groupings.grouping_name_of(&item_kind);
|
|
let module_level_order = self
|
|
.module_item_order_groupings
|
|
.module_level_order_of(&item_kind)
|
|
.unwrap_or_default();
|
|
|
|
if let Some(cur_t) = cur_t.as_ref() {
|
|
use std::cmp::Ordering; // Better legibility.
|
|
match module_level_order.cmp(&cur_t.order) {
|
|
Ordering::Less => {
|
|
Self::lint_member_item(
|
|
cx,
|
|
item,
|
|
cur_t.item,
|
|
"incorrect ordering of items (module item groupings specify another order)",
|
|
);
|
|
},
|
|
Ordering::Equal if item_kind == SourceItemOrderingModuleItemKind::Use => {
|
|
// Skip ordering use statements, as these should be ordered by rustfmt.
|
|
},
|
|
Ordering::Equal
|
|
if (grouping_name.is_some_and(|grouping_name| {
|
|
self.module_items_ordered_within_groupings.ordered_within(grouping_name)
|
|
}) && cur_t.name > get_item_name(item)) =>
|
|
{
|
|
Self::lint_member_item(
|
|
cx,
|
|
item,
|
|
cur_t.item,
|
|
"incorrect ordering of items (must be alphabetically ordered)",
|
|
);
|
|
},
|
|
Ordering::Equal | Ordering::Greater => {
|
|
// Nothing to do in this case, they're already in the right order.
|
|
},
|
|
}
|
|
}
|
|
|
|
// Makes a note of the current item for comparison with the next.
|
|
cur_t = Some(CurItem {
|
|
item,
|
|
order: module_level_order,
|
|
name: get_item_name(item),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Converts a [`rustc_hir::AssocItemKind`] to a
|
|
/// [`SourceItemOrderingTraitAssocItemKind`].
|
|
///
|
|
/// This is implemented here because `rustc_hir` is not a dependency of
|
|
/// `clippy_config`.
|
|
fn convert_assoc_item_kind(value: AssocItemKind) -> SourceItemOrderingTraitAssocItemKind {
|
|
#[allow(clippy::enum_glob_use)] // Very local glob use for legibility.
|
|
use SourceItemOrderingTraitAssocItemKind::*;
|
|
match value {
|
|
AssocItemKind::Const => Const,
|
|
AssocItemKind::Type => Type,
|
|
AssocItemKind::Fn { .. } => Fn,
|
|
}
|
|
}
|
|
|
|
/// Converts a [`rustc_hir::ItemKind`] to a
|
|
/// [`SourceItemOrderingModuleItemKind`].
|
|
///
|
|
/// This is implemented here because `rustc_hir` is not a dependency of
|
|
/// `clippy_config`.
|
|
fn convert_module_item_kind(value: &ItemKind<'_>) -> SourceItemOrderingModuleItemKind {
|
|
#[allow(clippy::enum_glob_use)] // Very local glob use for legibility.
|
|
use SourceItemOrderingModuleItemKind::*;
|
|
match value {
|
|
ItemKind::ExternCrate(..) => ExternCrate,
|
|
ItemKind::Use(..) => Use,
|
|
ItemKind::Static(..) => Static,
|
|
ItemKind::Const(..) => Const,
|
|
ItemKind::Fn { .. } => Fn,
|
|
ItemKind::Macro(..) => Macro,
|
|
ItemKind::Mod(..) => Mod,
|
|
ItemKind::ForeignMod { .. } => ForeignMod,
|
|
ItemKind::GlobalAsm { .. } => GlobalAsm,
|
|
ItemKind::TyAlias(..) => TyAlias,
|
|
ItemKind::Enum(..) => Enum,
|
|
ItemKind::Struct(..) => Struct,
|
|
ItemKind::Union(..) => Union,
|
|
ItemKind::Trait(..) => Trait,
|
|
ItemKind::TraitAlias(..) => TraitAlias,
|
|
ItemKind::Impl(..) => Impl,
|
|
}
|
|
}
|
|
|
|
/// Gets the item name for sorting purposes, which in the general case is
|
|
/// `item.ident.name`.
|
|
///
|
|
/// For trait impls, the name used for sorting will be the written path of
|
|
/// `item.self_ty` plus the written path of `item.of_trait`, joined with
|
|
/// exclamation marks. Exclamation marks are used because they are the first
|
|
/// printable ASCII character.
|
|
///
|
|
/// Trait impls generated using a derive-macro will have their path rewritten,
|
|
/// such that for example `Default` is `$crate::default::Default`, and
|
|
/// `std::clone::Clone` is `$crate::clone::Clone`. This behaviour is described
|
|
/// further in the [Rust Reference, Paths Chapter][rust_ref].
|
|
///
|
|
/// [rust_ref]: https://doc.rust-lang.org/reference/paths.html#crate-1
|
|
fn get_item_name(item: &Item<'_>) -> String {
|
|
match item.kind {
|
|
ItemKind::Impl(im) => {
|
|
if let TyKind::Path(path) = im.self_ty.kind {
|
|
match path {
|
|
QPath::Resolved(_, path) => {
|
|
let segs = path.segments.iter();
|
|
let mut segs: Vec<String> = segs.map(|s| s.ident.name.as_str().to_owned()).collect();
|
|
|
|
if let Some(of_trait) = im.of_trait {
|
|
let mut trait_segs: Vec<String> = of_trait
|
|
.path
|
|
.segments
|
|
.iter()
|
|
.map(|s| s.ident.name.as_str().to_owned())
|
|
.collect();
|
|
segs.append(&mut trait_segs);
|
|
}
|
|
|
|
segs.push(String::new());
|
|
segs.join("!!")
|
|
},
|
|
QPath::TypeRelative(_, _path_seg) => {
|
|
// This case doesn't exist in the clippy tests codebase.
|
|
String::new()
|
|
},
|
|
QPath::LangItem(_, _) => String::new(),
|
|
}
|
|
} else {
|
|
// Impls for anything that isn't a named type can be skipped.
|
|
String::new()
|
|
}
|
|
},
|
|
// FIXME: `Ident::empty` for anonymous items is a bit strange, is there
|
|
// a better way to do it?
|
|
_ => item
|
|
.kind
|
|
.ident()
|
|
.unwrap_or(rustc_span::Ident::empty())
|
|
.name
|
|
.as_str()
|
|
.to_owned(),
|
|
}
|
|
}
|