| /** |
| * @fileoverview A rule to disallow unnecessary assignments`. |
| * @author Yosuke Ota |
| */ |
| |
| "use strict"; |
| |
| const { findVariable } = require("@eslint-community/eslint-utils"); |
| |
| //------------------------------------------------------------------------------ |
| // Types |
| //------------------------------------------------------------------------------ |
| |
| /** @typedef {import("estree").Node} ASTNode */ |
| /** @typedef {import("estree").Pattern} Pattern */ |
| /** @typedef {import("estree").Identifier} Identifier */ |
| /** @typedef {import("estree").VariableDeclarator} VariableDeclarator */ |
| /** @typedef {import("estree").AssignmentExpression} AssignmentExpression */ |
| /** @typedef {import("estree").UpdateExpression} UpdateExpression */ |
| /** @typedef {import("estree").Expression} Expression */ |
| /** @typedef {import("eslint-scope").Scope} Scope */ |
| /** @typedef {import("eslint-scope").Variable} Variable */ |
| /** @typedef {import("../linter/code-path-analysis/code-path")} CodePath */ |
| /** @typedef {import("../linter/code-path-analysis/code-path-segment")} CodePathSegment */ |
| |
| //------------------------------------------------------------------------------ |
| // Helpers |
| //------------------------------------------------------------------------------ |
| |
| /** |
| * Extract identifier from the given pattern node used on the left-hand side of the assignment. |
| * @param {Pattern} pattern The pattern node to extract identifier |
| * @returns {Iterable<Identifier>} The extracted identifier |
| */ |
| function* extractIdentifiersFromPattern(pattern) { |
| switch (pattern.type) { |
| case "Identifier": |
| yield pattern; |
| return; |
| case "ObjectPattern": |
| for (const property of pattern.properties) { |
| yield* extractIdentifiersFromPattern( |
| property.type === "Property" ? property.value : property, |
| ); |
| } |
| return; |
| case "ArrayPattern": |
| for (const element of pattern.elements) { |
| if (!element) { |
| continue; |
| } |
| yield* extractIdentifiersFromPattern(element); |
| } |
| return; |
| case "RestElement": |
| yield* extractIdentifiersFromPattern(pattern.argument); |
| return; |
| case "AssignmentPattern": |
| yield* extractIdentifiersFromPattern(pattern.left); |
| |
| // no default |
| } |
| } |
| |
| /** |
| * Checks whether the given identifier node is evaluated after the assignment identifier. |
| * @param {AssignmentInfo} assignment The assignment info. |
| * @param {Identifier} identifier The identifier to check. |
| * @returns {boolean} `true` if the given identifier node is evaluated after the assignment identifier. |
| */ |
| function isIdentifierEvaluatedAfterAssignment(assignment, identifier) { |
| if (identifier.range[0] < assignment.identifier.range[1]) { |
| return false; |
| } |
| if ( |
| assignment.expression && |
| assignment.expression.range[0] <= identifier.range[0] && |
| identifier.range[1] <= assignment.expression.range[1] |
| ) { |
| /* |
| * The identifier node is in an expression that is evaluated before the assignment. |
| * e.g. x = id; |
| * ^^ identifier to check |
| * ^ assignment identifier |
| */ |
| return false; |
| } |
| |
| /* |
| * e.g. |
| * x = 42; id; |
| * ^^ identifier to check |
| * ^ assignment identifier |
| * let { x, y = id } = obj; |
| * ^^ identifier to check |
| * ^ assignment identifier |
| */ |
| return true; |
| } |
| |
| /** |
| * Checks whether the given identifier node is used between the assigned identifier and the equal sign. |
| * |
| * e.g. let { x, y = x } = obj; |
| * ^ identifier to check |
| * ^ assigned identifier |
| * @param {AssignmentInfo} assignment The assignment info. |
| * @param {Identifier} identifier The identifier to check. |
| * @returns {boolean} `true` if the given identifier node is used between the assigned identifier and the equal sign. |
| */ |
| function isIdentifierUsedBetweenAssignedAndEqualSign(assignment, identifier) { |
| if (!assignment.expression) { |
| return false; |
| } |
| return ( |
| assignment.identifier.range[1] <= identifier.range[0] && |
| identifier.range[1] <= assignment.expression.range[0] |
| ); |
| } |
| |
| //------------------------------------------------------------------------------ |
| // Rule Definition |
| //------------------------------------------------------------------------------ |
| |
| /** @type {import('../types').Rule.RuleModule} */ |
| module.exports = { |
| meta: { |
| type: "problem", |
| |
| docs: { |
| description: |
| "Disallow variable assignments when the value is not used", |
| recommended: false, |
| url: "https://eslint.org/docs/latest/rules/no-useless-assignment", |
| }, |
| |
| schema: [], |
| |
| messages: { |
| unnecessaryAssignment: |
| "This assigned value is not used in subsequent statements.", |
| }, |
| }, |
| |
| create(context) { |
| const sourceCode = context.sourceCode; |
| |
| /** |
| * @typedef {Object} ScopeStack |
| * @property {CodePath} codePath The code path of this scope stack. |
| * @property {Scope} scope The scope of this scope stack. |
| * @property {ScopeStack} upper The upper scope stack. |
| * @property {Record<string, ScopeStackSegmentInfo>} segments The map of ScopeStackSegmentInfo. |
| * @property {Set<CodePathSegment>} currentSegments The current CodePathSegments. |
| * @property {Map<Variable, AssignmentInfo[]>} assignments The map of list of AssignmentInfo for each variable. |
| * @property {Array} tryStatementBlocks The array of TryStatement block nodes in this scope stack. |
| */ |
| /** |
| * @typedef {Object} ScopeStackSegmentInfo |
| * @property {CodePathSegment} segment The code path segment. |
| * @property {Identifier|null} first The first identifier that appears within the segment. |
| * @property {Identifier|null} last The last identifier that appears within the segment. |
| * `first` and `last` are used to determine whether an identifier exists within the segment position range. |
| * Since it is used as a range of segments, we should originally hold all nodes, not just identifiers, |
| * but since the only nodes to be judged are identifiers, it is sufficient to have a range of identifiers. |
| */ |
| /** |
| * @typedef {Object} AssignmentInfo |
| * @property {Variable} variable The variable that is assigned. |
| * @property {Identifier} identifier The identifier that is assigned. |
| * @property {VariableDeclarator|AssignmentExpression|UpdateExpression} node The node where the variable was updated. |
| * @property {Expression|null} expression The expression that is evaluated before the assignment. |
| * @property {CodePathSegment[]} segments The code path segments where the assignment was made. |
| */ |
| |
| /** @type {ScopeStack} */ |
| let scopeStack = null; |
| |
| /** @type {Set<Scope>} */ |
| const codePathStartScopes = new Set(); |
| |
| /** |
| * Gets the scope of code path start from given scope |
| * @param {Scope} scope The initial scope |
| * @returns {Scope} The scope of code path start |
| * @throws {Error} Unexpected error |
| */ |
| function getCodePathStartScope(scope) { |
| let target = scope; |
| |
| while (target) { |
| if (codePathStartScopes.has(target)) { |
| return target; |
| } |
| target = target.upper; |
| } |
| |
| // Should be unreachable |
| return null; |
| } |
| |
| /** |
| * Verify the given scope stack. |
| * @param {ScopeStack} target The scope stack to verify. |
| * @returns {void} |
| */ |
| function verify(target) { |
| /** |
| * Checks whether the given identifier is used in the segment. |
| * @param {CodePathSegment} segment The code path segment. |
| * @param {Identifier} identifier The identifier to check. |
| * @returns {boolean} `true` if the identifier is used in the segment. |
| */ |
| function isIdentifierUsedInSegment(segment, identifier) { |
| const segmentInfo = target.segments[segment.id]; |
| |
| return ( |
| segmentInfo.first && |
| segmentInfo.last && |
| segmentInfo.first.range[0] <= identifier.range[0] && |
| identifier.range[1] <= segmentInfo.last.range[1] |
| ); |
| } |
| |
| /** |
| * Verifies whether the given assignment info is an used assignment. |
| * Report if it is an unused assignment. |
| * @param {AssignmentInfo} targetAssignment The assignment info to verify. |
| * @param {AssignmentInfo[]} allAssignments The list of all assignment info for variables. |
| * @returns {void} |
| */ |
| function verifyAssignmentIsUsed(targetAssignment, allAssignments) { |
| // Skip assignment if it is in a try block. |
| const isAssignmentInTryBlock = target.tryStatementBlocks.some( |
| tryBlock => |
| tryBlock.range[0] <= |
| targetAssignment.identifier.range[0] && |
| targetAssignment.identifier.range[1] <= |
| tryBlock.range[1], |
| ); |
| |
| if (isAssignmentInTryBlock) { |
| return; |
| } |
| |
| /** |
| * @typedef {Object} SubsequentSegmentData |
| * @property {CodePathSegment} segment The code path segment |
| * @property {AssignmentInfo} [assignment] The first occurrence of the assignment within the segment. |
| * There is no need to check if the variable is used after this assignment, |
| * as the value it was assigned will be used. |
| */ |
| |
| /** |
| * Information used in `getSubsequentSegments()`. |
| * To avoid unnecessary iterations, cache information that has already been iterated over, |
| * and if additional iterations are needed, start iterating from the retained position. |
| */ |
| const subsequentSegmentData = { |
| /** |
| * Cache of subsequent segment information list that have already been iterated. |
| * @type {SubsequentSegmentData[]} |
| */ |
| results: [], |
| |
| /** |
| * Subsequent segments that have already been iterated on. Used to avoid infinite loops. |
| * @type {Set<CodePathSegment>} |
| */ |
| subsequentSegments: new Set(), |
| |
| /** |
| * Unexplored code path segment. |
| * If additional iterations are needed, consume this information and iterate. |
| * @type {CodePathSegment[]} |
| */ |
| queueSegments: targetAssignment.segments.flatMap( |
| segment => segment.nextSegments, |
| ), |
| }; |
| |
| /** |
| * Gets the subsequent segments from the segment of |
| * the assignment currently being validated (targetAssignment). |
| * @returns {Iterable<SubsequentSegmentData>} the subsequent segments |
| */ |
| function* getSubsequentSegments() { |
| yield* subsequentSegmentData.results; |
| |
| while (subsequentSegmentData.queueSegments.length > 0) { |
| const nextSegment = |
| subsequentSegmentData.queueSegments.shift(); |
| |
| if ( |
| subsequentSegmentData.subsequentSegments.has( |
| nextSegment, |
| ) |
| ) { |
| continue; |
| } |
| subsequentSegmentData.subsequentSegments.add( |
| nextSegment, |
| ); |
| |
| const assignmentInSegment = allAssignments.find( |
| otherAssignment => |
| otherAssignment.segments.includes( |
| nextSegment, |
| ) && |
| !isIdentifierUsedBetweenAssignedAndEqualSign( |
| otherAssignment, |
| targetAssignment.identifier, |
| ), |
| ); |
| |
| if (!assignmentInSegment) { |
| /* |
| * Stores the next segment to explore. |
| * If `assignmentInSegment` exists, |
| * we are guarding it because we don't need to explore the next segment. |
| */ |
| subsequentSegmentData.queueSegments.push( |
| ...nextSegment.nextSegments, |
| ); |
| } |
| |
| /** @type {SubsequentSegmentData} */ |
| const result = { |
| segment: nextSegment, |
| assignment: assignmentInSegment, |
| }; |
| |
| subsequentSegmentData.results.push(result); |
| yield result; |
| } |
| } |
| |
| if ( |
| targetAssignment.variable.references.some( |
| ref => ref.identifier.type !== "Identifier", |
| ) |
| ) { |
| /** |
| * Skip checking for a variable that has at least one non-identifier reference. |
| * It's generated by plugins and cannot be handled reliably in the core rule. |
| */ |
| return; |
| } |
| |
| const readReferences = |
| targetAssignment.variable.references.filter(reference => |
| reference.isRead(), |
| ); |
| |
| if (!readReferences.length) { |
| /* |
| * It is not just an unnecessary assignment, but an unnecessary (unused) variable |
| * and thus should not be reported by this rule because it is reported by `no-unused-vars`. |
| */ |
| return; |
| } |
| |
| /** |
| * Other assignment on the current segment and after current assignment. |
| */ |
| const otherAssignmentAfterTargetAssignment = |
| allAssignments.find(assignment => { |
| if ( |
| assignment === targetAssignment || |
| (assignment.segments.length && |
| assignment.segments.every( |
| segment => |
| !targetAssignment.segments.includes( |
| segment, |
| ), |
| )) |
| ) { |
| return false; |
| } |
| if ( |
| isIdentifierEvaluatedAfterAssignment( |
| targetAssignment, |
| assignment.identifier, |
| ) |
| ) { |
| return true; |
| } |
| if ( |
| assignment.expression && |
| assignment.expression.range[0] <= |
| targetAssignment.identifier.range[0] && |
| targetAssignment.identifier.range[1] <= |
| assignment.expression.range[1] |
| ) { |
| /* |
| * The target assignment is in an expression that is evaluated before the assignment. |
| * e.g. x=(x=1); |
| * ^^^ targetAssignment |
| * ^^^^^^^ assignment |
| */ |
| return true; |
| } |
| |
| return false; |
| }); |
| |
| for (const reference of readReferences) { |
| /* |
| * If the scope of the reference is outside the current code path scope, |
| * we cannot track whether this assignment is not used. |
| * For example, it can also be called asynchronously. |
| */ |
| if ( |
| target.scope !== getCodePathStartScope(reference.from) |
| ) { |
| return; |
| } |
| |
| // Checks if it is used in the same segment as the target assignment. |
| if ( |
| isIdentifierEvaluatedAfterAssignment( |
| targetAssignment, |
| reference.identifier, |
| ) && |
| (isIdentifierUsedBetweenAssignedAndEqualSign( |
| targetAssignment, |
| reference.identifier, |
| ) || |
| targetAssignment.segments.some(segment => |
| isIdentifierUsedInSegment( |
| segment, |
| reference.identifier, |
| ), |
| )) |
| ) { |
| if ( |
| otherAssignmentAfterTargetAssignment && |
| isIdentifierEvaluatedAfterAssignment( |
| otherAssignmentAfterTargetAssignment, |
| reference.identifier, |
| ) |
| ) { |
| // There was another assignment before the reference. Therefore, it has not been used yet. |
| continue; |
| } |
| |
| // Uses in statements after the written identifier. |
| return; |
| } |
| |
| if (otherAssignmentAfterTargetAssignment) { |
| /* |
| * The assignment was followed by another assignment in the same segment. |
| * Therefore, there is no need to check the next segment. |
| */ |
| continue; |
| } |
| |
| // Check subsequent segments. |
| for (const subsequentSegment of getSubsequentSegments()) { |
| if ( |
| isIdentifierUsedInSegment( |
| subsequentSegment.segment, |
| reference.identifier, |
| ) |
| ) { |
| if ( |
| subsequentSegment.assignment && |
| isIdentifierEvaluatedAfterAssignment( |
| subsequentSegment.assignment, |
| reference.identifier, |
| ) |
| ) { |
| // There was another assignment before the reference. Therefore, it has not been used yet. |
| continue; |
| } |
| |
| // It is used |
| return; |
| } |
| } |
| } |
| context.report({ |
| node: targetAssignment.identifier, |
| messageId: "unnecessaryAssignment", |
| }); |
| } |
| |
| // Verify that each assignment in the code path is used. |
| for (const assignments of target.assignments.values()) { |
| assignments.sort( |
| (a, b) => a.identifier.range[0] - b.identifier.range[0], |
| ); |
| for (const assignment of assignments) { |
| verifyAssignmentIsUsed(assignment, assignments); |
| } |
| } |
| } |
| |
| return { |
| onCodePathStart(codePath, node) { |
| const scope = sourceCode.getScope(node); |
| |
| scopeStack = { |
| upper: scopeStack, |
| codePath, |
| scope, |
| segments: Object.create(null), |
| currentSegments: new Set(), |
| assignments: new Map(), |
| tryStatementBlocks: [], |
| }; |
| codePathStartScopes.add(scopeStack.scope); |
| }, |
| onCodePathEnd() { |
| verify(scopeStack); |
| |
| scopeStack = scopeStack.upper; |
| }, |
| onCodePathSegmentStart(segment) { |
| const segmentInfo = { segment, first: null, last: null }; |
| |
| scopeStack.segments[segment.id] = segmentInfo; |
| scopeStack.currentSegments.add(segment); |
| }, |
| onCodePathSegmentEnd(segment) { |
| scopeStack.currentSegments.delete(segment); |
| }, |
| TryStatement(node) { |
| scopeStack.tryStatementBlocks.push(node.block); |
| }, |
| Identifier(node) { |
| for (const segment of scopeStack.currentSegments) { |
| const segmentInfo = scopeStack.segments[segment.id]; |
| |
| if (!segmentInfo.first) { |
| segmentInfo.first = node; |
| } |
| segmentInfo.last = node; |
| } |
| }, |
| ":matches(VariableDeclarator[init!=null], AssignmentExpression, UpdateExpression):exit"( |
| node, |
| ) { |
| if (scopeStack.currentSegments.size === 0) { |
| // Ignore unreachable segments |
| return; |
| } |
| |
| const assignments = scopeStack.assignments; |
| |
| let pattern; |
| let expression = null; |
| |
| if (node.type === "VariableDeclarator") { |
| pattern = node.id; |
| expression = node.init; |
| } else if (node.type === "AssignmentExpression") { |
| pattern = node.left; |
| expression = node.right; |
| } else { |
| // UpdateExpression |
| pattern = node.argument; |
| } |
| |
| for (const identifier of extractIdentifiersFromPattern( |
| pattern, |
| )) { |
| const scope = sourceCode.getScope(identifier); |
| |
| /** @type {Variable} */ |
| const variable = findVariable(scope, identifier); |
| |
| if (!variable) { |
| continue; |
| } |
| |
| // We don't know where global variables are used. |
| if ( |
| variable.scope.type === "global" && |
| variable.defs.length === 0 |
| ) { |
| continue; |
| } |
| |
| /* |
| * If the scope of the variable is outside the current code path scope, |
| * we cannot track whether this assignment is not used. |
| */ |
| if ( |
| scopeStack.scope !== |
| getCodePathStartScope(variable.scope) |
| ) { |
| continue; |
| } |
| |
| // Variables marked by `markVariableAsUsed()` or |
| // exported by "exported" block comment. |
| if (variable.eslintUsed) { |
| continue; |
| } |
| |
| // Variables exported by ESM export syntax |
| if (variable.scope.type === "module") { |
| if ( |
| variable.defs.some( |
| def => |
| (def.type === "Variable" && |
| def.parent.parent.type === |
| "ExportNamedDeclaration") || |
| (def.type === "FunctionName" && |
| (def.node.parent.type === |
| "ExportNamedDeclaration" || |
| def.node.parent.type === |
| "ExportDefaultDeclaration")) || |
| (def.type === "ClassName" && |
| (def.node.parent.type === |
| "ExportNamedDeclaration" || |
| def.node.parent.type === |
| "ExportDefaultDeclaration")), |
| ) |
| ) { |
| continue; |
| } |
| if ( |
| variable.references.some( |
| reference => |
| reference.identifier.parent.type === |
| "ExportSpecifier", |
| ) |
| ) { |
| // It have `export { ... }` reference. |
| continue; |
| } |
| } |
| |
| let list = assignments.get(variable); |
| |
| if (!list) { |
| list = []; |
| assignments.set(variable, list); |
| } |
| list.push({ |
| variable, |
| identifier, |
| node, |
| expression, |
| segments: [...scopeStack.currentSegments], |
| }); |
| } |
| }, |
| }; |
| }, |
| }; |