#![allow(clippy::disallowed_macros)]
use crate::diagnostic::{render_help, HelpRenderTarget, Severity};
use crate::linter::LintResult;
use vize_carton::String;
use vize_carton::ToCompactString;
pub trait Emitter: Send + Sync {
fn emit(&self, result: &LintResult, source: &str) -> String;
fn emit_summary(&self, results: &[LintResult]) -> String;
fn name(&self) -> &'static str;
}
pub struct Telegraph {
emitters: Vec<Box<dyn Emitter>>,
}
impl Telegraph {
pub fn new() -> Self {
Self {
emitters: Vec::new(),
}
}
pub fn with_text() -> Self {
let mut telegraph = Self::new();
telegraph.add_emitter(Box::new(TextEmitter::default()));
telegraph
}
pub fn with_json() -> Self {
let mut telegraph = Self::new();
telegraph.add_emitter(Box::new(JsonEmitter));
telegraph
}
pub fn add_emitter(&mut self, emitter: Box<dyn Emitter>) {
self.emitters.push(emitter);
}
pub fn transmit(&self, result: &LintResult, source: &str) -> Vec<String> {
self.emitters
.iter()
.map(|e| e.emit(result, source))
.collect()
}
pub fn transmit_all(&self, results: &[(LintResult, String)]) -> Vec<String> {
self.emitters
.iter()
.map(|e| {
let mut output = String::default();
for (result, source) in results {
output.push_str(&e.emit(result, source));
}
output.push_str(
&e.emit_summary(&results.iter().map(|(r, _)| r.clone()).collect::<Vec<_>>()),
);
output
})
.collect()
}
}
impl Default for Telegraph {
fn default() -> Self {
Self::with_text()
}
}
#[derive(Default)]
pub struct TextEmitter {
pub colors: bool,
}
impl TextEmitter {
pub fn new(colors: bool) -> Self {
Self { colors }
}
}
impl Emitter for TextEmitter {
fn name(&self) -> &'static str {
"text"
}
fn emit(&self, result: &LintResult, source: &str) -> String {
use crate::output::format_results;
use crate::OutputFormat;
let files = vec![(result.filename.clone(), source.to_compact_string())];
format_results(std::slice::from_ref(result), &files, OutputFormat::Text)
}
fn emit_summary(&self, results: &[LintResult]) -> String {
let total_errors: usize = results.iter().map(|r| r.error_count).sum();
let total_warnings: usize = results.iter().map(|r| r.warning_count).sum();
let file_count = results.len();
if total_errors == 0 && total_warnings == 0 {
return String::default();
}
format!(
"\nFound {} error{} and {} warning{} in {} file{}.\n",
total_errors,
if total_errors == 1 { "" } else { "s" },
total_warnings,
if total_warnings == 1 { "" } else { "s" },
file_count,
if file_count == 1 { "" } else { "s" },
)
.into()
}
}
pub struct JsonEmitter;
impl Emitter for JsonEmitter {
fn name(&self) -> &'static str {
"json"
}
fn emit(&self, result: &LintResult, _source: &str) -> String {
use crate::output::format_results;
use crate::OutputFormat;
let files: Vec<(String, String)> = vec![];
format_results(std::slice::from_ref(result), &files, OutputFormat::Json)
}
fn emit_summary(&self, _results: &[LintResult]) -> String {
String::default()
}
}
pub struct LspEmitter;
#[derive(Debug, Clone, serde::Serialize)]
pub struct LspDiagnostic {
pub range: LspRange,
pub severity: u8,
pub message: String,
pub source: String,
pub code: String,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct LspRange {
pub start: LspPosition,
pub end: LspPosition,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct LspPosition {
pub line: u32,
pub character: u32,
}
impl LspEmitter {
pub fn to_lsp_diagnostics(result: &LintResult) -> Vec<LspDiagnostic> {
result
.diagnostics
.iter()
.map(|d| LspDiagnostic {
range: LspRange {
start: LspPosition {
line: 0,
character: d.start,
},
end: LspPosition {
line: 0,
character: d.end,
},
},
severity: match d.severity {
Severity::Error => 1,
Severity::Warning => 2,
},
message: if let Some(help) = &d.help {
format!(
"{}\n{}",
d.message,
render_help(help, HelpRenderTarget::PlainText)
)
.into()
} else {
d.message.to_compact_string()
},
source: "vize-patina".to_compact_string(),
code: d.rule_name.to_compact_string(),
})
.collect()
}
pub fn to_lsp_diagnostics_with_source(result: &LintResult, source: &str) -> Vec<LspDiagnostic> {
result
.diagnostics
.iter()
.map(|d| {
let (start_line, start_col) = offset_to_line_col(source, d.start as usize);
let (end_line, end_col) = offset_to_line_col(source, d.end as usize);
LspDiagnostic {
range: LspRange {
start: LspPosition {
line: start_line,
character: start_col,
},
end: LspPosition {
line: end_line,
character: end_col,
},
},
severity: match d.severity {
Severity::Error => 1,
Severity::Warning => 2,
},
message: if let Some(help) = &d.help {
format!(
"{}\n{}",
d.message,
render_help(help, HelpRenderTarget::PlainText)
)
.into()
} else {
d.message.to_compact_string()
},
source: "vize-patina".to_compact_string(),
code: d.rule_name.to_compact_string(),
}
})
.collect()
}
}
fn offset_to_line_col(source: &str, offset: usize) -> (u32, u32) {
let mut line = 0u32;
let mut col = 0u32;
let mut current_offset = 0;
for ch in source.chars() {
if current_offset >= offset {
break;
}
if ch == '\n' {
line += 1;
col = 0;
} else {
col += 1;
}
current_offset += ch.len_utf8();
}
(line, col)
}
impl Emitter for LspEmitter {
fn name(&self) -> &'static str {
"lsp"
}
fn emit(&self, result: &LintResult, _source: &str) -> String {
let diagnostics = Self::to_lsp_diagnostics(result);
serde_json::to_string_pretty(&diagnostics)
.unwrap_or_default()
.into()
}
fn emit_summary(&self, _results: &[LintResult]) -> String {
String::default()
}
}
#[doc(hidden)]
pub struct OxlintBridge {
}
#[cfg(test)]
mod tests {
use super::{offset_to_line_col, LintResult, LspEmitter, Telegraph};
use crate::diagnostic::LintDiagnostic;
use vize_carton::ToCompactString;
#[test]
fn test_telegraph_with_text() {
let telegraph = Telegraph::with_text();
assert_eq!(telegraph.emitters.len(), 1);
}
#[test]
fn test_telegraph_with_json() {
let telegraph = Telegraph::with_json();
assert_eq!(telegraph.emitters.len(), 1);
}
#[test]
fn test_lsp_diagnostic_conversion() {
let result = LintResult {
filename: "test.vue".to_compact_string(),
diagnostics: vec![LintDiagnostic::error(
"vue/require-v-for-key",
"Missing key",
50,
70,
)
.with_help("Add :key attribute")],
error_count: 1,
warning_count: 0,
};
let lsp_diagnostics = LspEmitter::to_lsp_diagnostics(&result);
assert_eq!(lsp_diagnostics.len(), 1);
assert_eq!(lsp_diagnostics[0].severity, 1); assert_eq!(lsp_diagnostics[0].code, "vue/require-v-for-key");
}
#[test]
fn test_lsp_diagnostic_with_source() {
let source = "line1\nline2\nline3 v-for=\"item in items\"";
let result = LintResult {
filename: "test.vue".to_compact_string(),
diagnostics: vec![LintDiagnostic::error(
"vue/require-v-for-key",
"Missing key",
18, 44, )],
error_count: 1,
warning_count: 0,
};
let lsp_diagnostics = LspEmitter::to_lsp_diagnostics_with_source(&result, source);
assert_eq!(lsp_diagnostics.len(), 1);
assert_eq!(lsp_diagnostics[0].range.start.line, 2); }
#[test]
fn test_offset_to_line_col() {
let source = "abc\ndef\nghi";
assert_eq!(offset_to_line_col(source, 0), (0, 0)); assert_eq!(offset_to_line_col(source, 3), (0, 3)); assert_eq!(offset_to_line_col(source, 4), (1, 0)); assert_eq!(offset_to_line_col(source, 8), (2, 0)); }
}