diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js new file mode 100644 index 0000000..e2f8e7e --- /dev/null +++ b/lib/rules/no-unused-vars.js @@ -0,0 +1,116 @@ +/** + * @fileoverview Add fixer to rule no-unused-vars. + * @author Pig Fang + */ +"use strict"; + +const ruleComposer = require("eslint-rule-composer"); +const utils = require("../utils"); + +const rule = utils.getFixableRule("no-unused-vars", false); + +/** + * Check if an expression has side effect. + * + * @param {Node} node AST node + * @returns {boolean} result + */ +function hasSideEffect(node) { + if (["Literal", "Identifier", "ThisExpression"].includes(node.type)) { + return false; + } + + if (node.type === "MemberExpression") { + return hasSideEffect(node.object) || hasSideEffect(node.property); + } + + if (node.type === "TemplateLiteral") { + return node.expressions.length !== 0; + } + + if (node.type === "AssignmentExpression") { + return hasSideEffect(node.right); + } + + return true; +} + +module.exports = ruleComposer.mapReports( + rule, + (problem, { sourceCode }) => { + problem.fix = fixer => { + const { node } = problem; + const { parent } = node; + + if (!parent) { + return null; + } + const grand = parent.parent; + + switch (parent.type) { + case "ImportSpecifier": + case "ImportDefaultSpecifier": + case "ImportNamespaceSpecifier": + if (!grand) { + return null; + } + + if (grand.specifiers.length === 1) { + return fixer.remove(grand); + } + + if (parent !== grand.specifiers[grand.specifiers.length - 1]) { + const comma = sourceCode.getTokenAfter(parent, { filter: token => token.value === "," }); + + return [fixer.remove(parent), fixer.remove(comma)]; + } + + if (grand.specifiers.filter(specifier => specifier.type === "ImportSpecifier").length === 1) { + const start = sourceCode.getTokenBefore(parent, { filter: token => token.value === "," }), + end = sourceCode.getTokenAfter(parent, { filter: token => token.value === "}" }); + + return fixer.removeRange([start.range[0], end.range[1]]); + } + + return fixer.removeRange([ + sourceCode.getTokenBefore(parent, { filter: token => token.value === "," }).range[0], + parent.range[1] + ]); + case "VariableDeclarator": + if (!grand) { + return null; + } + + if (parent.init && hasSideEffect(parent.init)) { + return null; + } + + if (grand.declarations.length === 1) { + return fixer.remove(grand); + } + + if (parent !== grand.declarations[grand.declarations.length - 1]) { + const comma = sourceCode.getTokenAfter(parent, { filter: token => token.value === "," }); + + return [fixer.remove(parent), fixer.remove(comma)]; + } + + return [ + fixer.remove(sourceCode.getTokenBefore(parent, { filter: token => token.value === "," })), + fixer.remove(parent) + ]; + case "AssignmentPattern": + if (hasSideEffect(parent.right)) { + return null; + } + return fixer.remove(parent); + case "RestElement": + case "Property": + return fixer.remove(parent); + default: + return null; + } + }; + return problem; + } +); diff --git a/readme.md b/readme.md index adff86e..2d7c65d 100644 --- a/readme.md +++ b/readme.md @@ -100,6 +100,7 @@ Name | ✔️ | 🛠 | Description [no-unneeded-ternary](https://eslint.org/docs/rules/no-unneeded-ternary) | | 🛠 | disallow ternary operators when simpler alternatives exist [no-unsafe-negation](https://eslint.org/docs/rules/no-unsafe-negation) | | 🛠 | disallow negating the left operand of relational operators [no-unused-labels](https://eslint.org/docs/rules/no-unused-labels) | | 🛠 | disallow unused labels +[no-unused-vars](https://eslint.org/docs/rules/no-unused-vars) | | 🛠 | disallow unused variables [no-useless-computed-key](https://eslint.org/docs/rules/no-useless-computed-key) | | 🛠 | disallow unnecessary computed property keys in object literals [no-useless-concat](https://eslint.org/docs/rules/no-useless-concat) | | 🛠 | disallow unnecessary concatenation of literals or template literals [no-useless-rename](https://eslint.org/docs/rules/no-useless-rename) | | 🛠 | disallow renaming import, export, and destructured assignments to the same name diff --git a/tests/lib/rules/no-unused-vars.js b/tests/lib/rules/no-unused-vars.js new file mode 100644 index 0000000..55b645a --- /dev/null +++ b/tests/lib/rules/no-unused-vars.js @@ -0,0 +1,190 @@ +/** + * @fileoverview Tests for rule no-unused-vars. + * @author Pig Fang + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/no-unused-vars"); +const RuleTester = require("eslint").RuleTester; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester(); + +ruleTester.run("no-unused-vars", rule, { + valid: [ + { + code: "import 'm'", + parserOptions: { sourceType: "module", ecmaVersion: 6 } + } + ], + invalid: [ + { + code: "import * as m from 'm'", + parserOptions: { sourceType: "module", ecmaVersion: 6 }, + output: "", + errors: [{ type: "Identifier" }] + }, + { + code: "import m from 'm'", + parserOptions: { sourceType: "module", ecmaVersion: 6 }, + output: "", + errors: [{ type: "Identifier" }] + }, + { + code: "import {m} from 'm'", + parserOptions: { sourceType: "module", ecmaVersion: 6 }, + output: "", + errors: [{ type: "Identifier" }] + }, + { + code: "import {m1 as m2} from 'm'", + parserOptions: { sourceType: "module", ecmaVersion: 6 }, + output: "", + errors: [{ type: "Identifier" }] + }, + { + code: "import m, {b} from 'm'; b;", + parserOptions: { sourceType: "module", ecmaVersion: 6 }, + output: "import {b} from 'm'; b;", + errors: [{ type: "Identifier" }] + }, + { + code: "import {a, b} from 'm'; b;", + parserOptions: { sourceType: "module", ecmaVersion: 6 }, + output: "import { b} from 'm'; b;", + errors: [{ type: "Identifier" }] + }, + { + code: "import {a1 as a2, b} from 'm'; b;", + parserOptions: { sourceType: "module", ecmaVersion: 6 }, + output: "import { b} from 'm'; b;", + errors: [{ type: "Identifier" }] + }, + { + code: "import {a, b} from 'm'; a;", + parserOptions: { sourceType: "module", ecmaVersion: 6 }, + output: "import {a} from 'm'; a;", + errors: [{ type: "Identifier" }] + }, + { + code: "import m, {a} from 'm'; m;", + parserOptions: { sourceType: "module", ecmaVersion: 6 }, + output: "import m from 'm'; m;", + errors: [{ type: "Identifier" }] + }, + { + code: "var a", + output: "", + errors: [{ type: "Identifier" }] + }, + { + code: "var a = b", + output: "", + errors: [{ type: "Identifier" }] + }, + { + code: "var a = undefined", + output: "", + errors: [{ type: "Identifier" }] + }, + { + code: "var a = null", + output: "", + errors: [{ type: "Identifier" }] + }, + { + code: "var a = 'b'", + output: "", + errors: [{ type: "Identifier" }] + }, + { + code: "var a = this", + output: "", + errors: [{ type: "Identifier" }] + }, + { + code: "var a = `template`", + parserOptions: { ecmaVersion: 6 }, + output: "", + errors: [{ type: "Identifier" }] + }, + { + code: "var a = this.value", + output: "", + errors: [{ type: "Identifier" }] + }, + { + code: "var a = b = c", + output: "", + errors: [{ type: "Identifier" }] + }, + { + code: "var a = `template-${value}`", + parserOptions: { ecmaVersion: 6 }, + output: null, + errors: [{ type: "Identifier" }] + }, + { + code: "var a = b()", + output: null, + errors: [{ type: "Identifier" }] + }, + { + code: "var a = (b()).c", + output: null, + errors: [{ type: "Identifier" }] + }, + { + code: "var a = b = c()", + output: null, + errors: [{ type: "Identifier" }] + }, + { + code: "var a = b, c = d; c;", + output: "var c = d; c;", + errors: [{ type: "Identifier" }] + }, + { + code: "var a = b, c = d; a;", + output: "var a = b ; a;", + errors: [{ type: "Identifier" }] + }, + { + code: "let {...a} = b", + output: "let {} = b", + parserOptions: { ecmaVersion: 2018 }, + errors: [{ type: "Identifier" }] + }, + { + code: "let {a} = b", + output: "let {} = b", + parserOptions: { ecmaVersion: 6 }, + errors: [{ type: "Identifier" }] + }, + { + code: "let {a1: a2} = b", + output: "let {} = b", + parserOptions: { ecmaVersion: 6 }, + errors: [{ type: "Identifier" }] + }, + { + code: "let {a = b} = c", + output: "let {} = c", + parserOptions: { ecmaVersion: 6 }, + errors: [{ type: "Identifier" }] + }, + { + code: "let {a = b()} = c", + output: null, + parserOptions: { ecmaVersion: 6 }, + errors: [{ type: "Identifier" }] + } + ] +});