Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add sort-keys rule #76

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
84c6698
add sort-keys
RobertAKARobin Jan 9, 2025
f0586af
adding tests for eslint/sort-keys
RobertAKARobin Jan 9, 2025
edfa0f8
made eslint/sort-keys tests work with json
RobertAKARobin Jan 9, 2025
162b1b5
account for line skips
RobertAKARobin Jan 9, 2025
a4aad1d
more semantic sort-keys code
RobertAKARobin Jan 9, 2025
357aacf
Handle when object members are joined by a comment
RobertAKARobin Jan 9, 2025
2535bac
formatting
RobertAKARobin Jan 9, 2025
d87ad4a
add allowLineSeparatedGroups option
RobertAKARobin Jan 10, 2025
7688cc3
Update src/rules/sort-keys.js
RobertAKARobin Jan 10, 2025
67615ba
Update readme
RobertAKARobin Jan 10, 2025
b77c64b
Add tests for nesting
RobertAKARobin Jan 10, 2025
559fa63
Add type fix for sort-keys meta.type
RobertAKARobin Jan 10, 2025
2cf5d8f
Added some tests for json5
RobertAKARobin Jan 11, 2025
7caf0c7
mv natural-compare to dependencies
RobertAKARobin Jan 11, 2025
d7d79a0
rm types from sort-keys because it makes dist grumpy
RobertAKARobin Jan 11, 2025
273389b
rm unneeded nullish check
RobertAKARobin Jan 12, 2025
1d6fc14
make sort-keys handle multiline comments
RobertAKARobin Jan 14, 2025
0b2a425
tabs => space
RobertAKARobin Jan 14, 2025
933539a
Update README.md
RobertAKARobin Jan 15, 2025
05f069d
stylistic updates
RobertAKARobin Jan 17, 2025
708311c
Commas do not count as group-separating lines
RobertAKARobin Jan 17, 2025
5f446f2
forget about commas, just check if empty line outside of comment
RobertAKARobin Jan 17, 2025
765cbde
rm unused languageOptions
RobertAKARobin Jan 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

//-----------------------------------------------------------------------------
Expand All @@ -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: {
Expand Down
178 changes: 178 additions & 0 deletions src/rules/sort-keys.js
Original file line number Diff line number Diff line change
@@ -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;
}
},
};
},
};
Loading
Loading