about summary refs log tree commit diff
path: root/clippy_lints/src/utils/conf.rs
diff options
context:
space:
mode:
Diffstat (limited to 'clippy_lints/src/utils/conf.rs')
-rw-r--r--clippy_lints/src/utils/conf.rs137
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)
+}