use std::{collections::HashSet, fs};
use crate::{linter::Linter, path::AbsPath};
use anyhow::{bail, ensure, Context, Result};
use glob::Pattern;
use log::debug;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct LintRunnerConfig {
#[serde(rename = "linter")]
pub linters: Vec<LintConfig>,
}
fn is_false(b: &bool) -> bool {
!(*b)
}
#[derive(Serialize, Deserialize, Clone)]
pub struct LintConfig {
pub code: String,
pub include_patterns: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exclude_patterns: Option<Vec<String>>,
pub command: Vec<String>,
pub init_command: Option<Vec<String>>,
#[serde(skip_serializing_if = "is_false", default = "bool::default")]
pub is_formatter: bool,
}
pub fn get_linters_from_config(
linter_configs: &[LintConfig],
skipped_linters: Option<HashSet<String>>,
taken_linters: Option<HashSet<String>>,
config_path: &AbsPath,
) -> Result<Vec<Linter>> {
let mut linters = Vec::new();
for lint_config in linter_configs {
let include_patterns = patterns_from_strs(&lint_config.include_patterns)?;
let exclude_patterns = if let Some(exclude_patterns) = &lint_config.exclude_patterns {
patterns_from_strs(exclude_patterns)?
} else {
Vec::new()
};
ensure!(
!lint_config.command.is_empty(),
"Invalid linter configuration: '{}' has an empty command list.",
lint_config.code
);
linters.push(Linter {
code: lint_config.code.clone(),
include_patterns,
exclude_patterns,
commands: lint_config.command.clone(),
init_commands: lint_config.init_command.clone(),
config_path: config_path.clone(),
});
}
let all_linters = linters
.iter()
.map(|l| &l.code)
.cloned()
.collect::<HashSet<_>>();
debug!("Found linters: {:?}", all_linters,);
if let Some(taken_linters) = taken_linters {
debug!("Taking linters: {:?}", taken_linters);
for linter in &taken_linters {
ensure!(
all_linters.contains(linter),
"Unknown linter specified in --take: {}. These linters are available: {:?}",
linter,
all_linters,
);
}
linters = linters
.into_iter()
.filter(|linter| taken_linters.contains(&linter.code))
.collect();
}
if let Some(skipped_linters) = skipped_linters {
debug!("Skipping linters: {:?}", skipped_linters);
for linter in &skipped_linters {
ensure!(
all_linters.contains(linter),
"Unknown linter specified in --skip: {}. These linters are available: {:?}",
linter,
all_linters,
);
}
linters = linters
.into_iter()
.filter(|linter| !skipped_linters.contains(&linter.code))
.collect();
}
Ok(linters)
}
impl LintRunnerConfig {
pub fn new(path: &AbsPath) -> Result<LintRunnerConfig> {
let lint_config = fs::read_to_string(&path)
.context(format!("Failed to read config file: '{}'.", path.display()))?;
LintRunnerConfig::new_from_string(&lint_config)
}
pub fn new_from_string(config_str: &str) -> Result<LintRunnerConfig> {
let config: LintRunnerConfig =
toml::from_str(config_str).context("Config file had invalid schema")?;
for linter in &config.linters {
if let Some(init_args) = &linter.init_command {
if init_args.iter().all(|arg| !arg.contains("{{DRYRUN}}")) {
bail!(
"Config for linter {} defines init args \
but does not take a {{{{DRYRUN}}}} argument.",
linter.code
);
}
}
}
Ok(config)
}
}
fn patterns_from_strs(pattern_strs: &[String]) -> Result<Vec<Pattern>> {
pattern_strs
.iter()
.map(|pattern_str| {
Pattern::new(pattern_str).map_err(|err| {
anyhow::Error::msg(err)
.context("Could not parse pattern from linter configuration.")
})
})
.collect()
}