Skip to content

Commit ef8de6e

Browse files
authored
feat(flow-mapping-curly-spacing): add emptyObjects option to control spacing in empty flow-mapping (#582)
1 parent 5a30909 commit ef8de6e

15 files changed

+271
-79
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-yml": minor
3+
---
4+
5+
feat(flow-mapping-curly-spacing): add `emptyObjects` option to control spacing in empty flow-mapping

docs/rules/flow-mapping-curly-spacing.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,38 @@ yml/flow-mapping-curly-spacing:
4141
- always # or "never"
4242
- arraysInObjects: false
4343
objectsInObjects: false
44+
emptyObjects: ignore
4445
```
4546
46-
Same as [object-curly-spacing] rule option. See [here](https://eslint.org/docs/rules/object-curly-spacing#options) for details.
47+
This rule has two options, a string option and an object option.
48+
49+
- First option:
50+
51+
- `"never"` (default) disallows spacing inside of braces
52+
- `"always"` requires spacing inside of braces (except `{}`)
53+
54+
- Second option:
55+
56+
- `"arraysInObjects"` control spacing inside of braces of mappings beginning and/or ending with a sequence element.
57+
- `true` requires spacing inside of braces of mappings beginning and/or ending with a sequence element (applies when the first option is set to `never`)
58+
- `false` disallows spacing inside of braces of mappings beginning and/or ending with a sequence element (applies when the first option is set to `always`)
59+
- `"objectsInObjects"` control spacing inside of braces of mappings beginning and/or ending with a mapping element.
60+
- `true` requires spacing inside of braces of mappings beginning and/or ending with a mapping element (applies when the first option is set to `never`)
61+
- `false` disallows spacing inside of braces of mappings beginning and/or ending with a mapping element (applies when the first option is set to `always`)
62+
- `"emptyObjects"` control spacing within empty mappings.
63+
- `"ignore"`(default) do not check spacing in empty mappings.
64+
- `"always"` require a space in empty mappings.
65+
- `"never"` disallow spaces in empty mappings.
66+
67+
These options are almost identical to those of the [@stylistic/object-curly-spacing] rule. See the [@stylistic/object-curly-spacing options](https://eslint.style/rules/object-curly-spacing#options) for details.
4768

4869
## :couple: Related rules
4970

71+
- [@stylistic/object-curly-spacing]
5072
- [object-curly-spacing]
5173

5274
[object-curly-spacing]: https://eslint.org/docs/rules/object-curly-spacing
75+
[@stylistic/object-curly-spacing]: https://eslint.style/rules/object-curly-spacing
5376

5477
## :rocket: Version
5578

src/language/yaml-source-code.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,13 @@ export class YAMLSourceCode extends TextSourceCodeBase<{
821821
return this.tokenStore.getCommentsAfter(nodeOrToken);
822822
}
823823

824+
public commentsExistBetween(
825+
first: YAMLSyntaxElement,
826+
second: YAMLSyntaxElement,
827+
): boolean {
828+
return this.tokenStore.commentsExistBetween(first, second);
829+
}
830+
824831
public isSpaceBetween(
825832
first: AST.Token | AST.Comment,
826833
second: AST.Token | AST.Comment,

src/rules/flow-mapping-curly-spacing.ts

Lines changed: 191 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,96 @@ import {
66
isOpeningBraceToken,
77
isOpeningBracketToken,
88
isTokenOnSameLine,
9+
isCommentToken,
910
} from "../utils/ast-utils.js";
1011
import type { YAMLToken } from "../types.js";
12+
import type { YAMLSourceCode } from "../language/yaml-source-code.js";
1113

1214
interface Schema1 {
1315
arraysInObjects?: boolean;
1416
objectsInObjects?: boolean;
17+
emptyObjects?: "ignore" | "always" | "never";
1518
}
19+
20+
/**
21+
* Parse rule options and return helpers for spacing checks.
22+
* @param options The options tuple from the rule configuration.
23+
* @param sourceCode The sourceCode object for node lookup.
24+
*/
25+
function parseOptions(
26+
options: [("always" | "never")?, Schema1?],
27+
sourceCode: YAMLSourceCode,
28+
) {
29+
const spaced = options[0] ?? "never";
30+
31+
/**
32+
* Determines whether an exception option is set relative to the base spacing.
33+
* @param option The option to check.
34+
*/
35+
function isOptionSet(
36+
option: "arraysInObjects" | "objectsInObjects",
37+
): boolean {
38+
return options[1] ? options[1][option] === (spaced === "never") : false;
39+
}
40+
41+
const arraysInObjectsException = isOptionSet("arraysInObjects");
42+
const objectsInObjectsException = isOptionSet("objectsInObjects");
43+
const emptyObjects = options[1]?.emptyObjects ?? "ignore";
44+
45+
/**
46+
* Whether the opening brace must be spaced, considering exceptions.
47+
* @param spaced The primary spaced option string.
48+
* @param second The token after the opening brace.
49+
*/
50+
function isOpeningCurlyBraceMustBeSpaced(
51+
spaced: "always" | "never",
52+
second: YAMLToken,
53+
) {
54+
const targetPenultimateType =
55+
arraysInObjectsException && isOpeningBracketToken(second)
56+
? "YAMLSequence"
57+
: objectsInObjectsException && isOpeningBraceToken(second)
58+
? "YAMLMapping"
59+
: null;
60+
61+
const node = sourceCode.getNodeByRangeIndex(second.range[0]);
62+
63+
return targetPenultimateType && node?.type === targetPenultimateType
64+
? spaced === "never"
65+
: spaced === "always";
66+
}
67+
68+
/**
69+
* Whether the closing brace must be spaced, considering exceptions.
70+
* @param spaced The primary spaced option string.
71+
* @param penultimate The token before the closing brace.
72+
*/
73+
function isClosingCurlyBraceMustBeSpaced(
74+
spaced: "always" | "never",
75+
penultimate: YAMLToken,
76+
) {
77+
const targetPenultimateType =
78+
arraysInObjectsException && isClosingBracketToken(penultimate)
79+
? "YAMLSequence"
80+
: objectsInObjectsException && isClosingBraceToken(penultimate)
81+
? "YAMLMapping"
82+
: null;
83+
84+
const node = sourceCode.getNodeByRangeIndex(penultimate.range[0]);
85+
86+
return targetPenultimateType && node?.type === targetPenultimateType
87+
? spaced === "never"
88+
: spaced === "always";
89+
}
90+
91+
return {
92+
spaced,
93+
emptyObjects,
94+
isOpeningCurlyBraceMustBeSpaced,
95+
isClosingCurlyBraceMustBeSpaced,
96+
};
97+
}
98+
1699
export default createRule("flow-mapping-curly-spacing", {
17100
meta: {
18101
docs: {
@@ -37,6 +120,10 @@ export default createRule("flow-mapping-curly-spacing", {
37120
objectsInObjects: {
38121
type: "boolean",
39122
},
123+
emptyObjects: {
124+
type: "string",
125+
enum: ["ignore", "always", "never"],
126+
},
40127
},
41128
additionalProperties: false,
42129
},
@@ -46,6 +133,9 @@ export default createRule("flow-mapping-curly-spacing", {
46133
requireSpaceAfter: "A space is required after '{{token}}'.",
47134
unexpectedSpaceBefore: "There should be no space before '{{token}}'.",
48135
unexpectedSpaceAfter: "There should be no space after '{{token}}'.",
136+
requiredSpaceInEmptyObject: "A space is required in empty flow mapping.",
137+
unexpectedSpaceInEmptyObject:
138+
"There should be no space in empty flow mapping.",
49139
},
50140
},
51141
create(context) {
@@ -54,55 +144,10 @@ export default createRule("flow-mapping-curly-spacing", {
54144
return {};
55145
}
56146

57-
const spaced = context.options[0] === "always";
58-
59-
/**
60-
* Determines whether an option is set, relative to the spacing option.
61-
* If spaced is "always", then check whether option is set to false.
62-
* If spaced is "never", then check whether option is set to true.
63-
* @param option The option to exclude.
64-
* @returns Whether or not the property is excluded.
65-
*/
66-
function isOptionSet(option: keyof NonNullable<Schema1>): boolean {
67-
return context.options[1]
68-
? context.options[1][option] === !spaced
69-
: false;
70-
}
71-
72-
const options = {
73-
spaced,
74-
arraysInObjectsException: isOptionSet("arraysInObjects"),
75-
objectsInObjectsException: isOptionSet("objectsInObjects"),
76-
isOpeningCurlyBraceMustBeSpaced(second: YAMLToken) {
77-
const targetPenultimateType =
78-
options.arraysInObjectsException && isOpeningBracketToken(second)
79-
? "YAMLSequence"
80-
: options.objectsInObjectsException && isOpeningBraceToken(second)
81-
? "YAMLMapping"
82-
: null;
83-
84-
return targetPenultimateType &&
85-
sourceCode.getNodeByRangeIndex(second.range[0])?.type ===
86-
targetPenultimateType
87-
? !options.spaced
88-
: options.spaced;
89-
},
90-
isClosingCurlyBraceMustBeSpaced(penultimate: YAMLToken) {
91-
const targetPenultimateType =
92-
options.arraysInObjectsException && isClosingBracketToken(penultimate)
93-
? "YAMLSequence"
94-
: options.objectsInObjectsException &&
95-
isClosingBraceToken(penultimate)
96-
? "YAMLMapping"
97-
: null;
98-
99-
return targetPenultimateType &&
100-
sourceCode.getNodeByRangeIndex(penultimate.range[0])?.type ===
101-
targetPenultimateType
102-
? !options.spaced
103-
: options.spaced;
104-
},
105-
};
147+
const options = parseOptions(
148+
context.options as [("always" | "never")?, Schema1?],
149+
sourceCode,
150+
);
106151

107152
/**
108153
* Reports that there shouldn't be a space after the first token
@@ -201,29 +246,30 @@ export default createRule("flow-mapping-curly-spacing", {
201246
*/
202247
function validateBraceSpacing(
203248
node: AST.YAMLNode,
204-
first: AST.Token,
249+
spaced: "always" | "never",
250+
openingToken: AST.Token,
205251
second: YAMLToken,
206252
penultimate: YAMLToken,
207-
last: YAMLToken,
253+
closingToken: YAMLToken,
208254
) {
209-
if (isTokenOnSameLine(first, second)) {
210-
const firstSpaced = sourceCode.isSpaceBetween(first, second);
255+
if (isTokenOnSameLine(openingToken, second)) {
256+
const firstSpaced = sourceCode.isSpaceBetween(openingToken, second);
211257

212-
if (options.isOpeningCurlyBraceMustBeSpaced(second)) {
213-
if (!firstSpaced) reportRequiredBeginningSpace(node, first);
258+
if (options.isOpeningCurlyBraceMustBeSpaced(spaced, second)) {
259+
if (!firstSpaced) reportRequiredBeginningSpace(node, openingToken);
214260
} else {
215261
if (firstSpaced && second.type !== "Line")
216-
reportNoBeginningSpace(node, first);
262+
reportNoBeginningSpace(node, openingToken);
217263
}
218264
}
219265

220-
if (isTokenOnSameLine(penultimate, last)) {
221-
const lastSpaced = sourceCode.isSpaceBetween(penultimate, last);
266+
if (isTokenOnSameLine(penultimate, closingToken)) {
267+
const lastSpaced = sourceCode.isSpaceBetween(penultimate, closingToken);
222268

223-
if (options.isClosingCurlyBraceMustBeSpaced(penultimate)) {
224-
if (!lastSpaced) reportRequiredEndingSpace(node, last);
269+
if (options.isClosingCurlyBraceMustBeSpaced(spaced, penultimate)) {
270+
if (!lastSpaced) reportRequiredEndingSpace(node, closingToken);
225271
} else {
226-
if (lastSpaced) reportNoEndingSpace(node, last);
272+
if (lastSpaced) reportNoEndingSpace(node, closingToken);
227273
}
228274
}
229275
}
@@ -249,19 +295,97 @@ export default createRule("flow-mapping-curly-spacing", {
249295
* Reports a given object node if spacing in curly braces is invalid.
250296
* @param node An ObjectExpression or ObjectPattern node to check.
251297
*/
298+
function checkSpaceInEmptyObject(node: AST.YAMLMapping) {
299+
if (options.emptyObjects === "ignore") {
300+
return;
301+
}
302+
303+
const openingToken = sourceCode.getFirstToken(node);
304+
const closingToken = sourceCode.getLastToken(node);
305+
306+
const second = sourceCode.getTokenAfter(openingToken, {
307+
includeComments: true,
308+
})!;
309+
if (second !== closingToken && isCommentToken(second)) {
310+
const penultimate = sourceCode.getTokenBefore(closingToken, {
311+
includeComments: true,
312+
})!;
313+
validateBraceSpacing(
314+
node,
315+
options.emptyObjects,
316+
openingToken,
317+
second,
318+
penultimate,
319+
closingToken,
320+
);
321+
return;
322+
}
323+
if (!isTokenOnSameLine(openingToken, closingToken)) return;
324+
325+
const sourceBetween = sourceCode.text.slice(
326+
openingToken.range[1],
327+
closingToken.range[0],
328+
);
329+
if (sourceBetween.trim() !== "") {
330+
return;
331+
}
332+
333+
if (options.emptyObjects === "always") {
334+
if (sourceBetween) return;
335+
context.report({
336+
node,
337+
loc: { start: openingToken.loc.end, end: closingToken.loc.start },
338+
messageId: "requiredSpaceInEmptyObject",
339+
fix(fixer) {
340+
return fixer.replaceTextRange(
341+
[openingToken.range[1], closingToken.range[0]],
342+
" ",
343+
);
344+
},
345+
});
346+
} else if (options.emptyObjects === "never") {
347+
if (!sourceBetween) return;
348+
context.report({
349+
node,
350+
loc: { start: openingToken.loc.end, end: closingToken.loc.start },
351+
messageId: "unexpectedSpaceInEmptyObject",
352+
fix(fixer) {
353+
return fixer.removeRange([
354+
openingToken.range[1],
355+
closingToken.range[0],
356+
]);
357+
},
358+
});
359+
}
360+
}
361+
362+
/**
363+
* Reports a given mapping node if spacing in curly braces is invalid.
364+
* @param node A YAMLMapping node to check.
365+
*/
252366
function checkForObject(node: AST.YAMLMapping) {
253-
if (node.pairs.length === 0) return;
367+
if (node.pairs.length === 0) {
368+
checkSpaceInEmptyObject(node);
369+
return;
370+
}
254371

255-
const first = sourceCode.getFirstToken(node);
256-
const last = getClosingBraceOfObject(node)!;
257-
const second = sourceCode.getTokenAfter(first, {
372+
const openingToken = sourceCode.getFirstToken(node);
373+
const closingToken = getClosingBraceOfObject(node)!;
374+
const second = sourceCode.getTokenAfter(openingToken, {
258375
includeComments: true,
259376
})!;
260-
const penultimate = sourceCode.getTokenBefore(last, {
377+
const penultimate = sourceCode.getTokenBefore(closingToken, {
261378
includeComments: true,
262379
})!;
263380

264-
validateBraceSpacing(node, first, second, penultimate, last);
381+
validateBraceSpacing(
382+
node,
383+
options.spaced,
384+
openingToken,
385+
second,
386+
penultimate,
387+
closingToken,
388+
);
265389
}
266390

267391
return {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"message": "A space is required in empty flow mapping.",
4+
"line": 2,
5+
"column": 4
6+
}
7+
]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# {"options":["never", {"emptyObjects":"always"}]}
2+
- {}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# flow-mapping-curly-spacing/invalid/empty-objects-always-input.yml
2+
- { }

0 commit comments

Comments
 (0)