From 872e8ad4ae6f907e477f0c3442d1a391e273bdc4 Mon Sep 17 00:00:00 2001 From: IWANABETHATGUY Date: Mon, 4 Dec 2023 01:06:47 +0800 Subject: [PATCH] feat: eslint-plugin-unicorn (recommended) prefer-node-protocol (#1618) part of https://github.com/oxc-project/oxc/issues/684 --- crates/oxc_ast/src/ast_kind.rs | 4 +- crates/oxc_ast/src/visit.rs | 9 +- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/unicorn/prefer_node_protocol.rs | 135 ++++++++++++++++++ .../src/snapshots/prefer_node_protocol.snap | 102 +++++++++++++ crates/oxc_linter/src/utils/mod.rs | 3 +- crates/oxc_linter/src/utils/node.rs | 67 +++++++++ 7 files changed, 316 insertions(+), 6 deletions(-) create mode 100644 crates/oxc_linter/src/rules/unicorn/prefer_node_protocol.rs create mode 100644 crates/oxc_linter/src/snapshots/prefer_node_protocol.snap create mode 100644 crates/oxc_linter/src/utils/node.rs diff --git a/crates/oxc_ast/src/ast_kind.rs b/crates/oxc_ast/src/ast_kind.rs index e7876d7e9..3506b27cb 100644 --- a/crates/oxc_ast/src/ast_kind.rs +++ b/crates/oxc_ast/src/ast_kind.rs @@ -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(), diff --git a/crates/oxc_ast/src/visit.rs b/crates/oxc_ast/src/visit.rs index d7491f7c3..7ee13aa75 100644 --- a/crates/oxc_ast/src/visit.rs +++ b/crates/oxc_ast/src/visit.rs @@ -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>) { diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 5973f2ae5..4d40c12f4 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -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, diff --git a/crates/oxc_linter/src/rules/unicorn/prefer_node_protocol.rs b/crates/oxc_linter/src/rules/unicorn/prefer_node_protocol.rs new file mode 100644 index 000000000..f632b8de2 --- /dev/null +++ b/crates/oxc_linter/src/rules/unicorn/prefer_node_protocol.rs @@ -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(); +} diff --git a/crates/oxc_linter/src/snapshots/prefer_node_protocol.snap b/crates/oxc_linter/src/snapshots/prefer_node_protocol.snap new file mode 100644 index 000000000..3f2cc6f45 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/prefer_node_protocol.snap @@ -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`. + + diff --git a/crates/oxc_linter/src/utils/mod.rs b/crates/oxc_linter/src/utils/mod.rs index 760ae04d8..b432d7dbd 100644 --- a/crates/oxc_linter/src/utils/mod.rs +++ b/crates/oxc_linter/src/utils/mod.rs @@ -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::*}; diff --git a/crates/oxc_linter/src/utils/node.rs b/crates/oxc_linter/src/utils/node.rs new file mode 100644 index 000000000..6d0ba14a0 --- /dev/null +++ b/crates/oxc_linter/src/utils/node.rs @@ -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", +];