Repository: eslint-community/eslint-plugin-es-x
Pull request #210 (2024-10-31 07:13:18)

Add `es-x/no-json-modules` rule
related to https://github.com/eslint-community/eslint-plugin-es-x/issues/201


Patch:
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 707092a..fceeb4b 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -12,6 +12,7 @@ There is a config that enables the rules in this category: [`no-new-in-esnext`]
 |:--------|:------------|:--:|
 | [es-x/no-dynamic-import-options](./no-dynamic-import-options.md) | disallow second parameter to `import()`. |  |
 | [es-x/no-import-attributes](./no-import-attributes.md) | disallow Import Attributes. |  |
+| [es-x/no-json-modules](./no-json-modules.md) | disallow JSON Modules. |  |
 | [es-x/no-regexp-duplicate-named-capturing-groups](./no-regexp-duplicate-named-capturing-groups.md) | disallow RegExp duplicate named capturing groups. |  |
 | [es-x/no-regexp-modifiers](./no-regexp-modifiers.md) | disallow RegExp Modifiers. |  |
 | [es-x/no-set-prototype-difference](./no-set-prototype-difference.md) | disallow the `Set.prototype.difference` method. |  |
diff --git a/docs/rules/no-json-modules.md b/docs/rules/no-json-modules.md
new file mode 100644
index 0000000..74c2625
--- /dev/null
+++ b/docs/rules/no-json-modules.md
@@ -0,0 +1,33 @@
+---
+title: "es-x/no-json-modules"
+description: "disallow JSON Modules"
+---
+
+# es-x/no-json-modules
+> disallow JSON Modules
+
+- ❗ <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- ✅ The following configurations enable this rule: [no-new-in-esnext]
+
+This rule reports ES2025 [JSON Modules](https://github.com/tc39/proposal-json-modules) as errors.
+
+## 💡 Examples
+
+⛔ Examples of **incorrect** code for this rule:
+
+<eslint-playground type="bad">
+
+```js
+/*eslint es-x/no-json-modules: error */
+import json from "./foo.json" with { type: "json" };
+import("foo.json", { with: { type: "json" } });
+```
+
+</eslint-playground>
+
+## 📚 References
+
+- [Rule source](https://github.com/eslint-community/eslint-plugin-es-x/blob/master/lib/rules/no-json-modules.js)
+- [Test source](https://github.com/eslint-community/eslint-plugin-es-x/blob/master/tests/lib/rules/no-json-modules.js)
+
+[no-new-in-esnext]: ../configs/index.md#no-new-in-esnext
diff --git a/lib/configs/flat/no-new-in-esnext.js b/lib/configs/flat/no-new-in-esnext.js
index ee6f4f8..f100a61 100644
--- a/lib/configs/flat/no-new-in-esnext.js
+++ b/lib/configs/flat/no-new-in-esnext.js
@@ -13,6 +13,7 @@ module.exports = {
     rules: {
         "es-x/no-dynamic-import-options": "error",
         "es-x/no-import-attributes": "error",
+        "es-x/no-json-modules": "error",
         "es-x/no-regexp-duplicate-named-capturing-groups": "error",
         "es-x/no-regexp-modifiers": "error",
         "es-x/no-set-prototype-difference": "error",
diff --git a/lib/configs/no-new-in-esnext.js b/lib/configs/no-new-in-esnext.js
index 819f61d..d6eed7b 100644
--- a/lib/configs/no-new-in-esnext.js
+++ b/lib/configs/no-new-in-esnext.js
@@ -9,6 +9,7 @@ module.exports = {
     rules: {
         "es-x/no-dynamic-import-options": "error",
         "es-x/no-import-attributes": "error",
+        "es-x/no-json-modules": "error",
         "es-x/no-regexp-duplicate-named-capturing-groups": "error",
         "es-x/no-regexp-modifiers": "error",
         "es-x/no-set-prototype-difference": "error",
diff --git a/lib/index.js b/lib/index.js
index 97ee0ef..9874280 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -223,6 +223,7 @@ module.exports = {
         "no-intl-segmenter": require("./rules/no-intl-segmenter"),
         "no-intl-supportedvaluesof": require("./rules/no-intl-supportedvaluesof"),
         "no-json": require("./rules/no-json"),
+        "no-json-modules": require("./rules/no-json-modules"),
         "no-json-superset": require("./rules/no-json-superset"),
         "no-keyword-properties": require("./rules/no-keyword-properties"),
         "no-labelled-function-declarations": require("./rules/no-labelled-function-declarations"),
diff --git a/lib/rules/no-json-modules.js b/lib/rules/no-json-modules.js
new file mode 100644
index 0000000..c71b795
--- /dev/null
+++ b/lib/rules/no-json-modules.js
@@ -0,0 +1,160 @@
+"use strict"
+
+const {
+    getPropertyName,
+    findVariable,
+    getStaticValue,
+} = require("@eslint-community/eslint-utils")
+const { getSourceCode } = require("eslint-compat-utils")
+
+/**
+ * @typedef {import('estree').ImportAttribute} ImportAttribute
+ * @typedef {import('estree').Expression} Expression
+ * @typedef {import('estree').Property} Property
+ */
+
+/**
+ * Get the attribute name from a ImportAttribute node.
+ * @param {ImportAttribute} node The node to get.
+ * @returns {string|null} The attribute name of the node.
+ */
+function getAttributeName(node) {
+    if (node.key.type === "Literal") {
+        return String(node.key.value)
+    }
+    return node.key.name
+}
+
+module.exports = {
+    meta: {
+        docs: {
+            description: "disallow JSON Modules.",
+            category: "ES2025",
+            recommended: false,
+            url: "http://eslint-community.github.io/eslint-plugin-es-x/rules/no-json-modules.html",
+        },
+        fixable: null,
+        messages: {
+            forbidden: "ES2025 JSON Modules are forbidden.",
+        },
+        schema: [],
+        type: "problem",
+    },
+    create(context) {
+        const sourceCode = getSourceCode(context)
+
+        function findProperty(node, name) {
+            for (const prop of node.properties) {
+                if (prop.type !== "Property") {
+                    continue
+                }
+                if (getPropertyName(prop, sourceCode.getScope(node)) === name) {
+                    return prop
+                }
+            }
+            return null
+        }
+
+        function findVariableInit(node) {
+            /** @type {Variable} */
+            const variable = findVariable(sourceCode.getScope(node), node)
+            const def = variable?.defs[0]
+            if (!def) {
+                return null
+            }
+            if (def.type !== "Variable" || !def.node.init) {
+                return null
+            }
+            return def.node.init
+        }
+
+        /**
+         * @param {Expression} optionsNode
+         * @returns {Property|null}
+         */
+        function findImportAttributeFromOptions(optionsNode) {
+            const traversed = new Set()
+            return find(optionsNode)
+
+            /** @param {Expression} node  */
+            function find(node) {
+                if (traversed.has(node)) {
+                    return null
+                }
+                traversed.add(node)
+                if (node.type === "ObjectExpression") {
+                    return findProperty(node, "with")
+                }
+                if (node.type === "Identifier") {
+                    const init = findVariableInit(node)
+                    return init && find(init)
+                }
+                return null
+            }
+        }
+
+        /**
+         * @param {Expression} optionsNode
+         * @returns {Property|null}
+         */
+        function findImportTypeAttributeFromOptions(optionsNode) {
+            const attributes = findImportAttributeFromOptions(optionsNode)
+            if (!attributes) {
+                return null
+            }
+            const traversed = new Set()
+            return find(attributes.value)
+
+            /** @param {Expression} node  */
+            function find(node) {
+                if (traversed.has(node)) {
+                    return null
+                }
+                traversed.add(node)
+                if (node.type === "ObjectExpression") {
+                    return findProperty(node, "type")
+                }
+                if (node.type === "Identifier") {
+                    const init = findVariableInit(node)
+                    return init && find(init)
+                }
+                return null
+            }
+        }
+
+        return {
+            /** @param {ImportAttribute} node */
+            ImportAttribute(node) {
+                if (
+                    getAttributeName(node) === "type" &&
+                    node.value.value === "json"
+                ) {
+                    context.report({ node, messageId: "forbidden" })
+                }
+            },
+            ImportExpression(node) {
+                if (!node.options) {
+                    return
+                }
+
+                const attribute = findImportTypeAttributeFromOptions(
+                    node.options,
+                )
+                if (!attribute) {
+                    return
+                }
+                if (
+                    getStaticValue(
+                        attribute.value,
+                        sourceCode.getScope(attribute.value),
+                    )?.value === "json"
+                ) {
+                    context.report({
+                        node: attribute,
+                        messageId: "forbidden",
+                    })
+                }
+            },
+        }
+    },
+}


Test patch:
diff --git a/tests/lib/rules/no-json-modules.js b/tests/lib/rules/no-json-modules.js
new file mode 100644
index 0000000..b0f776f
--- /dev/null
+++ b/tests/lib/rules/no-json-modules.js
@@ -0,0 +1,66 @@
+"use strict"
+
+const RuleTester = require("../../tester")
+const rule = require("../../../lib/rules/no-json-modules.js")
+
+if (!RuleTester.isSupported(2025)) {
+    //eslint-disable-next-line no-console
+    console.log("Skip the tests of no-json-modules.")
+    return
+}
+
+new RuleTester({
+    languageOptions: { sourceType: "module" },
+}).run("no-json-modules", rule, {
+    valid: [
+        "import foo from 'foo'",
+        "export {foo} from 'foo'",
+        "export * from 'foo'",
+        "import foo from 'foo' with { type: 'unknown' }",
+        "export {foo} from 'foo' with { type: 'unknown' }",
+        "export * from 'foo' with { type: 'unknown' }",
+        "import('foo')",
+        "import('foo', unknown)",
+        "import('foo', { with: unknown })",
+        "import('foo', { with: { type: 'unknown' } })",
+    ],
+    invalid: [
+        {
+            code: "import foo from 'foo' with { type: 'json' }",
+            errors: ["ES2025 JSON Modules are forbidden."],
+        },
+        {
+            code: "export {foo} from 'foo' with { type: 'json' }",
+            errors: ["ES2025 JSON Modules are forbidden."],
+        },
+        {
+            code: "export * from 'foo' with { type: 'json' }",
+            errors: ["ES2025 JSON Modules are forbidden."],
+        },
+        {
+            code: "import('foo', { with: { type: 'json'} })",
+            errors: ["ES2025 JSON Modules are forbidden."],
+        },
+        {
+            code: `
+                const options = { with: { type: 'json' } }
+                import('foo', options)
+            `,
+            errors: ["ES2025 JSON Modules are forbidden."],
+        },
+        {
+            code: `
+                const attributes = { type: 'json' }
+                import('foo', { with: attributes })
+            `,
+            errors: ["ES2025 JSON Modules are forbidden."],
+        },
+        {
+            code: `
+                const typeJson = 'json'
+                import('foo', { with: { type: typeJson } })
+            `,
+            errors: ["ES2025 JSON Modules are forbidden."],
+        },
+    ],
+})