Skip to content

Commit 5b2f954

Browse files
committed
Make alphabetic ordering in module item groups configurable (new default: off)
From feedback to this lint after its inclusion in clippy 1.82, this has turned out to be the most requested improvement. With this improvement, it is possible to make the lint check certain top-level structural checks on modules (e.g. use statements and module inclusions at the top), but still leaving everything else up to the developer.
1 parent 8d0c0eb commit 5b2f954

20 files changed

+765
-114
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6308,6 +6308,7 @@ Released 2018-09-13
63086308
[`min-ident-chars-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#min-ident-chars-threshold
63096309
[`missing-docs-in-crate-items`]: https://doc.rust-lang.org/clippy/lint_configuration.html#missing-docs-in-crate-items
63106310
[`module-item-order-groupings`]: https://doc.rust-lang.org/clippy/lint_configuration.html#module-item-order-groupings
6311+
[`module-items-ordered-within-groupings`]: https://doc.rust-lang.org/clippy/lint_configuration.html#module-items-ordered-within-groupings
63116312
[`msrv`]: https://doc.rust-lang.org/clippy/lint_configuration.html#msrv
63126313
[`pass-by-value-size-limit`]: https://doc.rust-lang.org/clippy/lint_configuration.html#pass-by-value-size-limit
63136314
[`pub-underscore-fields-behavior`]: https://doc.rust-lang.org/clippy/lint_configuration.html#pub-underscore-fields-behavior

book/src/lint_configuration.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,21 @@ The named groupings of different source item kinds within modules.
713713
* [`arbitrary_source_item_ordering`](https://rust-lang.github.io/rust-clippy/master/index.html#arbitrary_source_item_ordering)
714714

715715

716+
## `module-items-ordered-within-groupings`
717+
Whether the items within module groups should be ordered alphabetically or not.
718+
719+
This option can be configured to "all", "none", or a list of specific grouping names that should be checked
720+
(e.g. only "enums").
721+
722+
Unknown grouping names will simply be ignored.
723+
724+
**Default Value:** `"none"`
725+
726+
---
727+
**Affected lints:**
728+
* [`arbitrary_source_item_ordering`](https://rust-lang.github.io/rust-clippy/master/index.html#arbitrary_source_item_ordering)
729+
730+
716731
## `msrv`
717732
The minimum rust version that the project supports. Defaults to the `rust-version` field in `Cargo.toml`
718733

clippy_config/src/conf.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::types::{
33
DisallowedPath, MacroMatcher, MatchLintBehaviour, PubUnderscoreFieldsBehaviour, Rename, SourceItemOrdering,
44
SourceItemOrderingCategory, SourceItemOrderingModuleItemGroupings, SourceItemOrderingModuleItemKind,
55
SourceItemOrderingTraitAssocItemKind, SourceItemOrderingTraitAssocItemKinds,
6+
SourceItemOrderingWithinModuleItemGroupings,
67
};
78
use clippy_utils::msrvs::Msrv;
89
use rustc_errors::Applicability;
@@ -586,6 +587,15 @@ define_Conf! {
586587
/// The named groupings of different source item kinds within modules.
587588
#[lints(arbitrary_source_item_ordering)]
588589
module_item_order_groupings: SourceItemOrderingModuleItemGroupings = DEFAULT_MODULE_ITEM_ORDERING_GROUPS.into(),
590+
/// Whether the items within module groups should be ordered alphabetically or not.
591+
///
592+
/// This option can be configured to "all", "none", or a list of specific grouping names that should be checked
593+
/// (e.g. only "enums").
594+
///
595+
/// Unknown grouping names will simply be ignored.
596+
#[lints(arbitrary_source_item_ordering)]
597+
module_items_ordered_within_groupings: SourceItemOrderingWithinModuleItemGroupings =
598+
SourceItemOrderingWithinModuleItemGroupings::None,
589599
/// The minimum rust version that the project supports. Defaults to the `rust-version` field in `Cargo.toml`
590600
#[default_text = "current version"]
591601
#[lints(

clippy_config/src/types.rs

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ impl SourceItemOrderingModuleItemKind {
241241
pub struct SourceItemOrderingModuleItemGroupings {
242242
groups: Vec<(String, Vec<SourceItemOrderingModuleItemKind>)>,
243243
lut: HashMap<SourceItemOrderingModuleItemKind, usize>,
244+
back_lut: HashMap<SourceItemOrderingModuleItemKind, String>,
244245
}
245246

246247
impl SourceItemOrderingModuleItemGroupings {
@@ -256,6 +257,22 @@ impl SourceItemOrderingModuleItemGroupings {
256257
lut
257258
}
258259

260+
fn build_back_lut(
261+
groups: &[(String, Vec<SourceItemOrderingModuleItemKind>)],
262+
) -> HashMap<SourceItemOrderingModuleItemKind, String> {
263+
let mut lut = HashMap::new();
264+
for (group_name, items) in groups {
265+
for item in items {
266+
lut.insert(item.clone(), group_name.clone());
267+
}
268+
}
269+
lut
270+
}
271+
272+
pub fn grouping_name_of(&self, item: &SourceItemOrderingModuleItemKind) -> Option<String> {
273+
self.back_lut.get(item).cloned()
274+
}
275+
259276
pub fn module_level_order_of(&self, item: &SourceItemOrderingModuleItemKind) -> Option<usize> {
260277
self.lut.get(item).copied()
261278
}
@@ -266,7 +283,8 @@ impl From<&[(&str, &[SourceItemOrderingModuleItemKind])]> for SourceItemOrdering
266283
let groups: Vec<(String, Vec<SourceItemOrderingModuleItemKind>)> =
267284
value.iter().map(|item| (item.0.to_string(), item.1.to_vec())).collect();
268285
let lut = Self::build_lut(&groups);
269-
Self { groups, lut }
286+
let back_lut = Self::build_back_lut(&groups);
287+
Self { groups, lut, back_lut }
270288
}
271289
}
272290

@@ -284,6 +302,7 @@ impl<'de> Deserialize<'de> for SourceItemOrderingModuleItemGroupings {
284302
let groups = Vec::<(String, Vec<SourceItemOrderingModuleItemKind>)>::deserialize(deserializer)?;
285303
let items_total: usize = groups.iter().map(|(_, v)| v.len()).sum();
286304
let lut = Self::build_lut(&groups);
305+
let back_lut = Self::build_back_lut(&groups);
287306

288307
let mut expected_items = SourceItemOrderingModuleItemKind::all_variants();
289308
for item in lut.keys() {
@@ -306,7 +325,7 @@ impl<'de> Deserialize<'de> for SourceItemOrderingModuleItemGroupings {
306325
));
307326
}
308327

309-
Ok(Self { groups, lut })
328+
Ok(Self { groups, lut, back_lut })
310329
} else if items_total != all_items.len() {
311330
Err(de::Error::custom(format!(
312331
"Some module item kinds were configured more than once, or were missing, in the source ordering configuration. \
@@ -418,6 +437,83 @@ impl Serialize for SourceItemOrderingTraitAssocItemKinds {
418437
}
419438
}
420439

440+
/// Describes which specific groupings should have their items ordered
441+
/// alphabetically.
442+
///
443+
/// This is separate from defining and enforcing groupings. For example,
444+
/// defining enums are grouped before structs still allows for an enum B to be
445+
/// placed before an enum A. Only when enforcing ordering within the grouping,
446+
/// will it be checked if A is placed before B.
447+
#[derive(Clone, Debug)]
448+
pub enum SourceItemOrderingWithinModuleItemGroupings {
449+
/// All groupings should have their items ordered.
450+
All,
451+
452+
/// None of the groupings should have their order checked.
453+
None,
454+
455+
/// Only the specified groupings should have their order checked.
456+
Custom(Vec<String>),
457+
}
458+
459+
impl SourceItemOrderingWithinModuleItemGroupings {
460+
pub fn ordered_within(&self, grouping_name: &String) -> bool {
461+
match self {
462+
SourceItemOrderingWithinModuleItemGroupings::All => true,
463+
SourceItemOrderingWithinModuleItemGroupings::None => false,
464+
SourceItemOrderingWithinModuleItemGroupings::Custom(groups) => groups.contains(grouping_name),
465+
}
466+
}
467+
}
468+
469+
/// Helper struct for deserializing the [`SourceItemOrderingWithinModuleItemGroupings`].
470+
#[derive(Deserialize)]
471+
#[serde(untagged)]
472+
enum StringOrVecOfString {
473+
String(String),
474+
Vec(Vec<String>),
475+
}
476+
477+
impl<'de> Deserialize<'de> for SourceItemOrderingWithinModuleItemGroupings {
478+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
479+
where
480+
D: Deserializer<'de>,
481+
{
482+
let description = "The available options for configuring an ordering within module item groups are: \
483+
\"all\", \"none\", or a list of module item group names \
484+
(as configured with the `module_item_order_groupings` configuration option).";
485+
486+
match StringOrVecOfString::deserialize(deserializer) {
487+
Ok(StringOrVecOfString::String(preset)) if preset == "all" => {
488+
Ok(SourceItemOrderingWithinModuleItemGroupings::All)
489+
},
490+
Ok(StringOrVecOfString::String(preset)) if preset == "none" => {
491+
Ok(SourceItemOrderingWithinModuleItemGroupings::None)
492+
},
493+
Ok(StringOrVecOfString::String(preset)) => Err(de::Error::custom(format!(
494+
"Unknown configuration option: {preset}.\n{description}"
495+
))),
496+
Ok(StringOrVecOfString::Vec(groupings)) => {
497+
Ok(SourceItemOrderingWithinModuleItemGroupings::Custom(groupings))
498+
},
499+
Err(e) => Err(de::Error::custom(format!("{e}\n{description}"))),
500+
}
501+
}
502+
}
503+
504+
impl Serialize for SourceItemOrderingWithinModuleItemGroupings {
505+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
506+
where
507+
S: ser::Serializer,
508+
{
509+
match self {
510+
SourceItemOrderingWithinModuleItemGroupings::All => serializer.serialize_str("all"),
511+
SourceItemOrderingWithinModuleItemGroupings::None => serializer.serialize_str("none"),
512+
SourceItemOrderingWithinModuleItemGroupings::Custom(vec) => vec.serialize(serializer),
513+
}
514+
}
515+
}
516+
421517
// these impls are never actually called but are used by the various config options that default to
422518
// empty lists
423519
macro_rules! unimplemented_serialize {

clippy_lints/src/arbitrary_source_item_ordering.rs

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use clippy_config::Conf;
22
use clippy_config::types::{
33
SourceItemOrderingCategory, SourceItemOrderingModuleItemGroupings, SourceItemOrderingModuleItemKind,
44
SourceItemOrderingTraitAssocItemKind, SourceItemOrderingTraitAssocItemKinds,
5+
SourceItemOrderingWithinModuleItemGroupings,
56
};
67
use clippy_utils::diagnostics::span_lint_and_note;
78
use rustc_hir::{
@@ -37,7 +38,7 @@ declare_clippy_lint! {
3738
/// 2. Individual ordering rules per item kind.
3839
///
3940
/// The item kinds that can be linted are:
40-
/// - Module (with customized groupings, alphabetical within)
41+
/// - Module (with customized groupings, alphabetical within - configurable)
4142
/// - Trait (with customized order of associated items, alphabetical within)
4243
/// - Enum, Impl, Struct (purely alphabetical)
4344
///
@@ -58,8 +59,31 @@ declare_clippy_lint! {
5859
/// | `PascalCase` | "ty_alias", "opaque_ty", "enum", "struct", "union", "trait", "trait_alias", "impl" |
5960
/// | `lower_snake_case` | "fn" |
6061
///
62+
/// The groups' names are arbitrary and can be changed to suit the
63+
/// conventions that should be enforced for a specific project.
64+
///
6165
/// All item kinds must be accounted for to create an enforceable linting
62-
/// rule set.
66+
/// rule set. Following are some example configurations that may be useful.
67+
///
68+
/// Example: *module inclusions and use statements to be at the top*
69+
///
70+
/// ```toml
71+
/// module-item-order-groupings = [
72+
/// [ "modules", [ "extern_crate", "mod", "foreign_mod" ], ],
73+
/// [ "use", [ "use", ], ],
74+
/// [ "everything_else", [ "macro", "global_asm", "static", "const", "ty_alias", "enum", "struct", "union", "trait", "trait_alias", "impl", "fn", ], ],
75+
/// ]
76+
/// ```
77+
///
78+
/// Example: *only consts and statics should be alphabetically ordered*
79+
///
80+
/// It is also possible to configure a selection of module item groups that
81+
/// should be ordered alphabetically. This may be useful if for example
82+
/// statics and consts should be ordered, but the rest should be left open.
83+
///
84+
/// ```toml
85+
/// module-items-ordered-within-groupings = ["UPPER_SNAKE_CASE"]
86+
/// ```
6387
///
6488
/// ### Known Problems
6589
///
@@ -144,6 +168,7 @@ pub struct ArbitrarySourceItemOrdering {
144168
enable_ordering_for_struct: bool,
145169
enable_ordering_for_trait: bool,
146170
module_item_order_groupings: SourceItemOrderingModuleItemGroupings,
171+
module_items_ordered_within_groupings: SourceItemOrderingWithinModuleItemGroupings,
147172
}
148173

149174
impl ArbitrarySourceItemOrdering {
@@ -158,6 +183,7 @@ impl ArbitrarySourceItemOrdering {
158183
enable_ordering_for_struct: conf.source_item_ordering.contains(&Struct),
159184
enable_ordering_for_trait: conf.source_item_ordering.contains(&Trait),
160185
module_item_order_groupings: conf.module_item_order_groupings.clone(),
186+
module_items_ordered_within_groupings: conf.module_items_ordered_within_groupings.clone(),
161187
}
162188
}
163189

@@ -192,7 +218,7 @@ impl ArbitrarySourceItemOrdering {
192218
);
193219
}
194220

