]>
git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/key-spacing.js
2 * @fileoverview Rule to specify spacing of object literal keys and values
3 * @author Brandon Mills
7 //------------------------------------------------------------------------------
9 //------------------------------------------------------------------------------
11 const astUtils
= require("./utils/ast-utils");
13 //------------------------------------------------------------------------------
15 //------------------------------------------------------------------------------
18 * Checks whether a string contains a line terminator as defined in
19 * http://www.ecma-international.org/ecma-262/5.1/#sec-7.3
20 * @param {string} str String to test.
21 * @returns {boolean} True if str contains a line terminator.
23 function containsLineTerminator(str
) {
24 return astUtils
.LINEBREAK_MATCHER
.test(str
);
28 * Gets the last element of an array.
29 * @param {Array} arr An array.
30 * @returns {any} Last element of arr.
33 return arr
[arr
.length
- 1];
37 * Checks whether a node is contained on a single line.
38 * @param {ASTNode} node AST Node being evaluated.
39 * @returns {boolean} True if the node is a single line.
41 function isSingleLine(node
) {
42 return (node
.loc
.end
.line
=== node
.loc
.start
.line
);
46 * Checks whether the properties on a single line.
47 * @param {ASTNode[]} properties List of Property AST nodes.
48 * @returns {boolean} True if all properties is on a single line.
50 function isSingleLineProperties(properties
) {
51 const [firstProp
] = properties
,
52 lastProp
= last(properties
);
54 return firstProp
.loc
.start
.line
=== lastProp
.loc
.end
.line
;
58 * Initializes a single option property from the configuration with defaults for undefined values
59 * @param {Object} toOptions Object to be initialized
60 * @param {Object} fromOptions Object to be initialized from
61 * @returns {Object} The object with correctly initialized options and values
63 function initOptionProperty(toOptions
, fromOptions
) {
64 toOptions
.mode
= fromOptions
.mode
|| "strict";
66 // Set value of beforeColon
67 if (typeof fromOptions
.beforeColon
!== "undefined") {
68 toOptions
.beforeColon
= +fromOptions
.beforeColon
;
70 toOptions
.beforeColon
= 0;
73 // Set value of afterColon
74 if (typeof fromOptions
.afterColon
!== "undefined") {
75 toOptions
.afterColon
= +fromOptions
.afterColon
;
77 toOptions
.afterColon
= 1;
80 // Set align if exists
81 if (typeof fromOptions
.align
!== "undefined") {
82 if (typeof fromOptions
.align
=== "object") {
83 toOptions
.align
= fromOptions
.align
;
86 on
: fromOptions
.align
,
88 beforeColon
: toOptions
.beforeColon
,
89 afterColon
: toOptions
.afterColon
98 * Initializes all the option values (singleLine, multiLine and align) from the configuration with defaults for undefined values
99 * @param {Object} toOptions Object to be initialized
100 * @param {Object} fromOptions Object to be initialized from
101 * @returns {Object} The object with correctly initialized options and values
103 function initOptions(toOptions
, fromOptions
) {
104 if (typeof fromOptions
.align
=== "object") {
106 // Initialize the alignment configuration
107 toOptions
.align
= initOptionProperty({}, fromOptions
.align
);
108 toOptions
.align
.on
= fromOptions
.align
.on
|| "colon";
109 toOptions
.align
.mode
= fromOptions
.align
.mode
|| "strict";
111 toOptions
.multiLine
= initOptionProperty({}, (fromOptions
.multiLine
|| fromOptions
));
112 toOptions
.singleLine
= initOptionProperty({}, (fromOptions
.singleLine
|| fromOptions
));
114 } else { // string or undefined
115 toOptions
.multiLine
= initOptionProperty({}, (fromOptions
.multiLine
|| fromOptions
));
116 toOptions
.singleLine
= initOptionProperty({}, (fromOptions
.singleLine
|| fromOptions
));
118 // If alignment options are defined in multiLine, pull them out into the general align configuration
119 if (toOptions
.multiLine
.align
) {
121 on
: toOptions
.multiLine
.align
.on
,
122 mode
: toOptions
.multiLine
.align
.mode
|| toOptions
.multiLine
.mode
,
123 beforeColon
: toOptions
.multiLine
.align
.beforeColon
,
124 afterColon
: toOptions
.multiLine
.align
.afterColon
132 //------------------------------------------------------------------------------
134 //------------------------------------------------------------------------------
141 description
: "enforce consistent spacing between keys and values in object literal properties",
142 category
: "Stylistic Issues",
144 url
: "https://eslint.org/docs/rules/key-spacing"
147 fixable
: "whitespace",
157 enum: ["colon", "value"]
163 enum: ["strict", "minimum"]
166 enum: ["colon", "value"]
175 additionalProperties
: false
180 enum: ["strict", "minimum"]
189 additionalProperties
: false
198 enum: ["strict", "minimum"]
207 additionalProperties
: false
215 enum: ["colon", "value"]
221 enum: ["strict", "minimum"]
224 enum: ["colon", "value"]
233 additionalProperties
: false
238 enum: ["strict", "minimum"]
247 additionalProperties
: false
250 additionalProperties
: false
259 enum: ["strict", "minimum"]
268 additionalProperties
: false
274 enum: ["strict", "minimum"]
283 additionalProperties
: false
289 enum: ["strict", "minimum"]
292 enum: ["colon", "value"]
301 additionalProperties
: false
304 additionalProperties
: false
309 extraKey
: "Extra space after {{computed}}key '{{key}}'.",
310 extraValue
: "Extra space before value for {{computed}}key '{{key}}'.",
311 missingKey
: "Missing space after {{computed}}key '{{key}}'.",
312 missingValue
: "Missing space before value for {{computed}}key '{{key}}'."
320 * "key-spacing": [2, {
321 * beforeColon: false,
323 * align: "colon" // Optional, or "value"
326 const options
= context
.options
[0] || {},
327 ruleOptions
= initOptions({}, options
),
328 multiLineOptions
= ruleOptions
.multiLine
,
329 singleLineOptions
= ruleOptions
.singleLine
,
330 alignmentOptions
= ruleOptions
.align
|| null;
332 const sourceCode
= context
.getSourceCode();
335 * Checks whether a property is a member of the property group it follows.
336 * @param {ASTNode} lastMember The last Property known to be in the group.
337 * @param {ASTNode} candidate The next Property that might be in the group.
338 * @returns {boolean} True if the candidate property is part of the group.
340 function continuesPropertyGroup(lastMember
, candidate
) {
341 const groupEndLine
= lastMember
.loc
.start
.line
,
342 candidateStartLine
= candidate
.loc
.start
.line
;
344 if (candidateStartLine
- groupEndLine
<= 1) {
349 * Check that the first comment is adjacent to the end of the group, the
350 * last comment is adjacent to the candidate property, and that successive
351 * comments are adjacent to each other.
353 const leadingComments
= sourceCode
.getCommentsBefore(candidate
);
356 leadingComments
.length
&&
357 leadingComments
[0].loc
.start
.line
- groupEndLine
<= 1 &&
358 candidateStartLine
- last(leadingComments
).loc
.end
.line
<= 1
360 for (let i
= 1; i
< leadingComments
.length
; i
++) {
361 if (leadingComments
[i
].loc
.start
.line
- leadingComments
[i
- 1].loc
.end
.line
> 1) {
372 * Determines if the given property is key-value property.
373 * @param {ASTNode} property Property node to check.
374 * @returns {boolean} Whether the property is a key-value property.
376 function isKeyValueProperty(property
) {
379 property
.shorthand
||
380 property
.kind
!== "init" || property
.type
!== "Property") // Could be "ExperimentalSpreadProperty" or "SpreadElement"
385 * Starting from the given a node (a property.key node here) looks forward
386 * until it finds the last token before a colon punctuator and returns it.
387 * @param {ASTNode} node The node to start looking from.
388 * @returns {ASTNode} The last token before a colon punctuator.
390 function getLastTokenBeforeColon(node
) {
391 const colonToken
= sourceCode
.getTokenAfter(node
, astUtils
.isColonToken
);
393 return sourceCode
.getTokenBefore(colonToken
);
397 * Starting from the given a node (a property.key node here) looks forward
398 * until it finds the colon punctuator and returns it.
399 * @param {ASTNode} node The node to start looking from.
400 * @returns {ASTNode} The colon punctuator.
402 function getNextColon(node
) {
403 return sourceCode
.getTokenAfter(node
, astUtils
.isColonToken
);
407 * Gets an object literal property's key as the identifier name or string value.
408 * @param {ASTNode} property Property node whose key to retrieve.
409 * @returns {string} The property's key.
411 function getKey(property
) {
412 const key
= property
.key
;
414 if (property
.computed
) {
415 return sourceCode
.getText().slice(key
.range
[0], key
.range
[1]);
417 return astUtils
.getStaticPropertyName(property
);
421 * Reports an appropriately-formatted error if spacing is incorrect on one
423 * @param {ASTNode} property Key-value pair in an object literal.
424 * @param {string} side Side being verified - either "key" or "value".
425 * @param {string} whitespace Actual whitespace string.
426 * @param {int} expected Expected whitespace length.
427 * @param {string} mode Value of the mode as "strict" or "minimum"
430 function report(property
, side
, whitespace
, expected
, mode
) {
431 const diff
= whitespace
.length
- expected
,
432 nextColon
= getNextColon(property
.key
),
433 tokenBeforeColon
= sourceCode
.getTokenBefore(nextColon
, { includeComments
: true }),
434 tokenAfterColon
= sourceCode
.getTokenAfter(nextColon
, { includeComments
: true }),
435 isKeySide
= side
=== "key",
436 locStart
= isKeySide
? tokenBeforeColon
.loc
.start
: tokenAfterColon
.loc
.start
,
438 diffAbs
= Math
.abs(diff
),
439 spaces
= Array(diffAbs
+ 1).join(" ");
442 diff
&& mode
=== "strict" ||
443 diff
< 0 && mode
=== "minimum" ||
444 diff
> 0 && !expected
&& mode
=== "minimum") &&
445 !(expected
&& containsLineTerminator(whitespace
))
454 range
= [tokenBeforeColon
.range
[1], tokenBeforeColon
.range
[1] + diffAbs
];
456 range
= [tokenAfterColon
.range
[0] - diffAbs
, tokenAfterColon
.range
[0]];
458 fix = function(fixer
) {
459 return fixer
.removeRange(range
);
465 fix = function(fixer
) {
466 return fixer
.insertTextAfter(tokenBeforeColon
, spaces
);
469 fix = function(fixer
) {
470 return fixer
.insertTextBefore(tokenAfterColon
, spaces
);
478 messageId
= side
=== "key" ? "extraKey" : "extraValue";
480 messageId
= side
=== "key" ? "missingKey" : "missingValue";
484 node
: property
[side
],
488 computed
: property
.computed
? "computed " : "",
489 key
: getKey(property
)
497 * Gets the number of characters in a key, including quotes around string
498 * keys and braces around computed property keys.
499 * @param {ASTNode} property Property of on object literal.
500 * @returns {int} Width of the key.
502 function getKeyWidth(property
) {
503 const startToken
= sourceCode
.getFirstToken(property
);
504 const endToken
= getLastTokenBeforeColon(property
.key
);
506 return endToken
.range
[1] - startToken
.range
[0];
510 * Gets the whitespace around the colon in an object literal property.
511 * @param {ASTNode} property Property node from an object literal.
512 * @returns {Object} Whitespace before and after the property's colon.
514 function getPropertyWhitespace(property
) {
515 const whitespace
= /(\s*):(\s*)/u.exec(sourceCode
.getText().slice(
516 property
.key
.range
[1], property
.value
.range
[0]
521 beforeColon
: whitespace
[1],
522 afterColon
: whitespace
[2]
529 * Creates groups of properties.
530 * @param {ASTNode} node ObjectExpression node being evaluated.
531 * @returns {Array.<ASTNode[]>} Groups of property AST node lists.
533 function createGroups(node
) {
534 if (node
.properties
.length
=== 1) {
535 return [node
.properties
];
538 return node
.properties
.reduce((groups
, property
) => {
539 const currentGroup
= last(groups
),
540 prev
= last(currentGroup
);
542 if (!prev
|| continuesPropertyGroup(prev
, property
)) {
543 currentGroup
.push(property
);
545 groups
.push([property
]);
555 * Verifies correct vertical alignment of a group of properties.
556 * @param {ASTNode[]} properties List of Property AST nodes.
559 function verifyGroupAlignment(properties
) {
560 const length
= properties
.length
,
561 widths
= properties
.map(getKeyWidth
), // Width of keys, including quotes
562 align
= alignmentOptions
.on
; // "value" or "colon"
563 let targetWidth
= Math
.max(...widths
),
564 beforeColon
, afterColon
, mode
;
566 if (alignmentOptions
&& length
> 1) { // When aligning values within a group, use the alignment configuration.
567 beforeColon
= alignmentOptions
.beforeColon
;
568 afterColon
= alignmentOptions
.afterColon
;
569 mode
= alignmentOptions
.mode
;
571 beforeColon
= multiLineOptions
.beforeColon
;
572 afterColon
= multiLineOptions
.afterColon
;
573 mode
= alignmentOptions
.mode
;
576 // Conditionally include one space before or after colon
577 targetWidth
+= (align
=== "colon" ? beforeColon
: afterColon
);
579 for (let i
= 0; i
< length
; i
++) {
580 const property
= properties
[i
];
581 const whitespace
= getPropertyWhitespace(property
);
583 if (whitespace
) { // Object literal getters/setters lack a colon
584 const width
= widths
[i
];
586 if (align
=== "value") {
587 report(property
, "key", whitespace
.beforeColon
, beforeColon
, mode
);
588 report(property
, "value", whitespace
.afterColon
, targetWidth
- width
, mode
);
589 } else { // align = "colon"
590 report(property
, "key", whitespace
.beforeColon
, targetWidth
- width
, mode
);
591 report(property
, "value", whitespace
.afterColon
, afterColon
, mode
);
598 * Verifies spacing of property conforms to specified options.
599 * @param {ASTNode} node Property node being evaluated.
600 * @param {Object} lineOptions Configured singleLine or multiLine options
603 function verifySpacing(node
, lineOptions
) {
604 const actual
= getPropertyWhitespace(node
);
606 if (actual
) { // Object literal getters/setters lack colons
607 report(node
, "key", actual
.beforeColon
, lineOptions
.beforeColon
, lineOptions
.mode
);
608 report(node
, "value", actual
.afterColon
, lineOptions
.afterColon
, lineOptions
.mode
);
613 * Verifies spacing of each property in a list.
614 * @param {ASTNode[]} properties List of Property AST nodes.
615 * @param {Object} lineOptions Configured singleLine or multiLine options
618 function verifyListSpacing(properties
, lineOptions
) {
619 const length
= properties
.length
;
621 for (let i
= 0; i
< length
; i
++) {
622 verifySpacing(properties
[i
], lineOptions
);
627 * Verifies vertical alignment, taking into account groups of properties.
628 * @param {ASTNode} node ObjectExpression node being evaluated.
631 function verifyAlignment(node
) {
632 createGroups(node
).forEach(group
=> {
633 const properties
= group
.filter(isKeyValueProperty
);
635 if (properties
.length
> 0 && isSingleLineProperties(properties
)) {
636 verifyListSpacing(properties
, multiLineOptions
);
638 verifyGroupAlignment(properties
);
643 //--------------------------------------------------------------------------
645 //--------------------------------------------------------------------------
647 if (alignmentOptions
) { // Verify vertical alignment
650 ObjectExpression(node
) {
651 if (isSingleLine(node
)) {
652 verifyListSpacing(node
.properties
.filter(isKeyValueProperty
), singleLineOptions
);
654 verifyAlignment(node
);
661 // Obey beforeColon and afterColon in each property as configured
664 verifySpacing(node
, isSingleLine(node
.parent
) ? singleLineOptions
: multiLineOptions
);