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:
commit
900a5aa036
@ -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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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() {}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user