diff options
author | Akshay <[email protected]> | 2021-10-25 17:38:52 +0100 |
---|---|---|
committer | Akshay <[email protected]> | 2021-10-25 17:38:52 +0100 |
commit | 5f0a1e67c64082c848418daa2b51020eb42c5c12 (patch) | |
tree | 1d6d7db9ee5532e76d23b9f509a8299d0d34dc52 /bin/src | |
parent | 781c42cc9ce2e6a3f1024ea1f4e3f071cc8f2dd4 (diff) |
rework cli, use subcommands instead
Diffstat (limited to 'bin/src')
-rw-r--r-- | bin/src/config.rs | 213 | ||||
-rw-r--r-- | bin/src/err.rs | 17 | ||||
-rw-r--r-- | bin/src/fix.rs | 87 | ||||
-rw-r--r-- | bin/src/fix/all.rs | 86 | ||||
-rw-r--r-- | bin/src/fix/single.rs | 59 | ||||
-rw-r--r-- | bin/src/main.rs | 72 |
6 files changed, 326 insertions, 208 deletions
diff --git a/bin/src/config.rs b/bin/src/config.rs index 6d7bc49..202ff6c 100644 --- a/bin/src/config.rs +++ b/bin/src/config.rs | |||
@@ -15,118 +15,74 @@ use crate::err::ConfigErr; | |||
15 | #[derive(Clap, Debug)] | 15 | #[derive(Clap, Debug)] |
16 | #[clap(version = "0.1.0", author = "Akshay <[email protected]>")] | 16 | #[clap(version = "0.1.0", author = "Akshay <[email protected]>")] |
17 | pub struct Opts { | 17 | pub struct Opts { |
18 | /// File or directory to run statix on | 18 | #[clap(subcommand)] |
19 | #[clap(default_value = ".")] | 19 | pub cmd: SubCommand, |
20 | pub target: String, | 20 | } |
21 | |||
22 | #[derive(Clap, Debug)] | ||
23 | pub enum SubCommand { | ||
24 | /// Lints and suggestions for the nix programming language | ||
25 | Check(Check), | ||
26 | /// Find and fix issues raised by statix-check | ||
27 | Fix(Fix), | ||
28 | /// Fix exactly one issue at provided position | ||
29 | Single(Single), | ||
30 | } | ||
31 | |||
32 | #[derive(Clap, Debug)] | ||
33 | pub struct Check { | ||
34 | /// File or directory to run check on | ||
35 | #[clap(default_value = ".", parse(from_os_str))] | ||
36 | target: PathBuf, | ||
21 | 37 | ||
22 | /// Globs of file patterns to skip | 38 | /// Globs of file patterns to skip |
23 | #[clap(short, long)] | 39 | #[clap(short, long)] |
24 | pub ignore: Vec<String>, | 40 | ignore: Vec<String>, |
25 | 41 | ||
26 | /// Output format. | 42 | /// Output format. |
27 | /// Supported values: errfmt, json (on feature flag only) | 43 | /// Supported values: errfmt, json (on feature flag only) |
28 | #[clap(short = 'o', long)] | 44 | #[clap(short = 'o', long, default_value = "OutFormat::StdErr")] |
29 | format: Option<OutFormat>, | 45 | pub format: OutFormat, |
30 | |||
31 | /// Find and fix issues raised by statix | ||
32 | #[clap(short = 'f', long)] | ||
33 | pub fix: bool, | ||
34 | |||
35 | /// Do not fix files in place, display a diff instead | ||
36 | #[clap(short = 'd', long = "dry-run")] | ||
37 | diff_only: bool, | ||
38 | } | ||
39 | |||
40 | |||
41 | #[derive(Debug, Copy, Clone)] | ||
42 | pub enum OutFormat { | ||
43 | #[cfg(feature = "json")] | ||
44 | Json, | ||
45 | Errfmt, | ||
46 | StdErr, | ||
47 | } | 46 | } |
48 | 47 | ||
49 | impl Default for OutFormat { | 48 | impl Check { |
50 | fn default() -> Self { | 49 | pub fn vfs(&self) -> Result<ReadOnlyVfs, ConfigErr> { |
51 | OutFormat::StdErr | 50 | let files = walk_with_ignores(&self.ignore, &self.target)?; |
51 | vfs(files) | ||
52 | } | 52 | } |
53 | } | 53 | } |
54 | 54 | ||
55 | impl FromStr for OutFormat { | 55 | #[derive(Clap, Debug)] |
56 | type Err = &'static str; | 56 | pub struct Fix { |
57 | /// File or directory to run fix on | ||
58 | #[clap(default_value = ".", parse(from_os_str))] | ||
59 | target: PathBuf, | ||
57 | 60 | ||
58 | fn from_str(value: &str) -> Result<Self, Self::Err> { | 61 | /// Globs of file patterns to skip |
59 | match value.to_ascii_lowercase().as_str() { | 62 | #[clap(short, long)] |
60 | #[cfg(feature = "json")] "json" => Ok(Self::Json), | 63 | ignore: Vec<String>, |
61 | "errfmt" => Ok(Self::Errfmt), | ||
62 | "stderr" => Ok(Self::StdErr), | ||
63 | "json" => Err("statix was not compiled with the `json` feature flag"), | ||
64 | _ => Err("unknown output format, try: json, errfmt"), | ||
65 | } | ||
66 | } | ||
67 | } | ||
68 | 64 | ||
69 | #[derive(Debug)] | 65 | /// Do not fix files in place, display a diff instead |
70 | pub struct LintConfig { | 66 | #[clap(short, long = "dry-run")] |
71 | pub files: Vec<PathBuf>, | 67 | pub diff_only: bool, |
72 | pub format: OutFormat, | ||
73 | } | 68 | } |
74 | 69 | ||
75 | impl LintConfig { | 70 | impl Fix { |
76 | pub fn from_opts(opts: Opts) -> Result<Self, ConfigErr> { | ||
77 | let ignores = build_ignore_set(&opts.ignore).map_err(|err| { | ||
78 | ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) | ||
79 | })?; | ||
80 | |||
81 | let files = walk_nix_files(&opts.target)? | ||
82 | .filter(|path| !ignores.is_match(path)) | ||
83 | .collect(); | ||
84 | |||
85 | Ok(Self { | ||
86 | files, | ||
87 | format: opts.format.unwrap_or_default(), | ||
88 | }) | ||
89 | } | ||
90 | |||
91 | pub fn vfs(&self) -> Result<ReadOnlyVfs, ConfigErr> { | 71 | pub fn vfs(&self) -> Result<ReadOnlyVfs, ConfigErr> { |
92 | let mut vfs = ReadOnlyVfs::default(); | 72 | let files = walk_with_ignores(&self.ignore, &self.target)?; |
93 | for file in self.files.iter() { | 73 | vfs(files) |
94 | let _id = vfs.alloc_file_id(&file); | ||
95 | let data = fs::read_to_string(&file).map_err(ConfigErr::InvalidPath)?; | ||
96 | vfs.set_file_contents(&file, data.as_bytes()); | ||
97 | } | ||
98 | Ok(vfs) | ||
99 | } | 74 | } |
100 | } | 75 | } |
101 | 76 | ||
102 | pub struct FixConfig { | 77 | #[derive(Clap, Debug)] |
103 | pub files: Vec<PathBuf>, | 78 | pub struct Single { |
104 | pub diff_only: bool, | 79 | /// File to run single-fix on |
105 | } | 80 | #[clap(default_value = ".", parse(from_os_str))] |
106 | 81 | pub target: PathBuf, | |
107 | impl FixConfig { | 82 | |
108 | pub fn from_opts(opts: Opts) -> Result<Self, ConfigErr> { | 83 | /// Position to attempt a fix at |
109 | let ignores = build_ignore_set(&opts.ignore).map_err(|err| { | 84 | #[clap(short, long, parse(try_from_str = parse_line_col))] |
110 | ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) | 85 | pub position: (usize, usize), |
111 | })?; | ||
112 | |||
113 | let files = walk_nix_files(&opts.target)? | ||
114 | .filter(|path| !ignores.is_match(path)) | ||
115 | .collect(); | ||
116 | |||
117 | let diff_only = opts.diff_only; | ||
118 | Ok(Self { files, diff_only }) | ||
119 | } | ||
120 | |||
121 | pub fn vfs(&self) -> Result<ReadOnlyVfs, ConfigErr> { | ||
122 | let mut vfs = ReadOnlyVfs::default(); | ||
123 | for file in self.files.iter() { | ||
124 | let _id = vfs.alloc_file_id(&file); | ||
125 | let data = fs::read_to_string(&file).map_err(ConfigErr::InvalidPath)?; | ||
126 | vfs.set_file_contents(&file, data.as_bytes()); | ||
127 | } | ||
128 | Ok(vfs) | ||
129 | } | ||
130 | } | 86 | } |
131 | 87 | ||
132 | mod dirs { | 88 | mod dirs { |
@@ -185,7 +141,23 @@ mod dirs { | |||
185 | } | 141 | } |
186 | } | 142 | } |
187 | 143 | ||
188 | fn build_ignore_set(ignores: &Vec<String>) -> Result<GlobSet, GlobError> { | 144 | fn parse_line_col(src: &str) -> Result<(usize, usize), ConfigErr> { |
145 | let parts = src.split(","); | ||
146 | match parts.collect::<Vec<_>>().as_slice() { | ||
147 | [line, col] => { | ||
148 | let l = line | ||
149 | .parse::<usize>() | ||
150 | .map_err(|_| ConfigErr::InvalidPosition(src.to_owned()))?; | ||
151 | let c = col | ||
152 | .parse::<usize>() | ||
153 | .map_err(|_| ConfigErr::InvalidPosition(src.to_owned()))?; | ||
154 | Ok((l, c)) | ||
155 | } | ||
156 | _ => Err(ConfigErr::InvalidPosition(src.to_owned())), | ||
157 | } | ||
158 | } | ||
159 | |||
160 | fn build_ignore_set(ignores: &[String]) -> Result<GlobSet, GlobError> { | ||
189 | let mut set = GlobSetBuilder::new(); | 161 | let mut set = GlobSetBuilder::new(); |
190 | for pattern in ignores { | 162 | for pattern in ignores { |
191 | let glob = GlobBuilder::new(&pattern).build()?; | 163 | let glob = GlobBuilder::new(&pattern).build()?; |
@@ -198,3 +170,56 @@ fn walk_nix_files<P: AsRef<Path>>(target: P) -> Result<impl Iterator<Item = Path | |||
198 | let walker = dirs::Walker::new(target)?; | 170 | let walker = dirs::Walker::new(target)?; |
199 | Ok(walker.filter(|path: &PathBuf| matches!(path.extension(), Some(e) if e == "nix"))) | 171 | Ok(walker.filter(|path: &PathBuf| matches!(path.extension(), Some(e) if e == "nix"))) |
200 | } | 172 | } |
173 | |||
174 | fn walk_with_ignores<P: AsRef<Path>>( | ||
175 | ignores: &[String], | ||
176 | target: P, | ||
177 | ) -> Result<Vec<PathBuf>, ConfigErr> { | ||
178 | let ignores = build_ignore_set(ignores).map_err(|err| { | ||
179 | ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) | ||
180 | })?; | ||
181 | |||
182 | Ok(walk_nix_files(&target)? | ||
183 | .filter(|path| !ignores.is_match(path)) | ||
184 | .collect()) | ||
185 | } | ||
186 | |||
187 | fn vfs(files: Vec<PathBuf>) -> Result<ReadOnlyVfs, ConfigErr> { | ||
188 | let mut vfs = ReadOnlyVfs::default(); | ||
189 | for file in files.iter() { | ||
190 | let _id = vfs.alloc_file_id(&file); | ||
191 | let data = fs::read_to_string(&file).map_err(ConfigErr::InvalidPath)?; | ||
192 | vfs.set_file_contents(&file, data.as_bytes()); | ||
193 | } | ||
194 | Ok(vfs) | ||
195 | |||
196 | } | ||
197 | |||
198 | #[derive(Debug, Copy, Clone)] | ||
199 | pub enum OutFormat { | ||
200 | #[cfg(feature = "json")] | ||
201 | Json, | ||
202 | Errfmt, | ||
203 | StdErr, | ||
204 | } | ||
205 | |||
206 | impl Default for OutFormat { | ||
207 | fn default() -> Self { | ||
208 | OutFormat::StdErr | ||
209 | } | ||
210 | } | ||
211 | |||
212 | impl FromStr for OutFormat { | ||
213 | type Err = &'static str; | ||
214 | |||
215 | fn from_str(value: &str) -> Result<Self, Self::Err> { | ||
216 | match value.to_ascii_lowercase().as_str() { | ||
217 | #[cfg(feature = "json")] | ||
218 | "json" => Ok(Self::Json), | ||
219 | "errfmt" => Ok(Self::Errfmt), | ||
220 | "stderr" => Ok(Self::StdErr), | ||
221 | "json" => Err("statix was not compiled with the `json` feature flag"), | ||
222 | _ => Err("unknown output format, try: json, errfmt"), | ||
223 | } | ||
224 | } | ||
225 | } | ||
diff --git a/bin/src/err.rs b/bin/src/err.rs index 727e0cc..5f71b57 100644 --- a/bin/src/err.rs +++ b/bin/src/err.rs | |||
@@ -8,9 +8,10 @@ use thiserror::Error; | |||
8 | pub enum ConfigErr { | 8 | pub enum ConfigErr { |
9 | #[error("error parsing glob `{0:?}`: {1}")] | 9 | #[error("error parsing glob `{0:?}`: {1}")] |
10 | InvalidGlob(Option<String>, ErrorKind), | 10 | InvalidGlob(Option<String>, ErrorKind), |
11 | |||
12 | #[error("path error: {0}")] | 11 | #[error("path error: {0}")] |
13 | InvalidPath(#[from] io::Error), | 12 | InvalidPath(#[from] io::Error), |
13 | #[error("unable to parse `{0}` as line and column")] | ||
14 | InvalidPosition(String) | ||
14 | } | 15 | } |
15 | 16 | ||
16 | #[derive(Error, Debug)] | 17 | #[derive(Error, Debug)] |
@@ -28,11 +29,25 @@ pub enum FixErr { | |||
28 | } | 29 | } |
29 | 30 | ||
30 | #[derive(Error, Debug)] | 31 | #[derive(Error, Debug)] |
32 | pub enum SingleFixErr { | ||
33 | #[error("path error: {0}")] | ||
34 | InvalidPath(#[from] io::Error), | ||
35 | #[error("position out of bounds: line {0}, col {1}")] | ||
36 | OutOfBounds(usize, usize), | ||
37 | #[error("{0} is too large")] | ||
38 | Conversion(usize), | ||
39 | #[error("nothing to fix")] | ||
40 | NoOp, | ||
41 | } | ||
42 | |||
43 | #[derive(Error, Debug)] | ||
31 | pub enum StatixErr { | 44 | pub enum StatixErr { |
32 | #[error("linter error: {0}")] | 45 | #[error("linter error: {0}")] |
33 | Lint(#[from] LintErr), | 46 | Lint(#[from] LintErr), |
34 | #[error("fixer error: {0}")] | 47 | #[error("fixer error: {0}")] |
35 | Fix(#[from] FixErr), | 48 | Fix(#[from] FixErr), |
49 | #[error("single fix error: {0}")] | ||
50 | Single(#[from] SingleFixErr), | ||
36 | #[error("config error: {0}")] | 51 | #[error("config error: {0}")] |
37 | Config(#[from] ConfigErr), | 52 | Config(#[from] ConfigErr), |
38 | } | 53 | } |
diff --git a/bin/src/fix.rs b/bin/src/fix.rs index d9087fe..a7ddc4f 100644 --- a/bin/src/fix.rs +++ b/bin/src/fix.rs | |||
@@ -1,55 +1,14 @@ | |||
1 | use std::borrow::Cow; | 1 | use std::borrow::Cow; |
2 | 2 | ||
3 | use lib::{Report, LINTS}; | 3 | use rnix::TextRange; |
4 | use rnix::{parser::ParseError as RnixParseErr, TextRange, WalkEvent}; | ||
5 | 4 | ||
6 | type Source<'a> = Cow<'a, str>; | 5 | mod all; |
7 | 6 | pub use all::all; | |
8 | fn collect_fixes(source: &str) -> Result<Vec<Report>, RnixParseErr> { | ||
9 | let parsed = rnix::parse(source).as_result()?; | ||
10 | |||
11 | Ok(parsed | ||
12 | .node() | ||
13 | .preorder_with_tokens() | ||
14 | .filter_map(|event| match event { | ||
15 | WalkEvent::Enter(child) => LINTS.get(&child.kind()).map(|rules| { | ||
16 | rules | ||
17 | .iter() | ||
18 | .filter_map(|rule| rule.validate(&child)) | ||
19 | .filter(|report| report.total_suggestion_range().is_some()) | ||
20 | .collect::<Vec<_>>() | ||
21 | }), | ||
22 | _ => None, | ||
23 | }) | ||
24 | .flatten() | ||
25 | .collect()) | ||
26 | } | ||
27 | 7 | ||
28 | fn reorder(mut reports: Vec<Report>) -> Vec<Report> { | 8 | mod single; |
29 | use std::collections::VecDeque; | 9 | pub use single::single; |
30 | 10 | ||
31 | reports.sort_by(|a, b| { | 11 | type Source<'a> = Cow<'a, str>; |
32 | let a_range = a.range(); | ||
33 | let b_range = b.range(); | ||
34 | a_range.end().partial_cmp(&b_range.end()).unwrap() | ||
35 | }); | ||
36 | |||
37 | reports | ||
38 | .into_iter() | ||
39 | .fold(VecDeque::new(), |mut deque: VecDeque<Report>, new_elem| { | ||
40 | let front = deque.front(); | ||
41 | let new_range = new_elem.range(); | ||
42 | if let Some(front_range) = front.map(|f| f.range()) { | ||
43 | if new_range.start() > front_range.end() { | ||
44 | deque.push_front(new_elem); | ||
45 | } | ||
46 | } else { | ||
47 | deque.push_front(new_elem); | ||
48 | } | ||
49 | deque | ||
50 | }) | ||
51 | .into() | ||
52 | } | ||
53 | 12 | ||
54 | #[derive(Debug)] | 13 | #[derive(Debug)] |
55 | pub struct FixResult<'a> { | 14 | pub struct FixResult<'a> { |
@@ -68,37 +27,3 @@ impl<'a> FixResult<'a> { | |||
68 | Self { src, fixed: Vec::new() } | 27 | Self { src, fixed: Vec::new() } |
69 | } | 28 | } |
70 | } | 29 | } |
71 | |||
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 | } | ||
79 | |||
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 | } | ||
91 | |||
92 | Some(FixResult { | ||
93 | src: self.src.clone(), | ||
94 | fixed | ||
95 | }) | ||
96 | } | ||
97 | } | ||
98 | |||
99 | pub fn fix(src: &str) -> Option<FixResult> { | ||
100 | let src = Cow::from(src); | ||
101 | let _ = rnix::parse(&src).as_result().ok()?; | ||
102 | let initial = FixResult::empty(src); | ||
103 | initial.into_iter().last() | ||
104 | } | ||
diff --git a/bin/src/fix/all.rs b/bin/src/fix/all.rs new file mode 100644 index 0000000..8c0770d --- /dev/null +++ b/bin/src/fix/all.rs | |||
@@ -0,0 +1,86 @@ | |||
1 | use std::borrow::Cow; | ||
2 | |||
3 | use lib::{Report, LINTS}; | ||
4 | use rnix::{parser::ParseError as RnixParseErr, WalkEvent}; | ||
5 | |||
6 | use crate::fix::{Fixed, FixResult}; | ||
7 | |||
8 | fn collect_fixes(source: &str) -> Result<Vec<Report>, RnixParseErr> { | ||
9 | let parsed = rnix::parse(source).as_result()?; | ||
10 | |||
11 | Ok(parsed | ||
12 | .node() | ||
13 | .preorder_with_tokens() | ||
14 | .filter_map(|event| match event { | ||
15 | WalkEvent::Enter(child) => LINTS.get(&child.kind()).map(|rules| { | ||
16 | rules | ||
17 | .iter() | ||
18 | .filter_map(|rule| rule.validate(&child)) | ||
19 | .filter(|report| report.total_suggestion_range().is_some()) | ||
20 | .collect::<Vec<_>>() | ||
21 | }), | ||
22 | _ => None, | ||
23 | }) | ||
24 | .flatten() | ||
25 | .collect()) | ||
26 | } | ||
27 | |||
28 | fn reorder(mut reports: Vec<Report>) -> Vec<Report> { | ||
29 | use std::collections::VecDeque; | ||
30 | |||
31 | reports.sort_by(|a, b| { | ||
32 | let a_range = a.range(); | ||
33 | let b_range = b.range(); | ||
34 | a_range.end().partial_cmp(&b_range.end()).unwrap() | ||
35 | }); | ||
36 | |||
37 | reports | ||
38 | .into_iter() | ||
39 | .fold(VecDeque::new(), |mut deque: VecDeque<Report>, new_elem| { | ||
40 | let front = deque.front(); | ||
41 | let new_range = new_elem.range(); | ||
42 | if let Some(front_range) = front.map(|f| f.range()) { | ||
43 | if new_range.start() > front_range.end() { | ||
44 | deque.push_front(new_elem); | ||
45 | } | ||
46 | } else { | ||
47 | deque.push_front(new_elem); | ||
48 | } | ||
49 | deque | ||
50 | }) | ||
51 | .into() | ||
52 | } | ||
53 | |||
54 | impl<'a> Iterator for FixResult<'a> { | ||
55 | type Item = FixResult<'a>; | ||
56 | fn next(&mut self) -> Option<Self::Item> { | ||
57 | let all_reports = collect_fixes(&self.src).ok()?; | ||
58 | if all_reports.is_empty() { | ||
59 | return None; | ||
60 | } | ||
61 | |||
62 | let reordered = reorder(all_reports); | ||
63 | let fixed = reordered | ||
64 | .iter() | ||
65 | .map(|r| Fixed { | ||
66 | at: r.range(), | ||
67 | code: r.code, | ||
68 | }) | ||
69 | .collect::<Vec<_>>(); | ||
70 | for report in reordered { | ||
71 | report.apply(self.src.to_mut()); | ||
72 | } | ||
73 | |||
74 | Some(FixResult { | ||
75 | src: self.src.clone(), | ||
76 | fixed | ||
77 | }) | ||
78 | } | ||
79 | } | ||
80 | |||
81 | pub fn all(src: &str) -> Option<FixResult> { | ||
82 | let src = Cow::from(src); | ||
83 | let _ = rnix::parse(&src).as_result().ok()?; | ||
84 | let initial = FixResult::empty(src); | ||
85 | initial.into_iter().last() | ||
86 | } | ||
diff --git a/bin/src/fix/single.rs b/bin/src/fix/single.rs new file mode 100644 index 0000000..d430693 --- /dev/null +++ b/bin/src/fix/single.rs | |||
@@ -0,0 +1,59 @@ | |||
1 | use std::{borrow::Cow, convert::TryFrom}; | ||
2 | |||
3 | use lib::{Report, LINTS}; | ||
4 | use rnix::{TextRange, TextSize}; | ||
5 | |||
6 | use crate::err::SingleFixErr; | ||
7 | use crate::fix::Source; | ||
8 | |||
9 | pub struct SingleFixResult<'δ> { | ||
10 | pub src: Source<'δ>, | ||
11 | } | ||
12 | |||
13 | fn pos_to_byte(line: usize, col: usize, src: &str) -> Result<TextSize, SingleFixErr> { | ||
14 | let mut byte: TextSize = TextSize::of(""); | ||
15 | for (_, l) in src.lines().enumerate().take_while(|(i, _)| i <= &line) { | ||
16 | byte += TextSize::of(l); | ||
17 | } | ||
18 | byte += TextSize::try_from(col).map_err(|_| SingleFixErr::Conversion(col))?; | ||
19 | |||
20 | if usize::from(byte) >= src.len() { | ||
21 | Err(SingleFixErr::OutOfBounds(line, col)) | ||
22 | } else { | ||
23 | Ok(byte) | ||
24 | } | ||
25 | } | ||
26 | |||
27 | fn find(offset: TextSize, src: &str) -> Result<Report, SingleFixErr> { | ||
28 | // we don't really need the source to form a completely parsed tree | ||
29 | let parsed = rnix::parse(src); | ||
30 | |||
31 | let elem_at = parsed | ||
32 | .node() | ||
33 | .child_or_token_at_range(TextRange::empty(offset)) | ||
34 | .ok_or(SingleFixErr::NoOp)?; | ||
35 | |||
36 | LINTS | ||
37 | .get(&elem_at.kind()) | ||
38 | .map(|rules| { | ||
39 | rules | ||
40 | .iter() | ||
41 | .filter_map(|rule| rule.validate(&elem_at)) | ||
42 | .filter(|report| report.total_suggestion_range().is_some()) | ||
43 | .next() | ||
44 | }) | ||
45 | .flatten() | ||
46 | .ok_or(SingleFixErr::NoOp) | ||
47 | } | ||
48 | |||
49 | pub fn single(line: usize, col: usize, src: &str) -> Result<SingleFixResult, SingleFixErr> { | ||
50 | let mut src = Cow::from(src); | ||
51 | let offset = pos_to_byte(line, col, &*src)?; | ||
52 | let report = find(offset, &*src)?; | ||
53 | |||
54 | report.apply(src.to_mut()); | ||
55 | |||
56 | Ok(SingleFixResult { | ||
57 | src | ||
58 | }) | ||
59 | } | ||
diff --git a/bin/src/main.rs b/bin/src/main.rs index d0f69a0..9c57d91 100644 --- a/bin/src/main.rs +++ b/bin/src/main.rs | |||
@@ -6,50 +6,58 @@ mod traits; | |||
6 | 6 | ||
7 | use std::io; | 7 | use std::io; |
8 | 8 | ||
9 | use crate::{err::{StatixErr, FixErr}, traits::WriteDiagnostic}; | 9 | use crate::{err::{StatixErr, FixErr, SingleFixErr}, traits::WriteDiagnostic}; |
10 | 10 | ||
11 | use clap::Clap; | 11 | use clap::Clap; |
12 | use config::{FixConfig, LintConfig, Opts}; | 12 | use config::{Opts, SubCommand}; |
13 | use similar::TextDiff; | 13 | use similar::TextDiff; |
14 | 14 | ||
15 | fn _main() -> Result<(), StatixErr> { | 15 | fn _main() -> Result<(), StatixErr> { |
16 | let opts = Opts::parse(); | 16 | let opts = Opts::parse(); |
17 | if opts.fix { | 17 | match opts.cmd { |
18 | let fix_config = FixConfig::from_opts(opts)?; | 18 | SubCommand::Check(check_config) => { |
19 | let vfs = fix_config.vfs()?; | 19 | let vfs = check_config.vfs()?; |
20 | for entry in vfs.iter() { | 20 | let (lints, errors): (Vec<_>, Vec<_>) = vfs.iter().map(lint::lint).partition(Result::is_ok); |
21 | if let Some(fix_result) = fix::fix(entry.contents) { | 21 | let lint_results = lints.into_iter().map(Result::unwrap); |
22 | if fix_config.diff_only { | 22 | let errors = errors.into_iter().map(Result::unwrap_err); |
23 | let text_diff = TextDiff::from_lines(entry.contents, &fix_result.src); | 23 | |
24 | let old_file = format!("{}", entry.file_path.display()); | 24 | let mut stdout = io::stdout(); |
25 | let new_file = format!("{} [fixed]", entry.file_path.display()); | 25 | lint_results.for_each(|r| { |
26 | println!( | 26 | stdout.write(&r, &vfs, check_config.format).unwrap(); |
27 | "{}", | 27 | }); |
28 | text_diff | 28 | errors.for_each(|e| { |
29 | eprintln!("{}", e); | ||
30 | }); | ||
31 | }, | ||
32 | SubCommand::Fix(fix_config) => { | ||
33 | let vfs = fix_config.vfs()?; | ||
34 | for entry in vfs.iter() { | ||
35 | if let Some(fix_result) = fix::all(entry.contents) { | ||
36 | if fix_config.diff_only { | ||
37 | let text_diff = TextDiff::from_lines(entry.contents, &fix_result.src); | ||
38 | let old_file = format!("{}", entry.file_path.display()); | ||
39 | let new_file = format!("{} [fixed]", entry.file_path.display()); | ||
40 | println!( | ||
41 | "{}", | ||
42 | text_diff | ||
29 | .unified_diff() | 43 | .unified_diff() |
30 | .context_radius(4) | 44 | .context_radius(4) |
31 | .header(&old_file, &new_file) | 45 | .header(&old_file, &new_file) |
32 | ); | 46 | ); |
33 | } else { | 47 | } else { |
34 | let path = entry.file_path; | 48 | let path = entry.file_path; |
35 | std::fs::write(path, &*fix_result.src).map_err(FixErr::InvalidPath)?; | 49 | std::fs::write(path, &*fix_result.src).map_err(FixErr::InvalidPath)?; |
50 | } | ||
36 | } | 51 | } |
37 | } | 52 | } |
53 | }, | ||
54 | SubCommand::Single(single_config) => { | ||
55 | let path = single_config.target; | ||
56 | let src = std::fs::read_to_string(&path).map_err(SingleFixErr::InvalidPath)?; | ||
57 | let (line, col) = single_config.position; | ||
58 | let single_result = fix::single(line, col, &src)?; | ||
59 | std::fs::write(&path, &*single_result.src).map_err(SingleFixErr::InvalidPath)?; | ||
38 | } | 60 | } |
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); | ||
45 | |||
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 | }); | ||
53 | } | 61 | } |
54 | Ok(()) | 62 | Ok(()) |
55 | } | 63 | } |