From e68c247be11e1ea3fad20737f3e3672b855bc3ff Mon Sep 17 00:00:00 2001 From: "Robin (Robert) Thomas" Date: Sun, 19 Jan 2025 08:25:59 -0600 Subject: [PATCH] feat: Add sort-keys rule (#76) * add sort-keys * adding tests for eslint/sort-keys * made eslint/sort-keys tests work with json * account for line skips * more semantic sort-keys code * Handle when object members are joined by a comment * formatting * add allowLineSeparatedGroups option * Update src/rules/sort-keys.js Co-authored-by: Nicholas C. Zakas * Update readme * Add tests for nesting * Add type fix for sort-keys meta.type * Added some tests for json5 * mv natural-compare to dependencies * rm types from sort-keys because it makes dist grumpy * rm unneeded nullish check * make sort-keys handle multiline comments * tabs => space * Update README.md Co-authored-by: Nicholas C. Zakas * stylistic updates * Commas do not count as group-separating lines * forget about commas, just check if empty line outside of comment * rm unused languageOptions --------- Co-authored-by: Nicholas C. Zakas --- README.md | 1 + package.json | 3 +- src/index.js | 2 + src/rules/sort-keys.js | 178 +++ tests/rules/sort-keys.test.js | 2033 +++++++++++++++++++++++++++++++++ 5 files changed, 2216 insertions(+), 1 deletion(-) create mode 100644 src/rules/sort-keys.js create mode 100644 tests/rules/sort-keys.test.js diff --git a/README.md b/README.md index e9a1ebb..df5c3fd 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ export default [ like integers but are too large, and [subnormal numbers](https://en.wikipedia.org/wiki/Subnormal_number). - `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"`. +- `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. - `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. ## Configuration Comments diff --git a/package.json b/package.json index 1c4fb25..a106fc8 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,8 @@ "dependencies": { "@eslint/core": "^0.10.0", "@eslint/plugin-kit": "^0.2.5", - "@humanwhocodes/momoa": "^3.3.4" + "@humanwhocodes/momoa": "^3.3.4", + "natural-compare": "^1.4.0" }, "devDependencies": { "@types/eslint": "^8.56.10", diff --git a/src/index.js b/src/index.js index db9dbca..7ba3504 100644 --- a/src/index.js +++ b/src/index.js @@ -13,6 +13,7 @@ import noDuplicateKeys from "./rules/no-duplicate-keys.js"; import noEmptyKeys from "./rules/no-empty-keys.js"; import noUnsafeValues from "./rules/no-unsafe-values.js"; import noUnnormalizedKeys from "./rules/no-unnormalized-keys.js"; +import sortKeys from "./rules/sort-keys.js"; import topLevelInterop from "./rules/top-level-interop.js"; //----------------------------------------------------------------------------- @@ -34,6 +35,7 @@ const plugin = { "no-empty-keys": noEmptyKeys, "no-unsafe-values": noUnsafeValues, "no-unnormalized-keys": noUnnormalizedKeys, + "sort-keys": sortKeys, "top-level-interop": topLevelInterop, }, configs: { diff --git a/src/rules/sort-keys.js b/src/rules/sort-keys.js new file mode 100644 index 0000000..d779d63 --- /dev/null +++ b/src/rules/sort-keys.js @@ -0,0 +1,178 @@ +/** + * @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 + * @author Robin Thomas + */ + +import naturalCompare from "natural-compare"; + +const hasNonWhitespace = /\S/u; + +const comparators = { + ascending: { + alphanumeric: { + sensitive: (a, b) => a <= b, + insensitive: (a, b) => a.toLowerCase() <= b.toLowerCase(), + }, + natural: { + sensitive: (a, b) => naturalCompare(a, b) <= 0, + insensitive: (a, b) => + naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0, + }, + }, + descending: { + alphanumeric: { + sensitive: (a, b) => + comparators.ascending.alphanumeric.sensitive(b, a), + insensitive: (a, b) => + comparators.ascending.alphanumeric.insensitive(b, a), + }, + natural: { + sensitive: (a, b) => comparators.ascending.natural.sensitive(b, a), + insensitive: (a, b) => + comparators.ascending.natural.insensitive(b, a), + }, + }, +}; + +function getKey(member) { + return member.name.type === `Identifier` + ? member.name.name + : member.name.value; +} + +export default { + meta: { + type: /** @type {const} */ ("suggestion"), + + defaultOptions: [ + "asc", + { + allowLineSeparatedGroups: false, + caseSensitive: true, + minKeys: 2, + natural: false, + }, + ], + + docs: { + description: `Require JSON object keys to be sorted`, + }, + + messages: { + sortKeys: + "Expected object keys to be in {{sortName}} case-{{sensitivity}} {{direction}} order. '{{thisName}}' should be before '{{prevName}}'.", + }, + + schema: [ + { + enum: ["asc", "desc"], + }, + { + type: "object", + properties: { + caseSensitive: { + type: "boolean", + }, + natural: { + type: "boolean", + }, + minKeys: { + type: "integer", + minimum: 2, + }, + allowLineSeparatedGroups: { + type: "boolean", + }, + }, + additionalProperties: false, + }, + ], + }, + + create(context) { + const [ + directionShort, + { allowLineSeparatedGroups, caseSensitive, natural, minKeys }, + ] = context.options; + + const direction = directionShort === "asc" ? "ascending" : "descending"; + const sortName = natural ? "natural" : "alphanumeric"; + const sensitivity = caseSensitive ? "sensitive" : "insensitive"; + const isValidOrder = comparators[direction][sortName][sensitivity]; + + // 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 + const commentLineNums = new Set(); + for (const comment of context.sourceCode.comments) { + for ( + let lineNum = comment.loc.start.line; + lineNum <= comment.loc.end.line; + lineNum += 1 + ) { + commentLineNums.add(lineNum); + } + } + + // Note that there can be comments *inside* members, e.g. `{"foo: /* comment *\/ "bar"}`, but these are ignored when calculating line-separated groups + function isLineSeparated(prevMember, member) { + const prevMemberEndLine = prevMember.loc.end.line; + const thisStartLine = member.loc.start.line; + if (thisStartLine - prevMemberEndLine < 2) { + return false; + } + + for ( + let lineNum = prevMemberEndLine + 1; + lineNum < thisStartLine; + lineNum += 1 + ) { + if ( + !commentLineNums.has(lineNum) && + !hasNonWhitespace.test( + context.sourceCode.lines[lineNum - 1], + ) + ) { + return true; + } + } + + return false; + } + + return { + Object(node) { + let prevMember; + let prevName; + + if (node.members.length < minKeys) { + return; + } + + for (const member of node.members) { + const thisName = getKey(member); + + if ( + prevMember && + !isValidOrder(prevName, thisName) && + (!allowLineSeparatedGroups || + !isLineSeparated(prevMember, member)) + ) { + context.report({ + loc: member.name.loc, + messageId: "sortKeys", + data: { + thisName, + prevName, + direction, + sensitivity, + sortName, + }, + }); + } + + prevMember = member; + prevName = thisName; + } + }, + }; + }, +}; diff --git a/tests/rules/sort-keys.test.js b/tests/rules/sort-keys.test.js new file mode 100644 index 0000000..c15bb15 --- /dev/null +++ b/tests/rules/sort-keys.test.js @@ -0,0 +1,2033 @@ +/** + * @fileoverview Tests for sort-keys rule. Cribbed from https://github.com/eslint/eslint/blob/main/tests/lib/rules/sort-keys.js. TODO: How to maintain parity with eslint/sort-keys? + * @author Robin Thomas + */ + +import rule from "../../src/rules/sort-keys.js"; +import json from "../../src/index.js"; +import { RuleTester } from "eslint"; + +const ruleTester = new RuleTester({ + plugins: { + json, + }, + language: "json/json", +}); + +ruleTester.run("sort-keys", rule, { + valid: [ + // default (asc) + { code: '{"":1, "a":2}', options: [] }, + { code: '{"_":2, "a":1, "b":3}', options: [] }, + { code: '{"a":1, "b":3, "c":2}', options: [] }, + { code: '{"a":2, "b":3, "b_":1}', options: [] }, + { code: '{"C":3, "b_":1, "c":2}', options: [] }, + { code: '{"$":1, "A":3, "_":2, "a":4}', options: [] }, + { + code: `{$:1, 'A':3, "_":2, a:4}`, + language: `json/json5`, + options: [], + }, + { code: '{"1":1, "11":2, "2":4, "A":3}', options: [] }, + { code: '{"#":1, "Z":2, "À":3, "è":4}', options: [] }, + { code: '{"#":1, Z:2, À:3, è:4}', language: `json/json5`, options: [] }, + + // nested + { code: '{"a":1, "b":{"x":1, "y":1}, "c":1}', options: [] }, + { + code: ` + { + "a":1, + "b": { + "x":1, + "y":1 + }, + "c":1 + } + `, + options: [], + }, + { + code: ` + [ + { + "a":1, + "b": { + "x":1, + "y":1 + } + }, + { + "c":1, + "d":1 + } + ] + `, + options: [], + }, + { + code: ` + [ + { + "a":1, + b: { + "x":1, + y:1 + } + }, + { + "c":1, + d:1 + } + ] + `, + language: "json/json5", + options: [], + errors: [ + { + messageId: "sortKeys", + }, + ], + }, + + // asc + { + code: '{"_":2, "a":1, "b":3} // asc"', + language: "json/jsonc", + options: ["asc"], + }, + { code: '{"a":1, "b":3, "c":2}', options: ["asc"] }, + { code: '{"a":2, "b":3, "b_":1}', options: ["asc"] }, + { code: '{"C":3, "b_":1, "c":2}', options: ["asc"] }, + { code: '{"$":1, "A":3, "_":2, "a":4}', options: ["asc"] }, + { code: '{"1":1, "11":2, "2":4, "A":3}', options: ["asc"] }, + { code: '{"#":1, "Z":2, "À":3, "è":4}', options: ["asc"] }, + + // asc, minKeys should ignore unsorted keys when number of keys is less than minKeys + { code: '{"a":1, "c":2, "b":3}', options: ["asc", { minKeys: 4 }] }, + + // asc, insensitive + { + code: '{"_":2, "a":1, "b":3} // asc, insensitive', + language: "json/jsonc", + options: ["asc", { caseSensitive: false }], + }, + { + code: '{"a":1, "b":3, "c":2}', + options: ["asc", { caseSensitive: false }], + }, + { + code: '{"a":2, "b":3, "b_":1}', + options: ["asc", { caseSensitive: false }], + }, + { + code: '{"b_":1, "C":3, "c":2}', + options: ["asc", { caseSensitive: false }], + }, + { + code: '{"b_":1, "c":3, "C":2}', + options: ["asc", { caseSensitive: false }], + }, + { + code: '{"$":1, "_":2, "A":3, "a":4}', + options: ["asc", { caseSensitive: false }], + }, + { + code: '{"1":1, "11":2, "2":4, "A":3}', + options: ["asc", { caseSensitive: false }], + }, + { + code: '{"#":1, "Z":2, "À":3, "è":4}', + options: ["asc", { caseSensitive: false }], + }, + + // asc, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys + { + code: '{"$":1, "A":3, "_":2, "a":4}', + options: ["asc", { caseSensitive: false, minKeys: 5 }], + }, + + // asc, natural + { + code: '{"_":2, "a":1, "b":3} // asc, natural', + language: "json/jsonc", + options: ["asc", { natural: true }], + }, + { + code: '{"a":1, "b":3, "c":2}', + options: ["asc", { natural: true }], + }, + { + code: '{"a":2, "b":3, "b_":1}', + options: ["asc", { natural: true }], + }, + { + code: '{"C":3, "b_":1, "c":2}', + options: ["asc", { natural: true }], + }, + { + code: '{"$":1, "_":2, "A":3, "a":4}', + options: ["asc", { natural: true }], + }, + { + code: '{"1":1, "2":4, "11":2, "A":3}', + options: ["asc", { natural: true }], + }, + { + code: '{"#":1, "Z":2, "À":3, "è":4}', + options: ["asc", { natural: true }], + }, + + // asc, natural, minKeys should ignore unsorted keys when number of keys is less than minKeys + { + code: '{"b_":1, "a":2, "b":3}', + options: ["asc", { natural: true, minKeys: 4 }], + }, + + // asc, natural, insensitive + { + code: '{"_":2, "a":1, "b":3} // asc, natural, insensitive', + language: "json/jsonc", + options: ["asc", { natural: true, caseSensitive: false }], + }, + { + code: '{"a":1, "b":3, "c":2}', + options: ["asc", { natural: true, caseSensitive: false }], + }, + { + code: '{"a":2, "b":3, "b_":1}', + options: ["asc", { natural: true, caseSensitive: false }], + }, + { + code: '{"b_":1, "C":3, "c":2}', + options: ["asc", { natural: true, caseSensitive: false }], + }, + { + code: '{"b_":1, "c":3, "C":2}', + options: ["asc", { natural: true, caseSensitive: false }], + }, + { + code: '{"$":1, "_":2, "A":3, "a":4}', + options: ["asc", { natural: true, caseSensitive: false }], + }, + { + code: '{"1":1, "2":4, "11":2, "A":3}', + options: ["asc", { natural: true, caseSensitive: false }], + }, + { + code: '{"#":1, "Z":2, "À":3, "è":4}', + options: ["asc", { natural: true, caseSensitive: false }], + }, + + // asc, natural, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys + { + code: '{"a":1, "_":2, "b":3}', + options: [ + "asc", + { natural: true, caseSensitive: false, minKeys: 4 }, + ], + }, + + // desc + { + code: '{"b":3, "a":1, "_":2} // desc', + language: "json/jsonc", + options: ["desc"], + }, + { + code: `{b:3, "a":1, '_':2} // desc`, + language: "json/json5", + options: ["desc"], + }, + { code: '{"c":2, "b":3, "a":1}', options: ["desc"] }, + { code: '{"b_":1, "b":3, "a":2}', options: ["desc"] }, + { code: '{"c":2, "b_":1, "C":3}', options: ["desc"] }, + { code: '{"a":4, "_":2, "A":3, "$":1}', options: ["desc"] }, + { code: '{"A":3, "2":4, "11":2, "1":1}', options: ["desc"] }, + { code: '{"è":4, "À":3, "Z":2, "#":1}', options: ["desc"] }, + + // desc, minKeys should ignore unsorted keys when number of keys is less than minKeys + { + code: '{"a":1, "c":2, "b":3}', + options: ["desc", { minKeys: 4 }], + }, + + // desc, insensitive + { + code: '{"b":3, "a":1, "_":2} // desc, insensitive', + language: "json/jsonc", + options: ["desc", { caseSensitive: false }], + }, + { + code: '{"c":2, "b":3, "a":1}', + options: ["desc", { caseSensitive: false }], + }, + { + code: '{"b_":1, "b":3, "a":2}', + options: ["desc", { caseSensitive: false }], + }, + { + code: '{"c":2, "C":3, "b_":1}', + options: ["desc", { caseSensitive: false }], + }, + { + code: '{"C":2, "c":3, "b_":1}', + options: ["desc", { caseSensitive: false }], + }, + { + code: '{"a":4, "A":3, "_":2, "$":1}', + options: ["desc", { caseSensitive: false }], + }, + { + code: '{"A":3, "2":4, "11":2, "1":1}', + options: ["desc", { caseSensitive: false }], + }, + { + code: '{"è":4, "À":3, "Z":2, "#":1}', + options: ["desc", { caseSensitive: false }], + }, + + // desc, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys + { + code: '{"$":1, "_":2, "A":3, "a":4}', + options: ["desc", { caseSensitive: false, minKeys: 5 }], + }, + + // desc, natural + { + code: '{"b":3, "a":1, "_":2} // desc, natural', + language: "json/jsonc", + options: ["desc", { natural: true }], + }, + { + code: '{"c":2, "b":3, "a":1}', + options: ["desc", { natural: true }], + }, + { + code: '{"b_":1, "b":3, "a":2}', + options: ["desc", { natural: true }], + }, + { + code: '{"c":2, "b_":1, "C":3}', + options: ["desc", { natural: true }], + }, + { + code: '{"a":4, "A":3, "_":2, "$":1}', + options: ["desc", { natural: true }], + }, + { + code: '{"A":3, "11":2, "2":4, "1":1}', + options: ["desc", { natural: true }], + }, + { + code: '{"è":4, "À":3, "Z":2, "#":1}', + options: ["desc", { natural: true }], + }, + + // desc, natural, minKeys should ignore unsorted keys when number of keys is less than minKeys + { + code: '{"b_":1, "a":2, "b":3}', + options: ["desc", { natural: true, minKeys: 4 }], + }, + + // desc, natural, insensitive + { + code: '{"b":3, "a":1, "_":2} // desc, natural, insensitive', + language: "json/jsonc", + options: ["desc", { natural: true, caseSensitive: false }], + }, + { + code: '{"c":2, "b":3, "a":1}', + options: ["desc", { natural: true, caseSensitive: false }], + }, + { + code: '{"b_":1, "b":3, "a":2}', + options: ["desc", { natural: true, caseSensitive: false }], + }, + { + code: '{"c":2, "C":3, "b_":1}', + options: ["desc", { natural: true, caseSensitive: false }], + }, + { + code: '{"C":2, "c":3, "b_":1}', + options: ["desc", { natural: true, caseSensitive: false }], + }, + { + code: '{"a":4, "A":3, "_":2, "$":1}', + options: ["desc", { natural: true, caseSensitive: false }], + }, + { + code: '{"A":3, "11":2, "2":4, "1":1}', + options: ["desc", { natural: true, caseSensitive: false }], + }, + { + code: '{"è":4, "À":3, "Z":2, "#":1}', + options: ["desc", { natural: true, caseSensitive: false }], + }, + + // desc, natural, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys + { + code: '{"a":1, "_":2, "b":3}', + options: [ + "desc", + { natural: true, caseSensitive: false, minKeys: 4 }, + ], + }, + + // allowLineSeparatedGroups option + { + code: ` + { + "a": 1, + "b": 2, + "c": 3, + "e": 4, + "f": 5, + "g": 6 + } + `, + options: ["asc", { allowLineSeparatedGroups: false }], + }, + { + code: ` + { + "e": 1, + "f": 2, + "g": 3, + + "a": 4, + "b": 5, + "c": 6 + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + }, + { + code: ` + { + "b": 1, + + // comment + "a": 2, + "c": 3 + } + `, + language: "json/jsonc", + options: ["asc", { allowLineSeparatedGroups: true }], + }, + { + code: ` + { + "b": 1 + + , + + // comment + "a": 2, + "c": 3 + } + `, + language: "json/jsonc", + options: ["asc", { allowLineSeparatedGroups: true }], + }, + { + code: ` + { + "b": "/*", + + "a": "*/" + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + }, + { + code: ` + { + "b":1, + "c": { + "y":1, + "z":1, + + "x":1 + }, + + "a":1 + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + }, + + { + code: ` + { + "b":1, + a: { + "y":1, + x:1, + + "z":1 + }, + + c:1 + } + `, + language: `json/json5`, + options: ["desc", { allowLineSeparatedGroups: true }], + }, + + // Commas are not considered separating lines + { + code: ` + { + "b": 1 + + , + + "a": 2 + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + }, + { + code: ` + { + "a": 1 + + + , + "b": 2 + } + `, + options: ["asc", { allowLineSeparatedGroups: false }], + }, + { + code: ` + { + "b": 1 + // comment before comma + + , + "a": 2 + } + `, + language: "json/jsonc", + options: ["asc", { allowLineSeparatedGroups: true }], + }, + ], + invalid: [ + // default (asc) + { + code: '{"a":1, "":2} // default', + language: "json/jsonc", + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "", + prevName: "a", + }, + }, + ], + }, + { + code: '{"a":1, "_":2, "b":3} // default', + language: "json/jsonc", + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "_", + prevName: "a", + }, + }, + ], + }, + { + code: '{"a":1, "c":2, "b":3}', + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "b", + prevName: "c", + }, + }, + ], + }, + { + code: '{"b_":1, "a":2, "b":3}', + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "a", + prevName: "b_", + }, + }, + ], + }, + { + code: '{"b_":1, "c":2, "C":3}', + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "C", + prevName: "c", + }, + }, + ], + }, + { + code: '{"$":1, "_":2, "A":3, "a":4}', + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "A", + prevName: "_", + }, + }, + ], + }, + { + code: '{"1":1, "2":4, "A":3, "11":2}', + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "11", + prevName: "A", + }, + }, + ], + }, + { + code: '{"#":1, "À":3, "Z":2, "è":4}', + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "Z", + prevName: "À", + }, + }, + ], + }, + + // asc + { + code: '{"a":1, "_":2, "b":3} // asc', + language: "json/jsonc", + options: ["asc"], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "_", + prevName: "a", + }, + }, + ], + }, + { + code: '{"a":1, "c":2, "b":3}', + options: ["asc"], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "b", + prevName: "c", + }, + }, + ], + }, + { + code: '{"b_":1, "a":2, "b":3}', + options: ["asc"], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "a", + prevName: "b_", + }, + }, + ], + }, + { + code: '{"b_":1, "c":2, "C":3}', + options: ["asc"], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "C", + prevName: "c", + }, + }, + ], + }, + { + code: '{"$":1, "_":2, "A":3, "a":4}', + options: ["asc"], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "A", + prevName: "_", + }, + }, + ], + }, + { + code: '{"1":1, "2":4, "A":3, "11":2}', + options: ["asc"], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "11", + prevName: "A", + }, + }, + ], + }, + { + code: '{"#":1, "À":3, "Z":2, "è":4}', + options: ["asc"], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "Z", + prevName: "À", + }, + }, + ], + }, + + // asc, minKeys should error when number of keys is greater than or equal to minKeys + { + code: '{"a":1, "_":2, "b":3}', + options: ["asc", { minKeys: 3 }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "_", + prevName: "a", + }, + }, + ], + }, + + // asc, insensitive + { + code: '{"a":1, "_":2, "b":3} // asc, insensitive', + language: "json/jsonc", + options: ["asc", { caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "ascending", + thisName: "_", + prevName: "a", + }, + }, + ], + }, + { + code: '{"a":1, "c":2, "b":3}', + options: ["asc", { caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "ascending", + thisName: "b", + prevName: "c", + }, + }, + ], + }, + { + code: '{"b_":1, "a":2, "b":3}', + options: ["asc", { caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "ascending", + thisName: "a", + prevName: "b_", + }, + }, + ], + }, + { + code: '{"$":1, "A":3, "_":2, "a":4}', + options: ["asc", { caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "ascending", + thisName: "_", + prevName: "A", + }, + }, + ], + }, + { + code: '{"1":1, "2":4, "A":3, "11":2}', + options: ["asc", { caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "ascending", + thisName: "11", + prevName: "A", + }, + }, + ], + }, + { + code: '{"#":1, "À":3, "Z":2, "è":4}', + options: ["asc", { caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "ascending", + thisName: "Z", + prevName: "À", + }, + }, + ], + }, + + // asc, insensitive, minKeys should error when number of keys is greater than or equal to minKeys + { + code: '{"a":1, "_":2, "b":3}', + options: ["asc", { caseSensitive: false, minKeys: 3 }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "ascending", + thisName: "_", + prevName: "a", + }, + }, + ], + }, + + // asc, natural + { + code: '{"a":1, "_":2, "b":3} // asc, natural', + language: "json/jsonc", + options: ["asc", { natural: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "ascending", + thisName: "_", + prevName: "a", + }, + }, + ], + }, + { + code: '{"a":1, "c":2, "b":3}', + options: ["asc", { natural: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "ascending", + thisName: "b", + prevName: "c", + }, + }, + ], + }, + { + code: '{"b_":1, "a":2, "b":3}', + options: ["asc", { natural: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "ascending", + thisName: "a", + prevName: "b_", + }, + }, + ], + }, + { + code: '{"b_":1, "c":2, "C":3}', + options: ["asc", { natural: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "ascending", + thisName: "C", + prevName: "c", + }, + }, + ], + }, + { + code: '{"$":1, "A":3, "_":2, "a":4}', + options: ["asc", { natural: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "ascending", + thisName: "_", + prevName: "A", + }, + }, + ], + }, + { + code: '{"1":1, "2":4, "A":3, "11":2}', + options: ["asc", { natural: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "ascending", + thisName: "11", + prevName: "A", + }, + }, + ], + }, + { + code: '{"#":1, "À":3, "Z":2, "è":4}', + options: ["asc", { natural: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "ascending", + thisName: "Z", + prevName: "À", + }, + }, + ], + }, + + // asc, natural, minKeys should error when number of keys is greater than or equal to minKeys + { + code: '{"a":1, "_":2, "b":3}', + options: ["asc", { natural: true, minKeys: 2 }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "ascending", + thisName: "_", + prevName: "a", + }, + }, + ], + }, + + // asc, natural, insensitive + { + code: '{"a":1, "_":2, "b":3} // asc, natural, insensitive', + language: "json/jsonc", + options: ["asc", { natural: true, caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "ascending", + thisName: "_", + prevName: "a", + }, + }, + ], + }, + { + code: '{"a":1, "c":2, "b":3}', + options: ["asc", { natural: true, caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "ascending", + thisName: "b", + prevName: "c", + }, + }, + ], + }, + { + code: '{"b_":1, "a":2, "b":3}', + options: ["asc", { natural: true, caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "ascending", + thisName: "a", + prevName: "b_", + }, + }, + ], + }, + { + code: '{"$":1, "A":3, "_":2, "a":4}', + options: ["asc", { natural: true, caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "ascending", + thisName: "_", + prevName: "A", + }, + }, + ], + }, + { + code: '{"1":1, "11":2, "2":4, "A":3}', + options: ["asc", { natural: true, caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "ascending", + thisName: "2", + prevName: "11", + }, + }, + ], + }, + { + code: '{"#":1, "À":3, "Z":2, "è":4}', + options: ["asc", { natural: true, caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "ascending", + thisName: "Z", + prevName: "À", + }, + }, + ], + }, + + // asc, natural, insensitive, minKeys should error when number of keys is greater than or equal to minKeys + { + code: '{"a":1, "_":2, "b":3}', + options: [ + "asc", + { natural: true, caseSensitive: false, minKeys: 3 }, + ], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "ascending", + thisName: "_", + prevName: "a", + }, + }, + ], + }, + + // desc + { + code: '{"":1, "a":2} // desc', + language: "json/jsonc", + options: ["desc"], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "descending", + thisName: "a", + prevName: "", + }, + }, + ], + }, + { + code: '{"a":1, "_":2, "b":3} // desc', + language: "json/jsonc", + options: ["desc"], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "descending", + thisName: "b", + prevName: "_", + }, + }, + ], + }, + { + code: '{"a":1, "c":2, "b":3}', + options: ["desc"], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "descending", + thisName: "c", + prevName: "a", + }, + }, + ], + }, + { + code: '{"b_":1, "a":2, "b":3}', + options: ["desc"], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "descending", + thisName: "b", + prevName: "a", + }, + }, + ], + }, + { + code: '{"b_":1, "c":2, "C":3}', + options: ["desc"], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "descending", + thisName: "c", + prevName: "b_", + }, + }, + ], + }, + { + code: '{"$":1, "_":2, "A":3, "a":4}', + options: ["desc"], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "descending", + thisName: "_", + prevName: "$", + }, + }, + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "descending", + thisName: "a", + prevName: "A", + }, + }, + ], + }, + { + code: '{"1":1, "2":4, "A":3, "11":2}', + options: ["desc"], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "descending", + thisName: "2", + prevName: "1", + }, + }, + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "descending", + thisName: "A", + prevName: "2", + }, + }, + ], + }, + { + code: '{"#":1, "À":3, "Z":2, "è":4}', + options: ["desc"], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "descending", + thisName: "À", + prevName: "#", + }, + }, + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "descending", + thisName: "è", + prevName: "Z", + }, + }, + ], + }, + + // desc, minKeys should error when number of keys is greater than or equal to minKeys + { + code: '{"a":1, "_":2, "b":3}', + options: ["desc", { minKeys: 3 }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "descending", + thisName: "b", + prevName: "_", + }, + }, + ], + }, + + // desc, insensitive + { + code: '{"a":1, "_":2, "b":3} // desc, insensitive', + language: "json/jsonc", + options: ["desc", { caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "descending", + thisName: "b", + prevName: "_", + }, + }, + ], + }, + { + code: '{"a":1, "c":2, "b":3}', + options: ["desc", { caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "descending", + thisName: "c", + prevName: "a", + }, + }, + ], + }, + { + code: '{"b_":1, "a":2, "b":3}', + options: ["desc", { caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "descending", + thisName: "b", + prevName: "a", + }, + }, + ], + }, + { + code: '{"b_":1, "c":2, "C":3}', + options: ["desc", { caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "descending", + thisName: "c", + prevName: "b_", + }, + }, + ], + }, + { + code: '{"$":1, "_":2, "A":3, "a":4}', + options: ["desc", { caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "descending", + thisName: "_", + prevName: "$", + }, + }, + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "descending", + thisName: "A", + prevName: "_", + }, + }, + ], + }, + { + code: '{"1":1, "2":4, "A":3, "11":2}', + options: ["desc", { caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "descending", + thisName: "2", + prevName: "1", + }, + }, + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "descending", + thisName: "A", + prevName: "2", + }, + }, + ], + }, + { + code: '{"#":1, "À":3, "Z":2, "è":4}', + options: ["desc", { caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "descending", + thisName: "À", + prevName: "#", + }, + }, + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "descending", + thisName: "è", + prevName: "Z", + }, + }, + ], + }, + + // desc, insensitive should error when number of keys is greater than or equal to minKeys + { + code: '{"a":1, "_":2, "b":3}', + options: ["desc", { caseSensitive: false, minKeys: 2 }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "insensitive", + direction: "descending", + thisName: "b", + prevName: "_", + }, + }, + ], + }, + + // desc, natural + { + code: '{"a":1, "_":2, "b":3} // desc, natural', + language: "json/jsonc", + options: ["desc", { natural: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "descending", + thisName: "b", + prevName: "_", + }, + }, + ], + }, + { + code: '{"a":1, "c":2, "b":3}', + options: ["desc", { natural: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "descending", + thisName: "c", + prevName: "a", + }, + }, + ], + }, + { + code: '{"b_":1, "a":2, "b":3}', + options: ["desc", { natural: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "descending", + thisName: "b", + prevName: "a", + }, + }, + ], + }, + { + code: '{"b_":1, "c":2, "C":3}', + options: ["desc", { natural: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "descending", + thisName: "c", + prevName: "b_", + }, + }, + ], + }, + { + code: '{"$":1, "_":2, "A":3, "a":4}', + options: ["desc", { natural: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "descending", + thisName: "_", + prevName: "$", + }, + }, + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "descending", + thisName: "A", + prevName: "_", + }, + }, + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "descending", + thisName: "a", + prevName: "A", + }, + }, + ], + }, + { + code: '{"1":1, "2":4, "A":3, "11":2}', + options: ["desc", { natural: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "descending", + thisName: "2", + prevName: "1", + }, + }, + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "descending", + thisName: "A", + prevName: "2", + }, + }, + ], + }, + { + code: '{"#":1, "À":3, "Z":2, "è":4}', + options: ["desc", { natural: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "descending", + thisName: "À", + prevName: "#", + }, + }, + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "descending", + thisName: "è", + prevName: "Z", + }, + }, + ], + }, + + // desc, natural should error when number of keys is greater than or equal to minKeys + { + code: '{"a":1, "_":2, "b":3}', + options: ["desc", { natural: true, minKeys: 3 }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "sensitive", + direction: "descending", + thisName: "b", + prevName: "_", + }, + }, + ], + }, + + // desc, natural, insensitive + { + code: '{"a":1, "_":2, "b":3} // desc, natural, insensitive', + language: "json/jsonc", + options: ["desc", { natural: true, caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "descending", + thisName: "b", + prevName: "_", + }, + }, + ], + }, + { + code: '{"a":1, "c":2, "b":3}', + options: ["desc", { natural: true, caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "descending", + thisName: "c", + prevName: "a", + }, + }, + ], + }, + { + code: '{"b_":1, "a":2, "b":3}', + options: ["desc", { natural: true, caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "descending", + thisName: "b", + prevName: "a", + }, + }, + ], + }, + { + code: '{"b_":1, "c":2, "C":3}', + options: ["desc", { natural: true, caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "descending", + thisName: "c", + prevName: "b_", + }, + }, + ], + }, + { + code: '{"$":1, "_":2, "A":3, "a":4}', + options: ["desc", { natural: true, caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "descending", + thisName: "_", + prevName: "$", + }, + }, + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "descending", + thisName: "A", + prevName: "_", + }, + }, + ], + }, + { + code: '{"1":1, "2":4, "11":2, "A":3}', + options: ["desc", { natural: true, caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "descending", + thisName: "2", + prevName: "1", + }, + }, + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "descending", + thisName: "11", + prevName: "2", + }, + }, + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "descending", + thisName: "A", + prevName: "11", + }, + }, + ], + }, + { + code: '{"#":1, "À":3, "Z":2, "è":4}', + options: ["desc", { natural: true, caseSensitive: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "descending", + thisName: "À", + prevName: "#", + }, + }, + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "descending", + thisName: "è", + prevName: "Z", + }, + }, + ], + }, + + // desc, natural, insensitive should error when number of keys is greater than or equal to minKeys + { + code: '{"a":1, "_":2, "b":3}', + options: [ + "desc", + { natural: true, caseSensitive: false, minKeys: 2 }, + ], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "natural", + sensitivity: "insensitive", + direction: "descending", + thisName: "b", + prevName: "_", + }, + }, + ], + }, + + // When allowLineSeparatedGroups option is false + { + code: ` + { + "b": 1, + "c": 2, + "a": 3 + } + `, + options: ["asc", { allowLineSeparatedGroups: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "a", + prevName: "c", + }, + }, + ], + }, + { + code: ` + { + "b": 1, + + "c": 2, + + "a": 3 + } + `, + options: ["asc", { allowLineSeparatedGroups: false }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "a", + prevName: "c", + }, + }, + ], + }, + + // When allowLineSeparatedGroups option is true + { + code: ` + { + "b": "/*", + "a": "*/" + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "a", + prevName: "b", + }, + }, + ], + }, + { + code: ` + { + "b": 1 + // comment before comma + , "a": 2 + } + `, + language: "json/jsonc", + options: ["asc", { allowLineSeparatedGroups: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "a", + prevName: "b", + }, + }, + ], + }, + { + code: ` + [ + { + "b":1, + "a": { + "x":1, + "y":1 + }, + + "d":1, + "c":1 + } + ] + `, + options: ["desc", { allowLineSeparatedGroups: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "descending", + thisName: "y", + prevName: "x", + }, + }, + ], + }, + { + code: ` + { + "b": /*foo */ 1, + // some multiline comment + // using line comment style + "a": 2 // "a" and "b" are not line separated + } + `, + language: "json/jsonc", + options: ["asc", { allowLineSeparatedGroups: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "a", + prevName: "b", + }, + }, + ], + }, + { + code: ` + { + "b": 1, + /* some multiline comment + using block comment style */ + /* the empty line... + + ...in this one doesn't count */ + "a": 2 // "a" and "b" are not line separated + } + `, + language: "json/jsonc", + options: ["asc", { allowLineSeparatedGroups: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "a", + prevName: "b", + }, + }, + ], + }, + { + code: ` + { + "b": 1 + , + "a": 2 + } + `, + options: ["asc", { allowLineSeparatedGroups: true }], + errors: [ + { + messageId: "sortKeys", + data: { + sortName: "alphanumeric", + sensitivity: "sensitive", + direction: "ascending", + thisName: "a", + prevName: "b", + }, + }, + ], + }, + ], +});