From a786acff41a7a4f15077ffb103958f5510bd8d98 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Thu, 5 Sep 2024 15:39:25 +0200 Subject: [PATCH] feat(linter/import): add no-dynamic-require rule (#5389) Rule Detail: [link](https://github.com/import-js/eslint-plugin-import/blob/v2.29.1/docs/rules/no-dynamic-require.md) --- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/import/no_dynamic_require.rs | 132 ++++++++++++++++++ .../src/snapshots/no_dynamic_require.snap | 79 +++++++++++ 3 files changed, 213 insertions(+) create mode 100644 crates/oxc_linter/src/rules/import/no_dynamic_require.rs create mode 100644 crates/oxc_linter/src/snapshots/no_dynamic_require.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 7ec40bbb0e0ed..11ef4f45ae353 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -17,6 +17,7 @@ mod import { // pub mod no_deprecated; pub mod max_dependencies; pub mod no_duplicates; + pub mod no_dynamic_require; pub mod no_named_as_default; pub mod no_named_as_default_member; pub mod no_self_import; @@ -791,6 +792,7 @@ oxc_macros::declare_all_lint_rules! { import::no_named_as_default_member, import::no_self_import, // import::no_unused_modules, + import::no_dynamic_require, import::no_duplicates, import::no_default_export, import::no_webpack_loader_syntax, diff --git a/crates/oxc_linter/src/rules/import/no_dynamic_require.rs b/crates/oxc_linter/src/rules/import/no_dynamic_require.rs new file mode 100644 index 0000000000000..1ec1022741856 --- /dev/null +++ b/crates/oxc_linter/src/rules/import/no_dynamic_require.rs @@ -0,0 +1,132 @@ +use oxc_ast::{ast::Expression, AstKind}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, Span}; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn no_dnyamic_require_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Expected a literal string or immutable template literal") + .with_help("Replace the argument with a literal string or immutable template literal") + .with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct NoDynamicRequire { + esmodule: bool, +} + +declare_oxc_lint!( + /// ### What it does + /// + /// Forbid imports which use an expression for the module argument. + /// + /// ### Why is this bad? + /// + /// Import statements which use an expression resolved at runtime makes it to find where the + /// import comes from and some static code analysis tools might not be able to resolve them. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```javascript + /// require(name); + /// require(`../${name}`); + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```javascript + /// require('../name'); + /// require(`../name`); + /// ``` + NoDynamicRequire, + restriction, +); + +impl Rule for NoDynamicRequire { + fn from_configuration(value: serde_json::Value) -> Self { + let esmodule = value + .get(0) + .and_then(|config| config.get("esmodule")) + .and_then(serde_json::Value::as_bool) + .unwrap_or(false); + + Self { esmodule } + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { + AstKind::ImportExpression(import) => { + if self.esmodule && !is_static_value(&import.source) { + ctx.diagnostic(no_dnyamic_require_diagnostic(import.source.span())); + } + } + AstKind::CallExpression(call) => { + if call.arguments.is_empty() { + return; + } + + if !call.callee.is_specific_id("require") { + return; + } + + let Some(expr) = &call.arguments[0].as_expression() else { + return; + }; + + if !is_static_value(expr) { + ctx.diagnostic(no_dnyamic_require_diagnostic(call.callee.span())); + } + } + _ => {} + }; + } +} + +fn is_static_value(expr: &Expression) -> bool { + expr.is_string_literal() && expr.is_immutable_value() +} + +#[test] +fn test() { + use crate::tester::Tester; + use serde_json::json; + + let pass = vec![ + (r#"import _ from "lodash""#, None), + (r#"require("foo")"#, None), + ("require(`foo`)", None), + (r#"require("./foo")"#, None), + (r#"require("@scope/foo")"#, None), + ("require()", None), + (r#"require("./foo", "bar" + "okay")"#, None), + (r#"var foo = require("foo")"#, None), + ("var foo = require(`foo`)", None), + (r#"var foo = require("./foo")"#, None), + (r#"var foo = require("@scope/foo")"#, None), + (r#"import("foo")"#, Some(json!([{ "esmodule": true }]))), + ("import(`foo`)", Some(json!([{ "esmodule": true }]))), + (r#"import("./foo")"#, Some(json!([{ "esmodule": true }]))), + (r#"import("@scope/foo")"#, Some(json!([{ "esmodule": true }]))), + (r#"var foo = import("foo")"#, Some(json!([{ "esmodule": true }]))), + ("var foo = import(`foo`)", Some(json!([{ "esmodule": true }]))), + (r#"var foo = import("./foo")"#, Some(json!([{ "esmodule": true }]))), + (r#"var foo = import("@scope/foo")"#, Some(json!([{ "esmodule": true }]))), + ]; + + let fail = vec![ + (r#"require("../" + name)"#, None), + ("require(`../${name}`)", None), + ("require(name)", None), + ("require(name())", None), + ("require(`foo${x}`)", None), + ("var foo = require(`foo${x}`)", None), + (r#"require(name + "foo", "bar")"#, Some(json!([{ "esmodule": true }]))), + (r#"import("../" + "name")"#, Some(json!([{ "esmodule": true }]))), + ("import(`../${name}`)", Some(json!([{ "esmodule": true }]))), + ("import(name)", Some(json!([{ "esmodule": true }]))), + ("import(name())", Some(json!([{ "esmodule": true }]))), + ]; + + Tester::new(NoDynamicRequire::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_dynamic_require.snap b/crates/oxc_linter/src/snapshots/no_dynamic_require.snap new file mode 100644 index 0000000000000..201e55544a25c --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_dynamic_require.snap @@ -0,0 +1,79 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-import(no-dynamic-require): Expected a literal string or immutable template literal + ╭─[no_dynamic_require.tsx:1:1] + 1 │ require("../" + name) + · ─────── + ╰──── + help: Replace the argument with a literal string or immutable template literal + + ⚠ eslint-plugin-import(no-dynamic-require): Expected a literal string or immutable template literal + ╭─[no_dynamic_require.tsx:1:1] + 1 │ require(`../${name}`) + · ─────── + ╰──── + help: Replace the argument with a literal string or immutable template literal + + ⚠ eslint-plugin-import(no-dynamic-require): Expected a literal string or immutable template literal + ╭─[no_dynamic_require.tsx:1:1] + 1 │ require(name) + · ─────── + ╰──── + help: Replace the argument with a literal string or immutable template literal + + ⚠ eslint-plugin-import(no-dynamic-require): Expected a literal string or immutable template literal + ╭─[no_dynamic_require.tsx:1:1] + 1 │ require(name()) + · ─────── + ╰──── + help: Replace the argument with a literal string or immutable template literal + + ⚠ eslint-plugin-import(no-dynamic-require): Expected a literal string or immutable template literal + ╭─[no_dynamic_require.tsx:1:1] + 1 │ require(`foo${x}`) + · ─────── + ╰──── + help: Replace the argument with a literal string or immutable template literal + + ⚠ eslint-plugin-import(no-dynamic-require): Expected a literal string or immutable template literal + ╭─[no_dynamic_require.tsx:1:11] + 1 │ var foo = require(`foo${x}`) + · ─────── + ╰──── + help: Replace the argument with a literal string or immutable template literal + + ⚠ eslint-plugin-import(no-dynamic-require): Expected a literal string or immutable template literal + ╭─[no_dynamic_require.tsx:1:1] + 1 │ require(name + "foo", "bar") + · ─────── + ╰──── + help: Replace the argument with a literal string or immutable template literal + + ⚠ eslint-plugin-import(no-dynamic-require): Expected a literal string or immutable template literal + ╭─[no_dynamic_require.tsx:1:8] + 1 │ import("../" + "name") + · ────────────── + ╰──── + help: Replace the argument with a literal string or immutable template literal + + ⚠ eslint-plugin-import(no-dynamic-require): Expected a literal string or immutable template literal + ╭─[no_dynamic_require.tsx:1:8] + 1 │ import(`../${name}`) + · ──────────── + ╰──── + help: Replace the argument with a literal string or immutable template literal + + ⚠ eslint-plugin-import(no-dynamic-require): Expected a literal string or immutable template literal + ╭─[no_dynamic_require.tsx:1:8] + 1 │ import(name) + · ──── + ╰──── + help: Replace the argument with a literal string or immutable template literal + + ⚠ eslint-plugin-import(no-dynamic-require): Expected a literal string or immutable template literal + ╭─[no_dynamic_require.tsx:1:8] + 1 │ import(name()) + · ────── + ╰──── + help: Replace the argument with a literal string or immutable template literal