Skip to content

Commit b6ad6fc

Browse files
committed
Auto merge of #8823 - smoelius:unknown-field, r=xFrednet
Improve "unknown field" error messages Fixes #8806 Sample output: ``` error: error reading Clippy's configuration file `/home/smoelius/github/smoelius/rust-clippy/clippy.toml`: unknown field `foobar`, expected one of allow-expect-in-tests enable-raw-pointer-heuristic-for-send standard-macro-braces allow-unwrap-in-tests enforced-import-renames third-party allowed-scripts enum-variant-name-threshold too-large-for-stack array-size-threshold enum-variant-size-threshold too-many-arguments-threshold avoid-breaking-exported-api literal-representation-threshold too-many-lines-threshold await-holding-invalid-types max-fn-params-bools trivial-copy-size-limit blacklisted-names max-include-file-size type-complexity-threshold cargo-ignore-publish max-struct-bools unreadable-literal-lint-fractions cognitive-complexity-threshold max-suggested-slice-pattern-length upper-case-acronyms-aggressive cyclomatic-complexity-threshold max-trait-bounds vec-box-size-threshold disallowed-methods msrv verbose-bit-mask-threshold disallowed-types pass-by-value-size-limit warn-on-all-wildcard-imports doc-valid-idents single-char-binding-names-threshold at line 1 column 1 ``` You can test this by (say) adding `foobar = 42` to Clippy's root `clippy.toml` file, and running `cargo run --bin cargo-clippy`. Note that, to get the terminal width, this PR adds `termize` as a dependency to `cargo-clippy`. However, `termize` is also [how `rustc_errors` gets the terminal width](https://github.com/rust-lang/rust/blob/481db40311cdd241ae4d33f34f2f75732e44d8e8/compiler/rustc_errors/src/emitter.rs#L1607). So, hopefully, this is not a dealbreaker. r? `@xFrednet` changelog: Enhancements: the "unknown field" error messages for config files now wraps the field names.
2 parents bf2e631 + 5647257 commit b6ad6fc

File tree

5 files changed

+169
-10
lines changed

5 files changed

+169
-10
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ clippy_lints = { path = "clippy_lints" }
2525
semver = "1.0"
2626
rustc_tools_util = { path = "rustc_tools_util" }
2727
tempfile = { version = "3.2", optional = true }
28+
termize = "0.1"
2829

2930
[dev-dependencies]
3031
compiletest_rs = { version = "0.7.1", features = ["tmp"] }

clippy_lints/src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ mod zero_sized_map_values;
419419
// end lints modules, do not remove this comment, it’s used in `update_lints`
420420

421421
pub use crate::utils::conf::Conf;
422-
use crate::utils::conf::TryConf;
422+
use crate::utils::conf::{format_error, TryConf};
423423

424424
/// Register all pre expansion lints
425425
///
@@ -464,7 +464,7 @@ pub fn read_conf(sess: &Session) -> Conf {
464464
sess.struct_err(&format!(
465465
"error reading Clippy's configuration file `{}`: {}",
466466
file_name.display(),
467-
error
467+
format_error(error)
468468
))
469469
.emit();
470470
}

clippy_lints/src/utils/conf.rs

+122-7
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use serde::de::{Deserializer, IgnoredAny, IntoDeserializer, MapAccess, Visitor};
66
use serde::Deserialize;
77
use std::error::Error;
88
use std::path::{Path, PathBuf};
9-
use std::{env, fmt, fs, io};
9+
use std::str::FromStr;
10+
use std::{cmp, env, fmt, fs, io, iter};
1011

