use clap::{AppSettings, Parser};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use crate::git::hooks::CommitHook;
const IGNORED_CLAP_ERRORS: [clap::error::ErrorKind; 2] = [
clap::error::ErrorKind::DisplayHelp,
clap::error::ErrorKind::DisplayVersion,
];
#[allow(clippy::doc_markdown)]
#[derive(Parser, Debug)]
#[clap(
name = "lintje",
version,
long_version = long_version_output(),
verbatim_doc_comment,
setting(AppSettings::DeriveDisplayOrder)
)]
pub struct Lint {
#[clap(long = "no-branch", help_heading = "RULES", parse(from_flag = std::ops::Not::not))]
pub branch_validation: bool,
#[clap(long = "no-hints", help_heading = "RULES", parse(from_flag = std::ops::Not::not))]
pub hints: bool,
#[clap(long = "color", help_heading = "OUTPUT")]
pub color: bool,
#[clap(long = "no-color", help_heading = "OUTPUT")]
pub no_color: bool,
#[clap(
long,
arg_enum,
name = "hook file name",
help_heading = "INSTALLATION",
conflicts_with_all(&["commit (range)", "commit message file path"])
)]
pub install_hook: Option<CommitHook>,
#[clap(
long,
name = "commit message file path",
parse(from_os_str),
conflicts_with_all(&["commit (range)", "hook file name"]),
help_heading = "SELECTION"
)]
pub hook_message_file: Option<PathBuf>,
#[clap(long, help_heading = "OUTPUT")]
pub debug: bool,
#[clap(long, help_heading = "OUTPUT")]
pub verbose: bool,
#[clap(name = "commit (range)", help_heading = "SELECTION")]
pub selection: Option<String>,
}
impl Lint {
pub fn color(&self) -> bool {
if self.no_color {
return false;
}
if self.color {
return true;
}
true }
pub fn merge(&mut self, options: Vec<String>) {
self.update_from(options);
}
}
#[derive(Debug)]
pub struct ValidationContext {
pub changesets: bool,
}
pub fn fetch_options() -> Lint {
let cli_opts = cli_options();
match file_options(env::var("LINTJE_OPTIONS_PATH")) {
Some((path, file_options)) => {
let mut opts = parse_file_options(&path, &file_options);
opts.merge(cli_opts);
opts
}
None => Lint::parse_from(cli_opts),
}
}
fn cli_options() -> Vec<String> {
env::args_os()
.filter_map(|a| match a.into_string() {
Ok(s) => Some(s),
Err(e) => {
eprintln!("Unable to parse CLI argument: '{:?}'", e);
None
}
})
.collect::<Vec<String>>()
}
fn file_options(env_path: Result<String, std::env::VarError>) -> Option<(PathBuf, Vec<String>)> {
match env_path {
Ok(value) => {
let path = Path::new(&value);
if path.is_file() {
match fs::read_to_string(path) {
Ok(contents) => Some((path.to_path_buf(), parse_options_file(&contents))),
Err(e) => {
eprintln!("ERROR: Lintje options file could not be read: {}", e);
None
}
}
} else {
eprintln!(
"ERROR: Configured LINTJE_OPTIONS_PATH does not exist or is not a file. Path: '{}'",
path.display()
);
None
}
}
Err(_) => None,
}
}
fn parse_options_file(contents: &str) -> Vec<String> {
contents
.lines()
.into_iter()
.filter(|line| !line.starts_with('#')) .flat_map(|line| {
line.split(' ')
.map(std::string::ToString::to_string)
.collect::<Vec<String>>()
})
.collect::<Vec<String>>()
}
fn parse_file_options(path: &Path, options: &[String]) -> Lint {
let mut opts = vec!["lintje".to_string()];
opts.append(&mut options.to_owned());
match Lint::try_parse_from(&opts) {
Ok(opts) => opts,
Err(e) => {
if !IGNORED_CLAP_ERRORS.contains(&e.kind()) {
eprintln!("ERROR: Error parsing options file: {:?}", path);
}
e.exit()
}
}
}
fn long_version_output() -> &'static str {
concat!(
clap::crate_version!(),
"\n",
env!("LINTJE_BUILD_TARGET_TRIPLE")
)
}
#[cfg(test)]
mod tests {
use super::{file_options, parse_options_file, Lint};
use crate::test::*;
use clap::Parser;
use std::path::{Path, PathBuf};
fn test_dir(name: &str) -> PathBuf {
Path::new(TEST_DIR).join(name)
}
#[test]
fn color_flags() {
assert!(!Lint::parse_from(["lintje", "--color", "--no-color"]).color());
assert!(Lint::parse_from(["lintje", "--color"]).color());
assert!(!Lint::parse_from(["lintje", "--no-color"]).color());
assert!(Lint::parse_from(["lintje"]).color());
}
#[test]
fn merge_options() {
let mut opts = Lint::parse_from(vec![
"lintje".to_string(),
"--color".to_string(),
"--no-branch".to_string(),
]);
assert!(opts.hints);
opts.merge(vec![
"lintje".to_string(),
"--no-color".to_string(),
"--no-hints".to_string(),
]);
assert!(opts.color);
assert!(opts.no_color);
assert!(!opts.color());
assert!(!opts.branch_validation);
assert!(!opts.hints);
}
#[test]
fn options_file_valid() {
let dir = test_dir("options_file_valid");
let env_path = dir.join("options.txt");
prepare_test_dir(&dir);
create_file(&env_path, b"--color\n--no-hints --no-branch");
let (path, options) =
file_options(Ok(env_path.as_path().display().to_string())).expect("No options");
assert_eq!(path, env_path);
assert_eq!(options, vec!["--color", "--no-hints", "--no-branch"]);
}
#[test]
fn options_file_invalid() {
let env_path = PathBuf::from("test_options.txt");
assert_eq!(
file_options(Ok(env_path.as_path().display().to_string())),
None
);
}
#[test]
fn options_file_none() {
assert_eq!(file_options(Err(std::env::VarError::NotPresent)), None);
}
#[test]
fn parse_options_file_multi_line() {
let options = parse_options_file("--color\n--no-hints\n--no-branch");
assert_eq!(options, vec!["--color", "--no-hints", "--no-branch"]);
}
#[test]
fn parse_options_file_single_line() {
let options = parse_options_file("--color --no-hints --no-branch");
assert_eq!(options, vec!["--color", "--no-hints", "--no-branch"]);
}
#[test]
fn parse_options_file_ignore_comments() {
let options = parse_options_file("# Set color\n--color\n# Disable hints\n--no-hints");
assert_eq!(options, vec!["--color", "--no-hints"]);
}
}