@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.
package/dist/esm/index.js CHANGED
@@ -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
  // @ts-self-types="./index.d.ts"
@@ -37,14 +37,14 @@ const INLINE_CONFIG =
37
37
  class JSONTraversalStep extends VisitNodeStep {
38
38
  /**
39
39
  * The target of the step.
40
- * @type {Node}
40
+ * @type {AnyNode}
41
41
  */
42
42
  target = undefined;
43
43
 
44
44
  /**
45
45
  * Creates a new instance.
46
46
  * @param {Object} options The options for the step.
47
- * @param {Node} options.target The target of the step.
47
+ * @param {AnyNode} options.target The target of the step.
48
48
  * @param {1|2} options.phase The phase of the step.
49
49
  * @param {Array<any>} options.args The arguments of the step.
50
50
  */
@@ -55,13 +55,44 @@ class JSONTraversalStep extends VisitNodeStep {
55
55
  }
56
56
  }
57
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
+
58
89
  //-----------------------------------------------------------------------------
59
90
  // Exports
60
91
  //-----------------------------------------------------------------------------
61
92
 
62
93
  /**
63
94
  * JSON Source Code Object
64
- * @implements {TextSourceCode<{LangOptions: JSONLanguageOptions, RootNode: DocumentNode, SyntaxElementWithLoc: JSONSyntaxElement, ConfigNode: Token}>}
95
+ * @extends {TextSourceCodeBase<{LangOptions: JSONLanguageOptions, RootNode: DocumentNode, SyntaxElementWithLoc: JSONSyntaxElement, ConfigNode: Token}>}
65
96
  */
