| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299 | /** * @fileoverview Prefers object spread property over Object.assign * @author Sharmila Jesupaul * See LICENSE file in root directory for full license. */"use strict";const { CALL, ReferenceTracker } = require("eslint-utils");const {    isCommaToken,    isOpeningParenToken,    isClosingParenToken,    isParenthesised} = require("./utils/ast-utils");const ANY_SPACE = /\s/u;/** * Helper that checks if the Object.assign call has array spread * @param {ASTNode} node The node that the rule warns on * @returns {boolean} - Returns true if the Object.assign call has array spread */function hasArraySpread(node) {    return node.arguments.some(arg => arg.type === "SpreadElement");}/** * Determines whether the given node is an accessor property (getter/setter). * @param {ASTNode} node Node to check. * @returns {boolean} `true` if the node is a getter or a setter. */function isAccessorProperty(node) {    return node.type === "Property" &&        (node.kind === "get" || node.kind === "set");}/** * Determines whether the given object expression node has accessor properties (getters/setters). * @param {ASTNode} node `ObjectExpression` node to check. * @returns {boolean} `true` if the node has at least one getter/setter. */function hasAccessors(node) {    return node.properties.some(isAccessorProperty);}/** * Determines whether the given call expression node has object expression arguments with accessor properties (getters/setters). * @param {ASTNode} node `CallExpression` node to check. * @returns {boolean} `true` if the node has at least one argument that is an object expression with at least one getter/setter. */function hasArgumentsWithAccessors(node) {    return node.arguments        .filter(arg => arg.type === "ObjectExpression")        .some(hasAccessors);}/** * Helper that checks if the node needs parentheses to be valid JS. * The default is to wrap the node in parentheses to avoid parsing errors. * @param {ASTNode} node The node that the rule warns on * @param {Object} sourceCode in context sourcecode object * @returns {boolean} - Returns true if the node needs parentheses */function needsParens(node, sourceCode) {    const parent = node.parent;    switch (parent.type) {        case "VariableDeclarator":        case "ArrayExpression":        case "ReturnStatement":        case "CallExpression":        case "Property":            return false;        case "AssignmentExpression":            return parent.left === node && !isParenthesised(sourceCode, node);        default:            return !isParenthesised(sourceCode, node);    }}/** * Determines if an argument needs parentheses. The default is to not add parens. * @param {ASTNode} node The node to be checked. * @param {Object} sourceCode in context sourcecode object * @returns {boolean} True if the node needs parentheses */function argNeedsParens(node, sourceCode) {    switch (node.type) {        case "AssignmentExpression":        case "ArrowFunctionExpression":        case "ConditionalExpression":            return !isParenthesised(sourceCode, node);        default:            return false;    }}/** * Get the parenthesis tokens of a given ObjectExpression node. * This includes the braces of the object literal and enclosing parentheses. * @param {ASTNode} node The node to get. * @param {Token} leftArgumentListParen The opening paren token of the argument list. * @param {SourceCode} sourceCode The source code object to get tokens. * @returns {Token[]} The parenthesis tokens of the node. This is sorted by the location. */function getParenTokens(node, leftArgumentListParen, sourceCode) {    const parens = [sourceCode.getFirstToken(node), sourceCode.getLastToken(node)];    let leftNext = sourceCode.getTokenBefore(node);    let rightNext = sourceCode.getTokenAfter(node);    // Note: don't include the parens of the argument list.    while (        leftNext &&        rightNext &&        leftNext.range[0] > leftArgumentListParen.range[0] &&        isOpeningParenToken(leftNext) &&        isClosingParenToken(rightNext)    ) {        parens.push(leftNext, rightNext);        leftNext = sourceCode.getTokenBefore(leftNext);        rightNext = sourceCode.getTokenAfter(rightNext);    }    return parens.sort((a, b) => a.range[0] - b.range[0]);}/** * Get the range of a given token and around whitespaces. * @param {Token} token The token to get range. * @param {SourceCode} sourceCode The source code object to get tokens. * @returns {number} The end of the range of the token and around whitespaces. */function getStartWithSpaces(token, sourceCode) {    const text = sourceCode.text;    let start = token.range[0];    // If the previous token is a line comment then skip this step to avoid commenting this token out.    {        const prevToken = sourceCode.getTokenBefore(token, { includeComments: true });        if (prevToken && prevToken.type === "Line") {            return start;        }    }    // Detect spaces before the token.    while (ANY_SPACE.test(text[start - 1] || "")) {        start -= 1;    }    return start;}/** * Get the range of a given token and around whitespaces. * @param {Token} token The token to get range. * @param {SourceCode} sourceCode The source code object to get tokens. * @returns {number} The start of the range of the token and around whitespaces. */function getEndWithSpaces(token, sourceCode) {    const text = sourceCode.text;    let end = token.range[1];    // Detect spaces after the token.    while (ANY_SPACE.test(text[end] || "")) {        end += 1;    }    return end;}/** * Autofixes the Object.assign call to use an object spread instead. * @param {ASTNode|null} node The node that the rule warns on, i.e. the Object.assign call * @param {string} sourceCode sourceCode of the Object.assign call * @returns {Function} autofixer - replaces the Object.assign with a spread object. */function defineFixer(node, sourceCode) {    return function *(fixer) {        const leftParen = sourceCode.getTokenAfter(node.callee, isOpeningParenToken);        const rightParen = sourceCode.getLastToken(node);        // Remove everything before the opening paren: callee `Object.assign`, type arguments, and whitespace between the callee and the paren.        yield fixer.removeRange([node.range[0], leftParen.range[0]]);        // Replace the parens of argument list to braces.        if (needsParens(node, sourceCode)) {            yield fixer.replaceText(leftParen, "({");            yield fixer.replaceText(rightParen, "})");        } else {            yield fixer.replaceText(leftParen, "{");            yield fixer.replaceText(rightParen, "}");        }        // Process arguments.        for (const argNode of node.arguments) {            const innerParens = getParenTokens(argNode, leftParen, sourceCode);            const left = innerParens.shift();            const right = innerParens.pop();            if (argNode.type === "ObjectExpression") {                const maybeTrailingComma = sourceCode.getLastToken(argNode, 1);                const maybeArgumentComma = sourceCode.getTokenAfter(right);                /*                 * Make bare this object literal.                 * And remove spaces inside of the braces for better formatting.                 */                for (const innerParen of innerParens) {                    yield fixer.remove(innerParen);                }                const leftRange = [left.range[0], getEndWithSpaces(left, sourceCode)];                const rightRange = [                    Math.max(getStartWithSpaces(right, sourceCode), leftRange[1]), // Ensure ranges don't overlap                    right.range[1]                ];                yield fixer.removeRange(leftRange);                yield fixer.removeRange(rightRange);                // Remove the comma of this argument if it's duplication.                if (                    (argNode.properties.length === 0 || isCommaToken(maybeTrailingComma)) &&                    isCommaToken(maybeArgumentComma)                ) {                    yield fixer.remove(maybeArgumentComma);                }            } else {                // Make spread.                if (argNeedsParens(argNode, sourceCode)) {                    yield fixer.insertTextBefore(left, "...(");                    yield fixer.insertTextAfter(right, ")");                } else {                    yield fixer.insertTextBefore(left, "...");                }            }        }    };}/** @type {import('../shared/types').Rule} */module.exports = {    meta: {        type: "suggestion",        docs: {            description:                "Disallow using Object.assign with an object literal as the first argument and prefer the use of object spread instead",            recommended: false,            url: "https://eslint.org/docs/rules/prefer-object-spread"        },        schema: [],        fixable: "code",        messages: {            useSpreadMessage: "Use an object spread instead of `Object.assign` eg: `{ ...foo }`.",            useLiteralMessage: "Use an object literal instead of `Object.assign`. eg: `{ foo: bar }`."        }    },    create(context) {        const sourceCode = context.getSourceCode();        return {            Program() {                const scope = context.getScope();                const tracker = new ReferenceTracker(scope);                const trackMap = {                    Object: {                        assign: { [CALL]: true }                    }                };                // Iterate all calls of `Object.assign` (only of the global variable `Object`).                for (const { node } of tracker.iterateGlobalReferences(trackMap)) {                    if (                        node.arguments.length >= 1 &&                        node.arguments[0].type === "ObjectExpression" &&                        !hasArraySpread(node) &&                        !(                            node.arguments.length > 1 &&                            hasArgumentsWithAccessors(node)                        )                    ) {                        const messageId = node.arguments.length === 1                            ? "useLiteralMessage"                            : "useSpreadMessage";                        const fix = defineFixer(node, sourceCode);                        context.report({ node, messageId, fix });                    }                }            }        };    }};
 |