123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566 |
- /**
- * @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('../shared/types').Rule} */
- 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.
- */
- /**
- * @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) {
- /**
- * @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;
- }
- }
- 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()
- };
- 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);
- },
- 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]
- });
- }
- }
- };
- }
- };
|