66
97
  class JSONSourceCode extends TextSourceCodeBase {
67
98
  /**
@@ -72,7 +103,7 @@ class JSONSourceCode extends TextSourceCodeBase {
72
103
 
73
104
  /**
74
105
  * Cache of parent nodes.
75
- * @type {WeakMap<Node, Node>}
106
+ * @type {WeakMap<AnyNode, AnyNode>}
76
107
  */
77
108
  #parents = new WeakMap();
78
109
 
@@ -89,11 +120,23 @@ class JSONSourceCode extends TextSourceCodeBase {
89
120
  ast = undefined;
90
121
 
91
122
  /**
92
- * The comment node in the source code.
123
+ * The comment tokens in the source code.
93
124
  * @type {Array<Token>|undefined}
94
125
  */
95
126
  comments;
96
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
+
97
140
  /**
98
141
  * Creates a new instance.
99
142
  * @param {Object} options The options for the instance.
@@ -103,9 +146,11 @@ class JSONSourceCode extends TextSourceCodeBase {
103
146
  constructor({ text, ast }) {
104
147
  super({ text, ast });
105
148
  this.ast = ast;
106
- this.comments = ast.tokens
107
- ? ast.tokens.filter(token => token.type.endsWith("Comment"))
108
- : [];
149
+
150
+ const { comments, starts, ends } = processTokens(this.ast.tokens ?? []);
151
+ this.comments = comments;
152
+ this.#tokenStarts = starts;
153
+ this.#tokenEnds = ends;
109
154
  }
110
155
 
111
156
  /**
@@ -240,8 +285,8 @@ class JSONSourceCode extends TextSourceCodeBase {
240
285
 
241
286
  /**
242
287
  * Returns the parent of the given node.
243
- * @param {Node} node The node to get the parent of.
244
- * @returns {Node|undefined} The parent of the node.
288
+ * @param {AnyNode} node The node to get the parent of.
289
+ * @returns {AnyNode|undefined} The parent of the node.
245
290
  */
246
291
  getParent(node) {
247
292
  return this.#parents.get(node);
@@ -262,12 +307,15 @@ class JSONSourceCode extends TextSourceCodeBase {
262
307
 
263
308
  for (const { node, parent, phase } of iterator(this.ast)) {
264
309
  if (parent) {
265
- this.#parents.set(node, parent);
310
+ this.#parents.set(
311
+ /** @type {AnyNode} */ (node),
312
+ /** @type {AnyNode} */ (parent),
313
+ );
266
314
  }
267
315
 
268
316
  steps.push(
269
317
  new JSONTraversalStep({
270
- target: node,
318
+ target: /** @type {AnyNode} */ (node),
271
319
  phase: phase === "enter" ? 1 : 2,
272
320
  args: [node, parent],
273
321
  }),
@@ -276,10 +324,89 @@ class JSONSourceCode extends TextSourceCodeBase {
276
324
 
277
325
  return steps;
278
326
  }
327
+
328
+ /**
329
+ * Gets the token before the given node or token, optionally including comments.
330
+ * @param {AnyNode|Token} nodeOrToken The node or token to get the previous token for.
331
+ * @param {Object} [options] Options object.
332
+ * @param {boolean} [options.includeComments] If true, return comments when they are present.
333
+ * @returns {Token|null} The previous token or comment, or null if there is none.
334
+ */
335
+ getTokenBefore(nodeOrToken, { includeComments = false } = {}) {
336
+ const index = this.#tokenStarts.get(nodeOrToken.range[0]);
337
+
338
+ if (index === undefined) {
339
+ return null;
340
+ }
341
+
342
+ let previousIndex = index - 1;
343
+ if (previousIndex < 0) {
344
+ return null;
345
+ }
346
+
347
+ const tokens = this.ast.tokens;
348
+ let tokenOrComment = tokens[previousIndex];
349
+
350
+ if (includeComments) {
351
+ return tokenOrComment;
352
+ }
353
+
354
+ // skip comments
355
+ while (tokenOrComment?.type.endsWith("Comment")) {
356
+ previousIndex--;
357
+
358
+ if (previousIndex < 0) {
359
+ return null;
360
+ }
361
+
362
+ tokenOrComment = tokens[previousIndex];
363
+ }
364
+ return tokenOrComment;
365
+ }
366
+
367
+ /**
368
+ * Gets the token after the given node or token, skipping any comments unless includeComments is true.
369
+ * @param {AnyNode|Token} nodeOrToken The node or token to get the next token for.
370
+ * @param {Object} [options] Options object.
371
+ * @param {boolean} [options.includeComments=false] If true, return comments when they are present.
372
+ * @returns {Token|null} The next token or comment, or null if there is none.
373
+ */
374
+ getTokenAfter(nodeOrToken, { includeComments = false } = {}) {
375
+ const index = this.#tokenEnds.get(nodeOrToken.range[1]);
376
+
377
+ if (index === undefined) {
378
+ return null;
379
+ }
380
+
381
+ let nextIndex = index + 1;
382
+ const tokens = this.ast.tokens;
383
+ if (nextIndex >= tokens.length) {
384
+ return null;
385
+ }
386
+
387
+ let tokenOrComment = tokens[nextIndex];
388
+
389
+ if (includeComments) {
390
+ return tokenOrComment;
391
+ }
392
+
393
+ // skip comments
394
+ while (tokenOrComment?.type.endsWith("Comment")) {
395
+ nextIndex++;
396
+
397
+ if (nextIndex >= tokens.length) {
398
+ return null;
399
+ }
400
+
401
+ tokenOrComment = tokens[nextIndex];
402
+ }
403
+
404
+ return tokenOrComment;
405
+ }
279
406
  }
280
407
 
281
408
  /**
282
- * @filedescription Functions to fix up rules to provide missing methods on the `context` object.
409
+ * @fileoverview The JSONLanguage class.
283
410
  * @author Nicholas C. Zakas
284
411
  */
285
412
 
@@ -440,6 +567,13 @@ class JSONLanguage {
440
567
  /* eslint-enable class-methods-use-this -- Required to complete interface. */
441
568
  }
442
569
 
570
+ const rules$1 = /** @type {const} */ ({
571
+ "json/no-duplicate-keys": "error",
572
+ "json/no-empty-keys": "error",
573
+ "json/no-unnormalized-keys": "error",
574
+ "json/no-unsafe-values": "error"
575
+ });
576
+
443
577
  /**
444
578
  * @fileoverview Rule to prevent duplicate keys in JSON.
445
579
  * @author Nicholas C. Zakas
@@ -465,7 +599,9 @@ const rule$5 = {
465
599
  type: "problem",
466
600
 
467
601
  docs: {
602
+ recommended: true,
468
603
  description: "Disallow duplicate keys in JSON objects",
604
+ url: "https://github.com/eslint/json/tree/main/docs/rules/no-duplicate-keys.md",
469
605
  },
470
606
 
471
607
  messages: {
@@ -536,7 +672,9 @@ const rule$4 = {
536
672
  type: "problem",
537
673
 
538
674
  docs: {
675
+ recommended: true,
539
676
  description: "Disallow empty keys in JSON objects",
677
+ url: "https://github.com/eslint/json/tree/main/docs/rules/no-empty-keys.md",
540
678
  },
541
679
 
542
680
  messages: {
@@ -563,6 +701,80 @@ const rule$4 = {
563
701
  },
564
702
  };
565
703
 
704
+ /**
705
+ * @fileoverview Rule to detect unnormalized keys in JSON.
706
+ * @author Bradley Meck Farias
707
+ */
708
+
709
+ //-----------------------------------------------------------------------------
710
+ // Type Definitions
711
+ //-----------------------------------------------------------------------------
712
+
713
+ /**
714
+ *
715
+ * @typedef {"unnormalizedKey"} NoUnnormalizedKeysMessageIds
716
+ * @typedef {{ form: string }} NoUnnormalizedKeysOptions
717
+ * @typedef {JSONRuleDefinition<{ RuleOptions: [NoUnnormalizedKeysOptions], MessageIds: NoUnnormalizedKeysMessageIds }>} NoUnnormalizedKeysRuleDefinition
718
+ */
719
+
720
+ //-----------------------------------------------------------------------------
721
+ // Rule Definition
722
+ //-----------------------------------------------------------------------------
723
+
724
+ /** @type {NoUnnormalizedKeysRuleDefinition} */
725
+ const rule$3 = {
726
+ meta: {
727
+ type: "problem",
728
+
729
+ docs: {
730
+ recommended: true,
731
+ description: "Disallow JSON keys that are not normalized",
732
+ url: "https://github.com/eslint/json/tree/main/docs/rules/no-unnormalized-keys.md",
733
+ },
734
+
735
+ messages: {
736
+ unnormalizedKey: "Unnormalized key '{{key}}' found.",
737
+ },
738
+
739
+ schema: [
740
+ {
741
+ type: "object",
742
+ properties: {
743
+ form: {
744
+ enum: ["NFC", "NFD", "NFKC", "NFKD"],
745
+ },
746
+ },
747
+ additionalProperties: false,
748
+ },
749
+ ],
750
+ },
751
+
752
+ create(context) {
753
+ const form = context.options.length
754
+ ? context.options[0].form
755
+ : undefined;
756
+
757
+ return {
758
+ Member(node) {
759
+ const key =
760
+ node.name.type === "String"
761
+ ? node.name.value
762
+ : node.name.name;
763
+
764
+ if (key.normalize(form) !== key) {
765
+ context.report({
766
+ loc: node.name.loc,
767
+ messageId: "unnormalizedKey",
768
+ data: {
769
+ key,
770
+ },
771
+ });
772
+ }
773
+ },
774
+ };
775
+ },
776
+ };
777
+
566
778
  /**
567
779
  * @fileoverview Rule to detect unsafe values in JSON.
568
780
  * @author Bradley Meck Farias
@@ -598,12 +810,14 @@ const NON_ZERO = /[1-9]/u;
598
810
  //-----------------------------------------------------------------------------
599
811
 
600
812
  /** @type {NoUnsafeValuesRuleDefinition} */
601
- const rule$3 = {
813
+ const rule$2 = {
602
814
  meta: {
603
815
  type: "problem",
604
816
 
605
817
  docs: {
818
+ recommended: true,
606
819
  description: "Disallow JSON values that are unsafe for interchange",
820
+ url: "https://github.com/eslint/json/tree/main/docs/rules/no-unsafe-values.md",
607
821
  },
608
822
 
609
823
  messages: {
@@ -714,78 +928,6 @@ const rule$3 = {
714
928
  },
715
929
  };
716
930
 
717
- /**
718
- * @fileoverview Rule to detect unnormalized keys in JSON.
719
- * @author Bradley Meck Farias
720
- */
721
-
722
- //-----------------------------------------------------------------------------
723
- // Type Definitions
724
- //-----------------------------------------------------------------------------
725
-
726
- /**
727
- *
728
- * @typedef {"unnormalizedKey"} NoUnnormalizedKeysMessageIds
729
- * @typedef {{ form: string }} NoUnnormalizedKeysOptions
730
- * @typedef {JSONRuleDefinition<{ RuleOptions: [NoUnnormalizedKeysOptions], MessageIds: NoUnnormalizedKeysMessageIds }>} NoUnnormalizedKeysRuleDefinition
731
- */
732
-
733
- //-----------------------------------------------------------------------------
734
- // Rule Definition
735
- //-----------------------------------------------------------------------------
736
-
737
- /** @type {NoUnnormalizedKeysRuleDefinition} */
738
- const rule$2 = {
739
- meta: {
740
- type: "problem",
741
-
742
- docs: {
743
- description: "Disallow JSON keys that are not normalized",
744
- },
745
-
746
- messages: {
747
- unnormalizedKey: "Unnormalized key '{{key}}' found.",
748
- },
749
-
750
- schema: [
751
- {
752
- type: "object",
753
- properties: {
754
- form: {
755
- enum: ["NFC", "NFD", "NFKC", "NFKD"],
756
- },
757
- },
758
- additionalProperties: false,
759
- },
760
- ],
761
- },
762
-
763
- create(context) {
764
- const form = context.options.length
765
- ? context.options[0].form
766
- : undefined;
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
931
  /**
790
932
  * @fileoverview Rule to require JSON object keys to be sorted.
791
933
  * Copied largely from https://github.com/eslint/eslint/blob/main/lib/rules/sort-keys.js
@@ -888,7 +1030,9 @@ const rule$1 = {
888
1030
  ],
889
1031
 
890
1032
  docs: {
1033
+ recommended: false,
891
1034
  description: `Require JSON object keys to be sorted`,
1035
+ url: "https://github.com/eslint/json/tree/main/docs/rules/sort-keys.md",
892
1036
  },
893
1037
 
894
1038
  messages: {
@@ -1017,7 +1161,7 @@ const rule$1 = {
1017
1161
  };
1018
1162
 
1019
1163
  /**
1020
- * @fileoverview Rule to ensure top-level items are either an array or ojbect.
1164
+ * @fileoverview Rule to ensure top-level items are either an array or object.
1021
1165
  * @author Joe Hildebrand
1022
1166
  */
1023
1167
 
@@ -1041,8 +1185,10 @@ const rule = {
1041
1185
  type: "problem",
1042
1186
 
1043
1187
  docs: {
1188
+ recommended: false,
1044
1189
  description:
1045
1190
  "Require the JSON top-level value to be an array or object",
1191
+ url: "https://github.com/eslint/json/tree/main/docs/rules/top-level-interop.md",
1046
1192
  },
1047
1193
 
1048
1194
  messages: {
@@ -1067,6 +1213,15 @@ const rule = {
1067
1213
  },
1068
1214
  };
1069
1215
 
1216
+ var rules = {
1217
+ "no-duplicate-keys": rule$5,
1218
+ "no-empty-keys": rule$4,
1219
+ "no-unnormalized-keys": rule$3,
1220
+ "no-unsafe-values": rule$2,
1221
+ "sort-keys": rule$1,
1222
+ "top-level-interop": rule,
1223
+ };
1224
+
1070
1225
  /**
1071
1226
  * @fileoverview JSON plugin.
1072
1227
  * @author Nicholas C. Zakas
@@ -1080,30 +1235,18 @@ const rule = {
1080
1235
  const plugin = {
1081
1236
  meta: {
1082
1237
  name: "@eslint/json",
1083
- version: "0.12.0", // x-release-please-version
1238
+ version: "0.13.0", // x-release-please-version
1084
1239
  },
1085
1240
  languages: {
1086
1241
  json: new JSONLanguage({ mode: "json" }),
1087
1242
  jsonc: new JSONLanguage({ mode: "jsonc" }),
1088
1243
  json5: new JSONLanguage({ mode: "json5" }),
1089
1244
  },
1090
- rules: {
1091
- "no-duplicate-keys": rule$5,
1092
- "no-empty-keys": rule$4,
1093
- "no-unsafe-values": rule$3,
1094
- "no-unnormalized-keys": rule$2,
1095
- "sort-keys": rule$1,
1096
- "top-level-interop": rule,
1097
- },
1245
+ rules,
1098
1246
  configs: {
1099
1247
  recommended: {
1100
1248
  plugins: {},
1101
- rules: /** @type {const} */ ({
1102
- "json/no-duplicate-keys": "error",
1103
- "json/no-empty-keys": "error",
1104
- "json/no-unsafe-values": "error",
1105
- "json/no-unnormalized-keys": "error",
1106
- }),
1249
+ rules: rules$1,
1107
1250
  },
1108
1251
  },
1109
1252
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eslint/json",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "JSON linting plugin for ESLint",
5
5
  "author": "Nicholas C. Zakas",
6
6
  "type": "module",
@@ -40,7 +40,14 @@
40
40
  "eslint --fix",
41
41
  "prettier --write"
42
42
  ],
43
- "!(*.js)": "prettier --write --ignore-unknown"
43
+ "!(*.js)": "prettier --write --ignore-unknown",
44
+ "README.md": [
45
+ "npm run build:update-rules-docs"
46
+ ],
47
+ "{src/rules/*.js,tools/update-rules-docs.js}": [
48
+ "npm run build:update-rules-docs",
49
+ "git add README.md"
50
+ ]
44
51
  },
45
52
  "repository": {
46
53
  "type": "git",
@@ -53,14 +60,17 @@
53
60
  "scripts": {
54
61
  "build:dedupe-types": "node tools/dedupe-types.js dist/cjs/index.cjs dist/esm/index.js",
55
62
  "build:cts": "node tools/build-cts.js",
56
- "build": "rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && npm run build:cts",
63
+ "build:rules": "node tools/build-rules.js",
64
+ "build": "npm run build:rules && rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && npm run build:cts",
57
65
  "build:readme": "node tools/update-readme.js",
58
- "test:jsr": "npx jsr@latest publish --dry-run",
66
+ "build:update-rules-docs": "node tools/update-rules-docs.js",
67
+ "prepare": "npm run build",
59
68
  "pretest": "npm run build",
60
69
  "lint": "eslint",
61
70
  "fmt": "prettier --write .",
62
71
  "fmt:check": "prettier --check .",
63
72
  "test": "mocha tests/**/*.js",
73
+ "test:jsr": "npx jsr@latest publish --dry-run",
64
74
  "test:coverage": "c8 npm test",
65
75
  "test:types": "tsc -p tests/types/tsconfig.json"
66
76
  },
@@ -73,25 +83,26 @@
73
83
  ],
74
84
  "license": "Apache-2.0",
75
85
  "dependencies": {
76
- "@eslint/core": "^0.12.0",
77
- "@eslint/plugin-kit": "^0.2.7",
86
+ "@eslint/core": "^0.14.0",
87
+ "@eslint/plugin-kit": "^0.3.1",
78
88
  "@humanwhocodes/momoa": "^3.3.4",
79
89
  "natural-compare": "^1.4.0"
80
90
  },
81
91
  "devDependencies": {
82
- "c8": "^9.1.0",
92
+ "c8": "^10.1.3",
83
93
  "dedent": "^1.5.3",
84
- "eslint": "^9.23.0",
94
+ "eslint": "^9.25.1",
85
95
  "eslint-config-eslint": "^11.0.0",
86
96
  "eslint-plugin-eslint-plugin": "^6.3.2",
87
97
  "got": "^14.4.2",
88
98
  "lint-staged": "^15.2.7",
89
- "mocha": "^10.4.0",
99
+ "mdast-util-from-markdown": "^2.0.2",
100
+ "mocha": "^11.3.0",
90
101
  "prettier": "^3.4.1",
91
- "rollup": "^4.16.2",
102
+ "rollup": "^4.41.0",
92
103
  "rollup-plugin-copy": "^3.5.0",
93
104
  "rollup-plugin-delete": "^3.0.1",
94
- "typescript": "^5.4.5",
105
+ "typescript": "^5.8.3",
95
106
  "yorkie": "^2.0.0"
96
107
  },
97
108
  "engines": {