introduce --config flag

This commit is contained in:
Akshay 2021-11-20 18:56:26 +05:30
parent 4e063b2abc
commit 2b6012a79c
26 changed files with 231 additions and 103 deletions

10
Cargo.lock generated
View File

@ -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"

View File

@ -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" ]

View File

@ -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", default_value = ".")]
pub conf_path: PathBuf,
/// 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::<Vec<_>>())
}
}
pub fn lints(&self) -> Result<LintMap, ConfigErr> {
lints(&self.conf_path)
}
}
#[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", default_value = ".")]
pub conf_path: PathBuf,
/// 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<LintMap, ConfigErr> {
lints(&self.conf_path)
}
}
#[derive(Parser, Debug)]
@ -181,50 +204,6 @@ pub struct Explain {
pub target: u32,
}
fn parse_line_col(src: &str) -> Result<(usize, usize), ConfigErr> {
let parts = src.split(',');
match parts.collect::<Vec<_>>().as_slice() {
[line, col] => {
let l = line
.parse::<usize>()
.map_err(|_| ConfigErr::InvalidPosition(src.to_owned()))?;
let c = col
.parse::<usize>()
.map_err(|_| ConfigErr::InvalidPosition(src.to_owned()))?;
Ok((l, c))
}
_ => Err(ConfigErr::InvalidPosition(src.to_owned())),
}
}
fn parse_warning_code(src: &str) -> Result<u32, ConfigErr> {
let mut char_stream = src.chars();
let severity = char_stream
.next()
.ok_or_else(|| ConfigErr::InvalidWarningCode(src.to_owned()))?
.to_ascii_lowercase();
match severity {
'w' => char_stream
.collect::<String>()
.parse::<u32>()
.map_err(|_| ConfigErr::InvalidWarningCode(src.to_owned())),
_ => Ok(0),
}
}
fn vfs(files: Vec<PathBuf>) -> Result<ReadOnlyVfs, ConfigErr> {
let mut vfs = ReadOnlyVfs::default();
for file in files.iter() {
if let Ok(data) = fs::read_to_string(&file) {
let _id = vfs.alloc_file_id(&file);
vfs.set_file_contents(&file, data.as_bytes());
} else {
println!("{} contains non-utf8 content", file.display());
};
}
Ok(vfs)
}
#[derive(Debug, Copy, Clone)]
pub enum OutFormat {
#[cfg(feature = "json")]
@ -269,3 +248,100 @@ impl FromStr for OutFormat {
}
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ConfFile {
disabled: Vec<String>,
}
impl Default for ConfFile {
fn default() -> Self {
let disabled = vec![];
Self { disabled }
}
}
impl ConfFile {
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, ConfigErr> {
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)
})
}
pub fn discover<P: AsRef<Path>>(path: P) -> Result<Self, ConfigErr> {
let cannonical_path = fs::canonicalize(path.as_ref()).map_err(ConfigErr::InvalidPath)?;
for p in cannonical_path.ancestors() {
let statix_toml_path = p.with_file_name("statix.toml");
if statix_toml_path.exists() {
return Self::from_path(statix_toml_path);
};
}
Ok(Self::default())
}
pub fn dump(&self) -> String {
toml::ser::to_string_pretty(&self).unwrap()
}
}
fn parse_line_col(src: &str) -> Result<(usize, usize), ConfigErr> {
let parts = src.split(',');
match parts.collect::<Vec<_>>().as_slice() {
[line, col] => {
let do_parse = |val: &str| {
val.parse::<usize>()
.map_err(|_| ConfigErr::InvalidPosition(src.to_owned()))
};
let l = do_parse(line)?;
let c = do_parse(col)?;
Ok((l, c))
}
_ => Err(ConfigErr::InvalidPosition(src.to_owned())),
}
}
fn parse_warning_code(src: &str) -> Result<u32, ConfigErr> {
let mut char_stream = src.chars();
let severity = char_stream
.next()
.ok_or_else(|| ConfigErr::InvalidWarningCode(src.to_owned()))?
.to_ascii_lowercase();
match severity {
'w' => char_stream
.collect::<String>()
.parse::<u32>()
.map_err(|_| ConfigErr::InvalidWarningCode(src.to_owned())),
_ => Ok(0),
}
}
fn vfs(files: Vec<PathBuf>) -> Result<ReadOnlyVfs, ConfigErr> {
let mut vfs = ReadOnlyVfs::default();
for file in files.iter() {
if let Ok(data) = fs::read_to_string(&file) {
let _id = vfs.alloc_file_id(&file);
vfs.set_file_contents(&file, data.as_bytes());
} else {
println!("{} contains non-utf8 content", file.display());
};
}
Ok(vfs)
}
fn lints(conf_path: &PathBuf) -> Result<LintMap, ConfigErr> {
let config_file = ConfFile::discover(conf_path)?;
Ok(utils::lint_map_of(
(&*LINTS)
.into_iter()
.filter(|l| !config_file.disabled.iter().any(|check| check == l.name()))
.cloned()
.collect::<Vec<_>>()
.as_slice(),
))
}

View File

@ -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)]

View File

@ -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)

View File

@ -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<Fixed>,
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)

View File

