Skip to content

Commit 9c7f4b9

Browse files
authored
Add es-x/no-json-modules rule (#210)
1 parent 239a2a7 commit 9c7f4b9

File tree

7 files changed

+263
-0
lines changed

7 files changed

+263
-0
lines changed

docs/rules/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ There is a config that enables the rules in this category: [`no-new-in-esnext`]
1212
|:--------|:------------|:--:|
1313
| [es-x/no-dynamic-import-options](./no-dynamic-import-options.md) | disallow second parameter to `import()`. | |
1414
| [es-x/no-import-attributes](./no-import-attributes.md) | disallow Import Attributes. | |
15+
| [es-x/no-json-modules](./no-json-modules.md) | disallow JSON Modules. | |
1516
| [es-x/no-regexp-duplicate-named-capturing-groups](./no-regexp-duplicate-named-capturing-groups.md) | disallow RegExp duplicate named capturing groups. | |
1617
| [es-x/no-regexp-modifiers](./no-regexp-modifiers.md) | disallow RegExp Modifiers. | |
1718
| [es-x/no-set-prototype-difference](./no-set-prototype-difference.md) | disallow the `Set.prototype.difference` method. | |

docs/rules/no-json-modules.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
title: "es-x/no-json-modules"
3+
description: "disallow JSON Modules"
4+
---
5+
6+
# es-x/no-json-modules
7+
> disallow JSON Modules
8+
9+
- ❗ <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
10+
- ✅ The following configurations enable this rule: [no-new-in-esnext]
11+
12+
This rule reports ES2025 [JSON Modules](https://github.com/tc39/proposal-json-modules) as errors.
13+
14+
## 💡 Examples
15+
16+
⛔ Examples of **incorrect** code for this rule:
17+
18+
<eslint-playground type="bad">
19+
20+
```js
21+
/*eslint es-x/no-json-modules: error */
22+
import json from "./foo.json" with { type: "json" };
23+
import("foo.json", { with: { type: "json" } });
24+
```
25+
26+
</eslint-playground>
27+
28+
## 📚 References
29+
30+
- [Rule source](https://github.com/eslint-community/eslint-plugin-es-x/blob/master/lib/rules/no-json-modules.js)
31+
- [Test source](https://github.com/eslint-community/eslint-plugin-es-x/blob/master/tests/lib/rules/no-json-modules.js)
32+
33+
[no-new-in-esnext]: ../configs/index.md#no-new-in-esnext

lib/configs/flat/no-new-in-esnext.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = {
1313
rules: {
1414
"es-x/no-dynamic-import-options": "error",
1515
"es-x/no-import-attributes": "error",
16+
"es-x/no-json-modules": "error",
1617
"es-x/no-regexp-duplicate-named-capturing-groups": "error",
1718
"es-x/no-regexp-modifiers": "error",
1819
"es-x/no-set-prototype-difference": "error",

lib/configs/no-new-in-esnext.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module.exports = {
99
rules: {
1010
"es-x/no-dynamic-import-options": "error",
1111
"es-x/no-import-attributes": "error",
12+
"es-x/no-json-modules": "error",
1213
"es-x/no-regexp-duplicate-named-capturing-groups": "error",
1314
"es-x/no-regexp-modifiers": "error",
1415
"es-x/no-set-prototype-difference": "error",

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ module.exports = {
223223
"no-intl-segmenter": require("./rules/no-intl-segmenter"),
224224
"no-intl-supportedvaluesof": require("./rules/no-intl-supportedvaluesof"),
225225
"no-json": require("./rules/no-json"),
226+
"no-json-modules": require("./rules/no-json-modules"),
226227
"no-json-superset": require("./rules/no-json-superset"),
227228
"no-keyword-properties": require("./rules/no-keyword-properties"),
228229
"no-labelled-function-declarations": require("./rules/no-labelled-function-declarations"),

lib/rules/no-json-modules.js

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"use strict"
2+
3+
const {
4+
getPropertyName,
5+
findVariable,
6+
getStaticValue,
7+
} = require("@eslint-community/eslint-utils")
8+
const { getSourceCode } = require("eslint-compat-utils")
9+
10+
/**
11+
* @typedef {import('estree').ImportAttribute} ImportAttribute
12+
* @typedef {import('estree').Expression} Expression
13+
* @typedef {import('estree').Property} Property
14+
*/
15+
16+
/**
17+
* Get the attribute name from a ImportAttribute node.
18+
* @param {ImportAttribute} node The node to get.
19+
* @returns {string|null} The attribute name of the node.
20+
*/
21+
function getAttributeName(node) {
22+
if (node.key.type === "Literal") {
23+
return String(node.key.value)
24+
}
25+
return node.key.name
26+
}
27+
28+
module.exports = {
29+
meta: {
30+
docs: {
31+
description: "disallow JSON Modules.",
32+
category: "ES2025",
33+
recommended: false,
34+
url: "http://eslint-community.github.io/eslint-plugin-es-x/rules/no-json-modules.html",
35+
},
36+
fixable: null,
37+
messages: {
38+
forbidden: "ES2025 JSON Modules are forbidden.",
39+
},
40+
schema: [],
41+
type: "problem",
42+
},
43+
create(context) {
44+
const sourceCode = getSourceCode(context)
45+
46+
function findProperty(node, name) {
47+
for (const prop of node.properties) {
48+
if (prop.type !== "Property") {
49+
continue
50+
}
51+
if (getPropertyName(prop, sourceCode.getScope(node)) === name) {
52+
return prop
53+
}
54+
}
55+
return null
56+
}
57+
58+
function findVariableInit(node) {
59+
/** @type {Variable} */
60+
const variable = findVariable(sourceCode.getScope(node), node)
61+
const def = variable?.defs[0]
62+
if (!def) {
63+
return null
64+
}
65+
if (def.type !== "Variable" || !def.node.init) {
66+
return null
67+
}
68+
return def.node.init
69+
}
70+
71+
/**
72+
* @param {Expression} optionsNode
73+
* @returns {Property|null}
74+
*/
75+
function findImportAttributeFromOptions(optionsNode) {
76+
const traversed = new Set()
77+
return find(optionsNode)
78+
79+
/** @param {Expression} node */
80+
function find(node) {
81+
if (traversed.has(node)) {
82+
return null
83+
}
84+
traversed.add(node)
85+
if (node.type === "ObjectExpression") {
86+
return findProperty(node, "with")
87+
}
88+
if (node.type === "Identifier") {
89+
const init = findVariableInit(node)
90+
return init && find(init)
91+
}
92+
return null
93+
}
94+
}
95+
96+
/**
97+
* @param {Expression} optionsNode
98+
* @returns {Property|null}
99+
*/
100+
function findImportTypeAttributeFromOptions(optionsNode) {
101+
const attributes = findImportAttributeFromOptions(optionsNode)
102+
if (!attributes) {
103+
return null
104+
}
105+
const traversed = new Set()
106+
return find(attributes.value)
107+
108+
/** @param {Expression} node */
109+
function find(node) {
110+
if (traversed.has(node)) {
111+
return null
112+
}
113+
traversed.add(node)
114+
if (node.type === "ObjectExpression") {
115+
return findProperty(node, "type")
116+
}
117+
if (node.type === "Identifier") {
118+
const init = findVariableInit(node)
119+
return init && find(init)
120+
}
121+
return null
122+
}
123+
}
124+
125+
return {
126+
/** @param {ImportAttribute} node */
127+
ImportAttribute(node) {
128+
if (
129+
getAttributeName(node) === "type" &&
130+
node.value.value === "json"
131+
) {
132+
context.report({ node, messageId: "forbidden" })
133+
}
134+
},
135+
ImportExpression(node) {
136+
if (!node.options) {
137+
return
138+
}
139+
140+
const attribute = findImportTypeAttributeFromOptions(
141+
node.options,
142+
)
143+
if (!attribute) {
144+
return
145+
}
146+
if (
147+
getStaticValue(
148+
attribute.value,
149+
sourceCode.getScope(attribute.value),
150+
)?.value === "json"
151+
) {
152+
context.report({
153+
node: attribute,
154+
messageId: "forbidden",
155+
})
156+
}
157+
},
158+
}
159+
},
160+
}

tests/lib/rules/no-json-modules.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"use strict"
2+
3+
const RuleTester = require("../../tester")
4+
const rule = require("../../../lib/rules/no-json-modules.js")
5+
6+
if (!RuleTester.isSupported(2025)) {
7+
//eslint-disable-next-line no-console
8+
console.log("Skip the tests of no-json-modules.")
9+
return
10+
}
11+
12+
new RuleTester({
13+
languageOptions: { sourceType: "module" },
14+
}).run("no-json-modules", rule, {
15+
valid: [
16+
"import foo from 'foo'",
17+
"export {foo} from 'foo'",
18+
"export * from 'foo'",
19+
"import foo from 'foo' with { type: 'unknown' }",
20+
"export {foo} from 'foo' with { type: 'unknown' }",
21+
"export * from 'foo' with { type: 'unknown' }",
22+
"import('foo')",
23+
"import('foo', unknown)",
24+
"import('foo', { with: unknown })",
25+
"import('foo', { with: { type: 'unknown' } })",
26+
],
27+
invalid: [
28+
{
29+
code: "import foo from 'foo' with { type: 'json' }",
30+
errors: ["ES2025 JSON Modules are forbidden."],
31+
},
32+
{
33+
code: "export {foo} from 'foo' with { type: 'json' }",
34+
errors: ["ES2025 JSON Modules are forbidden."],
35+
},
36+
{
37+
code: "export * from 'foo' with { type: 'json' }",
38+
errors: ["ES2025 JSON Modules are forbidden."],
39+
},
40+
{
41+
code: "import('foo', { with: { type: 'json'} })",
42+
errors: ["ES2025 JSON Modules are forbidden."],
43+
},
44+
{
45+
code: `
46+
const options = { with: { type: 'json' } }
47+
import('foo', options)
48+
`,
49+
errors: ["ES2025 JSON Modules are forbidden."],
50+
},
51+
{
52+
code: `
53+
const attributes = { type: 'json' }
54+
import('foo', { with: attributes })
55+
`,
56+
errors: ["ES2025 JSON Modules are forbidden."],
57+
},
58+
{
59+
code: `
60+
const typeJson = 'json'
61+
import('foo', { with: { type: typeJson } })
62+
`,
63+
errors: ["ES2025 JSON Modules are forbidden."],
64+
},
65+
],
66+
})

0 commit comments

Comments
 (0)