use std::{ default::Default, fmt, fs, io, path::{Path, PathBuf}, str::FromStr, }; use clap::Clap; use globset::{Error as GlobError, GlobBuilder, GlobSet, GlobSetBuilder}; use vfs::ReadOnlyVfs; use crate::err::ConfigErr; #[derive(Clap, Debug)] #[clap(version, author, about)] pub struct Opts { #[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), /// Print detailed explanation for a lint warning Explain(Explain), } #[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)] ignore: Vec, /// Output format. /// Supported values: stderr, errfmt, json (on feature flag only) #[clap(short = 'o', long, default_value_t, parse(try_from_str))] pub format: OutFormat, } impl Check { pub fn vfs(&self) -> Result { let files = walk_with_ignores(&self.ignore, &self.target)?; vfs(files) } } #[derive(Clap, Debug)] pub struct Fix { /// File or directory to run fix on #[clap(default_value = ".", parse(from_os_str))] target: PathBuf, /// Globs of file patterns to skip #[clap(short, long)] ignore: Vec, /// Do not fix files in place, display a diff instead #[clap(short, long = "dry-run")] pub diff_only: bool, } impl Fix { pub fn vfs(&self) -> Result { let files = walk_with_ignores(&self.ignore, &self.target)?; vfs(files) } } #[derive(Clap, Debug)] pub struct Single { /// File to run single-fix on #[clap(parse(from_os_str))] pub target: Option, /// Position to attempt a fix at #[clap(short, long, parse(try_from_str = parse_line_col))] pub position: (usize, usize), /// Do not fix files in place, display a diff instead #[clap(short, long = "dry-run")] pub diff_only: bool, } #[derive(Clap, Debug)] pub struct Explain { /// Warning code to explain #[clap(parse(try_from_str = parse_warning_code))] pub target: u32, } mod dirs { use std::{ fs, io::{self, Error, ErrorKind}, path::{Path, PathBuf}, }; #[derive(Default, Debug)] pub struct Walker { dirs: Vec, files: Vec, } impl Walker { pub fn new>(target: P) -> io::Result { let target = target.as_ref().to_path_buf(); if !target.exists() { Err(Error::new( ErrorKind::NotFound, format!("file not found: {}", target.display()), )) } else if target.is_dir() { Ok(Self { dirs: vec![target], ..Default::default() }) } else { Ok(Self { files: vec![target], ..Default::default() }) } } } impl Iterator for Walker { type Item = PathBuf; fn next(&mut self) -> Option { if let Some(dir) = self.dirs.pop() { if dir.is_dir() { for entry in fs::read_dir(dir).ok()? { let entry = entry.ok()?; let path = entry.path(); if path.is_dir() { self.dirs.push(path); } else if path.is_file() { self.files.push(path); } } } } self.files.pop() } } } 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 parse_warning_code(src: &str) -> Result { let mut char_stream = src.chars(); let severity = char_stream .next() .ok_or_else(|| ConfigErr::InvalidWarningCode(src.to_owned()))? .to_ascii_lowercase(); match severity { 'w' => char_stream .collect::() .parse::() .map_err(|_| ConfigErr::InvalidWarningCode(src.to_owned())), _ => Ok(0), } } fn build_ignore_set(ignores: &[String]) -> Result { let mut set = GlobSetBuilder::new(); for pattern in ignores { let glob = GlobBuilder::new(pattern).build()?; set.add(glob); } set.build() } fn walk_nix_files>(target: P) -> Result, io::Error> { let walker = dirs::Walker::new(target)?; Ok(walker.filter(|path: &PathBuf| matches!(path.extension(), Some(e) if e == "nix"))) } fn walk_with_ignores>( 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() { if let Ok(data) = fs::read_to_string(&file) { let _id = vfs.alloc_file_id(&file); vfs.set_file_contents(&file, data.as_bytes()); } else { println!("{} contains non-utf8 content", file.display()); }; } 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 fmt::Display for OutFormat { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}", match self { #[cfg(feature = "json")] Self::Json => "json", Self::Errfmt => "errfmt", Self::StdErr => "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), #[cfg(not(feature = "json"))] "json" => Err("statix was not compiled with the `json` feature flag"), "errfmt" => Ok(Self::Errfmt), "stderr" => Ok(Self::StdErr), _ => Err("unknown output format, try: json, errfmt"), } } }