@eslint/json 0.8.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,10 +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.
158
- - `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"`.
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.
159
165
 
160
166
  ## Configuration Comments
161
167
 
@@ -222,8 +228,8 @@ export default [
222
228
 
223
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:
224
230
 
225
- - `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.
226
- - `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.
227
233
 
228
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.
229
235
 
@@ -251,7 +257,7 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).
251
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>
252
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>
253
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>
254
- <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>
255
261
  <h3>Technology Sponsors</h3>
256
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.
257
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>
@@ -543,6 +543,12 @@ 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
554
  type: /** @type {const} */ ("problem"),
@@ -552,7 +558,12 @@ var noUnsafeValues = {
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) {
@@ -654,6 +718,42 @@ var noUnnormalizedKeys = {
654
718
  },
655
719
  };
656
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
+
657
757
  /**
658
758
  * @fileoverview JSON plugin.
659
759
  * @author Nicholas C. Zakas
@@ -667,7 +767,7 @@ var noUnnormalizedKeys = {
667
767
  const plugin = {
668
768
  meta: {
669
769
  name: "@eslint/json",
670
- version: "0.8.0", // x-release-please-version
770
+ version: "0.9.0", // x-release-please-version
671
771
  },
672
772
  languages: {
673
773
  json: new JSONLanguage({ mode: "json" }),
@@ -679,6 +779,7 @@ const plugin = {
679
779
  "no-empty-keys": noEmptyKeys,
680
780
  "no-unsafe-values": noUnsafeValues,
681
781
  "no-unnormalized-keys": noUnnormalizedKeys,
782
+ "top-level-interop": topLevelInterop,
682
783
  },
683
784
  configs: {
684
785
  recommended: {
@@ -210,6 +210,9 @@ declare namespace plugin {
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
  };
@@ -241,6 +244,20 @@ declare namespace plugin {
241
244
  Member(node: any): void;
242
245
  };
243
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
+ };
244
261
  };
245
262
  namespace configs {
246
263
  namespace recommended {
@@ -210,6 +210,9 @@ declare namespace plugin {
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
  };
@@ -241,6 +244,20 @@ declare namespace plugin {
241
244
  Member(node: any): void;
242
245
  };
243
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
+ };
244
261
  };
245
262
  namespace configs {
246
263
  namespace recommended {
package/dist/esm/index.js CHANGED
@@ -540,6 +540,12 @@ 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
551
  type: /** @type {const} */ ("problem"),
@@ -549,7 +555,12 @@ var noUnsafeValues = {
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) {
@@ -651,6 +715,42 @@ var noUnnormalizedKeys = {
651
715
  },
652
716
  };
653
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
+
654
754
  /**
655
755
  * @fileoverview JSON plugin.
656
756
  * @author Nicholas C. Zakas
@@ -664,7 +764,7 @@ var noUnnormalizedKeys = {
664
764
  const plugin = {
665
765
  meta: {
666
766
  name: "@eslint/json",
667
- version: "0.8.0", // x-release-please-version
767
+ version: "0.9.0", // x-release-please-version
668
768
  },
669
769
  languages: {
670
770
  json: new JSONLanguage({ mode: "json" }),
@@ -676,6 +776,7 @@ const plugin = {
676
776
  "no-empty-keys": noEmptyKeys,
677
777
  "no-unsafe-values": noUnsafeValues,
678
778
  "no-unnormalized-keys": noUnnormalizedKeys,
779
+ "top-level-interop": topLevelInterop,
679
780
  },
680
781
  configs: {
681
782
  recommended: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eslint/json",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "JSON linting plugin for ESLint",
5
5
  "author": "Nicholas C. Zakas",
6
6
  "type": "module",
@@ -64,7 +64,7 @@
64
64
  "license": "Apache-2.0",
65
65
  "dependencies": {
66
66
  "@eslint/plugin-kit": "^0.2.3",
67
- "@humanwhocodes/momoa": "^3.3.3"
67
+ "@humanwhocodes/momoa": "^3.3.4"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@eslint/core": "^0.6.0",
@@ -77,7 +77,7 @@
77
77
  "got": "^14.4.2",
78
78
  "lint-staged": "^15.2.7",
79
79
  "mocha": "^10.4.0",
80
- "prettier": "^3.3.2",
80
+ "prettier": "^3.4.1",
81
81
  "rollup": "^4.16.2",
82
82
  "rollup-plugin-copy": "^3.5.0",
83
83
  "typescript": "^5.4.5",