From b4e0503a56beea1222be266cc6b186d89410d1f2 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Tue, 9 Jan 2024 17:41:22 +0900 Subject: [PATCH] feat: add `no-useless-assignment` rule (#17625) * feat: add `no-useless-assignment` rule * fix: add test cases and fix * chore: remove wrong comment * test: add test and update * feat: report if it is not used between assignments * fix: add test case and fix * Apply suggestions from code review Co-authored-by: Nicholas C. Zakas * docs: update * fix: false negatives for same segments & refactor * fix: rule-types.json * fix: add update assignment test case & fix * fix: add test for global vars & fix * Update docs/src/rules/no-useless-assignment.md Co-authored-by: Nicholas C. Zakas * docs: update description * docs: add further_reading * chore: improve jsdoc and rename function * docs: update doc * fix: false positives for try catch * test: fix tests/conf/eslint-all.js * feat: add no-useless-assignment to eslint-config-eslint * docs: fix document * Update docs/src/rules/no-useless-assignment.md Co-authored-by: Milos Djermanovic * fix: false negatives * docs: add example code * test: move test case * fix: false positives for assignment pattern * test: use flat-rule-tester * Update lib/rules/no-useless-assignment.js Co-authored-by: Milos Djermanovic * fix: false positives for `finally` block * fix: false positives for markVariableAsUsed() * chore: rename function * fix: false positives for unreachable segments * Update lib/rules/no-useless-assignment.js Co-authored-by: Nicholas C. Zakas * test: revert tests/conf/eslint-all.js * test: use rule-tester --------- Co-authored-by: Nicholas C. Zakas Co-authored-by: Milos Djermanovic --- docs/src/_data/further_reading_links.json | 35 + docs/src/rules/no-unused-vars.md | 2 + docs/src/rules/no-useless-assignment.md | 156 +++ lib/linter/code-path-analysis/code-path.js | 6 +- lib/linter/config-comment-parser.js | 17 +- lib/linter/linter.js | 2 +- lib/rules/array-bracket-newline.js | 2 +- lib/rules/indent-legacy.js | 2 +- lib/rules/indent.js | 2 +- lib/rules/index.js | 1 + lib/rules/key-spacing.js | 2 +- lib/rules/max-len.js | 2 +- lib/rules/no-dupe-class-members.js | 2 +- lib/rules/no-empty-function.js | 4 +- lib/rules/no-loss-of-precision.js | 2 +- lib/rules/no-trailing-spaces.js | 5 +- lib/rules/no-useless-assignment.js | 566 +++++++++++ lib/rules/utils/ast-utils.js | 4 +- lib/rules/yield-star-spacing.js | 2 +- lib/source-code/token-store/index.js | 4 +- packages/eslint-config-eslint/base.js | 1 + packages/eslint-config-eslint/eslintrc.js | 1 + packages/js/src/configs/eslint-all.js | 1 + tests/lib/rules/no-useless-assignment.js | 1009 ++++++++++++++++++++ tools/rule-types.json | 1 + 25 files changed, 1800 insertions(+), 31 deletions(-) create mode 100644 docs/src/rules/no-useless-assignment.md create mode 100644 lib/rules/no-useless-assignment.js create mode 100644 tests/lib/rules/no-useless-assignment.js diff --git a/docs/src/_data/further_reading_links.json b/docs/src/_data/further_reading_links.json index a33ce6c0b3b..db29e79227f 100644 --- a/docs/src/_data/further_reading_links.json +++ b/docs/src/_data/further_reading_links.json @@ -747,5 +747,40 @@ "logo": "https://codepoints.net/favicon.ico", "title": "U+1680 OGHAM SPACE MARK:   – Unicode – Codepoints", "description": " , codepoint U+1680 OGHAM SPACE MARK in Unicode, is located in the block “Ogham”. It belongs to the Ogham script and is a Space Separator." + }, + "https://en.wikipedia.org/wiki/Dead_store": { + "domain": "en.wikipedia.org", + "url": "https://en.wikipedia.org/wiki/Dead_store", + "logo": "https://en.wikipedia.org/static/apple-touch/wikipedia.png", + "title": "Dead store - Wikipedia", + "description": null + }, + "https://rules.sonarsource.com/javascript/RSPEC-1854/": { + "domain": "rules.sonarsource.com", + "url": "https://rules.sonarsource.com/javascript/RSPEC-1854/", + "logo": "https://rules.sonarsource.com/favicon.ico", + "title": "JavaScript static code analysis: Unused assignments should be removed", + "description": "Dead stores refer to assignments made to local variables that are subsequently never used or immediately overwritten. Such assignments are\nunnecessary and don’t contribute to the functionality or clarity of the code. They may even negatively impact performance. Removing them enhances code\ncleanlines…" + }, + "https://cwe.mitre.org/data/definitions/563.html": { + "domain": "cwe.mitre.org", + "url": "https://cwe.mitre.org/data/definitions/563.html", + "logo": "https://cwe.mitre.org/favicon.ico", + "title": "CWE - CWE-563: Assignment to Variable without Use (4.13)", + "description": "Common Weakness Enumeration (CWE) is a list of software weaknesses." + }, + "https://wiki.sei.cmu.edu/confluence/display/c/MSC13-C.+Detect+and+remove+unused+values": { + "domain": "wiki.sei.cmu.edu", + "url": "https://wiki.sei.cmu.edu/confluence/display/c/MSC13-C.+Detect+and+remove+unused+values", + "logo": "https://wiki.sei.cmu.edu/confluence/s/-ctumb3/9012/tu5x00/7/_/favicon.ico", + "title": "MSC13-C. Detect and remove unused values - SEI CERT C Coding Standard - Confluence", + "description": null + }, + "https://wiki.sei.cmu.edu/confluence/display/java/MSC56-J.+Detect+and+remove+superfluous+code+and+values": { + "domain": "wiki.sei.cmu.edu", + "url": "https://wiki.sei.cmu.edu/confluence/display/java/MSC56-J.+Detect+and+remove+superfluous+code+and+values", + "logo": "https://wiki.sei.cmu.edu/confluence/s/-ctumb3/9012/tu5x00/7/_/favicon.ico", + "title": "MSC56-J. Detect and remove superfluous code and values - SEI CERT Oracle Coding Standard for Java - Confluence", + "description": null } } \ No newline at end of file diff --git a/docs/src/rules/no-unused-vars.md b/docs/src/rules/no-unused-vars.md index ba9e18797c2..bca9c06d791 100644 --- a/docs/src/rules/no-unused-vars.md +++ b/docs/src/rules/no-unused-vars.md @@ -1,6 +1,8 @@ --- title: no-unused-vars rule_type: problem +related_rules: +- no-useless-assignment --- diff --git a/docs/src/rules/no-useless-assignment.md b/docs/src/rules/no-useless-assignment.md new file mode 100644 index 00000000000..0faf447a5df --- /dev/null +++ b/docs/src/rules/no-useless-assignment.md @@ -0,0 +1,156 @@ +--- +title: no-useless-assignment +rule_type: suggestion +related_rules: +- no-unused-vars +further_reading: +- https://en.wikipedia.org/wiki/Dead_store +- https://rules.sonarsource.com/javascript/RSPEC-1854/ +- https://cwe.mitre.org/data/definitions/563.html +- https://wiki.sei.cmu.edu/confluence/display/c/MSC13-C.+Detect+and+remove+unused+values +- https://wiki.sei.cmu.edu/confluence/display/java/MSC56-J.+Detect+and+remove+superfluous+code+and+values +--- + + +[Wikipedia describes a "dead store"](https://en.wikipedia.org/wiki/Dead_store) as follows: + +> In computer programming, a local variable that is assigned a value but is not read by any subsequent instruction is referred to as a **dead store**. + +"Dead stores" waste processing and memory, so it is better to remove unnecessary assignments to variables. + +Also, if the author intended the variable to be used, there is likely a mistake around the dead store. +For example, + +* you should have used a stored value but forgot to do so. +* you made a mistake in the name of the variable to be stored. + +```js +let id = "x1234"; // this is a "dead store" - this value ("x1234") is never read + +id = generateId(); + +doSomethingWith(id); +``` + +## Rule Details + +This rule aims to report variable assignments when the value is not used. + +Examples of **incorrect** code for this rule: + +::: incorrect + +```js +/* eslint no-useless-assignment: "error" */ + +function fn1() { + let v = 'used'; + doSomething(v); + v = 'unused'; +} + +function fn2() { + let v = 'used'; + if (condition) { + v = 'unused'; + return + } + doSomething(v); +} + +function fn3() { + let v = 'used'; + if (condition) { + doSomething(v); + } else { + v = 'unused'; + } +} + +function fn4() { + let v = 'unused'; + if (condition) { + v = 'used'; + doSomething(v); + return + } +} + +function fn5() { + let v = 'used'; + if (condition) { + let v = 'used'; + console.log(v); + v = 'unused'; + } + console.log(v); +} +``` + +::: + +Examples of **correct** code for this rule: + +::: correct + +```js +/* eslint no-useless-assignment: "error" */ + +function fn1() { + let v = 'used'; + doSomething(v); + v = 'used-2'; + doSomething(v); +} + +function fn2() { + let v = 'used'; + if (condition) { + v = 'used-2'; + doSomething(v); + return + } + doSomething(v); +} + +function fn3() { + let v = 'used'; + if (condition) { + doSomething(v); + } else { + v = 'used-2'; + doSomething(v); + } +} + +function fn4() { + let v = 'used'; + for (let i = 0; i < 10; i++) { + doSomething(v); + v = 'used in next iteration'; + } +} +``` + +::: + +This rule will not report variables that are never read. +Because it's clearly an unused variable. If you want it reported, please enable the [no-unused-vars](./no-unused-vars) rule. + +::: correct + +```js +/* eslint no-useless-assignment: "error" */ + +function fn() { + let v = 'unused'; + v = 'unused-2' + doSomething(); +} +``` + +::: + +## When Not To Use It + +If you don't want to be notified about values that are never read, you can safely disable this rule. diff --git a/lib/linter/code-path-analysis/code-path.js b/lib/linter/code-path-analysis/code-path.js index dd922a2b609..32b814c16ba 100644 --- a/lib/linter/code-path-analysis/code-path.js +++ b/lib/linter/code-path-analysis/code-path.js @@ -166,9 +166,9 @@ class CodePath { const lastSegment = resolvedOptions.last; // set up initial location information - let record = null; - let index = 0; - let end = 0; + let record; + let index; + let end; let segment = null; // segments that have already been visited during traversal diff --git a/lib/linter/config-comment-parser.js b/lib/linter/config-comment-parser.js index e20c678d8db..aaa56e1daa9 100644 --- a/lib/linter/config-comment-parser.js +++ b/lib/linter/config-comment-parser.js @@ -75,11 +75,9 @@ module.exports = class ConfigCommentParser { parseJsonConfig(string, location) { debug("Parsing JSON config"); - let items = {}; - // Parses a JSON-like comment by the same way as parsing CLI option. try { - items = levn.parse("Object", string) || {}; + const items = levn.parse("Object", string) || {}; // Some tests say that it should ignore invalid comments such as `/*eslint no-alert:abc*/`. // Also, commaless notations have invalid severity: @@ -102,11 +100,15 @@ module.exports = class ConfigCommentParser { * Optionator cannot parse commaless notations. * But we are supporting that. So this is a fallback for that. */ - items = {}; const normalizedString = string.replace(/([-a-zA-Z0-9/]+):/gu, "\"$1\":").replace(/(\]|[0-9])\s+(?=")/u, "$1,"); try { - items = JSON.parse(`{${normalizedString}}`); + const items = JSON.parse(`{${normalizedString}}`); + + return { + success: true, + config: items + }; } catch (ex) { debug("Manual parsing failed."); @@ -124,11 +126,6 @@ module.exports = class ConfigCommentParser { }; } - - return { - success: true, - config: items - }; } /** diff --git a/lib/linter/linter.js b/lib/linter/linter.js index d1aaae6560f..ed2e41b83c8 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -2139,7 +2139,7 @@ class Linter { * SourceCodeFixer. */ verifyAndFix(text, config, options) { - let messages = [], + let messages, fixedResult, fixed = false, passNumber = 0, diff --git a/lib/rules/array-bracket-newline.js b/lib/rules/array-bracket-newline.js index 12ef5b612d6..9eb725825a9 100644 --- a/lib/rules/array-bracket-newline.js +++ b/lib/rules/array-bracket-newline.js @@ -74,7 +74,7 @@ module.exports = { function normalizeOptionValue(option) { let consistent = false; let multiline = false; - let minItems = 0; + let minItems; if (option) { if (option === "consistent") { diff --git a/lib/rules/indent-legacy.js b/lib/rules/indent-legacy.js index 117815aee4a..4adb4230e8d 100644 --- a/lib/rules/indent-legacy.js +++ b/lib/rules/indent-legacy.js @@ -830,7 +830,7 @@ module.exports = { } let indent; - let nodesToCheck = []; + let nodesToCheck; /* * For this statements we should check indent from statement beginning, diff --git a/lib/rules/indent.js b/lib/rules/indent.js index 33cb881d3b3..bc812b13c3e 100644 --- a/lib/rules/indent.js +++ b/lib/rules/indent.js @@ -1407,7 +1407,7 @@ module.exports = { PropertyDefinition(node) { const firstToken = sourceCode.getFirstToken(node); const maybeSemicolonToken = sourceCode.getLastToken(node); - let keyLastToken = null; + let keyLastToken; // Indent key. if (node.computed) { diff --git a/lib/rules/index.js b/lib/rules/index.js index 5e449459f2f..5ff7b6f5dde 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -229,6 +229,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({ "no-unused-private-class-members": () => require("./no-unused-private-class-members"), "no-unused-vars": () => require("./no-unused-vars"), "no-use-before-define": () => require("./no-use-before-define"), + "no-useless-assignment": () => require("./no-useless-assignment"), "no-useless-backreference": () => require("./no-useless-backreference"), "no-useless-call": () => require("./no-useless-call"), "no-useless-catch": () => require("./no-useless-catch"), diff --git a/lib/rules/key-spacing.js b/lib/rules/key-spacing.js index 64f61255589..aa8fd37b8ad 100644 --- a/lib/rules/key-spacing.js +++ b/lib/rules/key-spacing.js @@ -489,7 +489,7 @@ module.exports = { } } - let messageId = ""; + let messageId; if (isExtra) { messageId = side === "key" ? "extraKey" : "extraValue"; diff --git a/lib/rules/max-len.js b/lib/rules/max-len.js index e9ab5e685f1..15910d5d95d 100644 --- a/lib/rules/max-len.js +++ b/lib/rules/max-len.js @@ -344,7 +344,7 @@ module.exports = { * comments to check. */ if (commentsIndex < comments.length) { - let comment = null; + let comment; // iterate over comments until we find one past the current line do { diff --git a/lib/rules/no-dupe-class-members.js b/lib/rules/no-dupe-class-members.js index f35a2b3089e..c3355321759 100644 --- a/lib/rules/no-dupe-class-members.js +++ b/lib/rules/no-dupe-class-members.js @@ -82,7 +82,7 @@ module.exports = { } const state = getState(name, node.static); - let isDuplicate = false; + let isDuplicate; if (kind === "get") { isDuplicate = (state.init || state.get); diff --git a/lib/rules/no-empty-function.js b/lib/rules/no-empty-function.js index 2fcb75534ac..b7dee94c4ea 100644 --- a/lib/rules/no-empty-function.js +++ b/lib/rules/no-empty-function.js @@ -40,7 +40,7 @@ const ALLOW_OPTIONS = Object.freeze([ */ function getKind(node) { const parent = node.parent; - let kind = ""; + let kind; if (node.type === "ArrowFunctionExpression") { return "arrowFunctions"; @@ -73,7 +73,7 @@ function getKind(node) { } // Detects prefix. - let prefix = ""; + let prefix; if (node.generator) { prefix = "generator"; diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index b3635e3d509..c50d8a89055 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -64,7 +64,7 @@ module.exports = { */ function notBaseTenLosesPrecision(node) { const rawString = getRaw(node).toUpperCase(); - let base = 0; + let base; if (rawString.startsWith("0B")) { base = 2; diff --git a/lib/rules/no-trailing-spaces.js b/lib/rules/no-trailing-spaces.js index eede46c8634..c188b1fa899 100644 --- a/lib/rules/no-trailing-spaces.js +++ b/lib/rules/no-trailing-spaces.js @@ -129,8 +129,7 @@ module.exports = { comments = sourceCode.getAllComments(), commentLineNumbers = getCommentLineNumbers(comments); - let totalLength = 0, - fixRange = []; + let totalLength = 0; for (let i = 0, ii = lines.length; i < ii; i++) { const lineNumber = i + 1; @@ -177,7 +176,7 @@ module.exports = { continue; } - fixRange = [rangeStart, rangeEnd]; + const fixRange = [rangeStart, rangeEnd]; if (!ignoreComments || !commentLineNumbers.has(lineNumber)) { report(node, location, fixRange); diff --git a/lib/rules/no-useless-assignment.js b/lib/rules/no-useless-assignment.js new file mode 100644 index 00000000000..cac8ba1fcd1 --- /dev/null +++ b/lib/rules/no-useless-assignment.js @@ -0,0 +1,566 @@ +/** + * @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} 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} segments The map of ScopeStackSegmentInfo. + * @property {Set} currentSegments The current CodePathSegments. + * @property {Map} 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} */ + 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} + */ + 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} 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] + }); + } + } + }; + } +}; diff --git a/lib/rules/utils/ast-utils.js b/lib/rules/utils/ast-utils.js index 1e5237df8b6..4b074e0198f 100644 --- a/lib/rules/utils/ast-utils.js +++ b/lib/rules/utils/ast-utils.js @@ -1909,8 +1909,8 @@ module.exports = { */ getFunctionHeadLoc(node, sourceCode) { const parent = node.parent; - let start = null; - let end = null; + let start; + let end; if (parent.type === "Property" || parent.type === "MethodDefinition" || parent.type === "PropertyDefinition") { start = parent.loc.start; diff --git a/lib/rules/yield-star-spacing.js b/lib/rules/yield-star-spacing.js index 9a67b78d25f..5cf3804abb8 100644 --- a/lib/rules/yield-star-spacing.js +++ b/lib/rules/yield-star-spacing.js @@ -79,7 +79,7 @@ module.exports = { const after = leftToken.value === "*"; const spaceRequired = mode[side]; const node = after ? leftToken : rightToken; - let messageId = ""; + let messageId; if (spaceRequired) { messageId = side === "before" ? "missingBefore" : "missingAfter"; diff --git a/lib/source-code/token-store/index.js b/lib/source-code/token-store/index.js index 46a96b2f4b1..d222c87bbf4 100644 --- a/lib/source-code/token-store/index.js +++ b/lib/source-code/token-store/index.js @@ -37,8 +37,8 @@ function createIndexMap(tokens, comments) { const map = Object.create(null); let tokenIndex = 0; let commentIndex = 0; - let nextStart = 0; - let range = null; + let nextStart; + let range; while (tokenIndex < tokens.length || commentIndex < comments.length) { nextStart = (commentIndex < comments.length) ? comments[commentIndex].range[0] : Number.MAX_SAFE_INTEGER; diff --git a/packages/eslint-config-eslint/base.js b/packages/eslint-config-eslint/base.js index 3773b2b6b4a..571a18ed55c 100644 --- a/packages/eslint-config-eslint/base.js +++ b/packages/eslint-config-eslint/base.js @@ -165,6 +165,7 @@ const jsConfigs = [js.configs.recommended, { } ], "no-use-before-define": "error", + "no-useless-assignment": "error", "no-useless-call": "error", "no-useless-computed-key": "error", "no-useless-concat": "error", diff --git a/packages/eslint-config-eslint/eslintrc.js b/packages/eslint-config-eslint/eslintrc.js index 774c1eba344..283e83fc1c1 100644 --- a/packages/eslint-config-eslint/eslintrc.js +++ b/packages/eslint-config-eslint/eslintrc.js @@ -322,6 +322,7 @@ module.exports = { } ], "no-use-before-define": "error", + "no-useless-assignment": "error", "no-useless-call": "error", "no-useless-computed-key": "error", "no-useless-concat": "error", diff --git a/packages/js/src/configs/eslint-all.js b/packages/js/src/configs/eslint-all.js index 2bf3564e453..3fe1552af9d 100644 --- a/packages/js/src/configs/eslint-all.js +++ b/packages/js/src/configs/eslint-all.js @@ -162,6 +162,7 @@ module.exports = Object.freeze({ "no-unused-private-class-members": "error", "no-unused-vars": "error", "no-use-before-define": "error", + "no-useless-assignment": "error", "no-useless-backreference": "error", "no-useless-call": "error", "no-useless-catch": "error", diff --git a/tests/lib/rules/no-useless-assignment.js b/tests/lib/rules/no-useless-assignment.js new file mode 100644 index 00000000000..4219df0da48 --- /dev/null +++ b/tests/lib/rules/no-useless-assignment.js @@ -0,0 +1,1009 @@ +/** + * @fileoverview Tests for no-useless-assignment rule. + * @author Yosuke Ota + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/no-useless-assignment"); +const RuleTester = require("../../../lib/rule-tester/rule-tester"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + plugins: { + test: { + rules: { + "use-a": { + create(context) { + const sourceCode = context.sourceCode; + + return { + VariableDeclaration(node) { + sourceCode.markVariableAsUsed("a", node); + } + }; + } + } + } + } + } +}); + + +ruleTester.run("no-useless-assignment", rule, { + valid: [ + + // Basic tests + `let v = 'used'; + console.log(v); + v = 'used-2' + console.log(v);`, + `function foo() { + let v = 'used'; + console.log(v); + v = 'used-2'; + console.log(v); + }`, + `function foo() { + let v = 'used'; + if (condition) { + v = 'used-2'; + console.log(v); + return + } + console.log(v); + }`, + `function foo() { + let v = 'used'; + if (condition) { + console.log(v); + } else { + v = 'used-2'; + console.log(v); + } + }`, + `function foo() { + let v = 'used'; + if (condition) { + // + } else { + v = 'used-2'; + } + console.log(v); + }`, + `var foo = function () { + let v = 'used'; + console.log(v); + v = 'used-2' + console.log(v); + }`, + `var foo = () => { + let v = 'used'; + console.log(v); + v = 'used-2' + console.log(v); + }`, + `class foo { + static { + let v = 'used'; + console.log(v); + v = 'used-2' + console.log(v); + } + }`, + `function foo () { + let v = 'used'; + for (let i = 0; i < 10; i++) { + console.log(v); + v = 'used in next iteration'; + } + }`, + `function foo () { + let i = 0; + i++; + i++; + console.log(i); + }`, + + // Exported + `export let foo = 'used'; + console.log(foo); + foo = 'unused like but exported';`, + `export function foo () {}; + console.log(foo); + foo = 'unused like but exported';`, + `export class foo {}; + console.log(foo); + foo = 'unused like but exported';`, + `export default function foo () {}; + console.log(foo); + foo = 'unused like but exported';`, + `export default class foo {}; + console.log(foo); + foo = 'unused like but exported';`, + `let foo = 'used'; + export { foo }; + console.log(foo); + foo = 'unused like but exported';`, + `function foo () {}; + export { foo }; + console.log(foo); + foo = 'unused like but exported';`, + `class foo {}; + export { foo }; + console.log(foo); + foo = 'unused like but exported';`, + { + code: + `/* exported foo */ + let foo = 'used'; + console.log(foo); + foo = 'unused like but exported with directive';`, + languageOptions: { sourceType: "script" } + }, + + // Mark variables as used via markVariableAsUsed() + `/*eslint test/use-a:1*/ + let a = 'used'; + console.log(a); + a = 'unused like but marked by markVariableAsUsed()'; + `, + + // Unknown variable + `v = 'used'; + console.log(v); + v = 'unused'`, + + // Unused variable + "let v = 'used variable';", + + // Unreachable + `function foo() { + return; + + const x = 1; + if (y) { + bar(x); + } + }`, + `function foo() { + const x = 1; + console.log(x); + return; + + x = 'Foo' + }`, + + // Update + `function foo() { + let a = 42; + console.log(a); + a++; + console.log(a); + }`, + `function foo() { + let a = 42; + console.log(a); + a--; + console.log(a); + }`, + + // Assign with update + `function foo() { + let a = 42; + console.log(a); + a = 10; + a = a + 1; + console.log(a); + }`, + `function foo() { + let a = 42; + console.log(a); + a = 10; + if (cond) { + a = a + 1; + } else { + a = 2 + a; + } + console.log(a); + }`, + + // Assign to complex patterns + `function foo() { + let a = 'used', b = 'used', c = 'used', d = 'used'; + console.log(a, b, c, d); + ({ a, arr: [b, c, ...d] } = fn()); + console.log(a, b, c, d); + }`, + `function foo() { + let a = 'used', b = 'used', c = 'used'; + console.log(a, b, c); + ({ a = 'unused', foo: b, ...c } = fn()); + console.log(a, b, c); + }`, + `function foo() { + let a = {}; + console.log(a); + a.b = 'unused like, but maybe used in setter'; + }`, + `function foo() { + let a = { b: 42 }; + console.log(a); + a.b++; + }`, + + // Value may be used in other scopes. + `function foo () { + let v = 'used'; + console.log(v); + function bar() { + v = 'used in outer scope'; + } + bar(); + console.log(v); + }`, + `function foo () { + let v = 'used'; + console.log(v); + setTimeout(() => console.log(v), 1); + v = 'used in other scope'; + }`, + + // Loops + `function foo () { + let v = 'used'; + console.log(v); + for (let i = 0; i < 10; i++) { + if (condition) { + v = 'maybe used'; + continue; + } + console.log(v); + } + }`, + + // Ignore known globals + `/* globals foo */ + const bk = foo; + foo = 42; + try { + // process + } finally { + foo = bk; + }`, + { + code: ` + const bk = console; + console = { log () {} }; + try { + // process + } finally { + console = bk; + }`, + languageOptions: { + globals: { console: false } + } + }, + + // Try catch finally + `let message = 'init'; + try { + const result = call(); + message = result.message; + } catch (e) { + // ignore + } + console.log(message)`, + `let message = 'init'; + try { + message = call().message; + } catch (e) { + // ignore + } + console.log(message)`, + `let v = 'init'; + try { + v = callA(); + try { + v = callB(); + } catch (e) { + // ignore + } + } catch (e) { + // ignore + } + console.log(v)`, + `let v = 'init'; + try { + try { + v = callA(); + } catch (e) { + // ignore + } + } catch (e) { + // ignore + } + console.log(v)`, + `let a; + try { + foo(); + } finally { + a = 5; + } + console.log(a);`, + + // An expression within an assignment. + `const obj = { a: 5 }; + const { a, b = a } = obj; + console.log(b); // 5`, + `const arr = [6]; + const [c, d = c] = arr; + console.log(d); // 6`, + `const obj = { a: 1 }; + let { + a, + b = (a = 2) + } = obj; + console.log(a, b);`, + `let { a, b: {c = a} = {} } = obj; + console.log(c);` + ], + invalid: [ + { + code: + `let v = 'used'; + console.log(v); + v = 'unused'`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 3, + column: 13 + } + ] + }, + { + code: + `function foo() { + let v = 'used'; + console.log(v); + v = 'unused'; + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 4, + column: 17 + } + ] + }, + { + code: + `function foo() { + let v = 'used'; + if (condition) { + v = 'unused'; + return + } + console.log(v); + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 4, + column: 21 + } + ] + }, + { + code: + `function foo() { + let v = 'used'; + if (condition) { + console.log(v); + } else { + v = 'unused'; + } + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 6, + column: 21 + } + ] + }, + { + code: + `var foo = function () { + let v = 'used'; + console.log(v); + v = 'unused' + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 4, + column: 17 + } + ] + }, + { + code: + `var foo = () => { + let v = 'used'; + console.log(v); + v = 'unused' + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 4, + column: 17 + } + ] + }, + { + code: + `class foo { + static { + let v = 'used'; + console.log(v); + v = 'unused' + } + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 5, + column: 21 + } + ] + }, + { + code: + `function foo() { + let v = 'unused'; + if (condition) { + v = 'used'; + console.log(v); + return + } + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 2, + column: 21 + } + ] + }, + { + code: + `function foo() { + let v = 'used'; + console.log(v); + v = 'unused'; + v = 'unused'; + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 4, + column: 17 + }, + { + messageId: "unnecessaryAssignment", + line: 5, + column: 17 + } + ] + }, + { + code: + `function foo() { + let v = 'used'; + console.log(v); + v = 'unused'; + v = 'used'; + console.log(v); + v = 'used'; + console.log(v); + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 4, + column: 17 + } + ] + }, + { + code: ` + let v; + v = 'unused'; + if (foo) { + v = 'used'; + } else { + v = 'used'; + } + console.log(v);`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 3, + column: 13 + } + ] + }, + { + code: + `function foo() { + let v = 'used'; + console.log(v); + v = 'unused'; + v = 'unused'; + v = 'used'; + console.log(v); + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 4, + column: 17 + }, + { + messageId: "unnecessaryAssignment", + line: 5, + column: 17 + } + ] + }, + { + code: + `function foo() { + let v = 'unused'; + if (condition) { + if (condition2) { + v = 'used-2'; + } else { + v = 'used-3'; + } + } else { + v = 'used-4'; + } + console.log(v); + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 2, + column: 21 + } + ] + }, + { + code: + `function foo() { + let v; + if (condition) { + v = 'unused'; + } else { + // + } + if (condition2) { + v = 'used-1'; + } else { + v = 'used-2'; + } + console.log(v); + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 4, + column: 21 + } + ] + }, + { + code: + `function foo() { + let v = 'used'; + if (condition) { + v = 'unused'; + v = 'unused'; + v = 'used'; + } + console.log(v); + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 4, + column: 21 + }, + { + messageId: "unnecessaryAssignment", + line: 5, + column: 21 + } + ] + }, + + // Update + { + code: + `function foo() { + let a = 42; + console.log(a); + a++; + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 4, + column: 17 + } + ] + }, + { + code: + `function foo() { + let a = 42; + console.log(a); + a--; + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 4, + column: 17 + } + ] + }, + + // Assign to complex patterns + { + code: + `function foo() { + let a = 'used', b = 'used', c = 'used', d = 'used'; + console.log(a, b, c, d); + ({ a, arr: [b, c,, ...d] } = fn()); + console.log(c); + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 4, + column: 20 + }, + { + messageId: "unnecessaryAssignment", + line: 4, + column: 29 + }, + { + messageId: "unnecessaryAssignment", + line: 4, + column: 39 + } + ] + }, + { + code: + `function foo() { + let a = 'used', b = 'used', c = 'used'; + console.log(a, b, c); + ({ a = 'unused', foo: b, ...c } = fn()); + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 4, + column: 20 + }, + { + messageId: "unnecessaryAssignment", + line: 4, + column: 39 + }, + { + messageId: "unnecessaryAssignment", + line: 4, + column: 45 + } + ] + }, + + // Variable used in other scopes, but write only. + { + code: + `function foo () { + let v = 'used'; + console.log(v); + setTimeout(() => v = 42, 1); + v = 'unused and variable is only updated in other scopes'; + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 5, + column: 17 + } + ] + }, + + // Code Path Segment End Statements + { + code: + `function foo() { + let v = 'used'; + if (condition) { + let v = 'used'; + console.log(v); + v = 'unused'; + } + console.log(v); + v = 'unused'; + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 6, + column: 21 + }, + { + messageId: "unnecessaryAssignment", + line: 9, + column: 17 + } + ] + }, + { + code: + `function foo() { + let v = 'used'; + if (condition) { + console.log(v); + v = 'unused'; + } else { + v = 'unused'; + } + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 5, + column: 21 + }, + { + messageId: "unnecessaryAssignment", + line: 7, + column: 21 + } + ] + }, + { + code: + `function foo () { + let v = 'used'; + console.log(v); + v = 'unused'; + return; + console.log(v); + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 4, + column: 17 + } + ] + }, + { + code: + `function foo () { + let v = 'used'; + console.log(v); + v = 'unused'; + throw new Error(); + console.log(v); + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 4, + column: 17 + } + ] + }, + { + code: + `function foo () { + let v = 'used'; + console.log(v); + for (let i = 0; i < 10; i++) { + v = 'unused'; + continue; + console.log(v); + } + } + function bar () { + let v = 'used'; + console.log(v); + for (let i = 0; i < 10; i++) { + v = 'unused'; + break; + console.log(v); + } + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 5, + column: 21 + }, + { + messageId: "unnecessaryAssignment", + line: 14, + column: 21 + } + ] + }, + { + code: + `function foo () { + let v = 'used'; + console.log(v); + for (let i = 0; i < 10; i++) { + if (condition) { + v = 'unused'; + break; + } + console.log(v); + } + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 6, + column: 25 + } + ] + }, + + // Try catch + { + code: + `let message = 'unused'; + try { + const result = call(); + message = result.message; + } catch (e) { + message = 'used'; + } + console.log(message)`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 1, + column: 5 + } + ] + }, + { + code: + `let message = 'unused'; + try { + message = 'used'; + console.log(message) + } catch (e) { + }`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 1, + column: 5 + } + ] + }, + { + code: + `let message = 'unused'; + try { + message = call(); + } catch (e) { + message = 'used'; + } + console.log(message)`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 1, + column: 5 + } + ] + }, + { + code: + `let v = 'unused'; + try { + v = callA(); + try { + v = callB(); + } catch (e) { + // ignore + } + } catch (e) { + v = 'used'; + } + console.log(v)`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 1, + column: 5 + } + ] + }, + + // An expression within an assignment. + { + code: ` + var x = 1; // used + x = x + 1; // unused + x = 5; // used + f(x);`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 3, + column: 13 + } + ] + }, + { + code: ` + var x = 1; // used + x = // used + x++; // unused + f(x);`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 4, + column: 17 + } + ] + }, + { + code: `const obj = { a: 1 }; + let { + a, + b = (a = 2) + } = obj; + a = 3 + console.log(a, b);`, + errors: [ + { + messageId: "unnecessaryAssignment", + line: 3, + column: 17 + }, + { + messageId: "unnecessaryAssignment", + line: 4, + column: 22 + } + ] + } + ] +}); diff --git a/tools/rule-types.json b/tools/rule-types.json index 35895f3838a..8a021f04556 100644 --- a/tools/rule-types.json +++ b/tools/rule-types.json @@ -216,6 +216,7 @@ "no-unused-private-class-members": "problem", "no-unused-vars": "problem", "no-use-before-define": "problem", + "no-useless-assignment": "problem", "no-useless-backreference": "problem", "no-useless-call": "suggestion", "no-useless-catch": "suggestion",