From f27372061a0effe3b00d400f4e577b9d9e0ad4c0 Mon Sep 17 00:00:00 2001 From: Akshay Date: Sat, 20 Nov 2021 18:57:28 +0530 Subject: add config option to fix and check --- Cargo.lock | 10 ++++ bin/Cargo.toml | 10 ++-- bin/src/config.rs | 148 +++++++++++++++++++++++++++++++++++--------------- bin/src/err.rs | 2 + bin/src/explain.rs | 7 +-- bin/src/fix.rs | 12 ++-- bin/src/fix/all.rs | 18 +++--- bin/src/fix/single.rs | 8 +-- bin/src/lib.rs | 9 +++ bin/src/lint.rs | 17 ++++-- bin/src/utils.rs | 24 ++++++++ lib/src/lib.rs | 19 ++----- 12 files changed, 198 insertions(+), 86 deletions(-) create mode 100644 bin/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 08881fd..2da3c9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -520,6 +520,7 @@ dependencies = [ "similar 2.1.0", "strip-ansi-escapes", "thiserror", + "toml", "vfs", ] @@ -612,6 +613,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + [[package]] name = "unicase" version = "2.6.0" diff --git a/bin/Cargo.toml b/bin/Cargo.toml index 7c48083..c898792 100644 --- a/bin/Cargo.toml +++ b/bin/Cargo.toml @@ -23,14 +23,14 @@ thiserror = "1.0.30" similar = "2.1.0" vfs = { path = "../vfs" } lib = { path = "../lib" } - -[dependencies.serde_json] -version = "1.0.68" -optional = true +toml = "0.5.8" [dependencies.serde] version = "1.0.68" features = [ "derive" ] + +[dependencies.serde_json] +version = "1.0.68" optional = true [dev-dependencies] @@ -38,4 +38,4 @@ insta = "1.8.0" strip-ansi-escapes = "0.1.1" [features] -json = [ "lib/json-out", "serde_json", "serde" ] +json = [ "lib/json-out", "serde_json" ] diff --git a/bin/src/config.rs b/bin/src/config.rs index d3944ac..98b41da 100644 --- a/bin/src/config.rs +++ b/bin/src/config.rs @@ -1,8 +1,15 @@ -use std::{default::Default, fmt, fs, path::PathBuf, str::FromStr}; +use std::{ + default::Default, + fmt, fs, + path::{Path, PathBuf}, + str::FromStr, +}; -use crate::{dirs, err::ConfigErr}; +use crate::{dirs, err::ConfigErr, utils, LintMap}; use clap::Parser; +use lib::LINTS; +use serde::{Deserialize, Serialize}; use vfs::ReadOnlyVfs; #[derive(Parser, Debug)] @@ -43,6 +50,10 @@ pub struct Check { #[clap(short = 'o', long, default_value_t, parse(try_from_str))] pub format: OutFormat, + /// Path to statix.toml + #[clap(short = 'c', long = "config")] + pub conf_path: Option, + /// Enable "streaming" mode, accept file on stdin, output diagnostics on stdout #[clap(short, long = "stdin")] pub streaming: bool, @@ -65,6 +76,10 @@ impl Check { vfs(files.collect::>()) } } + + pub fn lints(&self) -> Result { + lints(self.conf_path.as_ref()) + } } #[derive(Parser, Debug)] @@ -85,6 +100,10 @@ pub struct Fix { #[clap(short, long = "dry-run")] pub diff_only: bool, + /// Path to statix.toml + #[clap(short = 'c', long = "config")] + pub conf_path: Option, + /// Enable "streaming" mode, accept file on stdin, output diagnostics on stdout #[clap(short, long = "stdin")] pub streaming: bool, @@ -125,6 +144,10 @@ impl Fix { FixOut::Write } } + + pub fn lints(&self) -> Result { + lints(self.conf_path.as_ref()) + } } #[derive(Parser, Debug)] @@ -181,6 +204,72 @@ pub struct Explain { pub target: u32, } +#[derive(Debug, Copy, Clone)] +pub enum OutFormat { + #[cfg(feature = "json")] + Json, + Errfmt, + StdErr, +} + +impl Default for OutFormat { + fn default() -> Self { + OutFormat::StdErr + } +} + +impl fmt::Display for OutFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + #[cfg(feature = "json")] + Self::Json => "json", + Self::Errfmt => "errfmt", + Self::StdErr => "stderr", + } + ) + } +} + +impl FromStr for OutFormat { + type Err = &'static str; + + fn from_str(value: &str) -> Result { + match value.to_ascii_lowercase().as_str() { + #[cfg(feature = "json")] + "json" => Ok(Self::Json), + #[cfg(not(feature = "json"))] + "json" => Err("statix was not compiled with the `json` feature flag"), + "errfmt" => Ok(Self::Errfmt), + "stderr" => Ok(Self::StdErr), + _ => Err("unknown output format, try: json, errfmt"), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ConfFile { + checks: Vec, +} + +impl ConfFile { + pub fn from_path>(path: P) -> Result { + let path = path.as_ref(); + let config_file = fs::read_to_string(path).map_err(ConfigErr::InvalidPath)?; + toml::de::from_str(&config_file).map_err(|err| { + let pos = err.line_col(); + let msg = if let Some((line, col)) = pos { + format!("line {}, col {}", line, col) + } else { + "unknown".to_string() + }; + ConfigErr::ConfFileParse(msg) + }) + } +} + fn parse_line_col(src: &str) -> Result<(usize, usize), ConfigErr> { let parts = src.split(','); match parts.collect::>().as_slice() { @@ -225,47 +314,18 @@ fn vfs(files: Vec) -> Result { Ok(vfs) } -#[derive(Debug, Copy, Clone)] -pub enum OutFormat { - #[cfg(feature = "json")] - Json, - Errfmt, - StdErr, -} - -impl Default for OutFormat { - fn default() -> Self { - OutFormat::StdErr - } -} - -impl fmt::Display for OutFormat { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - #[cfg(feature = "json")] - Self::Json => "json", - Self::Errfmt => "errfmt", - Self::StdErr => "stderr", - } - ) - } -} - -impl FromStr for OutFormat { - type Err = &'static str; - - fn from_str(value: &str) -> Result { - match value.to_ascii_lowercase().as_str() { - #[cfg(feature = "json")] - "json" => Ok(Self::Json), - #[cfg(not(feature = "json"))] - "json" => Err("statix was not compiled with the `json` feature flag"), - "errfmt" => Ok(Self::Errfmt), - "stderr" => Ok(Self::StdErr), - _ => Err("unknown output format, try: json, errfmt"), - } +fn lints(conf_path: Option<&PathBuf>) -> Result { + if let Some(conf_path) = conf_path { + let config_file = ConfFile::from_path(conf_path)?; + Ok(utils::lint_map_of( + (&*LINTS) + .into_iter() + .filter(|l| config_file.checks.iter().any(|check| check == l.name())) + .cloned() + .collect::>() + .as_slice(), + )) + } else { + Ok(utils::lint_map()) } } diff --git a/bin/src/err.rs b/bin/src/err.rs index b776d9f..f44d27b 100644 --- a/bin/src/err.rs +++ b/bin/src/err.rs @@ -14,6 +14,8 @@ pub enum ConfigErr { InvalidPosition(String), #[error("unable to parse `{0}` as warning code")] InvalidWarningCode(String), + #[error("unable to parse config file, error at: `{0}`")] + ConfFileParse(String), } // #[derive(Error, Debug)] diff --git a/bin/src/explain.rs b/bin/src/explain.rs index de171aa..bcb8eed 100644 --- a/bin/src/explain.rs +++ b/bin/src/explain.rs @@ -1,11 +1,10 @@ -use crate::err::ExplainErr; - -use lib::LINTS; +use crate::{err::ExplainErr, utils}; pub fn explain(code: u32) -> Result<&'static str, ExplainErr> { + let lints = utils::lint_map(); match code { 0 => Ok("syntax error"), - _ => LINTS + _ => lints .values() .flatten() .find(|l| l.code() == code) diff --git a/bin/src/fix.rs b/bin/src/fix.rs index e4ea94d..4268567 100644 --- a/bin/src/fix.rs +++ b/bin/src/fix.rs @@ -1,19 +1,21 @@ use std::borrow::Cow; +use crate::LintMap; + use rnix::TextRange; mod all; -use all::all; +use all::all_with; mod single; use single::single; type Source<'a> = Cow<'a, str>; -#[derive(Debug)] pub struct FixResult<'a> { pub src: Source<'a>, pub fixed: Vec, + pub lints: &'a LintMap, } #[derive(Debug, Clone)] @@ -23,10 +25,11 @@ pub struct Fixed { } impl<'a> FixResult<'a> { - fn empty(src: Source<'a>) -> Self { + fn empty(src: Source<'a>, lints: &'a LintMap) -> Self { Self { src, fixed: Vec::new(), + lints, } } } @@ -43,8 +46,9 @@ pub mod main { pub fn all(fix_config: FixConfig) -> Result<(), StatixErr> { let vfs = fix_config.vfs()?; + let lints = fix_config.lints()?; for entry in vfs.iter() { - match (fix_config.out(), super::all(entry.contents)) { + match (fix_config.out(), super::all_with(entry.contents, &lints)) { (FixOut::Diff, fix_result) => { let src = fix_result .map(|r| r.src) diff --git a/bin/src/fix/all.rs b/bin/src/fix/all.rs index 7f04f2c..bbc39e8 100644 --- a/bin/src/fix/all.rs +++ b/bin/src/fix/all.rs @@ -1,18 +1,21 @@ use std::borrow::Cow; -use lib::{Report, LINTS}; +use lib::Report; use rnix::{parser::ParseError as RnixParseErr, WalkEvent}; -use crate::fix::{FixResult, Fixed}; +use crate::{ + fix::{FixResult, Fixed}, + LintMap, +}; -fn collect_fixes(source: &str) -> Result, RnixParseErr> { +fn collect_fixes(source: &str, lints: &LintMap) -> Result, RnixParseErr> { let parsed = rnix::parse(source).as_result()?; Ok(parsed .node() .preorder_with_tokens() .filter_map(|event| match event { - WalkEvent::Enter(child) => LINTS.get(&child.kind()).map(|rules| { + WalkEvent::Enter(child) => lints.get(&child.kind()).map(|rules| { rules .iter() .filter_map(|rule| rule.validate(&child)) @@ -54,7 +57,7 @@ fn reorder(mut reports: Vec) -> Vec { impl<'a> Iterator for FixResult<'a> { type Item = FixResult<'a>; fn next(&mut self) -> Option { - let all_reports = collect_fixes(&self.src).ok()?; + let all_reports = collect_fixes(&self.src, &self.lints).ok()?; if all_reports.is_empty() { return None; } @@ -74,13 +77,14 @@ impl<'a> Iterator for FixResult<'a> { Some(FixResult { src: self.src.clone(), fixed, + lints: self.lints, }) } } -pub fn all(src: &str) -> Option { +pub fn all_with<'a>(src: &'a str, lints: &'a LintMap) -> Option> { let src = Cow::from(src); let _ = rnix::parse(&src).as_result().ok()?; - let initial = FixResult::empty(src); + let initial = FixResult::empty(src, lints); initial.into_iter().last() } diff --git a/bin/src/fix/single.rs b/bin/src/fix/single.rs index b91dc45..d95cfda 100644 --- a/bin/src/fix/single.rs +++ b/bin/src/fix/single.rs @@ -1,10 +1,9 @@ use std::{borrow::Cow, convert::TryFrom}; -use lib::{Report, LINTS}; +use lib::Report; use rnix::{TextSize, WalkEvent}; -use crate::err::SingleFixErr; -use crate::fix::Source; +use crate::{err::SingleFixErr, fix::Source, utils}; pub struct SingleFixResult<'δ> { pub src: Source<'δ>, @@ -31,12 +30,13 @@ fn pos_to_byte(line: usize, col: usize, src: &str) -> Result Result { // we don't really need the source to form a completely parsed tree let parsed = rnix::parse(src); + let lints = utils::lint_map(); parsed .node() .preorder_with_tokens() .filter_map(|event| match event { - WalkEvent::Enter(child) => LINTS.get(&child.kind()).map(|rules| { + WalkEvent::Enter(child) => lints.get(&child.kind()).map(|rules| { rules .iter() .filter_map(|rule| rule.validate(&child)) diff --git a/bin/src/lib.rs b/bin/src/lib.rs index 49c1a41..0105334 100644 --- a/bin/src/lib.rs +++ b/bin/src/lib.rs @@ -5,3 +5,12 @@ pub mod explain; pub mod fix; pub mod lint; pub mod traits; + +mod utils; + +use std::collections::HashMap; + +use lib::Lint; +use rnix::SyntaxKind; + +pub type LintMap = HashMap>>; diff --git a/bin/src/lint.rs b/bin/src/lint.rs index 1138c23..3482d46 100644 --- a/bin/src/lint.rs +++ b/bin/src/lint.rs @@ -1,4 +1,6 @@ -use lib::{Report, LINTS}; +use crate::{utils, LintMap}; + +use lib::Report; use rnix::WalkEvent; use vfs::{FileId, VfsEntry}; @@ -8,18 +10,17 @@ pub struct LintResult { pub reports: Vec, } -pub fn lint(vfs_entry: VfsEntry) -> LintResult { +pub fn lint_with(vfs_entry: VfsEntry, lints: &LintMap) -> LintResult { let file_id = vfs_entry.file_id; let source = vfs_entry.contents; let parsed = rnix::parse(source); let error_reports = parsed.errors().into_iter().map(Report::from_parse_err); - let reports = parsed .node() .preorder_with_tokens() .filter_map(|event| match event { - WalkEvent::Enter(child) => LINTS.get(&child.kind()).map(|rules| { + WalkEvent::Enter(child) => lints.get(&child.kind()).map(|rules| { rules .iter() .filter_map(|rule| rule.validate(&child)) @@ -34,15 +35,21 @@ pub fn lint(vfs_entry: VfsEntry) -> LintResult { LintResult { file_id, reports } } +pub fn lint(vfs_entry: VfsEntry) -> LintResult { + lint_with(vfs_entry, &utils::lint_map()) +} + pub mod main { use std::io; - use super::lint; + use super::lint_with; use crate::{config::Check as CheckConfig, err::StatixErr, traits::WriteDiagnostic}; pub fn main(check_config: CheckConfig) -> Result<(), StatixErr> { let vfs = check_config.vfs()?; let mut stdout = io::stdout(); + let lints = check_config.lints()?; + let lint = |vfs_entry| lint_with(vfs_entry, &lints); vfs.iter().map(lint).for_each(|r| { stdout.write(&r, &vfs, check_config.format).unwrap(); }); diff --git a/bin/src/utils.rs b/bin/src/utils.rs new file mode 100644 index 0000000..747a761 --- /dev/null +++ b/bin/src/utils.rs @@ -0,0 +1,24 @@ +use std::collections::HashMap; + +use lib::{Lint, LINTS}; +use rnix::SyntaxKind; + +pub fn lint_map_of( + lints: &[&'static Box], +) -> HashMap>> { + let mut map = HashMap::new(); + for lint in lints.iter() { + let lint = *lint; + let matches = lint.match_kind(); + for m in matches { + map.entry(m) + .and_modify(|v: &mut Vec<_>| v.push(lint)) + .or_insert_with(|| vec![lint]); + } + } + map +} + +pub fn lint_map() -> HashMap>> { + lint_map_of(&*LINTS) +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 5347666..d96d9eb 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -255,31 +255,24 @@ pub trait Lint: Metadata + Explain + Rule + Send + Sync {} /// /// See `lints.rs` for usage. #[macro_export] -macro_rules! lint_map { +macro_rules! lints { ($($s:ident),*,) => { - lint_map!($($s),*); + lints!($($s),*); }; ($($s:ident),*) => { - use ::std::collections::HashMap; - use ::rnix::SyntaxKind; $( mod $s; )* ::lazy_static::lazy_static! { - pub static ref LINTS: HashMap>> = { - let mut map = HashMap::new(); + pub static ref LINTS: Vec<&'static Box> = { + let mut v = Vec::new(); $( { let temp_lint = &*$s::LINT; - let temp_matches = temp_lint.match_kind(); - for temp_match in temp_matches { - map.entry(temp_match) - .and_modify(|v: &mut Vec<_>| v.push(temp_lint)) - .or_insert_with(|| vec![temp_lint]); - } + v.push(temp_lint); } )* - map + v }; } } -- cgit v1.2.3