Skip to content

Commit

Permalink
Add prefer-json-parse-buffer rule (#1676)
Browse files Browse the repository at this point in the history
  • Loading branch information
fisker committed Dec 31, 2021
1 parent 6ab705b commit 84c9c70
Show file tree
Hide file tree
Showing 8 changed files with 850 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
AVA_FORCE_CI: not-ci
- run: npm run generate-rules-table
- run: npm run generate-usage-example
- run: git diff --exit-code --name-only
- run: git diff --exit-code
- uses: codecov/codecov-action@v1
with:
fail_ci_if_error: true
Expand Down
1 change: 1 addition & 0 deletions configs/recommended.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ module.exports = {
'unicorn/prefer-dom-node-text-content': 'error',
'unicorn/prefer-export-from': 'error',
'unicorn/prefer-includes': 'error',
'unicorn/prefer-json-parse-buffer': 'error',
'unicorn/prefer-keyboard-event-key': 'error',
'unicorn/prefer-math-trunc': 'error',
'unicorn/prefer-modern-dom-apis': 'error',
Expand Down
37 changes: 37 additions & 0 deletions docs/rules/prefer-json-parse-buffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Prefer reading a JSON file as a buffer

*This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config.*

🔧 *This rule is [auto-fixable](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems).*

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).

## Fail

```js
const packageJson = JSON.parse(await fs.readFile('./package.json', 'utf8'));
```

```js
const promise = fs.readFile('./package.json', {encoding: 'utf8'});
const packageJson = JSON.parse(promise);
```

## Pass

```js
const packageJson = JSON.parse(await fs.readFile('./package.json'));
```

```js
const promise = fs.readFile('./package.json', {encoding: 'utf8', signal});
const packageJson = JSON.parse(await promise);
```

```js
const data = JSON.parse(await fs.readFile('./file.json', 'buffer'));
```

```js
const data = JSON.parse(await fs.readFile('./file.json', 'gbk'));
```
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ Configure it in `package.json`.
"unicorn/prefer-dom-node-text-content": "error",
"unicorn/prefer-export-from": "error",
"unicorn/prefer-includes": "error",
"unicorn/prefer-json-parse-buffer": "error",
"unicorn/prefer-keyboard-event-key": "error",
"unicorn/prefer-math-trunc": "error",
"unicorn/prefer-modern-dom-apis": "error",
Expand Down Expand Up @@ -219,6 +220,7 @@ Each rule has emojis denoting:
| [prefer-dom-node-text-content](docs/rules/prefer-dom-node-text-content.md) | Prefer `.textContent` over `.innerText`. || | 💡 |
| [prefer-export-from](docs/rules/prefer-export-from.md) | Prefer `export…from` when re-exporting. || 🔧 | 💡 |
| [prefer-includes](docs/rules/prefer-includes.md) | Prefer `.includes()` over `.indexOf()` and `Array#some()` when checking for existence or non-existence. || 🔧 | 💡 |
| [prefer-json-parse-buffer](docs/rules/prefer-json-parse-buffer.md) | Prefer reading a JSON file as a buffer. || 🔧 | |
| [prefer-keyboard-event-key](docs/rules/prefer-keyboard-event-key.md) | Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. || 🔧 | |
| [prefer-math-trunc](docs/rules/prefer-math-trunc.md) | Enforce the use of `Math.trunc` instead of bitwise operators. || 🔧 | 💡 |
| [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()`. || 🔧 | |
Expand Down
158 changes: 158 additions & 0 deletions rules/prefer-json-parse-buffer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
'use strict';
const {findVariable, getStaticValue} = require('eslint-utils');
const {methodCallSelector} = require('./selectors/index.js');
const {removeArgument} = require('./fix/index.js');
const getKeyName = require('./utils/get-key-name.js');

const MESSAGE_ID = 'prefer-json-parse-buffer';
const messages = {
[MESSAGE_ID]: 'Prefer reading the JSON file as a buffer.',
};

const jsonParseArgumentSelector = [
methodCallSelector({
object: 'JSON',
method: 'parse',
argumentsLength: 1,
}),
' > .arguments:first-child',
].join('');

const getAwaitExpressionArgument = node => {
while (node.type === 'AwaitExpression') {
node = node.argument;
}

return node;
};

function getIdentifierDeclaration(node, scope) {
if (!node) {
return;
}

node = getAwaitExpressionArgument(node);

if (!node || node.type !== 'Identifier') {
return node;
}

const variable = findVariable(scope, node);
if (!variable) {
return;
}

const {identifiers, references} = variable;

if (identifiers.length !== 1 || references.length !== 2) {
return;
}

const [identifier] = identifiers;

if (
identifier.parent.type !== 'VariableDeclarator'
|| identifier.parent.id !== identifier
) {
return;
}

return getIdentifierDeclaration(identifier.parent.init, variable.scope);
}

const isUtf8EncodingStringNode = (node, scope) => {
const staticValue = getStaticValue(node, scope);
return staticValue && isUtf8EncodingString(staticValue.value);
};

const isUtf8EncodingString = value => {
if (typeof value !== 'string') {
return false;
}

value = value.toLowerCase();

return value === 'utf8' || value === 'utf-8';
};

function isUtf8Encoding(node, scope) {
if (
node.type === 'ObjectExpression'
&& node.properties.length === 1
&& node.properties[0].type === 'Property'
&& getKeyName(node.properties[0], scope) === 'encoding'
&& isUtf8EncodingStringNode(node.properties[0].value, scope)
) {
return true;
}

if (isUtf8EncodingStringNode(node, scope)) {
return true;
}

const staticValue = getStaticValue(node, scope);
if (!staticValue) {
return false;
}

const {value} = staticValue;
if (
typeof value === 'object'
&& Object.keys(value).length === 1
&& isUtf8EncodingString(value.encoding)
) {
return true;
}

return false;
}

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => ({
[jsonParseArgumentSelector](node) {
const scope = context.getScope();
node = getIdentifierDeclaration(node, scope);
if (
!(
node
&& node.type === 'CallExpression'
&& !node.optional
&& node.arguments.length === 2
&& !node.arguments.some(node => node.type === 'SpreadElement')
&& node.callee.type === 'MemberExpression'
&& !node.callee.optional
)
) {
return;
}

const method = getKeyName(node.callee, scope);
if (method !== 'readFile' && method !== 'readFileSync') {
return;
}

const [, charsetNode] = node.arguments;
if (!isUtf8Encoding(charsetNode, scope)) {
return;
}

return {
node: charsetNode,
messageId: MESSAGE_ID,
fix: fixer => removeArgument(fixer, charsetNode, context.getSourceCode()),
};
},
});

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer reading a JSON file as a buffer.',
},
fixable: 'code',
messages,
},
};
164 changes: 164 additions & 0 deletions test/prefer-json-parse-buffer.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/* eslint-disable no-template-curly-in-string */
import outdent from 'outdent';
import {getTester} from './utils/test.mjs';

const {test} = getTester(import.meta);

test.snapshot({
valid: [
'JSON.parse(await fs.readFile(file, "buffer"));',
'JSON.parse(await fs.readFile(file, "gbk"));',
'JSON.parse(await fs.readFile(file, ));',
'JSON.parse(await fs.readFile(file, unknown));',
'JSON.parse(await fs.readFile(...file, "utf8"));',
'JSON.parse(await fs.readFile(file, ..."utf8"));',
'JSON.parse(await fs.readFile(file, 0));',
'JSON.parse(await fs.readFile(file, "utf8", extraArgument));',
'JSON.parse(await fs.readFile?.(file, "utf8"));',
'JSON.parse(await fs?.readFile(file, "utf8"));',
'JSON.parse(await fs.notReadFileMethod(file, "utf8"));',
'JSON.parse?.(await fs.readFile(file, "utf8"));',
'JSON?.parse(await fs.readFile(file, "utf8"));',
'window.JSON.parse(await fs.readFile(file, "utf8"));',
'JSON.stringify(await fs.readFile(file, "utf8"));',
'NOT_JSON.parse(await fs.readFile(file, "utf8"));',
'for (const string of []) JSON.parse(string);',
'JSON.parse(await fs.readFile(file, "utf8"), extraArgument);',
'JSON.parse(foo);',
'JSON.parse();',
'JSON.parse(await fs.readFile(file, {encoding: "not-utf8"}));',
'JSON.parse(await fs.readFile(file, {encoding: "utf8", extraProperty: "utf8"}));',
'JSON.parse(await fs.readFile(file, {...encoding}));',
'JSON.parse(await fs.readFile(file, {encoding: unknown}));',
'const encoding = "gbk";JSON.parse(await fs.readFile(file, {encoding: encoding}));',
'const readingOptions = {encoding: "utf8", extraProperty: undefined};JSON.parse(await fs.readFile(file, readingOptions));',
outdent`
const {string} = await fs.readFile(file, "utf8");
JSON.parse(string);
`,
outdent`
const string = fs.readFile(file, () => {});
JSON.parse(string);
`,
outdent`
const abortControl = new AbortControl();
const {signal} = abortControl;
const promise = readFile(fileName, { encoding: "utf8", signal });
if (foo) {
JSON.parse(await promise);
} else {
controller.abort();
}
`,
outdent`
const string= await fs.readFile(file, "utf8");
console.log(string);
JSON.parse(string);
`,
outdent`
const string= await fs.readFile(file, "utf8");
JSON.parse(\`[\${string}]\`);
`,
outdent`
const foo = {};
foo.bar = await fs.readFile(file, "utf8");
JSON.parse(foo.bar);
`,
outdent`
const foo = await fs.readFile(file, "utf8");
const bar = await foo;
console.log(baz);
const baz = await bar;
JSON.parse(baz);
`,
outdent`
const foo = fs.readFile(file, "utf8");
function fn1() {
const foo = "{}";
JSON.parse(foo);
}
`,
],
invalid: [
'JSON.parse(await fs.readFile(file, "utf8"));',
'JSON.parse(await fs.readFile(file, "utf8",));',
'JSON.parse(await fs.readFile(file, "UTF-8"));',
'JSON.parse(await fs.readFileSync(file, "utf8"));',
'JSON.parse(fs.readFileSync(file, "utf8"));',
'const CHARSET = "UTF8"; JSON.parse(await fs.readFile(file, CHARSET));',
'const EIGHT = 8; JSON.parse(await fs.readFile(file, `utf${EIGHT}`));',
'JSON.parse(await fs["readFile"](file, "utf8"));',
'JSON.parse(await fs.readFile(file, {encoding: "utf8"}));',
'const EIGHT = 8; JSON.parse(await fs.readFile(file, {encoding: `utf${EIGHT}`}));',
'JSON.parse(await fs.readFile(file, {...({encoding: "utf8"})}));',
'const encoding = "utf8";JSON.parse(await fs.readFile(file, {encoding}));',
'const CHARSET = "utF-8", readingOptions = {encoding: CHARSET}; JSON.parse(await fs.readFile(file, readingOptions));',
'const EIGHT = 8, ENCODING = "encoding"; JSON.parse(await fs.readFile(file, {[ENCODING]: `utf${EIGHT}`}));',
outdent`
const string = await fs.readFile(file, "utf8");
JSON.parse(string);
`,
outdent`
let string = await fs.readFile(file, "utf8");
JSON.parse(string);
`,
outdent`
const foo = await await fs.readFile(file, "utf8");
const bar = await await foo;
const baz = await bar;
JSON.parse(baz);
`,
outdent`
var foo = await fs.readFile(file, "utf8");
let bar = await foo;
const baz = await bar;
JSON.parse(baz);
`,
outdent`
const foo = fs.readFile(file, "utf8");
async function fn1() {
const bar = await foo;
function fn2() {
const baz = bar;
JSON.parse(baz);
}
}
`,
outdent`
const buffer = fs.readFile(file, "utf8"); /* Should report */
const foo = buffer;
async function fn1() {
const buffer = fs.readFile(file, "utf8"); /* Should NOT report */
JSON.parse(await foo);
}
`,
outdent`
const buffer = fs.readFile(file, "utf8"); /* Should report */
const foo = buffer;
async function fn1() {
const buffer = fs.readFile(file, "utf8"); /* Should NOT report */
const baz = foo;
for (;;) {
const buffer = fs.readFile(file, "utf8"); /* Should NOT report */
JSON.parse(await baz);
}
}
`,
outdent`
const foo = fs.readFile(file, "utf8");
function fn1() {
JSON.parse(foo);
function fn2() {
const foo = "{}";
}
}
`,
// Maybe false positive, we can trace the callee if necessary
outdent`
const string = await NOT_A_FS_MODULE.readFile(file, "utf8");
JSON.parse(string);
`,
],
});

0 comments on commit 84c9c70

Please sign in to comment.