Skip to content

Commit ffe5943

Browse files
fiskersindresorhus
andauthored
Add prefer-response-static-json rule (#2778)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 8965b5d commit ffe5943

File tree

7 files changed

+373
-0
lines changed

7 files changed

+373
-0
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Prefer `Response.json()` over `new Response(JSON.stringify())`
2+
3+
💼 This rule is enabled in the following [configs](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config): ✅ `recommended`, ☑️ `unopinionated`.
4+
5+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6+
7+
<!-- end auto-generated rule header -->
8+
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->
9+
10+
Prefer using [`Response.json()`](https://developer.mozilla.org/en-US/docs/Web/API/Response/json_static) when possible.
11+
12+
## Examples
13+
14+
```js
15+
//
16+
const response = new Response(JSON.stringify(data));
17+
18+
//
19+
const response = Response.json(data);
20+
```

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export default [
169169
| [prefer-query-selector](docs/rules/prefer-query-selector.md) | Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()` and `.getElementsByName()`. || 🔧 | |
170170
| [prefer-reflect-apply](docs/rules/prefer-reflect-apply.md) | Prefer `Reflect.apply()` over `Function#apply()`. | ✅ ☑️ | 🔧 | |
171171
| [prefer-regexp-test](docs/rules/prefer-regexp-test.md) | Prefer `RegExp#test()` over `String#match()` and `RegExp#exec()`. | ✅ ☑️ | 🔧 | 💡 |
172+
| [prefer-response-static-json](docs/rules/prefer-response-static-json.md) | Prefer `Response.json()` over `new Response(JSON.stringify())`. | ✅ ☑️ | 🔧 | |
172173
| [prefer-set-has](docs/rules/prefer-set-has.md) | Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence. | ✅ ☑️ | 🔧 | 💡 |
173174
| [prefer-set-size](docs/rules/prefer-set-size.md) | Prefer using `Set#size` instead of `Array#length`. | ✅ ☑️ | 🔧 | |
174175
| [prefer-single-call](docs/rules/prefer-single-call.md) | Enforce combining multiple `Array#push()`, `Element#classList.{add,remove}()`, and `importScripts()` into one call. | ✅ ☑️ | 🔧 | 💡 |

rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export {default as 'prefer-prototype-methods'} from './prefer-prototype-methods.
112112
export {default as 'prefer-query-selector'} from './prefer-query-selector.js';
113113
export {default as 'prefer-reflect-apply'} from './prefer-reflect-apply.js';
114114
export {default as 'prefer-regexp-test'} from './prefer-regexp-test.js';
115+
export {default as 'prefer-response-static-json'} from './prefer-response-static-json.js';
115116
export {default as 'prefer-set-has'} from './prefer-set-has.js';
116117
export {default as 'prefer-set-size'} from './prefer-set-size.js';
117118
export {default as 'prefer-single-call'} from './prefer-single-call.js';
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {
2+
isNewExpression,
3+
isMethodCall,
4+
} from './ast/index.js';
5+
import {
6+
switchNewExpressionToCallExpression,
7+
} from './fix/index.js';
8+
import {
9+
getParenthesizedRange,
10+
isParenthesized,
11+
needsSemicolon,
12+
} from './utils/index.js';
13+
14+
const MESSAGE_ID = 'prefer-response-static-json';
15+
const messages = {
16+
[MESSAGE_ID]: 'Prefer using `Response.json(…)` over `JSON.stringify()`.',
17+
};
18+
19+
/** @param {import('eslint').Rule.RuleContext} context */
20+
const create = context => ({
21+
NewExpression(newExpression) {
22+
if (!isNewExpression(newExpression, {name: 'Response', minimumArguments: 1})) {
23+
return;
24+
}
25+
26+
const [jsonStringifyNode] = newExpression.arguments;
27+
if (!isMethodCall(jsonStringifyNode, {
28+
object: 'JSON',
29+
method: 'stringify',
30+
argumentsLength: 1,
31+
optionalCall: false,
32+
optionalMember: false,
33+
})) {
34+
return;
35+
}
36+
37+
return {
38+
node: jsonStringifyNode.callee,
39+
messageId: MESSAGE_ID,
40+
/** @param {import('eslint').Rule.RuleFixer} fixer */
41+
* fix(fixer) {
42+
const {sourceCode} = context;
43+
yield fixer.insertTextAfter(newExpression.callee, '.json');
44+
yield * switchNewExpressionToCallExpression(newExpression, sourceCode, fixer);
45+
46+
const [dataNode] = jsonStringifyNode.arguments;
47+
const callExpressionRange = getParenthesizedRange(jsonStringifyNode, sourceCode);
48+
const dataNodeRange = getParenthesizedRange(dataNode, sourceCode);
49+
// `(( JSON.stringify( (( data )), ) ))`
50+
// ^^^^^^^^^^^^^^^^^^^
51+
yield fixer.removeRange([callExpressionRange[0], dataNodeRange[0]]);
52+
// `(( JSON.stringify( (( data )), ) ))`
53+
// ^^^^^^
54+
yield fixer.removeRange([dataNodeRange[1], callExpressionRange[1]]);
55+
56+
if (
57+
!isParenthesized(newExpression, sourceCode)
58+
&& isParenthesized(newExpression.callee, sourceCode)
59+
) {
60+
const tokenBefore = sourceCode.getTokenBefore(newExpression);
61+
if (needsSemicolon(tokenBefore, sourceCode, '(')) {
62+
yield fixer.insertTextBefore(newExpression, ';');
63+
}
64+
}
65+
},
66+
};
67+
},
68+
});
69+
70+
/** @type {import('eslint').Rule.RuleModule} */
71+
const config = {
72+
create,
73+
meta: {
74+
type: 'suggestion',
75+
docs: {
76+
description: 'Prefer `Response.json()` over `new Response(JSON.stringify())`.',
77+
recommended: 'unopinionated',
78+
},
79+
fixable: 'code',
80+
81+
messages,
82+
},
83+
};
84+
85+
export default config;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import outdent from 'outdent';
2+
import {getTester} from './utils/test.js';
3+
4+
const {test} = getTester(import.meta);
5+
6+
test.snapshot({
7+
valid: [
8+
'Response.json(data)',
9+
'Response(JSON.stringify(data))',
10+
'new Response()',
11+
'new NotResponse(JSON.stringify(data))',
12+
'new Response(JSON.stringify(...data))',
13+
'new Response(JSON.stringify())',
14+
'new Response(JSON.stringify(data, extraArgument))',
15+
'new Response(JSON.stringify?.(data))',
16+
'new Response(JSON?.stringify(data))',
17+
'new Response(new JSON.stringify(data))',
18+
'new Response(JSON.not_stringify(data))',
19+
'new Response(NOT_JSON.stringify(data))',
20+
'new Response(data(JSON.stringify))',
21+
'new Response("" + JSON.stringify(data))',
22+
],
23+
invalid: [
24+
'new Response(JSON.stringify(data))',
25+
'new Response(JSON.stringify(data), extraArgument)',
26+
'new Response( (( JSON.stringify( (( 0, data )), ) )), )',
27+
outdent`
28+
function foo() {
29+
return new // comment
30+
Response(JSON.stringify(data))
31+
}
32+
`,
33+
'new Response(JSON.stringify(data), {status: 200})',
34+
outdent`
35+
foo
36+
new (( Response ))(JSON.stringify(data))
37+
`,
38+
outdent`
39+
foo;
40+
new (( Response ))(JSON.stringify(data))
41+
`,
42+
outdent`
43+
foo;
44+
(( new (( Response ))(JSON.stringify(data)) ))
45+
`,
46+
outdent`
47+
foo
48+
(( new (( Response ))(JSON.stringify(data)) ))
49+
`,
50+
],
51+
});
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
# Snapshot report for `test/prefer-response-static-json.js`
2+
3+
The actual snapshot is saved in `prefer-response-static-json.js.snap`.
4+
5+
Generated by [AVA](https://avajs.dev).
6+
7+
## invalid(1): new Response(JSON.stringify(data))
8+
9+
> Input
10+
11+
`␊
12+
1 | new Response(JSON.stringify(data))␊
13+
`
14+
15+
> Output
16+
17+
`␊
18+
1 | Response.json(data)␊
19+
`
20+
21+
> Error 1/1
22+
23+
`␊
24+
> 1 | new Response(JSON.stringify(data))␊
25+
| ^^^^^^^^^^^^^^ Prefer using \`Response.json(…)\` over \`JSON.stringify()\`.␊
26+
`
27+
28+
## invalid(2): new Response(JSON.stringify(data), extraArgument)
29+
30+
> Input
31+
32+
`␊
33+
1 | new Response(JSON.stringify(data), extraArgument)␊
34+
`
35+
36+
> Output
37+
38+
`␊
39+
1 | Response.json(data, extraArgument)␊
40+
`
41+
42+
> Error 1/1
43+
44+
`␊
45+
> 1 | new Response(JSON.stringify(data), extraArgument)␊
46+
| ^^^^^^^^^^^^^^ Prefer using \`Response.json(…)\` over \`JSON.stringify()\`.␊
47+
`
48+
49+
## invalid(3): new Response( (( JSON.stringify( (( 0, data )), ) )), )
50+
51+
> Input
52+
53+
`␊
54+
1 | new Response( (( JSON.stringify( (( 0, data )), ) )), )␊
55+
`
56+
57+
> Output
58+
59+
`␊
60+
1 | Response.json( (( 0, data )), )␊
61+
`
62+
63+
> Error 1/1
64+
65+
`␊
66+
> 1 | new Response( (( JSON.stringify( (( 0, data )), ) )), )␊
67+
| ^^^^^^^^^^^^^^ Prefer using \`Response.json(…)\` over \`JSON.stringify()\`.␊
68+
`
69+
70+
## invalid(4): function foo() { return new // comment Response(JSON.stringify(data)) }
71+
72+
> Input
73+
74+
`␊
75+
1 | function foo() {␊
76+
2 | return new // comment␊
77+
3 | Response(JSON.stringify(data))␊
78+
4 | }␊
79+
`
80+
81+
> Output
82+
83+
`␊
84+
1 | function foo() {␊
85+
2 | return ( // comment␊
86+
3 | Response.json(data))␊
87+
4 | }␊
88+
`
89+
90+
> Error 1/1
91+
92+
`␊
93+
1 | function foo() {␊
94+
2 | return new // comment␊
95+
> 3 | Response(JSON.stringify(data))␊
96+
| ^^^^^^^^^^^^^^ Prefer using \`Response.json(…)\` over \`JSON.stringify()\`.␊
97+
4 | }␊
98+
`
99+
100+
## invalid(5): new Response(JSON.stringify(data), {status: 200})
101+
102+
> Input
103+
104+
`␊
105+
1 | new Response(JSON.stringify(data), {status: 200})␊
106+
`
107+
108+
> Output
109+
110+
`␊
111+
1 | Response.json(data, {status: 200})␊
112+
`
113+
114+
> Error 1/1
115+
116+
`␊
117+
> 1 | new Response(JSON.stringify(data), {status: 200})␊
118+
| ^^^^^^^^^^^^^^ Prefer using \`Response.json(…)\` over \`JSON.stringify()\`.␊
119+
`
120+
121+
## invalid(6): foo new (( Response ))(JSON.stringify(data))
122+
123+
> Input
124+
125+
`␊
126+
1 | foo␊
127+
2 | new (( Response ))(JSON.stringify(data))␊
128+
`
129+
130+
> Output
131+
132+
`␊
133+
1 | foo␊
134+
2 | ;(( Response.json ))(data)␊
135+
`
136+
137+
> Error 1/1
138+
139+
`␊
140+
1 | foo␊
141+
> 2 | new (( Response ))(JSON.stringify(data))␊
142+
| ^^^^^^^^^^^^^^ Prefer using \`Response.json(…)\` over \`JSON.stringify()\`.␊
143+
`
144+
145+
## invalid(7): foo; new (( Response ))(JSON.stringify(data))
146+
147+
> Input
148+
149+
`␊
150+
1 | foo;␊
151+
2 | new (( Response ))(JSON.stringify(data))␊
152+
`
153+
154+
> Output
155+
156+
`␊
157+
1 | foo;␊
158+
2 | (( Response.json ))(data)␊
159+
`
160+
161+
> Error 1/1
162+
163+
`␊
164+
1 | foo;␊
165+
> 2 | new (( Response ))(JSON.stringify(data))␊
166+
| ^^^^^^^^^^^^^^ Prefer using \`Response.json(…)\` over \`JSON.stringify()\`.␊
167+
`
168+
169+
## invalid(8): foo; (( new (( Response ))(JSON.stringify(data)) ))
170+
171+
> Input
172+
173+
`␊
174+
1 | foo;␊
175+
2 | (( new (( Response ))(JSON.stringify(data)) ))␊
176+
`
177+
178+
> Output
179+
180+
`␊
181+
1 | foo;␊
182+
2 | (( (( Response.json ))(data) ))␊
183+
`
184+
185+
> Error 1/1
186+
187+
`␊
188+
1 | foo;␊
189+
> 2 | (( new (( Response ))(JSON.stringify(data)) ))␊
190+
| ^^^^^^^^^^^^^^ Prefer using \`Response.json(…)\` over \`JSON.stringify()\`.␊
191+
`
192+
193+
## invalid(9): foo (( new (( Response ))(JSON.stringify(data)) ))
194+
195+
> Input
196+
197+
`␊
198+
1 | foo␊
199+
2 | (( new (( Response ))(JSON.stringify(data)) ))␊
200+
`
201+
202+
> Output
203+
204+
`␊
205+
1 | foo␊
206+
2 | (( (( Response.json ))(data) ))␊
207+
`
208+
209+
> Error 1/1
210+
211+
`␊
212+
1 | foo␊
213+
> 2 | (( new (( Response ))(JSON.stringify(data)) ))␊
214+
| ^^^^^^^^^^^^^^ Prefer using \`Response.json(…)\` over \`JSON.stringify()\`.␊
215+
`
734 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)