no-useless-assignment.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  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. const readReferences = targetAssignment.variable.references.filter(reference => reference.isRead());
  269. if (!readReferences.length) {
  270. /*
  271. * It is not just an unnecessary assignment, but an unnecessary (unused) variable
  272. * and thus should not be reported by this rule because it is reported by `no-unused-vars`.
  273. */
  274. return;
  275. }
  276. /**
  277. * Other assignment on the current segment and after current assignment.
  278. */
  279. const otherAssignmentAfterTargetAssignment = allAssignments
  280. .find(assignment => {
  281. if (
  282. assignment === targetAssignment ||
  283. assignment.segments.length && assignment.segments.every(segment => !targetAssignment.segments.includes(segment))
  284. ) {
  285. return false;
  286. }
  287. if (isIdentifierEvaluatedAfterAssignment(targetAssignment, assignment.identifier)) {
  288. return true;
  289. }
  290. if (
  291. assignment.expression &&
  292. assignment.expression.range[0] <= targetAssignment.identifier.range[0] &&
  293. targetAssignment.identifier.range[1] <= assignment.expression.range[1]
  294. ) {
  295. /*
  296. * The target assignment is in an expression that is evaluated before the assignment.
  297. * e.g. x=(x=1);
  298. * ^^^ targetAssignment
  299. * ^^^^^^^ assignment
  300. */
  301. return true;
  302. }
  303. return false;
  304. });
  305. for (const reference of readReferences) {
  306. /*
  307. * If the scope of the reference is outside the current code path scope,
  308. * we cannot track whether this assignment is not used.
  309. * For example, it can also be called asynchronously.
  310. */
  311. if (target.scope !== getCodePathStartScope(reference.from)) {
  312. return;
  313. }
  314. // Checks if it is used in the same segment as the target assignment.
  315. if (
  316. isIdentifierEvaluatedAfterAssignment(targetAssignment, reference.identifier) &&
  317. (
  318. isIdentifierUsedBetweenAssignedAndEqualSign(targetAssignment, reference.identifier) ||
  319. targetAssignment.segments.some(segment => isIdentifierUsedInSegment(segment, reference.identifier))
  320. )
  321. ) {
  322. if (
  323. otherAssignmentAfterTargetAssignment &&
  324. isIdentifierEvaluatedAfterAssignment(otherAssignmentAfterTargetAssignment, reference.identifier)
  325. ) {
  326. // There was another assignment before the reference. Therefore, it has not been used yet.
  327. continue;
  328. }
  329. // Uses in statements after the written identifier.
  330. return;
  331. }
  332. if (otherAssignmentAfterTargetAssignment) {
  333. /*
  334. * The assignment was followed by another assignment in the same segment.
  335. * Therefore, there is no need to check the next segment.
  336. */
  337. continue;
  338. }
  339. // Check subsequent segments.
  340. for (const subsequentSegment of getSubsequentSegments()) {
  341. if (isIdentifierUsedInSegment(subsequentSegment.segment, reference.identifier)) {
  342. if (
  343. subsequentSegment.assignment &&
  344. isIdentifierEvaluatedAfterAssignment(subsequentSegment.assignment, reference.identifier)
  345. ) {
  346. // There was another assignment before the reference. Therefore, it has not been used yet.
  347. continue;
  348. }
  349. // It is used
  350. return;
  351. }
  352. }
  353. }
  354. context.report({
  355. node: targetAssignment.identifier,
  356. messageId: "unnecessaryAssignment"
  357. });
  358. }
  359. // Verify that each assignment in the code path is used.
  360. for (const assignments of target.assignments.values()) {
  361. assignments.sort((a, b) => a.identifier.range[0] - b.identifier.range[0]);
  362. for (const assignment of assignments) {
  363. verifyAssignmentIsUsed(assignment, assignments);
  364. }
  365. }
  366. }
  367. return {
  368. onCodePathStart(codePath, node) {
  369. const scope = sourceCode.getScope(node);
  370. scopeStack = {
  371. upper: scopeStack,
  372. codePath,
  373. scope,
  374. segments: Object.create(null),
  375. currentSegments: new Set(),
  376. assignments: new Map()
  377. };
  378. codePathStartScopes.add(scopeStack.scope);
  379. },
  380. onCodePathEnd() {
  381. verify(scopeStack);
  382. scopeStack = scopeStack.upper;
  383. },
  384. onCodePathSegmentStart(segment) {
  385. const segmentInfo = { segment, first: null, last: null };
  386. scopeStack.segments[segment.id] = segmentInfo;
  387. scopeStack.currentSegments.add(segment);
  388. },
  389. onCodePathSegmentEnd(segment) {
  390. scopeStack.currentSegments.delete(segment);
  391. },
  392. Identifier(node) {
  393. for (const segment of scopeStack.currentSegments) {
  394. const segmentInfo = scopeStack.segments[segment.id];
  395. if (!segmentInfo.first) {
  396. segmentInfo.first = node;
  397. }
  398. segmentInfo.last = node;
  399. }
  400. },
  401. ":matches(VariableDeclarator[init!=null], AssignmentExpression, UpdateExpression):exit"(node) {
  402. if (scopeStack.currentSegments.size === 0) {
  403. // Ignore unreachable segments
  404. return;
  405. }
  406. const assignments = scopeStack.assignments;
  407. let pattern;
  408. let expression = null;
  409. if (node.type === "VariableDeclarator") {
  410. pattern = node.id;
  411. expression = node.init;
  412. } else if (node.type === "AssignmentExpression") {
  413. pattern = node.left;
  414. expression = node.right;
  415. } else { // UpdateExpression
  416. pattern = node.argument;
  417. }
  418. for (const identifier of extractIdentifiersFromPattern(pattern)) {
  419. const scope = sourceCode.getScope(identifier);
  420. /** @type {Variable} */
  421. const variable = findVariable(scope, identifier);
  422. if (!variable) {
  423. continue;
  424. }
  425. // We don't know where global variables are used.
  426. if (variable.scope.type === "global" && variable.defs.length === 0) {
  427. continue;
  428. }
  429. /*
  430. * If the scope of the variable is outside the current code path scope,
  431. * we cannot track whether this assignment is not used.
  432. */
  433. if (scopeStack.scope !== getCodePathStartScope(variable.scope)) {
  434. continue;
  435. }
  436. // Variables marked by `markVariableAsUsed()` or
  437. // exported by "exported" block comment.
  438. if (variable.eslintUsed) {
  439. continue;
  440. }
  441. // Variables exported by ESM export syntax
  442. if (variable.scope.type === "module") {
  443. if (
  444. variable.defs
  445. .some(def => (
  446. (def.type === "Variable" && def.parent.parent.type === "ExportNamedDeclaration") ||
  447. (
  448. def.type === "FunctionName" &&
  449. (
  450. def.node.parent.type === "ExportNamedDeclaration" ||
  451. def.node.parent.type === "ExportDefaultDeclaration"
  452. )
  453. ) ||
  454. (
  455. def.type === "ClassName" &&
  456. (
  457. def.node.parent.type === "ExportNamedDeclaration" ||
  458. def.node.parent.type === "ExportDefaultDeclaration"
  459. )
  460. )
  461. ))
  462. ) {
  463. continue;
  464. }
  465. if (variable.references.some(reference => reference.identifier.parent.type === "ExportSpecifier")) {
  466. // It have `export { ... }` reference.
  467. continue;
  468. }
  469. }
  470. let list = assignments.get(variable);
  471. if (!list) {
  472. list = [];
  473. assignments.set(variable, list);
  474. }
  475. list.push({
  476. variable,
  477. identifier,
  478. node,
  479. expression,
  480. segments: [...scopeStack.currentSegments]
  481. });
  482. }
  483. }
  484. };
  485. }
  486. };