Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/lovely-donkeys-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-jsonc": minor
---

feat: add new `allowLineSeparatedGroups` option to the `jsonc/sort-keys` rule
4 changes: 3 additions & 1 deletion docs/rules/sort-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ The option receives multiple objects with the following properties:
- `caseSensitive` ... If `true`, enforce properties to be in case-sensitive order. Default is `true`.
- `natural` ... If `true`, enforce properties to be in natural order. Default is `false`.
- `minKeys` ... Specifies the minimum number of keys that an object should have in order for the object's unsorted keys to produce an error. Default is `2`, which means by default all objects with unsorted keys will result in lint errors.
- `allowLineSeparatedGroups` ... If `true`, the rule allows to group object keys through line breaks. In other words, a blank line after a property will reset the sorting of keys. Default is `false`.

You can also define options in the same format as the [sort-keys] rule.

Expand All @@ -113,7 +114,8 @@ You can also define options in the same format as the [sort-keys] rule.
{
"caseSensitive": true,
"natural": false,
"minKeys": 2
"minKeys": 2,
"allowLineSeparatedGroups": false
}
]
}
Expand Down
129 changes: 100 additions & 29 deletions lib/rules/sort-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type CompatibleWithESLintOptions =
caseSensitive?: boolean;
natural?: boolean;
minKeys?: number;
allowLineSeparatedGroups?: boolean;
}
];
type PatternOption = {
Expand All @@ -35,6 +36,7 @@ type PatternOption = {
}
)[];
minKeys?: number;
allowLineSeparatedGroups?: boolean;
};
type OrderObject = {
type?: OrderTypeOption;
Expand All @@ -45,6 +47,7 @@ type ParsedOption = {
isTargetObject: (node: JSONObjectData) => boolean;
ignore: (data: JSONPropertyData) => boolean;
isValidOrder: Validator;
allowLineSeparatedGroups: boolean;
orderText: string;
};
type Validator = (a: JSONPropertyData, b: JSONPropertyData) => boolean;
Expand Down Expand Up @@ -86,6 +89,11 @@ class JSONPropertyData {
public get name() {
return (this.cachedName ??= getPropertyName(this.node));
}

public getPrev(): JSONPropertyData | null {
const prevIndex = this.index - 1;
return prevIndex >= 0 ? this.object.properties[prevIndex] : null;
}
}
class JSONObjectData {
public readonly node: AST.JSONObjectExpression;
Expand All @@ -101,6 +109,36 @@ class JSONObjectData {
(e, index) => new JSONPropertyData(this, e, index)
));
}

public getPath(): string {
let path = "";
let curr: AST.JSONExpression = this.node;
let p: AST.JSONNode | null = curr.parent;
while (p) {
if (p.type === "JSONProperty") {
const name = getPropertyName(p);
if (/^[$_a-z][\w$]*$/iu.test(name)) {
path = `.${name}${path}`;
} else {
path = `[${JSON.stringify(name)}]${path}`;
}
curr = p.parent;
} else if (p.type === "JSONArrayExpression") {
const index = p.elements.indexOf(curr);
path = `[${index}]${path}`;
curr = p;
} else if (p.type === "JSONExpressionStatement") {
break;
} else {
curr = p;
}
p = curr.parent;
}
if (path.startsWith(".")) {
path = path.slice(1);
}
return path;
}
}

