mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 20:32: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::AssignmentExpression(_) => "AssignmentExpression".into(),
|
||||
Self::AwaitExpression(_) => "AwaitExpression".into(),
|
||||
Self::BinaryExpression(b) => {
|
||||
format!("BinaryExpression({})", b.operator.as_str()).into()
|
||||
}
|
||||
Self::BinaryExpression(b) => format!("BinaryExpression{}", b.operator.as_str()).into(),
|
||||
Self::CallExpression(_) => "CallExpression".into(),
|
||||
Self::ChainExpression(_) => "ChainExpression".into(),
|
||||
Self::ConditionalExpression(_) => "ConditionalExpression".into(),
|
||||
|
|
|
|||
|
|
@ -555,7 +555,6 @@ pub trait Visit<'a>: Sized {
|
|||
Expression::RegExpLiteral(lit) => self.visit_reg_expr_literal(lit),
|
||||
Expression::StringLiteral(lit) => self.visit_string_literal(lit),
|
||||
Expression::TemplateLiteral(lit) => self.visit_template_literal(lit),
|
||||
|
||||
Expression::Identifier(ident) => self.visit_identifier_reference(ident),
|
||||
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>) {
|
||||
let kind = AstKind::ImportExpression(self.alloc(expr));
|
||||
self.enter_node(kind);
|
||||
self.visit_expression(&expr.source);
|
||||
for arg in &expr.arguments {
|
||||
self.visit_expression(arg);
|
||||
}
|
||||
self.leave_node(kind);
|
||||
}
|
||||
|
||||
fn visit_logical_expression(&mut self, expr: &LogicalExpression<'a>) {
|
||||
|
|
@ -1287,7 +1289,7 @@ pub trait Visit<'a>: Sized {
|
|||
self.visit_import_declaration_specifier(specifier);
|
||||
}
|
||||
}
|
||||
// TODO: source
|
||||
self.visit_string_literal(&decl.source);
|
||||
// TODO: assertions
|
||||
}
|
||||
|
||||
|
|
@ -1335,6 +1337,9 @@ pub trait Visit<'a>: Sized {
|
|||
if let Some(decl) = &decl.declaration {
|
||||
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>) {
|
||||
|
|
|
|||
|
|
@ -197,6 +197,7 @@ mod unicorn {
|
|||
pub mod prefer_logical_operator_over_ternary;
|
||||
pub mod prefer_math_trunc;
|
||||
pub mod prefer_native_coercion_functions;
|
||||
pub mod prefer_node_protocol;
|
||||
pub mod prefer_number_properties;
|
||||
pub mod prefer_optional_catch_binding;
|
||||
pub mod prefer_query_selector;
|
||||
|
|
@ -333,6 +334,7 @@ oxc_macros::declare_all_lint_rules! {
|
|||
jest::valid_expect,
|
||||
jest::valid_title,
|
||||
unicorn::catch_error_name,
|
||||
unicorn::prefer_node_protocol,
|
||||
unicorn::empty_brace_spaces,
|
||||
unicorn::error_message,
|
||||
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 node;
|
||||
mod react;
|
||||
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