Permalink
Cannot retrieve contributors at this time
| /** | |
| * @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"); | |
| } | |
| /** | |
| * 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 incldues 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 the callee `Object.assign` | |
| yield fixer.remove(node.callee); | |
| // 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, "..."); | |
| } | |
| } | |
| } | |
| }; | |
| } | |
| 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.", | |
| category: "Stylistic Issues", | |
| 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) | |
| ) { | |
| const messageId = node.arguments.length === 1 | |
| ? "useLiteralMessage" | |
| : "useSpreadMessage"; | |
| const fix = defineFixer(node, sourceCode); | |
| context.report({ node, messageId, fix }); | |
| } | |
| } | |
| } | |
| }; | |
| } | |
| }; |