1112
/// Holds information used by `MISSING_ENFORCED_IMPORT_RENAMES` lint.
1213
#[derive(Clone, Debug, Deserialize)]
@@ -43,18 +44,33 @@ pub enum DisallowedType {
4344
#[derive(Default)]
4445
pub struct TryConf {
4546
pub conf: Conf,
46-
pub errors: Vec<String>,
47+
pub errors: Vec<Box<dyn Error>>,
4748
}
4849

4950
impl TryConf {
50-
fn from_error(error: impl Error) -> Self {
51+
fn from_error(error: impl Error + 'static) -> Self {
5152
Self {
5253
conf: Conf::default(),
53-
errors: vec![error.to_string()],
54+
errors: vec![Box::new(error)],
5455
}
5556
}
5657
}
5758

59+
#[derive(Debug)]
60+
struct ConfError(String);
61+
62+
impl fmt::Display for ConfError {
63+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64+
<String as fmt::Display>::fmt(&self.0, f)
65+
}
66+
}
67+
68+
impl Error for ConfError {}
69+
70+
fn conf_error(s: String) -> Box<dyn Error> {
71+
Box::new(ConfError(s))
72+
}
73+
5874
macro_rules! define_Conf {
5975
($(
6076
$(#[doc = $doc:literal])+
@@ -103,11 +119,11 @@ macro_rules! define_Conf {
103119
while let Some(name) = map.next_key::<&str>()? {
104120
match Field::deserialize(name.into_deserializer())? {
105121
$(Field::$name => {
106-
$(errors.push(format!("deprecated field `{}`. {}", name, $dep));)?
122+
$(errors.push(conf_error(format!("deprecated field `{}`. {}", name, $dep)));)?
107123
match map.next_value() {
108-
Err(e) => errors.push(e.to_string()),
124+
Err(e) => errors.push(conf_error(e.to_string())),
109125
Ok(value) => match $name {
110-
Some(_) => errors.push(format!("duplicate field `{}`", name)),
126+
Some(_) => errors.push(conf_error(format!("duplicate field `{}`", name))),
111127
None => $name = Some(value),
112128
}
113129
}
@@ -383,3 +399,102 @@ pub fn read(path: &Path) -> TryConf {
383399
};
384400
toml::from_str(&content).unwrap_or_else(TryConf::from_error)
385401
}
402+
403+
const SEPARATOR_WIDTH: usize = 4;
404+
405+
// Check whether the error is "unknown field" and, if so, list the available fields sorted and at
406+
// least one per line, more if `CLIPPY_TERMINAL_WIDTH` is set and allows it.
407+
pub fn format_error(error: Box<dyn Error>) -> String {
408+
let s = error.to_string();
409+
410+
if_chain! {
411+
if error.downcast::<toml::de::Error>().is_ok();
412+
if let Some((prefix, mut fields, suffix)) = parse_unknown_field_message(&s);
413+
then {
414+
use fmt::Write;
415+
416+
fields.sort_unstable();
417+
418+
let (rows, column_widths) = calculate_dimensions(&fields);
419+
420+
let mut msg = String::from(prefix);
421+
for row in 0..rows {
422+
write!(msg, "\n").unwrap();
423+
for (column, column_width) in column_widths.iter().copied().enumerate() {
424+
let index = column * rows + row;
425+
let field = fields.get(index).copied().unwrap_or_default();
426+
write!(
427+
msg,
428+
"{:separator_width$}{:field_width$}",
429+
" ",
430+
field,
431+
separator_width = SEPARATOR_WIDTH,
432+
field_width = column_width
433+
)
434+
.unwrap();
435+
}
436+
}
437+
write!(msg, "\n{}", suffix).unwrap();
438+
msg
439+
} else {
440+
s
441+
}
442+
}
443+
}
444+
445+
// `parse_unknown_field_message` will become unnecessary if
446+
// https://github.com/alexcrichton/toml-rs/pull/364 is merged.
447+
fn parse_unknown_field_message(s: &str) -> Option<(&str, Vec<&str>, &str)> {
448+
// An "unknown field" message has the following form:
449+
// unknown field `UNKNOWN`, expected one of `FIELD0`, `FIELD1`, ..., `FIELDN` at line X column Y
450+
// ^^ ^^^^ ^^
451+
if_chain! {
452+
if s.starts_with("unknown field");
453+
let slices = s.split("`, `").collect::<Vec<_>>();
454+
let n = slices.len();
455+
if n >= 2;
456+
if let Some((prefix, first_field)) = slices[0].rsplit_once(" `");
457+
if let Some((last_field, suffix)) = slices[n - 1].split_once("` ");
458+
then {
459+
let fields = iter::once(first_field)
460+
.chain(slices[1..n - 1].iter().copied())
461+
.chain(iter::once(last_field))
462+
.collect::<Vec<_>>();
463+
Some((prefix, fields, suffix))
464+
} else {
465+
None
466+
}
467+
}
468+
}
469+
470+
fn calculate_dimensions(fields: &[&str]) -> (usize, Vec<usize>) {
471+
let columns = env::var("CLIPPY_TERMINAL_WIDTH")
472+
.ok()
473+
.and_then(|s| <usize as FromStr>::from_str(&s).ok())
474+
.map_or(1, |terminal_width| {
475+
let max_field_width = fields.iter().map(|field| field.len()).max().unwrap();
476+
cmp::max(1, terminal_width / (SEPARATOR_WIDTH + max_field_width))
477+
});
478+
479+
let rows = (fields.len() + (columns - 1)) / columns;
480+
481+
let column_widths = (0..columns)
482+
.map(|column| {
483+
if column < columns - 1 {
484+
(0..rows)
485+
.map(|row| {
486+
let index = column * rows + row;
487+
let field = fields.get(index).copied().unwrap_or_default();
488+
field.len()
489+
})
490+
.max()
491+
.unwrap()
492+
} else {
493+
// Avoid adding extra space to the last column.
494+
0
495+
}
496+
})
497+
.collect::<Vec<_>>();
498+
499+
(rows, column_widths)
500+
}

src/main.rs

+4
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,12 @@ impl ClippyCmd {
123123
.map(|arg| format!("{}__CLIPPY_HACKERY__", arg))
124124
.collect();
125125

126+
// Currently, `CLIPPY_TERMINAL_WIDTH` is used only to format "unknown field" error messages.
127+
let terminal_width = termize::dimensions().map_or(0, |(w, _)| w);
128+
126129
cmd.env("RUSTC_WORKSPACE_WRAPPER", Self::path())
127130
.env("CLIPPY_ARGS", clippy_args)
131+
.env("CLIPPY_TERMINAL_WIDTH", terminal_width.to_string())
128132
.arg(self.cargo_subcommand)
129133
.args(&self.args);
130134

Original file line numberDiff line numberDiff line change
@@ -1,4 +1,43 @@
1-
error: error reading Clippy's configuration file `$DIR/clippy.toml`: unknown field `foobar`, expected one of `avoid-breaking-exported-api`, `msrv`, `blacklisted-names`, `cognitive-complexity-threshold`, `cyclomatic-complexity-threshold`, `doc-valid-idents`, `too-many-arguments-threshold`, `type-complexity-threshold`, `single-char-binding-names-threshold`, `too-large-for-stack`, `enum-variant-name-threshold`, `enum-variant-size-threshold`, `verbose-bit-mask-threshold`, `literal-representation-threshold`, `trivial-copy-size-limit`, `pass-by-value-size-limit`, `too-many-lines-threshold`, `array-size-threshold`, `vec-box-size-threshold`, `max-trait-bounds`, `max-struct-bools`, `max-fn-params-bools`, `warn-on-all-wildcard-imports`, `disallowed-methods`, `disallowed-types`, `unreadable-literal-lint-fractions`, `upper-case-acronyms-aggressive`, `cargo-ignore-publish`, `standard-macro-braces`, `enforced-import-renames`, `allowed-scripts`, `enable-raw-pointer-heuristic-for-send`, `max-suggested-slice-pattern-length`, `await-holding-invalid-types`, `max-include-file-size`, `allow-expect-in-tests`, `allow-unwrap-in-tests`, `third-party` at line 5 column 1
1+
error: error reading Clippy's configuration file `$DIR/clippy.toml`: unknown field `foobar`, expected one of
2+
allow-expect-in-tests
3+
allow-unwrap-in-tests
4+
allowed-scripts
5+
array-size-threshold
6+
avoid-breaking-exported-api
7+
await-holding-invalid-types
8+
blacklisted-names
9+
cargo-ignore-publish
10+
cognitive-complexity-threshold
11+
cyclomatic-complexity-threshold
12+
disallowed-methods
13+
disallowed-types
14+
doc-valid-idents
15+
enable-raw-pointer-heuristic-for-send
16+
enforced-import-renames
17+
enum-variant-name-threshold
18+
enum-variant-size-threshold
19+
literal-representation-threshold
20+
max-fn-params-bools
21+
max-include-file-size
22+
max-struct-bools
23+
max-suggested-slice-pattern-length
24+
max-trait-bounds
25+
msrv
26+
pass-by-value-size-limit
27+
single-char-binding-names-threshold
28+
standard-macro-braces
29+
third-party
30+
too-large-for-stack
31+
too-many-arguments-threshold
32+
too-many-lines-threshold
33+
trivial-copy-size-limit
34+
type-complexity-threshold
35+
unreadable-literal-lint-fractions
36+
upper-case-acronyms-aggressive
37+
vec-box-size-threshold
38+
verbose-bit-mask-threshold
39+
warn-on-all-wildcard-imports
40+
at line 5 column 1
241

342
error: aborting due to previous error
443

0 commit comments

Comments
 (0)