/**
Expand Down Expand Up @@ -153,6 +191,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
const insensitive = obj.caseSensitive === false;
const natural = Boolean(obj.natural);
const minKeys: number = obj.minKeys ?? 2;
const allowLineSeparatedGroups = obj.allowLineSeparatedGroups || false;
return [
{
isTargetObject: (node) => node.properties.length >= minKeys,
Expand All @@ -161,6 +200,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
orderText: `${natural ? "natural " : ""}${
insensitive ? "insensitive " : ""
}${type}ending`,
allowLineSeparatedGroups,
},
];
}
Expand All @@ -170,6 +210,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
const pathPattern = new RegExp(opt.pathPattern);
const hasProperties = opt.hasProperties ?? [];
const minKeys: number = opt.minKeys ?? 2;
const allowLineSeparatedGroups = opt.allowLineSeparatedGroups || false;
if (!Array.isArray(order)) {
const type: OrderTypeOption = order.type ?? "asc";
const insensitive = order.caseSensitive === false;
Expand All @@ -182,6 +223,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
orderText: `${natural ? "natural " : ""}${
insensitive ? "insensitive " : ""
}${type}ending`,
allowLineSeparatedGroups,
};
}
const parsedOrder: {
Expand Down Expand Up @@ -227,6 +269,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
return false;
},
orderText: "specified",
allowLineSeparatedGroups,
};

/**
Expand All @@ -242,29 +285,7 @@ function parseOptions(options: UserOptions): ParsedOption[] {
return false;
}
}

let path = "";
let curr: AST.JSONNode = data.node;
let p: AST.JSONNode | null = curr.parent;
while (p) {
if (p.type === "JSONProperty") {
const name = getPropertyName(p);
if (/^[$_a-z][\w$]*$/iu.test(name)) {
path = `.${name}${path}`;
} else {
path = `[${JSON.stringify(name)}]${path}`;
}
} else if (p.type === "JSONArrayExpression") {
const index = p.elements.indexOf(curr as never);
path = `[${index}]${path}`;
}
curr = p;
p = curr.parent;
}
if (path.startsWith(".")) {
path = path.slice(1);
}
return pathPattern.test(path);
return pathPattern.test(data.getPath());
}
});
}
Expand Down Expand Up @@ -339,6 +360,9 @@ export default createRule("sort-keys", {
type: "integer",
minimum: 2,
},
allowLineSeparatedGroups: {
type: "boolean",
},
},
required: ["pathPattern", "order"],
additionalProperties: false,
Expand All @@ -365,6 +389,9 @@ export default createRule("sort-keys", {
type: "integer",
minimum: 2,
},
allowLineSeparatedGroups: {
type: "boolean",
},
},
additionalProperties: false,
},
Expand All @@ -387,17 +414,30 @@ export default createRule("sort-keys", {
// Parse options.
const parsedOptions = parseOptions(context.options);

const sourceCode = context.getSourceCode();

/**
* Verify for property
*/
function verifyProperty(data: JSONPropertyData, option: ParsedOption) {
if (option.ignore(data)) {
return;
}
const prevList = data.object.properties
.slice(0, data.index)
.reverse()
.filter((d) => !option.ignore(d));
const prevList: JSONPropertyData[] = [];
let currTarget = data;
let prevTarget;
while ((prevTarget = currTarget.getPrev())) {
if (option.allowLineSeparatedGroups) {
if (hasBlankLine(prevTarget, currTarget)) {
break;
}
}

if (!option.ignore(prevTarget)) {
prevList.push(prevTarget);
}
currTarget = prevTarget;
}

if (prevList.length === 0) {
return;
Expand All @@ -413,7 +453,6 @@ export default createRule("sort-keys", {
orderText: option.orderText,
},
*fix(fixer) {
const sourceCode = context.getSourceCode();
let moveTarget = prevList[0];
for (const prev of prevList) {
if (option.isValidOrder(prev, data)) {
Expand Down Expand Up @@ -441,14 +480,46 @@ export default createRule("sort-keys", {
const insertTarget = sourceCode.getTokenBefore(
moveTarget.node as never
)!;
yield fixer.insertTextAfterRange(insertTarget.range, insertCode);
let insertRange = insertTarget.range;
const insertNext = sourceCode.getTokenAfter(insertTarget, {
includeComments: true,
})!;
if (insertNext.loc!.start.line - insertTarget.loc.end.line > 1) {
const offset = sourceCode.getIndexFromLoc({
line: insertNext.loc!.start.line - 1,
column: 0,
});
insertRange = [offset, offset];
}
yield fixer.insertTextAfterRange(insertRange, insertCode);

yield fixer.removeRange([removeStart, codeEnd]);
},
});
}
}

/**
* Checks whether the given two properties have a blank line between them.
*/
function hasBlankLine(prev: JSONPropertyData, next: JSONPropertyData) {
const tokenOrNodes = [
...sourceCode.getTokensBetween(prev.node as never, next.node as never, {
includeComments: true,
}),
next.node,
];
let prevLoc = prev.node.loc;
for (const t of tokenOrNodes) {
const loc = t.loc!;
if (loc.start.line - prevLoc.end.line > 1) {
return true;
}
prevLoc = loc;
}
return false;
}

return {
JSONObjectExpression(node: AST.JSONObjectExpression) {
const data = new JSONObjectData(node);
Expand Down
Loading