mirror of
https://github.com/danbulant/oxc
synced 2026-05-25 04:42:10 +00:00
feat: eslint-plugin-unicorn (recommended) prefer-node-protocol (#1618)
part of https://github.com/oxc-project/oxc/issues/684
This commit is contained in:
parent
f74fa975b1
commit
872e8ad4ae
7 changed files with 316 additions and 6 deletions
|
|
@ -527,9 +527,7 @@ impl<'a> AstKind<'a> {
|
||||||
Self::ArrowExpression(_) => "ArrowExpression".into(),
|
Self::ArrowExpression(_) => "ArrowExpression".into(),
|
||||||
Self::AssignmentExpression(_) => "AssignmentExpression".into(),
|
Self::AssignmentExpression(_) => "AssignmentExpression".into(),
|
||||||
Self::AwaitExpression(_) => "AwaitExpression".into(),
|
Self::AwaitExpression(_) => "AwaitExpression".into(),
|
||||||
Self::BinaryExpression(b) => {
|
Self::BinaryExpression(b) => format!("BinaryExpression{}", b.operator.as_str()).into(),
|
||||||
format!("BinaryExpression({})", b.operator.as_str()).into()
|
|
||||||
}
|
|
||||||
Self::CallExpression(_) => "CallExpression".into(),
|
Self::CallExpression(_) => "CallExpression".into(),
|
||||||
Self::ChainExpression(_) => "ChainExpression".into(),
|
Self::ChainExpression(_) => "ChainExpression".into(),
|
||||||
Self::ConditionalExpression(_) => "ConditionalExpression".into(),
|
Self::ConditionalExpression(_) => "ConditionalExpression".into(),
|
||||||
|
|
|
||||||
|
|
@ -555,7 +555,6 @@ pub trait Visit<'a>: Sized {
|
||||||
Expression::RegExpLiteral(lit) => self.visit_reg_expr_literal(lit),
|
Expression::RegExpLiteral(lit) => self.visit_reg_expr_literal(lit),
|
||||||
Expression::StringLiteral(lit) => self.visit_string_literal(lit),
|
Expression::StringLiteral(lit) => self.visit_string_literal(lit),
|
||||||
Expression::TemplateLiteral(lit) => self.visit_template_literal(lit),
|
Expression::TemplateLiteral(lit) => self.visit_template_literal(lit),
|
||||||
|
|
||||||
Expression::Identifier(ident) => self.visit_identifier_reference(ident),
|
Expression::Identifier(ident) => self.visit_identifier_reference(ident),
|
||||||
Expression::MetaProperty(meta) => self.visit_meta_property(meta),
|
Expression::MetaProperty(meta) => self.visit_meta_property(meta),
|
||||||
|
|
||||||
|
|
@ -732,10 +731,13 @@ pub trait Visit<'a>: Sized {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_import_expression(&mut self, expr: &ImportExpression<'a>) {
|
fn visit_import_expression(&mut self, expr: &ImportExpression<'a>) {
|
||||||
|
let kind = AstKind::ImportExpression(self.alloc(expr));
|
||||||
|
self.enter_node(kind);
|
||||||
self.visit_expression(&expr.source);
|
self.visit_expression(&expr.source);
|
||||||
for arg in &expr.arguments {
|
for arg in &expr.arguments {
|
||||||
self.visit_expression(arg);
|
self.visit_expression(arg);
|
||||||
}
|
}
|
||||||
|
self.leave_node(kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_logical_expression(&mut self, expr: &LogicalExpression<'a>) {
|
fn visit_logical_expression(&mut self, expr: &LogicalExpression<'a>) {
|
||||||
|
|
@ -1287,7 +1289,7 @@ pub trait Visit<'a>: Sized {
|
||||||
self.visit_import_declaration_specifier(specifier);
|
self.visit_import_declaration_specifier(specifier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: source
|
self.visit_string_literal(&decl.source);
|
||||||
// TODO: assertions
|
// TODO: assertions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1335,6 +1337,9 @@ pub trait Visit<'a>: Sized {
|
||||||
if let Some(decl) = &decl.declaration {
|
if let Some(decl) = &decl.declaration {
|
||||||
self.visit_declaration(decl);
|
self.visit_declaration(decl);
|
||||||
}
|
}
|
||||||
|
if let Some(ref source) = decl.source {
|
||||||
|
self.visit_string_literal(source);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_enum_member(&mut self, member: &TSEnumMember<'a>) {
|
fn visit_enum_member(&mut self, member: &TSEnumMember<'a>) {
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,7 @@ mod unicorn {
|
||||||
pub mod prefer_logical_operator_over_ternary;
|
pub mod prefer_logical_operator_over_ternary;
|
||||||
pub mod prefer_math_trunc;
|
pub mod prefer_math_trunc;
|
||||||
pub mod prefer_native_coercion_functions;
|
pub mod prefer_native_coercion_functions;
|
||||||
|
pub mod prefer_node_protocol;
|
||||||
pub mod prefer_number_properties;
|
pub mod prefer_number_properties;
|
||||||
pub mod prefer_optional_catch_binding;
|
pub mod prefer_optional_catch_binding;
|
||||||
pub mod prefer_query_selector;
|
pub mod prefer_query_selector;
|
||||||
|
|
@ -333,6 +334,7 @@ oxc_macros::declare_all_lint_rules! {
|
||||||
jest::valid_expect,
|
jest::valid_expect,
|
||||||
jest::valid_title,
|
jest::valid_title,
|
||||||
unicorn::catch_error_name,
|
unicorn::catch_error_name,
|
||||||
|
unicorn::prefer_node_protocol,
|
||||||
unicorn::empty_brace_spaces,
|
unicorn::empty_brace_spaces,
|
||||||
unicorn::error_message,
|
unicorn::error_message,
|
||||||
unicorn::escape_case,
|
unicorn::escape_case,
|
||||||
|
|
|
||||||
135
crates/oxc_linter/src/rules/unicorn/prefer_node_protocol.rs
Normal file
135
crates/oxc_linter/src/rules/unicorn/prefer_node_protocol.rs
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
use oxc_ast::{
|
||||||
|
ast::{Argument, CallExpression, Expression, ModuleDeclaration},
|
||||||
|
AstKind,
|
||||||
|
};
|
||||||
|
use oxc_diagnostics::{
|
||||||
|
miette::{self, Diagnostic},
|
||||||
|
thiserror::Error,
|
||||||
|
};
|
||||||
|
use oxc_macros::declare_oxc_lint;
|
||||||
|
use oxc_span::{Atom, Span};
|
||||||
|
|
||||||
|
use crate::{context::LintContext, rule::Rule, utils::NODE_BUILTINS_MODULE, AstNode};
|
||||||
|
|
||||||
|
#[derive(Debug, Error, Diagnostic)]
|
||||||
|
#[error("eslint-plugin-unicorn(prefer-node-protocol): Prefer using the `node:` protocol when importing Node.js builtin modules.")]
|
||||||
|
#[diagnostic(severity(warning), help("Prefer `node:{1}` over `{1}`."))]
|
||||||
|
struct PreferNodeProtocolDiagnostic(#[label] pub Span, String);
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct PreferNodeProtocol;
|
||||||
|
|
||||||
|
declare_oxc_lint!(
|
||||||
|
/// ### What it does
|
||||||
|
/// Prefer using the `node:protocol` when importing Node.js builtin modules
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// ### Example
|
||||||
|
/// ```javascript
|
||||||
|
/// // Bad
|
||||||
|
/// import fs from "fs";
|
||||||
|
/// // Good
|
||||||
|
/// import fs from "node:fs";
|
||||||
|
/// ```
|
||||||
|
PreferNodeProtocol,
|
||||||
|
style
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Rule for PreferNodeProtocol {
|
||||||
|
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||||
|
let string_lit_value_with_span = match node.kind() {
|
||||||
|
AstKind::ImportExpression(import) => match import.source {
|
||||||
|
Expression::StringLiteral(ref str_lit) => {
|
||||||
|
Some((str_lit.value.clone(), str_lit.span))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
AstKind::CallExpression(call) if !call.optional => get_static_require_arg(ctx, call),
|
||||||
|
AstKind::ModuleDeclaration(ModuleDeclaration::ImportDeclaration(import)) => {
|
||||||
|
Some((import.source.value.clone(), import.source.span))
|
||||||
|
}
|
||||||
|
AstKind::ModuleDeclaration(ModuleDeclaration::ExportNamedDeclaration(export)) => {
|
||||||
|
export.source.as_ref().map(|item| (item.value.clone(), item.span))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let Some((string_lit_value, span)) = string_lit_value_with_span else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let module_name = if let Some((prefix, postfix)) = string_lit_value.split_once('/') {
|
||||||
|
// `e.g. ignore "assert/"`
|
||||||
|
if postfix.is_empty() {
|
||||||
|
string_lit_value.to_string()
|
||||||
|
} else {
|
||||||
|
prefix.to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
string_lit_value.to_string()
|
||||||
|
};
|
||||||
|
if module_name.starts_with("node:") || !NODE_BUILTINS_MODULE.contains(&module_name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.diagnostic(PreferNodeProtocolDiagnostic(span, string_lit_value.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_static_require_arg<'a>(
|
||||||
|
ctx: &LintContext<'a>,
|
||||||
|
call: &CallExpression<'a>,
|
||||||
|
) -> Option<(Atom, Span)> {
|
||||||
|
let Expression::Identifier(ref id) = call.callee else { return None };
|
||||||
|
match call.arguments.as_slice() {
|
||||||
|
[Argument::Expression(Expression::StringLiteral(str))] if id.name == "require" => ctx
|
||||||
|
.semantic()
|
||||||
|
.scopes()
|
||||||
|
.root_unresolved_references()
|
||||||
|
.contains_key(&id.name)
|
||||||
|
.then(|| (str.value.clone(), str.span)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test() {
|
||||||
|
use crate::tester::Tester;
|
||||||
|
|
||||||
|
let pass = vec![
|
||||||
|
r#"import unicorn from "unicorn";"#,
|
||||||
|
r#"import fs from "./fs";"#,
|
||||||
|
r#"import fs from "unknown-builtin-module";"#,
|
||||||
|
r#"import fs from "node:fs";"#,
|
||||||
|
r#"import "punycode / ";"#,
|
||||||
|
r#"const fs = require("node:fs");"#,
|
||||||
|
r#"const fs = require("node:fs/promises");"#,
|
||||||
|
r"const fs = require(fs);",
|
||||||
|
r#"const fs = notRequire("fs");"#,
|
||||||
|
r#"const fs = foo.require("fs");"#,
|
||||||
|
r#"const fs = require.resolve("fs");"#,
|
||||||
|
r"const fs = require(`fs`);",
|
||||||
|
r#"const fs = require?.("fs");"#,
|
||||||
|
r#"const fs = require("fs", extra);"#,
|
||||||
|
r"const fs = require();",
|
||||||
|
r#"const fs = require(...["fs"]);"#,
|
||||||
|
r#"const fs = require("unicorn");"#,
|
||||||
|
];
|
||||||
|
|
||||||
|
let fail = vec![
|
||||||
|
r#"import fs from "fs";"#,
|
||||||
|
r#"export {promises} from "fs";"#,
|
||||||
|
r#"import fs from "fs/promises";"#,
|
||||||
|
r#"export {default} from "fs/promises";"#,
|
||||||
|
r#"import {promises} from "fs";"#,
|
||||||
|
r#"export {default as promises} from "fs";"#,
|
||||||
|
r"import {promises} from 'fs';",
|
||||||
|
r#"import "buffer";"#,
|
||||||
|
r#"import "child_process";"#,
|
||||||
|
r#"import "timers/promises";"#,
|
||||||
|
r#"const {promises} = require("fs")"#,
|
||||||
|
r"const fs = require('fs/promises')",
|
||||||
|
r#"export fs from "fs";"#,
|
||||||
|
r"await import('assert/strict')",
|
||||||
|
];
|
||||||
|
|
||||||
|
Tester::new_without_config(PreferNodeProtocol::NAME, pass, fail).test_and_snapshot();
|
||||||
|
}
|
||||||
102
crates/oxc_linter/src/snapshots/prefer_node_protocol.snap
Normal file
102
crates/oxc_linter/src/snapshots/prefer_node_protocol.snap
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
---
|
||||||
|
source: crates/oxc_linter/src/tester.rs
|
||||||
|
expression: prefer_node_protocol
|
||||||
|
---
|
||||||
|
⚠ eslint-plugin-unicorn(prefer-node-protocol): Prefer using the `node:` protocol when importing Node.js builtin modules.
|
||||||
|
╭─[prefer_node_protocol.tsx:1:1]
|
||||||
|
1 │ import fs from "fs";
|
||||||
|
· ────
|
||||||
|
╰────
|
||||||
|
help: Prefer `node:fs` over `fs`.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(prefer-node-protocol): Prefer using the `node:` protocol when importing Node.js builtin modules.
|
||||||
|
╭─[prefer_node_protocol.tsx:1:1]
|
||||||
|
1 │ export {promises} from "fs";
|
||||||
|
· ────
|
||||||
|
╰────
|
||||||
|
help: Prefer `node:fs` over `fs`.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(prefer-node-protocol): Prefer using the `node:` protocol when importing Node.js builtin modules.
|
||||||
|
╭─[prefer_node_protocol.tsx:1:1]
|
||||||
|
1 │ import fs from "fs/promises";
|
||||||
|
· ─────────────
|
||||||
|
╰────
|
||||||
|
help: Prefer `node:fs/promises` over `fs/promises`.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(prefer-node-protocol): Prefer using the `node:` protocol when importing Node.js builtin modules.
|
||||||
|
╭─[prefer_node_protocol.tsx:1:1]
|
||||||
|
1 │ export {default} from "fs/promises";
|
||||||
|
· ─────────────
|
||||||
|
╰────
|
||||||
|
help: Prefer `node:fs/promises` over `fs/promises`.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(prefer-node-protocol): Prefer using the `node:` protocol when importing Node.js builtin modules.
|
||||||
|
╭─[prefer_node_protocol.tsx:1:1]
|
||||||
|
1 │ import {promises} from "fs";
|
||||||
|
· ────
|
||||||
|
╰────
|
||||||
|
help: Prefer `node:fs` over `fs`.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(prefer-node-protocol): Prefer using the `node:` protocol when importing Node.js builtin modules.
|
||||||
|
╭─[prefer_node_protocol.tsx:1:1]
|
||||||
|
1 │ export {default as promises} from "fs";
|
||||||
|
· ────
|
||||||
|
╰────
|
||||||
|
help: Prefer `node:fs` over `fs`.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(prefer-node-protocol): Prefer using the `node:` protocol when importing Node.js builtin modules.
|
||||||
|
╭─[prefer_node_protocol.tsx:1:1]
|
||||||
|
1 │ import {promises} from 'fs';
|
||||||
|
· ────
|
||||||
|
╰────
|
||||||
|
help: Prefer `node:fs` over `fs`.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(prefer-node-protocol): Prefer using the `node:` protocol when importing Node.js builtin modules.
|
||||||
|
╭─[prefer_node_protocol.tsx:1:1]
|
||||||
|
1 │ import "buffer";
|
||||||
|
· ────────
|
||||||
|
╰────
|
||||||
|
help: Prefer `node:buffer` over `buffer`.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(prefer-node-protocol): Prefer using the `node:` protocol when importing Node.js builtin modules.
|
||||||
|
╭─[prefer_node_protocol.tsx:1:1]
|
||||||
|
1 │ import "child_process";
|
||||||
|
· ───────────────
|
||||||
|
╰────
|
||||||
|
help: Prefer `node:child_process` over `child_process`.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(prefer-node-protocol): Prefer using the `node:` protocol when importing Node.js builtin modules.
|
||||||
|
╭─[prefer_node_protocol.tsx:1:1]
|
||||||
|
1 │ import "timers/promises";
|
||||||
|
· ─────────────────
|
||||||
|
╰────
|
||||||
|
help: Prefer `node:timers/promises` over `timers/promises`.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(prefer-node-protocol): Prefer using the `node:` protocol when importing Node.js builtin modules.
|
||||||
|
╭─[prefer_node_protocol.tsx:1:1]
|
||||||
|
1 │ const {promises} = require("fs")
|
||||||
|
· ────
|
||||||
|
╰────
|
||||||
|
help: Prefer `node:fs` over `fs`.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(prefer-node-protocol): Prefer using the `node:` protocol when importing Node.js builtin modules.
|
||||||
|
╭─[prefer_node_protocol.tsx:1:1]
|
||||||
|
1 │ const fs = require('fs/promises')
|
||||||
|
· ─────────────
|
||||||
|
╰────
|
||||||
|
help: Prefer `node:fs/promises` over `fs/promises`.
|
||||||
|
|
||||||
|
× Unexpected token
|
||||||
|
╭─[prefer_node_protocol.tsx:1:1]
|
||||||
|
1 │ export fs from "fs";
|
||||||
|
· ──
|
||||||
|
╰────
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(prefer-node-protocol): Prefer using the `node:` protocol when importing Node.js builtin modules.
|
||||||
|
╭─[prefer_node_protocol.tsx:1:1]
|
||||||
|
1 │ await import('assert/strict')
|
||||||
|
· ───────────────
|
||||||
|
╰────
|
||||||
|
help: Prefer `node:assert/strict` over `assert/strict`.
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
mod jest;
|
mod jest;
|
||||||
|
mod node;
|
||||||
mod react;
|
mod react;
|
||||||
mod unicorn;
|
mod unicorn;
|
||||||
|
|
||||||
pub use self::{jest::*, react::*, unicorn::*};
|
pub use self::{jest::*, node::*, react::*, unicorn::*};
|
||||||
|
|
|
||||||
67
crates/oxc_linter/src/utils/node.rs
Normal file
67
crates/oxc_linter/src/utils/node.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
pub const NODE_BUILTINS_MODULE: phf::Set<&str> = phf::phf_set![
|
||||||
|
"_http_agent",
|
||||||
|
"_http_client",
|
||||||
|
"_http_common",
|
||||||
|
"_http_incoming",
|
||||||
|
"_http_outgoing",
|
||||||
|
"_http_server",
|
||||||
|
"_stream_duplex",
|
||||||
|
"_stream_passthrough",
|
||||||
|
"_stream_readable",
|
||||||
|
"_stream_transform",
|
||||||
|
"_stream_wrap",
|
||||||
|
"_stream_writable",
|
||||||
|
"_tls_common",
|
||||||
|
"_tls_wrap",
|
||||||
|
"assert",
|
||||||
|
"assert/strict",
|
||||||
|
"async_hooks",
|
||||||
|
"buffer",
|
||||||
|
"child_process",
|
||||||
|
"cluster",
|
||||||
|
"console",
|
||||||
|
"constants",
|
||||||
|
"crypto",
|
||||||
|
"dgram",
|
||||||
|
"diagnostics_channel",
|
||||||
|
"dns",
|
||||||
|
"dns/promises",
|
||||||
|
"domain",
|
||||||
|
"events",
|
||||||
|
"fs",
|
||||||
|
"fs/promises",
|
||||||
|
"http",
|
||||||
|
"http2",
|
||||||
|
"https",
|
||||||
|
"inspector",
|
||||||
|
"module",
|
||||||
|
"net",
|
||||||
|
"os",
|
||||||
|
"path",
|
||||||
|
"path/posix",
|
||||||
|
"path/win32",
|
||||||
|
"perf_hooks",
|
||||||
|
"process",
|
||||||
|
"punycode",
|
||||||
|
"querystring",
|
||||||
|
"readline",
|
||||||
|
"repl",
|
||||||
|
"stream",
|
||||||
|
"stream/consumers",
|
||||||
|
"stream/promises",
|
||||||
|
"stream/web",
|
||||||
|
"string_decoder",
|
||||||
|
"sys",
|
||||||
|
"timers",
|
||||||
|
"timers/promises",
|
||||||
|
"tls",
|
||||||
|
"trace_events",
|
||||||
|
"tty",
|
||||||
|
"url",
|
||||||
|
"util",
|
||||||
|
"util/types",
|
||||||
|
"v8",
|
||||||
|
"vm",
|
||||||
|
"worker_threads",
|
||||||
|
"zlib",
|
||||||
|
];
|
||||||
Loading…
Reference in a new issue