From b09f1f958423dee8c235f2eeb9c148b73936830f Mon Sep 17 00:00:00 2001 From: Akshay Date: Sun, 24 Oct 2021 13:26:37 +0530 Subject: rework cli, fix is now a flag, implement dry-run mode --- bin/src/config.rs | 38 ++++++++++------------------- bin/src/err.rs | 2 ++ bin/src/fix.rs | 60 ++++++++++++++++++++------------------------- bin/src/main.rs | 73 ++++++++++++++++++++++++++----------------------------- readme.md | 49 ++++++++++++++++++++++++++++++++++--- 5 files changed, 120 insertions(+), 102 deletions(-) diff --git a/bin/src/config.rs b/bin/src/config.rs index cb03a4b..6d7bc49 100644 --- a/bin/src/config.rs +++ b/bin/src/config.rs @@ -17,39 +17,30 @@ use crate::err::ConfigErr; pub struct Opts { /// File or directory to run statix on #[clap(default_value = ".")] - target: String, + pub target: String, - // /// Path to statix config - // #[clap(short, long, default_value = ".statix.toml")] - // config: String, - /// Regex of file patterns to not lint + /// Globs of file patterns to skip #[clap(short, long)] - ignore: Vec, + pub ignore: Vec, - /// Output format. Supported values: json, errfmt + /// Output format. + /// Supported values: errfmt, json (on feature flag only) #[clap(short = 'o', long)] format: Option, - #[clap(subcommand)] - pub subcmd: Option, -} - -#[derive(Clap, Debug)] -#[clap(version = "0.1.0", author = "Akshay ")] -pub enum SubCommand { /// Find and fix issues raised by statix - Fix(Fix), -} + #[clap(short = 'f', long)] + pub fix: bool, -#[derive(Clap, Debug)] -pub struct Fix { - /// Do not write to files, display a diff instead + /// Do not fix files in place, display a diff instead #[clap(short = 'd', long = "dry-run")] diff_only: bool, } + #[derive(Debug, Copy, Clone)] pub enum OutFormat { + #[cfg(feature = "json")] Json, Errfmt, StdErr, @@ -66,9 +57,10 @@ impl FromStr for OutFormat { fn from_str(value: &str) -> Result { match value.to_ascii_lowercase().as_str() { - "json" => Ok(Self::Json), + #[cfg(feature = "json")] "json" => Ok(Self::Json), "errfmt" => Ok(Self::Errfmt), "stderr" => Ok(Self::StdErr), + "json" => Err("statix was not compiled with the `json` feature flag"), _ => Err("unknown output format, try: json, errfmt"), } } @@ -122,11 +114,7 @@ impl FixConfig { .filter(|path| !ignores.is_match(path)) .collect(); - let diff_only = match opts.subcmd { - Some(SubCommand::Fix(f)) => f.diff_only, - _ => false, - }; - + let diff_only = opts.diff_only; Ok(Self { files, diff_only }) } diff --git a/bin/src/err.rs b/bin/src/err.rs index c9db4d5..727e0cc 100644 --- a/bin/src/err.rs +++ b/bin/src/err.rs @@ -23,6 +23,8 @@ pub enum LintErr { pub enum FixErr { #[error("[{0}] syntax error: {1}")] Parse(PathBuf, ParseError), + #[error("path error: {0}")] + InvalidPath(#[from] io::Error), } #[derive(Error, Debug)] diff --git a/bin/src/fix.rs b/bin/src/fix.rs index 478dbd9..d9087fe 100644 --- a/bin/src/fix.rs +++ b/bin/src/fix.rs @@ -65,48 +65,40 @@ pub struct Fixed { impl<'a> FixResult<'a> { fn empty(src: Source<'a>) -> Self { - Self { src, fixed: vec![] } + Self { src, fixed: Vec::new() } } } -fn next(mut src: Source) -> Result { - let all_reports = collect_fixes(&src)?; - - if all_reports.is_empty() { - return Ok(FixResult::empty(src)); - } +impl<'a> Iterator for FixResult<'a> { + type Item = FixResult<'a>; + fn next(&mut self) -> Option { + let all_reports = collect_fixes(&self.src).ok()?; + if all_reports.is_empty() { + return None; + } - let reordered = reorder(all_reports); + let reordered = reorder(all_reports); + let fixed = reordered + .iter() + .map(|r| Fixed { + at: r.range(), + code: r.code, + }) + .collect::>(); + for report in reordered { + report.apply(self.src.to_mut()); + } - let fixed = reordered - .iter() - .map(|r| Fixed { - at: r.range(), - code: r.code, + Some(FixResult { + src: self.src.clone(), + fixed }) - .collect::>(); - for report in reordered { - report.apply(src.to_mut()); } - - Ok(FixResult { - src, - fixed - }) } -pub fn fix(src: &str) -> Result { +pub fn fix(src: &str) -> Option { let src = Cow::from(src); - let _ = rnix::parse(&src).as_result()?; - let mut initial = FixResult::empty(src); - - while let Ok(next_result) = next(initial.src) { - if next_result.fixed.is_empty() { - return Ok(next_result); - } else { - initial = FixResult::empty(next_result.src); - } - } - - unreachable!("a fix caused a syntax error, please report a bug"); + let _ = rnix::parse(&src).as_result().ok()?; + let initial = FixResult::empty(src); + initial.into_iter().last() } diff --git a/bin/src/main.rs b/bin/src/main.rs index 6f0343e..d0f69a0 100644 --- a/bin/src/main.rs +++ b/bin/src/main.rs @@ -6,55 +6,50 @@ mod traits; use std::io; -use crate::{ - err::{FixErr, StatixErr}, - traits::WriteDiagnostic, -}; +use crate::{err::{StatixErr, FixErr}, traits::WriteDiagnostic}; use clap::Clap; -use config::{FixConfig, LintConfig, Opts, SubCommand}; +use config::{FixConfig, LintConfig, Opts}; use similar::TextDiff; fn _main() -> Result<(), StatixErr> { let opts = Opts::parse(); - match opts.subcmd { - Some(SubCommand::Fix(_)) => { - let fix_config = FixConfig::from_opts(opts)?; - let vfs = fix_config.vfs()?; - for entry in vfs.iter() { - match fix::fix(entry.contents) { - Ok(fix_result) => { - let text_diff = TextDiff::from_lines(entry.contents, &fix_result.src); - let old_file = format!("{}", entry.file_path.display()); - let new_file = format!("{} [fixed]", entry.file_path.display()); - println!( - "{}", - text_diff - .unified_diff() - .context_radius(4) - .header(&old_file, &new_file) - ); - } - Err(e) => eprintln!("{}", FixErr::Parse(entry.file_path.to_path_buf(), e)), + if opts.fix { + let fix_config = FixConfig::from_opts(opts)?; + let vfs = fix_config.vfs()?; + for entry in vfs.iter() { + if let Some(fix_result) = fix::fix(entry.contents) { + if fix_config.diff_only { + let text_diff = TextDiff::from_lines(entry.contents, &fix_result.src); + let old_file = format!("{}", entry.file_path.display()); + let new_file = format!("{} [fixed]", entry.file_path.display()); + println!( + "{}", + text_diff + .unified_diff() + .context_radius(4) + .header(&old_file, &new_file) + ); + } else { + let path = entry.file_path; + std::fs::write(path, &*fix_result.src).map_err(FixErr::InvalidPath)?; } } } - None => { - let lint_config = LintConfig::from_opts(opts)?; - let vfs = lint_config.vfs()?; - let (lints, errors): (Vec<_>, Vec<_>) = - vfs.iter().map(lint::lint).partition(Result::is_ok); - let lint_results = lints.into_iter().map(Result::unwrap); - let errors = errors.into_iter().map(Result::unwrap_err); + } else { + let lint_config = LintConfig::from_opts(opts)?; + let vfs = lint_config.vfs()?; + let (lints, errors): (Vec<_>, Vec<_>) = vfs.iter().map(lint::lint).partition(Result::is_ok); + let lint_results = lints.into_iter().map(Result::unwrap); + let errors = errors.into_iter().map(Result::unwrap_err); - let mut stderr = io::stderr(); - lint_results.for_each(|r| { - stderr.write(&r, &vfs, lint_config.format).unwrap(); - }); - errors.for_each(|e| { - eprintln!("{}", e); - }); - } + let mut stdout = io::stdout(); + lint_results.for_each(|r| { + stdout.write(&r, &vfs, lint_config.format).unwrap(); + }); + errors.for_each(|e| { + eprintln!("{}", e); + }); } Ok(()) } diff --git a/readme.md b/readme.md index 9ccb66e..68265cc 100644 --- a/readme.md +++ b/readme.md @@ -1,12 +1,53 @@ -## statix +# statix -`statix` intends to be a static analysis tool for the -Nix programming language. +> Lints and suggestions for the Nix programming language. + +`statix` highlights antipatterns in Nix code. `statix fix` +can fix several such occurrences. For the time-being, `statix` works only with ASTs produced by the `rnix-parser` crate and does not evaluate any nix code (imports, attr sets etc.). +## Installation + +`statix` is available via a nix flake: + +``` +nix run git+https://git.peppe.rs/languages/statix + +# or + +nix build git+https://git.peppe.rs/languages/statix +./result/bin/statix --help +``` + +## Usage + +``` +statix 0.1.0 + +Akshay + +Lints and suggestions for the Nix programming language + +USAGE: + statix [FLAGS] [OPTIONS] [--] [TARGET] + +ARGS: + File or directory to run statix on [default: .] + +FLAGS: + -d, --dry-run Do not fix files in place, display a diff instead + -f, --fix Find and fix issues raised by statix + -h, --help Print help information + -V, --version Print version information + +OPTIONS: + -i, --ignore ... Globs of file patterns to skip + -o, --format Output format. Supported values: errfmt, json (on feature flag only) +``` + ## Architecture `statix` has the following components: @@ -37,5 +78,5 @@ their metadata. ## TODO - Offline documentation for each lint -- Automatically fix all lints from suggestions generated - Test suite for lints and suggestions +- Output singleline/errfmt + vim plugin -- cgit v1.2.3