Lints and suggestions for the Nix programming language
1
fork

Configure Feed

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

initial implementation of multipass code fixer

Akshay c2f0582d dfcdaf91

+219 -26
+7
Cargo.lock
··· 330 330 checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 331 331 332 332 [[package]] 333 + name = "similar" 334 + version = "2.1.0" 335 + source = "registry+https://github.com/rust-lang/crates.io-index" 336 + checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3" 337 + 338 + [[package]] 333 339 name = "smol_str" 334 340 version = "0.1.18" 335 341 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 344 350 "globset", 345 351 "lib", 346 352 "rnix", 353 + "similar", 347 354 "thiserror", 348 355 "vfs", 349 356 ]
+2 -1
bin/Cargo.toml
··· 6 6 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 7 8 8 [dependencies] 9 - lib = { path = "../lib" } 10 9 ariadne = "0.1.3" 11 10 rnix = "0.9.0" 12 11 clap = "3.0.0-beta.4" 13 12 globset = "0.4.8" 14 13 thiserror = "1.0.30" 14 + similar = "2.1.0" 15 15 vfs = { path = "../vfs" } 16 + lib = { path = "../lib" }
+60 -18
bin/src/config.rs
··· 1 - use std::{default::Default, fs, path::PathBuf, str::FromStr}; 1 + use std::{ 2 + default::Default, 3 + fs, io, 4 + path::{Path, PathBuf}, 5 + str::FromStr, 6 + }; 2 7 3 8 use clap::Clap; 4 - use globset::{GlobBuilder, GlobSetBuilder}; 9 + use globset::{Error as GlobError, GlobBuilder, GlobSet, GlobSetBuilder}; 5 10 use vfs::ReadOnlyVfs; 6 11 7 12 use crate::err::ConfigErr; ··· 77 82 78 83 impl LintConfig { 79 84 pub fn from_opts(opts: Opts) -> Result<Self, ConfigErr> { 80 - let ignores = { 81 - let mut set = GlobSetBuilder::new(); 82 - for pattern in opts.ignore { 83 - let glob = GlobBuilder::new(&pattern).build().map_err(|err| { 84 - ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) 85 - })?; 86 - set.add(glob); 87 - } 88 - set.build().map_err(|err| { 89 - ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) 90 - }) 91 - }?; 92 - 93 - let walker = dirs::Walker::new(opts.target).map_err(ConfigErr::InvalidPath)?; 85 + let ignores = build_ignore_set(&opts.ignore).map_err(|err| { 86 + ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) 87 + })?; 94 88 95 - let files = walker 96 - .filter(|path| matches!(path.extension(), Some(e) if e == "nix")) 89 + let files = walk_nix_files(&opts.target)? 97 90 .filter(|path| !ignores.is_match(path)) 98 91 .collect(); 92 + 99 93 Ok(Self { 100 94 files, 101 95 format: opts.format.unwrap_or_default(), ··· 113 107 } 114 108 } 115 109 110 + pub struct FixConfig { 111 + pub files: Vec<PathBuf>, 112 + pub diff_only: bool, 113 + } 114 + 115 + impl FixConfig { 116 + pub fn from_opts(opts: Opts) -> Result<Self, ConfigErr> { 117 + let ignores = build_ignore_set(&opts.ignore).map_err(|err| { 118 + ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone()) 119 + })?; 120 + 121 + let files = walk_nix_files(&opts.target)? 122 + .filter(|path| !ignores.is_match(path)) 123 + .collect(); 124 + 125 + let diff_only = match opts.subcmd { 126 + Some(SubCommand::Fix(f)) => f.diff_only, 127 + _ => false, 128 + }; 129 + 130 + Ok(Self { files, diff_only }) 131 + } 132 + 133 + pub fn vfs(&self) -> Result<ReadOnlyVfs, ConfigErr> { 134 + let mut vfs = ReadOnlyVfs::default(); 135 + for file in self.files.iter() { 136 + let _id = vfs.alloc_file_id(&file); 137 + let data = fs::read_to_string(&file).map_err(ConfigErr::InvalidPath)?; 138 + vfs.set_file_contents(&file, data.as_bytes()); 139 + } 140 + Ok(vfs) 141 + } 142 + } 143 + 116 144 mod dirs { 117 145 use std::{ 118 146 fs, ··· 168 196 } 169 197 } 170 198 } 199 + 200 + fn build_ignore_set(ignores: &Vec<String>) -> Result<GlobSet, GlobError> { 201 + let mut set = GlobSetBuilder::new(); 202 + for pattern in ignores { 203 + let glob = GlobBuilder::new(&pattern).build()?; 204 + set.add(glob); 205 + } 206 + set.build() 207 + } 208 + 209 + fn walk_nix_files<P: AsRef<Path>>(target: P) -> Result<impl Iterator<Item = PathBuf>, io::Error> { 210 + let walker = dirs::Walker::new(target)?; 211 + Ok(walker.filter(|path: &PathBuf| matches!(path.extension(), Some(e) if e == "nix"))) 212 + }
+8
bin/src/err.rs
··· 20 20 } 21 21 22 22 #[derive(Error, Debug)] 23 + pub enum FixErr { 24 + #[error("[{0}] syntax error: {1}")] 25 + Parse(PathBuf, ParseError), 26 + } 27 + 28 + #[derive(Error, Debug)] 23 29 pub enum StatixErr { 24 30 #[error("linter error: {0}")] 25 31 Lint(#[from] LintErr), 32 + #[error("fixer error: {0}")] 33 + Fix(#[from] FixErr), 26 34 #[error("config error: {0}")] 27 35 Config(#[from] ConfigErr), 28 36 }
+112
bin/src/fix.rs
··· 1 + use std::borrow::Cow; 2 + 3 + use lib::{Report, LINTS}; 4 + use rnix::{parser::ParseError as RnixParseErr, TextRange, WalkEvent}; 5 + 6 + type Source<'a> = Cow<'a, str>; 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 + #[derive(Debug)] 55 + pub struct FixResult<'a> { 56 + pub src: Source<'a>, 57 + pub fixed: Vec<Fixed>, 58 + } 59 + 60 + #[derive(Debug, Clone)] 61 + pub struct Fixed { 62 + pub at: TextRange, 63 + pub code: u32, 64 + } 65 + 66 + impl<'a> FixResult<'a> { 67 + fn empty(src: Source<'a>) -> Self { 68 + Self { src, fixed: vec![] } 69 + } 70 + } 71 + 72 + fn next(mut src: Source) -> Result<FixResult, RnixParseErr> { 73 + let all_reports = collect_fixes(&src)?; 74 + 75 + if all_reports.is_empty() { 76 + return Ok(FixResult::empty(src)); 77 + } 78 + 79 + let reordered = reorder(all_reports); 80 + 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(src.to_mut()); 90 + } 91 + 92 + Ok(FixResult { 93 + src, 94 + fixed 95 + }) 96 + } 97 + 98 + pub fn fix(src: &str) -> Result<FixResult, RnixParseErr> { 99 + let src = Cow::from(src); 100 + let _ = rnix::parse(&src).as_result()?; 101 + let mut initial = FixResult::empty(src); 102 + 103 + while let Ok(next_result) = next(initial.src) { 104 + if next_result.fixed.is_empty() { 105 + return Ok(next_result); 106 + } else { 107 + initial = FixResult::empty(next_result.src); 108 + } 109 + } 110 + 111 + unreachable!("a fix caused a syntax error, please report a bug"); 112 + }
+2 -2
bin/src/lint.rs
··· 1 1 use crate::err::LintErr; 2 2 3 - use lib::{LINTS, Report}; 3 + use lib::{Report, LINTS}; 4 4 use rnix::WalkEvent; 5 - use vfs::{VfsEntry, FileId}; 5 + use vfs::{FileId, VfsEntry}; 6 6 7 7 #[derive(Debug)] 8 8 pub struct LintResult {
+28 -5
bin/src/main.rs
··· 1 1 mod config; 2 2 mod err; 3 + mod fix; 3 4 mod lint; 4 5 mod traits; 5 6 6 7 use std::io; 7 8 8 - use crate::{err::StatixErr, traits::WriteDiagnostic}; 9 + use crate::{ 10 + err::{FixErr, StatixErr}, 11 + traits::WriteDiagnostic, 12 + }; 9 13 10 14 use clap::Clap; 11 - use config::{LintConfig, Opts, SubCommand}; 15 + use config::{FixConfig, LintConfig, Opts, SubCommand}; 16 + use similar::TextDiff; 12 17 13 18 fn _main() -> Result<(), StatixErr> { 14 19 let opts = Opts::parse(); 15 20 match opts.subcmd { 16 21 Some(SubCommand::Fix(_)) => { 17 - eprintln!("`fix` not yet supported"); 22 + let fix_config = FixConfig::from_opts(opts)?; 23 + let vfs = fix_config.vfs()?; 24 + for entry in vfs.iter() { 25 + match fix::fix(entry.contents) { 26 + Ok(fix_result) => { 27 + let text_diff = TextDiff::from_lines(entry.contents, &fix_result.src); 28 + let old_file = format!("{}", entry.file_path.display()); 29 + let new_file = format!("{} [fixed]", entry.file_path.display()); 30 + println!( 31 + "{}", 32 + text_diff 33 + .unified_diff() 34 + .context_radius(4) 35 + .header(&old_file, &new_file) 36 + ); 37 + } 38 + Err(e) => eprintln!("{}", FixErr::Parse(entry.file_path.to_path_buf(), e)), 39 + } 40 + } 18 41 } 19 42 None => { 20 43 let lint_config = LintConfig::from_opts(opts)?; 21 44 let vfs = lint_config.vfs()?; 22 - let (reports, errors): (Vec<_>, Vec<_>) = 45 + let (lints, errors): (Vec<_>, Vec<_>) = 23 46 vfs.iter().map(lint::lint).partition(Result::is_ok); 24 - let lint_results = reports.into_iter().map(Result::unwrap); 47 + let lint_results = lints.into_iter().map(Result::unwrap); 25 48 let errors = errors.into_iter().map(Result::unwrap_err); 26 49 27 50 let mut stderr = io::stderr();