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)
This commit is contained in:
Jelle van der Waa 2024-09-05 15:39:25 +02:00 committed by GitHub
parent 979c16c486
commit a786acff41
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 213 additions and 0 deletions

View file

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

View file

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

View file

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