diff options
author | Akshay <[email protected]> | 2021-10-23 08:11:52 +0100 |
---|---|---|
committer | Akshay <[email protected]> | 2021-10-23 08:11:52 +0100 |
commit | c2f0582d1907dbef69e9ad42ba9d4301337fe1e8 (patch) | |
tree | aacf975dfeb8bd416b70abfe8a21a2ce7c325d0c /bin/src | |
parent | dfcdaf91674461a5150902cb3fdb8f198367ff20 (diff) |
initial implementation of multipass code fixer
Diffstat (limited to 'bin/src')
-rw-r--r-- | bin/src/config.rs | 78 | ||||
-rw-r--r-- | bin/src/err.rs | 8 | ||||
-rw-r--r-- | bin/src/fix.rs | 112 | ||||
-rw-r--r-- | bin/src/lint.rs | 4 | ||||
-rw-r--r-- | bin/src/main.rs | 33 |
5 files changed, 210 insertions, 25 deletions
diff --git a/bin/src/config.rs b/bin/src/config.rs index f2cf29d..077f73e 100644 --- a/bin/src/config.rs +++ b/bin/src/config.rs | |||
@@ -1,7 +1,12 @@ | |||
1 | use std::{default::Default, fs, path::PathBuf, str::FromStr}; | 1 | use std::{ |
2 | default::Default, | ||
3 | fs, io, | ||
4 | path::{Path, PathBuf}, | ||
5 | str::FromStr, | ||
6 | }; | ||
2 | 7 | ||
3 | use clap::Clap; | 8 | use clap::Clap; |
4 | use globset::{GlobBuilder, GlobSetBuilder}; | 9 | use globset::{Error as GlobError, GlobBuilder, GlobSet, GlobSetBuilder}; |
5 | use vfs::ReadOnlyVfs; | 10 | use vfs::ReadOnlyVfs; |
6 | 11 | ||
7 | use crate::err::ConfigErr; | 12 | use crate::err::ConfigErr; |
@@ -77,25 +82,14 @@ pub struct LintConfig { | |||
77 | 82 | ||
78 | impl LintConfig { | 83 | impl LintConfig { |
79 | pub fn from_opts(opts: Opts) -> Result<Self, ConfigErr> { | 84 | pub fn from_opts(opts: Opts) -> Result<Self, ConfigErr> { |
80 | let ignores = { | 85 | let ignores = build_ignore_set(&opts.ignore).map_err(|err| { |
81 | let mut set = GlobSetBuilder::new(); | 86 | ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) |
82 | for pattern in opts.ignore { | 87 | })?; |
83 | let glob = GlobBuilder::new(&pattern).build().map_err(|err| { | ||
84 | ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) | ||
85 | })?; | ||
86 | set.add(glob); | ||
87 | } | ||
88 | set.build().map_err(|err| { | ||
89 | ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) | ||
90 | }) | ||
91 | }?; | ||
92 | |||
93 | let walker = dirs::Walker::new(opts.target).map_err(ConfigErr::InvalidPath)?; | ||
94 | 88 | ||
95 | let files = walker | 89 | let files = walk_nix_files(&opts.target)? |
96 | .filter(|path| matches!(path.extension(), Some(e) if e == "nix")) | ||
97 | .filter(|path| !ignores.is_match(path)) | 90 | .filter(|path| !ignores.is_match(path)) |
98 | .collect(); | 91 | .collect(); |
92 | |||
99 | Ok(Self { | 93 | Ok(Self { |
100 | files, | 94 | files, |
101 | format: opts.format.unwrap_or_default(), | 95 | format: opts.format.unwrap_or_default(), |
@@ -113,6 +107,40 @@ impl LintConfig { | |||
113 | } | 107 | } |
114 | } | 108 | } |
115 | 109 | ||
110 | pub struct FixConfig { | ||
111 | pub files: Vec<PathBuf>, | ||
112 | pub diff_only: bool, | ||
113 | } | ||
114 | |||
115 | impl FixConfig { | ||
116 | pub fn from_opts(opts: Opts) -> Result<Self, ConfigErr> { | ||
117 | let ignores = build_ignore_set(&opts.ignore).map_err(|err| { | ||
118 | ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) | ||
119 | })?; | ||
120 | |||
121 | let files = walk_nix_files(&opts.target)? | ||
122 | .filter(|path| !ignores.is_match(path)) | ||
123 | .collect(); | ||
124 | |||
125 | let diff_only = match opts.subcmd { | ||
126 | Some(SubCommand::Fix(f)) => f.diff_only, | ||
127 | _ => false, | ||
128 | }; | ||
129 | |||
130 | Ok(Self { files, diff_only }) | ||
131 | } | ||
132 | |||
133 | pub fn vfs(&self) -> Result<ReadOnlyVfs, ConfigErr> { | ||
134 | let mut vfs = ReadOnlyVfs::default(); | ||
135 | for file in self.files.iter() { | ||
136 | let _id = vfs.alloc_file_id(&file); | ||
137 | let data = fs::read_to_string(&file).map_err(ConfigErr::InvalidPath)?; | ||
138 | vfs.set_file_contents(&file, data.as_bytes()); | ||
139 | } | ||
140 | Ok(vfs) | ||
141 | } | ||
142 | } | ||
143 | |||
116 | mod dirs { | 144 | mod dirs { |
117 | use std::{ | 145 | use std::{ |
118 | fs, | 146 | fs, |
@@ -168,3 +196,17 @@ mod dirs { | |||
168 | } | 196 | } |
169 | } | 197 | } |
170 | } | 198 | } |
199 | |||
200 | fn build_ignore_set(ignores: &Vec<String>) -> Result<GlobSet, GlobError> { | ||
201 | let mut set = GlobSetBuilder::new(); | ||
202 | for pattern in ignores { | ||
203 | let glob = GlobBuilder::new(&pattern).build()?; | ||
204 | set.add(glob); | ||
205 | } | ||
206 | set.build() | ||
207 | } | ||
208 | |||
209 | fn walk_nix_files<P: AsRef<Path>>(target: P) -> Result<impl Iterator<Item = PathBuf>, io::Error> { | ||
210 | let walker = dirs::Walker::new(target)?; | ||
211 | Ok(walker.filter(|path: &PathBuf| matches!(path.extension(), Some(e) if e == "nix"))) | ||
212 | } | ||
diff --git a/bin/src/err.rs b/bin/src/err.rs index b3a79c2..c9db4d5 100644 --- a/bin/src/err.rs +++ b/bin/src/err.rs | |||
@@ -20,9 +20,17 @@ pub enum LintErr { | |||
20 | } | 20 | } |
21 | 21 | ||
22 | #[derive(Error, Debug)] | 22 | #[derive(Error, Debug)] |
23 | pub enum FixErr { | ||
24 | #[error("[{0}] syntax error: {1}")] | ||
25 | Parse(PathBuf, ParseError), | ||
26 | } | ||
27 | |||
28 | #[derive(Error, Debug)] | ||
23 | pub enum StatixErr { | 29 | pub enum StatixErr { |
24 | #[error("linter error: {0}")] | 30 | #[error("linter error: {0}")] |
25 | Lint(#[from] LintErr), | 31 | Lint(#[from] LintErr), |
32 | #[error("fixer error: {0}")] | ||
33 | Fix(#[from] FixErr), | ||
26 | #[error("config error: {0}")] | 34 | #[error("config error: {0}")] |
27 | Config(#[from] ConfigErr), | 35 | Config(#[from] ConfigErr), |
28 | } | 36 | } |
diff --git a/bin/src/fix.rs b/bin/src/fix.rs new file mode 100644 index 0000000..478dbd9 --- /dev/null +++ b/bin/src/fix.rs | |||
@@ -0,0 +1,112 @@ | |||
1 | use std::borrow::Cow; | ||
2 | |||
3 | use lib::{Report, LINTS}; | ||
4 | use rnix::{parser::ParseError as RnixParseErr, TextRange, WalkEvent}; | ||
5 | |||
6 | type Source<'a> = Cow<'a, str>; | ||
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 | #[derive(Debug)] | ||
55 | pub struct FixResult<'a> { | ||
56 | pub src: Source<'a>, | ||
57 | pub fixed: Vec<Fixed>, | ||
58 | } | ||
59 | |||
60 | #[derive(Debug, Clone)] | ||
61 | pub struct Fixed { | ||
62 | pub at: TextRange, | ||
63 | pub code: u32, | ||
64 | } | ||
65 | |||
66 | impl<'a> FixResult<'a> { | ||
67 | fn empty(src: Source<'a>) -> Self { | ||
68 | Self { src, fixed: vec![] } | ||
69 | } | ||
70 | } | ||
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 | } | ||
78 | |||
79 | let reordered = reorder(all_reports); | ||
80 | |||
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(src.to_mut()); | ||
90 | } | ||
91 | |||
92 | Ok(FixResult { | ||
93 | src, | ||
94 | fixed | ||
95 | }) | ||
96 | } | ||
97 | |||
98 | pub fn fix(src: &str) -> Result<FixResult, RnixParseErr> { | ||
99 | 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"); | ||
112 | } | ||
diff --git a/bin/src/lint.rs b/bin/src/lint.rs index 76b2b8c..65ae824 100644 --- a/bin/src/lint.rs +++ b/bin/src/lint.rs | |||
@@ -1,8 +1,8 @@ | |||
1 | use crate::err::LintErr; | 1 | use crate::err::LintErr; |
2 | 2 | ||
3 | use lib::{LINTS, Report}; | 3 | use lib::{Report, LINTS}; |
4 | use rnix::WalkEvent; | 4 | use rnix::WalkEvent; |
5 | use vfs::{VfsEntry, FileId}; | 5 | use vfs::{FileId, VfsEntry}; |
6 | 6 | ||
7 | #[derive(Debug)] | 7 | #[derive(Debug)] |
8 | pub struct LintResult { | 8 | pub struct LintResult { |
diff --git a/bin/src/main.rs b/bin/src/main.rs index 161dcab..4cd525a 100644 --- a/bin/src/main.rs +++ b/bin/src/main.rs | |||
@@ -1,27 +1,50 @@ | |||
1 | mod config; | 1 | mod config; |
2 | mod err; | 2 | mod err; |
3 | mod fix; | ||
3 | mod lint; | 4 | mod lint; |
4 | mod traits; | 5 | mod traits; |
5 | 6 | ||
6 | use std::io; | 7 | use std::io; |
7 | 8 | ||
8 | use crate::{err::StatixErr, traits::WriteDiagnostic}; | 9 | use crate::{ |
10 | err::{FixErr, StatixErr}, | ||
11 | traits::WriteDiagnostic, | ||
12 | }; | ||
9 | 13 | ||
10 | use clap::Clap; | 14 | use clap::Clap; |
11 | use config::{LintConfig, Opts, SubCommand}; | 15 | use config::{FixConfig, LintConfig, Opts, SubCommand}; |
16 | use similar::TextDiff; | ||
12 | 17 | ||
13 | fn _main() -> Result<(), StatixErr> { | 18 | fn _main() -> Result<(), StatixErr> { |
14 | let opts = Opts::parse(); | 19 | let opts = Opts::parse(); |
15 | match opts.subcmd { | 20 | match opts.subcmd { |
16 | Some(SubCommand::Fix(_)) => { | 21 | Some(SubCommand::Fix(_)) => { |
17 | eprintln!("`fix` not yet supported"); | 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)), | ||
39 | } | ||
40 | } | ||
18 | } | 41 | } |
19 | None => { | 42 | None => { |
20 | let lint_config = LintConfig::from_opts(opts)?; | 43 | let lint_config = LintConfig::from_opts(opts)?; |
21 | let vfs = lint_config.vfs()?; | 44 | let vfs = lint_config.vfs()?; |
22 | let (reports, errors): (Vec<_>, Vec<_>) = | 45 | let (lints, errors): (Vec<_>, Vec<_>) = |
23 | vfs.iter().map(lint::lint).partition(Result::is_ok); | 46 | vfs.iter().map(lint::lint).partition(Result::is_ok); |
24 | let lint_results = reports.into_iter().map(Result::unwrap); | 47 | let lint_results = lints.into_iter().map(Result::unwrap); |
25 | let errors = errors.into_iter().map(Result::unwrap_err); | 48 | let errors = errors.into_iter().map(Result::unwrap_err); |
26 | 49 | ||
27 | let mut stderr = io::stderr(); | 50 | let mut stderr = io::stderr(); |