Auto merge of #12180 - y21:conf_lev_distance, r=blyxyas

Suggest existing configuration option if one is found

While working on/testing #12179, I made the mistake of using underscores instead of dashes for the field name in the clippy.toml file and ended up being confused for a few minutes until I found out what's wrong. With this change, clippy will suggest an existing field if there's one that's similar.
```
1 | allow_mixed_uninlined_format_args = true
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: perhaps you meant: `allow-mixed-uninlined-format-args`
```
(in hindsight, the current behavior of printing all the config options makes it obvious in most cases but I still think a suggestion like this would be nice to have)

I had to play around with the value a bit. A max distance of 5 seemed a bit too strong since it'd suggest changing `foobar` to `msrv`, which seemed odd, and 4 seemed just good enough to detect a typo of five underscores.

changelog: when an invalid field in clippy.toml is found, suggest the closest existing one if one is found
This commit is contained in:
bors 2024-01-24 23:13:11 +00:00
commit 900a5aa036
5 changed files with 137 additions and 13 deletions

View File

@ -2,7 +2,9 @@
use crate::types::{DisallowedPath, MacroMatcher, MatchLintBehaviour, PubUnderscoreFieldsBehaviour, Rename}; use crate::types::{DisallowedPath, MacroMatcher, MatchLintBehaviour, PubUnderscoreFieldsBehaviour, Rename};
use crate::ClippyConfiguration; use crate::ClippyConfiguration;
use rustc_data_structures::fx::FxHashSet; use rustc_data_structures::fx::FxHashSet;
use rustc_errors::Applicability;
use rustc_session::Session; use rustc_session::Session;
use rustc_span::edit_distance::edit_distance;
use rustc_span::{BytePos, Pos, SourceFile, Span, SyntaxContext}; use rustc_span::{BytePos, Pos, SourceFile, Span, SyntaxContext};
use serde::de::{IgnoredAny, IntoDeserializer, MapAccess, Visitor}; use serde::de::{IgnoredAny, IntoDeserializer, MapAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
@ -59,18 +61,25 @@ fn from_toml_error(file: &SourceFile, error: &toml::de::Error) -> Self {
#[derive(Debug)] #[derive(Debug)]
struct ConfError { struct ConfError {
message: String, message: String,
suggestion: Option<Suggestion>,
span: Span, span: Span,
} }
impl ConfError { impl ConfError {
fn from_toml(file: &SourceFile, error: &toml::de::Error) -> Self { fn from_toml(file: &SourceFile, error: &toml::de::Error) -> Self {
let span = error.span().unwrap_or(0..file.source_len.0 as usize); let span = error.span().unwrap_or(0..file.source_len.0 as usize);
Self::spanned(file, error.message(), span) Self::spanned(file, error.message(), None, span)
} }
fn spanned(file: &SourceFile, message: impl Into<String>, span: Range<usize>) -> Self { fn spanned(
file: &SourceFile,
message: impl Into<String>,
suggestion: Option<Suggestion>,
span: Range<usize>,
) -> Self {
Self { Self {
message: message.into(), message: message.into(),
suggestion,
span: Span::new( span: Span::new(
file.start_pos + BytePos::from_usize(span.start), file.start_pos + BytePos::from_usize(span.start),
file.start_pos + BytePos::from_usize(span.end), file.start_pos + BytePos::from_usize(span.end),
@ -147,16 +156,18 @@ fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error> where V: MapA
match Field::deserialize(name.get_ref().as_str().into_deserializer()) { match Field::deserialize(name.get_ref().as_str().into_deserializer()) {
Err(e) => { Err(e) => {
let e: FieldError = e; let e: FieldError = e;
errors.push(ConfError::spanned(self.0, e.0, name.span())); errors.push(ConfError::spanned(self.0, e.error, e.suggestion, name.span()));
} }
$(Ok(Field::$name) => { $(Ok(Field::$name) => {
$(warnings.push(ConfError::spanned(self.0, format!("deprecated field `{}`. {}", name.get_ref(), $dep), name.span()));)? $(warnings.push(ConfError::spanned(self.0, format!("deprecated field `{}`. {}", name.get_ref(), $dep), None, name.span()));)?
let raw_value = map.next_value::<toml::Spanned<toml::Value>>()?; let raw_value = map.next_value::<toml::Spanned<toml::Value>>()?;
let value_span = raw_value.span(); let value_span = raw_value.span();
match <$ty>::deserialize(raw_value.into_inner()) { match <$ty>::deserialize(raw_value.into_inner()) {
Err(e) => errors.push(ConfError::spanned(self.0, e.to_string().replace('\n', " ").trim(), value_span)), Err(e) => errors.push(ConfError::spanned(self.0, e.to_string().replace('\n', " ").trim(), None, value_span)),
Ok(value) => match $name { Ok(value) => match $name {
Some(_) => errors.push(ConfError::spanned(self.0, format!("duplicate field `{}`", name.get_ref()), name.span())), Some(_) => {
errors.push(ConfError::spanned(self.0, format!("duplicate field `{}`", name.get_ref()), None, name.span()));
}
None => { None => {
$name = Some(value); $name = Some(value);
// $new_conf is the same as one of the defined `$name`s, so // $new_conf is the same as one of the defined `$name`s, so
@ -165,7 +176,7 @@ fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error> where V: MapA
Some(_) => errors.push(ConfError::spanned(self.0, concat!( Some(_) => errors.push(ConfError::spanned(self.0, concat!(
"duplicate field `", stringify!($new_conf), "duplicate field `", stringify!($new_conf),
"` (provided as `", stringify!($name), "`)" "` (provided as `", stringify!($name), "`)"
), name.span())), ), None, name.span())),
None => $new_conf = $name.clone(), None => $new_conf = $name.clone(),
})? })?
}, },
@ -673,10 +684,16 @@ fn read_inner(sess: &Session, path: &io::Result<(Option<PathBuf>, Vec<String>)>)
// all conf errors are non-fatal, we just use the default conf in case of error // all conf errors are non-fatal, we just use the default conf in case of error
for error in errors { for error in errors {
sess.dcx().span_err( let mut diag = sess.dcx().struct_span_err(
error.span, error.span,
format!("error reading Clippy's configuration file: {}", error.message), format!("error reading Clippy's configuration file: {}", error.message),
); );
if let Some(sugg) = error.suggestion {
diag.span_suggestion(error.span, sugg.message, sugg.suggestion, Applicability::MaybeIncorrect);
}
diag.emit();
} }
for warning in warnings { for warning in warnings {
@ -693,19 +710,31 @@ fn read_inner(sess: &Session, path: &io::Result<(Option<PathBuf>, Vec<String>)>)
const SEPARATOR_WIDTH: usize = 4; const SEPARATOR_WIDTH: usize = 4;
#[derive(Debug)] #[derive(Debug)]
struct FieldError(String); struct FieldError {
error: String,
suggestion: Option<Suggestion>,
}
#[derive(Debug)]
struct Suggestion {
message: &'static str,
suggestion: &'static str,
}
impl std::error::Error for FieldError {} impl std::error::Error for FieldError {}
impl Display for FieldError { impl Display for FieldError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.pad(&self.0) f.pad(&self.error)
} }
} }
impl serde::de::Error for FieldError { impl serde::de::Error for FieldError {
fn custom<T: Display>(msg: T) -> Self { fn custom<T: Display>(msg: T) -> Self {
Self(msg.to_string()) Self {
error: msg.to_string(),
suggestion: None,
}
} }
fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self { fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
@ -727,7 +756,20 @@ fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
write!(msg, "{:SEPARATOR_WIDTH$}{field:column_width$}", " ").unwrap(); write!(msg, "{:SEPARATOR_WIDTH$}{field:column_width$}", " ").unwrap();
} }
} }
Self(msg)
let suggestion = expected
.iter()
.filter_map(|expected| {
let dist = edit_distance(field, expected, 4)?;
Some((dist, expected))
})
.min_by_key(|&(dist, _)| dist)
.map(|(_, suggestion)| Suggestion {
message: "perhaps you meant",
suggestion,
});
Self { error: msg, suggestion }
} }
} }

View File

@ -11,6 +11,7 @@
extern crate rustc_data_structures; extern crate rustc_data_structures;
#[allow(unused_extern_crates)] #[allow(unused_extern_crates)]
extern crate rustc_driver; extern crate rustc_driver;
extern crate rustc_errors;
extern crate rustc_session; extern crate rustc_session;
extern crate rustc_span; extern crate rustc_span;

View File

@ -3,6 +3,9 @@ foobar = 42
# so is this one # so is this one
barfoo = 53 barfoo = 53
# when using underscores instead of dashes, suggest the correct one
allow_mixed_uninlined_format_args = true
# that one is ignored # that one is ignored
[third-party] [third-party]
clippy-feature = "nightly" clippy-feature = "nightly"

View File

@ -1,3 +1,4 @@
//@no-rustfix
//@error-in-other-file: unknown field `foobar`, expected one of //@error-in-other-file: unknown field `foobar`, expected one of
fn main() {} fn main() {}

View File

@ -152,5 +152,82 @@ error: error reading Clippy's configuration file: unknown field `barfoo`, expect
LL | barfoo = 53 LL | barfoo = 53
| ^^^^^^ | ^^^^^^
error: aborting due to 2 previous errors error: error reading Clippy's configuration file: unknown field `allow_mixed_uninlined_format_args`, expected one of
absolute-paths-allowed-crates
absolute-paths-max-segments
accept-comment-above-attributes
accept-comment-above-statement
allow-dbg-in-tests
allow-expect-in-tests
allow-mixed-uninlined-format-args
allow-one-hash-in-raw-strings
allow-print-in-tests
allow-private-module-inception
allow-unwrap-in-tests
allowed-dotfiles
allowed-duplicate-crates
allowed-idents-below-min-chars
allowed-scripts
arithmetic-side-effects-allowed
arithmetic-side-effects-allowed-binary
arithmetic-side-effects-allowed-unary
array-size-threshold
avoid-breaking-exported-api
await-holding-invalid-types
blacklisted-names
cargo-ignore-publish
check-private-items
cognitive-complexity-threshold
cyclomatic-complexity-threshold
disallowed-macros
disallowed-methods
disallowed-names
disallowed-types
doc-valid-idents
enable-raw-pointer-heuristic-for-send
enforce-iter-loop-reborrow
enforced-import-renames
enum-variant-name-threshold
enum-variant-size-threshold
excessive-nesting-threshold
future-size-threshold
ignore-interior-mutability
large-error-threshold
literal-representation-threshold
matches-for-let-else
max-fn-params-bools
max-include-file-size
max-struct-bools
max-suggested-slice-pattern-length
max-trait-bounds
min-ident-chars-threshold
missing-docs-in-crate-items
msrv
pass-by-value-size-limit
pub-underscore-fields-behavior
semicolon-inside-block-ignore-singleline
semicolon-outside-block-ignore-multiline
single-char-binding-names-threshold
stack-size-threshold
standard-macro-braces
struct-field-name-threshold
suppress-restriction-lint-in-const
third-party
too-large-for-stack
too-many-arguments-threshold
too-many-lines-threshold
trivial-copy-size-limit
type-complexity-threshold
unnecessary-box-size
unreadable-literal-lint-fractions
upper-case-acronyms-aggressive
vec-box-size-threshold
verbose-bit-mask-threshold
warn-on-all-wildcard-imports
--> $DIR/$DIR/clippy.toml:7:1
|
LL | allow_mixed_uninlined_format_args = true
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: perhaps you meant: `allow-mixed-uninlined-format-args`
error: aborting due to 3 previous errors