diff options
-rw-r--r-- | bin/src/config.rs | 38 | ||||
-rw-r--r-- | bin/src/err.rs | 2 | ||||
-rw-r--r-- | bin/src/fix.rs | 60 | ||||
-rw-r--r-- | bin/src/main.rs | 73 | ||||
-rw-r--r-- | 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; | |||
17 | pub struct Opts { | 17 | pub 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]>")] | ||
39 | pub 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 |
45 | pub 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)] |
52 | pub enum OutFormat { | 42 | pub 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 { | |||
23 | pub enum FixErr { | 23 | pub 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 | ||
66 | impl<'a> FixResult<'a> { | 66 | impl<'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 | ||
72 | fn next(mut src: Source) -> Result<FixResult, RnixParseErr> { | 72 | impl<'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 | ||
98 | pub fn fix(src: &str) -> Result<FixResult, RnixParseErr> { | 99 | pub 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 | ||
7 | use std::io; | 7 | use std::io; |
8 | 8 | ||
9 | use crate::{ | 9 | use crate::{err::{StatixErr, FixErr}, traits::WriteDiagnostic}; |
10 | err::{FixErr, StatixErr}, | ||
11 | traits::WriteDiagnostic, | ||
12 | }; | ||
13 | 10 | ||
14 | use clap::Clap; | 11 | use clap::Clap; |
15 | use config::{FixConfig, LintConfig, Opts, SubCommand}; | 12 | use config::{FixConfig, LintConfig, Opts}; |
16 | use similar::TextDiff; | 13 | use similar::TextDiff; |
17 | 14 | ||
18 | fn _main() -> Result<(), StatixErr> { | 15 | fn _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 | } |
@@ -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. |
4 | Nix programming language. | 4 | |
5 | `statix` highlights antipatterns in Nix code. `statix fix` | ||
6 | can fix several such occurrences. | ||
5 | 7 | ||
6 | For the time-being, `statix` works only with ASTs | 8 | For the time-being, `statix` works only with ASTs |
7 | produced by the `rnix-parser` crate and does not evaluate | 9 | produced by the `rnix-parser` crate and does not evaluate |
8 | any nix code (imports, attr sets etc.). | 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 <[email protected]> | ||
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 | ## 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 | ||