Skip to content

Commit e68c247

Browse files
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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]>
1 parent d09d8cf commit e68c247

File tree

5 files changed

+2216
-1
lines changed

5 files changed

+2216
-1
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export default [
161161
like integers but are too large, and
162162
[subnormal numbers](https://en.wikipedia.org/wiki/Subnormal_number).
163163
- `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.
164165
- `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.
165166

166167
## Configuration Comments

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@
6565
"dependencies": {
6666
"@eslint/core": "^0.10.0",
6767
"@eslint/plugin-kit": "^0.2.5",
68-
"@humanwhocodes/momoa": "^3.3.4"
68+
"@humanwhocodes/momoa": "^3.3.4",
69+
"natural-compare": "^1.4.0"
6970
},
7071
"devDependencies": {
7172
"@types/eslint": "^8.56.10",

src/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import noDuplicateKeys from "./rules/no-duplicate-keys.js";
1313
import noEmptyKeys from "./rules/no-empty-keys.js";
1414
import noUnsafeValues from "./rules/no-unsafe-values.js";
1515
import noUnnormalizedKeys from "./rules/no-unnormalized-keys.js";
16+
import sortKeys from "./rules/sort-keys.js";
1617
import topLevelInterop from "./rules/top-level-interop.js";
1718

1819
//-----------------------------------------------------------------------------
@@ -34,6 +35,7 @@ const plugin = {
3435
"no-empty-keys": noEmptyKeys,
3536
"no-unsafe-values": noUnsafeValues,
3637
"no-unnormalized-keys": noUnnormalizedKeys,
38+
"sort-keys": sortKeys,
3739
"top-level-interop": topLevelInterop,
3840
},
3941
configs: {

src/rules/sort-keys.js

+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/**
2+
* @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
3+
* @author Robin Thomas
4+
*/
5+
6+
import naturalCompare from "natural-compare";
7+
8+
const hasNonWhitespace = /\S/u;
9+
10+
const comparators = {
11+
ascending: {
12+
alphanumeric: {
13+
sensitive: (a, b) => a <= b,
14+
insensitive: (a, b) => a.toLowerCase() <= b.toLowerCase(),
15+
},
16+
natural: {
17+
sensitive: (a, b) => naturalCompare(a, b) <= 0,
18+
insensitive: (a, b) =>
19+
naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0,
20+
},
21+
},
22+
descending: {
23+
alphanumeric: {
24+
sensitive: (a, b) =>
25+
comparators.ascending.alphanumeric.sensitive(b, a),
26+
insensitive: (a, b) =>
27+
comparators.ascending.alphanumeric.insensitive(b, a),
28+
},
29+
natural: {
30+
sensitive: (a, b) => comparators.ascending.natural.sensitive(b, a),
31+
insensitive: (a, b) =>
32+
comparators.ascending.natural.insensitive(b, a),
33+
},
34+
},
35+
};
36+
37+
function getKey(member) {
38+
return member.name.type === `Identifier`
39+
? member.name.name
40+
: member.name.value;
41+
}
42+
43+
export default {
44+
meta: {
45+
type: /** @type {const} */ ("suggestion"),
46+
47+
defaultOptions: [
48+
"asc",
49+
{
50+
allowLineSeparatedGroups: false,
51+
caseSensitive: true,
52+
minKeys: 2,
53+
natural: false,
54+
},
55+
],
56+
57+
docs: {
58+
description: `Require JSON object keys to be sorted`,
59+
},
60+
61+
messages: {
62+
sortKeys:
63+
"Expected object keys to be in {{sortName}} case-{{sensitivity}} {{direction}} order. '{{thisName}}' should be before '{{prevName}}'.",
64+
},
65+
66+
schema: [
67+
{
68+
enum: ["asc", "desc"],
69+
},
70+
{
71+
type: "object",
72+
properties: {
73+
caseSensitive: {
74+
type: "boolean",
75+
},
76+
natural: {
77+
type: "boolean",
78+
},
79+
minKeys: {
80+
type: "integer",
81+
minimum: 2,
82+
},
83+
allowLineSeparatedGroups: {
84+
type: "boolean",
85+
},
86+
},
87+
additionalProperties: false,
88+
},
89+
],
90+
},
91+
92+
create(context) {
93+
const [
94+
directionShort,
95+
{ allowLineSeparatedGroups, caseSensitive, natural, minKeys },
96+
] = context.options;
97+
98+
const direction = directionShort === "asc" ? "ascending" : "descending";
99+
const sortName = natural ? "natural" : "alphanumeric";
100+
const sensitivity = caseSensitive ? "sensitive" : "insensitive";
101+
const isValidOrder = comparators[direction][sortName][sensitivity];
102+
103+
// 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
104+
const commentLineNums = new Set();
105+
for (const comment of context.sourceCode.comments) {
106+
for (
107+
let lineNum = comment.loc.start.line;
108+
lineNum <= comment.loc.end.line;
109+
lineNum += 1
110+
) {
111+
commentLineNums.add(lineNum);
112+
}
113+
}
114+
115+
// Note that there can be comments *inside* members, e.g. `{"foo: /* comment *\/ "bar"}`, but these are ignored when calculating line-separated groups
116+
function isLineSeparated(prevMember, member) {
117+
const prevMemberEndLine = prevMember.loc.end.line;
118+
const thisStartLine = member.loc.start.line;
119+
if (thisStartLine - prevMemberEndLine < 2) {
120+
return false;
121+
}
122+
123+
for (
124+
let lineNum = prevMemberEndLine + 1;
125+
lineNum < thisStartLine;
126+
lineNum += 1
127+
) {
128+
if (
129+
!commentLineNums.has(lineNum) &&
130+
!hasNonWhitespace.test(
131+
context.sourceCode.lines[lineNum - 1],
132+
)
133+
) {
134+
return true;
135+
}
136+
}
137+
138+
return false;
139+
}
140+
141+
return {
142+
Object(node) {
143+
let prevMember;
144+
let prevName;
145+
146+
if (node.members.length < minKeys) {
147+
return;
148+
}
149+
150+
for (const member of node.members) {
151+
const thisName = getKey(member);
152+
153+
if (
154+
prevMember &&
155+
!isValidOrder(prevName, thisName) &&
156+
(!allowLineSeparatedGroups ||
157+
!isLineSeparated(prevMember, member))
158+
) {
159+
context.report({
160+
loc: member.name.loc,
161+
messageId: "sortKeys",
162+
data: {
163+
thisName,
164+
prevName,
165+
direction,
166+
sensitivity,
167+
sortName,
168+
},
169+
});
170+
}
171+
172+
prevMember = member;
173+
prevName = thisName;
174+
}
175+
},
176+
};
177+
},
178+
};

0 commit comments

Comments
 (0)