• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

spyoungtech / json-five / 5722742239

pending completion
5722742239

push

github

web-flow
Merge pull request #41 from spyoungtech/typing

Add Typing

469 of 469 new or added lines in 7 files covered. (100.0%)

1118 of 1150 relevant lines covered (97.22%)

3.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.43
/json5/parser.py
1
from __future__ import annotations
4✔
2

3
import ast
4✔
4
import sys
4✔
5
import typing
4✔
6
from functools import lru_cache
4✔
7
from typing import Any
4✔
8
from typing import Literal
4✔
9
from typing import Protocol
4✔
10

11
import regex as re
4✔
12
from sly import Parser  # type: ignore
4✔
13
from sly.yacc import SlyLogger  # type: ignore
4✔
14

15
from json5.model import *
4✔
16
from json5.tokenizer import JSON5Token
4✔
17
from json5.tokenizer import JSONLexer
4✔
18
from json5.tokenizer import tokenize
4✔
19
from json5.utils import JSON5DecodeError
4✔
20

21

22
class QuietSlyLogger(SlyLogger):  # type: ignore[misc]
4✔
23
    def warning(self, *args: Any, **kwargs: Any) -> None:
4✔
24
        return
×
25

26
    debug = warning
4✔
27
    info = warning
4✔
28

29

30
ESCAPE_SEQUENCES = {
4✔
31
    'b': '\u0008',
32
    'f': '\u000C',
33
    'n': '\u000A',
34
    'r': '\u000D',
35
    't': '\u0009',
36
    'v': '\u000B',
37
    '0': '\u0000',
38
    '\\': '\u005c',
39
    '"': '\u0022',
40
    "'": '\u0027',
41
}
42

43
# class TrailingComma:
44
#     pass
45

46

47
def replace_escape_literals(matchobj: re.Match[str]) -> str:
4✔
48
    s = matchobj.group(0)
4✔
49
    if s.startswith('\\0') and len(s) == 3:
4✔
50
        raise JSON5DecodeError("'\\0' MUST NOT be followed by a decimal digit", None)
4✔
51
    seq = matchobj.group(1)
4✔
52
    return ESCAPE_SEQUENCES.get(seq, seq)
4✔
53

54

55
@lru_cache(maxsize=1024)
4✔
56
def _latin_escape_replace(s: str) -> str:
4✔
57
    if s.startswith('\\x') and len(s) != 4:
4✔
58
        raise JSON5DecodeError("'\\x' MUST be followed by two hexadecimal digits", None)
4✔
59
    val: str = ast.literal_eval(f'"{s}"')
4✔
60
    if val == '\\':
4✔
61
        val = '\\\\'  # this is important; the subsequent regex will sub it back to \\
4✔
62
    return val
4✔
63

64

65
def latin_unicode_escape_replace(matchobj: re.Match[str]) -> str:
4✔
66
    s = matchobj.group(0)
4✔
67
    return _latin_escape_replace(s)
4✔
68

69

70
def _unicode_escape_replace(s: str) -> str:
4✔
71
    ret: str = ast.literal_eval(f'"{s}"')
4✔
72
    return ret
4✔
73

74

75
def unicode_escape_replace(matchobj: re.Match[str]) -> str:
4✔
76
    s = matchobj.group(0)
4✔
77
    return _unicode_escape_replace(s)
4✔
78

79

80
class T_TextProduction(Protocol):
4✔
81
    wsc0: list[Comment | str]
4✔
82
    wsc1: list[Comment | str]
4✔
83

84
    def __getitem__(self, i: Literal[1]) -> Value:
4✔
85
        ...
×
86

87

88
class T_TokenSlice(Protocol):
4✔
89
    def __getitem__(self, item: int) -> JSON5Token:
4✔
90
        ...
×
91

92

93
class T_FirstKeyValuePairProduction(Protocol):
4✔
94
    wsc0: list[Comment | str]
4✔
95
    wsc1: list[Comment | str]
4✔
96
    wsc2: list[Comment | str]
4✔
97
    key: Key
4✔
98
    value: Value
4✔
99

100
    def __getitem__(self, item: int) -> Key | Value:
4✔
101
        ...
×
102

103

104
class T_WSCProduction(Protocol):
4✔
105
    def __getitem__(self, item: Literal[0]) -> str | Comment:
4✔
106
        ...
×
107

108

