pub mod consistency;
pub mod registry;
pub mod security;
pub mod semantic;
pub mod structural;
use std::fmt;
use std::path::PathBuf;
use serde::Serialize;
use crate::file_type::FileType;
use crate::parser::ParsedDocument;
pub(crate) fn dictionary_lines(raw: &'static str) -> Vec<&'static str> {
raw.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct RuleId {
pub code: u16,
pub slug: &'static str,
}
impl RuleId {
pub const fn new(code: u16, slug: &'static str) -> Self {
Self { code, slug }
}
pub fn code_str(&self) -> String {
format!("AIL{:03}", self.code)
}
}
impl fmt::Display for RuleId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({})", self.code_str(), self.slug)
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, serde::Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Error,
Warning,
Info,
}
impl Severity {
pub fn as_str(self) -> &'static str {
match self {
Self::Error => "error",
Self::Warning => "warning",
Self::Info => "info",
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct TextEdit {
pub range: std::ops::Range<usize>,
pub replacement: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct Violation {
pub rule_id: RuleId,
pub severity: Severity,
pub message: String,
pub file: PathBuf,
pub line: Option<usize>,
pub column: Option<usize>,
pub fix_hint: Option<String>,
pub snippet: Option<String>,
pub source_url: Option<String>,
pub detail: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub fixes: Vec<TextEdit>,
}
impl Violation {
pub fn new(
rule_id: RuleId,
severity: Severity,
file: PathBuf,
message: impl Into<String>,
) -> Self {
Self {
rule_id,
severity,
file,
message: message.into(),
line: None,
column: None,
fix_hint: None,
snippet: None,
source_url: None,
detail: None,
fixes: Vec::new(),
}
}
pub fn at(mut self, line: usize, column: usize) -> Self {
self.line = Some(line);
self.column = Some(column);
self
}
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
pub fn with_fix(mut self, fix: TextEdit) -> Self {
self.fixes.push(fix);
self
}
}
#[derive(Debug)]
pub struct RuleContext<'a> {
pub config: &'a crate::config::Config,
pub options: Option<&'a serde_yaml::Value>,
pub severity: Severity,
}
pub trait Rule: Send + Sync {
fn id(&self) -> RuleId;
fn default_severity(&self) -> Severity;
fn description(&self) -> &'static str;
fn fix_hint(&self) -> &'static str;
fn run(&self, doc: &ParsedDocument, ctx: &RuleContext<'_>) -> Vec<Violation>;
fn applies_to(&self, file_type: FileType) -> bool {
file_type.is_ai_guidance()
}
}
pub trait BatchRule: Send + Sync {
fn id(&self) -> RuleId;
fn default_severity(&self) -> Severity;
fn description(&self) -> &'static str;
fn fix_hint(&self) -> &'static str;
fn run_batch(&self, docs: &[ParsedDocument], ctx: &RuleContext<'_>) -> Vec<Violation>;
fn applies_to(&self, file_type: FileType) -> bool {
file_type.is_ai_guidance()
}
}