mirror of
https://github.com/nerdypepper/statix.git
synced 2024-09-11 13:45:32 +03:00
initial implementation of multipass code fixer
This commit is contained in:
parent
dfcdaf9167
commit
c2f0582d19
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
@ -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" }
|
||||
|
@ -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")))
|
||||
}
|
||||
|
@ -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
112
bin/src/fix.rs
Normal 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");
|
||||
}
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user