109
class T_CommentProduction(Protocol):
4✔
110
    def __getitem__(self, item: Literal[0]) -> str:
4✔
111
        ...
×
112

113
    _slice: T_TokenSlice
4✔
114

115

116
class T_KeyValuePairsProduction(Protocol):
4✔
117
    first_key_value_pair: KeyValuePair
4✔
118
    subsequent_key_value_pair: list[KeyValuePair]
4✔
119

120

121
class T_JsonObjectProduction(Protocol):
4✔
122
    key_value_pairs: tuple[list[KeyValuePair], TrailingComma | None] | None
4✔
123
    _slice: T_TokenSlice
4✔
124
    wsc: list[Comment | str]
4✔
125

126

127
class SubsequentKeyValuePairProduction(Protocol):
4✔
128
    wsc: list[Comment | str]
4✔
129
    first_key_value_pair: KeyValuePair | None
4✔
130
    _slice: T_TokenSlice
4✔
131

132

133
class T_FirstArrayValueProduction(Protocol):
4✔
134
    def __getitem__(self, item: Literal[1]) -> Value:
4✔
135
        ...
×
136

137
    wsc: list[Comment | str]
4✔
138

139

140
class T_SubsequentArrayValueProduction(Protocol):
4✔
141
    first_array_value: Value | None
4✔
142
    wsc: list[Comment | str]
4✔
143
    _slice: T_TokenSlice
4✔
144

145

146
class T_ArrayValuesProduction(Protocol):
4✔
147
    first_array_value: Value
4✔
148
    subsequent_array_value: list[Value]
4✔
149

150

151
class T_JsonArrayProduction(Protocol):
4✔
152
    array_values: tuple[list[Value], TrailingComma | None] | None
4✔
153
    _slice: T_TokenSlice
4✔
154
    wsc: list[Comment | str]
4✔
155

156

157
class T_IdentifierProduction(Protocol):
4✔
158
    def __getitem__(self, item: Literal[0]) -> str:
4✔
159
        ...
×
160

161
    _slice: T_TokenSlice
4✔
162

163

164
class T_KeyProduction(Protocol):
4✔
165
    def __getitem__(self, item: Literal[1]) -> Identifier | DoubleQuotedString | SingleQuotedString:
4✔
166
        ...
×
167

168

169
class T_NumberProduction(Protocol):
4✔
170
    def __getitem__(self, item: Literal[0]) -> str:
4✔
171
        ...
×
172

173
    _slice: T_TokenSlice
4✔
174

175

176
class T_ValueNumberProduction(Protocol):
4✔
177
    number: Infinity | NaN | Float | Integer
4✔
178

179

180
class T_ExponentNotationProduction(Protocol):
4✔
181
    def __getitem__(self, item: int) -> str:
4✔
182
        ...
×
183

184

185
class T_StringTokenProduction(Protocol):
4✔
186
    def __getitem__(self, item: Literal[0]) -> str:
4✔
187
        ...
×
188

189
    _slice: T_TokenSlice
4✔
190

191

192
class T_StringProduction(Protocol):
4✔
193
    def __getitem__(self, item: Literal[0]) -> DoubleQuotedString | SingleQuotedString:
4✔
194
        ...
×
195

196

197
class T_ValueProduction(Protocol):
4✔
198
    def __getitem__(
4✔
199
        self, item: Literal[0]
200
    ) -> (
201
        DoubleQuotedString
202
        | SingleQuotedString
203
        | JSONObject
204
        | JSONArray
205
        | BooleanLiteral
206
        | NullLiteral
207
        | Infinity
208
        | Integer
209
        | Float
210
        | NaN
211
    ):
212
        ...
×
213

214

215
T_CallArg = typing.TypeVar('T_CallArg')
4✔
216
_: typing.Callable[..., typing.Callable[[T_CallArg], T_CallArg]]
4✔
217

218

219
class JSONParser(Parser):  # type: ignore[misc]
4✔
220
    # debugfile = 'parser.out'
221
    tokens = JSONLexer.tokens
4✔
222
    log = QuietSlyLogger(sys.stderr)
4✔
223

224
    def __init__(self, *args: Any, **kwargs: Any):
4✔
225
        super().__init__(*args, **kwargs)
4✔
226
        self.errors: list[JSON5DecodeError] = []
4✔
227
        self.last_token: JSON5Token | None = None
4✔
228
        self.seen_tokens: list[JSON5Token] = []
