@eslint/json 0.7.0 → 0.9.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/README.md CHANGED
@@ -30,9 +30,9 @@ deno add @eslint/json
30
30
 
31
31
  This package exports these languages:
32
32
 
33
- - `"json/json"` is for regular JSON files
34
- - `"json/jsonc"` is for JSON files that support comments ([JSONC](https://github.com/microsoft/node-jsonc-parser)) such as those used for Visual Studio Code configuration files
35
- - `"json/json5"` is for [JSON5](https://json5.org) files
33
+ - `"json/json"` is for regular JSON files
34
+ - `"json/jsonc"` is for JSON files that support comments ([JSONC](https://github.com/microsoft/node-jsonc-parser)) such as those used for Visual Studio Code configuration files
35
+ - `"json/json5"` is for [JSON5](https://json5.org) files
36
36
 
37
37
  Depending on which types of JSON files you'd like to lint, you can set up your `eslint.config.js` file to include just the files you'd like. Here's an example that lints JSON, JSONC, and JSON5 files:
38
38
 
@@ -152,9 +152,16 @@ export default [
152
152
 
153
153
  ## Rules
154
154
 
155
- - `no-duplicate-keys` - warns when there are two keys in an object with the same text.
156
- - `no-empty-keys` - warns when there is a key in an object that is an empty string or contains only whitespace (note: `package-lock.json` uses empty keys intentionally)
157
- - `no-unsafe-values` - warns on values that are unsafe for interchange, such as numbers outside safe range or lone surrogates.
155
+ - `no-duplicate-keys` - warns when there are two keys in an object with the same text.
156
+ - `no-empty-keys` - warns when there is a key in an object that is an empty string or contains only whitespace (note: `package-lock.json` uses empty keys intentionally)
157
+ - `no-unsafe-values` - warns on values that are unsafe for interchange, such
158
+ as strings with unmatched
159
+ [surrogates](https://en.wikipedia.org/wiki/UTF-16), numbers that evaluate to
160
+ Infinity, numbers that evaluate to zero unintentionally, numbers that look
161
+ like integers but are too large, and
162
+ [subnormal numbers](https://en.wikipedia.org/wiki/Subnormal_number).
163
+ - `no-unnormalized-keys` - warns on keys containing [unnormalized characters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize#description). You can optionally specify the normalization form via `{ form: "form_name" }`, where `form_name` can be any of `"NFC"`, `"NFD"`, `"NFKC"`, or `"NFKD"`.
164
+ - `top-level-interop` - warns when the top-level item in the document is neither an array nor an object. This can be enabled to ensure maximal interoperability with the oldest JSON parsers.
158
165
 
159
166
  ## Configuration Comments
160
167
 
@@ -221,8 +228,8 @@ export default [
221
228
 
222
229
  This plugin implements JSON parsing for ESLint using the language plugins API, which is the official way of supporting non-JavaScript languages in ESLint. This differs from the other plugins:
223
230
 
224
- - `eslint-plugin-json` uses a processor to parse the JSON, meaning it doesn't create an AST and you can't write custom rules for it.
225
- - `eslint-plugin-jsonc` uses a parser that still goes through the JavaScript linting functionality and requires several rules to disallow valid JavaScript syntax that is invalid in JSON.
231
+ - `eslint-plugin-json` uses a processor to parse the JSON, meaning it doesn't create an AST and you can't write custom rules for it.
232
+ - `eslint-plugin-jsonc` uses a parser that still goes through the JavaScript linting functionality and requires several rules to disallow valid JavaScript syntax that is invalid in JSON.
226
233
 
227
234
  As such, this plugin is more robust and faster than the others. You can write your own custom rules when using the languages in this plugin, too.
228
235
 
@@ -250,7 +257,7 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).
250
257
  <p><a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="128"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="128"></a></p><h3>Gold Sponsors</h3>
251
258
  <p><a href="https://trunk.io/"><img src="https://images.opencollective.com/trunkio/fb92d60/avatar.png" alt="trunk.io" height="96"></a></p><h3>Silver Sponsors</h3>
252
259
  <p><a href="https://www.serptriumph.com/"><img src="https://images.opencollective.com/serp-triumph5/fea3074/logo.png" alt="SERP Triumph" height="64"></a> <a href="https://www.jetbrains.com/"><img src="https://images.opencollective.com/jetbrains/fe76f99/logo.png" alt="JetBrains" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301?v=4" alt="American Express" height="64"></a> <a href="https://www.workleap.com"><img src="https://avatars.githubusercontent.com/u/53535748?u=d1e55d7661d724bf2281c1bfd33cb8f99fe2465f&v=4" alt="Workleap" height="64"></a></p><h3>Bronze Sponsors</h3>
253
- <p><a href="https://cybozu.co.jp/"><img src="https://images.opencollective.com/cybozu/933e46d/logo.png" alt="Cybozu" height="32"></a> <a href="https://syntax.fm"><img src="https://github.com/syntaxfm.png" alt="Syntax" height="32"></a> <a href="https://www.wordhint.net/"><img src="https://images.opencollective.com/wordhint/be86813/avatar.png" alt="WordHint" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340?v=4" alt="GitBook" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104?v=4" alt="Nx" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774?v=4" alt="HeroCoders" height="32"></a></p>
260
+ <p><a href="https://cybozu.co.jp/"><img src="https://images.opencollective.com/cybozu/933e46d/logo.png" alt="Cybozu" height="32"></a> <a href="https://syntax.fm"><img src="https://github.com/syntaxfm.png" alt="Syntax" height="32"></a> <a href="https://www.wordhint.net/"><img src="https://images.opencollective.com/wordhint/be86813/avatar.png" alt="WordHint" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340?v=4" alt="GitBook" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104?v=4" alt="Nx" height="32"></a> <a href="https://opensource.mercedes-benz.com/"><img src="https://avatars.githubusercontent.com/u/34240465?v=4" alt="Mercedes-Benz Group" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774?v=4" alt="HeroCoders" height="32"></a></p>
254
261
  <h3>Technology Sponsors</h3>
255
262
  Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.
256
263
  <p><a href="https://netlify.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/netlify-icon.svg" alt="Netlify" height="32"></a> <a href="https://algolia.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/algolia-icon.svg" alt="Algolia" height="32"></a> <a href="https://1password.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/1password-icon.svg" alt="1Password" height="32"></a></p>
@@ -455,7 +455,7 @@ class JSONLanguage {
455
455
 
456
456
  var noDuplicateKeys = {
457
457
  meta: {
458
- type: "problem",
458
+ type: /** @type {const} */ ("problem"),
459
459
 
460
460
  docs: {
461
461
  description: "Disallow duplicate keys in JSON objects",
@@ -508,7 +508,7 @@ var noDuplicateKeys = {
508
508
 
509
509
  var noEmptyKeys = {
510
510
  meta: {
511
- type: "problem",
511
+ type: /** @type {const} */ ("problem"),
512
512
 
513
513
  docs: {
514
514
  description: "Disallow empty keys in JSON objects",
@@ -543,16 +543,27 @@ var noEmptyKeys = {
543
543
  * @author Bradley Meck Farias
544
544
  */
545
545
 
546
+ // RFC 8259's `number` production, as a regex. Capture the integer part
547
+ // and the fractional part.
548
+ const NUMBER =
549
+ /^-?(?<int>0|([1-9][0-9]*))(?:\.(?<frac>[0-9]+))?(?:[eE][+-]?[0-9]+)?$/u;
550
+ const NON_ZERO = /[1-9]/u;
551
+
546
552
  var noUnsafeValues = {
547
553
  meta: {
548
- type: "problem",
554
+ type: /** @type {const} */ ("problem"),
549
555
 
550
556
  docs: {
551
557
  description: "Disallow JSON values that are unsafe for interchange",
552
558
  },
553
559
 
554
560
  messages: {
555
- unsafeNumber: "Number outside safe range found.",
561
+ unsafeNumber: "The number '{{ value }}' will evaluate to Infinity.",
562
+ unsafeInteger:
563
+ "The integer '{{ value }}' is outside the safe integer range.",
564
+ unsafeZero: "The number '{{ value }}' will evaluate to zero.",
565
+ subnormal:
566
+ "Unexpected subnormal number '{{ value }}' found, which may cause interoperability issues.",
556
567
  loneSurrogate: "Lone surrogate '{{ surrogate }}' found.",
557
568
  },
558
569
  },
@@ -560,11 +571,64 @@ var noUnsafeValues = {
560
571
  create(context) {
561
572
  return {
562
573
  Number(node) {
574
+ const value = context.sourceCode.getText(node);
575
+
563
576
  if (Number.isFinite(node.value) !== true) {
564
577
  context.report({
565
578
  loc: node.loc,
566
579
  messageId: "unsafeNumber",
580
+ data: { value },
567
581
  });
582
+ } else {
583
+ // Also matches -0, intentionally
584
+ if (node.value === 0) {
585
+ // If the value has been rounded down to 0, but there was some
586
+ // fraction or non-zero part before the e-, this is a very small
587
+ // number that doesn't fit inside an f64.
588
+ const match = value.match(NUMBER);
589
+ // assert(match, "If the regex is right, match is always truthy")
590
+
591
+ // If any part of the number other than the exponent has a
592
+ // non-zero digit in it, this number was not intended to be
593
+ // evaluated down to a zero.
594
+ if (
595
+ NON_ZERO.test(match.groups.int) ||
596
+ NON_ZERO.test(match.groups.frac)
597
+ ) {
598
+ context.report({
599
+ loc: node.loc,
600
+ messageId: "unsafeZero",
601
+ data: { value },
602
+ });
603
+ }
604
+ } else if (!/[.e]/iu.test(value)) {
605
+ // Intended to be an integer
606
+ if (
607
+ node.value > Number.MAX_SAFE_INTEGER ||
608
+ node.value < Number.MIN_SAFE_INTEGER
609
+ ) {
610
+ context.report({
611
+ loc: node.loc,
612
+ messageId: "unsafeInteger",
613
+ data: { value },
614
+ });
615
+ }
616
+ } else {
617
+ // Floating point. Check for subnormal.
618
+ const buffer = new ArrayBuffer(8);
619
+ const view = new DataView(buffer);
620
+ view.setFloat64(0, node.value, false);
621
+ const asBigInt = view.getBigUint64(0, false);
622
+ // Subnormals have an 11-bit exponent of 0 and a non-zero mantissa.
623
+ if ((asBigInt & 0x7ff0000000000000n) === 0n) {
624
+ context.report({
625
+ loc: node.loc,
626
+ messageId: "subnormal",
627
+ // Value included so that it's seen in scientific notation
628
+ data: node,
629
+ });
630
+ }
631
+ }
568
632
  }
569
633
  },
570
634
  String(node) {
@@ -599,6 +663,97 @@ var noUnsafeValues = {
599
663
  },
600
664
  };
601
665
 
666
+ /**
667
+ * @fileoverview Rule to detect unnormalized keys in JSON.
668
+ * @author Bradley Meck Farias
669
+ */
670
+
671
+ var noUnnormalizedKeys = {
672
+ meta: {
673
+ type: /** @type {const} */ ("problem"),
674
+
675
+ docs: {
676
+ description: "Disallow JSON keys that are not normalized",
677
+ },
678
+
679
+ messages: {
680
+ unnormalizedKey: "Unnormalized key '{{key}}' found.",
681
+ },
682
+
683
+ schema: [
684
+ {
685
+ type: "object",
686
+ properties: {
687
+ form: {
688
+ enum: ["NFC", "NFD", "NFKC", "NFKD"],
689
+ },
690
+ },
691
+ additionalProperties: false,
692
+ },
693
+ ],
694
+ },
695
+
696
+ create(context) {
697
+ const normalization = context.options.length
698
+ ? text => text.normalize(context.options[0].form)
699
+ : text => text.normalize();
700
+ return {
701
+ Member(node) {
702
+ const key =
703
+ node.name.type === "String"
704
+ ? node.name.value
705
+ : node.name.name;
706
+
707
+ if (normalization(key) !== key) {
708
+ context.report({
709
+ loc: node.name.loc,
710
+ messageId: "unnormalizedKey",
711
+ data: {
712
+ key,
713
+ },
714
+ });
715
+ }
716
+ },
717
+ };
718
+ },
719
+ };
720
+
721
+ /**
722
+ * @fileoverview Rule to ensure top-level items are either an array or ojbect.
723
+ * @author Joe Hildebrand
724
+ */
725
+
726
+ var topLevelInterop = {
727
+ meta: {
728
+ type: /** @type {const} */ ("problem"),
729
+
730
+ docs: {
731
+ description:
732
+ "Require the JSON top-level value to be an array or object",
733
+ },
734
+
735
+ messages: {
736
+ topLevel:
737
+ "Top level item should be array or object, got '{{type}}'.",
738
+ },
739
+ },
740
+
741
+ create(context) {
742
+ return {
743
+ Document(node) {
744
+ const { type } = node.body;
745
+ if (type !== "Object" && type !== "Array") {
746
+ context.report({
747
+ loc: node.loc,
748
+ messageId: "topLevel",
749
+ data: { type },
750
+ });
751
+ }
752
+ },
753
+ };
754
+ },
755
+ };
756
+
602
757
  /**
603
758
  * @fileoverview JSON plugin.
604
759
  * @author Nicholas C. Zakas
@@ -612,7 +767,7 @@ var noUnsafeValues = {
612
767
  const plugin = {
613
768
  meta: {
614
769
  name: "@eslint/json",
615
- version: "0.7.0", // x-release-please-version
770
+ version: "0.9.0", // x-release-please-version
616
771
  },
617
772
  languages: {
618
773
  json: new JSONLanguage({ mode: "json" }),
@@ -623,20 +778,26 @@ const plugin = {
623
778
  "no-duplicate-keys": noDuplicateKeys,
624
779
  "no-empty-keys": noEmptyKeys,
625
780
  "no-unsafe-values": noUnsafeValues,
781
+ "no-unnormalized-keys": noUnnormalizedKeys,
782
+ "top-level-interop": topLevelInterop,
626
783
  },
627
- configs: {},
628
- };
629
-
630
- Object.assign(plugin.configs, {
631
- recommended: {
632
- plugins: { json: plugin },
633
- rules: {
634
- "json/no-duplicate-keys": "error",
635
- "json/no-empty-keys": "error",
636
- "json/no-unsafe-values": "error",
784
+ configs: {
785
+ recommended: {
786
+ plugins: {},
787
+ rules: /** @type {const} */ ({
788
+ "json/no-duplicate-keys": "error",
789
+ "json/no-empty-keys": "error",
790
+ "json/no-unsafe-values": "error",
791
+ "json/no-unnormalized-keys": "error",
792
+ }),
637
793
  },
638
794
  },
639
- });
795
+ };
796
+
797
+ // eslint-disable-next-line no-lone-blocks -- The block syntax { ... } ensures that TypeScript does not get confused about the type of `plugin`.
798
+ {
799
+ plugin.configs.recommended.plugins.json = plugin;
800
+ }
640
801
 
641
802
  exports.JSONLanguage = JSONLanguage;
642
803
  exports.JSONSourceCode = JSONSourceCode;
@@ -174,7 +174,7 @@ declare namespace plugin {
174
174
  let rules: {
175
175
  "no-duplicate-keys": {
176
176
  meta: {
177
- type: string;
177
+ type: "problem";
178
178
  docs: {
179
179
  description: string;
180
180
  };
@@ -190,7 +190,7 @@ declare namespace plugin {
190
190
  };
191
191
  "no-empty-keys": {
192
192
  meta: {
193
- type: string;
193
+ type: "problem";
194
194
  docs: {
195
195
  description: string;
196
196
  };
@@ -204,12 +204,15 @@ declare namespace plugin {
204
204
  };
205
205
  "no-unsafe-values": {
206
206
  meta: {
207
- type: string;
207
+ type: "problem";
208
208
  docs: {
209
209
  description: string;
210
210
  };
211
211
  messages: {
212
212
  unsafeNumber: string;
213
+ unsafeInteger: string;
214
+ unsafeZero: string;
215
+ subnormal: string;
213
216
  loneSurrogate: string;
214
217
  };
215
218
  };
@@ -218,8 +221,56 @@ declare namespace plugin {
218
221
  String(node: any): void;
219
222
  };
220
223
  };
224
+ "no-unnormalized-keys": {
225
+ meta: {
226
+ type: "problem";
227
+ docs: {
228
+ description: string;
229
+ };
230
+ messages: {
231
+ unnormalizedKey: string;
232
+ };
233
+ schema: {
234
+ type: string;
235
+ properties: {
236
+ form: {
237
+ enum: string[];
238
+ };
239
+ };
240
+ additionalProperties: boolean;
241
+ }[];
242
+ };
243
+ create(context: any): {
244
+ Member(node: any): void;
245
+ };
246
+ };
247
+ "top-level-interop": {
248
+ meta: {
249
+ type: "problem";
250
+ docs: {
251
+ description: string;
252
+ };
253
+ messages: {
254
+ topLevel: string;
255
+ };
256
+ };
257
+ create(context: any): {
258
+ Document(node: any): void;
259
+ };
260
+ };
221
261
  };
222
- let configs: {};
262
+ namespace configs {
263
+ namespace recommended {
264
+ export let plugins: {};
265
+ let rules_1: {
266
+ readonly "json/no-duplicate-keys": "error";
267
+ readonly "json/no-empty-keys": "error";
268
+ readonly "json/no-unsafe-values": "error";
269
+ readonly "json/no-unnormalized-keys": "error";
270
+ };
271
+ export { rules_1 as rules };
272
+ }
273
+ }
223
274
  }
224
275
  import { TextSourceCodeBase } from '@eslint/plugin-kit';
225
276
  import { Directive } from '@eslint/plugin-kit';
@@ -174,7 +174,7 @@ declare namespace plugin {
174
174
  let rules: {
175
175
  "no-duplicate-keys": {
176
176
  meta: {
177
- type: string;
177
+ type: "problem";
178
178
  docs: {
179
179
  description: string;
180
180
  };
@@ -190,7 +190,7 @@ declare namespace plugin {
190
190
  };
191
191
  "no-empty-keys": {
192
192
  meta: {
193
- type: string;
193
+ type: "problem";
194
194
  docs: {
195
195
  description: string;
196
196
  };
@@ -204,12 +204,15 @@ declare namespace plugin {
204
204
  };
205
205
  "no-unsafe-values": {
206
206
  meta: {
207
- type: string;
207
+ type: "problem";
208
208
  docs: {
209
209
  description: string;
210
210
  };
211
211
  messages: {
212
212
  unsafeNumber: string;
213
+ unsafeInteger: string;
214
+ unsafeZero: string;
215
+ subnormal: string;
213
216
  loneSurrogate: string;
214
217
  };
215
218
  };
@@ -218,8 +221,56 @@ declare namespace plugin {
218
221
  String(node: any): void;
219
222
  };
220
223
  };
224
+ "no-unnormalized-keys": {
225
+ meta: {
226
+ type: "problem";
227
+ docs: {
228
+ description: string;
229
+ };
230
+ messages: {
231
+ unnormalizedKey: string;
232
+ };
233
+ schema: {
234
+ type: string;
235
+ properties: {
236
+ form: {
237
+ enum: string[];
238
+ };
239
+ };
240
+ additionalProperties: boolean;
241
+ }[];
242
+ };
243
+ create(context: any): {
244
+ Member(node: any): void;
245
+ };
246
+ };
247
+ "top-level-interop": {
248
+ meta: {
249
+ type: "problem";
250
+ docs: {
251
+ description: string;
252
+ };
253
+ messages: {
254
+ topLevel: string;
255
+ };
256
+ };
257
+ create(context: any): {
258
+ Document(node: any): void;
259
+ };
260
+ };
221
261
  };
222
- let configs: {};
262
+ namespace configs {
263
+ namespace recommended {
264
+ export let plugins: {};
265
+ let rules_1: {
266
+ readonly "json/no-duplicate-keys": "error";
267
+ readonly "json/no-empty-keys": "error";
268
+ readonly "json/no-unsafe-values": "error";
269
+ readonly "json/no-unnormalized-keys": "error";
270
+ };
271
+ export { rules_1 as rules };
272
+ }
273
+ }
223
274
  }
224
275
  import { TextSourceCodeBase } from '@eslint/plugin-kit';
225
276
  import { Directive } from '@eslint/plugin-kit';
package/dist/esm/index.js CHANGED
@@ -452,7 +452,7 @@ class JSONLanguage {
452
452
 
453
453
  var noDuplicateKeys = {
454
454
  meta: {
455
- type: "problem",
455
+ type: /** @type {const} */ ("problem"),
456
456
 
457
457
  docs: {
458
458
  description: "Disallow duplicate keys in JSON objects",
@@ -505,7 +505,7 @@ var noDuplicateKeys = {
505
505
 
506
506
  var noEmptyKeys = {
507
507
  meta: {
508
- type: "problem",
508
+ type: /** @type {const} */ ("problem"),
509
509
 
510
510
  docs: {
511
511
  description: "Disallow empty keys in JSON objects",
@@ -540,16 +540,27 @@ var noEmptyKeys = {
540
540
  * @author Bradley Meck Farias
541
541
  */
542
542
 
543
+ // RFC 8259's `number` production, as a regex. Capture the integer part
544
+ // and the fractional part.
545
+ const NUMBER =
546
+ /^-?(?<int>0|([1-9][0-9]*))(?:\.(?<frac>[0-9]+))?(?:[eE][+-]?[0-9]+)?$/u;
547
+ const NON_ZERO = /[1-9]/u;
548
+
543
549
  var noUnsafeValues = {
544
550
  meta: {
545
- type: "problem",
551
+ type: /** @type {const} */ ("problem"),
546
552
 
547
553
  docs: {
548
554
  description: "Disallow JSON values that are unsafe for interchange",
549
555
  },
550
556
 
551
557
  messages: {
552
- unsafeNumber: "Number outside safe range found.",
558
+ unsafeNumber: "The number '{{ value }}' will evaluate to Infinity.",
559
+ unsafeInteger:
560
+ "The integer '{{ value }}' is outside the safe integer range.",
561
+ unsafeZero: "The number '{{ value }}' will evaluate to zero.",
562
+ subnormal:
563
+ "Unexpected subnormal number '{{ value }}' found, which may cause interoperability issues.",
553
564
  loneSurrogate: "Lone surrogate '{{ surrogate }}' found.",
554
565
  },
555
566
  },
@@ -557,11 +568,64 @@ var noUnsafeValues = {
557
568
  create(context) {
558
569
  return {
559
570
  Number(node) {
571
+ const value = context.sourceCode.getText(node);
572
+
560
573
  if (Number.isFinite(node.value) !== true) {
561
574
  context.report({
562
575
  loc: node.loc,
563
576
  messageId: "unsafeNumber",
577
+ data: { value },
564
578
  });
579
+ } else {
580
+ // Also matches -0, intentionally
581
+ if (node.value === 0) {
582
+ // If the value has been rounded down to 0, but there was some
583
+ // fraction or non-zero part before the e-, this is a very small
584
+ // number that doesn't fit inside an f64.
585
+ const match = value.match(NUMBER);
586
+ // assert(match, "If the regex is right, match is always truthy")
587
+
588
+ // If any part of the number other than the exponent has a
589
+ // non-zero digit in it, this number was not intended to be
590
+ // evaluated down to a zero.
591
+ if (
592
+ NON_ZERO.test(match.groups.int) ||
593
+ NON_ZERO.test(match.groups.frac)
594
+ ) {
595
+ context.report({
596
+ loc: node.loc,
597
+ messageId: "unsafeZero",
598
+ data: { value },
599
+ });
600
+ }
601
+ } else if (!/[.e]/iu.test(value)) {
602
+ // Intended to be an integer
603
+ if (
604
+ node.value > Number.MAX_SAFE_INTEGER ||
605
+ node.value < Number.MIN_SAFE_INTEGER
606
+ ) {
607
+ context.report({
608
+ loc: node.loc,
609
+ messageId: "unsafeInteger",
610
+ data: { value },
611
+ });
612
+ }
613
+ } else {
614
+ // Floating point. Check for subnormal.
615
+ const buffer = new ArrayBuffer(8);
616
+ const view = new DataView(buffer);
617
+ view.setFloat64(0, node.value, false);
618
+ const asBigInt = view.getBigUint64(0, false);
619
+ // Subnormals have an 11-bit exponent of 0 and a non-zero mantissa.
620
+ if ((asBigInt & 0x7ff0000000000000n) === 0n) {
621
+ context.report({
622
+ loc: node.loc,
623
+ messageId: "subnormal",
624
+ // Value included so that it's seen in scientific notation
625
+ data: node,
626
+ });
627
+ }
628
+ }
565
629
  }
566
630
  },
567
631
  String(node) {
@@ -596,6 +660,97 @@ var noUnsafeValues = {
596
660
  },
597
661
  };
598
662
 
663
+ /**
664
+ * @fileoverview Rule to detect unnormalized keys in JSON.
665
+ * @author Bradley Meck Farias
666
+ */
667
+
668
+ var noUnnormalizedKeys = {
669
+ meta: {
670
+ type: /** @type {const} */ ("problem"),
671
+
672
+ docs: {
673
+ description: "Disallow JSON keys that are not normalized",
674
+ },
675
+
676
+ messages: {
677
+ unnormalizedKey: "Unnormalized key '{{key}}' found.",
678
+ },
679
+
680
+ schema: [
681
+ {
682
+ type: "object",
683
+ properties: {
684
+ form: {
685
+ enum: ["NFC", "NFD", "NFKC", "NFKD"],
686
+ },
687
+ },
688
+ additionalProperties: false,
689
+ },
690
+ ],
691
+ },
692
+
693
+ create(context) {
694
+ const normalization = context.options.length
695
+ ? text => text.normalize(context.options[0].form)
696
+ : text => text.normalize();
697
+ return {
698
+ Member(node) {
699
+ const key =
700
+ node.name.type === "String"
701
+ ? node.name.value
702
+ : node.name.name;
703
+
704
+ if (normalization(key) !== key) {
705
+ context.report({
706
+ loc: node.name.loc,
707
+ messageId: "unnormalizedKey",
708
+ data: {
709
+ key,
710
+ },
711
+ });
712
+ }
713
+ },
714
+ };
715
+ },
716
+ };
717
+
718
+ /**
719
+ * @fileoverview Rule to ensure top-level items are either an array or ojbect.
720
+ * @author Joe Hildebrand
721
+ */
722
+
723
+ var topLevelInterop = {
724
+ meta: {
725
+ type: /** @type {const} */ ("problem"),
726
+
727
+ docs: {
728
+ description:
729
+ "Require the JSON top-level value to be an array or object",
730
+ },
731
+
732
+ messages: {
733
+ topLevel:
734
+ "Top level item should be array or object, got '{{type}}'.",
735
+ },
736
+ },
737
+
738
+ create(context) {
739
+ return {
740
+ Document(node) {
741
+ const { type } = node.body;
742
+ if (type !== "Object" && type !== "Array") {
743
+ context.report({
744
+ loc: node.loc,
745
+ messageId: "topLevel",
746
+ data: { type },
747
+ });
748
+ }
749
+ },
750
+ };
751
+ },
752
+ };
753
+
599
754
  /**
600
755
  * @fileoverview JSON plugin.
601
756
  * @author Nicholas C. Zakas
@@ -609,7 +764,7 @@ var noUnsafeValues = {
609
764
  const plugin = {
610
765
  meta: {
611
766
  name: "@eslint/json",
612
- version: "0.7.0", // x-release-please-version
767
+ version: "0.9.0", // x-release-please-version
613
768
  },
614
769
  languages: {
615
770
  json: new JSONLanguage({ mode: "json" }),
@@ -620,19 +775,25 @@ const plugin = {
620
775
  "no-duplicate-keys": noDuplicateKeys,
621
776
  "no-empty-keys": noEmptyKeys,
622
777
  "no-unsafe-values": noUnsafeValues,
778
+ "no-unnormalized-keys": noUnnormalizedKeys,
779
+ "top-level-interop": topLevelInterop,
623
780
  },
624
- configs: {},
625
- };
626
-
627
- Object.assign(plugin.configs, {
628
- recommended: {
629
- plugins: { json: plugin },
630
- rules: {
631
- "json/no-duplicate-keys": "error",
632
- "json/no-empty-keys": "error",
633
- "json/no-unsafe-values": "error",
781
+ configs: {
782
+ recommended: {
783
+ plugins: {},
784
+ rules: /** @type {const} */ ({
785
+ "json/no-duplicate-keys": "error",
786
+ "json/no-empty-keys": "error",
787
+ "json/no-unsafe-values": "error",
788
+ "json/no-unnormalized-keys": "error",
789
+ }),
634
790
  },
635
791
  },
636
- });
792
+ };
793
+
794
+ // eslint-disable-next-line no-lone-blocks -- The block syntax { ... } ensures that TypeScript does not get confused about the type of `plugin`.
795
+ {
796
+ plugin.configs.recommended.plugins.json = plugin;
797
+ }
637
798
 
638
799
  export { JSONLanguage, JSONSourceCode, plugin as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eslint/json",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "JSON linting plugin for ESLint",
5
5
  "author": "Nicholas C. Zakas",
6
6
  "type": "module",
@@ -51,7 +51,8 @@
51
51
  "fmt": "prettier --write .",
52
52
  "fmt:check": "prettier --check .",
53
53
  "test": "mocha tests/**/*.js",
54
- "test:coverage": "c8 npm test"
54
+ "test:coverage": "c8 npm test",
55
+ "test:types": "tsc -p tests/types/tsconfig.json"
55
56
  },
56
57
  "keywords": [
57
58
  "eslint",
@@ -63,7 +64,7 @@
63
64
  "license": "Apache-2.0",
64
65
  "dependencies": {
65
66
  "@eslint/plugin-kit": "^0.2.3",
66
- "@humanwhocodes/momoa": "^3.3.3"
67
+ "@humanwhocodes/momoa": "^3.3.4"
67
68
  },
68
69
  "devDependencies": {
69
70
  "@eslint/core": "^0.6.0",
@@ -72,10 +73,11 @@
72
73
  "dedent": "^1.5.3",
73
74
  "eslint": "^9.11.1",
74
75
  "eslint-config-eslint": "^11.0.0",
76
+ "eslint-plugin-eslint-plugin": "^6.3.2",
75
77
  "got": "^14.4.2",
76
78
  "lint-staged": "^15.2.7",
77
79
  "mocha": "^10.4.0",
78
- "prettier": "^3.3.2",
80
+ "prettier": "^3.4.1",
79
81
  "rollup": "^4.16.2",
80
82
  "rollup-plugin-copy": "^3.5.0",
81
83
  "typescript": "^5.4.5",