@eslint/json 0.9.1 → 0.10.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
@@ -161,6 +161,7 @@ export default [
161
161
  like integers but are too large, and
162
162
  [subnormal numbers](https://en.wikipedia.org/wiki/Subnormal_number).
163
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
+ - `sort-keys` - warns when keys are not in the specified order. Based on the ESLint [`sort-keys`](https://eslint.org/docs/latest/rules/sort-keys) rule.
164
165
  - `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.
165
166
 
166
167
  ## Configuration Comments
@@ -257,7 +258,7 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).
257
258
  <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>
258
259
  <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>
259
260
  <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" alt="American Express" height="64"></a></p><h3>Bronze Sponsors</h3>
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://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" alt="GitBook" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104" alt="Nx" height="32"></a> <a href="https://opensource.mercedes-benz.com/"><img src="https://avatars.githubusercontent.com/u/34240465" alt="Mercedes-Benz Group" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774" alt="HeroCoders" height="32"></a></p>
261
+ <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://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" alt="GitBook" height="32"></a> <a href="https://nolebase.ayaka.io"><img src="https://avatars.githubusercontent.com/u/11081491" alt="Neko" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104" alt="Nx" height="32"></a> <a href="https://opensource.mercedes-benz.com/"><img src="https://avatars.githubusercontent.com/u/34240465" alt="Mercedes-Benz Group" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774" alt="HeroCoders" height="32"></a></p>
261
262
  <h3>Technology Sponsors</h3>
262
263
  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.
263
264
  <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>
@@ -4,6 +4,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var momoa = require('@humanwhocodes/momoa');
6
6
  var pluginKit = require('@eslint/plugin-kit');
7
+ var naturalCompare = require('natural-compare');
7
8
 
8
9
  /**
9
10
  * @fileoverview The JSONSourceCode class.
@@ -718,6 +719,184 @@ var noUnnormalizedKeys = {
718
719
  },
719
720
  };
720
721
 
722
+ /**
723
+ * @fileoverview Rule to require JSON object keys to be sorted. Copied largely from https://github.com/eslint/eslint/blob/main/lib/rules/sort-keys.js
724
+ * @author Robin Thomas
725
+ */
726
+
727
+
728
+ const hasNonWhitespace = /\S/u;
729
+
730
+ const comparators = {
731
+ ascending: {
732
+ alphanumeric: {
733
+ sensitive: (a, b) => a <= b,
734
+ insensitive: (a, b) => a.toLowerCase() <= b.toLowerCase(),
735
+ },
736
+ natural: {
737
+ sensitive: (a, b) => naturalCompare(a, b) <= 0,
738
+ insensitive: (a, b) =>
739
+ naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0,
740
+ },
741
+ },
742
+ descending: {
743
+ alphanumeric: {
744
+ sensitive: (a, b) =>
745
+ comparators.ascending.alphanumeric.sensitive(b, a),
746
+ insensitive: (a, b) =>
747
+ comparators.ascending.alphanumeric.insensitive(b, a),
748
+ },
749
+ natural: {
750
+ sensitive: (a, b) => comparators.ascending.natural.sensitive(b, a),
751
+ insensitive: (a, b) =>
752
+ comparators.ascending.natural.insensitive(b, a),
753
+ },
754
+ },
755
+ };
756
+
757
+ function getKey(member) {
758
+ return member.name.type === `Identifier`
759
+ ? member.name.name
760
+ : member.name.value;
761
+ }
762
+
763
+ var sortKeys = {
764
+ meta: {
765
+ type: /** @type {const} */ ("suggestion"),
766
+
767
+ defaultOptions: [
768
+ "asc",
769
+ {
770
+ allowLineSeparatedGroups: false,
771
+ caseSensitive: true,
772
+ minKeys: 2,
773
+ natural: false,
774
+ },
775
+ ],
776
+
777
+ docs: {
778
+ description: `Require JSON object keys to be sorted`,
779
+ },
780
+
781
+ messages: {
782
+ sortKeys:
783
+ "Expected object keys to be in {{sortName}} case-{{sensitivity}} {{direction}} order. '{{thisName}}' should be before '{{prevName}}'.",
784
+ },
785
+
786
+ schema: [
787
+ {
788
+ enum: ["asc", "desc"],
789
+ },
790
+ {
791
+ type: "object",
792
+ properties: {
793
+ caseSensitive: {
794
+ type: "boolean",
795
+ },
796
+ natural: {
797
+ type: "boolean",
798
+ },
799
+ minKeys: {
800
+ type: "integer",
801
+ minimum: 2,
802
+ },
803
+ allowLineSeparatedGroups: {
804
+ type: "boolean",
805
+ },
806
+ },
807
+ additionalProperties: false,
808
+ },
809
+ ],
810
+ },
811
+
812
+ create(context) {
813
+ const [
814
+ directionShort,
815
+ { allowLineSeparatedGroups, caseSensitive, natural, minKeys },
816
+ ] = context.options;
817
+
818
+ const direction = directionShort === "asc" ? "ascending" : "descending";
819
+ const sortName = natural ? "natural" : "alphanumeric";
820
+ const sensitivity = caseSensitive ? "sensitive" : "insensitive";
821
+ const isValidOrder = comparators[direction][sortName][sensitivity];
822
+
823
+ // Note that @humanwhocodes/momoa doesn't include comments in the object.members tree, so we can't just see if a member is preceded by a comment
824
+ const commentLineNums = new Set();
825
+ for (const comment of context.sourceCode.comments) {
826
+ for (
827
+ let lineNum = comment.loc.start.line;
828
+ lineNum <= comment.loc.end.line;
829
+ lineNum += 1
830
+ ) {
831
+ commentLineNums.add(lineNum);
832
+ }
833
+ }
834
+
835
+ // Note that there can be comments *inside* members, e.g. `{"foo: /* comment *\/ "bar"}`, but these are ignored when calculating line-separated groups
836
+ function isLineSeparated(prevMember, member) {
837
+ const prevMemberEndLine = prevMember.loc.end.line;
838
+ const thisStartLine = member.loc.start.line;
839
+ if (thisStartLine - prevMemberEndLine < 2) {
840
+ return false;
841
+ }
842
+
843
+ for (
844
+ let lineNum = prevMemberEndLine + 1;
845
+ lineNum < thisStartLine;
846
+ lineNum += 1
847
+ ) {
848
+ if (
849
+ !commentLineNums.has(lineNum) &&
850
+ !hasNonWhitespace.test(
851
+ context.sourceCode.lines[lineNum - 1],
852
+ )
853
+ ) {
854
+ return true;
855
+ }
856
+ }
857
+
858
+ return false;
859
+ }
860
+
861
+ return {
862
+ Object(node) {
863
+ let prevMember;
864
+ let prevName;
865
+
866
+ if (node.members.length < minKeys) {
867
+ return;
868
+ }
869
+
870
+ for (const member of node.members) {
871
+ const thisName = getKey(member);
872
+
873
+ if (
874
+ prevMember &&
875
+ !isValidOrder(prevName, thisName) &&
876
+ (!allowLineSeparatedGroups ||
877
+ !isLineSeparated(prevMember, member))
878
+ ) {
879
+ context.report({
880
+ loc: member.name.loc,
881
+ messageId: "sortKeys",
882
+ data: {
883
+ thisName,
884
+ prevName,
885
+ direction,
886
+ sensitivity,
887
+ sortName,
888
+ },
889
+ });
890
+ }
891
+
892
+ prevMember = member;
893
+ prevName = thisName;
894
+ }
895
+ },
896
+ };
897
+ },
898
+ };
899
+
721
900
  /**
722
901
  * @fileoverview Rule to ensure top-level items are either an array or ojbect.
723
902
  * @author Joe Hildebrand
@@ -767,7 +946,7 @@ var topLevelInterop = {
767
946
  const plugin = {
768
947
  meta: {
769
948
  name: "@eslint/json",
770
- version: "0.9.1", // x-release-please-version
949
+ version: "0.10.0", // x-release-please-version
771
950
  },
772
951
  languages: {
773
952
  json: new JSONLanguage({ mode: "json" }),
@@ -779,6 +958,7 @@ const plugin = {
779
958
  "no-empty-keys": noEmptyKeys,
780
959
  "no-unsafe-values": noUnsafeValues,
781
960
  "no-unnormalized-keys": noUnnormalizedKeys,
961
+ "sort-keys": sortKeys,
782
962
  "top-level-interop": topLevelInterop,
783
963
  },
784
964
  configs: {
@@ -244,6 +244,51 @@ declare namespace plugin {
244
244
  Member(node: any): void;
245
245
  };
246
246
  };
247
+ "sort-keys": {
248
+ meta: {
249
+ type: "suggestion";
250
+ defaultOptions: (string | {
251
+ allowLineSeparatedGroups: boolean;
252
+ caseSensitive: boolean;
253
+ minKeys: number;
254
+ natural: boolean;
255
+ })[];
256
+ docs: {
257
+ description: string;
258
+ };
259
+ messages: {
260
+ sortKeys: string;
261
+ };
262
+ schema: ({
263
+ enum: string[];
264
+ type?: undefined;
265
+ properties?: undefined;
266
+ additionalProperties?: undefined;
267
+ } | {
268
+ type: string;
269
+ properties: {
270
+ caseSensitive: {
271
+ type: string;
272
+ };
273
+ natural: {
274
+ type: string;
275
+ };
276
+ minKeys: {
277
+ type: string;
278
+ minimum: number;
279
+ };
280
+ allowLineSeparatedGroups: {
281
+ type: string;
282
+ };
283
+ };
284
+ additionalProperties: boolean;
285
+ enum?: undefined;
286
+ })[];
287
+ };
288
+ create(context: any): {
289
+ Object(node: any): void;
290
+ };
291
+ };
247
292
  "top-level-interop": {
248
293
  meta: {
249
294
  type: "problem";
@@ -244,6 +244,51 @@ declare namespace plugin {
244
244
  Member(node: any): void;
245
245
  };
246
246
  };
247
+ "sort-keys": {
248
+ meta: {
249
+ type: "suggestion";
250
+ defaultOptions: (string | {
251
+ allowLineSeparatedGroups: boolean;
252
+ caseSensitive: boolean;
253
+ minKeys: number;
254
+ natural: boolean;
255
+ })[];
256
+ docs: {
257
+ description: string;
258
+ };
259
+ messages: {
260
+ sortKeys: string;
261
+ };
262
+ schema: ({
263
+ enum: string[];
264
+ type?: undefined;
265
+ properties?: undefined;
266
+ additionalProperties?: undefined;
267
+ } | {
268
+ type: string;
269
+ properties: {
270
+ caseSensitive: {
271
+ type: string;
272
+ };
273
+ natural: {
274
+ type: string;
275
+ };
276
+ minKeys: {
277
+ type: string;
278
+ minimum: number;
279
+ };
280
+ allowLineSeparatedGroups: {
281
+ type: string;
282
+ };
283
+ };
284
+ additionalProperties: boolean;
285
+ enum?: undefined;
286
+ })[];
287
+ };
288
+ create(context: any): {
289
+ Object(node: any): void;
290
+ };
291
+ };
247
292
  "top-level-interop": {
248
293
  meta: {
249
294
  type: "problem";
package/dist/esm/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // @ts-self-types="./index.d.ts"
2
2
  import { iterator, visitorKeys, parse } from '@humanwhocodes/momoa';
3
3
  import { ConfigCommentParser, TextSourceCodeBase, Directive, VisitNodeStep } from '@eslint/plugin-kit';
4
+ import naturalCompare from 'natural-compare';
4
5
 
5
6
  /**
6
7
  * @fileoverview The JSONSourceCode class.
@@ -715,6 +716,184 @@ var noUnnormalizedKeys = {
715
716
  },
716
717
  };
717
718
 
719
+ /**
720
+ * @fileoverview Rule to require JSON object keys to be sorted. Copied largely from https://github.com/eslint/eslint/blob/main/lib/rules/sort-keys.js
721
+ * @author Robin Thomas
722
+ */
723
+
724
+
725
+ const hasNonWhitespace = /\S/u;
726
+
727
+ const comparators = {
728
+ ascending: {
729
+ alphanumeric: {
730
+ sensitive: (a, b) => a <= b,
731
+ insensitive: (a, b) => a.toLowerCase() <= b.toLowerCase(),
732
+ },
733
+ natural: {
734
+ sensitive: (a, b) => naturalCompare(a, b) <= 0,
735
+ insensitive: (a, b) =>
736
+ naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0,
737
+ },
738
+ },
739
+ descending: {
740
+ alphanumeric: {
741
+ sensitive: (a, b) =>
742
+ comparators.ascending.alphanumeric.sensitive(b, a),
743
+ insensitive: (a, b) =>
744
+ comparators.ascending.alphanumeric.insensitive(b, a),
745
+ },
746
+ natural: {
747
+ sensitive: (a, b) => comparators.ascending.natural.sensitive(b, a),
748
+ insensitive: (a, b) =>
749
+ comparators.ascending.natural.insensitive(b, a),
750
+ },
751
+ },
752
+ };
753
+
754
+ function getKey(member) {
755
+ return member.name.type === `Identifier`
756
+ ? member.name.name
757
+ : member.name.value;
758
+ }
759
+
760
+ var sortKeys = {
761
+ meta: {
762
+ type: /** @type {const} */ ("suggestion"),
763
+
764
+ defaultOptions: [
765
+ "asc",
766
+ {
767
+ allowLineSeparatedGroups: false,
768
+ caseSensitive: true,
769
+ minKeys: 2,
770
+ natural: false,
771
+ },
772
+ ],
773
+
774
+ docs: {
775
+ description: `Require JSON object keys to be sorted`,
776
+ },
777
+
778
+ messages: {
779
+ sortKeys:
780
+ "Expected object keys to be in {{sortName}} case-{{sensitivity}} {{direction}} order. '{{thisName}}' should be before '{{prevName}}'.",
781
+ },
782
+
783
+ schema: [
784
+ {
785
+ enum: ["asc", "desc"],
786
+ },
787
+ {
788
+ type: "object",
789
+ properties: {
790
+ caseSensitive: {
791
+ type: "boolean",
792
+ },
793
+ natural: {
794
+ type: "boolean",
795
+ },
796
+ minKeys: {
797
+ type: "integer",
798
+ minimum: 2,
799
+ },
800
+ allowLineSeparatedGroups: {
801
+ type: "boolean",
802
+ },
803
+ },
804
+ additionalProperties: false,
805
+ },
806
+ ],
807
+ },
808
+
809
+ create(context) {
810
+ const [
811
+ directionShort,
812
+ { allowLineSeparatedGroups, caseSensitive, natural, minKeys },
813
+ ] = context.options;
814
+
815
+ const direction = directionShort === "asc" ? "ascending" : "descending";
816
+ const sortName = natural ? "natural" : "alphanumeric";
817
+ const sensitivity = caseSensitive ? "sensitive" : "insensitive";
818
+ const isValidOrder = comparators[direction][sortName][sensitivity];
819
+
820
+ // Note that @humanwhocodes/momoa doesn't include comments in the object.members tree, so we can't just see if a member is preceded by a comment
821
+ const commentLineNums = new Set();
822
+ for (const comment of context.sourceCode.comments) {
823
+ for (
824
+ let lineNum = comment.loc.start.line;
825
+ lineNum <= comment.loc.end.line;
826
+ lineNum += 1
827
+ ) {
828
+ commentLineNums.add(lineNum);
829
+ }
830
+ }
831
+
832
+ // Note that there can be comments *inside* members, e.g. `{"foo: /* comment *\/ "bar"}`, but these are ignored when calculating line-separated groups
833
+ function isLineSeparated(prevMember, member) {
834
+ const prevMemberEndLine = prevMember.loc.end.line;
835
+ const thisStartLine = member.loc.start.line;
836
+ if (thisStartLine - prevMemberEndLine < 2) {
837
+ return false;
838
+ }
839
+
840
+ for (
841
+ let lineNum = prevMemberEndLine + 1;
842
+ lineNum < thisStartLine;
843
+ lineNum += 1
844
+ ) {
845
+ if (
846
+ !commentLineNums.has(lineNum) &&
847
+ !hasNonWhitespace.test(
848
+ context.sourceCode.lines[lineNum - 1],
849
+ )
850
+ ) {
851
+ return true;
852
+ }
853
+ }
854
+
855
+ return false;
856
+ }
857
+
858
+ return {
859
+ Object(node) {
860
+ let prevMember;
861
+ let prevName;
862
+
863
+ if (node.members.length < minKeys) {
864
+ return;
865
+ }
866
+
867
+ for (const member of node.members) {
868
+ const thisName = getKey(member);
869
+
870
+ if (
871
+ prevMember &&
872
+ !isValidOrder(prevName, thisName) &&
873
+ (!allowLineSeparatedGroups ||
874
+ !isLineSeparated(prevMember, member))
875
+ ) {
876
+ context.report({
877
+ loc: member.name.loc,
878
+ messageId: "sortKeys",
879
+ data: {
880
+ thisName,
881
+ prevName,
882
+ direction,
883
+ sensitivity,
884
+ sortName,
885
+ },
886
+ });
887
+ }
888
+
889
+ prevMember = member;
890
+ prevName = thisName;
891
+ }
892
+ },
893
+ };
894
+ },
895
+ };
896
+
718
897
  /**
719
898
  * @fileoverview Rule to ensure top-level items are either an array or ojbect.
720
899
  * @author Joe Hildebrand
@@ -764,7 +943,7 @@ var topLevelInterop = {
764
943
  const plugin = {
765
944
  meta: {
766
945
  name: "@eslint/json",
767
- version: "0.9.1", // x-release-please-version
946
+ version: "0.10.0", // x-release-please-version
768
947
  },
769
948
  languages: {
770
949
  json: new JSONLanguage({ mode: "json" }),
@@ -776,6 +955,7 @@ const plugin = {
776
955
  "no-empty-keys": noEmptyKeys,
777
956
  "no-unsafe-values": noUnsafeValues,
778
957
  "no-unnormalized-keys": noUnnormalizedKeys,
958
+ "sort-keys": sortKeys,
779
959
  "top-level-interop": topLevelInterop,
780
960
  },
781
961
  configs: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eslint/json",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "JSON linting plugin for ESLint",
5
5
  "author": "Nicholas C. Zakas",
6
6
  "type": "module",
@@ -65,7 +65,8 @@
65
65
  "dependencies": {
66
66
  "@eslint/core": "^0.10.0",
67
67
  "@eslint/plugin-kit": "^0.2.5",
68
- "@humanwhocodes/momoa": "^3.3.4"
68
+ "@humanwhocodes/momoa": "^3.3.4",
69
+ "natural-compare": "^1.4.0"
69
70
  },
70
71
  "devDependencies": {
71
72
  "@types/eslint": "^8.56.10",