Skip to content

Commit 84c9c70

Browse files
authored
Add prefer-json-parse-buffer rule (#1676)
1 parent 6ab705b commit 84c9c70

8 files changed

+850
-1
lines changed

.github/workflows/main.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
AVA_FORCE_CI: not-ci
4242
- run: npm run generate-rules-table
4343
- run: npm run generate-usage-example
44-
- run: git diff --exit-code --name-only
44+
- run: git diff --exit-code
4545
- uses: codecov/codecov-action@v1
4646
with:
4747
fail_ci_if_error: true

configs/recommended.js

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ module.exports = {
7070
'unicorn/prefer-dom-node-text-content': 'error',
7171
'unicorn/prefer-export-from': 'error',
7272
'unicorn/prefer-includes': 'error',
73+
'unicorn/prefer-json-parse-buffer': 'error',
7374
'unicorn/prefer-keyboard-event-key': 'error',
7475
'unicorn/prefer-math-trunc': 'error',
7576
'unicorn/prefer-modern-dom-apis': 'error',
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Prefer reading a JSON file as a buffer
2+
3+
*This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config.*
4+
5+
🔧 *This rule is [auto-fixable](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems).*
6+
7+
When reading and parsing a JSON file, it's unnecessary to read it as a string, because [`JSON.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) can also parse [`Buffer`](https://nodejs.org/api/buffer.html#buffer).
8+
9+
## Fail
10+
11+
```js
12+
const packageJson = JSON.parse(await fs.readFile('./package.json', 'utf8'));
13+
```
14+
15+
```js
16+
const promise = fs.readFile('./package.json', {encoding: 'utf8'});
17+
const packageJson = JSON.parse(promise);
18+
```
19+
20+
## Pass
21+
22+
```js
23+
const packageJson = JSON.parse(await fs.readFile('./package.json'));
24+
```
25+
26+
```js
27+
const promise = fs.readFile('./package.json', {encoding: 'utf8', signal});
28+
const packageJson = JSON.parse(await promise);
29+
```
30+
31+
```js
32+
const data = JSON.parse(await fs.readFile('./file.json', 'buffer'));
33+
```
34+
35+
```js
36+
const data = JSON.parse(await fs.readFile('./file.json', 'gbk'));
37+
```

readme.md

+2
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ Configure it in `package.json`.
103103
"unicorn/prefer-dom-node-text-content": "error",
104104
"unicorn/prefer-export-from": "error",
105105
"unicorn/prefer-includes": "error",
106+
"unicorn/prefer-json-parse-buffer": "error",
106107
"unicorn/prefer-keyboard-event-key": "error",
107108
"unicorn/prefer-math-trunc": "error",
108109
"unicorn/prefer-modern-dom-apis": "error",
@@ -219,6 +220,7 @@ Each rule has emojis denoting:
219220
| [prefer-dom-node-text-content](docs/rules/prefer-dom-node-text-content.md) | Prefer `.textContent` over `.innerText`. || | 💡 |
220221
| [prefer-export-from](docs/rules/prefer-export-from.md) | Prefer `export…from` when re-exporting. || 🔧 | 💡 |
221222
| [prefer-includes](docs/rules/prefer-includes.md) | Prefer `.includes()` over `.indexOf()` and `Array#some()` when checking for existence or non-existence. || 🔧 | 💡 |
223+
| [prefer-json-parse-buffer](docs/rules/prefer-json-parse-buffer.md) | Prefer reading a JSON file as a buffer. || 🔧 | |
222224
| [prefer-keyboard-event-key](docs/rules/prefer-keyboard-event-key.md) | Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. || 🔧 | |
223225
| [prefer-math-trunc](docs/rules/prefer-math-trunc.md) | Enforce the use of `Math.trunc` instead of bitwise operators. || 🔧 | 💡 |
224226
| [prefer-modern-dom-apis](docs/rules/prefer-modern-dom-apis.md) | Prefer `.before()` over `.insertBefore()`, `.replaceWith()` over `.replaceChild()`, prefer one of `.before()`, `.after()`, `.append()` or `.prepend()` over `insertAdjacentText()` and `insertAdjacentElement()`. || 🔧 | |

rules/prefer-json-parse-buffer.js

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
'use strict';
2+
const {findVariable, getStaticValue} = require('eslint-utils');
3+
const {methodCallSelector} = require('./selectors/index.js');
4+
const {removeArgument} = require('./fix/index.js');
5+
const getKeyName = require('./utils/get-key-name.js');
6+
7+
const MESSAGE_ID = 'prefer-json-parse-buffer';
8+
const messages = {
9+
[MESSAGE_ID]: 'Prefer reading the JSON file as a buffer.',
10+
};
11+
12+
const jsonParseArgumentSelector = [
13+
methodCallSelector({
14+
object: 'JSON',
15+
method: 'parse',
16+
argumentsLength: 1,
17+
}),
18+
' > .arguments:first-child',
19+
].join('');
20+
21+
const getAwaitExpressionArgument = node => {
22+
while (node.type === 'AwaitExpression') {
23+
node = node.argument;
24+
}
25+
26+
return node;
27+
};
28+
29+
function getIdentifierDeclaration(node, scope) {
30+
if (!node) {
31+
return;
32+
}
33+
34+
node = getAwaitExpressionArgument(node);
35+
36+
if (!node || node.type !== 'Identifier') {
37+
return node;
38+
}
39+
40+
const variable = findVariable(scope, node);
41+
if (!variable) {
42+
return;
43+
}
44+
45+
const {identifiers, references} = variable;
46+
47+
if (identifiers.length !== 1 || references.length !== 2) {
48+
return;
49+
}
50+
51+
const [identifier] = identifiers;
52+
53+
if (
54+
identifier.parent.type !== 'VariableDeclarator'
55+
|| identifier.parent.id !== identifier
56+
) {
57+
return;
58+
}
59+
60+
return getIdentifierDeclaration(identifier.parent.init, variable.scope);
61+
}
62+
63+
const isUtf8EncodingStringNode = (node, scope) => {
64+
const staticValue = getStaticValue(node, scope);
65+
return staticValue && isUtf8EncodingString(staticValue.value);
66+
};
67+
68+
const isUtf8EncodingString = value => {
69+
if (typeof value !== 'string') {
70+
return false;
71+
}
72+
73+
value = value.toLowerCase();
74+
75+
return value === 'utf8' || value === 'utf-8';
76+
};
77+
78+
function isUtf8Encoding(node, scope) {
79+
if (
80+
node.type === 'ObjectExpression'
81+
&& node.properties.length === 1
82+
&& node.properties[0].type === 'Property'
83+
&& getKeyName(node.properties[0], scope) === 'encoding'
84+
&& isUtf8EncodingStringNode(node.properties[0].value, scope)
85+
) {
86+
return true;
87+
}
88+
89+
if (isUtf8EncodingStringNode(node, scope)) {
90+
return true;
91+
}
92+
93+
const staticValue = getStaticValue(node, scope);
94+
if (!staticValue) {
95+
return false;
96+
}
97+
98+
const {value} = staticValue;
99+
if (
100+
typeof value === 'object'
101+
&& Object.keys(value).length === 1
102+
&& isUtf8EncodingString(value.encoding)
103+
) {
104+
return true;
105+
}
106+
107+
return false;
108+
}
109+
110+
/** @param {import('eslint').Rule.RuleContext} context */
111+
const create = context => ({
112+
[jsonParseArgumentSelector](node) {
113+
const scope = context.getScope();
114+
node = getIdentifierDeclaration(node, scope);
115+
if (
116+
!(
117+
node
118+
&& node.type === 'CallExpression'
119+
&& !node.optional
120+
&& node.arguments.length === 2
121+
&& !node.arguments.some(node => node.type === 'SpreadElement')
122+
&& node.callee.type === 'MemberExpression'
123+
&& !node.callee.optional
124+
)
125+
) {
126+
return;
127+
}
128+
129+
const method = getKeyName(node.callee, scope);
130+
if (method !== 'readFile' && method !== 'readFileSync') {
131+
return;
132+
}
133+
134+
const [, charsetNode] = node.arguments;
135+
if (!isUtf8Encoding(charsetNode, scope)) {
136+
return;
137+
}
138+
139+
return {
140+
node: charsetNode,
141+
messageId: MESSAGE_ID,
142+
fix: fixer => removeArgument(fixer, charsetNode, context.getSourceCode()),
143+
};
144+
},
145+
});
146+
147+
/** @type {import('eslint').Rule.RuleModule} */
148+
module.exports = {
149+
create,
150+
meta: {
151+
type: 'suggestion',
152+
docs: {
153+
description: 'Prefer reading a JSON file as a buffer.',
154+
},
155+
fixable: 'code',
156+
messages,
157+
},
158+
};

test/prefer-json-parse-buffer.mjs

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/* eslint-disable no-template-curly-in-string */
2+
import outdent from 'outdent';
3+
import {getTester} from './utils/test.mjs';
4+
5+
const {test} = getTester(import.meta);
6+
7+
test.snapshot({
8+
valid: [
9+
'JSON.parse(await fs.readFile(file, "buffer"));',
10+
'JSON.parse(await fs.readFile(file, "gbk"));',
11+
'JSON.parse(await fs.readFile(file, ));',
12+
'JSON.parse(await fs.readFile(file, unknown));',
13+
'JSON.parse(await fs.readFile(...file, "utf8"));',
14+
'JSON.parse(await fs.readFile(file, ..."utf8"));',
15+
'JSON.parse(await fs.readFile(file, 0));',
16+
'JSON.parse(await fs.readFile(file, "utf8", extraArgument));',
17+
'JSON.parse(await fs.readFile?.(file, "utf8"));',
18+
'JSON.parse(await fs?.readFile(file, "utf8"));',
19+
'JSON.parse(await fs.notReadFileMethod(file, "utf8"));',
20+
'JSON.parse?.(await fs.readFile(file, "utf8"));',
21+
'JSON?.parse(await fs.readFile(file, "utf8"));',
22+
'window.JSON.parse(await fs.readFile(file, "utf8"));',
23+
'JSON.stringify(await fs.readFile(file, "utf8"));',
24+
'NOT_JSON.parse(await fs.readFile(file, "utf8"));',
25+
'for (const string of []) JSON.parse(string);',
26+
'JSON.parse(await fs.readFile(file, "utf8"), extraArgument);',
27+
'JSON.parse(foo);',
28+
'JSON.parse();',
29+
'JSON.parse(await fs.readFile(file, {encoding: "not-utf8"}));',
30+
'JSON.parse(await fs.readFile(file, {encoding: "utf8", extraProperty: "utf8"}));',
31+
'JSON.parse(await fs.readFile(file, {...encoding}));',
32+
'JSON.parse(await fs.readFile(file, {encoding: unknown}));',
33+
'const encoding = "gbk";JSON.parse(await fs.readFile(file, {encoding: encoding}));',
34+
'const readingOptions = {encoding: "utf8", extraProperty: undefined};JSON.parse(await fs.readFile(file, readingOptions));',
35+
outdent`
36+
const {string} = await fs.readFile(file, "utf8");
37+
JSON.parse(string);
38+
`,
39+
outdent`
40+
const string = fs.readFile(file, () => {});
41+
JSON.parse(string);
42+
`,
43+
outdent`
44+
const abortControl = new AbortControl();
45+
const {signal} = abortControl;
46+
const promise = readFile(fileName, { encoding: "utf8", signal });
47+
if (foo) {
48+
JSON.parse(await promise);
49+
} else {
50+
controller.abort();
51+
}
52+
`,
53+
outdent`
54+
const string= await fs.readFile(file, "utf8");
55+
console.log(string);
56+
JSON.parse(string);
57+
`,
58+
outdent`
59+
const string= await fs.readFile(file, "utf8");
60+
JSON.parse(\`[\${string}]\`);
61+
`,
62+
outdent`
63+
const foo = {};
64+
foo.bar = await fs.readFile(file, "utf8");
65+
JSON.parse(foo.bar);
66+
`,
67+
outdent`
68+
const foo = await fs.readFile(file, "utf8");
69+
const bar = await foo;
70+
console.log(baz);
71+
const baz = await bar;
72+
JSON.parse(baz);
73+
`,
74+
outdent`
75+
const foo = fs.readFile(file, "utf8");
76+
function fn1() {
77+
const foo = "{}";
78+
JSON.parse(foo);
79+
}
80+
`,
81+
],
82+
invalid: [
83+
'JSON.parse(await fs.readFile(file, "utf8"));',
84+
'JSON.parse(await fs.readFile(file, "utf8",));',
85+
'JSON.parse(await fs.readFile(file, "UTF-8"));',
86+
'JSON.parse(await fs.readFileSync(file, "utf8"));',
87+
'JSON.parse(fs.readFileSync(file, "utf8"));',
88+
'const CHARSET = "UTF8"; JSON.parse(await fs.readFile(file, CHARSET));',
89+
'const EIGHT = 8; JSON.parse(await fs.readFile(file, `utf${EIGHT}`));',
90+
'JSON.parse(await fs["readFile"](file, "utf8"));',
91+
'JSON.parse(await fs.readFile(file, {encoding: "utf8"}));',
92+
'const EIGHT = 8; JSON.parse(await fs.readFile(file, {encoding: `utf${EIGHT}`}));',
93+
'JSON.parse(await fs.readFile(file, {...({encoding: "utf8"})}));',
94+
'const encoding = "utf8";JSON.parse(await fs.readFile(file, {encoding}));',
95+
'const CHARSET = "utF-8", readingOptions = {encoding: CHARSET}; JSON.parse(await fs.readFile(file, readingOptions));',
96+
'const EIGHT = 8, ENCODING = "encoding"; JSON.parse(await fs.readFile(file, {[ENCODING]: `utf${EIGHT}`}));',
97+
outdent`
98+
const string = await fs.readFile(file, "utf8");
99+
JSON.parse(string);
100+
`,
101+
outdent`
102+
let string = await fs.readFile(file, "utf8");
103+
JSON.parse(string);
104+
`,
105+
outdent`
106+
const foo = await await fs.readFile(file, "utf8");
107+
const bar = await await foo;
108+
const baz = await bar;
109+
JSON.parse(baz);
110+
`,
111+
outdent`
112+
var foo = await fs.readFile(file, "utf8");
113+
let bar = await foo;
114+
const baz = await bar;
115+
JSON.parse(baz);
116+
`,
117+
outdent`
118+
const foo = fs.readFile(file, "utf8");
119+
async function fn1() {
120+
const bar = await foo;
121+
122+
function fn2() {
123+
const baz = bar;
124+
JSON.parse(baz);
125+
}
126+
}
127+
`,
128+
outdent`
129+
const buffer = fs.readFile(file, "utf8"); /* Should report */
130+
const foo = buffer;
131+
async function fn1() {
132+
const buffer = fs.readFile(file, "utf8"); /* Should NOT report */
133+
JSON.parse(await foo);
134+
}
135+
`,
136+
outdent`
137+
const buffer = fs.readFile(file, "utf8"); /* Should report */
138+
const foo = buffer;
139+
async function fn1() {
140+
const buffer = fs.readFile(file, "utf8"); /* Should NOT report */
141+
const baz = foo;
142+
for (;;) {
143+
const buffer = fs.readFile(file, "utf8"); /* Should NOT report */
144+
JSON.parse(await baz);
145+
}
146+
}
147+
`,
148+
outdent`
149+
const foo = fs.readFile(file, "utf8");
150+
function fn1() {
151+
JSON.parse(foo);
152+
153+
function fn2() {
154+
const foo = "{}";
155+
}
156+
}
157+
`,
158+
// Maybe false positive, we can trace the callee if necessary
159+
outdent`
160+
const string = await NOT_A_FS_MODULE.readFile(file, "utf8");
161+
JSON.parse(string);
162+
`,
163+
],
164+
});

0 commit comments

Comments
 (0)