| 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);            }        };    }};
 |