| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822 |
- /**
- * @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,
- );
- },
- };
- },
- };
|