prefer-object-spread.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. /**
  2. * @fileoverview Prefers object spread property over Object.assign
  3. * @author Sharmila Jesupaul
  4. * See LICENSE file in root directory for full license.
  5. */
  6. "use strict";
  7. const { CALL, ReferenceTracker } = require("eslint-utils");
  8. const {
  9. isCommaToken,
  10. isOpeningParenToken,
  11. isClosingParenToken,
  12. isParenthesised
  13. } = require("./utils/ast-utils");
  14. const ANY_SPACE = /\s/u;
  15. /**
  16. * Helper that checks if the Object.assign call has array spread
  17. * @param {ASTNode} node The node that the rule warns on
  18. * @returns {boolean} - Returns true if the Object.assign call has array spread
  19. */
  20. function hasArraySpread(node) {
  21. return node.arguments.some(arg => arg.type === "SpreadElement");
  22. }
  23. /**
  24. * Determines whether the given node is an accessor property (getter/setter).
  25. * @param {ASTNode} node Node to check.
  26. * @returns {boolean} `true` if the node is a getter or a setter.
  27. */
  28. function isAccessorProperty(node) {
  29. return node.type === "Property" &&
  30. (node.kind === "get" || node.kind === "set");
  31. }
  32. /**
  33. * Determines whether the given object expression node has accessor properties (getters/setters).
  34. * @param {ASTNode} node `ObjectExpression` node to check.
  35. * @returns {boolean} `true` if the node has at least one getter/setter.
  36. */
  37. function hasAccessors(node) {
  38. return node.properties.some(isAccessorProperty);
  39. }
  40. /**
  41. * Determines whether the given call expression node has object expression arguments with accessor properties (getters/setters).
  42. * @param {ASTNode} node `CallExpression` node to check.
  43. * @returns {boolean} `true` if the node has at least one argument that is an object expression with at least one getter/setter.
  44. */
  45. function hasArgumentsWithAccessors(node) {
  46. return node.arguments
  47. .filter(arg => arg.type === "ObjectExpression")
  48. .some(hasAccessors);
  49. }
  50. /**
  51. * Helper that checks if the node needs parentheses to be valid JS.
  52. * The default is to wrap the node in parentheses to avoid parsing errors.
  53. * @param {ASTNode} node The node that the rule warns on
  54. * @param {Object} sourceCode in context sourcecode object
  55. * @returns {boolean} - Returns true if the node needs parentheses
  56. */
  57. function needsParens(node, sourceCode) {
  58. const parent = node.parent;
  59. switch (parent.type) {
  60. case "VariableDeclarator":
  61. case "ArrayExpression":
  62. case "ReturnStatement":
  63. case "CallExpression":
  64. case "Property":
  65. return false;
  66. case "AssignmentExpression":
  67. return parent.left === node && !isParenthesised(sourceCode, node);
  68. default:
  69. return !isParenthesised(sourceCode, node);
  70. }
  71. }
  72. /**
  73. * Determines if an argument needs parentheses. The default is to not add parens.
  74. * @param {ASTNode} node The node to be checked.
  75. * @param {Object} sourceCode in context sourcecode object
  76. * @returns {boolean} True if the node needs parentheses
  77. */
  78. function argNeedsParens(node, sourceCode) {
  79. switch (node.type) {
  80. case "AssignmentExpression":
  81. case "ArrowFunctionExpression":
  82. case "ConditionalExpression":
  83. return !isParenthesised(sourceCode, node);
  84. default:
  85. return false;
  86. }
  87. }
  88. /**
  89. * Get the parenthesis tokens of a given ObjectExpression node.
  90. * This includes the braces of the object literal and enclosing parentheses.
  91. * @param {ASTNode} node The node to get.
  92. * @param {Token} leftArgumentListParen The opening paren token of the argument list.
  93. * @param {SourceCode} sourceCode The source code object to get tokens.
  94. * @returns {Token[]} The parenthesis tokens of the node. This is sorted by the location.
  95. */
  96. function getParenTokens(node, leftArgumentListParen, sourceCode) {
  97. const parens = [sourceCode.getFirstToken(node), sourceCode.getLastToken(node)];
  98. let leftNext = sourceCode.getTokenBefore(node);
  99. let rightNext = sourceCode.getTokenAfter(node);
  100. // Note: don't include the parens of the argument list.
  101. while (
  102. leftNext &&
  103. rightNext &&
  104. leftNext.range[0] > leftArgumentListParen.range[0] &&
  105. isOpeningParenToken(leftNext) &&
  106. isClosingParenToken(rightNext)
  107. ) {
  108. parens.push(leftNext, rightNext);
  109. leftNext = sourceCode.getTokenBefore(leftNext);
  110. rightNext = sourceCode.getTokenAfter(rightNext);
  111. }
  112. return parens.sort((a, b) => a.range[0] - b.range[0]);
  113. }
  114. /**
  115. * Get the range of a given token and around whitespaces.
  116. * @param {Token} token The token to get range.
  117. * @param {SourceCode} sourceCode The source code object to get tokens.
  118. * @returns {number} The end of the range of the token and around whitespaces.
  119. */
  120. function getStartWithSpaces(token, sourceCode) {
  121. const text = sourceCode.text;
  122. let start = token.range[0];
  123. // If the previous token is a line comment then skip this step to avoid commenting this token out.
  124. {
  125. const prevToken = sourceCode.getTokenBefore(token, { includeComments: true });
  126. if (prevToken && prevToken.type === "Line") {
  127. return start;
  128. }
  129. }
  130. // Detect spaces before the token.
  131. while (ANY_SPACE.test(text[start - 1] || "")) {
  132. start -= 1;
  133. }
  134. return start;
  135. }
  136. /**
  137. * Get the range of a given token and around whitespaces.
  138. * @param {Token} token The token to get range.
  139. * @param {SourceCode} sourceCode The source code object to get tokens.
  140. * @returns {number} The start of the range of the token and around whitespaces.
  141. */
  142. function getEndWithSpaces(token, sourceCode) {
  143. const text = sourceCode.text;
  144. let end = token.range[1];
  145. // Detect spaces after the token.
  146. while (ANY_SPACE.test(text[end] || "")) {
  147. end += 1;
  148. }
  149. return end;
  150. }
  151. /**
  152. * Autofixes the Object.assign call to use an object spread instead.
  153. * @param {ASTNode|null} node The node that the rule warns on, i.e. the Object.assign call
  154. * @param {string} sourceCode sourceCode of the Object.assign call
  155. * @returns {Function} autofixer - replaces the Object.assign with a spread object.
  156. */
  157. function defineFixer(node, sourceCode) {
  158. return function *(fixer) {
  159. const leftParen = sourceCode.getTokenAfter(node.callee, isOpeningParenToken);
  160. const rightParen = sourceCode.getLastToken(node);
  161. // Remove everything before the opening paren: callee `Object.assign`, type arguments, and whitespace between the callee and the paren.
  162. yield fixer.removeRange([node.range[0], leftParen.range[0]]);
  163. // Replace the parens of argument list to braces.
  164. if (needsParens(node, sourceCode)) {
  165. yield fixer.replaceText(leftParen, "({");
  166. yield fixer.replaceText(rightParen, "})");
  167. } else {
  168. yield fixer.replaceText(leftParen, "{");
  169. yield fixer.replaceText(rightParen, "}");
  170. }
  171. // Process arguments.
  172. for (const argNode of node.arguments) {
  173. const innerParens = getParenTokens(argNode, leftParen, sourceCode);
  174. const left = innerParens.shift();
  175. const right = innerParens.pop();
  176. if (argNode.type === "ObjectExpression") {
  177. const maybeTrailingComma = sourceCode.getLastToken(argNode, 1);
  178. const maybeArgumentComma = sourceCode.getTokenAfter(right);
  179. /*
  180. * Make bare this object literal.
  181. * And remove spaces inside of the braces for better formatting.
  182. */
  183. for (const innerParen of innerParens) {
  184. yield fixer.remove(innerParen);
  185. }
  186. const leftRange = [left.range[0], getEndWithSpaces(left, sourceCode)];
  187. const rightRange = [
  188. Math.max(getStartWithSpaces(right, sourceCode), leftRange[1]), // Ensure ranges don't overlap
  189. right.range[1]
  190. ];
  191. yield fixer.removeRange(leftRange);
  192. yield fixer.removeRange(rightRange);
  193. // Remove the comma of this argument if it's duplication.
  194. if (
  195. (argNode.properties.length === 0 || isCommaToken(maybeTrailingComma)) &&
  196. isCommaToken(maybeArgumentComma)
  197. ) {
  198. yield fixer.remove(maybeArgumentComma);
  199. }
  200. } else {
  201. // Make spread.
  202. if (argNeedsParens(argNode, sourceCode)) {
  203. yield fixer.insertTextBefore(left, "...(");
  204. yield fixer.insertTextAfter(right, ")");
  205. } else {
  206. yield fixer.insertTextBefore(left, "...");
  207. }
  208. }
  209. }
  210. };
  211. }
  212. /** @type {import('../shared/types').Rule} */
  213. module.exports = {
  214. meta: {
  215. type: "suggestion",
  216. docs: {
  217. description:
  218. "Disallow using Object.assign with an object literal as the first argument and prefer the use of object spread instead",
  219. recommended: false,
  220. url: "https://eslint.org/docs/rules/prefer-object-spread"
  221. },
  222. schema: [],
  223. fixable: "code",
  224. messages: {
  225. useSpreadMessage: "Use an object spread instead of `Object.assign` eg: `{ ...foo }`.",
  226. useLiteralMessage: "Use an object literal instead of `Object.assign`. eg: `{ foo: bar }`."
  227. }
  228. },
  229. create(context) {
  230. const sourceCode = context.getSourceCode();
  231. return {
  232. Program() {
  233. const scope = context.getScope();
  234. const tracker = new ReferenceTracker(scope);
  235. const trackMap = {
  236. Object: {
  237. assign: { [CALL]: true }
  238. }
  239. };
  240. // Iterate all calls of `Object.assign` (only of the global variable `Object`).
  241. for (const { node } of tracker.iterateGlobalReferences(trackMap)) {
  242. if (
  243. node.arguments.length >= 1 &&
  244. node.arguments[0].type === "ObjectExpression" &&
  245. !hasArraySpread(node) &&
  246. !(
  247. node.arguments.length > 1 &&
  248. hasArgumentsWithAccessors(node)
  249. )
  250. ) {
  251. const messageId = node.arguments.length === 1
  252. ? "useLiteralMessage"
  253. : "useSpreadMessage";
  254. const fix = defineFixer(node, sourceCode);
  255. context.report({ node, messageId, fix });
  256. }
  257. }
  258. }
  259. };
  260. }
  261. };