aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAkshay <[email protected]>2021-10-23 08:11:52 +0100
committerAkshay <[email protected]>2021-10-23 08:11:52 +0100
commitc2f0582d1907dbef69e9ad42ba9d4301337fe1e8 (patch)
treeaacf975dfeb8bd416b70abfe8a21a2ce7c325d0c
parentdfcdaf91674461a5150902cb3fdb8f198367ff20 (diff)
initial implementation of multipass code fixer
-rw-r--r--Cargo.lock7
-rw-r--r--bin/Cargo.toml3
-rw-r--r--bin/src/config.rs78
-rw-r--r--bin/src/err.rs8
-rw-r--r--bin/src/fix.rs112
-rw-r--r--bin/src/lint.rs4
-rw-r--r--bin/src/main.rs33
7 files changed, 219 insertions, 26 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 69e4495..5fe64ab 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -330,6 +330,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
330checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 330checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
331 331
332[[package]] 332[[package]]
333name = "similar"
334version = "2.1.0"
335source = "registry+https://github.com/rust-lang/crates.io-index"
336checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3"
337
338[[package]]
333name = "smol_str" 339name = "smol_str"
334version = "0.1.18" 340version = "0.1.18"
335source = "registry+https://github.com/rust-lang/crates.io-index" 341source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -344,6 +350,7 @@ dependencies = [
344 "globset", 350 "globset",
345 "lib", 351 "lib",
346 "rnix", 352 "rnix",
353 "similar",
347 "thiserror", 354 "thiserror",
348 "vfs", 355 "vfs",
349] 356]
diff --git a/bin/Cargo.toml b/bin/Cargo.toml
index 0d6f970..af5a288 100644
--- a/bin/Cargo.toml
+++ b/bin/Cargo.toml
@@ -6,10 +6,11 @@ edition = "2018"
6# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 6# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 7
8[dependencies] 8[dependencies]
9lib = { path = "../lib" }
10ariadne = "0.1.3" 9ariadne = "0.1.3"
11rnix = "0.9.0" 10rnix = "0.9.0"
12clap = "3.0.0-beta.4" 11clap = "3.0.0-beta.4"
13globset = "0.4.8" 12globset = "0.4.8"
14thiserror = "1.0.30" 13thiserror = "1.0.30"
14similar = "2.1.0"
15vfs = { path = "../vfs" } 15vfs = { path = "../vfs" }
16lib = { path = "../lib" }
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 @@
1use std::{default::Default, fs, path::PathBuf, str::FromStr}; 1use std::{
2 default::Default,
3 fs, io,
4 path::{Path, PathBuf},
5 str::FromStr,
6};
2 7
3use clap::Clap; 8use clap::Clap;
4use globset::{GlobBuilder, GlobSetBuilder}; 9use globset::{Error as GlobError, GlobBuilder, GlobSet, GlobSetBuilder};
5use vfs::ReadOnlyVfs; 10use vfs::ReadOnlyVfs;
6 11
7use crate::err::ConfigErr; 12use crate::err::ConfigErr;
@@ -77,25 +82,14 @@ pub struct LintConfig {
77 82
78impl LintConfig { 83impl 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
110pub struct FixConfig {
111 pub files: Vec<PathBuf>,
112 pub diff_only: bool,
113}
114
115impl 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
116mod dirs { 144mod dirs {
117 use std::{ 145 use std::{
118 fs, 146 fs,
@@ -168,3 +196,17 @@ mod dirs {
168 } 196 }
169 } 197 }
170} 198}
199
200fn 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
209fn 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)]
23pub enum FixErr {
24 #[error("[{0}] syntax error: {1}")]
25 Parse(PathBuf, ParseError),
26}
27
28#[derive(Error, Debug)]
23pub enum StatixErr { 29pub 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 @@
1use std::borrow::Cow;
2
3use lib::{Report, LINTS};
4use rnix::{parser::ParseError as RnixParseErr, TextRange, WalkEvent};
5
6type Source<'a> = Cow<'a, str>;
7
8fn 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
28fn 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)]
55pub struct FixResult<'a> {
56 pub src: Source<'a>,
57 pub fixed: Vec<Fixed>,
58}
59
60#[derive(Debug, Clone)]
61pub struct Fixed {
62 pub at: TextRange,
63 pub code: u32,
64}
65
66impl<'a> FixResult<'a> {
67 fn empty(src: Source<'a>) -> Self {
68 Self { src, fixed: vec![] }
69 }
70}
71
72fn 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
98pub 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 @@
1use crate::err::LintErr; 1use crate::err::LintErr;
2 2
3use lib::{LINTS, Report}; 3use lib::{Report, LINTS};
4use rnix::WalkEvent; 4use rnix::WalkEvent;
5use vfs::{VfsEntry, FileId}; 5use vfs::{FileId, VfsEntry};
6 6
7#[derive(Debug)] 7#[derive(Debug)]
8pub struct LintResult { 8pub 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 @@
1mod config; 1mod config;
2mod err; 2mod err;
3mod fix;
3mod lint; 4mod lint;
4mod traits; 5mod traits;
5 6
6use std::io; 7use std::io;
7 8
8use crate::{err::StatixErr, traits::WriteDiagnostic}; 9use crate::{
10 err::{FixErr, StatixErr},
11 traits::WriteDiagnostic,
12};
9 13
10use clap::Clap; 14use clap::Clap;
11use config::{LintConfig, Opts, SubCommand}; 15use config::{FixConfig, LintConfig, Opts, SubCommand};
16use similar::TextDiff;
12 17
13fn _main() -> Result<(), StatixErr> { 18fn _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();