@eslint/json 0.14.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js DELETED
@@ -1,1266 +0,0 @@
1
-
2
- /**
3
- * @import { DocumentNode, AnyNode, Token, LocationRange, MemberNode } from "@humanwhocodes/momoa"
4
- * @import { FileProblem, DirectiveType, RulesConfig, Language, OkParseResult, ParseResult, File } from "@eslint/core"
5
- * @import { JSONSyntaxElement, JSONRuleDefinition } from "./types.ts"
6
- */
7
- // @ts-self-types="./index.d.ts"
8
- import { iterator, visitorKeys, parse } from '@humanwhocodes/momoa';
9
- import { ConfigCommentParser, TextSourceCodeBase, Directive, VisitNodeStep } from '@eslint/plugin-kit';
10
- import naturalCompare from 'natural-compare';
11
-
12
- /**
13
- * @fileoverview The JSONSourceCode class.
14
- * @author Nicholas C. Zakas
15
- */
16
-
17
-
18
- //-----------------------------------------------------------------------------
19
- // Types
20
- //-----------------------------------------------------------------------------
21
-
22
- /**
23
- */
24
-
25
- //-----------------------------------------------------------------------------
26
- // Helpers
27
- //-----------------------------------------------------------------------------
28
-
29
- const commentParser = new ConfigCommentParser();
30
-
31
- const INLINE_CONFIG =
32
- /^\s*eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|$)/u;
33
-
34
- /**
35
- * A class to represent a step in the traversal process.
36
- */
37
- class JSONTraversalStep extends VisitNodeStep {
38
- /**
39
- * The target of the step.
40
- * @type {AnyNode}
41
- */
42
- target = undefined;
43
-
44
- /**
45
- * Creates a new instance.
46
- * @param {Object} options The options for the step.
47
- * @param {AnyNode} options.target The target of the step.
48
- * @param {1|2} options.phase The phase of the step.
49
- * @param {Array<any>} options.args The arguments of the step.
50
- */
51
- constructor({ target, phase, args }) {
52
- super({ target, phase, args });
53
-
54
- this.target = target;
55
- }
56
- }
57
-
58
- /**
59
- * Processes tokens to extract comments and their starting tokens.
60
- * @param {Array<Token>} tokens The tokens to process.
61
- * @returns {{ comments: Array<Token>, starts: Map<number, number>, ends: Map<number, number>}}
62
- * An object containing an array of comments, a map of starting token range to token index, and
63
- * a map of ending token range to token index.
64
- */
65
- function processTokens(tokens) {
66
- /** @type {Array<Token>} */
67
- const comments = [];
68
-
69
- /** @type {Map<number, number>} */
70
- const starts = new Map();
71
-
72
- /** @type {Map<number, number>} */
73
- const ends = new Map();
74
-
75
- for (let i = 0; i < tokens.length; i++) {
76
- const token = tokens[i];
77
-
78
- if (token.type.endsWith("Comment")) {
79
- comments.push(token);
80
- }
81
-
82
- starts.set(token.range[0], i);
83
- ends.set(token.range[1], i);
84
- }
85
-
86
- return { comments, starts, ends };
87
- }
88
-
89
- //-----------------------------------------------------------------------------
90
- // Exports
91
- //-----------------------------------------------------------------------------
92
-
93
- /**
94
- * JSON Source Code Object
95
- * @extends {TextSourceCodeBase<{LangOptions: JSONLanguageOptions, RootNode: DocumentNode, SyntaxElementWithLoc: JSONSyntaxElement, ConfigNode: Token}>}
96
- */
97
- class JSONSourceCode extends TextSourceCodeBase {
98
- /**
99
- * Cached traversal steps.
100
- * @type {Array<JSONTraversalStep>|undefined}
101
- */
102
- #steps;
103
-
104
- /**
105
- * Cache of parent nodes.
106
- * @type {WeakMap<AnyNode, AnyNode>}
107
- */
108
- #parents = new WeakMap();
109
-
110
- /**
111
- * Collection of inline configuration comments.
112
- * @type {Array<Token>}
113
- */
114
- #inlineConfigComments;
115
-
116
- /**
117
- * The AST of the source code.
118
- * @type {DocumentNode}
119
- */
120
- ast = undefined;
121
-
122
- /**
123
- * The comment tokens in the source code.
124
- * @type {Array<Token>|undefined}
125
- */
126
- comments;
127
-
128
- /**
129
- * A map of token start positions to their corresponding index.
130
- * @type {Map<number, number>}
131
- */
132
- #tokenStarts;
133
-
134
- /**
135
- * A map of token end positions to their corresponding index.
136
- * @type {Map<number, number>}
137
- */
138
- #tokenEnds;
139
-
140
- /**
141
- * Creates a new instance.
142
- * @param {Object} options The options for the instance.
143
- * @param {string} options.text The source code text.
144
- * @param {DocumentNode} options.ast The root AST node.
145
- */
146
- constructor({ text, ast }) {
147
- super({ text, ast, lineEndingPattern: /\r\n|[\r\n]/u });
148
- this.ast = ast;
149
-
150
- const { comments, starts, ends } = processTokens(this.ast.tokens ?? []);
151
- this.comments = comments;
152
- this.#tokenStarts = starts;
153
- this.#tokenEnds = ends;
154
- }
155
-
156
- /**
157
- * Returns the value of the given comment.
158
- * @param {Token} comment The comment to get the value of.
159
- * @returns {string} The value of the comment.
160
- * @throws {Error} When an unexpected comment type is passed.
161
- */
162
- #getCommentValue(comment) {
163
- if (comment.type === "LineComment") {
164
- return this.getText(comment).slice(2); // strip leading `//`
165
- }
166
-
167
- if (comment.type === "BlockComment") {
168
- return this.getText(comment).slice(2, -2); // strip leading `/*` and trailing `*/`
169
- }
170
-
171
- throw new Error(`Unexpected comment type '${comment.type}'`);
172
- }
173
-
174
- /**
175
- * Returns an array of all inline configuration nodes found in the
176
- * source code.
177
- * @returns {Array<Token>} An array of all inline configuration nodes.
178
- */
179
- getInlineConfigNodes() {
180
- if (!this.#inlineConfigComments) {
181
- this.#inlineConfigComments = this.comments.filter(comment =>
182
- INLINE_CONFIG.test(this.#getCommentValue(comment)),
183
- );
184
- }
185
-
186
- return this.#inlineConfigComments ?? [];
187
- }
188
-
189
- /**
190
- * Returns directives that enable or disable rules along with any problems
191
- * encountered while parsing the directives.
192
- * @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
193
- * that ESLint needs to further process the directives.
194
- */
195
- getDisableDirectives() {
196
- /** @type {Array<FileProblem>} */
197
- const problems = [];
198
- /** @type {Array<Directive>} */
199
- const directives = [];
200
-
201
- this.getInlineConfigNodes().forEach(comment => {
202
- const { label, value, justification } =
203
- commentParser.parseDirective(this.#getCommentValue(comment));
204
-
205
- // `eslint-disable-line` directives are not allowed to span multiple lines as it would be confusing to which lines they apply
206
- if (
207
- label === "eslint-disable-line" &&
208
- comment.loc.start.line !== comment.loc.end.line
209
- ) {
210
- const message = `${label} comment should not span multiple lines.`;
211
-
212
- problems.push({
213
- ruleId: null,
214
- message,
215
- loc: comment.loc,
216
- });
217
- return;
218
- }
219
-
220
- switch (label) {
221
- case "eslint-disable":
222
- case "eslint-enable":
223
- case "eslint-disable-next-line":
224
- case "eslint-disable-line": {
225
- const directiveType = label.slice("eslint-".length);
226
-
227
- directives.push(
228
- new Directive({
229
- type: /** @type {DirectiveType} */ (directiveType),
230
- node: comment,
231
- value,
232
- justification,
233
- }),
234
- );
235
- }
236
-
237
- // no default
238
- }
239
- });
240
-
241
- return { problems, directives };
242
- }
243
-
244
- /**
245
- * Returns inline rule configurations along with any problems
246
- * encountered while parsing the configurations.
247
- * @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:LocationRange}>}} Information
248
- * that ESLint needs to further process the rule configurations.
249
- */
250
- applyInlineConfig() {
251
- /** @type {Array<FileProblem>} */
252
- const problems = [];
253
- /** @type {Array<{config:{rules:RulesConfig},loc:LocationRange}>} */
254
- const configs = [];
255
-
256
- this.getInlineConfigNodes().forEach(comment => {
257
- const { label, value } = commentParser.parseDirective(
258
- this.#getCommentValue(comment),
259
- );
260
-
261
- if (label === "eslint") {
262
- const parseResult = commentParser.parseJSONLikeConfig(value);
263
-
264
- if (parseResult.ok) {
265
- configs.push({
266
- config: {
267
- rules: parseResult.config,
268
- },
269
- loc: comment.loc,
270
- });
271
- } else {
272
- problems.push({
273
- ruleId: null,
274
- message:
275
- /** @type {{ok: false, error: { message: string }}} */ (
276
- parseResult
277
- ).error.message,
278
- loc: comment.loc,
279
- });
280
- }
281
- }
282
- });
283
-
284
- return {
285
- configs,
286
- problems,
287
- };
288
- }
289
-
290
- /**
291
- * Returns the parent of the given node.
292
- * @param {AnyNode} node The node to get the parent of.
293
- * @returns {AnyNode|undefined} The parent of the node.
294
- */
295
- getParent(node) {
296
- return this.#parents.get(node);
297
- }
298
-
299
- /**
300
- * Traverse the source code and return the steps that were taken.
301
- * @returns {Iterable<JSONTraversalStep>} The steps that were taken while traversing the source code.
302
- */
303
- traverse() {
304
- // Because the AST doesn't mutate, we can cache the steps
305
- if (this.#steps) {
306
- return this.#steps.values();
307
- }
308
-
309
- /** @type {Array<JSONTraversalStep>} */
310
- const steps = (this.#steps = []);
311
-
312
- for (const { node, parent, phase } of iterator(this.ast)) {
313
- if (parent) {
314
- this.#parents.set(
315
- /** @type {AnyNode} */ (node),
316
- /** @type {AnyNode} */ (parent),
317
- );
318
- }
319
-
320
- steps.push(
321
- new JSONTraversalStep({
322
- target: /** @type {AnyNode} */ (node),
323
- phase: phase === "enter" ? 1 : 2,
324
- args: [node, parent],
325
- }),
326
- );
327
- }
328
-
329
- return steps;
330
- }
331
-
332
- /**
333
- * Gets the token before the given node or token, optionally including comments.
334
- * @param {AnyNode|Token} nodeOrToken The node or token to get the previous token for.
335
- * @param {Object} [options] Options object.
336
- * @param {boolean} [options.includeComments] If true, return comments when they are present.
337
- * @returns {Token|null} The previous token or comment, or null if there is none.
338
- */
339
- getTokenBefore(nodeOrToken, { includeComments = false } = {}) {
340
- const index = this.#tokenStarts.get(nodeOrToken.range[0]);
341
-
342
- if (index === undefined) {
343
- return null;
344
- }
345
-
346
- let previousIndex = index - 1;
347
- if (previousIndex < 0) {
348
- return null;
349
- }
350
-
351
- const tokens = this.ast.tokens;
352
- let tokenOrComment = tokens[previousIndex];
353
-
354
- if (includeComments) {
355
- return tokenOrComment;
356
- }
357
-
358
- // skip comments
359
- while (tokenOrComment?.type.endsWith("Comment")) {
360
- previousIndex--;
361
-
362
- if (previousIndex < 0) {
363
- return null;
364
- }
365
-
366
- tokenOrComment = tokens[previousIndex];
367
- }
368
- return tokenOrComment;
369
- }
370
-
371
- /**
372
- * Gets the token after the given node or token, skipping any comments unless includeComments is true.
373
- * @param {AnyNode|Token} nodeOrToken The node or token to get the next token for.
374
- * @param {Object} [options] Options object.
375
- * @param {boolean} [options.includeComments=false] If true, return comments when they are present.
376
- * @returns {Token|null} The next token or comment, or null if there is none.
377
- */
378
- getTokenAfter(nodeOrToken, { includeComments = false } = {}) {
379
- const index = this.#tokenEnds.get(nodeOrToken.range[1]);
380
-
381
- if (index === undefined) {
382
- return null;
383
- }
384
-
385
- let nextIndex = index + 1;
386
- const tokens = this.ast.tokens;
387
- if (nextIndex >= tokens.length) {
388
- return null;
389
- }
390
-
391
- let tokenOrComment = tokens[nextIndex];
392
-
393
- if (includeComments) {
394
- return tokenOrComment;
395
- }
396
-
397
- // skip comments
398
- while (tokenOrComment?.type.endsWith("Comment")) {
399
- nextIndex++;
400
-
401
- if (nextIndex >= tokens.length) {
402
- return null;
403
- }
404
-
405
- tokenOrComment = tokens[nextIndex];
406
- }
407
-
408
- return tokenOrComment;
409
- }
410
- }
411
-
412
- /**
413
- * @fileoverview The JSONLanguage class.
414
- * @author Nicholas C. Zakas
415
- */
416
-
417
-
418
- //-----------------------------------------------------------------------------
419
- // Types
420
- //-----------------------------------------------------------------------------
421
-
422
- /**
423
- *
424
- * @typedef {OkParseResult<DocumentNode>} JSONOkParseResult
425
- * @typedef {ParseResult<DocumentNode>} JSONParseResult
426
- *
427
- * @typedef {Object} JSONLanguageOptions
428
- * @property {boolean} [allowTrailingCommas] Whether to allow trailing commas in JSONC mode.
429
- */
430
-
431
- //-----------------------------------------------------------------------------
432
- // Exports
433
- //-----------------------------------------------------------------------------
434
-
435
- /**
436
- * JSON Language Object
437
- * @implements {Language<{ LangOptions: JSONLanguageOptions; Code: JSONSourceCode; RootNode: DocumentNode; Node: AnyNode }>}
438
- */
439
- class JSONLanguage {
440
- /**
441
- * The type of file to read.
442
- * @type {"text"}
443
- */
444
- fileType = "text";
445
-
446
- /**
447
- * The line number at which the parser starts counting.
448
- * @type {0|1}
449
- */
450
- lineStart = 1;
451
-
452
- /**
453
- * The column number at which the parser starts counting.
454
- * @type {0|1}
455
- */
456
- columnStart = 1;
457
-
458
- /**
459
- * The name of the key that holds the type of the node.
460
- * @type {string}
461
- */
462
- nodeTypeKey = "type";
463
-
464
- /**
465
- * The parser mode.
466
- * @type {"json"|"jsonc"|"json5"}
467
- */
468
- #mode = "json";
469
-
470
- /**
471
- * The visitor keys.
472
- * @type {Record<string, string[]>}
473
- */
474
- visitorKeys = Object.fromEntries([...visitorKeys]);
475
-
476
- /**
477
- * Creates a new instance.
478
- * @param {Object} options The options to use for this instance.
479
- * @param {"json"|"jsonc"|"json5"} options.mode The parser mode to use.
480
- */
481
- constructor({ mode }) {
482
- this.#mode = mode;
483
- }
484
-
485
- /**
486
- * Validates the language options.
487
- * @param {JSONLanguageOptions} languageOptions The language options to validate.
488
- * @returns {void}
489
- * @throws {Error} When the language options are invalid.
490
- */
491
- validateLanguageOptions(languageOptions) {
492
- if (languageOptions.allowTrailingCommas !== undefined) {
493
- if (typeof languageOptions.allowTrailingCommas !== "boolean") {
494
- throw new Error(
495
- "allowTrailingCommas must be a boolean if provided.",
496
- );
497
- }
498
-
499
- // we know that allowTrailingCommas is a boolean here
500
-
501
- // only allowed in JSONC mode
502
- if (this.#mode !== "jsonc") {
503
- throw new Error(
504
- "allowTrailingCommas option is only available in JSONC.",
505
- );
506
- }
507
- }
508
- }
509
-
510
- /**
511
- * Parses the given file into an AST.
512
- * @param {File} file The virtual file to parse.
513
- * @param {{languageOptions: JSONLanguageOptions}} context The options to use for parsing.
514
- * @returns {JSONParseResult} The result of parsing.
515
- */
516
- parse(file, context) {
517
- // Note: BOM already removed
518
- const text = /** @type {string} */ (file.body);
519
- const allowTrailingCommas =
520
- context?.languageOptions?.allowTrailingCommas;
521
-
522
- /*
523
- * Check for parsing errors first. If there's a parsing error, nothing
524
- * else can happen. However, a parsing error does not throw an error
525
- * from this method - it's just considered a fatal error message, a
526
- * problem that ESLint identified just like any other.
527
- */
528
- try {
529
- const root = parse(text, {
530
- mode: this.#mode,
531
- ranges: true,
532
- tokens: true,
533
- allowTrailingCommas,
534
- });
535
-
536
- return {
537
- ok: true,
538
- ast: root,
539
- };
540
- } catch (ex) {
541
- // error messages end with (line:column) so we strip that off for ESLint
542
- const message = ex.message
543
- .slice(0, ex.message.lastIndexOf("("))
544
- .trim();
545
-
546
- return {
547
- ok: false,
548
- errors: [
549
- {
550
- ...ex,
551
- message,
552
- },
553
- ],
554
- };
555
- }
556
- }
557
-
558
- /* eslint-disable class-methods-use-this -- Required to complete interface. */
559
- /**
560
- * Creates a new `JSONSourceCode` object from the given information.
561
- * @param {File} file The virtual file to create a `JSONSourceCode` object from.
562
- * @param {JSONOkParseResult} parseResult The result returned from `parse()`.
563
- * @returns {JSONSourceCode} The new `JSONSourceCode` object.
564
- */
565
- createSourceCode(file, parseResult) {
566
- return new JSONSourceCode({
567
- text: /** @type {string} */ (file.body),
568
- ast: parseResult.ast,
569
- });
570
- }
571
- /* eslint-enable class-methods-use-this -- Required to complete interface. */
572
- }
573
-
574
- const rules$1 = /** @type {const} */ ({
575
- "json/no-duplicate-keys": "error",
576
- "json/no-empty-keys": "error",
577
- "json/no-unnormalized-keys": "error",
578
- "json/no-unsafe-values": "error"
579
- });
580
-
581
- /**
582
- * @fileoverview Rule to prevent duplicate keys in JSON.
583
- * @author Nicholas C. Zakas
584
- */
585
-
586
- //-----------------------------------------------------------------------------
587
- // Type Definitions
588
- //-----------------------------------------------------------------------------
589
-
590
- /**
591
- *
592
- * @typedef {"duplicateKey"} NoDuplicateKeysMessageIds
593
- * @typedef {JSONRuleDefinition<{ MessageIds: NoDuplicateKeysMessageIds }>} NoDuplicateKeysRuleDefinition
594
- */
595
-
596
- //-----------------------------------------------------------------------------
597
- // Rule Definition
598
- //-----------------------------------------------------------------------------
599
-
600
- /** @type {NoDuplicateKeysRuleDefinition} */
601
- const rule$5 = {
602
- meta: {
603
- type: "problem",
604
-
605
- docs: {
606
- recommended: true,
607
- description: "Disallow duplicate keys in JSON objects",
608
- url: "https://github.com/eslint/json/tree/main/docs/rules/no-duplicate-keys.md",
609
- },
610
-
611
- messages: {
612
- duplicateKey: 'Duplicate key "{{key}}" found.',
613
- },
614
- },
615
-
616
- create(context) {
617
- /** @type {Array<Map<string, MemberNode>|undefined>} */
618
- const objectKeys = [];
619
-
620
- /** @type {Map<string, MemberNode>|undefined} */
621
- let keys;
622
-
623
- return {
624
- Object() {
625
- objectKeys.push(keys);
626
- keys = new Map();
627
- },
628
-
629
- Member(node) {
630
- const key =
631
- node.name.type === "String"
632
- ? node.name.value
633
- : node.name.name;
634
-
635
- if (keys.has(key)) {
636
- context.report({
637
- loc: node.name.loc,
638
- messageId: "duplicateKey",
639
- data: {
640
- key,
641
- },
642
- });
643
- } else {
644
- keys.set(key, node);
645
- }
646
- },
647
- "Object:exit"() {
648
- keys = objectKeys.pop();
649
- },
650
- };
651
- },
652
- };
653
-
654
- /**
655
- * @fileoverview Rule to prevent empty keys in JSON.
656
- * @author Nicholas C. Zakas
657
- */
658
-
659
- //-----------------------------------------------------------------------------
660
- // Type Definitions
661
- //-----------------------------------------------------------------------------
662
-
663
- /**
664
- *
665
- * @typedef {"emptyKey"} NoEmptyKeysMessageIds
666
- * @typedef {JSONRuleDefinition<{ MessageIds: NoEmptyKeysMessageIds }>} NoEmptyKeysRuleDefinition
667
- */
668
-
669
- //-----------------------------------------------------------------------------
670
- // Rule Definition
671
- //-----------------------------------------------------------------------------
672
-
673
- /** @type {NoEmptyKeysRuleDefinition} */
674
- const rule$4 = {
675
- meta: {
676
- type: "problem",
677
-
678
- docs: {
679
- recommended: true,
680
- description: "Disallow empty keys in JSON objects",
681
- url: "https://github.com/eslint/json/tree/main/docs/rules/no-empty-keys.md",
682
- },
683
-
684
- messages: {
685
- emptyKey: "Empty key found.",
686
- },
687
- },
688
-
689
- create(context) {
690
- return {
691
- Member(node) {
692
- const key =
693
- node.name.type === "String"
694
- ? node.name.value
695
- : node.name.name;
696
-
697
- if (key.trim() === "") {
698
- context.report({
699
- loc: node.name.loc,
700
- messageId: "emptyKey",
701
- });
702
- }
703
- },
704
- };
705
- },
706
- };
707
-
708
- /**
709
- * @fileoverview Rule to detect unnormalized keys in JSON.
710
- * @author Bradley Meck Farias
711
- */
712
-
713
- //-----------------------------------------------------------------------------
714
- // Type Definitions
715
- //-----------------------------------------------------------------------------
716
-
717
- /**
718
- *
719
- * @typedef {"unnormalizedKey"} NoUnnormalizedKeysMessageIds
720
- * @typedef {{ form: string }} NoUnnormalizedKeysOptions
721
- * @typedef {JSONRuleDefinition<{ RuleOptions: [NoUnnormalizedKeysOptions], MessageIds: NoUnnormalizedKeysMessageIds }>} NoUnnormalizedKeysRuleDefinition
722
- */
723
-
724
- //-----------------------------------------------------------------------------
725
- // Rule Definition
726
- //-----------------------------------------------------------------------------
727
-
728
- /** @type {NoUnnormalizedKeysRuleDefinition} */
729
- const rule$3 = {
730
- meta: {
731
- type: "problem",
732
-
733
- docs: {
734
- recommended: true,
735
- description: "Disallow JSON keys that are not normalized",
736
- url: "https://github.com/eslint/json/tree/main/docs/rules/no-unnormalized-keys.md",
737
- },
738
-
739
- messages: {
740
- unnormalizedKey: "Unnormalized key '{{key}}' found.",
741
- },
742
-
743
- schema: [
744
- {
745
- type: "object",
746
- properties: {
747
- form: {
748
- enum: ["NFC", "NFD", "NFKC", "NFKD"],
749
- },
750
- },
751
- additionalProperties: false,
752
- },
753
- ],
754
-
755
- defaultOptions: [
756
- {
757
- form: "NFC",
758
- },
759
- ],
760
- },
761
-
762
- create(context) {
763
- const [{ form }] = context.options;
764
-
765
- return {
766
- Member(node) {
767
- const key =
768
- node.name.type === "String"
769
- ? node.name.value
770
- : node.name.name;
771
-
772
- if (key.normalize(form) !== key) {
773
- context.report({
774
- loc: node.name.loc,
775
- messageId: "unnormalizedKey",
776
- data: {
777
- key,
778
- },
779
- });
780
- }
781
- },
782
- };
783
- },
784
- };
785
-
786
- /**
787
- * @fileoverview Rule to detect unsafe values in JSON.
788
- * @author Bradley Meck Farias
789
- */
790
-
791
- //-----------------------------------------------------------------------------
792
- // Type Definitions
793
- //-----------------------------------------------------------------------------
794
-
795
- /**
796
- *
797
- * @typedef {"unsafeNumber"|"unsafeInteger"|"unsafeZero"|"subnormal"|"loneSurrogate"} NoUnsafeValuesMessageIds
798
- * @typedef {JSONRuleDefinition<{ MessageIds: NoUnsafeValuesMessageIds }>} NoUnsafeValuesRuleDefinition
799
- */
800
-
801
- //-----------------------------------------------------------------------------
802
- // Helpers
803
- //-----------------------------------------------------------------------------
804
-
805
- /*
806
- * This rule is based on the JSON grammar from RFC 8259, section 6.
807
- * https://tools.ietf.org/html/rfc8259#section-6
808
- *
809
- * We separately capture the integer and fractional parts of a number, so that
810
- * we can check for unsafe numbers that will evaluate to Infinity.
811
- */
812
- const NUMBER = /^-?(?<int>0|([1-9]\d*))(?:\.(?<frac>\d+))?(?:e[+-]?\d+)?$/iu;
813
- const NON_ZERO = /[1-9]/u;
814
-
815
- //-----------------------------------------------------------------------------
816
- // Rule Definition
817
- //-----------------------------------------------------------------------------
818
-
819
- /** @type {NoUnsafeValuesRuleDefinition} */
820
- const rule$2 = {
821
- meta: {
822
- type: "problem",
823
-
824
- docs: {
825
- recommended: true,
826
- description: "Disallow JSON values that are unsafe for interchange",
827
- url: "https://github.com/eslint/json/tree/main/docs/rules/no-unsafe-values.md",
828
- },
829
-
830
- messages: {
831
- unsafeNumber: "The number '{{ value }}' will evaluate to Infinity.",
832
- unsafeInteger:
833
- "The integer '{{ value }}' is outside the safe integer range.",
834
- unsafeZero: "The number '{{ value }}' will evaluate to zero.",
835
- subnormal:
836
- "Unexpected subnormal number '{{ value }}' found, which may cause interoperability issues.",
837
- loneSurrogate: "Lone surrogate '{{ surrogate }}' found.",
838
- },
839
- },
840
-
841
- create(context) {
842
- return {
843
- Number(node) {
844
- const value = context.sourceCode.getText(node);
845
-
846
- if (Number.isFinite(node.value) !== true) {
847
- context.report({
848
- loc: node.loc,
849
- messageId: "unsafeNumber",
850
- data: { value },
851
- });
852
- } else {
853
- // Also matches -0, intentionally
854
- if (node.value === 0) {
855
- // If the value has been rounded down to 0, but there was some
856
- // fraction or non-zero part before the e-, this is a very small
857
- // number that doesn't fit inside an f64.
858
- const match = value.match(NUMBER);
859
- // assert(match, "If the regex is right, match is always truthy")
860
-
861
- // If any part of the number other than the exponent has a
862
- // non-zero digit in it, this number was not intended to be
863
- // evaluated down to a zero.
864
- if (
865
- NON_ZERO.test(match.groups.int) ||
866
- NON_ZERO.test(match.groups.frac)
867
- ) {
868
- context.report({
869
- loc: node.loc,
870
- messageId: "unsafeZero",
871
- data: { value },
872
- });
873
- }
874
- } else if (!/[.e]/iu.test(value)) {
875
- // Intended to be an integer
876
- if (
877
- node.value > Number.MAX_SAFE_INTEGER ||
878
- node.value < Number.MIN_SAFE_INTEGER
879
- ) {
880
- context.report({
881
- loc: node.loc,
882
- messageId: "unsafeInteger",
883
- data: { value },
884
- });
885
- }
886
- } else {
887
- // Floating point. Check for subnormal.
888
- const buffer = new ArrayBuffer(8);
889
- const view = new DataView(buffer);
890
- view.setFloat64(0, node.value, false);
891
- const asBigInt = view.getBigUint64(0, false);
892
- // Subnormals have an 11-bit exponent of 0 and a non-zero mantissa.
893
- if ((asBigInt & 0x7ff0000000000000n) === 0n) {
894
- context.report({
895
- loc: node.loc,
896
- messageId: "subnormal",
897
- // Value included so that it's seen in scientific notation
898
- data: {
899
- value,
900
- },
901
- });
902
- }
903
- }
904
- }
905
- },
906
- String(node) {
907
- if (node.value.isWellFormed) {
908
- if (node.value.isWellFormed()) {
909
- return;
910
- }
911
- }
912
- // match any high surrogate and, if it exists, a paired low surrogate
913
- // match any low surrogate not already matched
914
- const surrogatePattern =
915
- /[\uD800-\uDBFF][\uDC00-\uDFFF]?|[\uDC00-\uDFFF]/gu;
916
- let match = surrogatePattern.exec(node.value);
917
- while (match) {
918
- // only need to report non-paired surrogates
919
- if (match[0].length < 2) {
920
- context.report({
921
- loc: node.loc,
922
- messageId: "loneSurrogate",
923
- data: {
924
- surrogate: JSON.stringify(match[0]).slice(
925
- 1,
926
- -1,
927
- ),
928
- },
929
- });
930
- }
931
- match = surrogatePattern.exec(node.value);
932
- }
933
- },
934
- };
935
- },
936
- };
937
-
938
- /**
939
- * @fileoverview Rule to require JSON object keys to be sorted.
940
- * Copied largely from https://github.com/eslint/eslint/blob/main/lib/rules/sort-keys.js
941
- * @author Robin Thomas
942
- */
943
-
944
-
945
- //-----------------------------------------------------------------------------
946
- // Type Definitions
947
- //-----------------------------------------------------------------------------
948
-
949
- /**
950
- *
951
- * @typedef {Object} SortOptions
952
- * @property {boolean} caseSensitive
953
- * @property {boolean} natural
954
- * @property {number} minKeys
955
- * @property {boolean} allowLineSeparatedGroups
956
- *
957
- * @typedef {"sortKeys"} SortKeysMessageIds
958
- * @typedef {"asc"|"desc"} SortDirection
959
- * @typedef {[SortDirection, SortOptions]} SortKeysRuleOptions
960
- * @typedef {JSONRuleDefinition<{ RuleOptions: SortKeysRuleOptions, MessageIds: SortKeysMessageIds }>} SortKeysRuleDefinition
961
- * @typedef {(a:string,b:string) => boolean} Comparator
962
- */
963
-
964
- //-----------------------------------------------------------------------------
965
- // Helpers
966
- //-----------------------------------------------------------------------------
967
-
968
- const hasNonWhitespace = /\S/u;
969
-
970
- const comparators = {
971
- ascending: {
972
- alphanumeric: {
973
- /** @type {Comparator} */
974
- sensitive: (a, b) => a <= b,
975
-
976
- /** @type {Comparator} */
977
- insensitive: (a, b) => a.toLowerCase() <= b.toLowerCase(),
978
- },
979
- natural: {
980
- /** @type {Comparator} */
981
- sensitive: (a, b) => naturalCompare(a, b) <= 0,
982
-
983
- /** @type {Comparator} */
984
- insensitive: (a, b) =>
985
- naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0,
986
- },
987
- },
988
- descending: {
989
- alphanumeric: {
990
- /** @type {Comparator} */
991
- sensitive: (a, b) =>
992
- comparators.ascending.alphanumeric.sensitive(b, a),
993
-
994
- /** @type {Comparator} */
995
- insensitive: (a, b) =>
996
- comparators.ascending.alphanumeric.insensitive(b, a),
997
- },
998
- natural: {
999
- /** @type {Comparator} */
1000
- sensitive: (a, b) => comparators.ascending.natural.sensitive(b, a),
1001
-
1002
- /** @type {Comparator} */
1003
- insensitive: (a, b) =>
1004
- comparators.ascending.natural.insensitive(b, a),
1005
- },
1006
- },
1007
- };
1008
-
1009
- /**
1010
- * Gets the MemberNode's string key value.
1011
- * @param {MemberNode} member
1012
- * @return {string}
1013
- */
1014
- function getKey(member) {
1015
- return member.name.type === "Identifier"
1016
- ? member.name.name
1017
- : member.name.value;
1018
- }
1019
-
1020
- //-----------------------------------------------------------------------------
1021
- // Rule Definition
1022
- //-----------------------------------------------------------------------------
1023
-
1024
- /** @type {SortKeysRuleDefinition} */
1025
- const rule$1 = {
1026
- meta: {
1027
- type: "suggestion",
1028
-
1029
- defaultOptions: [
1030
- "asc",
1031
- {
1032
- allowLineSeparatedGroups: false,
1033
- caseSensitive: true,
1034
- minKeys: 2,
1035
- natural: false,
1036
- },
1037
- ],
1038
-
1039
- docs: {
1040
- recommended: false,
1041
- description: `Require JSON object keys to be sorted`,
1042
- url: "https://github.com/eslint/json/tree/main/docs/rules/sort-keys.md",
1043
- },
1044
-
1045
- messages: {
1046
- sortKeys:
1047
- "Expected object keys to be in {{sortName}} case-{{sensitivity}} {{direction}} order. '{{thisName}}' should be before '{{prevName}}'.",
1048
- },
1049
-
1050
- schema: [
1051
- {
1052
- enum: ["asc", "desc"],
1053
- },
1054
- {
1055
- type: "object",
1056
- properties: {
1057
- caseSensitive: {
1058
- type: "boolean",
1059
- },
1060
- natural: {
1061
- type: "boolean",
1062
- },
1063
- minKeys: {
1064
- type: "integer",
1065
- minimum: 2,
1066
- },
1067
- allowLineSeparatedGroups: {
1068
- type: "boolean",
1069
- },
1070
- },
1071
- additionalProperties: false,
1072
- },
1073
- ],
1074
- },
1075
-
1076
- create(context) {
1077
- const [
1078
- directionShort,
1079
- { allowLineSeparatedGroups, caseSensitive, natural, minKeys },
1080
- ] = context.options;
1081
-
1082
- const direction = directionShort === "asc" ? "ascending" : "descending";
1083
- const sortName = natural ? "natural" : "alphanumeric";
1084
- const sensitivity = caseSensitive ? "sensitive" : "insensitive";
1085
- const isValidOrder = comparators[direction][sortName][sensitivity];
1086
-
1087
- // Note that @humanwhocodes/momoa doesn't include comments in the object.members tree, so we can't just see if a member is preceded by a comment
1088
- const commentLineNums = new Set();
1089
- for (const comment of context.sourceCode.comments) {
1090
- for (
1091
- let lineNum = comment.loc.start.line;
1092
- lineNum <= comment.loc.end.line;
1093
- lineNum += 1
1094
- ) {
1095
- commentLineNums.add(lineNum);
1096
- }
1097
- }
1098
-
1099
- /**
1100
- * Checks if two members are line-separated.
1101
- * @param {MemberNode} prevMember The previous member.
1102
- * @param {MemberNode} member The current member.
1103
- * @return {boolean}
1104
- */
1105
- function isLineSeparated(prevMember, member) {
1106
- // Note that there can be comments *inside* members, e.g. `{"foo: /* comment *\/ "bar"}`, but these are ignored when calculating line-separated groups
1107
- const prevMemberEndLine = prevMember.loc.end.line;
1108
- const thisStartLine = member.loc.start.line;
1109
- if (thisStartLine - prevMemberEndLine < 2) {
1110
- return false;
1111
- }
1112
-
1113
- for (
1114
- let lineNum = prevMemberEndLine + 1;
1115
- lineNum < thisStartLine;
1116
- lineNum += 1
1117
- ) {
1118
- if (
1119
- !commentLineNums.has(lineNum) &&
1120
- !hasNonWhitespace.test(
1121
- context.sourceCode.lines[lineNum - 1],
1122
- )
1123
- ) {
1124
- return true;
1125
- }
1126
- }
1127
-
1128
- return false;
1129
- }
1130
-
1131
- return {
1132
- Object(node) {
1133
- let prevMember;
1134
- let prevName;
1135
-
1136
- if (node.members.length < minKeys) {
1137
- return;
1138
- }
1139
-
1140
- for (const member of node.members) {
1141
- const thisName = getKey(member);
1142
-
1143
- if (
1144
- prevMember &&
1145
- !isValidOrder(prevName, thisName) &&
1146
- (!allowLineSeparatedGroups ||
1147
- !isLineSeparated(prevMember, member))
1148
- ) {
1149
- context.report({
1150
- loc: member.name.loc,
1151
- messageId: "sortKeys",
1152
- data: {
1153
- thisName,
1154
- prevName,
1155
- direction,
1156
- sensitivity,
1157
- sortName,
1158
- },
1159
- });
1160
- }
1161
-
1162
- prevMember = member;
1163
- prevName = thisName;
1164
- }
1165
- },
1166
- };
1167
- },
1168
- };
1169
-
1170
- /**
1171
- * @fileoverview Rule to ensure top-level items are either an array or object.
1172
- * @author Joe Hildebrand
1173
- */
1174
-
1175
- //-----------------------------------------------------------------------------
1176
- // Type Definitions
1177
- //-----------------------------------------------------------------------------
1178
-
1179
- /**
1180
- *
1181
- * @typedef {"topLevel"} TopLevelInteropMessageIds
1182
- * @typedef {JSONRuleDefinition<{ MessageIds: TopLevelInteropMessageIds }>} TopLevelInteropRuleDefinition
1183
- */
1184
-
1185
- //-----------------------------------------------------------------------------
1186
- // Rule Definition
1187
- //-----------------------------------------------------------------------------
1188
-
1189
- /** @type {TopLevelInteropRuleDefinition} */
1190
- const rule = {
1191
- meta: {
1192
- type: "problem",
1193
-
1194
- docs: {
1195
- recommended: false,
1196
- description:
1197
- "Require the JSON top-level value to be an array or object",
1198
- url: "https://github.com/eslint/json/tree/main/docs/rules/top-level-interop.md",
1199
- },
1200
-
1201
- messages: {
1202
- topLevel:
1203
- "Top level item should be array or object, got '{{type}}'.",
1204
- },
1205
- },
1206
-
1207
- create(context) {
1208
- return {
1209
- Document(node) {
1210
- const { type } = node.body;
1211
- if (type !== "Object" && type !== "Array") {
1212
- context.report({
1213
- loc: node.loc,
1214
- messageId: "topLevel",
1215
- data: { type },
1216
- });
1217
- }
1218
- },
1219
- };
1220
- },
1221
- };
1222
-
1223
- var rules = {
1224
- "no-duplicate-keys": rule$5,
1225
- "no-empty-keys": rule$4,
1226
- "no-unnormalized-keys": rule$3,
1227
- "no-unsafe-values": rule$2,
1228
- "sort-keys": rule$1,
1229
- "top-level-interop": rule,
1230
- };
1231
-
1232
- /**
1233
- * @fileoverview JSON plugin.
1234
- * @author Nicholas C. Zakas
1235
- */
1236
-
1237
-
1238
- //-----------------------------------------------------------------------------
1239
- // Plugin
1240
- //-----------------------------------------------------------------------------
1241
-
1242
- const plugin = {
1243
- meta: {
1244
- name: "@eslint/json",
1245
- version: "0.14.0", // x-release-please-version
1246
- },
1247
- languages: {
1248
- json: new JSONLanguage({ mode: "json" }),
1249
- jsonc: new JSONLanguage({ mode: "jsonc" }),
1250
- json5: new JSONLanguage({ mode: "json5" }),
1251
- },
1252
- rules,
1253
- configs: {
1254
- recommended: {
1255
- plugins: {},
1256
- rules: rules$1,
1257
- },
1258
- },
1259
- };
1260
-
1261
- // eslint-disable-next-line no-lone-blocks -- The block syntax { ... } ensures that TypeScript does not get confused about the type of `plugin`.
1262
- {
1263
- plugin.configs.recommended.plugins.json = plugin;
1264
- }
1265
-
1266
- export { JSONLanguage, JSONSourceCode, plugin as default };