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

ota-meshi / jsonc-eslint-parser / 17888386344

21 Sep 2025 03:28AM UTC coverage: 88.63%. Remained the same
17888386344

push

github

web-flow
chore: release jsonc-eslint-parser (#242)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

308 of 366 branches covered (84.15%)

Branch coverage included in aggregate %.

2 of 2 new or added lines in 1 file covered. (100.0%)

604 of 663 relevant lines covered (91.1%)

63.98 hits per line

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

91.27
/src/parser/validate.ts
1
import type {
2
  Node,
3
  ObjectExpression,
4
  Property,
5
  ArrayExpression,
6
  Literal,
7
  Identifier,
8
  UnaryExpression,
9
  TemplateLiteral,
10
  TemplateElement,
11
  BinaryExpression,
12
  Expression,
13
} from "estree";
14
import {
1✔
15
  throwUnexpectedNodeError,
16
  throwExpectedTokenError,
17
  throwUnexpectedTokenError,
18
  throwInvalidNumberError,
19
  throwUnexpectedSpaceError,
20
  throwUnexpectedError,
21
} from "./errors";
22
import type { TokenStore, MaybeNodeOrToken } from "./token-store";
23
import { isComma } from "./token-store";
1✔
24
import { isRegExpLiteral } from "./utils";
1✔
25
import type { JSONIdentifier } from "./ast";
26
import type { JSONSyntaxContext } from "./syntax-context";
27

28
const lineBreakPattern = /\r\n|[\n\r\u2028\u2029]/u;
1✔
29
const octalNumericLiteralPattern = /^0o/iu;
1✔
30
const legacyOctalNumericLiteralPattern = /^0\d/u;
1✔
31
const binaryNumericLiteralPattern = /^0b/iu;
1✔
32

33
const unicodeCodepointEscapePattern = /\\u\{[\dA-Fa-f]+\}/uy;
1✔
34

35
/**
36
 * Check if given string has unicode codepoint escape
37
 */
38
function hasUnicodeCodepointEscapes(code: string) {
39
  let escaped = false;
39✔
40
  for (let index = 0; index < code.length - 4; index++) {
39✔
41
    if (escaped) {
58✔
42
      escaped = false;
4✔
43
      continue;
4✔
44
    }
45
    const char = code[index];
54✔
46
    if (char === "\\") {
54✔
47
      unicodeCodepointEscapePattern.lastIndex = index;
12✔
48
      if (unicodeCodepointEscapePattern.test(code)) {
12✔
49
        return true;
8✔
50
      }
51
      escaped = true;
4✔
52
    }
53
  }
54
  return false;
31✔
55
}
56

57
/**
58
 * Validate ES node
59
 */
60
export function validateNode(
1✔
61
  node: Node,
62
  tokens: TokenStore,
63
  ctx: JSONSyntaxContext,
64
): void {
65
  if (node.type === "ObjectExpression") {
561✔
66
    validateObjectExpressionNode(node, tokens, ctx);
48✔
67
    return;
47✔
68
  }
69
  if (node.type === "Property") {
513✔
70
    validatePropertyNode(node, tokens, ctx);
60✔
71
    return;
54✔
72
  }
73
  if (node.type === "ArrayExpression") {
453✔
74
    validateArrayExpressionNode(node, tokens, ctx);
44✔
75
    return;
39✔
76
  }
77
  if (node.type === "Literal") {
409✔
78
    validateLiteralNode(node, tokens, ctx);
224✔
79
    return;
200✔
80
  }
81
  if (node.type === "UnaryExpression") {
185✔
82
    validateUnaryExpressionNode(node, tokens, ctx);
44✔
83
    return;
36✔
84
  }
85
  if (node.type === "Identifier") {
141✔
86
    validateIdentifierNode(node, tokens, ctx);
94✔
87
    return;
90✔
88
  }
89
  if (node.type === "TemplateLiteral") {
47✔
90
    validateTemplateLiteralNode(node, tokens, ctx);
10✔
91
    return;
9✔
92
  }
93
  if (node.type === "TemplateElement") {
37✔
94
    validateTemplateElementNode(node, tokens);
12✔
95
    return;
12✔
96
  }
97
  if (node.type === "BinaryExpression") {
25✔
98
    validateBinaryExpressionNode(node, tokens, ctx);
20✔
99
    return;
14✔
100
  }
101

102
  throw throwUnexpectedNodeError(node, tokens);
5✔
103
}
104

105
/**
106
 * Validate ObjectExpression node
107
 */
108
function validateObjectExpressionNode(
109
  node: ObjectExpression,
110
  tokens: TokenStore,
111
  ctx: JSONSyntaxContext,
112
): void {
113
  /* istanbul ignore next */
114
  if (node.type !== "ObjectExpression") {
115
    throw throwUnexpectedNodeError(node, tokens);
116
  }
117

118
  for (const prop of node.properties) {
48✔
119
    setParent(prop, node);
54✔
120
  }
121

122
  if (!ctx.trailingCommas) {
48✔
123
    const token = tokens.getTokenBefore(tokens.getLastToken(node));
3✔
124
    if (token && isComma(token)) {
3✔
125
      throw throwUnexpectedTokenError(",", token);
1✔
126
    }
127
  }
128
}
129

130
/**
131
 * Validate Property node
132
 */
133
function validatePropertyNode(
134
  node: Property,
135
  tokens: TokenStore,
136
  ctx: JSONSyntaxContext,
137
): void {
138
  if (node.type !== "Property") {
60!
139
    throw throwUnexpectedNodeError(node, tokens);
×
140
  }
141

142
  setParent(node.key, node);
60✔
143
  setParent(node.value, node);
60✔
144

145
  if (node.computed) {
60✔
146
    throw throwUnexpectedNodeError(node, tokens);
1✔
147
  }
148
  if (node.method) {
59!
149
    throw throwUnexpectedNodeError(node.value, tokens);
×
150
  }
151
  if (node.shorthand) {
59✔
152
    throw throwExpectedTokenError(":", node);
2✔
153
  }
154
  if (node.kind !== "init") {
57!
155
    throw throwExpectedTokenError(":", tokens.getFirstToken(node));
×
156
  }
157

158
  if (node.key.type === "Literal") {
57✔
159
    const keyValueType = typeof node.key.value;
23✔
160
    if (keyValueType === "number") {
23✔
161
      if (!ctx.numberProperties) {
5✔
162
        throw throwUnexpectedNodeError(node.key, tokens);
1✔
163
      }
164
    } else if (keyValueType !== "string") {
18!
165
      throw throwUnexpectedNodeError(node.key, tokens);
×
166
    }
167
  } else if (node.key.type === "Identifier") {
34!
168
    if (!ctx.unquoteProperties) {
34✔
169
      throw throwUnexpectedNodeError(node.key, tokens);
1✔
170
    }
171
  } else {
172
    throw throwUnexpectedNodeError(node.key, tokens);
×
173
  }
174
  if (node.value.type === "Identifier") {
55✔
175
    if (!isStaticValueIdentifier(node.value, ctx)) {
7✔
176
      throw throwUnexpectedNodeError(node.value, tokens);
1✔
177
    }
178
  }
179
}
180

181
/**
182
 * Validate ArrayExpression node
183
 */
184
function validateArrayExpressionNode(
185
  node: ArrayExpression,
186
  tokens: TokenStore,
187
  ctx: JSONSyntaxContext,
188
): void {
189
  /* istanbul ignore next */
190
  if (node.type !== "ArrayExpression") {
191
    throw throwUnexpectedNodeError(node, tokens);
192
  }
193

194
  if (!ctx.trailingCommas) {
44✔
195
    const token = tokens.getTokenBefore(tokens.getLastToken(node));
1✔
196
    if (token && isComma(token)) {
1✔
197
      throw throwUnexpectedTokenError(",", token);
1✔
198
    }
199
  }
200
  node.elements.forEach((child, index) => {
43✔
201
    if (!child) {
122✔
202
      if (ctx.sparseArrays) {
13✔
203
        return;
10✔
204
      }
205
      const beforeIndex = index - 1;
3✔
206
      const before =
207
        beforeIndex >= 0
3✔
208
          ? tokens.getLastToken(node.elements[beforeIndex]!)
209
          : tokens.getFirstToken(node);
210
      throw throwUnexpectedTokenError(
3✔
211
        ",",
212
        tokens.getTokenAfter(before, isComma)!,
213
      );
214
    }
215
    if (child.type === "Identifier") {
109✔
216
      if (!isStaticValueIdentifier(child, ctx)) {
10✔
217
        throw throwUnexpectedNodeError(child, tokens);
1✔
218
      }
219
    }
220
    setParent(child, node);
108✔
221
  });
222
}
223

224
/**
225
 * Validate Literal node
226
 */
227
function validateLiteralNode(
228
  node: Literal,
229
  tokens: TokenStore,
230
  ctx: JSONSyntaxContext,
231
): void {
232
  /* istanbul ignore next */
233
  if (node.type !== "Literal") {
234
    throw throwUnexpectedNodeError(node, tokens);
235
  }
236

237
  if (isRegExpLiteral(node)) {
224✔
238
    if (!ctx.regExpLiterals) {
4✔
239
      throw throwUnexpectedNodeError(node, tokens);
1✔
240
    }
241
  } else if (
220✔
242
    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- bigint
243
    (node as any).bigint
244
  ) {
245
    if (!ctx.bigintLiterals) {
4✔
246
      throw throwUnexpectedNodeError(node, tokens);
1✔
247
    }
248
  } else {
249
    validateLiteral(node, ctx);
216✔
250
  }
251
}
252

253
/* eslint-disable complexity -- ignore */
254
/**
255
 * Validate literal
256
 */
257
function validateLiteral(node: Literal, ctx: JSONSyntaxContext) {
258
  /* eslint-enable complexity -- ignore */
259
  const value = node.value;
216✔
260
  if (
216✔
261
    (!ctx.invalidJsonNumbers ||
681✔
262
      !ctx.leadingOrTrailingDecimalPoints ||
263
      !ctx.numericSeparators) &&
264
    typeof value === "number"
265
  ) {
266
    const text = node.raw!;
64✔
267
    if (!ctx.leadingOrTrailingDecimalPoints) {
64✔
268
      if (text.startsWith(".")) {
15✔
269
        throw throwUnexpectedTokenError(".", node);
1✔
270
      }
271
      if (text.endsWith(".")) {
14✔
272
        throw throwUnexpectedTokenError(".", {
1✔
273
          range: [node.range![1] - 1, node.range![1]],
274
          loc: {
275
            start: {
276
              line: node.loc!.end.line,
277
              column: node.loc!.end.column - 1,
278
            },
279
            end: node.loc!.end,
280
          },
281
        });
282
      }
283
    }
284
    if (!ctx.numericSeparators) {
62✔
285
      if (text.includes("_")) {
62✔
286
        const index = text.indexOf("_");
1✔
287
        throw throwUnexpectedTokenError("_", {
1✔
288
          range: [node.range![0] + index, node.range![0] + index + 1],
289
          loc: {
290
            start: {
291
              line: node.loc!.start.line,
292
              column: node.loc!.start.column + index,
293
            },
294
            end: {
295
              line: node.loc!.start.line,
296
              column: node.loc!.start.column + index + 1,
297
            },
298
          },
299
        });
300
      }
301
    }
302
    if (!ctx.octalNumericLiterals) {
61✔
303
      if (octalNumericLiteralPattern.test(text)) {
61✔
304
        throw throwUnexpectedError("octal numeric literal", node);
3✔
305
      }
306
    }
307
    if (!ctx.legacyOctalNumericLiterals) {
58✔
308
      if (legacyOctalNumericLiteralPattern.test(text)) {
58✔
309
        throw throwUnexpectedError("legacy octal numeric literal", node);
2✔
310
      }
311
    }
312
    if (!ctx.binaryNumericLiterals) {
56✔
313
      if (binaryNumericLiteralPattern.test(text)) {
56✔
314
        throw throwUnexpectedError("binary numeric literal", node);
3✔
315
      }
316
    }
317
    if (!ctx.invalidJsonNumbers) {
53✔
318
      try {
8✔
319
        JSON.parse(text);
8✔
320
      } catch {
321
        throw throwInvalidNumberError(text, node);
1✔
322
      }
323
    }
324
  }
325
  if (
204✔
326
    (!ctx.multilineStrings ||
649✔
327
      !ctx.singleQuotes ||
328
      !ctx.unicodeCodepointEscapes) &&
329
    typeof value === "string"
330
  ) {
331
    if (!ctx.singleQuotes) {
41✔
332
      if (node.raw!.startsWith("'")) {
22✔
333
        throw throwUnexpectedError("single quoted", node);
1✔
334
      }
335
    }
336
    if (!ctx.multilineStrings) {
40✔
337
      if (lineBreakPattern.test(node.raw!)) {
21✔
338
        throw throwUnexpectedError("multiline string", node);
1✔
339
      }
340
    }
341
    if (!ctx.unicodeCodepointEscapes) {
39✔
342
      if (hasUnicodeCodepointEscapes(node.raw!)) {
39✔
343
        throw throwUnexpectedError("unicode codepoint escape", node);
8✔
344
      }
345
    }
346
  }
347

348
  return undefined;
194✔
349
}
350

351
/**
352
 * Validate UnaryExpression node
353
 */
354
function validateUnaryExpressionNode(
355
  node: UnaryExpression,
356
  tokens: TokenStore,
357
  ctx: JSONSyntaxContext,
358
): void {
359
  /* istanbul ignore next */
360
  if (node.type !== "UnaryExpression") {
361
    throw throwUnexpectedNodeError(node, tokens);
362
  }
363
  const operator = node.operator;
44✔
364

365
  if (operator === "+") {
44✔
366
    if (!ctx.plusSigns) {
23✔
367
      throw throwUnexpectedTokenError("+", node);
1✔
368
    }
369
  } else if (operator !== "-") {
21✔
370
    throw throwUnexpectedNodeError(node, tokens);
1✔
371
  }
372
  const argument = node.argument;
42✔
373
  if (argument.type === "Literal") {
42✔
374
    if (typeof argument.value !== "number") {
22✔
375
      throw throwUnexpectedNodeError(argument, tokens);
1✔
376
    }
377
  } else if (argument.type === "Identifier") {
20✔
378
    if (!isNumberIdentifier(argument, ctx)) {
19✔
379
      throw throwUnexpectedNodeError(argument, tokens);
3✔
380
    }
381
  } else {
382
    throw throwUnexpectedNodeError(argument, tokens);
1✔
383
  }
384
  if (!ctx.spacedSigns) {
37✔
385
    if (node.range![0] + 1 < argument.range![0]) {
1✔
386
      throw throwUnexpectedSpaceError(tokens.getFirstToken(node));
1✔
387
    }
388
  }
389

390
  setParent(argument, node);
36✔
391
}
392

393
/**
394
 * Validate Identifier node
395
 */
396
function validateIdentifierNode(
397
  node: Identifier,
398
  tokens: TokenStore,
399
  ctx: JSONSyntaxContext,
400
): void {
401
  /* istanbul ignore next */
402
  if (node.type !== "Identifier") {
403
    throw throwUnexpectedNodeError(node, tokens);
404
  }
405

406
  if (!ctx.escapeSequenceInIdentifier) {
94✔
407
    if (node.name.length < node.range![1] - node.range![0]) {
56✔
408
      throw throwUnexpectedError("escape sequence", node);
4✔
409
    }
410
  }
411
}
412

413
/**
414
 * Validate TemplateLiteral node
415
 */
416
function validateTemplateLiteralNode(
417
  node: TemplateLiteral,
418
  tokens: TokenStore,
419
  ctx: JSONSyntaxContext,
420
): void {
421
  /* istanbul ignore next */
422
  if (node.type !== "TemplateLiteral") {
423
    throw throwUnexpectedNodeError(node, tokens);
424
  }
425
  if (!ctx.templateLiterals) {
10✔
426
    throw throwUnexpectedNodeError(node, tokens);
1✔
427
  }
428
  if (node.expressions.length) {
9!
429
    const token = tokens.getFirstToken(node.quasis[0]);
×
430
    const loc: MaybeNodeOrToken = {
×
431
      loc: {
432
        start: {
433
          line: token.loc.end.line,
434
          column: token.loc.end.column - 2,
435
        },
436
        end: token.loc.end,
437
      },
438
      range: [token.range[1] - 2, token.range[1]],
439
    };
440
    throw throwUnexpectedTokenError("$", loc);
×
441
  }
442

443
  if (!ctx.unicodeCodepointEscapes) {
9!
444
    if (hasUnicodeCodepointEscapes(node.quasis[0].value.raw)) {
×
445
      throw throwUnexpectedError("unicode codepoint escape", node);
×
446
    }
447
  }
448
  for (const q of node.quasis) {
9✔
449
    setParent(q, node);
9✔
450
  }
451
}
452

453
/**
454
 * Validate TemplateElement node
455
 */
456
function validateTemplateElementNode(
457
  node: TemplateElement,
458
  tokens: TokenStore,
459
): void {
460
  /* istanbul ignore next */
461
  if (node.type !== "TemplateElement") {
462
    throw throwUnexpectedNodeError(node, tokens);
463
  }
464
  const { cooked } = node.value;
12✔
465
  if (cooked == null) {
12!
466
    throw throwUnexpectedNodeError(node, tokens);
×
467
  }
468
  const startOffset = -1;
12✔
469
  const endOffset = node.tail ? 1 : 2;
12✔
470

471
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore
472
  (node as any).start += startOffset;
12✔
473
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore
474
  (node as any).end += endOffset;
12✔
475

476
  node.range![0] += startOffset;
12✔
477
  node.range![1] += endOffset;
12✔
478

479
  node.loc!.start.column += startOffset;
12✔
480
  node.loc!.end.column += endOffset;
12✔
481
}
482

483
/**
484
 * Validate BinaryExpression node
485
 */
486
function validateBinaryExpressionNode(
487
  node: BinaryExpression,
488
  tokens: TokenStore,
489
  ctx: JSONSyntaxContext,
490
): void {
491
  /* istanbul ignore next */
492
  if (node.type !== "BinaryExpression") {
493
    throw throwUnexpectedNodeError(node, tokens);
494
  }
495
  if (!ctx.staticExpressions) {
20✔
496
    throw throwUnexpectedNodeError(node, tokens);
3✔
497
  }
498
  const { operator, left, right } = node;
17✔
499
  if (
17✔
500
    operator !== "+" &&
52✔
501
    operator !== "-" &&
502
    operator !== "*" &&
503
    operator !== "/" &&
504
    operator !== "%" &&
505
    operator !== "**"
506
  ) {
507
    throw throwOperatorError();
1✔
508
  }
509
  if (left.type === "PrivateIdentifier") {
16!
510
    throw throwUnexpectedNodeError(left, tokens);
×
511
  }
512
  validateExpr(left, throwOperatorError);
16✔
513
  validateExpr(right, () => throwUnexpectedNodeError(right, tokens));
15✔
514

515
  /**
516
   * Validate Expression node
517
   */
518
  function validateExpr(expr: Expression, throwError: () => void) {
519
    if (expr.type === "Literal") {
31!
520
      if (typeof expr.value !== "number") {
31✔
521
        throw throwError();
2✔
522
      }
523
    } else if (
×
524
      expr.type !== "BinaryExpression" &&
×
525
      expr.type !== "UnaryExpression"
526
    ) {
527
      throw throwError();
×
528
    }
529
    setParent(expr, node);
29✔
530
  }
531

532
  /**
533
   * Throw error
534
   */
535
  function throwOperatorError(): never {
536
    throw throwUnexpectedTokenError(
2✔
537
      operator,
538
      tokens.getTokenAfter(
2!
539
        tokens.getFirstToken(node),
540
        (t) => t.value === operator,
2✔
541
      ) || node,
542
    );
543
  }
544
}
545

546
/**
547
 * Check if given node is NaN or Infinity or undefined
548
 */
549
export function isStaticValueIdentifier<I extends Identifier | JSONIdentifier>(
1✔
550
  node: I,
551
  ctx: JSONSyntaxContext,
552
): node is I & { name: "NaN" | "Infinity" | "undefined" } {
553
  if (isNumberIdentifier(node, ctx)) {
27✔
554
    return true;
14✔
555
  }
556
  return node.name === "undefined" && ctx.undefinedKeywords;
13✔
557
}
558

559
/**
560
 * Check if given node is NaN or Infinity
561
 */
562
function isNumberIdentifier<I extends Identifier | JSONIdentifier>(
563
  node: I,
564
  ctx: JSONSyntaxContext,
565
): node is I & { name: "NaN" | "Infinity" } {
566
  if (node.name === "Infinity" && ctx.infinities) {
46✔
567
    return true;
15✔
568
  }
569
  if (node.name === "NaN" && ctx.nans) {
31✔
570
    return true;
15✔
571
  }
572
  return false;
16✔
573
}
574

575
/** Set parent node */
576
function setParent(prop: Node, parent: Node) {
577
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore
578
  (prop as any).parent = parent;
356✔
579
}
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

© 2025 Coveralls, Inc