You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

364 line
13 KiB

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