about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2024-01-24 23:13:11 +0000
committerbors <bors@rust-lang.org>2024-01-24 23:13:11 +0000
commit900a5aa036c6070fdec8acd6d871a4bfc89b2fb4 (patch)
tree8cf88dbf80bb13a6ceb0f73633d4be03dd99bfa3
parent76a75bf145bbdfb688354d6f9bdebf3fc56af936 (diff)
parent4780637cbc4b914218753a23ee54f5785826e403 (diff)
downloadrust-900a5aa036c6070fdec8acd6d871a4bfc89b2fb4.tar.gz
rust-900a5aa036c6070fdec8acd6d871a4bfc89b2fb4.zip
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
-rw-r--r--clippy_config/src/conf.rs66
-rw-r--r--clippy_config/src/lib.rs1
-rw-r--r--tests/ui-toml/toml_unknown_key/clippy.toml3
-rw-r--r--tests/ui-toml/toml_unknown_key/conf_unknown_key.rs1
-rw-r--r--tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr79
5 files changed, 137 insertions, 13 deletions
diff --git a/clippy_config/src/conf.rs b/clippy_config/src/conf.rs
index 626b23f7309..4e9ddbf8259 100644
--- a/clippy_config/src/conf.rs
+++ b/clippy_config/src/conf.rs
@@ -2,7 +2,9 @@ use crate::msrvs::Msrv;
 use crate::types::{DisallowedPath, MacroMatcher, MatchLintBehaviour, PubUnderscoreFieldsBehaviour, Rename};
 use crate::ClippyConfiguration;
 use rustc_data_structures::fx::FxHashSet;
+use rustc_errors::Applicability;
 use rustc_session::Session;
+use rustc_span::edit_distance::edit_distance;
 use rustc_span::{BytePos, Pos, SourceFile, Span, SyntaxContext};
 use serde::de::{IgnoredAny, IntoDeserializer, MapAccess, Visitor};
 use serde::{Deserialize, Deserializer, Serialize};
@@ -59,18 +61,25 @@ impl TryConf {
 #[derive(Debug)]
 struct ConfError {
     message: String,
+    suggestion: Option<Suggestion>,
     span: Span,
 }
 
 impl ConfError {
     fn from_toml(file: &SourceFile, error: &toml::de::Error) -> Self {
         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 {
             message: message.into(),
+            suggestion,
             span: Span::new(
                 file.start_pos + BytePos::from_usize(span.start),
                 file.start_pos + BytePos::from_usize(span.end),
@@ -147,16 +156,18 @@ macro_rules! define_Conf {
                     match Field::deserialize(name.get_ref().as_str().into_deserializer()) {
                         Err(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) => {
-                            $(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 value_span = raw_value.span();
                             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 {
-                                    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 => {
                                         $name = Some(value);
                                         // $new_conf is the same as one of the defined `$name`s, so
@@ -165,7 +176,7 @@ macro_rules! define_Conf {
                                             Some(_) => errors.push(ConfError::spanned(self.0, concat!(
                                                 "duplicate field `", stringify!($new_conf),
                                                 "` (provided as `", stringify!($name), "`)"
-                                            ), name.span())),
+                                            ), None, name.span())),
                                             None => $new_conf = $name.clone(),
                                         })?
                                     },
@@ -673,10 +684,16 @@ impl Conf {
 
         // all conf errors are non-fatal, we just use the default conf in case of error
         for error in errors {
-            sess.dcx().span_err(
+            let mut diag = sess.dcx().struct_span_err(
                 error.span,
                 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 {
@@ -693,19 +710,31 @@ impl Conf {
 const SEPARATOR_WIDTH: usize = 4;
 
 #[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 Display for FieldError {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
-        f.pad(&self.0)
+        f.pad(&self.error)
     }
 }
 
 impl serde::de::Error for FieldError {
     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 {
@@ -727,7 +756,20 @@ impl serde::de::Error for FieldError {
                 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 }
     }
 }
 
diff --git a/clippy_config/src/lib.rs b/clippy_config/src/lib.rs
index f5dcb16d670..533e375a310 100644
--- a/clippy_config/src/lib.rs
+++ b/clippy_config/src/lib.rs
@@ -11,6 +11,7 @@ extern crate rustc_ast;
 extern crate rustc_data_structures;
 #[allow(unused_extern_crates)]
 extern crate rustc_driver;
+extern crate rustc_errors;
 extern crate rustc_session;
 extern crate rustc_span;
 
diff --git a/tests/ui-toml/toml_unknown_key/clippy.toml b/tests/ui-toml/toml_unknown_key/clippy.toml
index b77b4580051..2b63f6e5c26 100644
--- a/tests/ui-toml/toml_unknown_key/clippy.toml
+++ b/tests/ui-toml/toml_unknown_key/clippy.toml
@@ -3,6 +3,9 @@ foobar = 42
 # so is this one
 barfoo = 53
 
+# when using underscores instead of dashes, suggest the correct one
+allow_mixed_uninlined_format_args = true
+
 # that one is ignored
 [third-party]
 clippy-feature = "nightly"
diff --git a/tests/ui-toml/toml_unknown_key/conf_unknown_key.rs b/tests/ui-toml/toml_unknown_key/conf_unknown_key.rs
index 38009627757..49139b60a9f 100644
--- a/tests/ui-toml/toml_unknown_key/conf_unknown_key.rs
+++ b/tests/ui-toml/toml_unknown_key/conf_unknown_key.rs
@@ -1,3 +1,4 @@
+//@no-rustfix
 //@error-in-other-file: unknown field `foobar`, expected one of
 
 fn main() {}
diff --git a/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr b/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr
index 09fc9dfa463..fc683e514ba 100644
--- a/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr
+++ b/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr
@@ -152,5 +152,82 @@ error: error reading Clippy's configuration file: unknown field `barfoo`, expect
 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