2025-01-01 04:12:39 +09:00
|
|
|
// Copyright 2018-2025 the Deno authors. MIT license.
|
2024-07-27 09:01:42 -04:00
|
|
|
|
|
|
|
use deno_ast::diagnostics::Diagnostic;
|
|
|
|
use deno_core::error::AnyError;
|
feat(lint): add JavaScript plugin support (#27203)
This commit adds an unstable lint plugin API.
Plugins are specified in the `deno.json` file under
`lint.plugins` option like so:
```
{
"lint": {
"plugins": [
"./plugins/my-plugin.ts",
"jsr:@deno/lint-plugin1",
"npm:@deno/lint-plugin2"
]
}
}
```
The API is considered unstable and might be subject
to changes in the future.
Plugin API was modelled after ESLint API for the
most part, but there are no guarantees for compatibility.
The AST format exposed to plugins is closely modelled
after the AST that `typescript-eslint` uses.
Lint plugins use the visitor pattern and can add
diagnostics like so:
```
export default {
name: "lint-plugin",
rules: {
"plugin-rule": {
create(context) {
return {
Identifier(node) {
if (node.name === "a") {
context.report({
node,
message: "should be b",
fix(fixer) {
return fixer.replaceText(node, "_b");
},
});
}
},
};
},
},
},
} satisfies Deno.lint.Plugin;
```
Besides reporting errors (diagnostics) plugins can provide
automatic fixes that use text replacement to apply changes.
---------
Co-authored-by: Marvin Hagemeister <marvin@deno.com>
Co-authored-by: David Sherret <dsherret@gmail.com>
2025-02-05 16:59:24 +01:00
|
|
|
use deno_core::error::CoreError;
|
2024-07-27 09:01:42 -04:00
|
|
|
use deno_core::serde_json;
|
|
|
|
use deno_lint::diagnostic::LintDiagnostic;
|
|
|
|
use deno_runtime::colors;
|
feat(lint): add JavaScript plugin support (#27203)
This commit adds an unstable lint plugin API.
Plugins are specified in the `deno.json` file under
`lint.plugins` option like so:
```
{
"lint": {
"plugins": [
"./plugins/my-plugin.ts",
"jsr:@deno/lint-plugin1",
"npm:@deno/lint-plugin2"
]
}
}
```
The API is considered unstable and might be subject
to changes in the future.
Plugin API was modelled after ESLint API for the
most part, but there are no guarantees for compatibility.
The AST format exposed to plugins is closely modelled
after the AST that `typescript-eslint` uses.
Lint plugins use the visitor pattern and can add
diagnostics like so:
```
export default {
name: "lint-plugin",
rules: {
"plugin-rule": {
create(context) {
return {
Identifier(node) {
if (node.name === "a") {
context.report({
node,
message: "should be b",
fix(fixer) {
return fixer.replaceText(node, "_b");
},
});
}
},
};
},
},
},
} satisfies Deno.lint.Plugin;
```
Besides reporting errors (diagnostics) plugins can provide
automatic fixes that use text replacement to apply changes.
---------
Co-authored-by: Marvin Hagemeister <marvin@deno.com>
Co-authored-by: David Sherret <dsherret@gmail.com>
2025-02-05 16:59:24 +01:00
|
|
|
use deno_runtime::fmt_errors::format_js_error;
|
2024-07-27 09:01:42 -04:00
|
|
|
use log::info;
|
|
|
|
use serde::Serialize;
|
|
|
|
|
|
|
|
use super::LintError;
|
2024-12-31 12:13:39 -05:00
|
|
|
use crate::args::LintReporterKind;
|
2024-07-27 09:01:42 -04:00
|
|
|
|
2024-09-05 10:51:40 +02:00
|
|
|
const JSON_SCHEMA_VERSION: u8 = 1;
|
|
|
|
|
2024-07-27 09:01:42 -04:00
|
|
|
pub fn create_reporter(kind: LintReporterKind) -> Box<dyn LintReporter + Send> {
|
|
|
|
match kind {
|
|
|
|
LintReporterKind::Pretty => Box::new(PrettyLintReporter::new()),
|
|
|
|
LintReporterKind::Json => Box::new(JsonLintReporter::new()),
|
|
|
|
LintReporterKind::Compact => Box::new(CompactLintReporter::new()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub trait LintReporter {
|
|
|
|
fn visit_diagnostic(&mut self, d: &LintDiagnostic);
|
|
|
|
fn visit_error(&mut self, file_path: &str, err: &AnyError);
|
|
|
|
fn close(&mut self, check_count: usize);
|
|
|
|
}
|
|
|
|
|
|
|
|
struct PrettyLintReporter {
|
|
|
|
lint_count: u32,
|
|
|
|
fixable_diagnostics: u32,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PrettyLintReporter {
|
|
|
|
fn new() -> PrettyLintReporter {
|
|
|
|
PrettyLintReporter {
|
|
|
|
lint_count: 0,
|
|
|
|
fixable_diagnostics: 0,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl LintReporter for PrettyLintReporter {
|
|
|
|
fn visit_diagnostic(&mut self, d: &LintDiagnostic) {
|
|
|
|
self.lint_count += 1;
|
|
|
|
if !d.details.fixes.is_empty() {
|
|
|
|
self.fixable_diagnostics += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
log::error!("{}\n", d.display());
|
|
|
|
}
|
|
|
|
|
|
|
|
fn visit_error(&mut self, file_path: &str, err: &AnyError) {
|
|
|
|
log::error!("Error linting: {file_path}");
|
feat(lint): add JavaScript plugin support (#27203)
This commit adds an unstable lint plugin API.
Plugins are specified in the `deno.json` file under
`lint.plugins` option like so:
```
{
"lint": {
"plugins": [
"./plugins/my-plugin.ts",
"jsr:@deno/lint-plugin1",
"npm:@deno/lint-plugin2"
]
}
}
```
The API is considered unstable and might be subject
to changes in the future.
Plugin API was modelled after ESLint API for the
most part, but there are no guarantees for compatibility.
The AST format exposed to plugins is closely modelled
after the AST that `typescript-eslint` uses.
Lint plugins use the visitor pattern and can add
diagnostics like so:
```
export default {
name: "lint-plugin",
rules: {
"plugin-rule": {
create(context) {
return {
Identifier(node) {
if (node.name === "a") {
context.report({
node,
message: "should be b",
fix(fixer) {
return fixer.replaceText(node, "_b");
},
});
}
},
};
},
},
},
} satisfies Deno.lint.Plugin;
```
Besides reporting errors (diagnostics) plugins can provide
automatic fixes that use text replacement to apply changes.
---------
Co-authored-by: Marvin Hagemeister <marvin@deno.com>
Co-authored-by: David Sherret <dsherret@gmail.com>
2025-02-05 16:59:24 +01:00
|
|
|
let text =
|
|
|
|
if let Some(CoreError::Js(js_error)) = err.downcast_ref::<CoreError>() {
|
|
|
|
format_js_error(js_error)
|
|
|
|
} else {
|
|
|
|
format!("{err:#}")
|
|
|
|
};
|
|
|
|
for line in text.split('\n') {
|
|
|
|
if line.is_empty() {
|
|
|
|
log::error!("");
|
|
|
|
} else {
|
|
|
|
log::error!(" {}", line);
|
|
|
|
}
|
|
|
|
}
|
2024-07-27 09:01:42 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
fn close(&mut self, check_count: usize) {
|
|
|
|
let fixable_suffix = if self.fixable_diagnostics > 0 {
|
|
|
|
colors::gray(format!(" ({} fixable via --fix)", self.fixable_diagnostics))
|
|
|
|
.to_string()
|
|
|
|
} else {
|
|
|
|
"".to_string()
|
|
|
|
};
|
|
|
|
match self.lint_count {
|
|
|
|
1 => info!("Found 1 problem{}", fixable_suffix),
|
|
|
|
n if n > 1 => {
|
|
|
|
info!("Found {} problems{}", self.lint_count, fixable_suffix)
|
|
|
|
}
|
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
|
|
|
|
match check_count {
|
|
|
|
1 => info!("Checked 1 file"),
|
|
|
|
n => info!("Checked {} files", n),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct CompactLintReporter {
|
|
|
|
lint_count: u32,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl CompactLintReporter {
|
|
|
|
fn new() -> CompactLintReporter {
|
|
|
|
CompactLintReporter { lint_count: 0 }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl LintReporter for CompactLintReporter {
|
|
|
|
fn visit_diagnostic(&mut self, d: &LintDiagnostic) {
|
|
|
|
self.lint_count += 1;
|
|
|
|
|
|
|
|
match &d.range {
|
|
|
|
Some(range) => {
|
|
|
|
let text_info = &range.text_info;
|
|
|
|
let range = &range.range;
|
|
|
|
let line_and_column = text_info.line_and_column_display(range.start);
|
|
|
|
log::error!(
|
|
|
|
"{}: line {}, col {} - {} ({})",
|
|
|
|
d.specifier,
|
|
|
|
line_and_column.line_number,
|
|
|
|
line_and_column.column_number,
|
|
|
|
d.message(),
|
|
|
|
d.code(),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
log::error!("{}: {} ({})", d.specifier, d.message(), d.code())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn visit_error(&mut self, file_path: &str, err: &AnyError) {
|
|
|
|
log::error!("Error linting: {file_path}");
|
|
|
|
log::error!(" {err}");
|
|
|
|
}
|
|
|
|
|
|
|
|
fn close(&mut self, check_count: usize) {
|
|
|
|
match self.lint_count {
|
|
|
|
1 => info!("Found 1 problem"),
|
|
|
|
n if n > 1 => info!("Found {} problems", self.lint_count),
|
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
|
|
|
|
match check_count {
|
|
|
|
1 => info!("Checked 1 file"),
|
|
|
|
n => info!("Checked {} files", n),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WARNING: Ensure doesn't change because it's used in the JSON output
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
|
|
|
#[serde(rename_all = "camelCase")]
|
|
|
|
pub struct JsonDiagnosticLintPosition {
|
|
|
|
/// The 1-indexed line number.
|
|
|
|
pub line: usize,
|
|
|
|
/// The 0-indexed column index.
|
|
|
|
pub col: usize,
|
|
|
|
pub byte_pos: usize,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl JsonDiagnosticLintPosition {
|
|
|
|
pub fn new(byte_index: usize, loc: deno_ast::LineAndColumnIndex) -> Self {
|
|
|
|
JsonDiagnosticLintPosition {
|
|
|
|
line: loc.line_index + 1,
|
|
|
|
col: loc.column_index,
|
|
|
|
byte_pos: byte_index,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WARNING: Ensure doesn't change because it's used in the JSON output
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
|
|
struct JsonLintDiagnosticRange {
|
|
|
|
pub start: JsonDiagnosticLintPosition,
|
|
|
|
pub end: JsonDiagnosticLintPosition,
|
|
|
|
}
|
|
|
|
|
|
|
|
// WARNING: Ensure doesn't change because it's used in the JSON output
|
|
|
|
#[derive(Clone, Serialize)]
|
|
|
|
struct JsonLintDiagnostic {
|
|
|
|
pub filename: String,
|
|
|
|
pub range: Option<JsonLintDiagnosticRange>,
|
|
|
|
pub message: String,
|
|
|
|
pub code: String,
|
|
|
|
pub hint: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
struct JsonLintReporter {
|
2024-09-05 10:51:40 +02:00
|
|
|
version: u8,
|
2024-07-27 09:01:42 -04:00
|
|
|
diagnostics: Vec<JsonLintDiagnostic>,
|
|
|
|
errors: Vec<LintError>,
|
2024-11-20 20:59:43 +01:00
|
|
|
checked_files: Vec<String>,
|
2024-07-27 09:01:42 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
impl JsonLintReporter {
|
|
|
|
fn new() -> JsonLintReporter {
|
|
|
|
JsonLintReporter {
|
2024-09-05 10:51:40 +02:00
|
|
|
version: JSON_SCHEMA_VERSION,
|
2024-07-27 09:01:42 -04:00
|
|
|
diagnostics: Vec::new(),
|
|
|
|
errors: Vec::new(),
|
2024-11-20 20:59:43 +01:00
|
|
|
checked_files: Vec::new(),
|
2024-07-27 09:01:42 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl LintReporter for JsonLintReporter {
|
|
|
|
fn visit_diagnostic(&mut self, d: &LintDiagnostic) {
|
|
|
|
self.diagnostics.push(JsonLintDiagnostic {
|
|
|
|
filename: d.specifier.to_string(),
|
|
|
|
range: d.range.as_ref().map(|range| {
|
|
|
|
let text_info = &range.text_info;
|
|
|
|
let range = range.range;
|
|
|
|
JsonLintDiagnosticRange {
|
|
|
|
start: JsonDiagnosticLintPosition::new(
|
|
|
|
range.start.as_byte_index(text_info.range().start),
|
|
|
|
text_info.line_and_column_index(range.start),
|
|
|
|
),
|
|
|
|
end: JsonDiagnosticLintPosition::new(
|
|
|
|
range.end.as_byte_index(text_info.range().start),
|
|
|
|
text_info.line_and_column_index(range.end),
|
|
|
|
),
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
message: d.message().to_string(),
|
|
|
|
code: d.code().to_string(),
|
|
|
|
hint: d.hint().map(|h| h.to_string()),
|
|
|
|
});
|
2024-11-20 20:59:43 +01:00
|
|
|
|
|
|
|
let file_path = d
|
|
|
|
.specifier
|
|
|
|
.to_file_path()
|
|
|
|
.unwrap()
|
|
|
|
.to_string_lossy()
|
|
|
|
.to_string();
|
|
|
|
|
|
|
|
if !self.checked_files.contains(&file_path) {
|
|
|
|
self.checked_files.push(file_path);
|
|
|
|
}
|
2024-07-27 09:01:42 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
fn visit_error(&mut self, file_path: &str, err: &AnyError) {
|
|
|
|
self.errors.push(LintError {
|
|
|
|
file_path: file_path.to_string(),
|
|
|
|
message: err.to_string(),
|
|
|
|
});
|
2024-11-20 20:59:43 +01:00
|
|
|
|
|
|
|
if !self.checked_files.contains(&file_path.to_string()) {
|
|
|
|
self.checked_files.push(file_path.to_string());
|
|
|
|
}
|
2024-07-27 09:01:42 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
fn close(&mut self, _check_count: usize) {
|
|
|
|
sort_diagnostics(&mut self.diagnostics);
|
2024-11-20 20:59:43 +01:00
|
|
|
self.checked_files.sort();
|
2024-07-27 09:01:42 -04:00
|
|
|
let json = serde_json::to_string_pretty(&self);
|
|
|
|
#[allow(clippy::print_stdout)]
|
|
|
|
{
|
|
|
|
println!("{}", json.unwrap());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn sort_diagnostics(diagnostics: &mut [JsonLintDiagnostic]) {
|
|
|
|
// Sort so that we guarantee a deterministic output which is useful for tests
|
|
|
|
diagnostics.sort_by(|a, b| {
|
|
|
|
use std::cmp::Ordering;
|
|
|
|
let file_order = a.filename.cmp(&b.filename);
|
|
|
|
match file_order {
|
|
|
|
Ordering::Equal => match &a.range {
|
|
|
|
Some(a_range) => match &b.range {
|
|
|
|
Some(b_range) => {
|
|
|
|
let line_order = a_range.start.line.cmp(&b_range.start.line);
|
|
|
|
match line_order {
|
|
|
|
Ordering::Equal => a_range.start.col.cmp(&b_range.start.col),
|
|
|
|
_ => line_order,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => Ordering::Less,
|
|
|
|
},
|
|
|
|
None => match &b.range {
|
|
|
|
Some(_) => Ordering::Greater,
|
|
|
|
None => Ordering::Equal,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
_ => file_order,
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|