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