@eslint/json 0.12.0 → 0.13.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,7 +1,7 @@
1
1
 
2
2
  /**
3
- * @import { DocumentNode, Node, Token, AnyNode, MemberNode } from "@humanwhocodes/momoa"
4
- * @import { SourceLocation, FileProblem, DirectiveType, RulesConfig, TextSourceCode, Language, OkParseResult, ParseResult, File } from "@eslint/core"
3
+ * @import { DocumentNode, AnyNode, Token, MemberNode } from "@humanwhocodes/momoa"
4
+ * @import { SourceLocation, FileProblem, DirectiveType, RulesConfig, Language, OkParseResult, ParseResult, File } from "@eslint/core"
5
5
  * @import { JSONSyntaxElement, JSONRuleDefinition } from "./types.ts"
6
6
  */
7
7
  'use strict';
@@ -40,14 +40,14 @@ const INLINE_CONFIG =
40
40
  class JSONTraversalStep extends pluginKit.VisitNodeStep {
41
41
  /**
42
42
  * The target of the step.
43
- * @type {Node}
43
+ * @type {AnyNode}
44
44
  */
45
45
  target = undefined;
46
46
 
47
47
  /**
48
48
  * Creates a new instance.
49
49
  * @param {Object} options The options for the step.
50
- * @param {Node} options.target The target of the step.
50
+ * @param {AnyNode} options.target The target of the step.
51
51
  * @param {1|2} options.phase The phase of the step.
52
52
  * @param {Array<any>} options.args The arguments of the step.
53
53
  */
@@ -58,13 +58,44 @@ class JSONTraversalStep extends pluginKit.VisitNodeStep {
58
58
  }
59
59
  }
60
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
+
61
92
  //-----------------------------------------------------------------------------
62
93
  // Exports
63
94
  //-----------------------------------------------------------------------------
64
95
 
65
96
  /**
66
97
  * JSON Source Code Object
67
- * @implements {TextSourceCode<{LangOptions: JSONLanguageOptions, RootNode: DocumentNode, SyntaxElementWithLoc: JSONSyntaxElement, ConfigNode: Token}>}
98
+ * @extends {TextSourceCodeBase<{LangOptions: JSONLanguageOptions, RootNode: DocumentNode, SyntaxElementWithLoc: JSONSyntaxElement, ConfigNode: Token}>}
68
99
  */
