From c2f0582d1907dbef69e9ad42ba9d4301337fe1e8 Mon Sep 17 00:00:00 2001 From: Akshay Date: Sat, 23 Oct 2021 12:41:52 +0530 Subject: initial implementation of multipass code fixer --- Cargo.lock | 7 ++++ bin/Cargo.toml | 3 +- bin/src/config.rs | 78 ++++++++++++++++++++++++++++--------- bin/src/err.rs | 8 ++++ bin/src/fix.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ bin/src/lint.rs | 4 +- bin/src/main.rs | 33 +++++++++++++--- 7 files changed, 219 insertions(+), 26 deletions(-) create mode 100644 bin/src/fix.rs diff --git a/Cargo.lock b/Cargo.lock index 69e4495..5fe64ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -329,6 +329,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "similar" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3" + [[package]] name = "smol_str" version = "0.1.18" @@ -344,6 +350,7 @@ dependencies = [ "globset", "lib", "rnix", + "similar", "thiserror", "vfs", ] 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" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -lib = { path = "../lib" } ariadne = "0.1.3" rnix = "0.9.0" clap = "3.0.0-beta.4" globset = "0.4.8" thiserror = "1.0.30" +similar = "2.1.0" vfs = { path = "../vfs" } +lib = { 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 @@ -use std::{default::Default, fs, path::PathBuf, str::FromStr}; +use std::{ + default::Default, + fs, io, + path::{Path, PathBuf}, + str::FromStr, +}; use clap::Clap; -use globset::{GlobBuilder, GlobSetBuilder}; +use globset::{Error as GlobError, GlobBuilder, GlobSet, GlobSetBuilder}; use vfs::ReadOnlyVfs; use crate::err::ConfigErr; @@ -77,25 +82,14 @@ pub struct LintConfig { impl LintConfig { pub fn from_opts(opts: Opts) -> Result { - let ignores = { - let mut set = GlobSetBuilder::new(); - for pattern in opts.ignore { - let glob = GlobBuilder::new(&pattern).build().map_err(|err| { - ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) - })?; - set.add(glob); - } - set.build().map_err(|err| { - ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) - }) - }?; - - let walker = dirs::Walker::new(opts.target).map_err(ConfigErr::InvalidPath)?; + let ignores = build_ignore_set(&opts.ignore).map_err(|err| { + ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) + })?; - let files = walker - .filter(|path| matches!(path.extension(), Some(e) if e == "nix")) + let files = walk_nix_files(&opts.target)? .filter(|path| !ignores.is_match(path)) .collect(); + Ok(Self { files, format: opts.format.unwrap_or_default(), @@ -113,6 +107,40 @@ impl LintConfig { } } +pub struct FixConfig { + pub files: Vec, + pub diff_only: bool, +} + +impl FixConfig { + pub fn from_opts(opts: Opts) -> Result { + let ignores = build_ignore_set(&opts.ignore).map_err(|err| { + ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) + })?; + + let files = walk_nix_files(&opts.target)? + .filter(|path| !ignores.is_match(path)) + .collect(); + + let diff_only = match opts.subcmd { + Some(SubCommand::Fix(f)) => f.diff_only, + _ => false, + }; + + Ok(Self { files, diff_only }) + } + + pub fn vfs(&self) -> Result { + let mut vfs = ReadOnlyVfs::default(); + for file in self.files.iter() { + let _id = vfs.alloc_file_id(&file); + let data = fs::read_to_string(&file).map_err(ConfigErr::InvalidPath)?; + vfs.set_file_contents(&file, data.as_bytes()); + } + Ok(vfs) + } +} + mod dirs { use std::{ fs, @@ -168,3 +196,17 @@ mod dirs { } } } + +fn build_ignore_set(ignores: &Vec) -> Result { + let mut set = GlobSetBuilder::new(); + for pattern in ignores { + let glob = GlobBuilder::new(&pattern).build()?; + set.add(glob); + } + set.build() +} + +fn walk_nix_files>(target: P) -> Result, io::Error> { + let walker = dirs::Walker::new(target)?; + Ok(walker.filter(|path: &PathBuf| matches!(path.extension(), Some(e) if e == "nix"))) +} 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 @@ -19,10 +19,18 @@ pub enum LintErr { Parse(PathBuf, ParseError), } +#[derive(Error, Debug)] +pub enum FixErr { + #[error("[{0}] syntax error: {1}")] + Parse(PathBuf, ParseError), +} + #[derive(Error, Debug)] pub enum StatixErr { #[error("linter error: {0}")] Lint(#[from] LintErr), + #[error("fixer error: {0}")] + Fix(#[from] FixErr), #[error("config error: {0}")] Config(#[from] ConfigErr), } 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 @@ +use std::borrow::Cow; + +use lib::{Report, LINTS}; +use rnix::{parser::ParseError as RnixParseErr, TextRange, WalkEvent}; + +type Source<'a> = Cow<'a, str>; + +fn collect_fixes(source: &str) -> Result, RnixParseErr> { + let parsed = rnix::parse(source).as_result()?; + + Ok(parsed + .node() + .preorder_with_tokens() + .filter_map(|event| match event { + WalkEvent::Enter(child) => LINTS.get(&child.kind()).map(|rules| { + rules + .iter() + .filter_map(|rule| rule.validate(&child)) + .filter(|report| report.total_suggestion_range().is_some()) + .collect::>() + }), + _ => None, + }) + .flatten() + .collect()) +} + +fn reorder(mut reports: Vec) -> Vec { + use std::collections::VecDeque; + + reports.sort_by(|a, b| { + let a_range = a.range(); + let b_range = b.range(); + a_range.end().partial_cmp(&b_range.end()).unwrap() + }); + + reports + .into_iter() + .fold(VecDeque::new(), |mut deque: VecDeque, new_elem| { + let front = deque.front(); + let new_range = new_elem.range(); + if let Some(front_range) = front.map(|f| f.range()) { + if new_range.start() > front_range.end() { + deque.push_front(new_elem); + } + } else { + deque.push_front(new_elem); + } + deque + }) + .into() +} + +#[derive(Debug)] +pub struct FixResult<'a> { + pub src: Source<'a>, + pub fixed: Vec, +} + +#[derive(Debug, Clone)] +pub struct Fixed { + pub at: TextRange, + pub code: u32, +} + +impl<'a> FixResult<'a> { + fn empty(src: Source<'a>) -> Self { + Self { src, fixed: vec![] } + } +} + +fn next(mut src: Source) -> Result { + let all_reports = collect_fixes(&src)?; + + if all_reports.is_empty() { + return Ok(FixResult::empty(src)); + } + + let reordered = reorder(all_reports); + + let fixed = reordered + .iter() + .map(|r| Fixed { + at: r.range(), + code: r.code, + }) + .collect::>(); + for report in reordered { + report.apply(src.to_mut()); + } + + Ok(FixResult { + src, + fixed + }) +} + +pub fn fix(src: &str) -> Result { + let src = Cow::from(src); + let _ = rnix::parse(&src).as_result()?; + let mut initial = FixResult::empty(src); + + while let Ok(next_result) = next(initial.src) { + if next_result.fixed.is_empty() { + return Ok(next_result); + } else { + initial = FixResult::empty(next_result.src); + } + } + + unreachable!("a fix caused a syntax error, please report a bug"); +} 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 @@ use crate::err::LintErr; -use lib::{LINTS, Report}; +use lib::{Report, LINTS}; use rnix::WalkEvent; -use vfs::{VfsEntry, FileId}; +use vfs::{FileId, VfsEntry}; #[derive(Debug)] 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 @@ mod config; mod err; +mod fix; mod lint; mod traits; use std::io; -use crate::{err::StatixErr, traits::WriteDiagnostic}; +use crate::{ + err::{FixErr, StatixErr}, + traits::WriteDiagnostic, +}; use clap::Clap; -use config::{LintConfig, Opts, SubCommand}; +use config::{FixConfig, LintConfig, Opts, SubCommand}; +use similar::TextDiff; fn _main() -> Result<(), StatixErr> { let opts = Opts::parse(); match opts.subcmd { Some(SubCommand::Fix(_)) => { - eprintln!("`fix` not yet supported"); + let fix_config = FixConfig::from_opts(opts)?; + let vfs = fix_config.vfs()?; + for entry in vfs.iter() { + match fix::fix(entry.contents) { + Ok(fix_result) => { + let text_diff = TextDiff::from_lines(entry.contents, &fix_result.src); + let old_file = format!("{}", entry.file_path.display()); + let new_file = format!("{} [fixed]", entry.file_path.display()); + println!( + "{}", + text_diff + .unified_diff() + .context_radius(4) + .header(&old_file, &new_file) + ); + } + Err(e) => eprintln!("{}", FixErr::Parse(entry.file_path.to_path_buf(), e)), + } + } } None => { let lint_config = LintConfig::from_opts(opts)?; let vfs = lint_config.vfs()?; - let (reports, errors): (Vec<_>, Vec<_>) = + let (lints, errors): (Vec<_>, Vec<_>) = vfs.iter().map(lint::lint).partition(Result::is_ok); - let lint_results = reports.into_iter().map(Result::unwrap); + let lint_results = lints.into_iter().map(Result::unwrap); let errors = errors.into_iter().map(Result::unwrap_err); let mut stderr = io::stderr(); -- cgit v1.2.3