feat: eslint-plugin-unicorn (recommended) prefer-node-protocol (#1618)

part of https://github.com/oxc-project/oxc/issues/684
This commit is contained in:
IWANABETHATGUY 2023-12-04 01:06:47 +08:00 committed by GitHub
parent f74fa975b1
commit 872e8ad4ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 316 additions and 6 deletions

View file

@ -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(),

View file

@ -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>) {

View file

@ -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,

View 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();
}

View 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`.

View file

@ -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::*};

View 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",
];