4✔
229
        self.expecting: list[list[str]] = []
4✔
230

231
    @_('{ wsc } value { wsc }')
4✔
232
    def text(self, p: T_TextProduction) -> JSONText:
4✔
233
        node = JSONText(value=p[1])
4✔
234
        for wsc in p.wsc0:
4✔
235
            node.wsc_before.append(wsc)
4✔
236
        for wsc in p.wsc1:
4✔
237
            node.wsc_after.append(wsc)
4✔
238
        return node
4✔
239

240
    @_('key { wsc } seen_colon COLON { wsc } object_value_seen value { wsc }')
4✔
241
    def first_key_value_pair(self, p: T_FirstKeyValuePairProduction) -> KeyValuePair:
4✔
242
        key = p[0]
4✔
243
        for wsc in p.wsc0:
4✔
244
            key.wsc_after.append(wsc)
4✔
245
        value = p[6]
4✔
246
        for wsc in p.wsc1:
4✔
247
            value.wsc_before.append(wsc)
4✔
248
        for wsc in p.wsc2:
4✔
249
            value.wsc_after.append(wsc)
4✔
250
        return KeyValuePair(key=p.key, value=p.value, tok=getattr(key, 'tok'))
4✔
251

252
    @_('object_delimiter_seen COMMA { wsc } [ first_key_value_pair ]')
4✔
253
    def subsequent_key_value_pair(self, p: SubsequentKeyValuePairProduction) -> KeyValuePair | TrailingComma:
4✔
254
        node: KeyValuePair | TrailingComma
255
        if p.first_key_value_pair:
4✔
256
            node = p.first_key_value_pair
4✔
257
            for wsc in p.wsc:
4✔
258
                node.key.wsc_before.append(wsc)
4✔
259
        else:
260
            node = TrailingComma(tok=p._slice[1])
4✔
261
            for wsc in p.wsc:
4✔
262
                node.wsc_after.append(wsc)
4✔
263
        return node
4✔
264

265
    @_('WHITESPACE', 'comment')
4✔
266
    def wsc(self, p: T_WSCProduction) -> str | Comment:
4✔
267
        return p[0]
4✔
268

269
    @_('BLOCK_COMMENT')
4✔
270
    def comment(self, p: T_CommentProduction) -> BlockComment:
4✔
271
        return BlockComment(p[0], tok=p._slice[0])
4✔
272

273
    @_('LINE_COMMENT')  # type: ignore[no-redef]
4✔
274
    def comment(self, p: T_CommentProduction):
4✔
275
        return LineComment(p[0], tok=p._slice[0])
4✔
276

277
    @_('first_key_value_pair { subsequent_key_value_pair }')
4✔
278
    def key_value_pairs(self, p: T_KeyValuePairsProduction) -> tuple[list[KeyValuePair], TrailingComma | None]:
4✔
279
        ret = [
4✔
280
            p.first_key_value_pair,
281
        ]
282
        num_sqvp = len(p.subsequent_key_value_pair)
4✔
283
        for index, value in enumerate(p.subsequent_key_value_pair):
4✔
284
            if isinstance(value, TrailingComma):
4✔
285
                if index + 1 != num_sqvp:
4✔
286
                    offending_token = value.tok
4✔
287
                    self.errors.append(JSON5DecodeError("Syntax Error: multiple trailing commas", offending_token))
4✔
288
                return ret, value
4✔
289
            else:
290
                ret.append(value)
4✔
291
        return ret, None
4✔
292

293
    @_('')
4✔
294
    def seen_LBRACE(self, p: Any) -> None:
4✔
295
        self.expecting.append(['RBRACE', 'key'])
4✔
296

297
    @_('')
4✔
298
    def seen_key(self, p: Any) -> None:
4✔
299
        self.expecting.pop()
4✔
300
        self.expecting.append(['COLON'])
4✔
301

302
    @_('')
4✔
303
    def seen_colon(self, p: Any) -> None:
4✔
304
        self.expecting.pop()
4✔
305
        self.expecting.append(['value'])
4✔
306

307
    @_('')
4✔
308
    def object_value_seen(self, p: Any) -> None:
4✔
309
        self.expecting.pop()
4✔
310
        self.expecting.append(['COMMA', 'RBRACE'])
4✔
311

312
    @_('')
4✔
313
    def object_delimiter_seen(self, p: Any) -> None:
4✔
314
        self.expecting.pop()