69
100
  class JSONSourceCode extends pluginKit.TextSourceCodeBase {
70
101
  /**
@@ -75,7 +106,7 @@ class JSONSourceCode extends pluginKit.TextSourceCodeBase {
75
106
 
76
107
  /**
77
108
  * Cache of parent nodes.
78
- * @type {WeakMap<Node, Node>}
109
+ * @type {WeakMap<AnyNode, AnyNode>}
79
110
  */
80
111
  #parents = new WeakMap();
81
112
 
@@ -92,11 +123,23 @@ class JSONSourceCode extends pluginKit.TextSourceCodeBase {
92
123
  ast = undefined;
93
124
 
94
125
  /**
95
- * The comment node in the source code.
126
+ * The comment tokens in the source code.
96
127
  * @type {Array<Token>|undefined}
97
128
  */
98
129
  comments;
99
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
+
100
143
  /**
101
144
  * Creates a new instance.
102
145
  * @param {Object} options The options for the instance.
@@ -106,9 +149,11 @@ class JSONSourceCode extends pluginKit.TextSourceCodeBase {
106
149
  constructor({ text, ast }) {
107
150
  super({ text, ast });
108
151
  this.ast = ast;
109
- this.comments = ast.tokens
110
- ? ast.tokens.filter(token => token.type.endsWith("Comment"))
111
- : [];
152
+
153
+ const { comments, starts, ends } = processTokens(this.ast.tokens ?? []);
154
+ this.comments = comments;
155
+ this.#tokenStarts = starts;
156
+ this.#tokenEnds = ends;
112
157
  }
113
158
 
114
159
  /**
@@ -243,8 +288,8 @@ class JSONSourceCode extends pluginKit.TextSourceCodeBase {
243
288
 
244
289
  /**
245
290
  * Returns the parent of the given node.
246
- * @param {Node} node The node to get the parent of.
247
- * @returns {Node|undefined} The parent of the node.
291
+ * @param {AnyNode} node The node to get the parent of.
292
+ * @returns {AnyNode|undefined} The parent of the node.
248
293
  */
249
294
  getParent(node) {
250
295
  return this.#parents.get(node);
@@ -265,12 +310,15 @@ class JSONSourceCode extends pluginKit.TextSourceCodeBase {
265
310
 
266
311
  for (const { node, parent, phase } of momoa.iterator(this.ast)) {
267
312
  if (parent) {
268
- this.#parents.set(node, parent);
313
+ this.#parents.set(
314
+ /** @type {AnyNode} */ (node),
315
+ /** @type {AnyNode} */ (parent),
316
+ );
269
317
  }
270
318
 
271
319
  steps.push(
272
320
  new JSONTraversalStep({
273
- target: node,
321
+ target: /** @type {AnyNode} */ (node),
274
322
  phase: phase === "enter" ? 1 : 2,
275
323
  args: [node, parent],
276
324
  }),
@@ -279,10 +327,89 @@ class JSONSourceCode extends pluginKit.TextSourceCodeBase {
279
327
 
280
328
  return steps;
281
329
  }
330
+
331
+ /**
332
+ * Gets the token before the given node or token, optionally including comments.
333
+ * @param {AnyNode|Token} nodeOrToken The node or token to get the previous token for.
334
+ * @param {Object} [options] Options object.
335
+ * @param {boolean} [options.includeComments] If true, return comments when they are present.
336
+ * @returns {Token|null} The previous token or comment, or null if there is none.
337
+ */
338
+ getTokenBefore(nodeOrToken, { includeComments = false } = {}) {
339
+ const index = this.#tokenStarts.get(nodeOrToken.range[0]);
340
+
341
+ if (index === undefined) {
342
+ return null;
343
+ }
344
+
345
+ let previousIndex = index - 1;
346
+ if (previousIndex < 0) {
347
+ return null;
348
+ }
349
+
350
+ const tokens = this.ast.tokens;
351
+ let tokenOrComment = tokens[previousIndex];
352
+
353
+ if (includeComments) {
354
+ return tokenOrComment;
355
+ }
356
+
357
+ // skip comments
358
+ while (tokenOrComment?.type.endsWith("Comment")) {
359
+ previousIndex--;
360
+
361
+ if (previousIndex < 0) {
362
+ return null;
363
+ }
364
+
365
+ tokenOrComment = tokens[previousIndex];
366
+ }
367
+ return tokenOrComment;
368
+ }
369
+
370
+ /**
371
+ * Gets the token after the given node or token, skipping any comments unless includeComments is true.
372
+ * @param {AnyNode|Token} nodeOrToken The node or token to get the next token for.
373
+ * @param {Object} [options] Options object.
374
+ * @param {boolean} [options.includeComments=false] If true, return comments when they are present.
375
+ * @returns {Token|null} The next token or comment, or null if there is none.
376
+ */
377
+ getTokenAfter(nodeOrToken, { includeComments = false } = {}) {
378
+ const index = this.#tokenEnds.get(nodeOrToken.range[1]);
379
+
380
+ if (index === undefined) {
381
+ return null;
382
+ }
383
+
384
+ let nextIndex = index + 1;
385
+ const tokens = this.ast.tokens;
386
+ if (nextIndex >= tokens.length) {
387
+ return null;
388
+ }
389
+
390
+ let tokenOrComment = tokens[nextIndex];
391
+
392
+ if (includeComments) {
393
+ return tokenOrComment;
394
+ }
395
+
396
+ // skip comments
397
+ while (tokenOrComment?.type.endsWith("Comment")) {
398
+ nextIndex++;
399
+
400
+ if (nextIndex >= tokens.length) {
401
+ return null;
402
+ }
403
+
404
+ tokenOrComment = tokens[nextIndex];
405
+ }
406
+
407
+ return tokenOrComment;
408
+ }
282
409
  }
283
410
 
284
411
  /**
285
- * @filedescription Functions to fix up rules to provide missing methods on the `context` object.
412
+ * @fileoverview The JSONLanguage class.
286
413
  * @author Nicholas C. Zakas
287
414
  */
288
415
 
@@ -443,6 +570,13 @@ class JSONLanguage {
443
570
  /* eslint-enable class-methods-use-this -- Required to complete interface. */
444
571
  }
445
572
 
573
+ const rules$1 = /** @type {const} */ ({
574
+ "json/no-duplicate-keys": "error",
575
+ "json/no-empty-keys": "error",
576
+ "json/no-unnormalized-keys": "error",
577
+ "json/no-unsafe-values": "error"
578
+ });
579
+
446
580
  /**
447
581
  * @fileoverview Rule to prevent duplicate keys in JSON.
448
582
  * @author Nicholas C. Zakas
@@ -468,7 +602,9 @@ const rule$5 = {
468
602
  type: "problem",
469
603
 
470
604
  docs: {
605
+ recommended: true,
471
606
  description: "Disallow duplicate keys in JSON objects",
607
+ url: "https://github.com/eslint/json/tree/main/docs/rules/no-duplicate-keys.md",
472
608
  },
473
609
 
474
610
  messages: {
@@ -539,7 +675,9 @@ const rule$4 = {
539
675
  type: "problem",
540
676
 
541
677
  docs: {
678
+ recommended: true,
542
679
  description: "Disallow empty keys in JSON objects",
680
+ url: "https://github.com/eslint/json/tree/main/docs/rules/no-empty-keys.md",
543
681
  },
544
682
 
545
683
  messages: {
@@ -566,6 +704,80 @@ const rule$4 = {
566
704
  },
567
705
  };
568
706
 
707
+ /**
708
+ * @fileoverview Rule to detect unnormalized keys in JSON.
709
+ * @author Bradley Meck Farias
710
+ */
711
+
712
+ //-----------------------------------------------------------------------------
713
+ // Type Definitions
714
+ //-----------------------------------------------------------------------------
715
+
716
+ /**
717
+ *
718
+ * @typedef {"unnormalizedKey"} NoUnnormalizedKeysMessageIds
719
+ * @typedef {{ form: string }} NoUnnormalizedKeysOptions
720
+ * @typedef {JSONRuleDefinition<{ RuleOptions: [NoUnnormalizedKeysOptions], MessageIds: NoUnnormalizedKeysMessageIds }>} NoUnnormalizedKeysRuleDefinition
721
+ */
722
+
723
+ //-----------------------------------------------------------------------------
724
+ // Rule Definition
725
+ //-----------------------------------------------------------------------------
726
+
727
+ /** @type {NoUnnormalizedKeysRuleDefinition} */
728
+ const rule$3 = {
729
+ meta: {
730
+ type: "problem",
731
+
732
+ docs: {
733
+ recommended: true,
734
+ description: "Disallow JSON keys that are not normalized",
735
+ url: "https://github.com/eslint/json/tree/main/docs/rules/no-unnormalized-keys.md",
736
+ },
737
+
738
+ messages: {
739
+ unnormalizedKey: "Unnormalized key '{{key}}' found.",
740
+ },
741
+
742
+ schema: [
743
+ {
744
+ type: "object",
745
+ properties: {
746
+ form: {
747
+ enum: ["NFC", "NFD", "NFKC", "NFKD"],
748
+ },
749
+ },
750
+ additionalProperties: false,
751
+ },
752
+ ],
753
+ },
754
+
755
+ create(context) {
756
+ const form = context.options.length
757
+ ? context.options[0].form
758
+ : undefined;
759
+
760
+ return {
761
+ Member(node) {
762
+ const key =
763
+ node.name.type === "String"
764
+ ? node.name.value
765
+ : node.name.name;
766
+
767
+ if (key.normalize(form) !== key) {
768
+ context.report({
769
+ loc: node.name.loc,
770
+ messageId: "unnormalizedKey",
771
+ data: {
772
+ key,
773
+ },
774
+ });
775
+ }
776
+ },
777
+ };
778
+ },
779
+ };
780
+
569
781
  /**
570
782
  * @fileoverview Rule to detect unsafe values in JSON.
571
783
  * @author Bradley Meck Farias
@@ -601,12 +813,14 @@ const NON_ZERO = /[1-9]/u;
601
813
  //-----------------------------------------------------------------------------
602
814
 
603
815
  /** @type {NoUnsafeValuesRuleDefinition} */
604
- const rule$3 = {
816
+ const rule$2 = {
605
817
  meta: {
606
818
  type: "problem",
607
819
 
608
820
  docs: {
821
+ recommended: true,
609
822
  description: "Disallow JSON values that are unsafe for interchange",
823
+ url: "https://github.com/eslint/json/tree/main/docs/rules/no-unsafe-values.md",
610
824
  },
611
825
 
612
826
  messages: {
@@ -717,78 +931,6 @@ const rule$3 = {
717
931
  },
718
932
  };
719
933
 
720
- /**
721
- * @fileoverview Rule to detect unnormalized keys in JSON.
722
- * @author Bradley Meck Farias
723
- */
724
-
725
- //-----------------------------------------------------------------------------
726
- // Type Definitions
727
- //-----------------------------------------------------------------------------
728
-
729
- /**
730
- *
731
- * @typedef {"unnormalizedKey"} NoUnnormalizedKeysMessageIds
732
- * @typedef {{ form: string }} NoUnnormalizedKeysOptions
733
- * @typedef {JSONRuleDefinition<{ RuleOptions: [NoUnnormalizedKeysOptions], MessageIds: NoUnnormalizedKeysMessageIds }>} NoUnnormalizedKeysRuleDefinition
734
- */
735
-
736
- //-----------------------------------------------------------------------------
737
- // Rule Definition
738
- //-----------------------------------------------------------------------------
739
-
740
- /** @type {NoUnnormalizedKeysRuleDefinition} */
741
- const rule$2 = {
742
- meta: {
743
- type: "problem",
744
-
745
- docs: {
746
- description: "Disallow JSON keys that are not normalized",
747
- },
748
-
749
- messages: {
750
- unnormalizedKey: "Unnormalized key '{{key}}' found.",
751
- },
752
-
753
- schema: [
754
- {
755
- type: "object",
756
- properties: {
757
- form: {
758
- enum: ["NFC", "NFD", "NFKC", "NFKD"],
759
- },
760
- },
761
- additionalProperties: false,
762
- },
763
- ],
764
- },
765
-
766
- create(context) {
767
- const form = context.options.length
768
- ? context.options[0].form
769
- : undefined;
770
-
771
- return {
772
- Member(node) {
773
- const key =
774
- node.name.type === "String"
775
- ? node.name.value
776
- : node.name.name;
777
-
778
- if (key.normalize(form) !== key) {
779
- context.report({
780
- loc: node.name.loc,
781
- messageId: "unnormalizedKey",
782
- data: {
783
- key,
784
- },
785
- });
786
- }
787
- },
788
- };
789
- },
790
- };
791
-
792
934
  /**
793
935
  * @fileoverview Rule to require JSON object keys to be sorted.
794
936
  * Copied largely from https://github.com/eslint/eslint/blob/main/lib/rules/sort-keys.js
@@ -891,7 +1033,9 @@ const rule$1 = {
891
1033
  ],
892
1034
 
893
1035
  docs: {
1036
+ recommended: false,
894
1037
  description: `Require JSON object keys to be sorted`,
1038
+ url: "https://github.com/eslint/json/tree/main/docs/rules/sort-keys.md",
895
1039
  },
896
1040
 
897
1041
  messages: {
@@ -1020,7 +1164,7 @@ const rule$1 = {
1020
1164
  };
1021
1165
 
1022
1166
  /**
1023
- * @fileoverview Rule to ensure top-level items are either an array or ojbect.
1167
+ * @fileoverview Rule to ensure top-level items are either an array or object.
1024
1168
  * @author Joe Hildebrand
1025
1169
  */
1026
1170
 
@@ -1044,8 +1188,10 @@ const rule = {
1044
1188
  type: "problem",
1045
1189
 
1046
1190
  docs: {
1191
+ recommended: false,
1047
1192
  description:
1048
1193
  "Require the JSON top-level value to be an array or object",
1194
+ url: "https://github.com/eslint/json/tree/main/docs/rules/top-level-interop.md",
1049
1195
  },
1050
1196
 
1051
1197
  messages: {
@@ -1070,6 +1216,15 @@ const rule = {
1070
1216
  },
1071
1217
  };
1072
1218
 
1219
+ var rules = {
1220
+ "no-duplicate-keys": rule$5,
1221
+ "no-empty-keys": rule$4,
1222
+ "no-unnormalized-keys": rule$3,
1223
+ "no-unsafe-values": rule$2,
1224
+ "sort-keys": rule$1,
1225
+ "top-level-interop": rule,
1226
+ };
1227
+
1073
1228
  /**
1074
1229
  * @fileoverview JSON plugin.
1075
1230
  * @author Nicholas C. Zakas
@@ -1083,30 +1238,18 @@ const rule = {
1083
1238
  const plugin = {
1084
1239
  meta: {
1085
1240
  name: "@eslint/json",
1086
- version: "0.12.0", // x-release-please-version
1241
+ version: "0.13.0", // x-release-please-version
1087
1242
  },
1088
1243
  languages: {
1089
1244
  json: new JSONLanguage({ mode: "json" }),
1090
1245
  jsonc: new JSONLanguage({ mode: "jsonc" }),
1091
1246
  json5: new JSONLanguage({ mode: "json5" }),
1092
1247
  },
1093
- rules: {
1094
- "no-duplicate-keys": rule$5,
1095
- "no-empty-keys": rule$4,
1096
- "no-unsafe-values": rule$3,
1097
- "no-unnormalized-keys": rule$2,
1098
- "sort-keys": rule$1,
1099
- "top-level-interop": rule,
1100
- },
1248
+ rules,
1101
1249
  configs: {
1102
1250
  recommended: {
1103
1251
  plugins: {},
1104
- rules: /** @type {const} */ ({
1105
- "json/no-duplicate-keys": "error",
1106
- "json/no-empty-keys": "error",
1107
- "json/no-unsafe-values": "error",
1108
- "json/no-unnormalized-keys": "error",
1109
- }),
1252
+ rules: rules$1,
1110
1253
  },
1111
1254
  },
1112
1255
  };