Lints and suggestions for the Nix programming language
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

rework cli, fix is now a flag, implement dry-run mode

Akshay b09f1f95 5de0ba05

+120 -102
+13 -25
bin/src/config.rs
··· 17 17 pub struct Opts { 18 18 /// File or directory to run statix on 19 19 #[clap(default_value = ".")] 20 - target: String, 20 + pub target: String, 21 21 22 - // /// Path to statix config 23 - // #[clap(short, long, default_value = ".statix.toml")] 24 - // config: String, 25 - /// Regex of file patterns to not lint 22 + /// Globs of file patterns to skip 26 23 #[clap(short, long)] 27 - ignore: Vec<String>, 24 + pub ignore: Vec<String>, 28 25 29 - /// Output format. Supported values: json, errfmt 26 + /// Output format. 27 + /// Supported values: errfmt, json (on feature flag only) 30 28 #[clap(short = 'o', long)] 31 29 format: Option<OutFormat>, 32 30 33 - #[clap(subcommand)] 34 - pub subcmd: Option<SubCommand>, 35 - } 36 - 37 - #[derive(Clap, Debug)] 38 - #[clap(version = "0.1.0", author = "Akshay <nerdy@peppe.rs>")] 39 - pub enum SubCommand { 40 31 /// Find and fix issues raised by statix 41 - Fix(Fix), 42 - } 32 + #[clap(short = 'f', long)] 33 + pub fix: bool, 43 34 44 - #[derive(Clap, Debug)] 45 - pub struct Fix { 46 - /// Do not write to files, display a diff instead 35 + /// Do not fix files in place, display a diff instead 47 36 #[clap(short = 'd', long = "dry-run")] 48 37 diff_only: bool, 49 38 } 50 39 40 + 51 41 #[derive(Debug, Copy, Clone)] 52 42 pub enum OutFormat { 43 + #[cfg(feature = "json")] 53 44 Json, 54 45 Errfmt, 55 46 StdErr, ··· 66 57 67 58 fn from_str(value: &str) -> Result<Self, Self::Err> { 68 59 match value.to_ascii_lowercase().as_str() { 69 - "json" => Ok(Self::Json), 60 + #[cfg(feature = "json")] "json" => Ok(Self::Json), 70 61 "errfmt" => Ok(Self::Errfmt), 71 62 "stderr" => Ok(Self::StdErr), 63 + "json" => Err("statix was not compiled with the `json` feature flag"), 72 64 _ => Err("unknown output format, try: json, errfmt"), 73 65 } 74 66 } ··· 122 114 .filter(|path| !ignores.is_match(path)) 123 115 .collect(); 124 116 125 - let diff_only = match opts.subcmd { 126 - Some(SubCommand::Fix(f)) => f.diff_only, 127 - _ => false, 128 - }; 129 - 117 + let diff_only = opts.diff_only; 130 118 Ok(Self { files, diff_only }) 131 119 } 132 120
+2
bin/src/err.rs
··· 23 23 pub enum FixErr { 24 24 #[error("[{0}] syntax error: {1}")] 25 25 Parse(PathBuf, ParseError), 26 + #[error("path error: {0}")] 27 + InvalidPath(#[from] io::Error), 26 28 } 27 29 28 30 #[derive(Error, Debug)]
+26 -34
bin/src/fix.rs
··· 65 65 66 66 impl<'a> FixResult<'a> { 67 67 fn empty(src: Source<'a>) -> Self { 68 - Self { src, fixed: vec![] } 68 + Self { src, fixed: Vec::new() } 69 69 } 70 70 } 71 71 72 - fn next(mut src: Source) -> Result<FixResult, RnixParseErr> { 73 - let all_reports = collect_fixes(&src)?; 74 - 75 - if all_reports.is_empty() { 76 - return Ok(FixResult::empty(src)); 77 - } 72 + impl<'a> Iterator for FixResult<'a> { 73 + type Item = FixResult<'a>; 74 + fn next(&mut self) -> Option<Self::Item> { 75 + let all_reports = collect_fixes(&self.src).ok()?; 76 + if all_reports.is_empty() { 77 + return None; 78 + } 78 79 79 - let reordered = reorder(all_reports); 80 + let reordered = reorder(all_reports); 81 + let fixed = reordered 82 + .iter() 83 + .map(|r| Fixed { 84 + at: r.range(), 85 + code: r.code, 86 + }) 87 + .collect::<Vec<_>>(); 88 + for report in reordered { 89 + report.apply(self.src.to_mut()); 90 + } 80 91 81 - let fixed = reordered 82 - .iter() 83 - .map(|r| Fixed { 84 - at: r.range(), 85 - code: r.code, 92 + Some(FixResult { 93 + src: self.src.clone(), 94 + fixed 86 95 }) 87 - .collect::<Vec<_>>(); 88 - for report in reordered { 89 - report.apply(src.to_mut()); 90 96 } 91 - 92 - Ok(FixResult { 93 - src, 94 - fixed 95 - }) 96 97 } 97 98 98 - pub fn fix(src: &str) -> Result<FixResult, RnixParseErr> { 99 + pub fn fix(src: &str) -> Option<FixResult> { 99 100 let src = Cow::from(src); 100 - let _ = rnix::parse(&src).as_result()?; 101 - let mut initial = FixResult::empty(src); 102 - 103 - while let Ok(next_result) = next(initial.src) { 104 - if next_result.fixed.is_empty() { 105 - return Ok(next_result); 106 - } else { 107 - initial = FixResult::empty(next_result.src); 108 - } 109 - } 110 - 111 - unreachable!("a fix caused a syntax error, please report a bug"); 101 + let _ = rnix::parse(&src).as_result().ok()?; 102 + let initial = FixResult::empty(src); 103 + initial.into_iter().last() 112 104 }
+34 -39
bin/src/main.rs
··· 6 6 7 7 use std::io; 8 8 9 - use crate::{ 10 - err::{FixErr, StatixErr}, 11 - traits::WriteDiagnostic, 12 - }; 9 + use crate::{err::{StatixErr, FixErr}, traits::WriteDiagnostic}; 13 10 14 11 use clap::Clap; 15 - use config::{FixConfig, LintConfig, Opts, SubCommand}; 12 + use config::{FixConfig, LintConfig, Opts}; 16 13 use similar::TextDiff; 17 14 18 15 fn _main() -> Result<(), StatixErr> { 19 16 let opts = Opts::parse(); 20 - match opts.subcmd { 21 - Some(SubCommand::Fix(_)) => { 22 - let fix_config = FixConfig::from_opts(opts)?; 23 - let vfs = fix_config.vfs()?; 24 - for entry in vfs.iter() { 25 - match fix::fix(entry.contents) { 26 - Ok(fix_result) => { 27 - let text_diff = TextDiff::from_lines(entry.contents, &fix_result.src); 28 - let old_file = format!("{}", entry.file_path.display()); 29 - let new_file = format!("{} [fixed]", entry.file_path.display()); 30 - println!( 31 - "{}", 32 - text_diff 33 - .unified_diff() 34 - .context_radius(4) 35 - .header(&old_file, &new_file) 36 - ); 37 - } 38 - Err(e) => eprintln!("{}", FixErr::Parse(entry.file_path.to_path_buf(), e)), 17 + if opts.fix { 18 + let fix_config = FixConfig::from_opts(opts)?; 19 + let vfs = fix_config.vfs()?; 20 + for entry in vfs.iter() { 21 + if let Some(fix_result) = fix::fix(entry.contents) { 22 + if fix_config.diff_only { 23 + let text_diff = TextDiff::from_lines(entry.contents, &fix_result.src); 24 + let old_file = format!("{}", entry.file_path.display()); 25 + let new_file = format!("{} [fixed]", entry.file_path.display()); 26 + println!( 27 + "{}", 28 + text_diff 29 + .unified_diff() 30 + .context_radius(4) 31 + .header(&old_file, &new_file) 32 + ); 33 + } else { 34 + let path = entry.file_path; 35 + std::fs::write(path, &*fix_result.src).map_err(FixErr::InvalidPath)?; 39 36 } 40 37 } 41 38 } 42 - None => { 43 - let lint_config = LintConfig::from_opts(opts)?; 44 - let vfs = lint_config.vfs()?; 45 - let (lints, errors): (Vec<_>, Vec<_>) = 46 - vfs.iter().map(lint::lint).partition(Result::is_ok); 47 - let lint_results = lints.into_iter().map(Result::unwrap); 48 - let errors = errors.into_iter().map(Result::unwrap_err); 39 + } else { 40 + let lint_config = LintConfig::from_opts(opts)?; 41 + let vfs = lint_config.vfs()?; 42 + let (lints, errors): (Vec<_>, Vec<_>) = vfs.iter().map(lint::lint).partition(Result::is_ok); 43 + let lint_results = lints.into_iter().map(Result::unwrap); 44 + let errors = errors.into_iter().map(Result::unwrap_err); 49 45 50 - let mut stderr = io::stderr(); 51 - lint_results.for_each(|r| { 52 - stderr.write(&r, &vfs, lint_config.format).unwrap(); 53 - }); 54 - errors.for_each(|e| { 55 - eprintln!("{}", e); 56 - }); 57 - } 46 + let mut stdout = io::stdout(); 47 + lint_results.for_each(|r| { 48 + stdout.write(&r, &vfs, lint_config.format).unwrap(); 49 + }); 50 + errors.for_each(|e| { 51 + eprintln!("{}", e); 52 + }); 58 53 } 59 54 Ok(()) 60 55 }
+45 -4
readme.md
··· 1 - ## statix 1 + # statix 2 + 3 + > Lints and suggestions for the Nix programming language. 2 4 3 - `statix` intends to be a static analysis tool for the 4 - Nix programming language. 5 + `statix` highlights antipatterns in Nix code. `statix fix` 6 + can fix several such occurrences. 5 7 6 8 For the time-being, `statix` works only with ASTs 7 9 produced by the `rnix-parser` crate and does not evaluate 8 10 any nix code (imports, attr sets etc.). 9 11 12 + ## Installation 13 + 14 + `statix` is available via a nix flake: 15 + 16 + ``` 17 + nix run git+https://git.peppe.rs/languages/statix 18 + 19 + # or 20 + 21 + nix build git+https://git.peppe.rs/languages/statix 22 + ./result/bin/statix --help 23 + ``` 24 + 25 + ## Usage 26 + 27 + ``` 28 + statix 0.1.0 29 + 30 + Akshay <nerdy@peppe.rs> 31 + 32 + Lints and suggestions for the Nix programming language 33 + 34 + USAGE: 35 + statix [FLAGS] [OPTIONS] [--] [TARGET] 36 + 37 + ARGS: 38 + <TARGET> File or directory to run statix on [default: .] 39 + 40 + FLAGS: 41 + -d, --dry-run Do not fix files in place, display a diff instead 42 + -f, --fix Find and fix issues raised by statix 43 + -h, --help Print help information 44 + -V, --version Print version information 45 + 46 + OPTIONS: 47 + -i, --ignore <IGNORE>... Globs of file patterns to skip 48 + -o, --format <FORMAT> Output format. Supported values: errfmt, json (on feature flag only) 49 + ``` 50 + 10 51 ## Architecture 11 52 12 53 `statix` has the following components: ··· 37 78 ## TODO 38 79 39 80 - Offline documentation for each lint 40 - - Automatically fix all lints from suggestions generated 41 81 - Test suite for lints and suggestions 82 + - Output singleline/errfmt + vim plugin