4✔
315
        self.expecting.append(['RBRACE', 'key'])
4✔
316

317
    @_('')
4✔
318
    def seen_RBRACE(self, p: Any) -> None:
4✔
319
        self.expecting.pop()
4✔
320

321
    @_('seen_LBRACE LBRACE { wsc } [ key_value_pairs ] seen_RBRACE RBRACE')
4✔
322
    def json_object(self, p: T_JsonObjectProduction) -> JSONObject:
4✔
323
        if not p.key_value_pairs:
4✔
324
            node = JSONObject(leading_wsc=list(p.wsc or []), tok=p._slice[0])
4✔
325
        else:
326
            kvps, trailing_comma = p.key_value_pairs
4✔
327
            node = JSONObject(*kvps, trailing_comma=trailing_comma, leading_wsc=list(p.wsc or []), tok=p._slice[0])
4✔
328

329
        return node
4✔
330

331
    @_('array_value_seen value { wsc }')
4✔
332
    def first_array_value(self, p: T_FirstArrayValueProduction) -> Value:
4✔
333
        node = p[1]
4✔
334
        for wsc in p.wsc:
4✔
335
            node.wsc_after.append(wsc)
4✔
336
        return node
4✔
337

338
    @_('array_delimiter_seen COMMA { wsc } [ first_array_value ]')
4✔
339
    def subsequent_array_value(self, p: T_SubsequentArrayValueProduction) -> Value | TrailingComma:
4✔
340
        node: Value | TrailingComma
341
        if p.first_array_value:
4✔
342
            node = p.first_array_value
4✔
343
            for wsc in p.wsc:
4✔
344
                node.wsc_before.append(wsc)
4✔
345
        else:
346
            node = TrailingComma(tok=p._slice[1])
4✔
347
            for wsc in p.wsc:
4✔
348
                node.wsc_after.append(wsc)
4✔
349
        return node
4✔
350

351
    @_('first_array_value { subsequent_array_value }')
4✔
352
    def array_values(self, p: T_ArrayValuesProduction) -> tuple[list[Value], TrailingComma | None]:
4✔
353
        ret = [
4✔
354
            p.first_array_value,
355
        ]
356
        num_values = len(p.subsequent_array_value)
4✔
357
        for index, value in enumerate(p.subsequent_array_value):
4✔
358
            if isinstance(value, TrailingComma):
4✔
359
                if index + 1 != num_values:
4✔
360
                    self.errors.append(JSON5DecodeError("Syntax Error: multiple trailing commas", value.tok))
4✔
361
                    return ret, value
4✔
362
                return ret, value
4✔
363
            else:
364
                ret.append(value)
4✔
365
        return ret, None
4✔
366

367
    @_('seen_LBRACKET LBRACKET { wsc } [ array_values ] seen_RBRACKET RBRACKET')
4✔
368
    def json_array(self, p: T_JsonArrayProduction) -> JSONArray:
4✔
369
        if not p.array_values:
4✔
370
            node = JSONArray(tok=p._slice[1])
4✔
371
        else:
372
            values, trailing_comma = p.array_values
4✔
373
            node = JSONArray(*values, trailing_comma=trailing_comma, tok=p._slice[1])
4✔
374

375
        for wsc in p.wsc:
4✔
376
            node.leading_wsc.append(wsc)
4✔
377

378
        return node
4✔
379

380
    @_('')
4✔
381
    def seen_LBRACKET(self, p: Any) -> None:
4✔
382
        self.expecting.append(['RBRACKET', 'value'])
4✔
383

384
    @_('')
4✔
385
    def seen_RBRACKET(self, p: Any) -> None:
4✔
386
        self.expecting.pop()
4✔
387

388
    @_('')
4✔
389
    def array_delimiter_seen(self, p: Any) -> None:
4✔
390
        assert len(self.expecting[-1]) == 2
4✔
391
        self.expecting[-1].pop()
4✔
392
        self.expecting[-1].append('value')
4✔
393

394
    @_('')
4✔
395
    def array_value_seen(self, p: Any) -> None:
4✔
396
        assert len(self.expecting[-1]) == 2
4✔
397
        assert self.expecting[-1][-1] == 'value'
4✔
398
        self.expecting[-1].pop()
4✔
399
        self.expecting[-1].append('COMMA')
4✔
400

401
    @_('NAME')
4✔
402
    def identifier(self, p: T_IdentifierProduction) -> Identifier:
