prefer-const.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  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. // Helpers
  8. //------------------------------------------------------------------------------
  9. const PATTERN_TYPE = /^(?:.+?Pattern|RestElement|SpreadProperty|ExperimentalRestProperty|Property)$/;
  10. const DECLARATION_HOST_TYPE = /^(?:Program|BlockStatement|SwitchCase)$/;
  11. const DESTRUCTURING_HOST_TYPE = /^(?:VariableDeclarator|AssignmentExpression)$/;
  12. /**
  13. * Adds multiple items to the tail of an array.
  14. *
  15. * @param {any[]} array - A destination to add.
  16. * @param {any[]} values - Items to be added.
  17. * @returns {void}
  18. */
  19. const pushAll = Function.apply.bind(Array.prototype.push);
  20. /**
  21. * Checks whether a given node is located at `ForStatement.init` or not.
  22. *
  23. * @param {ASTNode} node - A node to check.
  24. * @returns {boolean} `true` if the node is located at `ForStatement.init`.
  25. */
  26. function isInitOfForStatement(node) {
  27. return node.parent.type === "ForStatement" && node.parent.init === node;
  28. }
  29. /**
  30. * Checks whether a given Identifier node becomes a VariableDeclaration or not.
  31. *
  32. * @param {ASTNode} identifier - An Identifier node to check.
  33. * @returns {boolean} `true` if the node can become a VariableDeclaration.
  34. */
  35. function canBecomeVariableDeclaration(identifier) {
  36. let node = identifier.parent;
  37. while (PATTERN_TYPE.test(node.type)) {
  38. node = node.parent;
  39. }
  40. return (
  41. node.type === "VariableDeclarator" ||
  42. (
  43. node.type === "AssignmentExpression" &&
  44. node.parent.type === "ExpressionStatement" &&
  45. DECLARATION_HOST_TYPE.test(node.parent.parent.type)
  46. )
  47. );
  48. }
  49. /**
  50. * Gets an identifier node of a given variable.
  51. *
  52. * If the initialization exists or one or more reading references exist before
  53. * the first assignment, the identifier node is the node of the declaration.
  54. * Otherwise, the identifier node is the node of the first assignment.
  55. *
  56. * If the variable should not change to const, this function returns null.
  57. * - If the variable is reassigned.
  58. * - If the variable is never initialized nor assigned.
  59. * - If the variable is initialized in a different scope from the declaration.
  60. * - If the unique assignment of the variable cannot change to a declaration.
  61. * e.g. `if (a) b = 1` / `return (b = 1)`
  62. * - If the variable is declared in the global scope and `eslintUsed` is `true`.
  63. * `/*exported foo` directive comment makes such variables. This rule does not
  64. * warn such variables because this rule cannot distinguish whether the
  65. * exported variables are reassigned or not.
  66. *
  67. * @param {eslint-scope.Variable} variable - A variable to get.
  68. * @param {boolean} ignoreReadBeforeAssign -
  69. * The value of `ignoreReadBeforeAssign` option.
  70. * @returns {ASTNode|null}
  71. * An Identifier node if the variable should change to const.
  72. * Otherwise, null.
  73. */
  74. function getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign) {
  75. if (variable.eslintUsed && variable.scope.type === "global") {
  76. return null;
  77. }
  78. // Finds the unique WriteReference.
  79. let writer = null;
  80. let isReadBeforeInit = false;
  81. const references = variable.references;
  82. for (let i = 0; i < references.length; ++i) {
  83. const reference = references[i];
  84. if (reference.isWrite()) {
  85. const isReassigned = (
  86. writer !== null &&
  87. writer.identifier !== reference.identifier
  88. );
  89. if (isReassigned) {
  90. return null;
  91. }
  92. writer = reference;
  93. } else if (reference.isRead() && writer === null) {
  94. if (ignoreReadBeforeAssign) {
  95. return null;
  96. }
  97. isReadBeforeInit = true;
  98. }
  99. }
  100. /*
  101. * If the assignment is from a different scope, ignore it.
  102. * If the assignment cannot change to a declaration, ignore it.
  103. */
  104. const shouldBeConst = (
  105. writer !== null &&
  106. writer.from === variable.scope &&
  107. canBecomeVariableDeclaration(writer.identifier)
  108. );
  109. if (!shouldBeConst) {
  110. return null;
  111. }
  112. if (isReadBeforeInit) {
  113. return variable.defs[0].name;
  114. }
  115. return writer.identifier;
  116. }
  117. /**
  118. * Gets the VariableDeclarator/AssignmentExpression node that a given reference
  119. * belongs to.
  120. * This is used to detect a mix of reassigned and never reassigned in a
  121. * destructuring.
  122. *
  123. * @param {eslint-scope.Reference} reference - A reference to get.
  124. * @returns {ASTNode|null} A VariableDeclarator/AssignmentExpression node or
  125. * null.
  126. */
  127. function getDestructuringHost(reference) {
  128. if (!reference.isWrite()) {
  129. return null;
  130. }
  131. let node = reference.identifier.parent;
  132. while (PATTERN_TYPE.test(node.type)) {
  133. node = node.parent;
  134. }
  135. if (!DESTRUCTURING_HOST_TYPE.test(node.type)) {
  136. return null;
  137. }
  138. return node;
  139. }
  140. /**
  141. * Groups by the VariableDeclarator/AssignmentExpression node that each
  142. * reference of given variables belongs to.
  143. * This is used to detect a mix of reassigned and never reassigned in a
  144. * destructuring.
  145. *
  146. * @param {eslint-scope.Variable[]} variables - Variables to group by destructuring.
  147. * @param {boolean} ignoreReadBeforeAssign -
  148. * The value of `ignoreReadBeforeAssign` option.
  149. * @returns {Map<ASTNode, ASTNode[]>} Grouped identifier nodes.
  150. */
  151. function groupByDestructuring(variables, ignoreReadBeforeAssign) {
  152. const identifierMap = new Map();
  153. for (let i = 0; i < variables.length; ++i) {
  154. const variable = variables[i];
  155. const references = variable.references;
  156. const identifier = getIdentifierIfShouldBeConst(variable, ignoreReadBeforeAssign);
  157. let prevId = null;
  158. for (let j = 0; j < references.length; ++j) {
  159. const reference = references[j];
  160. const id = reference.identifier;
  161. /*
  162. * Avoid counting a reference twice or more for default values of
  163. * destructuring.
  164. */
  165. if (id === prevId) {
  166. continue;
  167. }
  168. prevId = id;
  169. // Add the identifier node into the destructuring group.
  170. const group = getDestructuringHost(reference);
  171. if (group) {
  172. if (identifierMap.has(group)) {
  173. identifierMap.get(group).push(identifier);
  174. } else {
  175. identifierMap.set(group, [identifier]);
  176. }
  177. }
  178. }
  179. }
  180. return identifierMap;
  181. }
  182. /**
  183. * Finds the nearest parent of node with a given type.
  184. *
  185. * @param {ASTNode} node – The node to search from.
  186. * @param {string} type – The type field of the parent node.
  187. * @param {Function} shouldStop – a predicate that returns true if the traversal should stop, and false otherwise.
  188. * @returns {ASTNode} The closest ancestor with the specified type; null if no such ancestor exists.
  189. */
  190. function findUp(node, type, shouldStop) {
  191. if (!node || shouldStop(node)) {
  192. return null;
  193. }
  194. if (node.type === type) {
  195. return node;
  196. }
  197. return findUp(node.parent, type, shouldStop);
  198. }
  199. //------------------------------------------------------------------------------
  200. // Rule Definition
  201. //------------------------------------------------------------------------------
  202. module.exports = {
  203. meta: {
  204. docs: {
  205. description: "require `const` declarations for variables that are never reassigned after declared",
  206. category: "ECMAScript 6",
  207. recommended: false,
  208. url: "https://eslint.org/docs/rules/prefer-const"
  209. },
  210. fixable: "code",
  211. schema: [
  212. {
  213. type: "object",
  214. properties: {
  215. destructuring: { enum: ["any", "all"] },
  216. ignoreReadBeforeAssign: { type: "boolean" }
  217. },
  218. additionalProperties: false
  219. }
  220. ]
  221. },
  222. create(context) {
  223. const options = context.options[0] || {};
  224. const sourceCode = context.getSourceCode();
  225. const checkingMixedDestructuring = options.destructuring !== "all";
  226. const ignoreReadBeforeAssign = options.ignoreReadBeforeAssign === true;
  227. const variables = [];
  228. /**
  229. * Reports given identifier nodes if all of the nodes should be declared
  230. * as const.
  231. *
  232. * The argument 'nodes' is an array of Identifier nodes.
  233. * This node is the result of 'getIdentifierIfShouldBeConst()', so it's
  234. * nullable. In simple declaration or assignment cases, the length of
  235. * the array is 1. In destructuring cases, the length of the array can
  236. * be 2 or more.
  237. *
  238. * @param {(eslint-scope.Reference|null)[]} nodes -
  239. * References which are grouped by destructuring to report.
  240. * @returns {void}
  241. */
  242. function checkGroup(nodes) {
  243. const nodesToReport = nodes.filter(Boolean);
  244. if (nodes.length && (checkingMixedDestructuring || nodesToReport.length === nodes.length)) {
  245. const varDeclParent = findUp(nodes[0], "VariableDeclaration", parentNode => parentNode.type.endsWith("Statement"));
  246. const shouldFix = varDeclParent &&
  247. /*
  248. * If there are multiple variable declarations, like {let a = 1, b = 2}, then
  249. * do not attempt to fix if one of the declarations should be `const`. It's
  250. * too hard to know how the developer would want to automatically resolve the issue.
  251. */
  252. varDeclParent.declarations.length === 1 &&
  253. // Don't do a fix unless the variable is initialized (or it's in a for-in or for-of loop)
  254. (varDeclParent.parent.type === "ForInStatement" || varDeclParent.parent.type === "ForOfStatement" || varDeclParent.declarations[0].init) &&
  255. /*
  256. * If options.destucturing is "all", then this warning will not occur unless
  257. * every assignment in the destructuring should be const. In that case, it's safe
  258. * to apply the fix.
  259. */
  260. nodesToReport.length === nodes.length;
  261. nodesToReport.forEach(node => {
  262. context.report({
  263. node,
  264. message: "'{{name}}' is never reassigned. Use 'const' instead.",
  265. data: node,
  266. fix: shouldFix ? fixer => fixer.replaceText(sourceCode.getFirstToken(varDeclParent), "const") : null
  267. });
  268. });
  269. }
  270. }
  271. return {
  272. "Program:exit"() {
  273. groupByDestructuring(variables, ignoreReadBeforeAssign).forEach(checkGroup);
  274. },
  275. VariableDeclaration(node) {
  276. if (node.kind === "let" && !isInitOfForStatement(node)) {
  277. pushAll(variables, context.getDeclaredVariables(node));
  278. }
  279. }
  280. };
  281. }
  282. };