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