no-useless-assignment.js 23 KB

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