4✔
403
        raw_value = p[0]
4✔
404
        name = re.sub(r'\\u[0-9a-fA-F]{4}', unicode_escape_replace, raw_value)
4✔
405
        pattern = r'[\w_\$]([\w_\d\$\p{Pc}\p{Mn}\p{Mc}\u200C\u200D])*'
4✔
406
        if not re.fullmatch(pattern, name):
4✔
407
            self.errors.append(JSON5DecodeError("Invalid identifier name", p._slice[0]))
4✔
408
        return Identifier(name=name, raw_value=raw_value, tok=p._slice[0])
4✔
409

410
    @_('seen_key identifier', 'seen_key string')
4✔
411
    def key(self, p: T_KeyProduction) -> Identifier | DoubleQuotedString | SingleQuotedString:
4✔
412
        node = p[1]
4✔
413
        return node
4✔
414

415
    @_('INTEGER')
4✔
416
    def number(self, p: T_NumberProduction):
4✔
417
        return Integer(p[0], tok=p._slice[0])
4✔
418

419
    @_('FLOAT')  # type: ignore[no-redef]
4✔
420
    def number(self, p: T_NumberProduction):
4✔
421
        return Float(p[0], tok=p._slice[0])
4✔
422

423
    @_('OCTAL')  # type: ignore[no-redef]
4✔
424
    def number(self, p: T_NumberProduction):
4✔
425
        self.errors.append(JSON5DecodeError("Invalid integer literal. Octals are not allowed", p._slice[0]))
4✔
426
        raw_value = p[0]
4✔
427
        if re.search(r'[89]+', raw_value):
4✔
428
            self.errors.append(JSON5DecodeError("Invalid octal format. Octal digits must be in range 0-7", p._slice[0]))
4✔
429
            return Integer(raw_value=oct(0), is_octal=True, tok=p._slice[0])
4✔
430
        return Integer(raw_value, is_octal=True, tok=p._slice[0])
4✔
431

432
    @_('INFINITY')  # type: ignore[no-redef]
4✔
433
    def number(self, p: Any) -> Infinity:
4✔
434
        return Infinity()
4✔
435

436
    @_('NAN')  # type: ignore[no-redef]
4✔
437
    def number(self, p: Any) -> NaN:
4✔
438
        return NaN()
4✔
439

440
    @_('MINUS number')
4✔
441
    def value(self, p: T_ValueNumberProduction) -> UnaryOp:
4✔
442
        if isinstance(p.number, Infinity):
4✔
443
            p.number.negative = True
4✔
444
        node = UnaryOp(op='-', value=p.number)
4✔
445
        return node
4✔
446

447
    @_('PLUS number')  # type: ignore[no-redef]
4✔
448
    def value(self, p: T_ValueNumberProduction):
4✔
449
        node = UnaryOp(op='+', value=p.number)
4✔
450
        return node
4✔
451

452
    @_('INTEGER EXPONENT', 'FLOAT EXPONENT')  # type: ignore[no-redef]
4✔
453
    def number(self, p: T_ExponentNotationProduction) -> Float:
4✔
454
        exp_notation = p[1][0]  # e or E
4✔
455
        return Float(p[0] + p[1], exp_notation=exp_notation)
4✔
456

457
    @_('HEXADECIMAL')  # type: ignore[no-redef]
4✔
458
    def number(self, p: T_NumberProduction) -> Integer:
4✔
459
        return Integer(p[0], is_hex=True)
4✔
460

461
    @_('DOUBLE_QUOTE_STRING')
4✔
462
    def double_quoted_string(self, p: T_StringTokenProduction) -> DoubleQuotedString:
4✔
463
        raw_value = p[0]
4✔
464
        contents = raw_value[1:-1]
4✔
465
        terminator_in_string = re.search(r'(?<!\\)([\u000D\u2028\u2029]|(?<!\r)\n)', contents)
4✔
466
        if terminator_in_string:
4✔
467
            end = terminator_in_string.span()[0]
4✔
468
            before_terminator = terminator_in_string.string[:end]
4✔
469
            tok = p._slice[0]
4✔
470
            pos = tok.index + len(before_terminator)
4✔
471
            doc = tok.doc
4✔
472
            lineno = doc.count('\n', 0, pos) + 1
4✔
473
            colno = pos - doc.rfind('\n', 0, pos) + 1
4✔
474
            index = pos + 1
