diff --git a/src/rules/sort-keys.js b/src/rules/sort-keys.js index ce4ceec..056ebb5 100644 --- a/src/rules/sort-keys.js +++ b/src/rules/sort-keys.js @@ -96,6 +96,8 @@ const rule = { meta: { type: "suggestion", + fixable: "code", + defaultOptions: [ "asc", { @@ -198,6 +200,33 @@ const rule = { return false; } + /** + * Compares two key names according to the rule options and returns a number + * suitable for Array.prototype.sort. + * @param {string} a First key to compare. + * @param {string} b Second key to compare. + * @returns {number} Negative if a < b, positive if a > b, 0 if equal. + */ + function compareKeys(a, b) { + let a1 = a; + let b1 = b; + if (!caseSensitive) { + a1 = a1.toLowerCase(); + b1 = b1.toLowerCase(); + } + if (natural) { + const r = naturalCompare(a1, b1); + return directionShort === "asc" ? r : -r; + } + if (a1 < b1) { + return directionShort === "asc" ? -1 : 1; + } + if (a1 > b1) { + return directionShort === "asc" ? 1 : -1; + } + return 0; + } + return { Object(node) { let prevMember; @@ -226,6 +255,126 @@ const rule = { sensitivity, sortName, }, + fix(fixer) { + const text = context.sourceCode.text; + + // If there are any comment tokens within this object, do not attempt to autofix + const hasComments = + context.sourceCode.comments.some( + comment => + comment.range[0] >= node.range[0] && + comment.range[1] <= node.range[1], + ); + + if (hasComments) { + return null; + } + + const groups = []; + let groupStart = 0; + for (let i = 1; i < node.members.length; i++) { + if ( + allowLineSeparatedGroups && + isLineSeparated( + node.members[i - 1], + node.members[i], + ) + ) { + groups.push({ + start: groupStart, + end: i - 1, + }); + groupStart = i; + } + } + groups.push({ + start: groupStart, + end: node.members.length - 1, + }); + + const fixes = []; + for (const group of groups) { + const groupLen = + group.end - group.start + 1; + if (groupLen <= 1) { + continue; + } + + let isSorted = true; + for ( + let i = group.start + 1; + i <= group.end; + i++ + ) { + const prevKey = getKey( + node.members[i - 1], + ); + const thisKey = getKey(node.members[i]); + if (compareKeys(prevKey, thisKey) > 0) { + isSorted = false; + break; + } + } + if (isSorted) { + continue; + } + + const items = []; + const seps = []; + for ( + let i = group.start; + i <= group.end; + i++ + ) { + const m = node.members[i]; + items.push({ + key: getKey(m), + start: m.range[0], + end: m.range[1], + text: text.slice( + m.range[0], + m.range[1], + ), + }); + + if (i < group.end) { + seps.push( + text.slice( + node.members[i].range[1], + node.members[i + 1] + .range[0], + ), + ); + } + } + + const replaceStart = items[0].start; + const replaceEnd = items.at(-1).end; + + const rebuilt = items + .slice() + .sort((a, b) => + compareKeys(a.key, b.key), + ) + .map( + (item, i) => + item.text + + (i < seps.length + ? seps[i] + : ""), + ) + .join(""); + + fixes.push( + fixer.replaceTextRange( + [replaceStart, replaceEnd], + rebuilt, + ), + ); + } + + return fixes; + }, }); } diff --git a/tests/rules/sort-keys.test.js b/tests/rules/sort-keys.test.js index c15bb15..f1fff8f 100644 --- a/tests/rules/sort-keys.test.js +++ b/tests/rules/sort-keys.test.js @@ -518,6 +518,7 @@ ruleTester.run("sort-keys", rule, { // default (asc) { code: '{"a":1, "":2} // default', + output: '{"":2, "a":1} // default', language: "json/jsonc", errors: [ { @@ -534,6 +535,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"a":1, "_":2, "b":3} // default', + output: '{"_":2, "a":1, "b":3} // default', language: "json/jsonc", errors: [ { @@ -550,6 +552,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"a":1, "c":2, "b":3}', + output: '{"a":1, "b":3, "c":2}', errors: [ { messageId: "sortKeys", @@ -565,6 +568,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"b_":1, "a":2, "b":3}', + output: '{"a":2, "b":3, "b_":1}', errors: [ { messageId: "sortKeys", @@ -580,6 +584,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"b_":1, "c":2, "C":3}', + output: '{"C":3, "b_":1, "c":2}', errors: [ { messageId: "sortKeys", @@ -595,6 +600,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"$":1, "_":2, "A":3, "a":4}', + output: '{"$":1, "A":3, "_":2, "a":4}', errors: [ { messageId: "sortKeys", @@ -610,6 +616,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"1":1, "2":4, "A":3, "11":2}', + output: '{"1":1, "11":2, "2":4, "A":3}', errors: [ { messageId: "sortKeys", @@ -625,6 +632,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"#":1, "À":3, "Z":2, "è":4}', + output: '{"#":1, "Z":2, "À":3, "è":4}', errors: [ { messageId: "sortKeys", @@ -642,6 +650,7 @@ ruleTester.run("sort-keys", rule, { // asc { code: '{"a":1, "_":2, "b":3} // asc', + output: '{"_":2, "a":1, "b":3} // asc', language: "json/jsonc", options: ["asc"], errors: [ @@ -659,6 +668,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"a":1, "c":2, "b":3}', + output: '{"a":1, "b":3, "c":2}', options: ["asc"], errors: [ { @@ -675,6 +685,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"b_":1, "a":2, "b":3}', + output: '{"a":2, "b":3, "b_":1}', options: ["asc"], errors: [ { @@ -691,6 +702,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"b_":1, "c":2, "C":3}', + output: '{"C":3, "b_":1, "c":2}', options: ["asc"], errors: [ { @@ -707,6 +719,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"$":1, "_":2, "A":3, "a":4}', + output: '{"$":1, "A":3, "_":2, "a":4}', options: ["asc"], errors: [ { @@ -723,6 +736,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"1":1, "2":4, "A":3, "11":2}', + output: '{"1":1, "11":2, "2":4, "A":3}', options: ["asc"], errors: [ { @@ -739,6 +753,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"#":1, "À":3, "Z":2, "è":4}', + output: '{"#":1, "Z":2, "À":3, "è":4}', options: ["asc"], errors: [ { @@ -757,6 +772,7 @@ ruleTester.run("sort-keys", rule, { // asc, minKeys should error when number of keys is greater than or equal to minKeys { code: '{"a":1, "_":2, "b":3}', + output: '{"_":2, "a":1, "b":3}', options: ["asc", { minKeys: 3 }], errors: [ { @@ -775,6 +791,7 @@ ruleTester.run("sort-keys", rule, { // asc, insensitive { code: '{"a":1, "_":2, "b":3} // asc, insensitive', + output: '{"_":2, "a":1, "b":3} // asc, insensitive', language: "json/jsonc", options: ["asc", { caseSensitive: false }], errors: [ @@ -792,6 +809,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"a":1, "c":2, "b":3}', + output: '{"a":1, "b":3, "c":2}', options: ["asc", { caseSensitive: false }], errors: [ { @@ -808,6 +826,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"b_":1, "a":2, "b":3}', + output: '{"a":2, "b":3, "b_":1}', options: ["asc", { caseSensitive: false }], errors: [ { @@ -824,6 +843,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"$":1, "A":3, "_":2, "a":4}', + output: '{"$":1, "_":2, "A":3, "a":4}', options: ["asc", { caseSensitive: false }], errors: [ { @@ -840,6 +860,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"1":1, "2":4, "A":3, "11":2}', + output: '{"1":1, "11":2, "2":4, "A":3}', options: ["asc", { caseSensitive: false }], errors: [ { @@ -856,6 +877,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"#":1, "À":3, "Z":2, "è":4}', + output: '{"#":1, "Z":2, "À":3, "è":4}', options: ["asc", { caseSensitive: false }], errors: [ { @@ -874,6 +896,7 @@ ruleTester.run("sort-keys", rule, { // asc, insensitive, minKeys should error when number of keys is greater than or equal to minKeys { code: '{"a":1, "_":2, "b":3}', + output: '{"_":2, "a":1, "b":3}', options: ["asc", { caseSensitive: false, minKeys: 3 }], errors: [ { @@ -892,6 +915,7 @@ ruleTester.run("sort-keys", rule, { // asc, natural { code: '{"a":1, "_":2, "b":3} // asc, natural', + output: '{"_":2, "a":1, "b":3} // asc, natural', language: "json/jsonc", options: ["asc", { natural: true }], errors: [ @@ -909,6 +933,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"a":1, "c":2, "b":3}', + output: '{"a":1, "b":3, "c":2}', options: ["asc", { natural: true }], errors: [ { @@ -925,6 +950,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"b_":1, "a":2, "b":3}', + output: '{"a":2, "b":3, "b_":1}', options: ["asc", { natural: true }], errors: [ { @@ -941,6 +967,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"b_":1, "c":2, "C":3}', + output: '{"C":3, "b_":1, "c":2}', options: ["asc", { natural: true }], errors: [ { @@ -957,6 +984,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"$":1, "A":3, "_":2, "a":4}', + output: '{"$":1, "_":2, "A":3, "a":4}', options: ["asc", { natural: true }], errors: [ { @@ -973,6 +1001,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"1":1, "2":4, "A":3, "11":2}', + output: '{"1":1, "2":4, "11":2, "A":3}', options: ["asc", { natural: true }], errors: [ { @@ -989,6 +1018,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"#":1, "À":3, "Z":2, "è":4}', + output: '{"#":1, "Z":2, "À":3, "è":4}', options: ["asc", { natural: true }], errors: [ { @@ -1007,6 +1037,7 @@ ruleTester.run("sort-keys", rule, { // asc, natural, minKeys should error when number of keys is greater than or equal to minKeys { code: '{"a":1, "_":2, "b":3}', + output: '{"_":2, "a":1, "b":3}', options: ["asc", { natural: true, minKeys: 2 }], errors: [ { @@ -1025,6 +1056,7 @@ ruleTester.run("sort-keys", rule, { // asc, natural, insensitive { code: '{"a":1, "_":2, "b":3} // asc, natural, insensitive', + output: '{"_":2, "a":1, "b":3} // asc, natural, insensitive', language: "json/jsonc", options: ["asc", { natural: true, caseSensitive: false }], errors: [ @@ -1042,6 +1074,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"a":1, "c":2, "b":3}', + output: '{"a":1, "b":3, "c":2}', options: ["asc", { natural: true, caseSensitive: false }], errors: [ { @@ -1058,6 +1091,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"b_":1, "a":2, "b":3}', + output: '{"a":2, "b":3, "b_":1}', options: ["asc", { natural: true, caseSensitive: false }], errors: [ { @@ -1074,6 +1108,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"$":1, "A":3, "_":2, "a":4}', + output: '{"$":1, "_":2, "A":3, "a":4}', options: ["asc", { natural: true, caseSensitive: false }], errors: [ { @@ -1090,6 +1125,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"1":1, "11":2, "2":4, "A":3}', + output: '{"1":1, "2":4, "11":2, "A":3}', options: ["asc", { natural: true, caseSensitive: false }], errors: [ { @@ -1106,6 +1142,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"#":1, "À":3, "Z":2, "è":4}', + output: '{"#":1, "Z":2, "À":3, "è":4}', options: ["asc", { natural: true, caseSensitive: false }], errors: [ { @@ -1124,6 +1161,7 @@ ruleTester.run("sort-keys", rule, { // asc, natural, insensitive, minKeys should error when number of keys is greater than or equal to minKeys { code: '{"a":1, "_":2, "b":3}', + output: '{"_":2, "a":1, "b":3}', options: [ "asc", { natural: true, caseSensitive: false, minKeys: 3 }, @@ -1145,6 +1183,7 @@ ruleTester.run("sort-keys", rule, { // desc { code: '{"":1, "a":2} // desc', + output: '{"a":2, "":1} // desc', language: "json/jsonc", options: ["desc"], errors: [ @@ -1162,6 +1201,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"a":1, "_":2, "b":3} // desc', + output: '{"b":3, "a":1, "_":2} // desc', language: "json/jsonc", options: ["desc"], errors: [ @@ -1179,6 +1219,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"a":1, "c":2, "b":3}', + output: '{"c":2, "b":3, "a":1}', options: ["desc"], errors: [ { @@ -1195,6 +1236,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"b_":1, "a":2, "b":3}', + output: '{"b_":1, "b":3, "a":2}', options: ["desc"], errors: [ { @@ -1211,6 +1253,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"b_":1, "c":2, "C":3}', + output: '{"c":2, "b_":1, "C":3}', options: ["desc"], errors: [ { @@ -1227,6 +1270,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"$":1, "_":2, "A":3, "a":4}', + output: '{"a":4, "_":2, "A":3, "$":1}', options: ["desc"], errors: [ { @@ -1253,6 +1297,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"1":1, "2":4, "A":3, "11":2}', + output: '{"A":3, "2":4, "11":2, "1":1}', options: ["desc"], errors: [ { @@ -1279,6 +1324,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"#":1, "À":3, "Z":2, "è":4}', + output: '{"è":4, "À":3, "Z":2, "#":1}', options: ["desc"], errors: [ { @@ -1307,6 +1353,7 @@ ruleTester.run("sort-keys", rule, { // desc, minKeys should error when number of keys is greater than or equal to minKeys { code: '{"a":1, "_":2, "b":3}', + output: '{"b":3, "a":1, "_":2}', options: ["desc", { minKeys: 3 }], errors: [ { @@ -1325,6 +1372,7 @@ ruleTester.run("sort-keys", rule, { // desc, insensitive { code: '{"a":1, "_":2, "b":3} // desc, insensitive', + output: '{"b":3, "a":1, "_":2} // desc, insensitive', language: "json/jsonc", options: ["desc", { caseSensitive: false }], errors: [ @@ -1342,6 +1390,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"a":1, "c":2, "b":3}', + output: '{"c":2, "b":3, "a":1}', options: ["desc", { caseSensitive: false }], errors: [ { @@ -1358,6 +1407,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"b_":1, "a":2, "b":3}', + output: '{"b_":1, "b":3, "a":2}', options: ["desc", { caseSensitive: false }], errors: [ { @@ -1374,6 +1424,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"b_":1, "c":2, "C":3}', + output: '{"c":2, "C":3, "b_":1}', options: ["desc", { caseSensitive: false }], errors: [ { @@ -1390,6 +1441,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"$":1, "_":2, "A":3, "a":4}', + output: '{"A":3, "a":4, "_":2, "$":1}', options: ["desc", { caseSensitive: false }], errors: [ { @@ -1416,6 +1468,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"1":1, "2":4, "A":3, "11":2}', + output: '{"A":3, "2":4, "11":2, "1":1}', options: ["desc", { caseSensitive: false }], errors: [ { @@ -1442,6 +1495,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"#":1, "À":3, "Z":2, "è":4}', + output: '{"è":4, "À":3, "Z":2, "#":1}', options: ["desc", { caseSensitive: false }], errors: [ { @@ -1470,6 +1524,7 @@ ruleTester.run("sort-keys", rule, { // desc, insensitive should error when number of keys is greater than or equal to minKeys { code: '{"a":1, "_":2, "b":3}', + output: '{"b":3, "a":1, "_":2}', options: ["desc", { caseSensitive: false, minKeys: 2 }], errors: [ { @@ -1488,6 +1543,7 @@ ruleTester.run("sort-keys", rule, { // desc, natural { code: '{"a":1, "_":2, "b":3} // desc, natural', + output: '{"b":3, "a":1, "_":2} // desc, natural', language: "json/jsonc", options: ["desc", { natural: true }], errors: [ @@ -1505,6 +1561,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"a":1, "c":2, "b":3}', + output: '{"c":2, "b":3, "a":1}', options: ["desc", { natural: true }], errors: [ { @@ -1521,6 +1578,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"b_":1, "a":2, "b":3}', + output: '{"b_":1, "b":3, "a":2}', options: ["desc", { natural: true }], errors: [ { @@ -1537,6 +1595,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"b_":1, "c":2, "C":3}', + output: '{"c":2, "b_":1, "C":3}', options: ["desc", { natural: true }], errors: [ { @@ -1553,6 +1612,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"$":1, "_":2, "A":3, "a":4}', + output: '{"a":4, "A":3, "_":2, "$":1}', options: ["desc", { natural: true }], errors: [ { @@ -1589,6 +1649,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"1":1, "2":4, "A":3, "11":2}', + output: '{"A":3, "11":2, "2":4, "1":1}', options: ["desc", { natural: true }], errors: [ { @@ -1615,6 +1676,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"#":1, "À":3, "Z":2, "è":4}', + output: '{"è":4, "À":3, "Z":2, "#":1}', options: ["desc", { natural: true }], errors: [ { @@ -1643,6 +1705,7 @@ ruleTester.run("sort-keys", rule, { // desc, natural should error when number of keys is greater than or equal to minKeys { code: '{"a":1, "_":2, "b":3}', + output: '{"b":3, "a":1, "_":2}', options: ["desc", { natural: true, minKeys: 3 }], errors: [ { @@ -1661,6 +1724,7 @@ ruleTester.run("sort-keys", rule, { // desc, natural, insensitive { code: '{"a":1, "_":2, "b":3} // desc, natural, insensitive', + output: '{"b":3, "a":1, "_":2} // desc, natural, insensitive', language: "json/jsonc", options: ["desc", { natural: true, caseSensitive: false }], errors: [ @@ -1678,6 +1742,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"a":1, "c":2, "b":3}', + output: '{"c":2, "b":3, "a":1}', options: ["desc", { natural: true, caseSensitive: false }], errors: [ { @@ -1694,6 +1759,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"b_":1, "a":2, "b":3}', + output: '{"b_":1, "b":3, "a":2}', options: ["desc", { natural: true, caseSensitive: false }], errors: [ { @@ -1710,6 +1776,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"b_":1, "c":2, "C":3}', + output: '{"c":2, "C":3, "b_":1}', options: ["desc", { natural: true, caseSensitive: false }], errors: [ { @@ -1726,6 +1793,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"$":1, "_":2, "A":3, "a":4}', + output: '{"A":3, "a":4, "_":2, "$":1}', options: ["desc", { natural: true, caseSensitive: false }], errors: [ { @@ -1752,6 +1820,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"1":1, "2":4, "11":2, "A":3}', + output: '{"A":3, "11":2, "2":4, "1":1}', options: ["desc", { natural: true, caseSensitive: false }], errors: [ { @@ -1788,6 +1857,7 @@ ruleTester.run("sort-keys", rule, { }, { code: '{"#":1, "À":3, "Z":2, "è":4}', + output: '{"è":4, "À":3, "Z":2, "#":1}', options: ["desc", { natural: true, caseSensitive: false }], errors: [ { @@ -1816,6 +1886,7 @@ ruleTester.run("sort-keys", rule, { // desc, natural, insensitive should error when number of keys is greater than or equal to minKeys { code: '{"a":1, "_":2, "b":3}', + output: '{"b":3, "a":1, "_":2}', options: [ "desc", { natural: true, caseSensitive: false, minKeys: 2 }, @@ -1843,6 +1914,13 @@ ruleTester.run("sort-keys", rule, { "a": 3 } `, + output: ` + { + "a": 3, + "b": 1, + "c": 2 + } + `, options: ["asc", { allowLineSeparatedGroups: false }], errors: [ { @@ -1867,6 +1945,15 @@ ruleTester.run("sort-keys", rule, { "a": 3 } `, + output: ` + { + "a": 3, + + "b": 1, + + "c": 2 + } + `, options: ["asc", { allowLineSeparatedGroups: false }], errors: [ { @@ -1890,6 +1977,12 @@ ruleTester.run("sort-keys", rule, { "a": "*/" } `, + output: ` + { + "a": "*/", + "b": "/*" + } + `, options: ["asc", { allowLineSeparatedGroups: true }], errors: [ { @@ -1937,6 +2030,20 @@ ruleTester.run("sort-keys", rule, { "y":1 }, + "d":1, + "c":1 + } + ] + `, + output: ` + [ + { + "b":1, + "a": { + "y":1, + "x":1 + }, + "d":1, "c":1 } @@ -2015,6 +2122,13 @@ ruleTester.run("sort-keys", rule, { "a": 2 } `, + output: ` + { + "a": 2 + , + "b": 1 + } + `, options: ["asc", { allowLineSeparatedGroups: true }], errors: [ {