Skip to content

Commit

Permalink
feat: add allowLineSeparatedGroups option to the yml/sort-keys ru…
Browse files Browse the repository at this point in the history
…le (#247)

* feat: add `allowLineSeparatedGroups` option to the `yml/sort-keys` rule

* Create rotten-singers-run.md

* fix
  • Loading branch information
ota-meshi committed Jun 13, 2023
1 parent 8d9e966 commit 2b9f295
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 26 deletions.
5 changes: 5 additions & 0 deletions .changeset/rotten-singers-run.md
@@ -0,0 +1,5 @@
---
"eslint-plugin-yml": minor
---

feat: add `allowLineSeparatedGroups` option to the `yml/sort-keys` rule
2 changes: 2 additions & 0 deletions docs/rules/sort-keys.md
Expand Up @@ -82,6 +82,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 @@ -92,6 +93,7 @@ yml/sort-keys:
- caseSensitive: true
natural: false
minKeys: 2
allowLineSeparatedGroups: false
```

See [here](https://eslint.org/docs/rules/sort-keys#options) for details.
Expand Down
107 changes: 81 additions & 26 deletions src/rules/sort-keys.ts
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 = {
isTargetMapping: (node: YAMLMappingData) => boolean;
ignore: (data: YAMLPairData) => boolean;
isValidOrder: Validator;
allowLineSeparatedGroups: boolean;
orderText: string;
};
type Validator = (a: YAMLPairData, b: YAMLPairData) => boolean;
Expand Down Expand Up @@ -115,6 +118,11 @@ class YAMLPairData {
this.mapping.sourceCode
));
}

public getPrev(): YAMLPairData | null {
const prevIndex = this.index - 1;
return prevIndex >= 0 ? this.mapping.pairs[prevIndex] : null;
}
}
class YAMLMappingData {
public readonly node: AST.YAMLMapping;
Expand Down Expand Up @@ -153,6 +161,31 @@ class YAMLMappingData {
new YAMLPairData(this, e, index, this.anchorAliasMap.get(e)!)
));
}

public getPath(sourceCode: SourceCode): string {
let path = "";
let curr: AST.YAMLNode = this.node;
let p: AST.YAMLNode | null = curr.parent;
while (p) {
if (p.type === "YAMLPair") {
const name = getPropertyName(p, sourceCode);
if (/^[$_a-z][\w$]*$/iu.test(name)) {
path = `.${name}${path}`;
} else {
path = `[${JSON.stringify(name)}]${path}`;
}
} else if (p.type === "YAMLSequence") {
const index = p.entries.indexOf(curr as never);
path = `[${index}]${path}`;
}
curr = p;
p = curr.parent;
}
if (path.startsWith(".")) {
path = path.slice(1);
}
return path;
}
}

/**
Expand Down Expand Up @@ -207,6 +240,7 @@ function parseOptions(
const insensitive = obj.caseSensitive === false;
const natural = Boolean(obj.natural);
const minKeys: number = obj.minKeys ?? 2;
const allowLineSeparatedGroups = obj.allowLineSeparatedGroups || false;
return [
{
isTargetMapping: (data) => data.node.pairs.length >= minKeys,
Expand All @@ -215,6 +249,7 @@ function parseOptions(
orderText: `${natural ? "natural " : ""}${
insensitive ? "insensitive " : ""
}${type}ending`,
allowLineSeparatedGroups,
},
];
}
Expand All @@ -224,6 +259,7 @@ function parseOptions(
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 @@ -236,6 +272,7 @@ function parseOptions(
orderText: `${natural ? "natural " : ""}${
insensitive ? "insensitive " : ""
}${type}ending`,
allowLineSeparatedGroups,
};
}
const parsedOrder: {
Expand Down Expand Up @@ -281,6 +318,7 @@ function parseOptions(
return false;
},
orderText: "specified",
allowLineSeparatedGroups,
};

/**
Expand All @@ -297,28 +335,7 @@ function parseOptions(
}
}

let path = "";
let curr: AST.YAMLNode = data.node;
let p: AST.YAMLNode | null = curr.parent;
while (p) {
if (p.type === "YAMLPair") {
const name = getPropertyName(p, sourceCode);
if (/^[$_a-z][\w$]*$/iu.test(name)) {
path = `.${name}${path}`;
} else {
path = `[${JSON.stringify(name)}]${path}`;
}
} else if (p.type === "YAMLSequence") {
const index = p.entries.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(sourceCode));
}
});
}
Expand Down Expand Up @@ -393,6 +410,9 @@ export default createRule("sort-keys", {
type: "integer",
minimum: 2,
},
allowLineSeparatedGroups: {
type: "boolean",
},
},
required: ["pathPattern", "order"],
additionalProperties: false,
Expand All @@ -419,6 +439,9 @@ export default createRule("sort-keys", {
type: "integer",
minimum: 2,
},
allowLineSeparatedGroups: {
type: "boolean",
},
},
additionalProperties: false,
},
Expand Down Expand Up @@ -488,10 +511,21 @@ export default createRule("sort-keys", {
if (ignore(data, option)) {
return;
}
const prevList = data.mapping.pairs
.slice(0, data.index)
.reverse()
.filter((d) => !ignore(d, option));
const prevList: YAMLPairData[] = [];
let currTarget = data;
let prevTarget;
while ((prevTarget = currTarget.getPrev())) {
if (option.allowLineSeparatedGroups) {
if (hasBlankLine(prevTarget, currTarget)) {
break;
}
}

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

if (prevList.length === 0) {
return;
Expand Down Expand Up @@ -525,6 +559,27 @@ export default createRule("sort-keys", {
}
}

/**
* Checks whether the given two properties have a blank line between them.
*/
function hasBlankLine(prev: YAMLPairData, next: YAMLPairData) {
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;
}

type PairStack = {
upper: PairStack | null;
anchors: Set<string>;
Expand Down
@@ -0,0 +1,8 @@
{
"options": ["asc", { "allowLineSeparatedGroups": true }],
"settings": {
"yml": {
"indent": 8
}
}
}
@@ -0,0 +1,7 @@
[
{
"message": "Expected mapping keys to be in ascending order. 'a' should be before 'b'.",
"line": 6,
"column": 1
}
]
@@ -0,0 +1,5 @@
x: 1
y: 2

b: 3
a: 4
@@ -0,0 +1,6 @@
# sort-keys/invalid/allowLineSeparatedGroups/separated-input.yaml
x: 1
y: 2

a: 4
b: 3
@@ -0,0 +1,8 @@
{
"options": ["asc", { "allowLineSeparatedGroups": true }],
"settings": {
"yml": {
"indent": 8
}
}
}
@@ -0,0 +1,5 @@
x: 1
y: 2

a: 3
b: 4

0 comments on commit 2b9f295

Please sign in to comment.