use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use anyhow::{Context, Result, bail};
use serde::Deserialize;
use crate::rules;
use crate::{Diagnostic, Severity};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SeveritySetting {
Off,
Info,
#[serde(alias = "warn")]
Warning,
Error,
}
impl SeveritySetting {
pub const fn severity(self) -> Option<Severity> {
match self {
Self::Off => None,
Self::Info => Some(Severity::Info),
Self::Warning => Some(Severity::Warning),
Self::Error => Some(Severity::Error),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
pub rules: BTreeMap<String, SeveritySetting>,
pub ignore: Vec<String>,
}
impl Default for Config {
fn default() -> Self {
Self {
rules: BTreeMap::new(),
ignore: vec![
".git/**".into(),
"node_modules/**".into(),
"vendor/**".into(),
"dist/**".into(),
"build/**".into(),
"target/**".into(),
".next/**".into(),
".cache/**".into(),
"venv/**".into(),
".venv/**".into(),
],
}
}
}
impl Config {
pub fn load(root: &Path, explicit: Option<&Path>) -> Result<Self> {
let path = if let Some(path) = explicit {
Some(if path.is_absolute() {
path.to_path_buf()
} else {
root.join(path)
})
} else {
[".agentlint.json", ".agentlint.yaml", ".agentlint.yml"]
.iter()
.map(|name| root.join(name))
.find(|candidate| candidate.is_file())
};
let Some(path) = path else {
return Ok(Self::default());
};
let text = fs::read_to_string(&path)
.with_context(|| format!("failed to read configuration {}", path.display()))?;
let parsed: Self = match path.extension().and_then(|value| value.to_str()) {
Some("json") => serde_json::from_str(&text)
.with_context(|| format!("invalid JSON configuration {}", path.display()))?,
Some("yaml" | "yml") => serde_yaml::from_str(&text)
.with_context(|| format!("invalid YAML configuration {}", path.display()))?,
_ => bail!("configuration must use .json, .yaml, or .yml"),
};
for id in parsed.rules.keys() {
if rules::definition(id).is_none() {
bail!("configuration references unknown rule `{id}`");
}
}
Ok(parsed.with_default_ignores())
}
fn with_default_ignores(mut self) -> Self {
let defaults = Self::default().ignore;
for pattern in defaults {
if !self.ignore.contains(&pattern) {
self.ignore.push(pattern);
}
}
self
}
pub fn severity(&self, rule: &str) -> Option<Severity> {
self.rules.get(rule).map_or_else(
|| Some(rules::default_severity(rule)),
|value| value.severity(),
)
}
pub fn apply(&self, mut diagnostic: Diagnostic) -> Option<Diagnostic> {
diagnostic.severity = self.severity(&diagnostic.rule)?;
Some(diagnostic)
}
}
pub fn starter_yaml() -> &'static str {
r"# agentlint configuration
rules:
AGL301: warning
AGL401: error
AGL502: warning
ignore:
- examples/**
"
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disabled_rule_has_no_severity() {
let mut config = Config::default();
config.rules.insert("AGL301".into(), SeveritySetting::Off);
assert_eq!(config.severity("AGL301"), None);
}
}