Skip to content

Commit 819f3c7

Browse files
authored
arbitrary_source_item_ordering: Make alphabetic ordering in module item groups optional (rust-lang#13718)
From feedback to the `arbitrary_source_item_ordering` lint after its inclusion in clippy 1.82, making alphabetic ordering within module item groups has turned out to be the most requested improvement. With this improvement, it is possible to make the lint perform 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. Implements parts of the suggestions from rust-lang#13675. A catch-all-group is still to be implemented. changelog: [`arbitrary_source_item_ordering`]: Make alphabetic ordering in module item groups optional (off by default)
2 parents 3d775b3 + 5df6887 commit 819f3c7

26 files changed

+871
-129
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6372,6 +6372,7 @@ Released 2018-09-13
63726372
[`min-ident-chars-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#min-ident-chars-threshold
63736373
[`missing-docs-in-crate-items`]: https://doc.rust-lang.org/clippy/lint_configuration.html#missing-docs-in-crate-items
63746374
[`module-item-order-groupings`]: https://doc.rust-lang.org/clippy/lint_configuration.html#module-item-order-groupings
6375+
[`module-items-ordered-within-groupings`]: https://doc.rust-lang.org/clippy/lint_configuration.html#module-items-ordered-within-groupings
63756376
[`msrv`]: https://doc.rust-lang.org/clippy/lint_configuration.html#msrv
63766377
[`pass-by-value-size-limit`]: https://doc.rust-lang.org/clippy/lint_configuration.html#pass-by-value-size-limit
63776378
[`pub-underscore-fields-behavior`]: https://doc.rust-lang.org/clippy/lint_configuration.html#pub-underscore-fields-behavior

book/src/lint_configuration.md

+13
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,19 @@ The named groupings of different source item kinds within modules.
744744
* [`arbitrary_source_item_ordering`](https://rust-lang.github.io/rust-clippy/master/index.html#arbitrary_source_item_ordering)
745745

746746

747+
## `module-items-ordered-within-groupings`
748+
Whether the items within module groups should be ordered alphabetically or not.
749+
750+
This option can be configured to "all", "none", or a list of specific grouping names that should be checked
751+
(e.g. only "enums").
752+
753+
**Default Value:** `"none"`
754+
755+
---
756+
**Affected lints:**
757+
* [`arbitrary_source_item_ordering`](https://rust-lang.github.io/rust-clippy/master/index.html#arbitrary_source_item_ordering)
758+
759+
747760
## `msrv`
748761
The minimum rust version that the project supports. Defaults to the `rust-version` field in `Cargo.toml`
749762

clippy_config/src/conf.rs

+66-12
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@ use crate::types::{
33
DisallowedPath, DisallowedPathWithoutReplacement, MacroMatcher, MatchLintBehaviour, PubUnderscoreFieldsBehaviour,
44
Rename, SourceItemOrdering, SourceItemOrderingCategory, SourceItemOrderingModuleItemGroupings,
55
SourceItemOrderingModuleItemKind, SourceItemOrderingTraitAssocItemKind, SourceItemOrderingTraitAssocItemKinds,
6+
SourceItemOrderingWithinModuleItemGroupings,
67
};
78
use clippy_utils::msrvs::Msrv;
9+
use itertools::Itertools;
810
use rustc_errors::Applicability;
911
use rustc_session::Session;
1012
use rustc_span::edit_distance::edit_distance;
1113
use rustc_span::{BytePos, Pos, SourceFile, Span, SyntaxContext};
1214
use serde::de::{IgnoredAny, IntoDeserializer, MapAccess, Visitor};
1315
use serde::{Deserialize, Deserializer, Serialize};
16+
use std::collections::HashMap;
1417
use std::fmt::{Debug, Display, Formatter};
1518
use std::ops::Range;
1619
use std::path::PathBuf;
@@ -79,6 +82,7 @@ const DEFAULT_SOURCE_ITEM_ORDERING: &[SourceItemOrderingCategory] = {
7982
#[derive(Default)]
8083
struct TryConf {
8184
conf: Conf,
85+
value_spans: HashMap<String, Range<usize>>,
8286
errors: Vec<ConfError>,
8387
warnings: Vec<ConfError>,
8488
}
@@ -87,6 +91,7 @@ impl TryConf {
8791
fn from_toml_error(file: &SourceFile, error: &toml::de::Error) -> Self {
8892
Self {
8993
conf: Conf::default(),
94+
value_spans: HashMap::default(),
9095
errors: vec![ConfError::from_toml(file, error)],
9196
warnings: vec![],
9297
}
@@ -210,6 +215,7 @@ macro_rules! define_Conf {
210215
}
211216

212217
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error> where V: MapAccess<'de> {
218+
let mut value_spans = HashMap::new();
213219
let mut errors = Vec::new();
214220
let mut warnings = Vec::new();
215221
$(let mut $name = None;)*
@@ -232,6 +238,7 @@ macro_rules! define_Conf {
232238
}
233239
None => {
234240
$name = Some(value);
241+
value_spans.insert(name.get_ref().as_str().to_string(), value_span);
235242
// $new_conf is the same as one of the defined `$name`s, so
236243
// this variable is defined in line 2 of this function.
237244
$(match $new_conf {
@@ -250,7 +257,7 @@ macro_rules! define_Conf {
250257
}
251258
}
252259
let conf = Conf { $($name: $name.unwrap_or_else(defaults::$name),)* };
253-
Ok(TryConf { conf, errors, warnings })
260+
Ok(TryConf { conf, value_spans, errors, warnings })
254261
}
255262
}
256263

@@ -596,6 +603,13 @@ define_Conf! {
596603
/// The named groupings of different source item kinds within modules.
597604
#[lints(arbitrary_source_item_ordering)]
598605
module_item_order_groupings: SourceItemOrderingModuleItemGroupings = DEFAULT_MODULE_ITEM_ORDERING_GROUPS.into(),
606+
/// Whether the items within module groups should be ordered alphabetically or not.
607+
///
608+
/// This option can be configured to "all", "none", or a list of specific grouping names that should be checked
609+
/// (e.g. only "enums").
610+
#[lints(arbitrary_source_item_ordering)]
611+
module_items_ordered_within_groupings: SourceItemOrderingWithinModuleItemGroupings =
612+
SourceItemOrderingWithinModuleItemGroupings::None,
599613
/// The minimum rust version that the project supports. Defaults to the `rust-version` field in `Cargo.toml`
600614
#[default_text = "current version"]
601615
#[lints(
@@ -815,6 +829,36 @@ fn deserialize(file: &SourceFile) -> TryConf {
815829
&mut conf.conf.allow_renamed_params_for,
816830
DEFAULT_ALLOWED_TRAITS_WITH_RENAMED_PARAMS,
817831
);
832+
833+
// Confirms that the user has not accidentally configured ordering requirements for groups that
834+
// aren't configured.
835+
if let SourceItemOrderingWithinModuleItemGroupings::Custom(groupings) =
836+
&conf.conf.module_items_ordered_within_groupings
837+
{
838+
for grouping in groupings {
839+
if !conf.conf.module_item_order_groupings.is_grouping(grouping) {
840+
// Since this isn't fixable by rustfix, don't emit a `Suggestion`. This just adds some useful
841+
// info for the user instead.
842+
843+
let names = conf.conf.module_item_order_groupings.grouping_names();
844+
let suggestion = suggest_candidate(grouping, names.iter().map(String::as_str))
845+
.map(|s| format!(" perhaps you meant `{s}`?"))
846+
.unwrap_or_default();
847+
let names = names.iter().map(|s| format!("`{s}`")).join(", ");
848+
let message = format!(
849+
"unknown ordering group: `{grouping}` was not specified in `module-items-ordered-within-groupings`,{suggestion} expected one of: {names}"
850+
);
851+
852+
let span = conf
853+
.value_spans
854+
.get("module_item_order_groupings")
855+
.cloned()
856+
.unwrap_or_default();
857+
conf.errors.push(ConfError::spanned(file, message, None, span));
858+
}
859+
}
860+
}
861+
818862
// TODO: THIS SHOULD BE TESTED, this comment will be gone soon
819863
if conf.conf.allowed_idents_below_min_chars.iter().any(|e| e == "..") {
820864
conf.conf
@@ -860,6 +904,7 @@ impl Conf {
860904

861905
let TryConf {
862906
mut conf,
907+
value_spans: _,
863908
errors,
864909
warnings,
865910
} = match path {
@@ -950,17 +995,10 @@ impl serde::de::Error for FieldError {
950995
}
951996
}
952997

953-
let suggestion = expected
954-
.iter()
955-
.filter_map(|expected| {
956-
let dist = edit_distance(field, expected, 4)?;
957-
Some((dist, expected))
958-
})
959-
.min_by_key(|&(dist, _)| dist)
960-
.map(|(_, suggestion)| Suggestion {
961-
message: "perhaps you meant",
962-
suggestion,
963-
});
998+
let suggestion = suggest_candidate(field, expected).map(|suggestion| Suggestion {
999+
message: "perhaps you meant",
1000+
suggestion,
1001+
});
9641002

9651003
Self { error: msg, suggestion }
9661004
}
@@ -998,6 +1036,22 @@ fn calculate_dimensions(fields: &[&str]) -> (usize, Vec<usize>) {
9981036
(rows, column_widths)
9991037
}
10001038

1039+
/// Given a user-provided value that couldn't be matched to a known option, finds the most likely
1040+
/// candidate among candidates that the user might have meant.
1041+
fn suggest_candidate<'a, I>(value: &str, candidates: I) -> Option<&'a str>
1042+
where
1043+
I: IntoIterator<Item = &'a str>,
1044+
{
1045+
candidates
1046+
.into_iter()
1047+
.filter_map(|expected| {
1048+
let dist = edit_distance(value, expected, 4)?;
1049+
Some((dist, expected))
1050+
})
1051+
.min_by_key(|&(dist, _)| dist)
1052+
.map(|(_, suggestion)| suggestion)
1053+
}
1054+
10011055
#[cfg(test)]
10021056
mod tests {
10031057
use serde::de::IgnoredAny;

clippy_config/src/types.rs

+106-2
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ impl SourceItemOrderingModuleItemKind {
305305
pub struct SourceItemOrderingModuleItemGroupings {
306306
groups: Vec<(String, Vec<SourceItemOrderingModuleItemKind>)>,
307307
lut: HashMap<SourceItemOrderingModuleItemKind, usize>,
308+
back_lut: HashMap<SourceItemOrderingModuleItemKind, String>,
308309
}
309310

310311
impl SourceItemOrderingModuleItemGroupings {
@@ -320,6 +321,30 @@ impl SourceItemOrderingModuleItemGroupings {
320321
lut
321322
}
322323

324+
fn build_back_lut(
325+
groups: &[(String, Vec<SourceItemOrderingModuleItemKind>)],
326+
) -> HashMap<SourceItemOrderingModuleItemKind, String> {
327+
let mut lut = HashMap::new();
328+
for (group_name, items) in groups {
329+
for item in items {
330+
lut.insert(item.clone(), group_name.clone());
331+
}
332+
}
333+
lut
334+
}
335+
336+
pub fn grouping_name_of(&self, item: &SourceItemOrderingModuleItemKind) -> Option<&String> {
337+
self.back_lut.get(item)
338+
}
339+
340+
pub fn grouping_names(&self) -> Vec<String> {
341+
self.groups.iter().map(|(name, _)| name.clone()).collect()
342+
}
343+
344+
pub fn is_grouping(&self, grouping: &str) -> bool {
345+
self.groups.iter().any(|(g, _)| g == grouping)
346+
}
347+
323348
pub fn module_level_order_of(&self, item: &SourceItemOrderingModuleItemKind) -> Option<usize> {
324349
self.lut.get(item).copied()
325350
}
@@ -330,7 +355,8 @@ impl From<&[(&str, &[SourceItemOrderingModuleItemKind])]> for SourceItemOrdering
330355
let groups: Vec<(String, Vec<SourceItemOrderingModuleItemKind>)> =
331356
value.iter().map(|item| (item.0.to_string(), item.1.to_vec())).collect();
332357
let lut = Self::build_lut(&groups);
333-
Self { groups, lut }
358+
let back_lut = Self::build_back_lut(&groups);
359+
Self { groups, lut, back_lut }
334360
}
335361
}
336362

@@ -348,6 +374,7 @@ impl<'de> Deserialize<'de> for SourceItemOrderingModuleItemGroupings {
348374
let groups = Vec::<(String, Vec<SourceItemOrderingModuleItemKind>)>::deserialize(deserializer)?;
349375
let items_total: usize = groups.iter().map(|(_, v)| v.len()).sum();
350376
let lut = Self::build_lut(&groups);
377+
let back_lut = Self::build_back_lut(&groups);
351378

352379
let mut expected_items = SourceItemOrderingModuleItemKind::all_variants();
353380
for item in lut.keys() {
@@ -370,7 +397,7 @@ impl<'de> Deserialize<'de> for SourceItemOrderingModuleItemGroupings {
370397
));
371398
}
372399

373-
Ok(Self { groups, lut })
400+
Ok(Self { groups, lut, back_lut })
374401
} else if items_total != all_items.len() {
375402
Err(de::Error::custom(format!(
376403
"Some module item kinds were configured more than once, or were missing, in the source ordering configuration. \
@@ -482,6 +509,83 @@ impl Serialize for SourceItemOrderingTraitAssocItemKinds {
482509
}
483510
}
484511

512+
/// Describes which specific groupings should have their items ordered
513+
/// alphabetically.
514+
///
515+
/// This is separate from defining and enforcing groupings. For example,
516+
/// defining enums are grouped before structs still allows for an enum B to be
517+
/// placed before an enum A. Only when enforcing ordering within the grouping,
518+
/// will it be checked if A is placed before B.
519+
#[derive(Clone, Debug)]
520+
pub enum SourceItemOrderingWithinModuleItemGroupings {
521+
/// All groupings should have their items ordered.
522+
All,
523+
524+
/// None of the groupings should have their order checked.
525+
None,
526+
527+
/// Only the specified groupings should have their order checked.
528+
Custom(Vec<String>),
529+
}
530+
531+
impl SourceItemOrderingWithinModuleItemGroupings {
532+
pub fn ordered_within(&self, grouping_name: &String) -> bool {
533+
match self {
534+
SourceItemOrderingWithinModuleItemGroupings::All => true,
535+
SourceItemOrderingWithinModuleItemGroupings::None => false,
536+
SourceItemOrderingWithinModuleItemGroupings::Custom(groups) => groups.contains(grouping_name),
537+
}
538+
}
539+
}
540+
541+
/// Helper struct for deserializing the [`SourceItemOrderingWithinModuleItemGroupings`].
542+
#[derive(Deserialize)]
543+
#[serde(untagged)]
544+
enum StringOrVecOfString {
545+
String(String),
546+
Vec(Vec<String>),
547+
}
548+
549+
impl<'de> Deserialize<'de> for SourceItemOrderingWithinModuleItemGroupings {
550+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
551+
where
552+
D: Deserializer<'de>,
553+
{
554+
let description = "The available options for configuring an ordering within module item groups are: \
555+
\"all\", \"none\", or a list of module item group names \
556+
(as configured with the `module-item-order-groupings` configuration option).";
557+
558+
match StringOrVecOfString::deserialize(deserializer) {
559+
Ok(StringOrVecOfString::String(preset)) if preset == "all" => {
560+
Ok(SourceItemOrderingWithinModuleItemGroupings::All)
561+
},
562+
Ok(StringOrVecOfString::String(preset)) if preset == "none" => {
563+
Ok(SourceItemOrderingWithinModuleItemGroupings::None)
564+
},
565+
Ok(StringOrVecOfString::String(preset)) => Err(de::Error::custom(format!(
566+
"Unknown configuration option: {preset}.\n{description}"
567+
))),
568+
Ok(StringOrVecOfString::Vec(groupings)) => {
569+
Ok(SourceItemOrderingWithinModuleItemGroupings::Custom(groupings))
570+
},
571+
Err(e) => Err(de::Error::custom(format!("{e}\n{description}"))),
572+
}
573+
}
574+
}
575+
576+
impl Serialize for SourceItemOrderingWithinModuleItemGroupings {
577+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
578+
where
579+
S: ser::Serializer,
580+
{
581+
match self {
582+
SourceItemOrderingWithinModuleItemGroupings::All => serializer.serialize_str("all"),
583+
SourceItemOrderingWithinModuleItemGroupings::None => serializer.serialize_str("none"),
584+
SourceItemOrderingWithinModuleItemGroupings::Custom(vec) => vec.serialize(serializer),
585+
}
586+
}
587+
}
588+
485589
// these impls are never actually called but are used by the various config options that default to
486590
// empty lists
487591
macro_rules! unimplemented_serialize {

0 commit comments

Comments
 (0)