Skip to content

Commit bb8873d

Browse files
authored
Fixed parsing error with Infinity and NaN. (#2)
* Fixed parsing error with Infinity and NaN. * fix * fix * revert
1 parent b0ff0b3 commit bb8873d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+5013
-41
lines changed

docs/rules/valid-json-number.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ This rule reports numbers that cannot be parsed with JSON.
2424
"GOOD": [123, 0.4, -42],
2525

2626
/* ✗ BAD */
27-
"BAD": [123., .4, +42]
27+
"BAD": [123., .4, +42, Infinity, NaN]
2828
}
2929
```
3030

lib/parser/ast.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,41 +33,76 @@ export interface JSONProgram extends BaseJSONNode {
3333
body: [JSONExpressionStatement]
3434
comments: Comment[]
3535
tokens: AST.Token[]
36+
parent?: null
3637
}
3738

3839
export interface JSONExpressionStatement extends BaseJSONNode {
3940
type: "JSONExpressionStatement"
4041
expression: JSONExpression
42+
parent?: JSONProgram
4143
}
4244

4345
export type JSONExpression =
4446
| JSONArrayExpression
4547
| JSONObjectExpression
4648
| JSONLiteral
49+
| JSONUnaryExpression
50+
| JSONNumberIdentifier
4751

4852
export interface JSONArrayExpression extends BaseJSONNode {
4953
type: "JSONArrayExpression"
5054
elements: JSONExpression[]
55+
parent?: JSONArrayExpression | JSONProperty | JSONExpressionStatement
5156
}
5257

5358
export interface JSONObjectExpression extends BaseJSONNode {
5459
type: "JSONObjectExpression"
5560
properties: JSONProperty[]
61+
parent?: JSONArrayExpression | JSONProperty | JSONExpressionStatement
5662
}
5763

5864
export interface JSONProperty extends BaseJSONNode {
5965
type: "JSONProperty"
6066
key: JSONIdentifier | JSONLiteral
6167
value: JSONExpression
68+
parent?: JSONObjectExpression
6269
}
6370

6471
export interface JSONIdentifier extends BaseJSONNode {
6572
type: "JSONIdentifier"
6673
name: string
74+
parent?:
75+
| JSONArrayExpression
76+
| JSONProperty
77+
| JSONExpressionStatement
78+
| JSONUnaryExpression
79+
}
80+
81+
export interface JSONNumberIdentifier extends JSONIdentifier {
82+
name: "Infinity" | "NaN"
6783
}
6884

6985
export interface JSONLiteral extends BaseJSONNode {
7086
type: "JSONLiteral"
7187
value: string | boolean | number | null
7288
raw: string
89+
parent?:
90+
| JSONArrayExpression
91+
| JSONProperty
92+
| JSONExpressionStatement
93+
| JSONUnaryExpression
94+
}
95+
96+
export interface JSONNumberLiteral extends JSONLiteral {
97+
type: "JSONLiteral"
98+
value: number
99+
raw: string
100+
}
101+
102+
export interface JSONUnaryExpression extends BaseJSONNode {
103+
type: "JSONUnaryExpression"
104+
operator: "-" | "+"
105+
prefix: true
106+
argument: JSONNumberLiteral | JSONNumberIdentifier
107+
parent?: JSONArrayExpression | JSONProperty | JSONExpressionStatement
73108
}

lib/parser/convert.ts

Lines changed: 80 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import {
2222
JSONLiteral,
2323
JSONIdentifier,
2424
Locations,
25+
JSONUnaryExpression,
26+
JSONNumberIdentifier,
27+
JSONNumberLiteral,
2528
} from "./ast"
2629
import { getKeys, getNodes } from "./traverse"
2730
import {
@@ -45,9 +48,12 @@ export function convertNode(node: Node, tokens: AST.Token[]): JSONNode {
4548
if (node.type === "ArrayExpression") {
4649
return convertArrayExpressionNode(node, tokens)
4750
}
48-
if (node.type === "Literal" || node.type === "UnaryExpression") {
51+
if (node.type === "Literal") {
4952
return convertLiteralNode(node, tokens)
5053
}
54+
if (node.type === "UnaryExpression") {
55+
return convertUnaryExpressionNode(node, tokens)
56+
}
5157
if (node.type === "Identifier") {
5258
return convertIdentifierNode(node, tokens)
5359
}
@@ -84,7 +90,9 @@ function convertProgramNode(node: Program, tokens: AST.Token[]): JSONProgram {
8490
}
8591
const expression = bodyNode.expression
8692
if (expression.type === "Identifier") {
87-
return throwUnexpectedNodeError(expression, tokens)
93+
if (!isNumIdentifier(expression)) {
94+
return throwUnexpectedNodeError(expression, tokens)
95+
}
8896
}
8997
const body: JSONExpressionStatement = {
9098
type: "JSONExpressionStatement",
@@ -156,7 +164,9 @@ function convertPropertyNode(
156164
return throwUnexpectedNodeError(node.key, tokens)
157165
}
158166
if (node.value.type === "Identifier") {
159-
return throwUnexpectedNodeError(node.value, tokens)
167+
if (!isNumIdentifier(node.value)) {
168+
return throwUnexpectedNodeError(node.value, tokens)
169+
}
160170
}
161171
const nn: JSONProperty = {
162172
type: "JSONProperty",
@@ -192,7 +202,9 @@ function convertArrayExpressionNode(
192202
)
193203
}
194204
if (child.type === "Identifier") {
195-
return throwUnexpectedNodeError(child, tokens)
205+
if (!isNumIdentifier(child)) {
206+
return throwUnexpectedNodeError(child, tokens)
207+
}
196208
}
197209
return convertNode(child, tokens) as JSONExpression
198210
}),
@@ -205,56 +217,80 @@ function convertArrayExpressionNode(
205217
/**
206218
* Convert Literal node to JSONLiteral node
207219
*/
208-
function convertLiteralNode(
209-
node: Literal | UnaryExpression,
210-
tokens: AST.Token[],
211-
): JSONLiteral {
220+
function convertLiteralNode(node: Literal, tokens: AST.Token[]): JSONLiteral {
212221
/* istanbul ignore next */
213-
if (node.type !== "Literal" && node.type !== "UnaryExpression") {
222+
if (node.type !== "Literal") {
214223
return throwUnexpectedNodeError(node, tokens)
215224
}
216-
let literal: Literal
217-
let value: number | string | null | boolean | RegExp | undefined
218-
if (node.type === "UnaryExpression") {
219-
if (node.operator !== "-" && node.operator !== "+") {
220-
return throwUnexpectedNodeError(node, tokens)
221-
}
222-
const argument = node.argument
223-
if (argument.type !== "Literal" || typeof argument.value !== "number") {
224-
return throwUnexpectedNodeError(node.argument, tokens)
225-
}
226-
if (node.range![0] + 1 !== argument.range![0]) {
227-
return throwUnexpectedTokenError(" ", argument)
228-
}
229-
literal = argument
230-
value = node.operator === "-" ? -argument.value : argument.value
231-
} else {
232-
literal = node
233-
value = node.value
234-
}
225+
const value = node.value
235226

236-
if ((literal as RegExpLiteral).regex) {
237-
return throwUnexpectedNodeError(literal, tokens)
227+
if ((node as RegExpLiteral).regex) {
228+
return throwUnexpectedNodeError(node, tokens)
238229
}
239-
if ((literal as any).bigint) {
240-
return throwUnexpectedNodeError(literal, tokens)
230+
if ((node as any).bigint) {
231+
return throwUnexpectedNodeError(node, tokens)
241232
}
242233
if (value !== null) {
243234
if (
244235
typeof value !== "string" &&
245236
typeof value !== "number" &&
246237
typeof value !== "boolean"
247238
) {
248-
return throwUnexpectedNodeError(literal, tokens)
239+
return throwUnexpectedNodeError(node, tokens)
249240
}
250241
}
251242
const nn: JSONLiteral = {
252243
type: "JSONLiteral",
253244
value,
254-
raw: literal.raw!,
245+
raw: node.raw!,
255246
...getFixLocation(node),
256247
}
257-
checkUnexpectKeys(literal, tokens, nn)
248+
checkUnexpectKeys(node, tokens, nn)
249+
return nn
250+
}
251+
252+
/**
253+
* Convert UnaryExpression node to JSONUnaryExpression node
254+
*/
255+
function convertUnaryExpressionNode(
256+
node: UnaryExpression,
257+
tokens: AST.Token[],
258+
): JSONUnaryExpression {
259+
/* istanbul ignore next */
260+
if (node.type !== "UnaryExpression") {
261+
return throwUnexpectedNodeError(node, tokens)
262+
}
263+
const operator = node.operator
264+
265+
if (operator !== "-" && operator !== "+") {
266+
return throwUnexpectedNodeError(node, tokens)
267+
}
268+
const argument = node.argument
269+
if (argument.type === "Literal") {
270+
if (typeof argument.value !== "number") {
271+
return throwUnexpectedNodeError(argument, tokens)
272+
}
273+
} else if (argument.type === "Identifier") {
274+
if (!isNumIdentifier(argument)) {
275+
return throwUnexpectedNodeError(argument, tokens)
276+
}
277+
} else {
278+
return throwUnexpectedNodeError(argument, tokens)
279+
}
280+
if (node.range![0] + 1 !== argument.range![0]) {
281+
return throwUnexpectedTokenError(" ", argument)
282+
}
283+
284+
const nn: JSONUnaryExpression = {
285+
type: "JSONUnaryExpression",
286+
operator,
287+
prefix: true,
288+
argument: convertNode(argument, tokens) as
289+
| JSONNumberLiteral
290+
| JSONNumberIdentifier,
291+
...getFixLocation(node),
292+
}
293+
checkUnexpectKeys(node, tokens, nn)
258294
return nn
259295
}
260296

@@ -278,6 +314,15 @@ function convertIdentifierNode(
278314
return nn
279315
}
280316

317+
/**
318+
* Check if given node is NaN or Infinity
319+
*/
320+
function isNumIdentifier(
321+
node: Identifier,
322+
): node is Identifier & { name: "NaN" | "Infinity" } {
323+
return node.name === "NaN" || node.name === "Infinity"
324+
}
325+
281326
/**
282327
* Check unknown keys
283328
*/

lib/rules/valid-json-number.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import type { JSONLiteral } from "../parser/ast"
1+
import type {
2+
JSONLiteral,
3+
JSONUnaryExpression,
4+
JSONIdentifier,
5+
} from "../parser/ast"
26
import type { RuleListener } from "../types"
37
import { createRule } from "../utils"
8+
import { isExpression } from "../utils/ast"
49

510
export default createRule("valid-json-number", {
611
meta: {
@@ -22,6 +27,23 @@ export default createRule("valid-json-number", {
2227
}
2328
const sourceCode = context.getSourceCode()
2429
return {
30+
JSONUnaryExpression(node: JSONUnaryExpression) {
31+
if (node.operator === "+") {
32+
if (node.argument.type === "JSONIdentifier") {
33+
return
34+
}
35+
context.report({
36+
loc: node.loc,
37+
messageId: "invalid",
38+
fix(fixer) {
39+
return fixer.removeRange([
40+
node.range[0],
41+
node.range[0] + 1,
42+
])
43+
},
44+
})
45+
}
46+
},
2547
JSONLiteral(node: JSONLiteral) {
2648
if (typeof node.value !== "number") {
2749
return
@@ -42,6 +64,15 @@ export default createRule("valid-json-number", {
4264
})
4365
}
4466
},
67+
JSONIdentifier(node: JSONIdentifier) {
68+
if (!isExpression(node)) {
69+
return
70+
}
71+
context.report({
72+
loc: node.loc,
73+
messageId: "invalid",
74+
})
75+
},
4576
}
4677
},
4778
})

lib/utils/ast.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { JSONNode, JSONExpression } from "../parser/ast"
2+
3+
/**
4+
* Checks if given node is JSONExpression
5+
*/
6+
export function isExpression<N extends JSONNode>(
7+
node: N,
8+
): node is N & JSONExpression {
9+
if (node.type === "JSONIdentifier") {
10+
const parent = node.parent!
11+
if (parent.type === "JSONProperty" && parent.key === node) {
12+
return false
13+
}
14+
return true
15+
}
16+
if (
17+
node.type === "JSONObjectExpression" ||
18+
node.type === "JSONArrayExpression" ||
19+
node.type === "JSONUnaryExpression" ||
20+
node.type === "JSONLiteral"
21+
) {
22+
return true
23+
}
24+
return false
25+
}

lib/utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export function defineWrapperListener(
4646
for (const key of Object.keys(listener)) {
4747
const original = listener[key]
4848
const jsonKey = key.replace(
49-
/(?:^|\b)(ExpressionStatement|ArrayExpression|ObjectExpression|Property|Identifier|Literal)(?:\b|$)/gu,
49+
/(?:^|\b)(ExpressionStatement|ArrayExpression|ObjectExpression|Property|Identifier|Literal|UnaryExpression)(?:\b|$)/gu,
5050
"JSON$1",
5151
)
5252
jsonListener[jsonKey] = function (node: JSONNode, ...args) {

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
"lint": "eslint \"tests\" \"lib\" \"docs/.vuepress\" --ext .js,.vue,.ts",
1414
"eslint-fix": "eslint \"tests\" \"lib\" \"docs/.vuepress\" --ext .js,.vue,.ts --fix",
1515
"pretest": "npm run build",
16-
"pretest:base": "npm run setup-types",
1716
"test:base": "mocha --require ts-node/register \"tests/lib/**/*.ts\" --reporter dot --timeout 60000",
18-
"test": "npm run test:nyc",
17+
"test": "npm run test:base",
18+
"pretest:nyc": "npm run build",
1919
"test:nyc": "nyc --reporter=lcov npm run test:base",
2020
"test:debug": "mocha --require ts-node/register --inspect-brk \"tests/lib/**/*.ts\" --reporter dot",
2121
"update": "ts-node ./tools/update.ts && npm run eslint-fix && npm run test:nyc",
@@ -28,7 +28,8 @@
2828
"docs-deploysetup:copy": "npx cpx \"docs/\\.vuepress/dist/**\" . -u",
2929
"preversion": "npm test && npm run update && git add .",
3030
"version": "npm run eslint-fix && git add .",
31-
"setup-types": "node ./tools/setup-eslint-rule-types.js"
31+
"setup-types": "node ./tools/setup-eslint-rule-types.js",
32+
"update-fixtures": "ts-node ./tools/update-fixtures.ts"
3233
},
3334
"repository": {
3435
"type": "git",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[1,2,3,[4,5]]

0 commit comments

Comments
 (0)