@eslint/json 0.12.0 → 0.13.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.
@@ -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
  /**
@@ -151,7 +196,9 @@ class JSONSourceCode extends pluginKit.TextSourceCodeBase {
151
196
  * that ESLint needs to further process the directives.
152
197
  */
153
198
  getDisableDirectives() {
199
+ /** @type {Array<FileProblem>} */
154
200
  const problems = [];
201
+ /** @type {Array<Directive>} */
155
202
  const directives = [];
156
203
 
157
204
  this.getInlineConfigNodes().forEach(comment => {
@@ -204,7 +251,9 @@ class JSONSourceCode extends pluginKit.TextSourceCodeBase {
204
251
  * that ESLint needs to further process the rule configurations.
205
252
  */
206
253
  applyInlineConfig() {
254
+ /** @type {Array<FileProblem>} */
207
255
  const problems = [];
256
+ /** @type {Array<{config:{rules:RulesConfig},loc:SourceLocation}>} */
208
257
  const configs = [];
209
258
 
210
259
  this.getInlineConfigNodes().forEach(comment => {
@@ -243,8 +292,8 @@ class JSONSourceCode extends pluginKit.TextSourceCodeBase {
243
292
 
244
293
  /**
245
294
  * 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.
295
+ * @param {AnyNode} node The node to get the parent of.
296
+ * @returns {AnyNode|undefined} The parent of the node.
248
297
  */
249
298
  getParent(node) {
250
299
  return this.#parents.get(node);
@@ -265,12 +314,15 @@ class JSONSourceCode extends pluginKit.TextSourceCodeBase {
265
314
 
266
315
  for (const { node, parent, phase } of momoa.iterator(this.ast)) {
267
316
  if (parent) {
268
- this.#parents.set(node, parent);
317
+ this.#parents.set(
318
+ /** @type {AnyNode} */ (node),
319
+ /** @type {AnyNode} */ (parent),
320
+ );
269
321
  }
270
322
 
271
323
  steps.push(
272
324
  new JSONTraversalStep({
273
- target: node,
325
+ target: /** @type {AnyNode} */ (node),
274
326
  phase: phase === "enter" ? 1 : 2,
275
327
  args: [node, parent],
276
328
  }),
@@ -279,10 +331,89 @@ class JSONSourceCode extends pluginKit.TextSourceCodeBase {
279
331
 
280
332
  return steps;
281
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
+ }
282
413
  }
283
414
 
284
415
  /**
285
- * @filedescription Functions to fix up rules to provide missing methods on the `context` object.
416
+ * @fileoverview The JSONLanguage class.
286
417
  * @author Nicholas C. Zakas
287
418
  */
288
419
 
@@ -443,6 +574,13 @@ class JSONLanguage {
443
574
  /* eslint-enable class-methods-use-this -- Required to complete interface. */
444
575
  }
445
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
+
446
584
  /**
447
585
  * @fileoverview Rule to prevent duplicate keys in JSON.
448
586
  * @author Nicholas C. Zakas
@@ -468,7 +606,9 @@ const rule$5 = {
468
606
  type: "problem",
469
607
 
470
608
  docs: {
609
+ recommended: true,
471
610
  description: "Disallow duplicate keys in JSON objects",
611
+ url: "https://github.com/eslint/json/tree/main/docs/rules/no-duplicate-keys.md",
472
612
  },
473
613
 
474
614
  messages: {
@@ -539,7 +679,9 @@ const rule$4 = {
539
679
  type: "problem",
540
680
 
541
681
  docs: {
682
+ recommended: true,
542
683
  description: "Disallow empty keys in JSON objects",
684
+ url: "https://github.com/eslint/json/tree/main/docs/rules/no-empty-keys.md",
543
685
  },
544
686
 
545
687
  messages: {
@@ -566,6 +708,80 @@ const rule$4 = {
566
708
  },
567
709
  };
568
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
+
759
+ create(context) {
760
+ const form = context.options.length
761
+ ? context.options[0].form
762
+ : undefined;
763
+
764
+ return {
765
+ Member(node) {
766
+ const key =
767
+ node.name.type === "String"
768
+ ? node.name.value
769
+ : node.name.name;
770
+
771
+ if (key.normalize(form) !== key) {
772
+ context.report({
773
+ loc: node.name.loc,
774
+ messageId: "unnormalizedKey",
775
+ data: {
776
+ key,
777
+ },
778
+ });
779
+ }
780
+ },
781
+ };
782
+ },
783
+ };
784
+
569
785
  /**
570
786
  * @fileoverview Rule to detect unsafe values in JSON.
571
787
  * @author Bradley Meck Farias
@@ -601,12 +817,14 @@ const NON_ZERO = /[1-9]/u;
601
817
  //-----------------------------------------------------------------------------
602
818
 
603
819
  /** @type {NoUnsafeValuesRuleDefinition} */
604
- const rule$3 = {
820
+ const rule$2 = {
605
821
  meta: {
606
822
  type: "problem",
607
823
 
608
824
  docs: {
825
+ recommended: true,
609
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",
610
828
  },
611
829
 
612
830
  messages: {
@@ -717,78 +935,6 @@ const rule$3 = {
717
935
  },
718
936
  };
719
937
 
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
938
  /**
793
939
  * @fileoverview Rule to require JSON object keys to be sorted.
794
940
  * Copied largely from https://github.com/eslint/eslint/blob/main/lib/rules/sort-keys.js
@@ -891,7 +1037,9 @@ const rule$1 = {
891
1037
  ],
892
1038
 
893
1039
  docs: {
1040
+ recommended: false,
894
1041
  description: `Require JSON object keys to be sorted`,
1042
+ url: "https://github.com/eslint/json/tree/main/docs/rules/sort-keys.md",
895
1043
  },
896
1044
 
897
1045
  messages: {
@@ -1020,7 +1168,7 @@ const rule$1 = {
1020
1168
  };
1021
1169
 
1022
1170
  /**
1023
- * @fileoverview Rule to ensure top-level items are either an array or ojbect.
1171
+ * @fileoverview Rule to ensure top-level items are either an array or object.
1024
1172
  * @author Joe Hildebrand
1025
1173
  */
1026
1174
 
@@ -1044,8 +1192,10 @@ const rule = {
1044
1192
  type: "problem",
1045
1193
 
1046
1194
  docs: {
1195
+ recommended: false,
1047
1196
  description:
1048
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",
1049
1199
  },
1050
1200
 
1051
1201
  messages: {
@@ -1070,6 +1220,15 @@ const rule = {
1070
1220
  },
1071
1221
  };
1072
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
+
1073
1232
  /**
1074
1233
  * @fileoverview JSON plugin.
1075
1234
  * @author Nicholas C. Zakas
@@ -1083,30 +1242,18 @@ const rule = {
1083
1242
  const plugin = {
1084
1243
  meta: {
1085
1244
  name: "@eslint/json",
1086
- version: "0.12.0", // x-release-please-version
1245
+ version: "0.13.1", // x-release-please-version
1087
1246
  },
1088
1247
  languages: {
1089
1248
  json: new JSONLanguage({ mode: "json" }),
1090
1249
  jsonc: new JSONLanguage({ mode: "jsonc" }),
1091
1250
  json5: new JSONLanguage({ mode: "json5" }),
1092
1251
  },
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
- },
1252
+ rules,
1101
1253
  configs: {
1102
1254
  recommended: {
1103
1255
  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
- }),
1256
+ rules: rules$1,
1110
1257
  },
1111
1258
  },
1112
1259
  };