use crate::ast::Span;
use serde::Deserialize;
use std::path::PathBuf;
pub static DEFAULT_LINTS: &str = include_str!("../lints.toml");
pub const MAX_NESTING_DEPTH: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Error,
Warning,
Hint,
}
#[derive(Debug, Clone, Deserialize)]
pub struct LintRule {
pub id: String,
pub pattern: String,
#[serde(default)]
pub replacement: String,
pub message: String,
#[serde(default = "default_severity")]
pub severity: Severity,
}
fn default_severity() -> Severity {
Severity::Warning
}
#[derive(Debug, Clone, Deserialize)]
pub struct LintConfig {
#[serde(rename = "lint")]
pub rules: Vec<LintRule>,
}
impl LintConfig {
pub fn from_toml(toml_str: &str) -> Result<Self, String> {
toml::from_str(toml_str).map_err(|e| format!("Failed to parse lint config: {}", e))
}
pub fn default_config() -> Result<Self, String> {
Self::from_toml(DEFAULT_LINTS)
}
pub fn merge(&mut self, other: LintConfig) {
for rule in other.rules {
if let Some(existing) = self.rules.iter_mut().find(|r| r.id == rule.id) {
*existing = rule;
} else {
self.rules.push(rule);
}
}
}
}
#[derive(Debug, Clone)]
pub struct CompiledPattern {
pub rule: LintRule,
pub elements: Vec<PatternElement>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PatternElement {
Word(String),
SingleWildcard(String),
MultiWildcard,
}
impl CompiledPattern {
pub fn compile(rule: LintRule) -> Result<Self, String> {
let mut elements = Vec::new();
let mut multi_wildcard_count = 0;
for token in rule.pattern.split_whitespace() {
if token == "$..." {
multi_wildcard_count += 1;
elements.push(PatternElement::MultiWildcard);
} else if token.starts_with('$') {
elements.push(PatternElement::SingleWildcard(token.to_string()));
} else {
elements.push(PatternElement::Word(token.to_string()));
}
}
if elements.is_empty() {
return Err(format!("Empty pattern in lint rule '{}'", rule.id));
}
if multi_wildcard_count > 1 {
return Err(format!(
"Pattern in lint rule '{}' has {} multi-wildcards ($...), but at most 1 is allowed",
rule.id, multi_wildcard_count
));
}
Ok(CompiledPattern { rule, elements })
}
}
#[derive(Debug, Clone)]
pub struct LintDiagnostic {
pub id: String,
pub message: String,
pub severity: Severity,
pub replacement: String,
pub file: PathBuf,
pub line: usize,
pub end_line: Option<usize>,
pub start_column: Option<usize>,
pub end_column: Option<usize>,
pub word_name: String,
pub start_index: usize,
pub end_index: usize,
}
#[derive(Debug, Clone)]
pub(super) struct WordInfo<'a> {
pub(super) name: &'a str,
pub(super) span: Option<&'a Span>,
}
pub fn format_diagnostics(diagnostics: &[LintDiagnostic]) -> String {
let mut output = String::new();
for d in diagnostics {
let severity_str = match d.severity {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Hint => "hint",
};
let location = match d.start_column {
Some(col) => format!("{}:{}:{}", d.file.display(), d.line + 1, col + 1),
None => format!("{}:{}", d.file.display(), d.line + 1),
};
output.push_str(&format!(
"{}: {} [{}]: {}\n",
location, severity_str, d.id, d.message
));
if !d.replacement.is_empty() {
output.push_str(&format!(" suggestion: replace with `{}`\n", d.replacement));
} else if d.replacement.is_empty() && d.message.contains("no effect") {
output.push_str(" suggestion: remove this code\n");
}
}
output
}