Skip to content

Commit

Permalink
feat(linter/import): add no-dynamic-require rule (#5389)
Browse files Browse the repository at this point in the history
  • Loading branch information
jelly authored Sep 5, 2024
1 parent 979c16c commit a786acf
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
132 changes: 132 additions & 0 deletions crates/oxc_linter/src/rules/import/no_dynamic_require.rs
Original file line number Diff line number Diff line change
@@ -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();
}
79 changes: 79 additions & 0 deletions crates/oxc_linter/src/snapshots/no_dynamic_require.snap
Original file line number Diff line number Diff line change
@@ -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]
1require("../" + 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]
1require(`../${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]
1require(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]
1require(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]
1require(`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]
1var 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]
1require(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]
1import("../" + "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]
1import(`../${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]
1import(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]
1import(name())
· ──────
╰────
help: Replace the argument with a literal string or immutable template literal

0 comments on commit a786acf

Please sign in to comment.