no-useless-assignment.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. /**
  2. * @fileoverview A rule to disallow unnecessary assignments`.
  3. * @author Yosuke Ota
  4. */
  5. "use strict";
  6. const { findVariable } = require("@eslint-community/eslint-utils");
  7. //------------------------------------------------------------------------------
  8. // Types
  9. //------------------------------------------------------------------------------
  10. /** @typedef {import("estree").Node} ASTNode */
  11. /** @typedef {import("estree").Pattern} Pattern */
  12. /** @typedef {import("estree").Identifier} Identifier */
  13. /** @typedef {import("estree").VariableDeclarator} VariableDeclarator */
  14. /** @typedef {import("estree").AssignmentExpression} AssignmentExpression */
  15. /** @typedef {import("estree").UpdateExpression} UpdateExpression */
  16. /** @typedef {import("estree").Expression} Expression */
  17. /** @typedef {import("eslint-scope").Scope} Scope */
  18. /** @typedef {import("eslint-scope").Variable} Variable */
  19. /** @typedef {import("../linter/code-path-analysis/code-path")} CodePath */
  20. /** @typedef {import("../linter/code-path-analysis/code-path-segment")} CodePathSegment */
  21. //------------------------------------------------------------------------------
  22. // Helpers
  23. //------------------------------------------------------------------------------
  24. /**
  25. * Extract identifier from the given pattern node used on the left-hand side of the assignment.
  26. * @param {Pattern} pattern The pattern node to extract identifier
  27. * @returns {Iterable<Identifier>} The extracted identifier
  28. */
  29. function* extractIdentifiersFromPattern(pattern) {
  30. switch (pattern.type) {
  31. case "Identifier":
  32. yield pattern;
  33. return;
  34. case "ObjectPattern":
  35. for (const property of pattern.properties) {
  36. yield* extractIdentifiersFromPattern(
  37. property.type === "Property" ? property.value : property,
  38. );
  39. }
  40. return;
  41. case "ArrayPattern":
  42. for (const element of pattern.elements) {
  43. if (!element) {
  44. continue;
  45. }
  46. yield* extractIdentifiersFromPattern(element);
  47. }
  48. return;
  49. case "RestElement":
  50. yield* extractIdentifiersFromPattern(pattern.argument);
  51. return;
  52. case "AssignmentPattern":
  53. yield* extractIdentifiersFromPattern(pattern.left);
  54. // no default
  55. }
  56. }
  57. /**
  58. * Checks whether the given identifier node is evaluated after the assignment identifier.
  59. * @param {AssignmentInfo} assignment The assignment info.
  60. * @param {Identifier} identifier The identifier to check.
  61. * @returns {boolean} `true` if the given identifier node is evaluated after the assignment identifier.
  62. */
  63. function isIdentifierEvaluatedAfterAssignment(assignment, identifier) {
  64. if (identifier.range[0] < assignment.identifier.range[1]) {
  65. return false;
  66. }
  67. if (
  68. assignment.expression &&
  69. assignment.expression.range[0] <= identifier.range[0] &&
  70. identifier.range[1] <= assignment.expression.range[1]
  71. ) {
  72. /*
  73. * The identifier node is in an expression that is evaluated before the assignment.
  74. * e.g. x = id;
  75. * ^^ identifier to check
  76. * ^ assignment identifier
  77. */
  78. return false;
  79. }
  80. /*
  81. * e.g.
  82. * x = 42; id;
  83. * ^^ identifier to check
  84. * ^ assignment identifier
  85. * let { x, y = id } = obj;
  86. * ^^ identifier to check
  87. * ^ assignment identifier
  88. */
  89. return true;
  90. }
  91. /**
  92. * Checks whether the given identifier node is used between the assigned identifier and the equal sign.
  93. *
  94. * e.g. let { x, y = x } = obj;
  95. * ^ identifier to check
  96. * ^ assigned identifier
  97. * @param {AssignmentInfo} assignment The assignment info.
  98. * @param {Identifier} identifier The identifier to check.
  99. * @returns {boolean} `true` if the given identifier node is used between the assigned identifier and the equal sign.
  100. */
  101. function isIdentifierUsedBetweenAssignedAndEqualSign(assignment, identifier) {
  102. if (!assignment.expression) {
  103. return false;
  104. }
  105. return (
  106. assignment.identifier.range[1] <= identifier.range[0] &&
  107. identifier.range[1] <= assignment.expression.range[0]
  108. );
  109. }
  110. //------------------------------------------------------------------------------
  111. // Rule Definition
  112. //------------------------------------------------------------------------------
  113. /** @type {import('../types').Rule.RuleModule} */
  114. module.exports = {
  115. meta: {
  116. type: "problem",
  117. docs: {
  118. description:
  119. "Disallow variable assignments when the value is not used",
  120. recommended: false,
  121. url: "https://eslint.org/docs/latest/rules/no-useless-assignment",
  122. },
  123. schema: [],
  124. messages: {
  125. unnecessaryAssignment:
  126. "This assigned value is not used in subsequent statements.",
  127. },
  128. },
  129. create(context) {
  130. const sourceCode = context.sourceCode;
  131. /**
  132. * @typedef {Object} ScopeStack
  133. * @property {CodePath} codePath The code path of this scope stack.
  134. * @property {Scope} scope The scope of this scope stack.
  135. * @property {ScopeStack} upper The upper scope stack.
  136. * @property {Record<string, ScopeStackSegmentInfo>} segments The map of ScopeStackSegmentInfo.
  137. * @property {Set<CodePathSegment>} currentSegments The current CodePathSegments.
  138. * @property {Map<Variable, AssignmentInfo[]>} assignments The map of list of AssignmentInfo for each variable.
  139. * @property {Array} tryStatementBlocks The array of TryStatement block nodes in this scope stack.
  140. */
  141. /**
  142. * @typedef {Object} ScopeStackSegmentInfo
  143. * @property {CodePathSegment} segment The code path segment.
  144. * @property {Identifier|null} first The first identifier that appears within the segment.
  145. * @property {Identifier|null} last The last identifier that appears within the segment.
  146. * `first` and `last` are used to determine whether an identifier exists within the segment position range.
  147. * Since it is used as a range of segments, we should originally hold all nodes, not just identifiers,
  148. * but since the only nodes to be judged are identifiers, it is sufficient to have a range of identifiers.
  149. */
  150. /**
  151. * @typedef {Object} AssignmentInfo
  152. * @property {Variable} variable The variable that is assigned.
  153. * @property {Identifier} identifier The identifier that is assigned.
  154. * @property {VariableDeclarator|AssignmentExpression|UpdateExpression} node The node where the variable was updated.
  155. * @property {Expression|null} expression The expression that is evaluated before the assignment.
  156. * @property {CodePathSegment[]} segments The code path segments where the assignment was made.
  157. */
  158. /** @type {ScopeStack} */
  159. let scopeStack = null;
  160. /** @type {Set<Scope>} */
  161. const codePathStartScopes = new Set();
  162. /**
  163. * Gets the scope of code path start from given scope
  164. * @param {Scope} scope The initial scope
  165. * @returns {Scope} The scope of code path start
  166. * @throws {Error} Unexpected error
  167. */
  168. function getCodePathStartScope(scope) {
  169. let target = scope;
  170. while (target) {
  171. if (codePathStartScopes.has(target)) {
  172. return target;
  173. }
  174. target = target.upper;
  175. }
  176. // Should be unreachable
  177. return null;
  178. }
  179. /**
  180. * Verify the given scope stack.
  181. * @param {ScopeStack} target The scope stack to verify.
  182. * @returns {void}
  183. */
  184. function verify(target) {
  185. /**
  186. * Checks whether the given identifier is used in the segment.
  187. * @param {CodePathSegment} segment The code path segment.
  188. * @param {Identifier} identifier The identifier to check.
  189. * @returns {boolean} `true` if the identifier is used in the segment.
  190. */
  191. function isIdentifierUsedInSegment(segment, identifier) {
  192. const segmentInfo = target.segments[segment.id];
  193. return (
  194. segmentInfo.first &&
  195. segmentInfo.last &&
  196. segmentInfo.first.range[0] <= identifier.range[0] &&
  197. identifier.range[1] <= segmentInfo.last.range[1]
  198. );
  199. }
  200. /**
  201. * Verifies whether the given assignment info is an used assignment.
  202. * Report if it is an unused assignment.
  203. * @param {AssignmentInfo} targetAssignment The assignment info to verify.
  204. * @param {AssignmentInfo[]} allAssignments The list of all assignment info for variables.
  205. * @returns {void}
  206. */
  207. function verifyAssignmentIsUsed(targetAssignment, allAssignments) {
  208. // Skip assignment if it is in a try block.
  209. const isAssignmentInTryBlock = target.tryStatementBlocks.some(
  210. tryBlock =>
  211. tryBlock.range[0] <=
  212. targetAssignment.identifier.range[0] &&
  213. targetAssignment.identifier.range[1] <=
  214. tryBlock.range[1],
  215. );
  216. if (isAssignmentInTryBlock) {
  217. return;
  218. }
  219. /**
  220. * @typedef {Object} SubsequentSegmentData
  221. * @property {CodePathSegment} segment The code path segment
  222. * @property {AssignmentInfo} [assignment] The first occurrence of the assignment within the segment.
  223. * There is no need to check if the variable is used after this assignment,
  224. * as the value it was assigned will be used.
  225. */
  226. /**
  227. * Information used in `getSubsequentSegments()`.
  228. * To avoid unnecessary iterations, cache information that has already been iterated over,
  229. * and if additional iterations are needed, start iterating from the retained position.
  230. */
  231. const subsequentSegmentData = {
  232. /**
  233. * Cache of subsequent segment information list that have already been iterated.
  234. * @type {SubsequentSegmentData[]}
  235. */
  236. results: [],
  237. /**
  238. * Subsequent segments that have already been iterated on. Used to avoid infinite loops.
  239. * @type {Set<CodePathSegment>}
  240. */
  241. subsequentSegments: new Set(),
  242. /**
  243. * Unexplored code path segment.
  244. * If additional iterations are needed, consume this information and iterate.
  245. * @type {CodePathSegment[]}
  246. */
  247. queueSegments: targetAssignment.segments.flatMap(
  248. segment => segment.nextSegments,
  249. ),
  250. };
  251. /**
  252. * Gets the subsequent segments from the segment of
  253. * the assignment currently being validated (targetAssignment).
  254. * @returns {Iterable<SubsequentSegmentData>} the subsequent segments
  255. */
  256. function* getSubsequentSegments() {
  257. yield* subsequentSegmentData.results;
  258. while (subsequentSegmentData.queueSegments.length > 0) {
  259. const nextSegment =
  260. subsequentSegmentData.queueSegments.shift();
  261. if (
  262. subsequentSegmentData.subsequentSegments.has(
  263. nextSegment,
  264. )
  265. ) {
  266. continue;
  267. }
  268. subsequentSegmentData.subsequentSegments.add(
  269. nextSegment,
  270. );
  271. const assignmentInSegment = allAssignments.find(
  272. otherAssignment =>
  273. otherAssignment.segments.includes(
  274. nextSegment,
  275. ) &&
  276. !isIdentifierUsedBetweenAssignedAndEqualSign(
  277. otherAssignment,
  278. targetAssignment.identifier,
  279. ),
  280. );
  281. if (!assignmentInSegment) {
  282. /*
  283. * Stores the next segment to explore.
  284. * If `assignmentInSegment` exists,
  285. * we are guarding it because we don't need to explore the next segment.
  286. */
  287. subsequentSegmentData.queueSegments.push(
  288. ...nextSegment.nextSegments,
  289. );
  290. }
  291. /** @type {SubsequentSegmentData} */
  292. const result = {
  293. segment: nextSegment,
  294. assignment: assignmentInSegment,
  295. };
  296. subsequentSegmentData.results.push(result);
  297. yield result;
  298. }
  299. }
  300. if (
  301. targetAssignment.variable.references.some(
  302. ref => ref.identifier.type !== "Identifier",
  303. )
  304. ) {
  305. /**
  306. * Skip checking for a variable that has at least one non-identifier reference.
  307. * It's generated by plugins and cannot be handled reliably in the core rule.
  308. */
  309. return;
  310. }
  311. const readReferences =
  312. targetAssignment.variable.references.filter(reference =>
  313. reference.isRead(),
  314. );
  315. if (!readReferences.length) {
  316. /*
  317. * It is not just an unnecessary assignment, but an unnecessary (unused) variable
  318. * and thus should not be reported by this rule because it is reported by `no-unused-vars`.
  319. */
  320. return;
  321. }
  322. /**
  323. * Other assignment on the current segment and after current assignment.
  324. */
  325. const otherAssignmentAfterTargetAssignment =
  326. allAssignments.find(assignment => {
  327. if (
  328. assignment === targetAssignment ||
  329. (assignment.segments.length &&
  330. assignment.segments.every(
  331. segment =>
  332. !targetAssignment.segments.includes(
  333. segment,
  334. ),
  335. ))
  336. ) {
  337. return false;
  338. }
  339. if (
  340. isIdentifierEvaluatedAfterAssignment(
  341. targetAssignment,
  342. assignment.identifier,
  343. )
  344. ) {
  345. return true;
  346. }
  347. if (
  348. assignment.expression &&
  349. assignment.expression.range[0] <=
  350. targetAssignment.identifier.range[0] &&
  351. targetAssignment.identifier.range[1] <=
  352. assignment.expression.range[1]
  353. ) {
  354. /*
  355. * The target assignment is in an expression that is evaluated before the assignment.
  356. * e.g. x=(x=1);
  357. * ^^^ targetAssignment
  358. * ^^^^^^^ assignment
  359. */
  360. return true;
  361. }
  362. return false;
  363. });
  364. for (const reference of readReferences) {
  365. /*
  366. * If the scope of the reference is outside the current code path scope,
  367. * we cannot track whether this assignment is not used.
  368. * For example, it can also be called asynchronously.
  369. */
  370. if (
  371. target.scope !== getCodePathStartScope(reference.from)
  372. ) {
  373. return;
  374. }
  375. // Checks if it is used in the same segment as the target assignment.
  376. if (
  377. isIdentifierEvaluatedAfterAssignment(
  378. targetAssignment,
  379. reference.identifier,
  380. ) &&
  381. (isIdentifierUsedBetweenAssignedAndEqualSign(
  382. targetAssignment,
  383. reference.identifier,
  384. ) ||
  385. targetAssignment.segments.some(segment =>
  386. isIdentifierUsedInSegment(
  387. segment,
  388. reference.identifier,
  389. ),
  390. ))
  391. ) {
  392. if (
  393. otherAssignmentAfterTargetAssignment &&
  394. isIdentifierEvaluatedAfterAssignment(
  395. otherAssignmentAfterTargetAssignment,
  396. reference.identifier,
  397. )
  398. ) {
  399. // There was another assignment before the reference. Therefore, it has not been used yet.
  400. continue;
  401. }
  402. // Uses in statements after the written identifier.
  403. return;
  404. }
  405. if (otherAssignmentAfterTargetAssignment) {
  406. /*
  407. * The assignment was followed by another assignment in the same segment.
  408. * Therefore, there is no need to check the next segment.
  409. */
  410. continue;
  411. }
  412. // Check subsequent segments.
  413. for (const subsequentSegment of getSubsequentSegments()) {
  414. if (
  415. isIdentifierUsedInSegment(
  416. subsequentSegment.segment,
  417. reference.identifier,
  418. )
  419. ) {
  420. if (
  421. subsequentSegment.assignment &&
  422. isIdentifierEvaluatedAfterAssignment(
  423. subsequentSegment.assignment,
  424. reference.identifier,
  425. )
  426. ) {
  427. // There was another assignment before the reference. Therefore, it has not been used yet.
  428. continue;
  429. }
  430. // It is used
  431. return;
  432. }
  433. }
  434. }
  435. context.report({
  436. node: targetAssignment.identifier,
  437. messageId: "unnecessaryAssignment",
  438. });
  439. }
  440. // Verify that each assignment in the code path is used.
  441. for (const assignments of target.assignments.values()) {
  442. assignments.sort(
  443. (a, b) => a.identifier.range[0] - b.identifier.range[0],
  444. );
  445. for (const assignment of assignments) {
  446. verifyAssignmentIsUsed(assignment, assignments);
  447. }
  448. }
  449. }
  450. return {
  451. onCodePathStart(codePath, node) {
  452. const scope = sourceCode.getScope(node);
  453. scopeStack = {
  454. upper: scopeStack,
  455. codePath,
  456. scope,
  457. segments: Object.create(null),
  458. currentSegments: new Set(),
  459. assignments: new Map(),
  460. tryStatementBlocks: [],
  461. };
  462. codePathStartScopes.add(scopeStack.scope);
  463. },
  464. onCodePathEnd() {
  465. verify(scopeStack);
  466. scopeStack = scopeStack.upper;
  467. },
  468. onCodePathSegmentStart(segment) {
  469. const segmentInfo = { segment, first: null, last: null };
  470. scopeStack.segments[segment.id] = segmentInfo;
  471. scopeStack.currentSegments.add(segment);
  472. },
  473. onCodePathSegmentEnd(segment) {
  474. scopeStack.currentSegments.delete(segment);
  475. },
  476. TryStatement(node) {
  477. scopeStack.tryStatementBlocks.push(node.block);
  478. },
  479. Identifier(node) {
  480. for (const segment of scopeStack.currentSegments) {
  481. const segmentInfo = scopeStack.segments[segment.id];
  482. if (!segmentInfo.first) {
  483. segmentInfo.first = node;
  484. }
  485. segmentInfo.last = node;
  486. }
  487. },
  488. ":matches(VariableDeclarator[init!=null], AssignmentExpression, UpdateExpression):exit"(
  489. node,
  490. ) {
  491. if (scopeStack.currentSegments.size === 0) {
  492. // Ignore unreachable segments
  493. return;
  494. }
  495. const assignments = scopeStack.assignments;
  496. let pattern;
  497. let expression = null;
  498. if (node.type === "VariableDeclarator") {
  499. pattern = node.id;
  500. expression = node.init;
  501. } else if (node.type === "AssignmentExpression") {
  502. pattern = node.left;
  503. expression = node.right;
  504. } else {
  505. // UpdateExpression
  506. pattern = node.argument;
  507. }
  508. for (const identifier of extractIdentifiersFromPattern(
  509. pattern,
  510. )) {
  511. const scope = sourceCode.getScope(identifier);
  512. /** @type {Variable} */
  513. const variable = findVariable(scope, identifier);
  514. if (!variable) {
  515. continue;
  516. }
  517. // We don't know where global variables are used.
  518. if (
  519. variable.scope.type === "global" &&
  520. variable.defs.length === 0
  521. ) {
  522. continue;
  523. }
  524. /*
  525. * If the scope of the variable is outside the current code path scope,
  526. * we cannot track whether this assignment is not used.
  527. */
  528. if (
  529. scopeStack.scope !==
  530. getCodePathStartScope(variable.scope)
  531. ) {
  532. continue;
  533. }
  534. // Variables marked by `markVariableAsUsed()` or
  535. // exported by "exported" block comment.
  536. if (variable.eslintUsed) {
  537. continue;
  538. }
  539. // Variables exported by ESM export syntax
  540. if (variable.scope.type === "module") {
  541. if (
  542. variable.defs.some(
  543. def =>
  544. (def.type === "Variable" &&
  545. def.parent.parent.type ===
  546. "ExportNamedDeclaration") ||
  547. (def.type === "FunctionName" &&
  548. (def.node.parent.type ===
  549. "ExportNamedDeclaration" ||
  550. def.node.parent.type ===
  551. "ExportDefaultDeclaration")) ||
  552. (def.type === "ClassName" &&
  553. (def.node.parent.type ===
  554. "ExportNamedDeclaration" ||
  555. def.node.parent.type ===
  556. "ExportDefaultDeclaration")),
  557. )
  558. ) {
  559. continue;
  560. }
  561. if (
  562. variable.references.some(
  563. reference =>
  564. reference.identifier.parent.type ===
  565. "ExportSpecifier",
  566. )
  567. ) {
  568. // It have `export { ... }` reference.
  569. continue;
  570. }
  571. }
  572. let list = assignments.get(variable);
  573. if (!list) {
  574. list = [];
  575. assignments.set(variable, list);
  576. }
  577. list.push({
  578. variable,
  579. identifier,
  580. node,
  581. expression,
  582. segments: [...scopeStack.currentSegments],
  583. });
  584. }
  585. },
  586. };
  587. },
  588. };