From 29c0c2bb09b9b33f1e4e81db0e46bdd089dc2847 Mon Sep 17 00:00:00 2001 From: mcarton Date: Sun, 21 Feb 2016 20:11:32 +0100 Subject: [PATCH] Start implementing a configuration file --- Cargo.toml | 1 + src/conf.rs | 184 +++++++++++++++++++++++ src/lib.rs | 26 +++- tests/compile-fail/conf_bad_arg.rs | 6 + tests/compile-fail/conf_non_existant.rs | 6 + tests/compile-fail/conf_unknown_key.rs | 6 + tests/compile-fail/conf_unknown_key.toml | 1 + 7 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 src/conf.rs create mode 100644 tests/compile-fail/conf_bad_arg.rs create mode 100644 tests/compile-fail/conf_non_existant.rs create mode 100644 tests/compile-fail/conf_unknown_key.rs create mode 100644 tests/compile-fail/conf_unknown_key.toml diff --git a/Cargo.toml b/Cargo.toml index 853815e049f..a10164b5fcf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ plugin = true regex-syntax = "0.2.2" regex_macros = { version = "0.1.28", optional = true } semver = "0.2.1" +toml = "0.1" unicode-normalization = "0.1" [dev-dependencies] diff --git a/src/conf.rs b/src/conf.rs new file mode 100644 index 00000000000..e275bd701bd --- /dev/null +++ b/src/conf.rs @@ -0,0 +1,184 @@ +use std::{fmt, fs, io}; +use std::io::Read; +use syntax::{ast, codemap, ptr}; +use syntax::parse::token; +use toml; + +/// Get the configuration file from arguments. +pub fn conf_file(args: &[ptr::P]) -> Result, (&'static str, codemap::Span)> { + for arg in args { + match arg.node { + ast::MetaItemKind::Word(ref name) | ast::MetaItemKind::List(ref name, _) => { + if name == &"conf_file" { + return Err(("`conf_file` must be a named value", arg.span)); + } + } + ast::MetaItemKind::NameValue(ref name, ref value) => { + if name == &"conf_file" { + return if let ast::LitKind::Str(ref file, _) = value.node { + Ok(Some(file.clone())) + } else { + Err(("`conf_file` value must be a string", value.span)) + } + } + } + } + } + + Ok(None) +} + +/// Error from reading a configuration file. +#[derive(Debug)] +pub enum ConfError { + IoError(io::Error), + TomlError(Vec), + TypeError(&'static str, &'static str, &'static str), + UnknownKey(String), +} + +impl fmt::Display for ConfError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + ConfError::IoError(ref err) => { + err.fmt(f) + } + ConfError::TomlError(ref errs) => { + let mut first = true; + for err in errs { + if !first { + try!(", ".fmt(f)); + first = false; + } + + try!(err.fmt(f)); + } + + Ok(()) + } + ConfError::TypeError(ref key, ref expected, ref got) => { + write!(f, "`{}` is expected to be a `{}` but is a `{}`", key, expected, got) + } + ConfError::UnknownKey(ref key) => { + write!(f, "unknown key `{}`", key) + } + } + } +} + +impl From for ConfError { + fn from(e: io::Error) -> Self { + ConfError::IoError(e) + } +} + +macro_rules! define_Conf { + ($(($toml_name: tt, $rust_name: ident, $default: expr, $ty: ident),)+) => { + /// Type used to store lint configuration. + pub struct Conf { + $(pub $rust_name: $ty,)+ + } + + impl Default for Conf { + fn default() -> Conf { + Conf { + $($rust_name: $default,)+ + } + } + } + + impl Conf { + /// Set the property `name` (which must be the `toml` name) to the given value + #[allow(cast_sign_loss)] + fn set(&mut self, name: String, value: toml::Value) -> Result<(), ConfError> { + match name.as_str() { + $( + define_Conf!(PAT $toml_name) => { + if let Some(value) = define_Conf!(CONV $ty, value) { + self.$rust_name = value; + } + else { + return Err(ConfError::TypeError(define_Conf!(EXPR $toml_name), + stringify!($ty), + value.type_str())); + } + }, + )+ + _ => { + return Err(ConfError::UnknownKey(name)); + } + } + + Ok(()) + } + } + }; + + // hack to convert tts + (PAT $pat: pat) => { $pat }; + (EXPR $e: expr) => { $e }; + + // how to read the value? + (CONV i64, $value: expr) => { $value.as_integer() }; + (CONV u64, $value: expr) => { $value.as_integer().iter().filter_map(|&i| if i >= 0 { Some(i as u64) } else { None }).next() }; + (CONV String, $value: expr) => { $value.as_str().map(Into::into) }; + (CONV StringVec, $value: expr) => {{ + let slice = $value.as_slice(); + + if let Some(slice) = slice { + if slice.iter().any(|v| v.as_str().is_none()) { + None + } + else { + Some(slice.iter().map(|v| v.as_str().unwrap_or_else(|| unreachable!()).to_owned()).collect()) + } + } + else { + None + } + }}; +} + +/// To keep the `define_Conf!` macro simple +pub type StringVec = Vec; + +define_Conf! { + ("blacklisted-names", blacklisted_names, vec!["foo".to_owned(), "bar".to_owned(), "baz".to_owned()], StringVec), + ("cyclomatic-complexity-threshold", cyclomatic_complexity_threshold, 25, u64), + ("too-many-arguments-threshold", too_many_arguments_threshold, 6, u64), + ("type-complexity-threshold", type_complexity_threshold, 250, u64), +} + +/// Read the `toml` configuration file. The function will ignore “File not found” errors iif +/// `!must_exist`, in which case, it will return the default configuration. +pub fn read_conf(path: &str, must_exist: bool) -> Result { + let mut conf = Conf::default(); + + let file = match fs::File::open(path) { + Ok(mut file) => { + let mut buf = String::new(); + try!(file.read_to_string(&mut buf)); + buf + } + Err(ref err) if !must_exist && err.kind() == io::ErrorKind::NotFound => { + return Ok(conf); + } + Err(err) => { + return Err(err.into()); + } + }; + + let mut parser = toml::Parser::new(&file); + let toml = if let Some(toml) = parser.parse() { + toml + } + else { + return Err(ConfError::TomlError(parser.errors)); + }; + + for (key, value) in toml { + try!(conf.set(key, value)); + } + + Ok(conf) +} diff --git a/src/lib.rs b/src/lib.rs index 4b9d5d8c9c4..f3c3564796a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,8 @@ fn main() { #[macro_use] extern crate rustc_front; +extern crate toml; + // Only for the compile time checking of paths extern crate core; extern crate collections; @@ -35,6 +37,7 @@ fn main() { use rustc_plugin::Registry; +mod conf; pub mod consts; #[macro_use] pub mod utils; @@ -107,6 +110,27 @@ mod reexport { #[plugin_registrar] #[cfg_attr(rustfmt, rustfmt_skip)] pub fn plugin_registrar(reg: &mut Registry) { + let conferr = match conf::conf_file(reg.args()) { + Ok(Some(file_name)) => { + conf::read_conf(&file_name, true) + } + Ok(None) => { + conf::read_conf("Clippy.toml", false) + } + Err((err, span)) => { + reg.sess.struct_span_err(span, err).emit(); + return; + } + }; + + let conf = match conferr { + Ok(conf) => conf, + Err(err) => { + reg.sess.struct_err(&format!("error reading Clippy's configuration file: {}", err)).emit(); + return; + } + }; + reg.register_late_lint_pass(box types::TypePass); reg.register_late_lint_pass(box misc::TopLevelRefPass); reg.register_late_lint_pass(box misc::CmpNan); @@ -157,7 +181,7 @@ pub fn plugin_registrar(reg: &mut Registry) { reg.register_late_lint_pass(box map_clone::MapClonePass); reg.register_late_lint_pass(box temporary_assignment::TemporaryAssignmentPass); reg.register_late_lint_pass(box transmute::UselessTransmute); - reg.register_late_lint_pass(box cyclomatic_complexity::CyclomaticComplexity::new(25)); + reg.register_late_lint_pass(box cyclomatic_complexity::CyclomaticComplexity::new(conf.cyclomatic_complexity_threshold)); reg.register_late_lint_pass(box escape::EscapePass); reg.register_early_lint_pass(box misc_early::MiscEarly); reg.register_late_lint_pass(box misc::UsedUnderscoreBinding); diff --git a/tests/compile-fail/conf_bad_arg.rs b/tests/compile-fail/conf_bad_arg.rs new file mode 100644 index 00000000000..68b902719f6 --- /dev/null +++ b/tests/compile-fail/conf_bad_arg.rs @@ -0,0 +1,6 @@ +// error-pattern: `conf_file` must be a named value + +#![feature(plugin)] +#![plugin(clippy(conf_file))] + +fn main() {} diff --git a/tests/compile-fail/conf_non_existant.rs b/tests/compile-fail/conf_non_existant.rs new file mode 100644 index 00000000000..13ab7f6cebf --- /dev/null +++ b/tests/compile-fail/conf_non_existant.rs @@ -0,0 +1,6 @@ +// error-pattern: error reading Clippy's configuration file: No such file or directory + +#![feature(plugin)] +#![plugin(clippy(conf_file="./tests/compile-fail/non_existant_conf.toml"))] + +fn main() {} diff --git a/tests/compile-fail/conf_unknown_key.rs b/tests/compile-fail/conf_unknown_key.rs new file mode 100644 index 00000000000..02131d94d52 --- /dev/null +++ b/tests/compile-fail/conf_unknown_key.rs @@ -0,0 +1,6 @@ +// error-pattern: error reading Clippy's configuration file: unknown key `foobar` + +#![feature(plugin)] +#![plugin(clippy(conf_file="./tests/compile-fail/conf_unknown_key.toml"))] + +fn main() {} diff --git a/tests/compile-fail/conf_unknown_key.toml b/tests/compile-fail/conf_unknown_key.toml new file mode 100644 index 00000000000..df298ea78d4 --- /dev/null +++ b/tests/compile-fail/conf_unknown_key.toml @@ -0,0 +1 @@ +foobar = 42 -- 2.44.0