195-
fn lint_member_item<T: LintContext>(cx: &T, item: &Item<'_>, before_item: &Item<'_>) {
221+
fn lint_member_item<T: LintContext>(cx: &T, item: &Item<'_>, before_item: &Item<'_>, msg: &'static str) {
196222
let span = if item.ident.as_str().is_empty() {
197223
&item.span
198224
} else {
@@ -216,14 +242,7 @@ impl ArbitrarySourceItemOrdering {
216242
return;
217243
}
218244

219-
span_lint_and_note(
220-
cx,
221-
ARBITRARY_SOURCE_ITEM_ORDERING,
222-
*span,
223-
"incorrect ordering of items (must be alphabetically ordered)",
224-
Some(*before_span),
225-
note,
226-
);
245+
span_lint_and_note(cx, ARBITRARY_SOURCE_ITEM_ORDERING, *span, msg, Some(*before_span), note);
227246
}
228247

229248
/// Produces a linting warning for incorrectly ordered trait items.
@@ -403,6 +422,7 @@ impl<'tcx> LateLintPass<'tcx> for ArbitrarySourceItemOrdering {
403422
}
404423

405424
let item_kind = convert_module_item_kind(&item.kind);
425+
let grouping_name = self.module_item_order_groupings.grouping_name_of(&item_kind);
406426
let module_level_order = self
407427
.module_item_order_groupings
408428
.module_level_order_of(&item_kind)
@@ -412,13 +432,28 @@ impl<'tcx> LateLintPass<'tcx> for ArbitrarySourceItemOrdering {
412432
use std::cmp::Ordering; // Better legibility.
413433
match module_level_order.cmp(&cur_t.order) {
414434
Ordering::Less => {
415-
Self::lint_member_item(cx, item, cur_t.item);
435+
Self::lint_member_item(
436+
cx,
437+
item,
438+
cur_t.item,
439+
"incorrect ordering of items (module item groupings specify another order)",
440+
);
416441
},
417442
Ordering::Equal if item_kind == SourceItemOrderingModuleItemKind::Use => {
418443
// Skip ordering use statements, as these should be ordered by rustfmt.
419444
},
420-
Ordering::Equal if cur_t.name > get_item_name(item) => {
421-
Self::lint_member_item(cx, item, cur_t.item);
445+
Ordering::Equal
446+
if (grouping_name.is_some_and(|grouping_name| {
447+
self.module_items_ordered_within_groupings
448+
.ordered_within(&grouping_name)
449+
}) && cur_t.name > get_item_name(item)) =>
450+
{
451+
Self::lint_member_item(
452+
cx,
453+
item,
454+
cur_t.item,
455+
"incorrect ordering of items (must be alphabetically ordered)",
456+
);
422457
},
423458
Ordering::Equal | Ordering::Greater => {
424459
// Nothing to do in this case, they're already in the right order.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module-items-ordered-within-groupings = true

tests/ui-toml/arbitrary_source_item_ordering/default_exp/clippy.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ module-item-order-groupings = [
99
["PascalCase", ["ty_alias", "enum", "struct", "union", "trait", "trait_alias", "impl"]],
1010
["lower_snake_case", ["fn"]]
1111
]
12-
12+
module-items-ordered-within-groupings = "none"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module-items-ordered-within-groupings = [
2+
"PascalCase",
3+
"ignored unknown grouping",
4+
]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module-items-ordered-within-groupings = "all"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
error: error reading Clippy's configuration file: data did not match any variant of untagged enum StringOrVecOfString The available options for configuring an ordering within module item groups are: "all", "none", or a list of module item group names (as configured with the `module_item_order_groupings` configuration option).
2+
--> $DIR/tests/ui-toml/arbitrary_source_item_ordering/bad_conf_4/clippy.toml:1:41
3+
|
4+
LL | module-items-ordered-within-groupings = true
5+
| ^^^^
6+
7+
error: aborting due to 1 previous error
8+

tests/ui-toml/arbitrary_source_item_ordering/ordering_good.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
//@aux-build:../../ui/auxiliary/proc_macros.rs
2-
//@revisions: default default_exp bad_conf_1 bad_conf_2 bad_conf_3
2+
//@revisions: default default_exp bad_conf_1 bad_conf_2 bad_conf_3 bad_conf_4
33
//@[default] rustc-env:CLIPPY_CONF_DIR=tests/ui-toml/arbitrary_source_item_ordering/default
44
//@[default_exp] rustc-env:CLIPPY_CONF_DIR=tests/ui-toml/arbitrary_source_item_ordering/default_exp
55
//@[bad_conf_1] rustc-env:CLIPPY_CONF_DIR=tests/ui-toml/arbitrary_source_item_ordering/bad_conf_1
66
//@[bad_conf_2] rustc-env:CLIPPY_CONF_DIR=tests/ui-toml/arbitrary_source_item_ordering/bad_conf_2
77
//@[bad_conf_3] rustc-env:CLIPPY_CONF_DIR=tests/ui-toml/arbitrary_source_item_ordering/bad_conf_3
8+
//@[bad_conf_4] rustc-env:CLIPPY_CONF_DIR=tests/ui-toml/arbitrary_source_item_ordering/bad_conf_4
89

910
#![allow(dead_code)]
1011
#![warn(clippy::arbitrary_source_item_ordering)]

0 commit comments

Comments
 (0)