use crate::Rule;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum LoadError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("YAML parse error: {0}")]
Yaml(#[from] serde_yaml::Error),
#[error("JSON parse error: {0}")]
Json(#[from] serde_json::Error),
#[error("Invalid file extension: {0}")]
InvalidExtension(String),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct LintConfig {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub extends: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub settings: Option<LintSettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rules: Option<RuleOverrides>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inline_rules: Vec<Rule>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct LintSettings {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub severity_threshold: Option<crate::Severity>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct RuleOverrides(pub std::collections::HashMap<String, RuleOverride>);
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct RuleOverride {
#[serde(default)]
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub severity: Option<crate::Severity>,
}
impl Default for RuleOverride {
fn default() -> Self {
Self {
enabled: true,
severity: None,
}
}
}
pub struct RuleLoader;
impl RuleLoader {
pub fn load_file(path: impl AsRef<Path>) -> Result<LintConfig, LoadError> {
let path = path.as_ref();
let content = std::fs::read_to_string(path)?;
Self::load_from_str(&content, path)
}
pub fn load_from_str(content: &str, path: impl AsRef<Path>) -> Result<LintConfig, LoadError> {
let path = path.as_ref();
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
match ext {
"yaml" | "yml" => Self::from_yaml(content),
"json" => Self::from_json(content),
_ => {
Self::from_yaml(content).or_else(|_| Self::from_json(content))
}
}
}
pub fn from_yaml(yaml: &str) -> Result<LintConfig, LoadError> {
Ok(serde_yaml::from_str(yaml)?)
}
pub fn from_json(json: &str) -> Result<LintConfig, LoadError> {
Ok(serde_json::from_str(json)?)
}
pub fn rule_from_yaml(yaml: &str) -> Result<Rule, LoadError> {
Ok(serde_yaml::from_str(yaml)?)
}
pub fn rule_from_json(json: &str) -> Result<Rule, LoadError> {
Ok(serde_json::from_str(json)?)
}
pub fn rules_from_yaml(yaml: &str) -> Result<Vec<Rule>, LoadError> {
Ok(serde_yaml::from_str(yaml)?)
}
pub fn rules_from_json(json: &str) -> Result<Vec<Rule>, LoadError> {
Ok(serde_json::from_str(json)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Severity;
#[test]
fn test_load_lint_config() {
let yaml = r#"
extends:
- "default"
settings:
severity_threshold: Warning
rules:
RL001:
enabled: true
severity: Error
RL003:
enabled: false
inline_rules:
- id: "PROJECT001"
name: "no-println"
severity: Warning
query:
kind: Function
message: "Use tracing macros instead of println!"
"#;
let config = RuleLoader::from_yaml(yaml).unwrap();
assert_eq!(config.extends, vec!["default"]);
assert!(config.settings.is_some());
assert_eq!(config.inline_rules.len(), 1);
assert_eq!(config.inline_rules[0].id, "PROJECT001");
}
#[test]
fn test_load_single_rule() {
let yaml = r#"
id: "RL001"
name: "no-unwrap"
severity: Error
query:
kind: Function
match:
vis: Public
body:
contains:
- node: MethodCall
message: "Avoid unwrap() in public function"
suggestion: "Use ? operator or expect()"
"#;
let rule = RuleLoader::rule_from_yaml(yaml).unwrap();
assert_eq!(rule.id, "RL001");
assert_eq!(rule.name, "no-unwrap");
assert_eq!(rule.severity, Severity::Error);
assert!(rule.query.body.is_some());
}
#[test]
fn test_load_rules_list() {
let yaml = r#"
- id: "RL001"
name: "no-unwrap"
severity: Warning
query:
kind: Function
message: "Avoid unwrap()"
- id: "RL002"
name: "no-panic"
severity: Error
query:
kind: Function
message: "Avoid panic!()"
"#;
let rules = RuleLoader::rules_from_yaml(yaml).unwrap();
assert_eq!(rules.len(), 2);
assert_eq!(rules[0].id, "RL001");
assert_eq!(rules[1].id, "RL002");
}
#[test]
fn test_rule_override() {
let yaml = r#"
rules:
RL001:
enabled: true
severity: Error
RL002:
enabled: false
"#;
let config = RuleLoader::from_yaml(yaml).unwrap();
let overrides = config.rules.unwrap();
assert!(overrides.0.contains_key("RL001"));
assert!(overrides.0.contains_key("RL002"));
assert!(overrides.0["RL001"].enabled);
assert!(!overrides.0["RL002"].enabled);
}
}