use oxc_diagnostics::OxcDiagnostic;
use oxc_span::Span;
use serde::Serialize;
use vize_carton::CompactString;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Error,
Warning,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HelpLevel {
None,
Short,
#[default]
Full,
}
impl HelpLevel {
pub fn process(&self, help: &str) -> Option<String> {
match self {
HelpLevel::None => None,
HelpLevel::Short => Some(strip_markdown_first_line(help)),
HelpLevel::Full => Some(help.to_string()),
}
}
}
fn strip_markdown_first_line(text: &str) -> String {
let mut in_code_block = false;
for line in text.lines() {
let trimmed = line.trim();
if trimmed.starts_with("```") {
in_code_block = !in_code_block;
continue;
}
if in_code_block {
continue;
}
if trimmed.is_empty() {
continue;
}
let stripped = trimmed.replace("**", "").replace("__", "").replace('`', "");
let stripped = stripped.trim_start_matches('#').trim();
if stripped.is_empty() {
continue;
}
return stripped.to_string();
}
text.lines().next().unwrap_or(text).to_string()
}
#[derive(Debug, Clone, Serialize)]
pub struct TextEdit {
pub start: u32,
pub end: u32,
pub new_text: String,
}
impl TextEdit {
#[inline]
pub fn new(start: u32, end: u32, new_text: impl Into<String>) -> Self {
Self {
start,
end,
new_text: new_text.into(),
}
}
#[inline]
pub fn insert(offset: u32, text: impl Into<String>) -> Self {
Self::new(offset, offset, text)
}
#[inline]
pub fn delete(start: u32, end: u32) -> Self {
Self::new(start, end, "")
}
#[inline]
pub fn replace(start: u32, end: u32, text: impl Into<String>) -> Self {
Self::new(start, end, text)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Fix {
pub message: String,
pub edits: Vec<TextEdit>,
}
impl Fix {
#[inline]
pub fn new(message: impl Into<String>, edit: TextEdit) -> Self {
Self {
message: message.into(),
edits: vec![edit],
}
}
#[inline]
pub fn with_edits(message: impl Into<String>, edits: Vec<TextEdit>) -> Self {
Self {
message: message.into(),
edits,
}
}
#[inline]
pub fn apply(&self, source: &str) -> String {
let mut result = source.to_string();
let mut edits = self.edits.clone();
edits.sort_by(|a, b| b.start.cmp(&a.start));
for edit in edits {
let start = edit.start as usize;
let end = edit.end as usize;
if start <= result.len() && end <= result.len() {
result.replace_range(start..end, &edit.new_text);
}
}
result
}
}
#[derive(Debug, Clone)]
pub struct LintDiagnostic {
pub rule_name: &'static str,
pub severity: Severity,
pub message: CompactString,
pub start: u32,
pub end: u32,
pub help: Option<CompactString>,
pub labels: Vec<Label>,
pub fix: Option<Fix>,
}
#[derive(Debug, Clone)]
pub struct Label {
pub message: CompactString,
pub start: u32,
pub end: u32,
}
impl LintDiagnostic {
#[inline]
pub fn error(
rule_name: &'static str,
message: impl Into<CompactString>,
start: u32,
end: u32,
) -> Self {
Self {
rule_name,
severity: Severity::Error,
message: message.into(),
start,
end,
help: None,
labels: Vec::new(),
fix: None,
}
}
#[inline]
pub fn warn(
rule_name: &'static str,
message: impl Into<CompactString>,
start: u32,
end: u32,
) -> Self {
Self {
rule_name,
severity: Severity::Warning,
message: message.into(),
start,
end,
help: None,
labels: Vec::new(),
fix: None,
}
}
#[inline]
pub fn with_help(mut self, help: impl Into<CompactString>) -> Self {
self.help = Some(help.into());
self
}
#[inline]
pub fn with_label(mut self, message: impl Into<CompactString>, start: u32, end: u32) -> Self {
self.labels.push(Label {
message: message.into(),
start,
end,
});
self
}
#[inline]
pub fn with_fix(mut self, fix: Fix) -> Self {
self.fix = Some(fix);
self
}
#[inline]
pub fn has_fix(&self) -> bool {
self.fix.is_some()
}
#[inline]
pub fn formatted_message(&self) -> String {
format!("[vize:{}] {}", self.rule_name, self.message)
}
#[inline]
pub fn into_oxc_diagnostic(self) -> OxcDiagnostic {
let formatted_msg = format!("[vize:{}] {}", self.rule_name, self.message);
let mut diag = match self.severity {
Severity::Error => OxcDiagnostic::error(formatted_msg),
Severity::Warning => OxcDiagnostic::warn(formatted_msg),
};
diag = diag.with_label(Span::new(self.start, self.end));
if let Some(help) = self.help {
diag = diag.with_help(help.to_string());
}
for label in self.labels {
diag =
diag.and_label(Span::new(label.start, label.end).label(label.message.to_string()));
}
diag
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct LintSummary {
pub error_count: usize,
pub warning_count: usize,
pub file_count: usize,
}
impl LintSummary {
#[inline]
pub fn add(&mut self, diagnostic: &LintDiagnostic) {
match diagnostic.severity {
Severity::Error => self.error_count += 1,
Severity::Warning => self.warning_count += 1,
}
}
#[inline]
pub fn has_errors(&self) -> bool {
self.error_count > 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_help_level_full() {
let level = HelpLevel::Full;
let help = "**Why:** Use `:key` for tracking.\n\n```vue\n<li :key=\"id\">\n```";
let result = level.process(help);
assert_eq!(result, Some(help.to_string()));
}
#[test]
fn test_help_level_none() {
let level = HelpLevel::None;
let result = level.process("Any help text");
assert_eq!(result, None);
}
#[test]
fn test_help_level_short_strips_markdown() {
let level = HelpLevel::Short;
let help = "**Why:** The `:key` attribute helps Vue track items.\n\n**Fix:**\n```vue\n<li :key=\"id\">\n```";
let result = level.process(help);
assert_eq!(
result,
Some("Why: The :key attribute helps Vue track items.".to_string())
);
}
#[test]
fn test_help_level_short_skips_code_blocks() {
let level = HelpLevel::Short;
let help = "```vue\n<li :key=\"id\">\n```\nUse unique keys";
let result = level.process(help);
assert_eq!(result, Some("Use unique keys".to_string()));
}
#[test]
fn test_help_level_short_simple_text() {
let level = HelpLevel::Short;
let help = "Add a key attribute to the element";
let result = level.process(help);
assert_eq!(
result,
Some("Add a key attribute to the element".to_string())
);
}
#[test]
fn test_strip_markdown_first_line_with_backticks() {
let result = strip_markdown_first_line("Use `v-model` instead of `{{ }}`");
assert_eq!(result, "Use v-model instead of {{ }}");
}
}