prefer-const.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. /**
  2. * @fileoverview A rule to suggest using of const declaration for variables that are never reassigned after declared.
  3. * @author Toru Nagashima
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const FixTracker = require("./utils/fix-tracker");
  10. const astUtils = require("./utils/ast-utils");
  11. //------------------------------------------------------------------------------
  12. // Helpers
  13. //------------------------------------------------------------------------------
  14. const PATTERN_TYPE =
  15. /^(?:.+?Pattern|RestElement|SpreadProperty|ExperimentalRestProperty|Property)$/u;
  16. const DECLARATION_HOST_TYPE =
  17. /^(?:Program|BlockStatement|StaticBlock|SwitchCase)$/u;
  18. const DESTRUCTURING_HOST_TYPE =
  19. /^(?:VariableDeclarator|AssignmentExpression)$/u;
  20. /**
  21. * Checks whether a given node is located at `ForStatement.init` or not.
  22. * @param {ASTNode} node A node to check.
  23. * @returns {boolean} `true` if the node is located at `ForStatement.init`.
  24. */
  25. function isInitOfForStatement(node) {
  26. return node.parent.type === "ForStatement" && node.parent.init === node;
  27. }
  28. /**
  29. * Checks whether a given Identifier node becomes a VariableDeclaration or not.
  30. * @param {ASTNode} identifier An Identifier node to check.
  31. * @returns {boolean} `true` if the node can become a VariableDeclaration.
  32. */
  33. function canBecomeVariableDeclaration(identifier) {
  34. let node = identifier.parent;
  35. while (PATTERN_TYPE.test(node.type)) {
  36. node = node.parent;
  37. }
  38. return (
  39. node.type === "VariableDeclarator" ||
  40. (node.type === "AssignmentExpression" &&
  41. node.parent.type === "ExpressionStatement" &&
  42. DECLARATION_HOST_TYPE.test(node.parent.parent.type))
  43. );
  44. }
  45. /**
  46. * Checks if an property or element is from outer scope or function parameters
  47. * in destructing pattern.
  48. * @param {string} name A variable name to be checked.
  49. * @param {eslint-scope.Scope} initScope A scope to start find.
  50. * @returns {boolean} Indicates if the variable is from outer scope or function parameters.
  51. */
  52. function isOuterVariableInDestructing(name, initScope) {
  53. if (
  54. initScope.through.some(
  55. ref => ref.resolved && ref.resolved.name === name,
  56. )
  57. ) {
  58. return true;
  59. }
  60. const variable = astUtils.getVariableByName(initScope, name);
  61. if (variable !== null) {
  62. return variable.defs.some(def => def.type === "Parameter");
  63. }
  64. return false;
  65. }
  66. /**
  67. * Gets the VariableDeclarator/AssignmentExpression node that a given reference
  68. * belongs to.
  69. * This is used to detect a mix of reassigned and never reassigned in a
  70. * destructuring.
  71. * @param {eslint-scope.Reference} reference A reference to get.
  72. * @returns {ASTNode|null} A VariableDeclarator/AssignmentExpression node or
  73. * null.
  74. */
  75. function getDestructuringHost(reference) {
  76. if (!reference.isWrite()) {
  77. return null;
  78. }
  79. let node = reference.identifier.parent;
  80. while (PATTERN_TYPE.test(node.type)) {
  81. node = node.parent;
  82. }
  83. if (!DESTRUCTURING_HOST_TYPE.test(node.type)) {
  84. return null;
  85. }
  86. return node;
  87. }
  88. /**
  89. * Determines if a destructuring assignment node contains
  90. * any MemberExpression nodes. This is used to determine if a
  91. * variable that is only written once using destructuring can be
  92. * safely converted into a const declaration.
  93. * @param {ASTNode} node The ObjectPattern or ArrayPattern node to check.
  94. * @returns {boolean} True if the destructuring pattern contains
  95. * a MemberExpression, false if not.
  96. */
  97. function hasMemberExpressionAssignment(node) {
  98. switch (node.type) {
  99. case "ObjectPattern":
  100. return node.properties.some(prop => {
  101. if (prop) {
  102. /*
  103. * Spread elements have an argument property while
  104. * others have a value property. Because different
  105. * parsers use different node types for spread elements,
  106. * we just check if there is an argument property.
  107. */
  108. return hasMemberExpressionAssignment(
  109. prop.argument || prop.value,
  110. );
  111. }
  112. return false;
  113. });
  114. case "ArrayPattern":
  115. return node.elements.some(element => {
  116. if (element) {
  117. return hasMemberExpressionAssignment(element);
  118. }
  119. return false;
  120. });
  121. case "AssignmentPattern":
  122. return hasMemberExpressionAssignment(node.left);
  123. case "MemberExpression":
  124. return true;
  125. // no default
  126. }
  127. return false;
  128. }
  129. /**
  130. * Gets an identifier node of a given variable.
  131. *
  132. * If the initialization exists or one or more reading references exist before
  133. * the first assignment, the identifier node is the node of the declaration.
  134. * Otherwise, the identifier node is the node of the first assignment.
  135. *
  136. * If the variable should not change to const, this function returns null.
  137. * - If the variable is reassigned.
  138. * - If the variable is never initialized nor assigned.
  139. * - If the variable is initialized in a different scope from the declaration.
  140. * - If the unique assignment of the variable cannot change to a declaration.
  141. * e.g. `if (a) b = 1` / `return (b = 1)`
  142. * - If the variable is declared in the global scope and `eslintUsed` is `true`.
  143. * `/*exported foo` directive comment makes such variables. This rule does not
  144. * warn such variables because this rule cannot distinguish whether the
  145. * exported variables are reassigned or not.
  146. * @param {eslint-scope.Variable} variable A variable to get.
  147. * @param {boolean} ignoreReadBeforeAssign
  148. * The value of `ignoreReadBeforeAssign` option.
  149. * @returns {ASTNode|null}
  150. * An Identifier node if the variable should change to const.
  151. * Otherwise, null.
  152. */
  153. function getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign) {
  154. if (variable.eslintUsed && variable.scope.type === "global") {
  155. return null;
  156. }
  157. // Finds the unique WriteReference.
  158. let writer = null;
  159. let isReadBeforeInit = false;
  160. const references = variable.references;
  161. for (let i = 0; i < references.length; ++i) {
  162. const reference = references[i];
  163. if (reference.isWrite()) {
  164. const isReassigned =
  165. writer !== null && writer.identifier !== reference.identifier;
  166. if (isReassigned) {
  167. return null;
  168. }
  169. const destructuringHost = getDestructuringHost(reference);
  170. if (
  171. destructuringHost !== null &&
  172. destructuringHost.left !== void 0
  173. ) {
  174. const leftNode = destructuringHost.left;
  175. let hasOuterVariables = false,
  176. hasNonIdentifiers = false;
  177. if (leftNode.type === "ObjectPattern") {
  178. const properties = leftNode.properties;
  179. hasOuterVariables = properties
  180. .filter(prop => prop.value)
  181. .map(prop => prop.value.name)
  182. .some(name =>
  183. isOuterVariableInDestructing(name, variable.scope),
  184. );
  185. hasNonIdentifiers = hasMemberExpressionAssignment(leftNode);
  186. } else if (leftNode.type === "ArrayPattern") {
  187. const elements = leftNode.elements;
  188. hasOuterVariables = elements
  189. .map(element => element && element.name)
  190. .some(name =>
  191. isOuterVariableInDestructing(name, variable.scope),
  192. );
  193. hasNonIdentifiers = hasMemberExpressionAssignment(leftNode);
  194. }
  195. if (hasOuterVariables || hasNonIdentifiers) {
  196. return null;
  197. }
  198. }
  199. writer = reference;
  200. } else if (reference.isRead() && writer === null) {
  201. if (ignoreReadBeforeAssign) {
  202. return null;
  203. }
  204. isReadBeforeInit = true;
  205. }
  206. }
  207. /*
  208. * If the assignment is from a different scope, ignore it.
  209. * If the assignment cannot change to a declaration, ignore it.
  210. */
  211. const shouldBeConst =
  212. writer !== null &&
  213. writer.from === variable.scope &&
  214. canBecomeVariableDeclaration(writer.identifier);
  215. if (!shouldBeConst) {
  216. return null;
  217. }
  218. if (isReadBeforeInit) {
  219. return variable.defs[0].name;
  220. }
  221. return writer.identifier;
  222. }
  223. /**
  224. * Groups by the VariableDeclarator/AssignmentExpression node that each
  225. * reference of given variables belongs to.
  226. * This is used to detect a mix of reassigned and never reassigned in a
  227. * destructuring.
  228. * @param {eslint-scope.Variable[]} variables Variables to group by destructuring.
  229. * @param {boolean} ignoreReadBeforeAssign
  230. * The value of `ignoreReadBeforeAssign` option.
  231. * @returns {Map<ASTNode, ASTNode[]>} Grouped identifier nodes.
  232. */
  233. function groupByDestructuring(variables, ignoreReadBeforeAssign) {
  234. const identifierMap = new Map();
  235. for (let i = 0; i < variables.length; ++i) {
  236. const variable = variables[i];
  237. const references = variable.references;
  238. const identifier = getIdentifierIfShouldBeConst(
  239. variable,
  240. ignoreReadBeforeAssign,
  241. );
  242. let prevId = null;
  243. for (let j = 0; j < references.length; ++j) {
  244. const reference = references[j];
  245. const id = reference.identifier;
  246. /*
  247. * Avoid counting a reference twice or more for default values of
  248. * destructuring.
  249. */
  250. if (id === prevId) {
  251. continue;
  252. }
  253. prevId = id;
  254. // Add the identifier node into the destructuring group.
  255. const group = getDestructuringHost(reference);
  256. if (group) {
  257. if (identifierMap.has(group)) {
  258. identifierMap.get(group).push(identifier);
  259. } else {
  260. identifierMap.set(group, [identifier]);
  261. }
  262. }
  263. }
  264. }
  265. return identifierMap;
  266. }
  267. /**
  268. * Finds the nearest parent of node with a given type.
  269. * @param {ASTNode} node The node to search from.
  270. * @param {string} type The type field of the parent node.
  271. * @param {Function} shouldStop A predicate that returns true if the traversal should stop, and false otherwise.
  272. * @returns {ASTNode} The closest ancestor with the specified type; null if no such ancestor exists.
  273. */
  274. function findUp(node, type, shouldStop) {
  275. if (!node || shouldStop(node)) {
  276. return null;
  277. }
  278. if (node.type === type) {
  279. return node;
  280. }
  281. return findUp(node.parent, type, shouldStop);
  282. }
  283. //------------------------------------------------------------------------------
  284. // Rule Definition
  285. //------------------------------------------------------------------------------
  286. /** @type {import('../types').Rule.RuleModule} */
  287. module.exports = {
  288. meta: {
  289. type: "suggestion",
  290. defaultOptions: [
  291. {
  292. destructuring: "any",
  293. ignoreReadBeforeAssign: false,
  294. },
  295. ],
  296. docs: {
  297. description:
  298. "Require `const` declarations for variables that are never reassigned after declared",
  299. recommended: false,
  300. url: "https://eslint.org/docs/latest/rules/prefer-const",
  301. },
  302. fixable: "code",
  303. schema: [
  304. {
  305. type: "object",
  306. properties: {
  307. destructuring: { enum: ["any", "all"] },
  308. ignoreReadBeforeAssign: { type: "boolean" },
  309. },
  310. additionalProperties: false,
  311. },
  312. ],
  313. messages: {
  314. useConst: "'{{name}}' is never reassigned. Use 'const' instead.",
  315. },
  316. },
  317. create(context) {
  318. const [{ destructuring, ignoreReadBeforeAssign }] = context.options;
  319. const shouldMatchAnyDestructuredVariable = destructuring !== "all";
  320. const sourceCode = context.sourceCode;
  321. const variables = [];
  322. let reportCount = 0;
  323. let checkedId = null;
  324. let checkedName = "";
  325. /**
  326. * Reports given identifier nodes if all of the nodes should be declared
  327. * as const.
  328. *
  329. * The argument 'nodes' is an array of Identifier nodes.
  330. * This node is the result of 'getIdentifierIfShouldBeConst()', so it's
  331. * nullable. In simple declaration or assignment cases, the length of
  332. * the array is 1. In destructuring cases, the length of the array can
  333. * be 2 or more.
  334. * @param {(eslint-scope.Reference|null)[]} nodes
  335. * References which are grouped by destructuring to report.
  336. * @returns {void}
  337. */
  338. function checkGroup(nodes) {
  339. const nodesToReport = nodes.filter(Boolean);
  340. if (
  341. nodes.length &&
  342. (shouldMatchAnyDestructuredVariable ||
  343. nodesToReport.length === nodes.length)
  344. ) {
  345. const varDeclParent = findUp(
  346. nodes[0],
  347. "VariableDeclaration",
  348. parentNode => parentNode.type.endsWith("Statement"),
  349. );
  350. const isVarDecParentNull = varDeclParent === null;
  351. if (
  352. !isVarDecParentNull &&
  353. varDeclParent.declarations.length > 0
  354. ) {
  355. const firstDeclaration = varDeclParent.declarations[0];
  356. if (firstDeclaration.init) {
  357. const firstDecParent = firstDeclaration.init.parent;
  358. /*
  359. * First we check the declaration type and then depending on
  360. * if the type is a "VariableDeclarator" or its an "ObjectPattern"
  361. * we compare the name and id from the first identifier, if the names are different
  362. * we assign the new name, id and reset the count of reportCount and nodeCount in
  363. * order to check each block for the number of reported errors and base our fix
  364. * based on comparing nodes.length and nodesToReport.length.
  365. */
  366. if (firstDecParent.type === "VariableDeclarator") {
  367. if (firstDecParent.id.name !== checkedName) {
  368. checkedName = firstDecParent.id.name;
  369. reportCount = 0;
  370. }
  371. if (firstDecParent.id.type === "ObjectPattern") {
  372. if (firstDecParent.init.name !== checkedName) {
  373. checkedName = firstDecParent.init.name;
  374. reportCount = 0;
  375. }
  376. }
  377. if (firstDecParent.id !== checkedId) {
  378. checkedId = firstDecParent.id;
  379. reportCount = 0;
  380. }
  381. }
  382. }
  383. }
  384. let shouldFix =
  385. varDeclParent &&
  386. // Don't do a fix unless all variables in the declarations are initialized (or it's in a for-in or for-of loop)
  387. (varDeclParent.parent.type === "ForInStatement" ||
  388. varDeclParent.parent.type === "ForOfStatement" ||
  389. varDeclParent.declarations.every(
  390. declaration => declaration.init,
  391. )) &&
  392. /*
  393. * If options.destructuring is "all", then this warning will not occur unless
  394. * every assignment in the destructuring should be const. In that case, it's safe
  395. * to apply the fix.
  396. */
  397. nodesToReport.length === nodes.length;
  398. if (
  399. !isVarDecParentNull &&
  400. varDeclParent.declarations &&
  401. varDeclParent.declarations.length !== 1
  402. ) {
  403. if (
  404. varDeclParent &&
  405. varDeclParent.declarations &&
  406. varDeclParent.declarations.length >= 1
  407. ) {
  408. /*
  409. * Add nodesToReport.length to a count, then comparing the count to the length
  410. * of the declarations in the current block.
  411. */
  412. reportCount += nodesToReport.length;
  413. let totalDeclarationsCount = 0;
  414. varDeclParent.declarations.forEach(declaration => {
  415. if (declaration.id.type === "ObjectPattern") {
  416. totalDeclarationsCount +=
  417. declaration.id.properties.length;
  418. } else if (declaration.id.type === "ArrayPattern") {
  419. totalDeclarationsCount +=
  420. declaration.id.elements.length;
  421. } else {
  422. totalDeclarationsCount += 1;
  423. }
  424. });
  425. shouldFix =
  426. shouldFix && reportCount === totalDeclarationsCount;
  427. }
  428. }
  429. nodesToReport.forEach(node => {
  430. context.report({
  431. node,
  432. messageId: "useConst",
  433. data: node,
  434. fix: shouldFix
  435. ? fixer => {
  436. const letKeywordToken =
  437. sourceCode.getFirstToken(
  438. varDeclParent,
  439. t => t.value === varDeclParent.kind,
  440. );
  441. /**
  442. * Extend the replacement range to the whole declaration,
  443. * in order to prevent other fixes in the same pass
  444. * https://github.com/eslint/eslint/issues/13899
  445. */
  446. return new FixTracker(fixer, sourceCode)
  447. .retainRange(varDeclParent.range)
  448. .replaceTextRange(
  449. letKeywordToken.range,
  450. "const",
  451. );
  452. }
  453. : null,
  454. });
  455. });
  456. }
  457. }
  458. return {
  459. "Program:exit"() {
  460. groupByDestructuring(variables, ignoreReadBeforeAssign).forEach(
  461. checkGroup,
  462. );
  463. },
  464. VariableDeclaration(node) {
  465. if (node.kind === "let" && !isInitOfForStatement(node)) {
  466. variables.push(...sourceCode.getDeclaredVariables(node));
  467. }
  468. },
  469. };
  470. },
  471. };