@ -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<Vec<Report>, RnixParseErr> {
fn collect_fixes(source: &str, lints: &LintMap) -> Result<Vec<Report>, 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<Report>) -> Vec<Report> {
impl<'a> Iterator for FixResult<'a> {
type Item = FixResult<'a>;
fn next(&mut self) -> Option<Self::Item> {
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<FixResult> {
pub fn all_with<'a>(src: &'a str, lints: &'a LintMap) -> Option<FixResult<'a>> {
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()
}

View File

@ -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<TextSize, SingleFix
fn find(offset: TextSize, src: &str) -> Result<Report, SingleFixErr> {
// 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))

View File

@ -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<SyntaxKind, Vec<&'static Box<dyn Lint>>>;

View File

@ -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<Report>,
}
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();
});

24
bin/src/utils.rs Normal file
View File

@ -0,0 +1,24 @@
use std::collections::HashMap;
use lib::{Lint, LINTS};
use rnix::SyntaxKind;
pub fn lint_map_of(
lints: &[&'static Box<dyn Lint>],
) -> HashMap<SyntaxKind, Vec<&'static Box<dyn Lint>>> {
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<SyntaxKind, Vec<&'static Box<dyn Lint>>> {
lint_map_of(&*LINTS)
}

View File

@ -97,7 +97,7 @@
in
pkgs.mkShell {
nativeBuildInputs = [
pkgs.cargo-watch
pkgs.bacon
pkgs.cargo-insta
rust-analyzer
toolchain

View File

@ -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<SyntaxKind, Vec<&'static Box<dyn $crate::Lint>>> = {
let mut map = HashMap::new();
pub static ref LINTS: Vec<&'static Box<dyn $crate::Lint>> = {
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
};
}
}

View File

@ -1,6 +1,6 @@
use crate::lint_map;
use crate::lints;
lint_map! {
lints! {
bool_comparison,
empty_let_in,
manual_inherit,

View File

@ -37,7 +37,7 @@ use rowan::Direction;
/// a + b
/// ```
#[lint(
name = "collapsible let in",
name = "collapsible_let_in",
note = "These let-in expressions are collapsible",
code = 6,
match_with = SyntaxKind::NODE_LET_IN

View File

@ -27,7 +27,7 @@ use rnix::{
/// e == null
/// ```
#[lint(
name = "deprecated isNull",
name = "deprecated_is_null",
note = "Found usage of deprecated builtin isNull",
code = 13,
match_with = SyntaxKind::NODE_APPLY

View File

@ -26,7 +26,7 @@ use rnix::{
/// pkgs.statix
/// ```
#[lint(
name = "empty let-in",
name = "empty_let_in",
note = "Useless let-in expression",
code = 2,
match_with = SyntaxKind::NODE_LET_IN

View File

@ -35,7 +35,7 @@ use rnix::{
/// };
/// ```
#[lint(
name = "empty pattern",
name = "empty_pattern",
note = "Found empty pattern in function argument",
code = 10,
match_with = SyntaxKind::NODE_PATTERN

View File

@ -34,7 +34,7 @@ use rnix::{
/// map double [ 1 2 3 ]
/// ```
#[lint(
name = "eta reduction",
name = "eta_reduction",
note = "This function expression is eta reducible",
code = 7,
match_with = SyntaxKind::NODE_LAMBDA

View File

@ -36,7 +36,7 @@ use rnix::{
/// }.body
/// ```
#[lint(
name = "legacy let syntax",
name = "legacy_let_syntax",
note = "Using undocumented `let` syntax",
code = 5,
match_with = SyntaxKind::NODE_LEGACY_LET

View File

@ -32,7 +32,7 @@ use rnix::{
/// { inherit a; b = 3; }
/// ```
#[lint(
name = "manual inherit",
name = "manual_inherit",
note = "Assignment instead of inherit",
code = 3,
match_with = SyntaxKind::NODE_KEY_VALUE

View File

@ -32,7 +32,7 @@ use rnix::{
/// null
/// ```
#[lint(
name = "manual inherit from",
name = "manual_inherit_from",
note = "Assignment instead of inherit from",
code = 4,
match_with = SyntaxKind::NODE_KEY_VALUE

View File

@ -27,7 +27,7 @@ use rnix::{
/// inputs: inputs.nixpkgs
/// ```
#[lint(
name = "redundant pattern bind",
name = "redundant_pattern_bind",
note = "Found redundant pattern bind in function argument",
code = 11,
match_with = SyntaxKind::NODE_PATTERN

View File

@ -32,7 +32,7 @@ use rnix::{
/// pkgs
/// ```
#[lint(
name = "unquoted splice",
name = "unquoted_splice",
note = "Found unquoted splice expression",
code = 9,
match_with = SyntaxKind::NODE_DYNAMIC

View File

@ -38,7 +38,7 @@ use rnix::{types::TypedNode, NodeOrToken, SyntaxElement, SyntaxKind};
/// }
/// ```
#[lint(
name = "unquoted uri",
name = "unquoted_uri",
note = "Found unquoted URI expression",
code = 12,
match_with = SyntaxKind::TOKEN_URI

View File

@ -33,7 +33,7 @@ use rnix::{
/// 2 + 3
/// ```
#[lint(
name = "useless parens",
name = "useless_parens",
note = "These parentheses can be omitted",
code = 8,
match_with = [