prefer-const.js 18 KB

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