2025-01-01 04:12:39 +09:00
|
|
|
// Copyright 2018-2025 the Deno authors. MIT license.
|
2024-12-21 00:58:03 +01:00
|
|
|
|
|
|
|
use deno_ast::MediaType;
|
|
|
|
use deno_ast::ModuleSpecifier;
|
2025-01-08 14:52:32 -08:00
|
|
|
use deno_ast::ParseDiagnostic;
|
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_ast::SourceRange;
|
|
|
|
use deno_ast::SourceTextInfo;
|
|
|
|
use deno_ast::SourceTextProvider;
|
2024-12-21 00:58:03 +01:00
|
|
|
use deno_core::op2;
|
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::OpState;
|
|
|
|
use deno_lint::diagnostic::LintDiagnostic;
|
|
|
|
use deno_lint::diagnostic::LintDiagnosticDetails;
|
|
|
|
use deno_lint::diagnostic::LintDiagnosticRange;
|
|
|
|
use deno_lint::diagnostic::LintFix;
|
|
|
|
use deno_lint::diagnostic::LintFixChange;
|
|
|
|
use tokio_util::sync::CancellationToken;
|
2024-12-21 00:58:03 +01:00
|
|
|
|
|
|
|
use crate::tools::lint;
|
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 crate::tools::lint::PluginLogger;
|
|
|
|
use crate::util::text_encoding::Utf16Map;
|
2024-12-21 00:58:03 +01:00
|
|
|
|
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
|
|
|
deno_core::extension!(
|
|
|
|
deno_lint_ext,
|
|
|
|
ops = [
|
|
|
|
op_lint_create_serialized_ast,
|
|
|
|
op_lint_report,
|
|
|
|
op_lint_get_source,
|
|
|
|
op_is_cancelled
|
|
|
|
],
|
|
|
|
options = {
|
|
|
|
logger: PluginLogger,
|
|
|
|
},
|
|
|
|
// TODO(bartlomieju): this should only be done,
|
|
|
|
// if not in the "test worker".
|
|
|
|
middleware = |op| match op.name {
|
|
|
|
"op_print" => op_print(),
|
|
|
|
_ => op,
|
|
|
|
},
|
|
|
|
state = |state, options| {
|
|
|
|
state.put(options.logger);
|
|
|
|
state.put(LintPluginContainer::default());
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
deno_core::extension!(
|
|
|
|
deno_lint_ext_for_test,
|
|
|
|
ops = [op_lint_create_serialized_ast, op_is_cancelled],
|
|
|
|
state = |state| {
|
|
|
|
state.put(LintPluginContainer::default());
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
pub struct LintPluginContainer {
|
|
|
|
pub diagnostics: Vec<LintDiagnostic>,
|
|
|
|
pub source_text_info: Option<SourceTextInfo>,
|
|
|
|
pub utf_16_map: Option<Utf16Map>,
|
|
|
|
pub specifier: Option<ModuleSpecifier>,
|
|
|
|
pub token: CancellationToken,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl LintPluginContainer {
|
|
|
|
pub fn set_cancellation_token(
|
|
|
|
&mut self,
|
|
|
|
maybe_token: Option<CancellationToken>,
|
|
|
|
) {
|
|
|
|
let token = maybe_token.unwrap_or_default();
|
|
|
|
self.token = token;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn set_info_for_file(
|
|
|
|
&mut self,
|
|
|
|
specifier: ModuleSpecifier,
|
|
|
|
source_text_info: SourceTextInfo,
|
|
|
|
utf16_map: Utf16Map,
|
|
|
|
) {
|
|
|
|
self.specifier = Some(specifier);
|
|
|
|
self.utf_16_map = Some(utf16_map);
|
|
|
|
self.source_text_info = Some(source_text_info);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn report(
|
|
|
|
&mut self,
|
|
|
|
id: String,
|
|
|
|
message: String,
|
|
|
|
hint: Option<String>,
|
|
|
|
start_utf16: usize,
|
|
|
|
end_utf16: usize,
|
|
|
|
fix: Option<LintReportFix>,
|
|
|
|
) -> Result<(), LintReportError> {
|
|
|
|
fn out_of_range_err(
|
|
|
|
map: &Utf16Map,
|
|
|
|
start_utf16: usize,
|
|
|
|
end_utf16: usize,
|
|
|
|
) -> LintReportError {
|
|
|
|
LintReportError::IncorrectRange {
|
|
|
|
start: start_utf16,
|
|
|
|
end: end_utf16,
|
|
|
|
source_end: map.text_content_length_utf16().into(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn utf16_to_utf8_range(
|
|
|
|
utf16_map: &Utf16Map,
|
|
|
|
source_text_info: &SourceTextInfo,
|
|
|
|
start_utf16: usize,
|
|
|
|
end_utf16: usize,
|
|
|
|
) -> Result<SourceRange, LintReportError> {
|
|
|
|
let Some(start) =
|
|
|
|
utf16_map.utf16_to_utf8_offset((start_utf16 as u32).into())
|
|
|
|
else {
|
|
|
|
return Err(out_of_range_err(utf16_map, start_utf16, end_utf16));
|
|
|
|
};
|
|
|
|
let Some(end) = utf16_map.utf16_to_utf8_offset((end_utf16 as u32).into())
|
|
|
|
else {
|
|
|
|
return Err(out_of_range_err(utf16_map, start_utf16, end_utf16));
|
|
|
|
};
|
|
|
|
let start_pos = source_text_info.start_pos();
|
|
|
|
Ok(SourceRange::new(
|
|
|
|
start_pos + start.into(),
|
|
|
|
start_pos + end.into(),
|
|
|
|
))
|
|
|
|
}
|
|
|
|
|
|
|
|
let source_text_info = self.source_text_info.as_ref().unwrap();
|
|
|
|
let utf16_map = self.utf_16_map.as_ref().unwrap();
|
|
|
|
let specifier = self.specifier.clone().unwrap();
|
|
|
|
let diagnostic_range =
|
|
|
|
utf16_to_utf8_range(utf16_map, source_text_info, start_utf16, end_utf16)?;
|
|
|
|
let range = LintDiagnosticRange {
|
|
|
|
range: diagnostic_range,
|
|
|
|
description: None,
|
|
|
|
text_info: source_text_info.clone(),
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut fixes: Vec<LintFix> = vec![];
|
|
|
|
|
|
|
|
if let Some(fix) = fix {
|
|
|
|
let fix_range = utf16_to_utf8_range(
|
|
|
|
utf16_map,
|
|
|
|
source_text_info,
|
|
|
|
fix.range.0,
|
|
|
|
fix.range.1,
|
|
|
|
)?;
|
|
|
|
fixes.push(LintFix {
|
|
|
|
changes: vec![LintFixChange {
|
|
|
|
new_text: fix.text.into(),
|
|
|
|
range: fix_range,
|
|
|
|
}],
|
|
|
|
description: format!("Fix this {} problem", id).into(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let lint_diagnostic = LintDiagnostic {
|
|
|
|
specifier,
|
|
|
|
range: Some(range),
|
|
|
|
details: LintDiagnosticDetails {
|
|
|
|
message,
|
|
|
|
code: id,
|
|
|
|
hint,
|
|
|
|
fixes,
|
|
|
|
custom_docs_url: None,
|
|
|
|
info: vec![],
|
|
|
|
},
|
|
|
|
};
|
|
|
|
self.diagnostics.push(lint_diagnostic);
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[op2(fast)]
|
|
|
|
pub fn op_print(state: &mut OpState, #[string] msg: &str, is_err: bool) {
|
|
|
|
let logger = state.borrow::<PluginLogger>();
|
|
|
|
|
|
|
|
if is_err {
|
|
|
|
logger.error(msg);
|
|
|
|
} else {
|
|
|
|
logger.log(msg);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[op2(fast)]
|
|
|
|
fn op_is_cancelled(state: &mut OpState) -> bool {
|
|
|
|
let container = state.borrow::<LintPluginContainer>();
|
|
|
|
container.token.is_cancelled()
|
|
|
|
}
|
2024-12-21 00:58:03 +01:00
|
|
|
|
2025-01-08 14:52:32 -08:00
|
|
|
#[derive(Debug, thiserror::Error, deno_error::JsError)]
|
|
|
|
pub enum LintError {
|
|
|
|
#[class(inherit)]
|
|
|
|
#[error(transparent)]
|
|
|
|
Io(#[from] std::io::Error),
|
|
|
|
#[class(inherit)]
|
|
|
|
#[error(transparent)]
|
|
|
|
ParseDiagnostic(#[from] ParseDiagnostic),
|
|
|
|
#[class(type)]
|
|
|
|
#[error("Failed to parse path as URL: {0}")]
|
|
|
|
PathParse(std::path::PathBuf),
|
|
|
|
}
|
|
|
|
|
2024-12-21 00:58:03 +01:00
|
|
|
#[op2]
|
|
|
|
#[buffer]
|
|
|
|
fn op_lint_create_serialized_ast(
|
|
|
|
#[string] file_name: &str,
|
|
|
|
#[string] source: String,
|
2025-01-08 14:52:32 -08:00
|
|
|
) -> Result<Vec<u8>, LintError> {
|
2024-12-21 00:58:03 +01:00
|
|
|
let file_text = deno_ast::strip_bom(source);
|
|
|
|
let path = std::env::current_dir()?.join(file_name);
|
2025-01-08 14:52:32 -08:00
|
|
|
let specifier = ModuleSpecifier::from_file_path(&path)
|
|
|
|
.map_err(|_| LintError::PathParse(path))?;
|
2024-12-21 00:58:03 +01:00
|
|
|
let media_type = MediaType::from_specifier(&specifier);
|
|
|
|
let parsed_source = deno_ast::parse_program(deno_ast::ParseParams {
|
|
|
|
specifier,
|
|
|
|
text: file_text.into(),
|
|
|
|
media_type,
|
|
|
|
capture_tokens: false,
|
|
|
|
scope_analysis: false,
|
|
|
|
maybe_syntax: None,
|
|
|
|
})?;
|
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 utf16_map = Utf16Map::new(parsed_source.text().as_ref());
|
|
|
|
Ok(lint::serialize_ast_to_buffer(&parsed_source, &utf16_map))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(serde::Deserialize)]
|
|
|
|
struct LintReportFix {
|
|
|
|
text: String,
|
|
|
|
range: (usize, usize),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, thiserror::Error, deno_error::JsError)]
|
|
|
|
pub enum LintReportError {
|
|
|
|
#[class(type)]
|
|
|
|
#[error("Invalid range [{start}, {end}], the source has a range of [0, {source_end}]")]
|
|
|
|
IncorrectRange {
|
|
|
|
start: usize,
|
|
|
|
end: usize,
|
|
|
|
source_end: u32,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
#[op2]
|
|
|
|
fn op_lint_report(
|
|
|
|
state: &mut OpState,
|
|
|
|
#[string] id: String,
|
|
|
|
#[string] message: String,
|
|
|
|
#[string] hint: Option<String>,
|
|
|
|
#[smi] start_utf16: usize,
|
|
|
|
#[smi] end_utf16: usize,
|
|
|
|
#[serde] fix: Option<LintReportFix>,
|
|
|
|
) -> Result<(), LintReportError> {
|
|
|
|
let container = state.borrow_mut::<LintPluginContainer>();
|
|
|
|
container.report(id, message, hint, start_utf16, end_utf16, fix)?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[op2]
|
|
|
|
#[string]
|
|
|
|
fn op_lint_get_source(state: &mut OpState) -> String {
|
|
|
|
let container = state.borrow_mut::<LintPluginContainer>();
|
|
|
|
container
|
|
|
|
.source_text_info
|
|
|
|
.as_ref()
|
|
|
|
.unwrap()
|
|
|
|
.text_str()
|
|
|
|
.to_string()
|
2024-12-21 00:58:03 +01:00
|
|
|
}
|