initial implementation of multipass code fixer

This commit is contained in:
Akshay 2021-10-23 12:41:52 +05:30
parent dfcdaf9167
commit c2f0582d19
7 changed files with 219 additions and 26 deletions

7
Cargo.lock generated
View File

@ -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",
]

View File

@ -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" }

View File

@ -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<Self, ConfigErr> {
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 ignores = build_ignore_set(&opts.ignore).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 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<PathBuf>,
pub diff_only: bool,
}
impl FixConfig {
pub fn from_opts(opts: Opts) -> Result<Self, ConfigErr> {
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<ReadOnlyVfs, ConfigErr> {
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<String>) -> Result<GlobSet, GlobError> {
let mut set = GlobSetBuilder::new();
for pattern in ignores {
let glob = GlobBuilder::new(&pattern).build()?;
set.add(glob);
}
set.build()
}
fn walk_nix_files<P: AsRef<Path>>(target: P) -> Result<impl Iterator<Item = PathBuf>, io::Error> {
let walker = dirs::Walker::new(target)?;
Ok(walker.filter(|path: &PathBuf| matches!(path.extension(), Some(e) if e == "nix")))
}

View File

@ -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),
}

112
bin/src/fix.rs Normal file
View File

@ -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<Vec<Report>, 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::<Vec<_>>()
}),
_ => None,
})
.flatten()
.collect())
}
fn reorder(mut reports: Vec<Report>) -> Vec<Report> {
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<Report>, 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<Fixed>,
}
#[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<FixResult, RnixParseErr> {
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::<Vec<_>>();
for report in reordered {
report.apply(src.to_mut());
}
Ok(FixResult {
src,
fixed
})
}
pub fn fix(src: &str) -> Result<FixResult, RnixParseErr> {
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");
}

View File

@ -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 {

View File

@ -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();