@eslint/json 0.14.0 → 1.0.0

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