4✔
475
            errmsg = f"Illegal line terminator (line {lineno} column {colno} (char {index}) without continuation"
4✔
476
            self.errors.append(JSON5DecodeError(errmsg, tok))
4✔
477
        contents = re.sub(r'\\(\r\n|[\u000A\u000D\u2028\u2029])', '', contents)
4✔
478
        try:
4✔
479
            contents = re.sub(r'(\\x[a-fA-F0-9]{0,2}|\\u[0-9a-fA-F]{4})', latin_unicode_escape_replace, contents)
4✔
480
        except JSON5DecodeError as exc:
4✔
481
            self.errors.append(JSON5DecodeError(exc.args[0], p._slice[0]))
4✔
482
        try:
4✔
483
            contents = re.sub(r'\\(0\d|.)', replace_escape_literals, contents)
4✔
484
        except JSON5DecodeError as exc:
4✔
485
            self.errors.append(JSON5DecodeError(exc.args[0], p._slice[0]))
4✔
486
        return DoubleQuotedString(contents, raw_value=raw_value, tok=p._slice[0])
4✔
487

488
    @_("SINGLE_QUOTE_STRING")
4✔
489
    def single_quoted_string(self, p: T_StringTokenProduction) -> SingleQuotedString:
4✔
490
        raw_value = p[0]
4✔
491
        contents = raw_value[1:-1]
4✔
492
        terminator_in_string = re.search(r'(?<!\\)([\u000D\u2028\u2029]|(?<!\r)\n)', contents)
4✔
493
        if terminator_in_string:
4✔
494
            end = terminator_in_string.span()[0]
4✔
495
            before_terminator = terminator_in_string.string[:end]
4✔
496
            tok = p._slice[0]
4✔
497
            pos = tok.index + len(before_terminator)
4✔
498
            doc = tok.doc
4✔
499
            lineno = doc.count('\n', 0, pos) + 1
4✔
500
            colno = pos - doc.rfind('\n', 0, pos) + 1
4✔
501
            index = pos + 1
4✔
502
            errmsg = f"Illegal line terminator (line {lineno} column {colno} (char {index}) without continuation"
4✔
503
            self.errors.append(JSON5DecodeError(errmsg, tok))
4✔
504
        contents = re.sub(r'\\(\r\n|[\u000A\u000D\u2028\u2029])', '', contents)
4✔
505
        try:
4✔
506
            contents = re.sub(r'(\\x[a-fA-F0-9]{0,2}|\\u[0-9a-fA-F]{4})', latin_unicode_escape_replace, contents)
4✔
507
        except JSON5DecodeError as exc:
4✔
508
            self.errors.append(JSON5DecodeError(exc.args[0], p._slice[0]))
4✔
509
        try:
4✔
510
            contents = re.sub(r'\\(0\d|.)', replace_escape_literals, contents)
4✔
511
        except JSON5DecodeError as exc:
4✔
512
            self.errors.append(JSON5DecodeError(exc.args[0], p._slice[0]))
4✔
513
        return SingleQuotedString(contents, raw_value=raw_value, tok=p._slice[0])
4✔
514

515
    @_('double_quoted_string', 'single_quoted_string')
4✔
516
    def string(self, p: T_StringProduction) -> SingleQuotedString | DoubleQuotedString:
4✔
517
        return p[0]
4✔
518

519
    @_('TRUE')
4✔
520
    def boolean(self, p: Any) -> BooleanLiteral:
4✔
521
        return BooleanLiteral(True)
4✔
522

523
    @_('FALSE')  # type: ignore[no-redef]
4✔
524
    def boolean(self, p: Any) -> BooleanLiteral:
4✔
525
        return BooleanLiteral(False)
4✔
526

527
    @_('NULL')
4✔
528
    def null(self, p: Any) -> NullLiteral:
4✔
529
        return NullLiteral()
4✔
530

531
    @_(  # type: ignore[no-redef]
4✔
532
        'string',
533
        'json_object',
534
        'json_array',
535
        'boolean',
536
        'null',
537
        'number',
538
    )
539
    def value(
4✔
540
        self, p: T_ValueProduction
541
    ) -> (
542
        DoubleQuotedString
543
        | SingleQuotedString
544
        | JSONObject
545
        | JSONArray
546
        | BooleanLiteral
547
        | NullLiteral
548
        | Infinity
549
        | Integer
550
        | Float
551
        | NaN
552
    ):
553
        node = p[0]
