Skip to content

Commit 0156f8e

Browse files
authored
Fix incorrect JSON being silently parsed. (#113)
1 parent 02d20e7 commit 0156f8e

File tree

5 files changed

+231
-8
lines changed

5 files changed

+231
-8
lines changed

src/parser/extend-parser.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import type { Parser, Options } from "acorn"
44
import type { Comment } from "../types"
55
import type { Node } from "estree"
66
import { getAcorn } from "./modules/acorn"
7-
import { ParseError, throwUnexpectedCommentError } from "./errors"
7+
import {
8+
ParseError,
9+
throwUnexpectedCommentError,
10+
throwUnexpectedTokenError,
11+
} from "./errors"
812
import { TokenConvertor } from "./convert"
913
import type { JSONSyntaxContext } from "./syntax-context"
1014

@@ -36,10 +40,20 @@ export function getParser(): typeof Parser {
3640
nodes: Node[]
3741
},
3842
code: string,
43+
pos: number,
3944
) {
4045
super(
4146
((): Options => {
4247
const tokenConvertor = new TokenConvertor(options.ctx, code)
48+
49+
const onToken: Options["onToken"] =
50+
options.onToken ||
51+
((token) => {
52+
const t = tokenConvertor.convertToken(token)
53+
if (t) {
54+
this[PRIVATE].tokenStore.add(t)
55+
}
56+
})
4357
return {
4458
// do not use spread, because we don't want to pass any unknown options to acorn
4559
ecmaVersion: options.ecmaVersion,
@@ -49,12 +63,7 @@ export function getParser(): typeof Parser {
4963
allowReserved: true,
5064

5165
// Collect tokens
52-
onToken: (token) => {
53-
const t = tokenConvertor.convertToken(token)
54-
if (t) {
55-
this[PRIVATE].tokenStore.add(t)
56-
}
57-
},
66+
onToken,
5867

5968
// Collect comments
6069
onComment: (
@@ -82,6 +91,7 @@ export function getParser(): typeof Parser {
8291
}
8392
})(),
8493
code,
94+
pos,
8595
)
8696
this[PRIVATE] = {
8797
code,
@@ -171,3 +181,26 @@ export function getParser(): typeof Parser {
171181

172182
return parserCache
173183
}
184+
185+
/** Get extend parser */
186+
export function getAnyTokenErrorParser(): typeof Parser {
187+
const parser = class ExtendParser extends getParser() {
188+
public constructor(options: Options, code: string, pos: number) {
189+
super(
190+
{
191+
...options,
192+
onToken: (token) => {
193+
return throwUnexpectedTokenError(
194+
code.slice(...token.range!),
195+
token,
196+
)
197+
},
198+
},
199+
code,
200+
pos,
201+
)
202+
}
203+
}
204+
205+
return parser
206+
}

src/parser/parser.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { convertProgramNode } from "./convert"
77
import { TokenStore } from "./token-store"
88
import type { JSONProgram } from "./ast"
99
import { lte } from "semver"
10-
import { getParser } from "./extend-parser"
10+
import { getAnyTokenErrorParser, getParser } from "./extend-parser"
1111
import type { JSONSyntaxContext } from "./syntax-context"
1212

1313
const DEFAULT_ECMA_VERSION = "latest"
@@ -58,6 +58,27 @@ export function parseForESLint(
5858
;(node as any).type = `JSON${node.type}`
5959
}
6060
const ast = convertProgramNode(baseAst as never, tokenStore, ctx, code)
61+
let lastIndex = Math.max(
62+
baseAst.range![1],
63+
comments[comments.length - 1]?.range![1] ?? 0,
64+
)
65+
let lastChar = code[lastIndex]
66+
while (
67+
lastChar === "\n" ||
68+
lastChar === "\r" ||
69+
lastChar === " " ||
70+
lastChar === "\t"
71+
) {
72+
lastIndex++
73+
lastChar = code[lastIndex]
74+
}
75+
if (lastIndex < code.length) {
76+
getAnyTokenErrorParser().parseExpressionAt(
77+
code,
78+
lastIndex,
79+
parserOptions,
80+
)
81+
}
6182
ast.tokens = tokens
6283
ast.comments = comments
6384
return {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
/* comment */
3+
}
4+
// comment
5+
/* comment */
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
{
2+
"type": "Program",
3+
"body": [
4+
{
5+
"type": "JSONExpressionStatement",
6+
"expression": {
7+
"type": "JSONObjectExpression",
8+
"properties": [],
9+
"range": [
10+
0,
11+
19
12+
],
13+
"loc": {
14+
"start": {
15+
"line": 1,
16+
"column": 0
17+
},
18+
"end": {
19+
"line": 3,
20+
"column": 1
21+
}
22+
}
23+
},
24+
"range": [
25+
0,
26+
19
27+
],
28+
"loc": {
29+
"start": {
30+
"line": 1,
31+
"column": 0
32+
},
33+
"end": {
34+
"line": 3,
35+
"column": 1
36+
}
37+
}
38+
}
39+
],
40+
"comments": [
41+
{
42+
"type": "Block",
43+
"value": " comment ",
44+
"range": [
45+
4,
46+
17
47+
],
48+
"loc": {
49+
"start": {
50+
"line": 2,
51+
"column": 2
52+
},
53+
"end": {
54+
"line": 2,
55+
"column": 15
56+
}
57+
}
58+
},
59+
{
60+
"type": "Line",
61+
"value": " comment",
62+
"range": [
63+
20,
64+
30
65+
],
66+
"loc": {
67+
"start": {
68+
"line": 4,
69+
"column": 0
70+
},
71+
"end": {
72+
"line": 4,
73+
"column": 10
74+
}
75+
}
76+
},
77+
{
78+
"type": "Block",
79+
"value": " comment ",
80+
"range": [
81+
33,
82+
46
83+
],
84+
"loc": {
85+
"start": {
86+
"line": 5,
87+
"column": 2
88+
},
89+
"end": {
90+
"line": 5,
91+
"column": 15
92+
}
93+
}
94+
}
95+
],
96+
"tokens": [
97+
{
98+
"type": "Punctuator",
99+
"value": "{",
100+
"range": [
101+
0,
102+
1
103+
],
104+
"loc": {
105+
"start": {
106+
"line": 1,
107+
"column": 0
108+
},
109+
"end": {
110+
"line": 1,
111+
"column": 1
112+
}
113+
}
114+
},
115+
{
116+
"type": "Punctuator",
117+
"value": "}",
118+
"range": [
119+
18,
120+
19
121+
],
122+
"loc": {
123+
"start": {
124+
"line": 3,
125+
"column": 0
126+
},
127+
"end": {
128+
"line": 3,
129+
"column": 1
130+
}
131+
}
132+
}
133+
],
134+
"range": [
135+
0,
136+
47
137+
],
138+
"loc": {
139+
"start": {
140+
"line": 1,
141+
"column": 0
142+
},
143+
"end": {
144+
"line": 6,
145+
"column": 0
146+
}
147+
}
148+
}

tests/src/parser/errors.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ function getParseError(code: string): ParseError {
2424

2525
describe("Check that parsing error is correct.", () => {
2626
for (const { code, message, lineNumber, column, index, char } of [
27+
{
28+
code: `"foo": 42`,
29+
message: "Unexpected token ':'.",
30+
lineNumber: 1,
31+
column: 6,
32+
index: 5,
33+
char: ":",
34+
},
35+
{
36+
code: `"foo":`,
37+
message: "Unexpected token ':'.",
38+
lineNumber: 1,
39+
column: 6,
40+
index: 5,
41+
char: ":",
42+
},
2743
{
2844
code: `
2945
{

0 commit comments

Comments
 (0)