@@ -6,13 +6,96 @@ import {
66 isOpeningBraceToken ,
77 isOpeningBracketToken ,
88 isTokenOnSameLine ,
9+ isCommentToken ,
910} from "../utils/ast-utils.js" ;
1011import type { YAMLToken } from "../types.js" ;
12+ import type { YAMLSourceCode } from "../language/yaml-source-code.js" ;
1113
1214interface 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+
1699export 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 {
0 commit comments