use dyn_clone::DynClone;
use serde::{Deserialize, Serialize};
use std::ops::Range;
use thiserror::Error;
use crate::lint_context::LintContext;
#[macro_export]
macro_rules! impl_rule_clone {
($ty:ty) => {
impl $ty {
fn box_clone(&self) -> Box<dyn Rule> {
Box::new(self.clone())
}
}
};
}
#[derive(Debug, Error)]
pub enum LintError {
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Fix failed: {0}")]
FixFailed(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Parsing error: {0}")]
ParsingError(String),
}
pub type LintResult = Result<Vec<LintWarning>, LintError>;
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct LintWarning {
pub message: String,
pub line: usize, pub column: usize, pub end_line: usize, pub end_column: usize, pub severity: Severity,
pub fix: Option<Fix>,
pub rule_name: Option<String>,
}
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
pub struct Fix {
pub range: Range<usize>,
pub replacement: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub additional_edits: Vec<Fix>,
}
impl Fix {
pub fn new(range: Range<usize>, replacement: String) -> Self {
Self {
range,
replacement,
additional_edits: Vec::new(),
}
}
pub fn with_additional_edits(range: Range<usize>, replacement: String, additional_edits: Vec<Fix>) -> Self {
Self {
range,
replacement,
additional_edits,
}
}
}
#[derive(Debug, PartialEq, Clone, Copy, Serialize, schemars::JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Error,
Warning,
Info,
}
impl<'de> serde::Deserialize<'de> for Severity {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.to_lowercase().as_str() {
"error" => Ok(Severity::Error),
"warning" => Ok(Severity::Warning),
"info" => Ok(Severity::Info),
_ => Err(serde::de::Error::custom(format!(
"Invalid severity: '{s}'. Valid values: error, warning, info"
))),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuleCategory {
Heading,
List,
CodeBlock,
Link,
Image,
Html,
Emphasis,
Whitespace,
Blockquote,
Table,
FrontMatter,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FixCapability {
FullyFixable,
ConditionallyFixable,
Unfixable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CrossFileScope {
#[default]
None,
Workspace,
}
pub trait Rule: DynClone + Send + Sync {
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
fn check(&self, ctx: &LintContext) -> LintResult;
fn fix(&self, ctx: &LintContext) -> Result<String, LintError>;
fn should_skip(&self, _ctx: &LintContext) -> bool {
false
}
fn category(&self) -> RuleCategory {
RuleCategory::Other }
fn as_any(&self) -> &dyn std::any::Any;
fn default_config_section(&self) -> Option<(String, toml::Value)> {
None
}
fn config_aliases(&self) -> Option<std::collections::HashMap<String, String>> {
None
}
fn polymorphic_config_keys(&self) -> &'static [&'static str] {
&[]
}
fn fix_capability(&self) -> FixCapability {
FixCapability::FullyFixable }
fn cross_file_scope(&self) -> CrossFileScope {
CrossFileScope::None
}
fn contribute_to_index(&self, _ctx: &LintContext, _file_index: &mut crate::workspace_index::FileIndex) {
}
fn cross_file_check(
&self,
_file_path: &std::path::Path,
_file_index: &crate::workspace_index::FileIndex,
_workspace_index: &crate::workspace_index::WorkspaceIndex,
) -> LintResult {
Ok(Vec::new()) }
fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
where
Self: Sized,
{
panic!(
"from_config not implemented for rule: {}",
std::any::type_name::<Self>()
);
}
}
dyn_clone::clone_trait_object!(Rule);
pub trait RuleExt {
fn downcast_ref<T: 'static>(&self) -> Option<&T>;
}
impl<R: Rule + 'static> RuleExt for Box<R> {
fn downcast_ref<T: 'static>(&self) -> Option<&T> {
if std::any::TypeId::of::<R>() == std::any::TypeId::of::<T>() {
unsafe { Some(&*std::ptr::from_ref(self.as_ref()).cast::<T>()) }
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_severity_serialization() {
let warning = LintWarning {
message: "Test warning".to_string(),
line: 1,
column: 1,
end_line: 1,
end_column: 10,
severity: Severity::Warning,
fix: None,
rule_name: Some("MD001".to_string()),
};
let serialized = serde_json::to_string(&warning).unwrap();
assert!(serialized.contains("\"severity\":\"warning\""));
let error = LintWarning {
severity: Severity::Error,
..warning
};
let serialized = serde_json::to_string(&error).unwrap();
assert!(serialized.contains("\"severity\":\"error\""));
}
#[test]
fn test_fix_serialization() {
let fix = Fix::new(0..10, "fixed text".to_string());
let warning = LintWarning {
message: "Test warning".to_string(),
line: 1,
column: 1,
end_line: 1,
end_column: 10,
severity: Severity::Warning,
fix: Some(fix),
rule_name: Some("MD001".to_string()),
};
let serialized = serde_json::to_string(&warning).unwrap();
assert!(serialized.contains("\"fix\""));
assert!(serialized.contains("\"replacement\":\"fixed text\""));
}
#[test]
fn test_rule_category_equality() {
assert_eq!(RuleCategory::Heading, RuleCategory::Heading);
assert_ne!(RuleCategory::Heading, RuleCategory::List);
let categories = [
RuleCategory::Heading,
RuleCategory::List,
RuleCategory::CodeBlock,
RuleCategory::Link,
RuleCategory::Image,
RuleCategory::Html,
RuleCategory::Emphasis,
RuleCategory::Whitespace,
RuleCategory::Blockquote,
RuleCategory::Table,
RuleCategory::FrontMatter,
RuleCategory::Other,
];
for (i, cat1) in categories.iter().enumerate() {
for (j, cat2) in categories.iter().enumerate() {
if i == j {
assert_eq!(cat1, cat2);
} else {
assert_ne!(cat1, cat2);
}
}
}
}
#[test]
fn test_lint_error_conversions() {
use std::io;
let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
let lint_error: LintError = io_error.into();
match lint_error {
LintError::IoError(_) => {}
_ => panic!("Expected IoError variant"),
}
let invalid_input = LintError::InvalidInput("bad input".to_string());
assert_eq!(invalid_input.to_string(), "Invalid input: bad input");
let fix_failed = LintError::FixFailed("couldn't fix".to_string());
assert_eq!(fix_failed.to_string(), "Fix failed: couldn't fix");
let parsing_error = LintError::ParsingError("parse error".to_string());
assert_eq!(parsing_error.to_string(), "Parsing error: parse error");
}
}