Lints and suggestions for the Nix programming language
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

rework cli, use subcommands instead

Akshay 5f0a1e67 781c42cc

+325 -207
+118 -93
bin/src/config.rs
··· 15 15 #[derive(Clap, Debug)] 16 16 #[clap(version = "0.1.0", author = "Akshay <nerdy@peppe.rs>")] 17 17 pub struct Opts { 18 - /// File or directory to run statix on 19 - #[clap(default_value = ".")] 20 - pub target: String, 18 + #[clap(subcommand)] 19 + pub cmd: SubCommand, 20 + } 21 + 22 + #[derive(Clap, Debug)] 23 + pub enum SubCommand { 24 + /// Lints and suggestions for the nix programming language 25 + Check(Check), 26 + /// Find and fix issues raised by statix-check 27 + Fix(Fix), 28 + /// Fix exactly one issue at provided position 29 + Single(Single), 30 + } 31 + 32 + #[derive(Clap, Debug)] 33 + pub struct Check { 34 + /// File or directory to run check on 35 + #[clap(default_value = ".", parse(from_os_str))] 36 + target: PathBuf, 21 37 22 38 /// Globs of file patterns to skip 23 39 #[clap(short, long)] 24 - pub ignore: Vec<String>, 40 + ignore: Vec<String>, 25 41 26 42 /// Output format. 27 43 /// Supported values: errfmt, json (on feature flag only) 28 - #[clap(short = 'o', long)] 29 - format: Option<OutFormat>, 30 - 31 - /// Find and fix issues raised by statix 32 - #[clap(short = 'f', long)] 33 - pub fix: bool, 34 - 35 - /// Do not fix files in place, display a diff instead 36 - #[clap(short = 'd', long = "dry-run")] 37 - diff_only: bool, 44 + #[clap(short = 'o', long, default_value = "OutFormat::StdErr")] 45 + pub format: OutFormat, 38 46 } 39 47 40 - 41 - #[derive(Debug, Copy, Clone)] 42 - pub enum OutFormat { 43 - #[cfg(feature = "json")] 44 - Json, 45 - Errfmt, 46 - StdErr, 47 - } 48 - 49 - impl Default for OutFormat { 50 - fn default() -> Self { 51 - OutFormat::StdErr 48 + impl Check { 49 + pub fn vfs(&self) -> Result<ReadOnlyVfs, ConfigErr> { 50 + let files = walk_with_ignores(&self.ignore, &self.target)?; 51 + vfs(files) 52 52 } 53 53 } 54 54 55 - impl FromStr for OutFormat { 56 - type Err = &'static str; 55 + #[derive(Clap, Debug)] 56 + pub struct Fix { 57 + /// File or directory to run fix on 58 + #[clap(default_value = ".", parse(from_os_str))] 59 + target: PathBuf, 57 60 58 - fn from_str(value: &str) -> Result<Self, Self::Err> { 59 - match value.to_ascii_lowercase().as_str() { 60 - #[cfg(feature = "json")] "json" => Ok(Self::Json), 61 - "errfmt" => Ok(Self::Errfmt), 62 - "stderr" => Ok(Self::StdErr), 63 - "json" => Err("statix was not compiled with the `json` feature flag"), 64 - _ => Err("unknown output format, try: json, errfmt"), 65 - } 66 - } 67 - } 61 + /// Globs of file patterns to skip 62 + #[clap(short, long)] 63 + ignore: Vec<String>, 68 64 69 - #[derive(Debug)] 70 - pub struct LintConfig { 71 - pub files: Vec<PathBuf>, 72 - pub format: OutFormat, 65 + /// Do not fix files in place, display a diff instead 66 + #[clap(short, long = "dry-run")] 67 + pub diff_only: bool, 73 68 } 74 69 75 - impl LintConfig { 76 - pub fn from_opts(opts: Opts) -> Result<Self, ConfigErr> { 77 - let ignores = build_ignore_set(&opts.ignore).map_err(|err| { 78 - ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) 79 - })?; 80 - 81 - let files = walk_nix_files(&opts.target)? 82 - .filter(|path| !ignores.is_match(path)) 83 - .collect(); 84 - 85 - Ok(Self { 86 - files, 87 - format: opts.format.unwrap_or_default(), 88 - }) 89 - } 90 - 70 + impl Fix { 91 71 pub fn vfs(&self) -> Result<ReadOnlyVfs, ConfigErr> { 92 - let mut vfs = ReadOnlyVfs::default(); 93 - for file in self.files.iter() { 94 - let _id = vfs.alloc_file_id(&file); 95 - let data = fs::read_to_string(&file).map_err(ConfigErr::InvalidPath)?; 96 - vfs.set_file_contents(&file, data.as_bytes()); 97 - } 98 - Ok(vfs) 72 + let files = walk_with_ignores(&self.ignore, &self.target)?; 73 + vfs(files) 99 74 } 100 75 } 101 76 102 - pub struct FixConfig { 103 - pub files: Vec<PathBuf>, 104 - pub diff_only: bool, 105 - } 106 - 107 - impl FixConfig { 108 - pub fn from_opts(opts: Opts) -> Result<Self, ConfigErr> { 109 - let ignores = build_ignore_set(&opts.ignore).map_err(|err| { 110 - ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) 111 - })?; 112 - 113 - let files = walk_nix_files(&opts.target)? 114 - .filter(|path| !ignores.is_match(path)) 115 - .collect(); 116 - 117 - let diff_only = opts.diff_only; 118 - Ok(Self { files, diff_only }) 119 - } 77 + #[derive(Clap, Debug)] 78 + pub struct Single { 79 + /// File to run single-fix on 80 + #[clap(default_value = ".", parse(from_os_str))] 81 + pub target: PathBuf, 120 82 121 - pub fn vfs(&self) -> Result<ReadOnlyVfs, ConfigErr> { 122 - let mut vfs = ReadOnlyVfs::default(); 123 - for file in self.files.iter() { 124 - let _id = vfs.alloc_file_id(&file); 125 - let data = fs::read_to_string(&file).map_err(ConfigErr::InvalidPath)?; 126 - vfs.set_file_contents(&file, data.as_bytes()); 127 - } 128 - Ok(vfs) 129 - } 83 + /// Position to attempt a fix at 84 + #[clap(short, long, parse(try_from_str = parse_line_col))] 85 + pub position: (usize, usize), 130 86 } 131 87 132 88 mod dirs { ··· 185 141 } 186 142 } 187 143 188 - fn build_ignore_set(ignores: &Vec<String>) -> Result<GlobSet, GlobError> { 144 + fn parse_line_col(src: &str) -> Result<(usize, usize), ConfigErr> { 145 + let parts = src.split(","); 146 + match parts.collect::<Vec<_>>().as_slice() { 147 + [line, col] => { 148 + let l = line 149 + .parse::<usize>() 150 + .map_err(|_| ConfigErr::InvalidPosition(src.to_owned()))?; 151 + let c = col 152 + .parse::<usize>() 153 + .map_err(|_| ConfigErr::InvalidPosition(src.to_owned()))?; 154 + Ok((l, c)) 155 + } 156 + _ => Err(ConfigErr::InvalidPosition(src.to_owned())), 157 + } 158 + } 159 + 160 + fn build_ignore_set(ignores: &[String]) -> Result<GlobSet, GlobError> { 189 161 let mut set = GlobSetBuilder::new(); 190 162 for pattern in ignores { 191 163 let glob = GlobBuilder::new(&pattern).build()?; ··· 198 170 let walker = dirs::Walker::new(target)?; 199 171 Ok(walker.filter(|path: &PathBuf| matches!(path.extension(), Some(e) if e == "nix"))) 200 172 } 173 + 174 + fn walk_with_ignores<P: AsRef<Path>>( 175 + ignores: &[String], 176 + target: P, 177 + ) -> Result<Vec<PathBuf>, ConfigErr> { 178 + let ignores = build_ignore_set(ignores).map_err(|err| { 179 + ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) 180 + })?; 181 + 182 + Ok(walk_nix_files(&target)? 183 + .filter(|path| !ignores.is_match(path)) 184 + .collect()) 185 + } 186 + 187 + fn vfs(files: Vec<PathBuf>) -> Result<ReadOnlyVfs, ConfigErr> { 188 + let mut vfs = ReadOnlyVfs::default(); 189 + for file in files.iter() { 190 + let _id = vfs.alloc_file_id(&file); 191 + let data = fs::read_to_string(&file).map_err(ConfigErr::InvalidPath)?; 192 + vfs.set_file_contents(&file, data.as_bytes()); 193 + } 194 + Ok(vfs) 195 + 196 + } 197 + 198 + #[derive(Debug, Copy, Clone)] 199 + pub enum OutFormat { 200 + #[cfg(feature = "json")] 201 + Json, 202 + Errfmt, 203 + StdErr, 204 + } 205 + 206 + impl Default for OutFormat { 207 + fn default() -> Self { 208 + OutFormat::StdErr 209 + } 210 + } 211 + 212 + impl FromStr for OutFormat { 213 + type Err = &'static str; 214 + 215 + fn from_str(value: &str) -> Result<Self, Self::Err> { 216 + match value.to_ascii_lowercase().as_str() { 217 + #[cfg(feature = "json")] 218 + "json" => Ok(Self::Json), 219 + "errfmt" => Ok(Self::Errfmt), 220 + "stderr" => Ok(Self::StdErr), 221 + "json" => Err("statix was not compiled with the `json` feature flag"), 222 + _ => Err("unknown output format, try: json, errfmt"), 223 + } 224 + } 225 + }
+16 -1
bin/src/err.rs
··· 8 8 pub enum ConfigErr { 9 9 #[error("error parsing glob `{0:?}`: {1}")] 10 10 InvalidGlob(Option<String>, ErrorKind), 11 - 12 11 #[error("path error: {0}")] 13 12 InvalidPath(#[from] io::Error), 13 + #[error("unable to parse `{0}` as line and column")] 14 + InvalidPosition(String) 14 15 } 15 16 16 17 #[derive(Error, Debug)] ··· 28 29 } 29 30 30 31 #[derive(Error, Debug)] 32 + pub enum SingleFixErr { 33 + #[error("path error: {0}")] 34 + InvalidPath(#[from] io::Error), 35 + #[error("position out of bounds: line {0}, col {1}")] 36 + OutOfBounds(usize, usize), 37 + #[error("{0} is too large")] 38 + Conversion(usize), 39 + #[error("nothing to fix")] 40 + NoOp, 41 + } 42 + 43 + #[derive(Error, Debug)] 31 44 pub enum StatixErr { 32 45 #[error("linter error: {0}")] 33 46 Lint(#[from] LintErr), 34 47 #[error("fixer error: {0}")] 35 48 Fix(#[from] FixErr), 49 + #[error("single fix error: {0}")] 50 + Single(#[from] SingleFixErr), 36 51 #[error("config error: {0}")] 37 52 Config(#[from] ConfigErr), 38 53 }
+6 -81
bin/src/fix.rs
··· 1 1 use std::borrow::Cow; 2 2 3 - use lib::{Report, LINTS}; 4 - use rnix::{parser::ParseError as RnixParseErr, TextRange, WalkEvent}; 5 - 6 - type Source<'a> = Cow<'a, str>; 3 + use rnix::TextRange; 7 4 8 - fn 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 - } 5 + mod all; 6 + pub use all::all; 27 7 28 - fn reorder(mut reports: Vec<Report>) -> Vec<Report> { 29 - use std::collections::VecDeque; 8 + mod single; 9 + pub use single::single; 30 10 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 - } 11 + type Source<'a> = Cow<'a, str>; 53 12 54 13 #[derive(Debug)] 55 14 pub struct FixResult<'a> { ··· 68 27 Self { src, fixed: Vec::new() } 69 28 } 70 29 } 71 - 72 - impl<'a> Iterator for FixResult<'a> { 73 - type Item = FixResult<'a>; 74 - fn next(&mut self) -> Option<Self::Item> { 75 - let all_reports = collect_fixes(&self.src).ok()?; 76 - if all_reports.is_empty() { 77 - return None; 78 - } 79 - 80 - let reordered = reorder(all_reports); 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(self.src.to_mut()); 90 - } 91 - 92 - Some(FixResult { 93 - src: self.src.clone(), 94 - fixed 95 - }) 96 - } 97 - } 98 - 99 - pub fn fix(src: &str) -> Option<FixResult> { 100 - let src = Cow::from(src); 101 - let _ = rnix::parse(&src).as_result().ok()?; 102 - let initial = FixResult::empty(src); 103 - initial.into_iter().last() 104 - }
+86
bin/src/fix/all.rs
··· 1 + use std::borrow::Cow; 2 + 3 + use lib::{Report, LINTS}; 4 + use rnix::{parser::ParseError as RnixParseErr, WalkEvent}; 5 + 6 + use crate::fix::{Fixed, FixResult}; 7 + 8 + fn 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 + 28 + fn 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 + impl<'a> Iterator for FixResult<'a> { 55 + type Item = FixResult<'a>; 56 + fn next(&mut self) -> Option<Self::Item> { 57 + let all_reports = collect_fixes(&self.src).ok()?; 58 + if all_reports.is_empty() { 59 + return None; 60 + } 61 + 62 + let reordered = reorder(all_reports); 63 + let fixed = reordered 64 + .iter() 65 + .map(|r| Fixed { 66 + at: r.range(), 67 + code: r.code, 68 + }) 69 + .collect::<Vec<_>>(); 70 + for report in reordered { 71 + report.apply(self.src.to_mut()); 72 + } 73 + 74 + Some(FixResult { 75 + src: self.src.clone(), 76 + fixed 77 + }) 78 + } 79 + } 80 + 81 + pub fn all(src: &str) -> Option<FixResult> { 82 + let src = Cow::from(src); 83 + let _ = rnix::parse(&src).as_result().ok()?; 84 + let initial = FixResult::empty(src); 85 + initial.into_iter().last() 86 + }
+59
bin/src/fix/single.rs
··· 1 + use std::{borrow::Cow, convert::TryFrom}; 2 + 3 + use lib::{Report, LINTS}; 4 + use rnix::{TextRange, TextSize}; 5 + 6 + use crate::err::SingleFixErr; 7 + use crate::fix::Source; 8 + 9 + pub struct SingleFixResult<'δ> { 10 + pub src: Source<'δ>, 11 + } 12 + 13 + fn pos_to_byte(line: usize, col: usize, src: &str) -> Result<TextSize, SingleFixErr> { 14 + let mut byte: TextSize = TextSize::of(""); 15 + for (_, l) in src.lines().enumerate().take_while(|(i, _)| i <= &line) { 16 + byte += TextSize::of(l); 17 + } 18 + byte += TextSize::try_from(col).map_err(|_| SingleFixErr::Conversion(col))?; 19 + 20 + if usize::from(byte) >= src.len() { 21 + Err(SingleFixErr::OutOfBounds(line, col)) 22 + } else { 23 + Ok(byte) 24 + } 25 + } 26 + 27 + fn find(offset: TextSize, src: &str) -> Result<Report, SingleFixErr> { 28 + // we don't really need the source to form a completely parsed tree 29 + let parsed = rnix::parse(src); 30 + 31 + let elem_at = parsed 32 + .node() 33 + .child_or_token_at_range(TextRange::empty(offset)) 34 + .ok_or(SingleFixErr::NoOp)?; 35 + 36 + LINTS 37 + .get(&elem_at.kind()) 38 + .map(|rules| { 39 + rules 40 + .iter() 41 + .filter_map(|rule| rule.validate(&elem_at)) 42 + .filter(|report| report.total_suggestion_range().is_some()) 43 + .next() 44 + }) 45 + .flatten() 46 + .ok_or(SingleFixErr::NoOp) 47 + } 48 + 49 + pub fn single(line: usize, col: usize, src: &str) -> Result<SingleFixResult, SingleFixErr> { 50 + let mut src = Cow::from(src); 51 + let offset = pos_to_byte(line, col, &*src)?; 52 + let report = find(offset, &*src)?; 53 + 54 + report.apply(src.to_mut()); 55 + 56 + Ok(SingleFixResult { 57 + src 58 + }) 59 + }
+40 -32
bin/src/main.rs
··· 6 6 7 7 use std::io; 8 8 9 - use crate::{err::{StatixErr, FixErr}, traits::WriteDiagnostic}; 9 + use crate::{err::{StatixErr, FixErr, SingleFixErr}, traits::WriteDiagnostic}; 10 10 11 11 use clap::Clap; 12 - use config::{FixConfig, LintConfig, Opts}; 12 + use config::{Opts, SubCommand}; 13 13 use similar::TextDiff; 14 14 15 15 fn _main() -> Result<(), StatixErr> { 16 16 let opts = Opts::parse(); 17 - if opts.fix { 18 - let fix_config = FixConfig::from_opts(opts)?; 19 - let vfs = fix_config.vfs()?; 20 - for entry in vfs.iter() { 21 - if let Some(fix_result) = fix::fix(entry.contents) { 22 - if fix_config.diff_only { 23 - let text_diff = TextDiff::from_lines(entry.contents, &fix_result.src); 24 - let old_file = format!("{}", entry.file_path.display()); 25 - let new_file = format!("{} [fixed]", entry.file_path.display()); 26 - println!( 27 - "{}", 28 - text_diff 17 + match opts.cmd { 18 + SubCommand::Check(check_config) => { 19 + let vfs = check_config.vfs()?; 20 + let (lints, errors): (Vec<_>, Vec<_>) = vfs.iter().map(lint::lint).partition(Result::is_ok); 21 + let lint_results = lints.into_iter().map(Result::unwrap); 22 + let errors = errors.into_iter().map(Result::unwrap_err); 23 + 24 + let mut stdout = io::stdout(); 25 + lint_results.for_each(|r| { 26 + stdout.write(&r, &vfs, check_config.format).unwrap(); 27 + }); 28 + errors.for_each(|e| { 29 + eprintln!("{}", e); 30 + }); 31 + }, 32 + SubCommand::Fix(fix_config) => { 33 + let vfs = fix_config.vfs()?; 34 + for entry in vfs.iter() { 35 + if let Some(fix_result) = fix::all(entry.contents) { 36 + if fix_config.diff_only { 37 + let text_diff = TextDiff::from_lines(entry.contents, &fix_result.src); 38 + let old_file = format!("{}", entry.file_path.display()); 39 + let new_file = format!("{} [fixed]", entry.file_path.display()); 40 + println!( 41 + "{}", 42 + text_diff 29 43 .unified_diff() 30 44 .context_radius(4) 31 45 .header(&old_file, &new_file) 32 - ); 33 - } else { 34 - let path = entry.file_path; 35 - std::fs::write(path, &*fix_result.src).map_err(FixErr::InvalidPath)?; 46 + ); 47 + } else { 48 + let path = entry.file_path; 49 + std::fs::write(path, &*fix_result.src).map_err(FixErr::InvalidPath)?; 50 + } 36 51 } 37 52 } 53 + }, 54 + SubCommand::Single(single_config) => { 55 + let path = single_config.target; 56 + let src = std::fs::read_to_string(&path).map_err(SingleFixErr::InvalidPath)?; 57 + let (line, col) = single_config.position; 58 + let single_result = fix::single(line, col, &src)?; 59 + std::fs::write(&path, &*single_result.src).map_err(SingleFixErr::InvalidPath)?; 38 60 } 39 - } else { 40 - let lint_config = LintConfig::from_opts(opts)?; 41 - let vfs = lint_config.vfs()?; 42 - let (lints, errors): (Vec<_>, Vec<_>) = vfs.iter().map(lint::lint).partition(Result::is_ok); 43 - let lint_results = lints.into_iter().map(Result::unwrap); 44 - let errors = errors.into_iter().map(Result::unwrap_err); 45 - 46 - let mut stdout = io::stdout(); 47 - lint_results.for_each(|r| { 48 - stdout.write(&r, &vfs, lint_config.format).unwrap(); 49 - }); 50 - errors.for_each(|e| { 51 - eprintln!("{}", e); 52 - }); 53 61 } 54 62 Ok(()) 55 63 }