| /** |
| * @fileoverview Rule to specify spacing of object literal keys and values |
| * @author Brandon Mills |
| * @deprecated in ESLint v8.53.0 |
| */ |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Requirements |
| //------------------------------------------------------------------------------ |
| |
| const astUtils = require("./utils/ast-utils"); |
| const { getGraphemeCount } = require("../shared/string-utils"); |
| |
| /** |
| * Checks whether a string contains a line terminator as defined in |
| * http://www.ecma-international.org/ecma-262/5.1/#sec-7.3 |
| * @param {string} str String to test. |
| * @returns {boolean} True if str contains a line terminator. |
| */ |
| function containsLineTerminator(str) { |
| return astUtils.LINEBREAK_MATCHER.test(str); |
| } |
| |
| /** |
| * Gets the last element of an array. |
| * @param {Array} arr An array. |
| * @returns {any} Last element of arr. |
| */ |
| function last(arr) { |
| return arr.at(-1); |
| } |
| |
| /** |
| * Checks whether a node is contained on a single line. |
| * @param {ASTNode} node AST Node being evaluated. |
| * @returns {boolean} True if the node is a single line. |
| */ |
| function isSingleLine(node) { |
| return node.loc.end.line === node.loc.start.line; |
| } |
| |
| /** |
| * Checks whether the properties on a single line. |
| * @param {ASTNode[]} properties List of Property AST nodes. |
| * @returns {boolean} True if all properties is on a single line. |
| */ |
| function isSingleLineProperties(properties) { |
| const [firstProp] = properties, |
| lastProp = last(properties); |
| |
| return firstProp.loc.start.line === lastProp.loc.end.line; |
| } |
| |
| /** |
| * Initializes a single option property from the configuration with defaults for undefined values |
| * @param {Object} toOptions Object to be initialized |
| * @param {Object} fromOptions Object to be initialized from |
| * @returns {Object} The object with correctly initialized options and values |
| */ |
| function initOptionProperty(toOptions, fromOptions) { |
| toOptions.mode = fromOptions.mode || "strict"; |
| |
| // Set value of beforeColon |
| if (typeof fromOptions.beforeColon !== "undefined") { |
| toOptions.beforeColon = +fromOptions.beforeColon; |
| } else { |
| toOptions.beforeColon = 0; |
| } |
| |
| // Set value of afterColon |
| if (typeof fromOptions.afterColon !== "undefined") { |
| toOptions.afterColon = +fromOptions.afterColon; |
| } else { |
| toOptions.afterColon = 1; |
| } |
| |
| // Set align if exists |
| if (typeof fromOptions.align !== "undefined") { |
| if (typeof fromOptions.align === "object") { |
| toOptions.align = fromOptions.align; |
| } else { |
| // "string" |
| toOptions.align = { |
| on: fromOptions.align, |
| mode: toOptions.mode, |
| beforeColon: toOptions.beforeColon, |
| afterColon: toOptions.afterColon, |
| }; |
| } |
| } |
| |
| return toOptions; |
| } |
| |
| /** |
| * Initializes all the option values (singleLine, multiLine and align) from the configuration with defaults for undefined values |
| * @param {Object} toOptions Object to be initialized |
| * @param {Object} fromOptions Object to be initialized from |
| * @returns {Object} The object with correctly initialized options and values |
| */ |
| function initOptions(toOptions, fromOptions) { |
| if (typeof fromOptions.align === "object") { |
| // Initialize the alignment configuration |
| toOptions.align = initOptionProperty({}, fromOptions.align); |
| toOptions.align.on = fromOptions.align.on || "colon"; |
| toOptions.align.mode = fromOptions.align.mode || "strict"; |
| |
| toOptions.multiLine = initOptionProperty( |
| {}, |
| fromOptions.multiLine || fromOptions, |
| ); |
| toOptions.singleLine = initOptionProperty( |
| {}, |
| fromOptions.singleLine || fromOptions, |
| ); |
| } else { |
| // string or undefined |
| toOptions.multiLine = initOptionProperty( |
| {}, |
| fromOptions.multiLine || fromOptions, |
| ); |
| toOptions.singleLine = initOptionProperty( |
| {}, |
| fromOptions.singleLine || fromOptions, |
| ); |
| |
| // If alignment options are defined in multiLine, pull them out into the general align configuration |
| if (toOptions.multiLine.align) { |
| toOptions.align = { |
| on: toOptions.multiLine.align.on, |
| mode: |
| toOptions.multiLine.align.mode || toOptions.multiLine.mode, |
| beforeColon: toOptions.multiLine.align.beforeColon, |
| afterColon: toOptions.multiLine.align.afterColon, |
| }; |
| } |
| } |
| |
| return toOptions; |
| } |
| |
| //------------------------------------------------------------------------------ |
| // Rule Definition |
| //------------------------------------------------------------------------------ |
| |
| /** @type {import('../types').Rule.RuleModule} */ |
| module.exports = { |
| meta: { |
| deprecated: { |
| message: "Formatting rules are being moved out of ESLint core.", |
| url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/", |
| deprecatedSince: "8.53.0", |
| availableUntil: "11.0.0", |
| replacedBy: [ |
| { |
| message: |
| "ESLint Stylistic now maintains deprecated stylistic core rules.", |
| url: "https://eslint.style/guide/migration", |
| plugin: { |
| name: "@stylistic/eslint-plugin", |
| url: "https://eslint.style", |
| }, |
| rule: { |
| name: "key-spacing", |
| url: "https://eslint.style/rules/key-spacing", |
| }, |
| }, |
| ], |
| }, |
| type: "layout", |
| |
| docs: { |
| description: |
| "Enforce consistent spacing between keys and values in object literal properties", |
| recommended: false, |
| url: "https://eslint.org/docs/latest/rules/key-spacing", |
| }, |
| |
| fixable: "whitespace", |
| |
| schema: [ |
| { |
| anyOf: [ |
| { |
| type: "object", |
| properties: { |
| align: { |
| anyOf: [ |
| { |
| enum: ["colon", "value"], |
| }, |
| { |
| type: "object", |
| properties: { |
| mode: { |
| enum: ["strict", "minimum"], |
| }, |
| on: { |
| enum: ["colon", "value"], |
| }, |
| beforeColon: { |
| type: "boolean", |
| }, |
| afterColon: { |
| type: "boolean", |
| }, |
| }, |
| additionalProperties: false, |
| }, |
| ], |
| }, |
| mode: { |
| enum: ["strict", "minimum"], |
| }, |
| beforeColon: { |
| type: "boolean", |
| }, |
| afterColon: { |
| type: "boolean", |
| }, |
| }, |
| additionalProperties: false, |
| }, |
| { |
| type: "object", |
| properties: { |
| singleLine: { |
| type: "object", |
| properties: { |
| mode: { |
| enum: ["strict", "minimum"], |
| }, |
| beforeColon: { |
| type: "boolean", |
| }, |
| afterColon: { |
| type: "boolean", |
| }, |
| }, |
| additionalProperties: false, |
| }, |
| multiLine: { |
| type: "object", |
| properties: { |
| align: { |
| anyOf: [ |
| { |
| enum: ["colon", "value"], |
| }, |
| { |
| type: "object", |
| properties: { |
| mode: { |
| enum: [ |
| "strict", |
| "minimum", |
| ], |
| }, |
| on: { |
| enum: [ |
| "colon", |
| "value", |
| ], |
| }, |
| beforeColon: { |
| type: "boolean", |
| }, |
| afterColon: { |
| type: "boolean", |
| }, |
| }, |
| additionalProperties: false, |
| }, |
| ], |
| }, |
| mode: { |
| enum: ["strict", "minimum"], |
| }, |
| beforeColon: { |
| type: "boolean", |
| }, |
| afterColon: { |
| type: "boolean", |
| }, |
| }, |
| additionalProperties: false, |
| }, |
| }, |
| additionalProperties: false, |
| }, |
| { |
| type: "object", |
| properties: { |
| singleLine: { |
| type: "object", |
| properties: { |
| mode: { |
| enum: ["strict", "minimum"], |
| }, |
| beforeColon: { |
| type: "boolean", |
| }, |
| afterColon: { |
| type: "boolean", |
| }, |
| }, |
| additionalProperties: false, |
| }, |
| multiLine: { |
| type: "object", |
| properties: { |
| mode: { |
| enum: ["strict", "minimum"], |
| }, |
| beforeColon: { |
| type: "boolean", |
| }, |
| afterColon: { |
| type: "boolean", |
| }, |
| }, |
| additionalProperties: false, |
| }, |
| align: { |
| type: "object", |
| properties: { |
| mode: { |
| enum: ["strict", "minimum"], |
| }, |
| on: { |
| enum: ["colon", "value"], |
| }, |
| beforeColon: { |
| type: "boolean", |
| }, |
| afterColon: { |
| type: "boolean", |
| }, |
| }, |
| additionalProperties: false, |
| }, |
| }, |
| additionalProperties: false, |
| }, |
| ], |
| }, |
| ], |
| messages: { |
| extraKey: "Extra space after {{computed}}key '{{key}}'.", |
| extraValue: |
| "Extra space before value for {{computed}}key '{{key}}'.", |
| missingKey: "Missing space after {{computed}}key '{{key}}'.", |
| missingValue: |
| "Missing space before value for {{computed}}key '{{key}}'.", |
| }, |
| }, |
| |
| create(context) { |
| /** |
| * OPTIONS |
| * "key-spacing": [2, { |
| * beforeColon: false, |
| * afterColon: true, |
| * align: "colon" // Optional, or "value" |
| * } |
| */ |
| const options = context.options[0] || {}, |
| ruleOptions = initOptions({}, options), |
| multiLineOptions = ruleOptions.multiLine, |
| singleLineOptions = ruleOptions.singleLine, |
| alignmentOptions = ruleOptions.align || null; |
| |
| const sourceCode = context.sourceCode; |
| |
| /** |
| * Determines if the given property is key-value property. |
| * @param {ASTNode} property Property node to check. |
| * @returns {boolean} Whether the property is a key-value property. |
| */ |
| function isKeyValueProperty(property) { |
| return !( |
| ( |
| property.method || |
| property.shorthand || |
| property.kind !== "init" || |
| property.type !== "Property" |
| ) // Could be "ExperimentalSpreadProperty" or "SpreadElement" |
| ); |
| } |
| |
| /** |
| * Starting from the given node (a property.key node here) looks forward |
| * until it finds the colon punctuator and returns it. |
| * @param {ASTNode} node The node to start looking from. |
| * @returns {ASTNode} The colon punctuator. |
| */ |
| function getNextColon(node) { |
| return sourceCode.getTokenAfter(node, astUtils.isColonToken); |
| } |
| |
| /** |
| * Starting from the given node (a property.key node here) looks forward |
| * until it finds the last token before a colon punctuator and returns it. |
| * @param {ASTNode} node The node to start looking from. |
| * @returns {ASTNode} The last token before a colon punctuator. |
| */ |
| function getLastTokenBeforeColon(node) { |
| const colonToken = getNextColon(node); |
| |
| return sourceCode.getTokenBefore(colonToken); |
| } |
| |
| /** |
| * Starting from the given node (a property.key node here) looks forward |
| * until it finds the first token after a colon punctuator and returns it. |
| * @param {ASTNode} node The node to start looking from. |
| * @returns {ASTNode} The first token after a colon punctuator. |
| */ |
| function getFirstTokenAfterColon(node) { |
| const colonToken = getNextColon(node); |
| |
| return sourceCode.getTokenAfter(colonToken); |
| } |
| |
| /** |
| * Checks whether a property is a member of the property group it follows. |
| * @param {ASTNode} lastMember The last Property known to be in the group. |
| * @param {ASTNode} candidate The next Property that might be in the group. |
| * @returns {boolean} True if the candidate property is part of the group. |
| */ |
| function continuesPropertyGroup(lastMember, candidate) { |
| const groupEndLine = lastMember.loc.start.line, |
| candidateValueStartLine = ( |
| isKeyValueProperty(candidate) |
| ? getFirstTokenAfterColon(candidate.key) |
| : candidate |
| ).loc.start.line; |
| |
| if (candidateValueStartLine - groupEndLine <= 1) { |
| return true; |
| } |
| |
| /* |
| * Check that the first comment is adjacent to the end of the group, the |
| * last comment is adjacent to the candidate property, and that successive |
| * comments are adjacent to each other. |
| */ |
| const leadingComments = sourceCode.getCommentsBefore(candidate); |
| |
| if ( |
| leadingComments.length && |
| leadingComments[0].loc.start.line - groupEndLine <= 1 && |
| candidateValueStartLine - last(leadingComments).loc.end.line <= |
| 1 |
| ) { |
| for (let i = 1; i < leadingComments.length; i++) { |
| if ( |
| leadingComments[i].loc.start.line - |
| leadingComments[i - 1].loc.end.line > |
| 1 |
| ) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Gets an object literal property's key as the identifier name or string value. |
| * @param {ASTNode} property Property node whose key to retrieve. |
| * @returns {string} The property's key. |
| */ |
| function getKey(property) { |
| const key = property.key; |
| |
| if (property.computed) { |
| return sourceCode.getText().slice(key.range[0], key.range[1]); |
| } |
| return astUtils.getStaticPropertyName(property); |
| } |
| |
| /** |
| * Reports an appropriately-formatted error if spacing is incorrect on one |
| * side of the colon. |
| * @param {ASTNode} property Key-value pair in an object literal. |
| * @param {string} side Side being verified - either "key" or "value". |
| * @param {string} whitespace Actual whitespace string. |
| * @param {number} expected Expected whitespace length. |
| * @param {string} mode Value of the mode as "strict" or "minimum" |
| * @returns {void} |
| */ |
| function report(property, side, whitespace, expected, mode) { |
| const diff = whitespace.length - expected; |
| |
| if ( |
| ((diff && mode === "strict") || |
| (diff < 0 && mode === "minimum") || |
| (diff > 0 && !expected && mode === "minimum")) && |
| !(expected && containsLineTerminator(whitespace)) |
| ) { |
| const nextColon = getNextColon(property.key), |
| tokenBeforeColon = sourceCode.getTokenBefore(nextColon, { |
| includeComments: true, |
| }), |
| tokenAfterColon = sourceCode.getTokenAfter(nextColon, { |
| includeComments: true, |
| }), |
| isKeySide = side === "key", |
| isExtra = diff > 0, |
| diffAbs = Math.abs(diff), |
| spaces = Array(diffAbs + 1).join(" "); |
| |
| const locStart = isKeySide |
| ? tokenBeforeColon.loc.end |
| : nextColon.loc.start; |
| const locEnd = isKeySide |
| ? nextColon.loc.start |
| : tokenAfterColon.loc.start; |
| const missingLoc = isKeySide |
| ? tokenBeforeColon.loc |
| : tokenAfterColon.loc; |
| const loc = isExtra |
| ? { start: locStart, end: locEnd } |
| : missingLoc; |
| |
| let fix; |
| |
| if (isExtra) { |
| let range; |
| |
| // Remove whitespace |
| if (isKeySide) { |
| range = [ |
| tokenBeforeColon.range[1], |
| tokenBeforeColon.range[1] + diffAbs, |
| ]; |
| } else { |
| range = [ |
| tokenAfterColon.range[0] - diffAbs, |
| tokenAfterColon.range[0], |
| ]; |
| } |
| fix = function (fixer) { |
| return fixer.removeRange(range); |
| }; |
| } else { |
| // Add whitespace |
| if (isKeySide) { |
| fix = function (fixer) { |
| return fixer.insertTextAfter( |
| tokenBeforeColon, |
| spaces, |
| ); |
| }; |
| } else { |
| fix = function (fixer) { |
| return fixer.insertTextBefore( |
| tokenAfterColon, |
| spaces, |
| ); |
| }; |
| } |
| } |
| |
| let messageId; |
| |
| if (isExtra) { |
| messageId = side === "key" ? "extraKey" : "extraValue"; |
| } else { |
| messageId = side === "key" ? "missingKey" : "missingValue"; |
| } |
| |
| context.report({ |
| node: property[side], |
| loc, |
| messageId, |
| data: { |
| computed: property.computed ? "computed " : "", |
| key: getKey(property), |
| }, |
| fix, |
| }); |
| } |
| } |
| |
| /** |
| * Gets the number of characters in a key, including quotes around string |
| * keys and braces around computed property keys. |
| * @param {ASTNode} property Property of on object literal. |
| * @returns {number} Width of the key. |
| */ |
| function getKeyWidth(property) { |
| const startToken = sourceCode.getFirstToken(property); |
| const endToken = getLastTokenBeforeColon(property.key); |
| |
| return getGraphemeCount( |
| sourceCode |
| .getText() |
| .slice(startToken.range[0], endToken.range[1]), |
| ); |
| } |
| |
| /** |
| * Gets the whitespace around the colon in an object literal property. |
| * @param {ASTNode} property Property node from an object literal. |
| * @returns {Object} Whitespace before and after the property's colon. |
| */ |
| function getPropertyWhitespace(property) { |
| const whitespace = /(\s*):(\s*)/u.exec( |
| sourceCode |
| .getText() |
| .slice(property.key.range[1], property.value.range[0]), |
| ); |
| |
| if (whitespace) { |
| return { |
| beforeColon: whitespace[1], |
| afterColon: whitespace[2], |
| }; |
| } |
| return null; |
| } |
| |
| /** |
| * Creates groups of properties. |
| * @param {ASTNode} node ObjectExpression node being evaluated. |
| * @returns {Array<ASTNode[]>} Groups of property AST node lists. |
| */ |
| function createGroups(node) { |
| if (node.properties.length === 1) { |
| return [node.properties]; |
| } |
| |
| return node.properties.reduce( |
| (groups, property) => { |
| const currentGroup = last(groups), |
| prev = last(currentGroup); |
| |
| if (!prev || continuesPropertyGroup(prev, property)) { |
| currentGroup.push(property); |
| } else { |
| groups.push([property]); |
| } |
| |
| return groups; |
| }, |
| [[]], |
| ); |
| } |
| |
| /** |
| * Verifies correct vertical alignment of a group of properties. |
| * @param {ASTNode[]} properties List of Property AST nodes. |
| * @returns {void} |
| */ |
| function verifyGroupAlignment(properties) { |
| const length = properties.length, |
| widths = properties.map(getKeyWidth), // Width of keys, including quotes |
| align = alignmentOptions.on; // "value" or "colon" |
| let targetWidth = Math.max(...widths), |
| beforeColon, |
| afterColon, |
| mode; |
| |
| if (alignmentOptions && length > 1) { |
| // When aligning values within a group, use the alignment configuration. |
| beforeColon = alignmentOptions.beforeColon; |
| afterColon = alignmentOptions.afterColon; |
| mode = alignmentOptions.mode; |
| } else { |
| beforeColon = multiLineOptions.beforeColon; |
| afterColon = multiLineOptions.afterColon; |
| mode = alignmentOptions.mode; |
| } |
| |
| // Conditionally include one space before or after colon |
| targetWidth += align === "colon" ? beforeColon : afterColon; |
| |
| for (let i = 0; i < length; i++) { |
| const property = properties[i]; |
| const whitespace = getPropertyWhitespace(property); |
| |
| if (whitespace) { |
| // Object literal getters/setters lack a colon |
| const width = widths[i]; |
| |
| if (align === "value") { |
| report( |
| property, |
| "key", |
| whitespace.beforeColon, |
| beforeColon, |
| mode, |
| ); |
| report( |
| property, |
| "value", |
| whitespace.afterColon, |
| targetWidth - width, |
| mode, |
| ); |
| } else { |
| // align = "colon" |
| report( |
| property, |
| "key", |
| whitespace.beforeColon, |
| targetWidth - width, |
| mode, |
| ); |
| report( |
| property, |
| "value", |
| whitespace.afterColon, |
| afterColon, |
| mode, |
| ); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Verifies spacing of property conforms to specified options. |
| * @param {ASTNode} node Property node being evaluated. |
| * @param {Object} lineOptions Configured singleLine or multiLine options |
| * @returns {void} |
| */ |
| function verifySpacing(node, lineOptions) { |
| const actual = getPropertyWhitespace(node); |
| |
| if (actual) { |
| // Object literal getters/setters lack colons |
| report( |
| node, |
| "key", |
| actual.beforeColon, |
| lineOptions.beforeColon, |
| lineOptions.mode, |
| ); |
| report( |
| node, |
| "value", |
| actual.afterColon, |
| lineOptions.afterColon, |
| lineOptions.mode, |
| ); |
| } |
| } |
| |
| /** |
| * Verifies spacing of each property in a list. |
| * @param {ASTNode[]} properties List of Property AST nodes. |
| * @param {Object} lineOptions Configured singleLine or multiLine options |
| * @returns {void} |
| */ |
| function verifyListSpacing(properties, lineOptions) { |
| const length = properties.length; |
| |
| for (let i = 0; i < length; i++) { |
| verifySpacing(properties[i], lineOptions); |
| } |
| } |
| |
| /** |
| * Verifies vertical alignment, taking into account groups of properties. |
| * @param {ASTNode} node ObjectExpression node being evaluated. |
| * @returns {void} |
| */ |
| function verifyAlignment(node) { |
| createGroups(node).forEach(group => { |
| const properties = group.filter(isKeyValueProperty); |
| |
| if ( |
| properties.length > 0 && |
| isSingleLineProperties(properties) |
| ) { |
| verifyListSpacing(properties, multiLineOptions); |
| } else { |
| verifyGroupAlignment(properties); |
| } |
| }); |
| } |
| |
| //-------------------------------------------------------------------------- |
| // Public API |
| //-------------------------------------------------------------------------- |
| |
| if (alignmentOptions) { |
| // Verify vertical alignment |
| |
| return { |
| ObjectExpression(node) { |
| if (isSingleLine(node)) { |
| verifyListSpacing( |
| node.properties.filter(isKeyValueProperty), |
| singleLineOptions, |
| ); |
| } else { |
| verifyAlignment(node); |
| } |
| }, |
| }; |
| } |
| |
| // Obey beforeColon and afterColon in each property as configured |
| return { |
| Property(node) { |
| verifySpacing( |
| node, |
| isSingleLine(node.parent) |
| ? singleLineOptions |
| : multiLineOptions, |
| ); |
| }, |
| }; |
| }, |
| }; |