From 5f0a1e67c64082c848418daa2b51020eb42c5c12 Mon Sep 17 00:00:00 2001 From: Akshay Date: Mon, 25 Oct 2021 22:08:52 +0530 Subject: rework cli, use subcommands instead --- bin/src/config.rs | 213 ++++++++++++++++++++++++++++---------------------- bin/src/err.rs | 17 +++- bin/src/fix.rs | 87 ++------------------- bin/src/fix/all.rs | 86 ++++++++++++++++++++ bin/src/fix/single.rs | 59 ++++++++++++++ bin/src/main.rs | 72 +++++++++-------- 6 files changed, 326 insertions(+), 208 deletions(-) create mode 100644 bin/src/fix/all.rs create mode 100644 bin/src/fix/single.rs diff --git a/bin/src/config.rs b/bin/src/config.rs index 6d7bc49..202ff6c 100644 --- a/bin/src/config.rs +++ b/bin/src/config.rs @@ -15,118 +15,74 @@ use crate::err::ConfigErr; #[derive(Clap, Debug)] #[clap(version = "0.1.0", author = "Akshay ")] pub struct Opts { - /// File or directory to run statix on - #[clap(default_value = ".")] - pub target: String, + #[clap(subcommand)] + pub cmd: SubCommand, +} + +#[derive(Clap, Debug)] +pub enum SubCommand { + /// Lints and suggestions for the nix programming language + Check(Check), + /// Find and fix issues raised by statix-check + Fix(Fix), + /// Fix exactly one issue at provided position + Single(Single), +} + +#[derive(Clap, Debug)] +pub struct Check { + /// File or directory to run check on + #[clap(default_value = ".", parse(from_os_str))] + target: PathBuf, /// Globs of file patterns to skip #[clap(short, long)] - pub ignore: Vec, + ignore: Vec, /// Output format. /// Supported values: errfmt, json (on feature flag only) - #[clap(short = 'o', long)] - format: Option, - - /// Find and fix issues raised by statix - #[clap(short = 'f', long)] - pub fix: bool, - - /// Do not fix files in place, display a diff instead - #[clap(short = 'd', long = "dry-run")] - diff_only: bool, -} - - -#[derive(Debug, Copy, Clone)] -pub enum OutFormat { - #[cfg(feature = "json")] - Json, - Errfmt, - StdErr, + #[clap(short = 'o', long, default_value = "OutFormat::StdErr")] + pub format: OutFormat, } -impl Default for OutFormat { - fn default() -> Self { - OutFormat::StdErr +impl Check { + pub fn vfs(&self) -> Result { + let files = walk_with_ignores(&self.ignore, &self.target)?; + vfs(files) } } -impl FromStr for OutFormat { - type Err = &'static str; +#[derive(Clap, Debug)] +pub struct Fix { + /// File or directory to run fix on + #[clap(default_value = ".", parse(from_os_str))] + target: PathBuf, - fn from_str(value: &str) -> Result { - match value.to_ascii_lowercase().as_str() { - #[cfg(feature = "json")] "json" => Ok(Self::Json), - "errfmt" => Ok(Self::Errfmt), - "stderr" => Ok(Self::StdErr), - "json" => Err("statix was not compiled with the `json` feature flag"), - _ => Err("unknown output format, try: json, errfmt"), - } - } -} + /// Globs of file patterns to skip + #[clap(short, long)] + ignore: Vec, -#[derive(Debug)] -pub struct LintConfig { - pub files: Vec, - pub format: OutFormat, + /// Do not fix files in place, display a diff instead + #[clap(short, long = "dry-run")] + pub diff_only: bool, } -impl LintConfig { - 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(); - - Ok(Self { - files, - format: opts.format.unwrap_or_default(), - }) - } - +impl Fix { 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) + let files = walk_with_ignores(&self.ignore, &self.target)?; + vfs(files) } } -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 = opts.diff_only; - 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) - } +#[derive(Clap, Debug)] +pub struct Single { + /// File to run single-fix on + #[clap(default_value = ".", parse(from_os_str))] + pub target: PathBuf, + + /// Position to attempt a fix at + #[clap(short, long, parse(try_from_str = parse_line_col))] + pub position: (usize, usize), } mod dirs { @@ -185,7 +141,23 @@ mod dirs { } } -fn build_ignore_set(ignores: &Vec) -> Result { +fn parse_line_col(src: &str) -> Result<(usize, usize), ConfigErr> { + let parts = src.split(","); + match parts.collect::>().as_slice() { + [line, col] => { + let l = line + .parse::() + .map_err(|_| ConfigErr::InvalidPosition(src.to_owned()))?; + let c = col + .parse::() + .map_err(|_| ConfigErr::InvalidPosition(src.to_owned()))?; + Ok((l, c)) + } + _ => Err(ConfigErr::InvalidPosition(src.to_owned())), + } +} + +fn build_ignore_set(ignores: &[String]) -> Result { let mut set = GlobSetBuilder::new(); for pattern in ignores { let glob = GlobBuilder::new(&pattern).build()?; @@ -198,3 +170,56 @@ fn walk_nix_files>(target: P) -> Result>( + ignores: &[String], + target: P, +) -> Result, ConfigErr> { + let ignores = build_ignore_set(ignores).map_err(|err| { + ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) + })?; + + Ok(walk_nix_files(&target)? + .filter(|path| !ignores.is_match(path)) + .collect()) +} + +fn vfs(files: Vec) -> Result { + let mut vfs = ReadOnlyVfs::default(); + for file in 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) + +} + +#[derive(Debug, Copy, Clone)] +pub enum OutFormat { + #[cfg(feature = "json")] + Json, + Errfmt, + StdErr, +} + +impl Default for OutFormat { + fn default() -> Self { + OutFormat::StdErr + } +} + +impl FromStr for OutFormat { + type Err = &'static str; + + fn from_str(value: &str) -> Result { + match value.to_ascii_lowercase().as_str() { + #[cfg(feature = "json")] + "json" => Ok(Self::Json), + "errfmt" => Ok(Self::Errfmt), + "stderr" => Ok(Self::StdErr), + "json" => Err("statix was not compiled with the `json` feature flag"), + _ => Err("unknown output format, try: json, errfmt"), + } + } +} diff --git a/bin/src/err.rs b/bin/src/err.rs index 727e0cc..5f71b57 100644 --- a/bin/src/err.rs +++ b/bin/src/err.rs @@ -8,9 +8,10 @@ use thiserror::Error; pub enum ConfigErr { #[error("error parsing glob `{0:?}`: {1}")] InvalidGlob(Option, ErrorKind), - #[error("path error: {0}")] InvalidPath(#[from] io::Error), + #[error("unable to parse `{0}` as line and column")] + InvalidPosition(String) } #[derive(Error, Debug)] @@ -27,12 +28,26 @@ pub enum FixErr { InvalidPath(#[from] io::Error), } +#[derive(Error, Debug)] +pub enum SingleFixErr { + #[error("path error: {0}")] + InvalidPath(#[from] io::Error), + #[error("position out of bounds: line {0}, col {1}")] + OutOfBounds(usize, usize), + #[error("{0} is too large")] + Conversion(usize), + #[error("nothing to fix")] + NoOp, +} + #[derive(Error, Debug)] pub enum StatixErr { #[error("linter error: {0}")] Lint(#[from] LintErr), #[error("fixer error: {0}")] Fix(#[from] FixErr), + #[error("single fix error: {0}")] + Single(#[from] SingleFixErr), #[error("config error: {0}")] Config(#[from] ConfigErr), } diff --git a/bin/src/fix.rs b/bin/src/fix.rs index d9087fe..a7ddc4f 100644 --- a/bin/src/fix.rs +++ b/bin/src/fix.rs @@ -1,55 +1,14 @@ use std::borrow::Cow; -use lib::{Report, LINTS}; -use rnix::{parser::ParseError as RnixParseErr, TextRange, WalkEvent}; +use rnix::TextRange; -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()) -} +mod all; +pub use all::all; -fn reorder(mut reports: Vec) -> Vec { - use std::collections::VecDeque; +mod single; +pub use single::single; - 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() -} +type Source<'a> = Cow<'a, str>; #[derive(Debug)] pub struct FixResult<'a> { @@ -68,37 +27,3 @@ impl<'a> FixResult<'a> { Self { src, fixed: Vec::new() } } } - -impl<'a> Iterator for FixResult<'a> { - type Item = FixResult<'a>; - fn next(&mut self) -> Option { - let all_reports = collect_fixes(&self.src).ok()?; - if all_reports.is_empty() { - return None; - } - - 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(self.src.to_mut()); - } - - Some(FixResult { - src: self.src.clone(), - fixed - }) - } -} - -pub fn fix(src: &str) -> Option { - let src = Cow::from(src); - let _ = rnix::parse(&src).as_result().ok()?; - let initial = FixResult::empty(src); - initial.into_iter().last() -} diff --git a/bin/src/fix/all.rs b/bin/src/fix/all.rs new file mode 100644 index 0000000..8c0770d --- /dev/null +++ b/bin/src/fix/all.rs @@ -0,0 +1,86 @@ +use std::borrow::Cow; + +use lib::{Report, LINTS}; +use rnix::{parser::ParseError as RnixParseErr, WalkEvent}; + +use crate::fix::{Fixed, FixResult}; + +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() +} + +impl<'a> Iterator for FixResult<'a> { + type Item = FixResult<'a>; + fn next(&mut self) -> Option { + let all_reports = collect_fixes(&self.src).ok()?; + if all_reports.is_empty() { + return None; + } + + 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(self.src.to_mut()); + } + + Some(FixResult { + src: self.src.clone(), + fixed + }) + } +} + +pub fn all(src: &str) -> Option { + let src = Cow::from(src); + let _ = rnix::parse(&src).as_result().ok()?; + let initial = FixResult::empty(src); + initial.into_iter().last() +} diff --git a/bin/src/fix/single.rs b/bin/src/fix/single.rs new file mode 100644 index 0000000..d430693 --- /dev/null +++ b/bin/src/fix/single.rs @@ -0,0 +1,59 @@ +use std::{borrow::Cow, convert::TryFrom}; + +use lib::{Report, LINTS}; +use rnix::{TextRange, TextSize}; + +use crate::err::SingleFixErr; +use crate::fix::Source; + +pub struct SingleFixResult<'δ> { + pub src: Source<'δ>, +} + +fn pos_to_byte(line: usize, col: usize, src: &str) -> Result { + let mut byte: TextSize = TextSize::of(""); + for (_, l) in src.lines().enumerate().take_while(|(i, _)| i <= &line) { + byte += TextSize::of(l); + } + byte += TextSize::try_from(col).map_err(|_| SingleFixErr::Conversion(col))?; + + if usize::from(byte) >= src.len() { + Err(SingleFixErr::OutOfBounds(line, col)) + } else { + Ok(byte) + } +} + +fn find(offset: TextSize, src: &str) -> Result { + // we don't really need the source to form a completely parsed tree + let parsed = rnix::parse(src); + + let elem_at = parsed + .node() + .child_or_token_at_range(TextRange::empty(offset)) + .ok_or(SingleFixErr::NoOp)?; + + LINTS + .get(&elem_at.kind()) + .map(|rules| { + rules + .iter() + .filter_map(|rule| rule.validate(&elem_at)) + .filter(|report| report.total_suggestion_range().is_some()) + .next() + }) + .flatten() + .ok_or(SingleFixErr::NoOp) +} + +pub fn single(line: usize, col: usize, src: &str) -> Result { + let mut src = Cow::from(src); + let offset = pos_to_byte(line, col, &*src)?; + let report = find(offset, &*src)?; + + report.apply(src.to_mut()); + + Ok(SingleFixResult { + src + }) +} diff --git a/bin/src/main.rs b/bin/src/main.rs index d0f69a0..9c57d91 100644 --- a/bin/src/main.rs +++ b/bin/src/main.rs @@ -6,50 +6,58 @@ mod traits; use std::io; -use crate::{err::{StatixErr, FixErr}, traits::WriteDiagnostic}; +use crate::{err::{StatixErr, FixErr, SingleFixErr}, traits::WriteDiagnostic}; use clap::Clap; -use config::{FixConfig, LintConfig, Opts}; +use config::{Opts, SubCommand}; use similar::TextDiff; fn _main() -> Result<(), StatixErr> { let opts = Opts::parse(); - if opts.fix { - let fix_config = FixConfig::from_opts(opts)?; - let vfs = fix_config.vfs()?; - for entry in vfs.iter() { - if let Some(fix_result) = fix::fix(entry.contents) { - if fix_config.diff_only { - 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 + match opts.cmd { + SubCommand::Check(check_config) => { + let vfs = check_config.vfs()?; + let (lints, errors): (Vec<_>, Vec<_>) = vfs.iter().map(lint::lint).partition(Result::is_ok); + let lint_results = lints.into_iter().map(Result::unwrap); + let errors = errors.into_iter().map(Result::unwrap_err); + + let mut stdout = io::stdout(); + lint_results.for_each(|r| { + stdout.write(&r, &vfs, check_config.format).unwrap(); + }); + errors.for_each(|e| { + eprintln!("{}", e); + }); + }, + SubCommand::Fix(fix_config) => { + let vfs = fix_config.vfs()?; + for entry in vfs.iter() { + if let Some(fix_result) = fix::all(entry.contents) { + if fix_config.diff_only { + 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) - ); - } else { - let path = entry.file_path; - std::fs::write(path, &*fix_result.src).map_err(FixErr::InvalidPath)?; + ); + } else { + let path = entry.file_path; + std::fs::write(path, &*fix_result.src).map_err(FixErr::InvalidPath)?; + } } } + }, + SubCommand::Single(single_config) => { + let path = single_config.target; + let src = std::fs::read_to_string(&path).map_err(SingleFixErr::InvalidPath)?; + let (line, col) = single_config.position; + let single_result = fix::single(line, col, &src)?; + std::fs::write(&path, &*single_result.src).map_err(SingleFixErr::InvalidPath)?; } - } else { - let lint_config = LintConfig::from_opts(opts)?; - let vfs = lint_config.vfs()?; - let (lints, errors): (Vec<_>, Vec<_>) = vfs.iter().map(lint::lint).partition(Result::is_ok); - let lint_results = lints.into_iter().map(Result::unwrap); - let errors = errors.into_iter().map(Result::unwrap_err); - - let mut stdout = io::stdout(); - lint_results.for_each(|r| { - stdout.write(&r, &vfs, lint_config.format).unwrap(); - }); - errors.for_each(|e| { - eprintln!("{}", e); - }); } Ok(()) } -- cgit v1.2.3