4✔
554
        return node
4✔
555

556
    @_('UNTERMINATED_SINGLE_QUOTE_STRING', 'UNTERMINATED_DOUBLE_QUOTE_STRING')  # type: ignore[no-redef]
4✔
557
    def string(self, p: T_StringTokenProduction) -> SingleQuotedString | DoubleQuotedString:
4✔
558
        self.error(p._slice[0])
4✔
559
        raw = p[0]
4✔
560
        if raw.startswith('"'):
4✔
561
            return DoubleQuotedString(raw[1:], raw_value=raw)
4✔
562
        return SingleQuotedString(raw[1:], raw_value=raw)
4✔
563

564
    def error(self, token: JSON5Token | None) -> JSON5Token | None:
4✔
565
        if token:
4✔
566
            if self.expecting:
4✔
567
                expected = self.expecting[-1]
4✔
568

569
                message = f"Syntax Error. Was expecting {' or '.join(expected)}"
4✔
570
            else:
571
                message = 'Syntax Error'
×
572

573
            self.errors.append(JSON5DecodeError(message, token))
4✔
574
            try:
4✔
575
                return next(self.tokens)  # type: ignore
4✔
576
            except StopIteration:
4✔
577
                # EOF
578
                class tok:
4✔
579
                    type = '$end'
4✔
580
                    value = None
4✔
581
                    lineno = None
4✔
582
                    index = None
4✔
583
                    end = None
4✔
584

585
                return JSON5Token(tok(), None)  # type: ignore[arg-type]
4✔
586
        elif self.last_token:
4✔
587
            doc = self.last_token.doc
4✔
588
            pos = len(doc)
4✔
589
            lineno = doc.count('\n', 0, pos) + 1
4✔
590
            colno = pos - doc.rfind('\n', 0, pos)
4✔
591
            message = f'Expecting value. Unexpected EOF at: ' f'line {lineno} column {colno} (char {pos})'
4✔
592
            if self.expecting:
4✔
593
                expected = self.expecting[-1]
4✔
594
                message += f'. Was expecting {f" or ".join(expected)}'
4✔
595
            self.errors.append(JSON5DecodeError(message, None))
4✔
596
        else:
597
            #  Empty file
598
            self.errors.append(JSON5DecodeError('Expecting value. Received unexpected EOF', None))
4✔
599
        return None
4✔
600

601
    def _token_gen(self, tokens: typing.Iterable[JSON5Token]) -> typing.Generator[JSON5Token, None, None]:
4✔
602
        for tok in tokens:
4✔
603
            self.last_token = tok
4✔
604
            self.seen_tokens.append(tok)
4✔
605
            yield tok
4✔
606

607
    def parse(self, tokens: typing.Iterable[JSON5Token]) -> JSONText:
4✔
608
        tokens = self._token_gen(tokens)
4✔
609
        model: JSONText = super().parse(tokens)
4✔
610
        if self.errors:
4✔
611
            if len(self.errors) > 1:
4✔
612
                primary_error = self.errors[0]
4✔
613
                msg = (
4✔
614
                    "There were multiple errors parsing the JSON5 document.\n"
615
                    "The primary error was: \n\t{}\n"
616
                    "Additionally, the following errors were also detected:\n\t{}"
617
                )
618

619
                num_additional_errors = len(self.errors) - 1
4✔
620
                additional_errors = '\n\t'.join(err.args[0] for err in self.errors[1:6])
4✔
621
                if num_additional_errors > 5:
4✔
622
                    additional_errors += f'\n\t{num_additional_errors - 5} additional error(s) truncated'
×
623
                msg = msg.format(primary_error.args[0], additional_errors)
4✔
624
                err = JSON5DecodeError(msg, None)
4✔
625
                err.lineno = primary_error.lineno
4✔
626
                err.token = primary_error.token
4✔
627
                err.index = primary_error.index
4✔
628
                raise err
4✔
629
            else:
630
                raise self.errors[0]
4✔
631
        return model
4✔
632

633

634
def parse_tokens(raw_tokens: typing.Iterable[JSON5Token]) -> JSONText:
4✔
635
    parser = JSONParser()
4✔
636
    return parser.parse(raw_tokens)
4✔
637

638

639
def parse_source(text: str) -> JSONText:
4✔
640
    tokens = tokenize(text)
4✔
641
    model = parse_tokens(tokens)
4✔
642
    return model
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc