use rowan::TextRange;
use serde::Deserialize;
use super::{
ExternalLinterParser, LinterError, ParseContext, map_concatenated_edit_to_original,
map_tool_line_col_to_original,
};
use crate::linter::diagnostics::{Diagnostic, DiagnosticOrigin, Location};
#[derive(Debug, Deserialize)]
struct EslintFileReport {
messages: Vec<EslintMessage>,
}
#[derive(Debug, Deserialize)]
struct EslintMessage {
#[serde(default, rename = "ruleId")]
rule_id: Option<String>,
severity: i64,
message: String,
line: usize,
column: usize,
#[serde(default, rename = "endLine")]
end_line: Option<usize>,
#[serde(default, rename = "endColumn")]
end_column: Option<usize>,
#[serde(default)]
fix: Option<EslintFix>,
#[serde(default)]
suggestions: Vec<EslintSuggestion>,
}
#[derive(Debug, Deserialize)]
struct EslintSuggestion {
fix: EslintFix,
}
#[derive(Debug, Deserialize)]
struct EslintFix {
range: [usize; 2],
text: String,
}
pub(crate) struct EslintParser;
impl ExternalLinterParser for EslintParser {
const NAME: &'static str = "eslint";
fn parse(ctx: &ParseContext<'_>) -> Result<Vec<Diagnostic>, LinterError> {
use crate::linter::diagnostics::{Edit, Fix};
let reports: Vec<EslintFileReport> = serde_json::from_str(ctx.output)
.map_err(|e| LinterError::ParseError(format!("invalid eslint JSON: {}", e)))?;
let mut diagnostics = Vec::new();
for report in reports {
for msg in report.messages {
let line = msg.line;
let column = msg.column;
let start_offset = map_tool_line_col_to_original(ctx, line, column)
.unwrap_or(ctx.original_input.len());
let end_line = msg.end_line.unwrap_or(line);
let end_column = msg.end_column.unwrap_or(column.saturating_add(1));
let end_offset = map_tool_line_col_to_original(ctx, end_line, end_column)
.unwrap_or(ctx.original_input.len());
let location = Location::from_range(
TextRange::new((start_offset as u32).into(), (end_offset as u32).into()),
ctx.original_input,
);
let code = msg.rule_id.unwrap_or_else(|| "eslint".to_string());
let diagnostic = match msg.severity {
2 => Diagnostic::error(location, code, msg.message),
1 => Diagnostic::info(location, code, msg.message),
_ => Diagnostic::warning(location, code, msg.message),
}
.with_origin(DiagnosticOrigin::External);
let selected_fix = msg
.fix
.or_else(|| msg.suggestions.into_iter().next().map(|s| s.fix));
let mapped_fix =
if let (Some(mappings), Some(eslint_fix)) = (ctx.mappings, selected_fix) {
map_concatenated_edit_to_original(
ctx.linted_input,
eslint_fix.range[0],
eslint_fix.range[1],
&eslint_fix.text,
mappings,
)
.map(|(fix_start, fix_end)| {
Fix::safe(
"Apply ESLint fix",
vec![Edit {
range: TextRange::new(
(fix_start as u32).into(),
(fix_end as u32).into(),
),
replacement: eslint_fix.text,
}],
)
})
} else {
None
};
diagnostics.push(if let Some(fix) = mapped_fix {
diagnostic.with_fix(fix)
} else {
diagnostic
});
}
}
Ok(diagnostics)
}
}