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.

476 lines
17 KiB

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