123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678 |
- /**
- * @fileoverview Rule to specify spacing of object literal keys and values
- * @author Brandon Mills
- */
- "use strict";
- //------------------------------------------------------------------------------
- // Requirements
- //------------------------------------------------------------------------------
- const astUtils = require("./utils/ast-utils");
- const GraphemeSplitter = require("grapheme-splitter");
- const splitter = new GraphemeSplitter();
- //------------------------------------------------------------------------------
- // Helpers
- //------------------------------------------------------------------------------
- /**
- * 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[arr.length - 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('../shared/types').Rule} */
- module.exports = {
- meta: {
- type: "layout",
- docs: {
- description: "Enforce consistent spacing between keys and values in object literal properties",
- recommended: false,
- url: "https://eslint.org/docs/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.getSourceCode();
- /**
- * 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"
- );
- }
- /**
- * 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) ? candidate.value : 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;
- }
- /**
- * Starting from the given a 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 = sourceCode.getTokenAfter(node, astUtils.isColonToken);
- return sourceCode.getTokenBefore(colonToken);
- }
- /**
- * Starting from the given a 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);
- }
- /**
- * 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 {int} 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 {int} Width of the key.
- */
- function getKeyWidth(property) {
- const startToken = sourceCode.getFirstToken(property);
- const endToken = getLastTokenBeforeColon(property.key);
- return splitter.countGraphemes(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);
- }
- };
- }
- };
|