diff options
Diffstat (limited to 'clippy_lints/src/utils/conf.rs')
| -rw-r--r-- | clippy_lints/src/utils/conf.rs | 137 |
1 files changed, 130 insertions, 7 deletions
diff --git a/clippy_lints/src/utils/conf.rs b/clippy_lints/src/utils/conf.rs index 74b0168a179..cd4d16fe95f 100644 --- a/clippy_lints/src/utils/conf.rs +++ b/clippy_lints/src/utils/conf.rs @@ -6,7 +6,8 @@ use serde::de::{Deserializer, IgnoredAny, IntoDeserializer, MapAccess, Visitor}; use serde::Deserialize; use std::error::Error; use std::path::{Path, PathBuf}; -use std::{env, fmt, fs, io}; +use std::str::FromStr; +use std::{cmp, env, fmt, fs, io, iter}; /// Holds information used by `MISSING_ENFORCED_IMPORT_RENAMES` lint. #[derive(Clone, Debug, Deserialize)] @@ -43,18 +44,33 @@ pub enum DisallowedType { #[derive(Default)] pub struct TryConf { pub conf: Conf, - pub errors: Vec<String>, + pub errors: Vec<Box<dyn Error>>, } impl TryConf { - fn from_error(error: impl Error) -> Self { + fn from_error(error: impl Error + 'static) -> Self { Self { conf: Conf::default(), - errors: vec![error.to_string()], + errors: vec![Box::new(error)], } } } +#[derive(Debug)] +struct ConfError(String); + +impl fmt::Display for ConfError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + <String as fmt::Display>::fmt(&self.0, f) + } +} + +impl Error for ConfError {} + +fn conf_error(s: String) -> Box<dyn Error> { + Box::new(ConfError(s)) +} + macro_rules! define_Conf { ($( $(#[doc = $doc:literal])+ @@ -103,11 +119,11 @@ macro_rules! define_Conf { while let Some(name) = map.next_key::<&str>()? { match Field::deserialize(name.into_deserializer())? { $(Field::$name => { - $(errors.push(format!("deprecated field `{}`. {}", name, $dep));)? + $(errors.push(conf_error(format!("deprecated field `{}`. {}", name, $dep)));)? match map.next_value() { - Err(e) => errors.push(e.to_string()), + Err(e) => errors.push(conf_error(e.to_string())), Ok(value) => match $name { - Some(_) => errors.push(format!("duplicate field `{}`", name)), + Some(_) => errors.push(conf_error(format!("duplicate field `{}`", name))), None => $name = Some(value), } } @@ -316,6 +332,14 @@ define_Conf! { /// /// The maximum size of a file included via `include_bytes!()` or `include_str!()`, in bytes (max_include_file_size: u64 = 1_000_000), + /// Lint: EXPECT_USED. + /// + /// Whether `expect` should be allowed in test functions + (allow_expect_in_tests: bool = false), + /// Lint: UNWRAP_USED. + /// + /// Whether `unwrap` should be allowed in test functions + (allow_unwrap_in_tests: bool = false), } /// Search for the configuration file. @@ -375,3 +399,102 @@ pub fn read(path: &Path) -> TryConf { }; toml::from_str(&content).unwrap_or_else(TryConf::from_error) } + +const SEPARATOR_WIDTH: usize = 4; + +// Check whether the error is "unknown field" and, if so, list the available fields sorted and at +// least one per line, more if `CLIPPY_TERMINAL_WIDTH` is set and allows it. +pub fn format_error(error: Box<dyn Error>) -> String { + let s = error.to_string(); + + if_chain! { + if error.downcast::<toml::de::Error>().is_ok(); + if let Some((prefix, mut fields, suffix)) = parse_unknown_field_message(&s); + then { + use fmt::Write; + + fields.sort_unstable(); + + let (rows, column_widths) = calculate_dimensions(&fields); + + let mut msg = String::from(prefix); + for row in 0..rows { + write!(msg, "\n").unwrap(); + for (column, column_width) in column_widths.iter().copied().enumerate() { + let index = column * rows + row; + let field = fields.get(index).copied().unwrap_or_default(); + write!( + msg, + "{:separator_width$}{:field_width$}", + " ", + field, + separator_width = SEPARATOR_WIDTH, + field_width = column_width + ) + .unwrap(); + } + } + write!(msg, "\n{}", suffix).unwrap(); + msg + } else { + s + } + } +} + +// `parse_unknown_field_message` will become unnecessary if +// https://github.com/alexcrichton/toml-rs/pull/364 is merged. +fn parse_unknown_field_message(s: &str) -> Option<(&str, Vec<&str>, &str)> { + // An "unknown field" message has the following form: + // unknown field `UNKNOWN`, expected one of `FIELD0`, `FIELD1`, ..., `FIELDN` at line X column Y + // ^^ ^^^^ ^^ + if_chain! { + if s.starts_with("unknown field"); + let slices = s.split("`, `").collect::<Vec<_>>(); + let n = slices.len(); + if n >= 2; + if let Some((prefix, first_field)) = slices[0].rsplit_once(" `"); + if let Some((last_field, suffix)) = slices[n - 1].split_once("` "); + then { + let fields = iter::once(first_field) + .chain(slices[1..n - 1].iter().copied()) + .chain(iter::once(last_field)) + .collect::<Vec<_>>(); + Some((prefix, fields, suffix)) + } else { + None + } + } +} + +fn calculate_dimensions(fields: &[&str]) -> (usize, Vec<usize>) { + let columns = env::var("CLIPPY_TERMINAL_WIDTH") + .ok() + .and_then(|s| <usize as FromStr>::from_str(&s).ok()) + .map_or(1, |terminal_width| { + let max_field_width = fields.iter().map(|field| field.len()).max().unwrap(); + cmp::max(1, terminal_width / (SEPARATOR_WIDTH + max_field_width)) + }); + + let rows = (fields.len() + (columns - 1)) / columns; + + let column_widths = (0..columns) + .map(|column| { + if column < columns - 1 { + (0..rows) + .map(|row| { + let index = column * rows + row; + let field = fields.get(index).copied().unwrap_or_default(); + field.len() + }) + .max() + .unwrap() + } else { + // Avoid adding extra space to the last column. + 0 + } + }) + .collect::<Vec<_>>(); + + (rows, column_widths) +} |
