diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js index aa6f6dfe990b..ac9f0c9bedc0 100644 --- a/lib/rules/no-unused-vars.js +++ b/lib/rules/no-unused-vars.js @@ -874,6 +874,8 @@ module.exports = { const parentType = parent.type; const tokenBefore = sourceCode.getTokenBefore(id); const tokenAfter = sourceCode.getTokenAfter(id); + const isFunction = astUtils.isFunction; + const isLoop = astUtils.isLoop; /** * get range from token before of a given node @@ -881,7 +883,7 @@ module.exports = { * @param {number} skips number of token to skip * @returns {number} start range of token before the identifier */ - function getBeforeToken(node, skips) { + function getPreviousTokenStart(node, skips) { return sourceCode.getTokenBefore(node, skips).range[0]; } @@ -891,7 +893,7 @@ module.exports = { * @param {number} skips number of token to skip * @returns {number} end range of token after the identifier */ - function getAfterToken(node, skips) { + function getNextTokenEnd(node, skips) { return sourceCode.getTokenAfter(node, skips).range[1]; } @@ -913,28 +915,6 @@ module.exports = { return sourceCode.getTokenAfter(node).value; } - /** - * check whether a given node is function - * @param {ASTNode} node parent of identifier - * @returns {boolean} true if node is function - */ - function isTypeFunction(node) { - return ( - node.type === "FunctionDeclaration" || - node.type === "FunctionExpression" || - node.type === "ArrowFunctionExpression" - ); - } - - /** - * check if given node is forOf or forIn loop - * @param {ASTNode} node node to check - * @returns {boolean} true if node is forOf or forIn loop - */ - function isLoop(node) { - return (node.type === "ForOfStatement" || node.type === "ForInStatement"); - } - /** * Check if an array has a single element with null as other element. * @param {ASTNode} node ArrayPattern node @@ -945,39 +925,58 @@ module.exports = { } /** - * Gets fixes in variable declarations and function parameters - * @param {ASTNode} node parent node of identifier + * give fixes for unused variables in function parameters + * @param {ASTNode} node node to check * @returns {Object} fixer object */ - function fixVariables(node) { + function fixFunctionParameters(node) { + const parentNode = node.parent; - // remove unused declared variables such as var a; or var a, b; - if (node.parent.type === "VariableDeclarator") { - if (node.parent.parent.declarations.length === 1) { - return fixer.removeRange(node.parent.parent.range); + if (isFunction(parentNode)) { + + // remove unused function parameter if there is only a single parameter + if (parentNode.params.length === 1) { + return fixer.removeRange(node.range); } - if (getTokenBeforeValue(node.parent) === ",") { - return fixer.removeRange([getBeforeToken(node.parent), node.parent.range[1]]); + // remove first unused function parameter when there are multiple parameters + if (getTokenBeforeValue(node) === "(" && getTokenAfterValue(node) === ",") { + return fixer.removeRange([node.range[0], getNextTokenEnd(node)]); } - return fixer.removeRange([node.parent.range[0], getAfterToken(node.parent)]); + // remove unused function parameters except first one when there are multiple parameters + return fixer.removeRange([getPreviousTokenStart(node), node.range[1]]); } - // remove unused varibles inside functions arguments - if (isTypeFunction(node.parent)) { - if (node.parent.params.length === 1) { - return fixer.removeRange(node.range); + return null; + } + + /** + * Gets fixes in variable declarations and function parameters + * @param {ASTNode} node parent node of identifier + * @returns {Object} fixer object + */ + function fixVariables(node) { + const parentNode = node.parent; + + // remove unused declared variables such as var a = b; or var a = b, c; + if (parentNode.type === "VariableDeclarator") { + + // remove unused declared variable with single declaration such as 'var a = b;' + if (parentNode.parent.declarations.length === 1) { + return fixer.removeRange(parentNode.parent.range); } - if (getTokenBeforeValue(node) === "(" && getTokenAfterValue(node) === ",") { - return fixer.removeRange([node.range[0], getAfterToken(node)]); + // remove unused declared variable with multiple declaration except first one such as 'var a = b, c = d;' + if (getTokenBeforeValue(parentNode) === ",") { + return fixer.removeRange([getPreviousTokenStart(parentNode), parentNode.range[1]]); } - return fixer.removeRange([getBeforeToken(node), node.range[1]]); + // remove first unused declared variable when there are multiple declarations + return fixer.removeRange([parentNode.range[0], getNextTokenEnd(parentNode)]); } - return null; + return fixFunctionParameters(node); } /** @@ -985,47 +984,39 @@ module.exports = { * @param {ASTNode} node parent node to check * @returns {Object} fixer object */ - function fixObjectAndArrayVariable(node) { - if (node.parent.type === "VariableDeclarator") { + function fixObjectAndArrayVariableVariable(node) { + const parentNode = node.parent; + + if (parentNode.type === "VariableDeclarator") { // skip variable in for (const [ foo ] of bar); - if (isLoop(node.parent.parent.parent)) { + if (isLoop(parentNode.parent.parent)) { return null; } - if (node.parent.parent.declarations.length === 1) { - return fixer.removeRange(node.parent.parent.range); - } - - if (getTokenBeforeValue(node.parent) === ",") { - return fixer.removeRange([getBeforeToken(node.parent), node.parent.range[1]]); + // remove complete declaration when there is an unused variable in 'const { a } = foo;', same for arrays. + if (parentNode.parent.declarations.length === 1) { + return fixer.removeRange(parentNode.parent.range); } - return fixer.removeRange([node.parent.range[0], getAfterToken(node.parent)]); - } - - if (isTypeFunction(node.parent)) { - if (node.parent.params.length === 1) { - return fixer.removeRange(node.range); - } - - if (getTokenBeforeValue(node) === "(" && getTokenAfterValue(node) === ",") { - return fixer.removeRange([node.range[0], getAfterToken(node)]); + // fix 'let bar = "hello", { a } = foo;' to 'let bar = "hello";' if 'a' is unused, same for arrays. + if (getTokenBeforeValue(parentNode) === ",") { + return fixer.removeRange([getPreviousTokenStart(parentNode), parentNode.range[1]]); } - return fixer.removeRange([getBeforeToken(node), node.range[1]]); + // fix 'let { a } = foo, bar = "hello";' to 'let bar = "hello";' if 'a' is unused, same for arrays. + return fixer.removeRange([parentNode.range[0], getNextTokenEnd(parentNode)]); } + // fixes [{a: {k}}], [{a: [k]}] if (getTokenBeforeValue(node) === ":") { - if (node.parent.parent.type === "ObjectPattern") { - - // fixes [{a: k}], [{a: {k}}], [{a: [k]}] + if (parentNode.parent.type === "ObjectPattern") { // eslint-disable-next-line no-use-before-define -- due to interdependency of functions return fixObjectWithValueSeperator(node); } } - return null; + return fixFunctionParameters(node); } /** @@ -1034,25 +1025,33 @@ module.exports = { * @returns {Object} fixer object */ function fixNestedObjectVariable(node) { + const parentNode = node.parent; + + // fix for {{ a: { b } }} if ( - node.parent.parent.parent.parent.type === "ObjectPattern" && - node.parent.parent.properties.length === 1 + parentNode.parent.parent.parent.type === "ObjectPattern" && + parentNode.parent.properties.length === 1 ) { - return fixNestedObjectVariable(node.parent.parent); + return fixNestedObjectVariable(parentNode.parent); } - if (node.parent.parent.type === "ObjectPattern") { - if (node.parent.parent.properties.length === 1) { - return fixVariables(node.parent.parent); + // fix for { a: { b } } + if (parentNode.parent.type === "ObjectPattern") { + + // fix for unused variables in dectructured object with single property in variable decalartion and function parameter + if (parentNode.parent.properties.length === 1) { + return fixVariables(parentNode.parent); } - if (getTokenBeforeValue(node.parent) === "{") { + // fix for first unused property when there are multiple properties such as '{ a: { b }, c }' + if (getTokenBeforeValue(parentNode) === "{") { return fixer.removeRange( - [node.parent.range[0], getAfterToken(node.parent)] + [parentNode.range[0], getNextTokenEnd(parentNode)] ); } - return fixer.removeRange([getBeforeToken(node.parent), node.parent.range[1]]); + // fix for unused property except first one when there are multiple properties such as '{ k, a: { b } }' + return fixer.removeRange([getPreviousTokenStart(parentNode), parentNode.range[1]]); } return null; @@ -1064,49 +1063,59 @@ module.exports = { * @returns {Object} fixer object */ function fixNestedArrayVariable(node) { - if (node.parent.parent.type === "ArrayPattern" && hasSingleElement(node.parent)) { - return fixNestedArrayVariable(node.parent); + const parentNode = node.parent; + + // fix for nested arrays [[ a ]] + if (parentNode.parent.type === "ArrayPattern" && hasSingleElement(parentNode)) { + return fixNestedArrayVariable(parentNode); } - if (node.parent.elements.length === 1 || hasSingleElement(node.parent)) { + if (hasSingleElement(parentNode)) { // fixes { a: [{ b }] } or { a: [[ b ]] } - if (getTokenBeforeValue(node.parent) === ":") { - return fixObjectAndArrayVariable(node.parent); + if (getTokenBeforeValue(parentNode) === ":") { + return fixObjectAndArrayVariableVariable(parentNode); } // fixes [a, ...[[ b ]]] or [a, ...[{ b }]] - if (node.parent.parent.type === "RestElement") { + if (parentNode.parent.type === "RestElement") { // eslint-disable-next-line no-use-before-define -- due to interdependency of functions - return fixRestInPattern(node.parent.parent); + return fixRestInPattern(parentNode.parent); } - return fixVariables(node.parent); + // fix unused variables in destructured array in variable declaration or function parameter + return fixVariables(parentNode); } + // remove last unused array element if ( getTokenBeforeValue(node) === "," && getTokenAfterValue(node) === "]" ) { - return fixer.removeRange([getBeforeToken(node), node.range[1]]); + return fixer.removeRange([getPreviousTokenStart(node), node.range[1]]); } + // remove unused array element return fixer.removeRange(node.range); } /** - * fix {a: k}, {a: {k}} or {a: [k]} like cases + * fix cases like {a: {k}} or {a: [k]} * @param {ASTNode} node parent node to check * @returns {Object} fixer object */ function fixObjectWithValueSeperator(node) { + const parentNode = node.parent.parent; + + // fix cases like [{a : { b }}] or [{a : [ b ]}] if ( - node.parent.parent.parent.type === "ArrayPattern" && - node.parent.parent.properties.length === 1 + parentNode.parent.type === "ArrayPattern" && + parentNode.properties.length === 1 ) { - return fixNestedArrayVariable(node.parent.parent); + return fixNestedArrayVariable(parentNode); } + // fix cases like {a: {k}} or {a: [k]} return fixNestedObjectVariable(node); } @@ -1116,33 +1125,39 @@ module.exports = { * @returns {Object} fixer object */ function fixRestInPattern(node) { - if (isTypeFunction(node.parent)) { - if (node.parent.params.length === 1) { + const parentNode = node.parent; + + // fix [...[a]] or [...{a}] in function parameters + if (isFunction(parentNode)) { + if (parentNode.params.length === 1) { return fixer.removeRange(node.range); } - return fixer.removeRange([getBeforeToken(node), node.range[1]]); + return fixer.removeRange([getPreviousTokenStart(node), node.range[1]]); } - if (node.parent.type === "ArrayPattern") { - if (node.parent.elements.length === 1 || hasSingleElement(node.parent)) { + // fix rest in nested array pattern like [[a, ...[b]]] + if (parentNode.type === "ArrayPattern") { - // fix [[a, ...[b]]] - if (node.parent.parent.type === "ArrayPattern") { - return fixNestedArrayVariable(node.parent); + // fix [[...[b]]] + if (hasSingleElement(parentNode)) { + if (parentNode.parent.type === "ArrayPattern") { + return fixNestedArrayVariable(parentNode); } - return fixVariables(node.parent); + // fix 'const [...[b]] = foo; and function foo([...[b]]) {} + return fixVariables(parentNode); } - return fixer.removeRange([getBeforeToken(node), node.range[1]]); + // fix [[a, ...[b]]] + return fixer.removeRange([getPreviousTokenStart(node), node.range[1]]); } return null; } // skip fix when variable is reassigned - if (writeReferences.length > 1 && id.parent.type !== "AssignmentPattern") { + if (writeReferences.length > 1 && parentType !== "AssignmentPattern") { return null; } @@ -1155,14 +1170,17 @@ module.exports = { return null; } + // // remove unused declared variable with single declaration like 'var a = b;' return fixer.removeRange(parent.parent.range); } + // // remove unused declared variable with multiple declaration except first one like 'var a = b, c = d;' if (tokenBefore.value === ",") { return fixer.removeRange([tokenBefore.range[0], parent.range[1]]); } - return fixer.removeRange([parent.range[0], getAfterToken(parent)]); + // remove first unused declared variable when there are multiple declarations + return fixer.removeRange([parent.range[0], getNextTokenEnd(parent)]); } // remove variables in object patterns @@ -1184,22 +1202,25 @@ module.exports = { * function a({a}) {} * fix const { a: { b } } = foo; */ - return fixObjectAndArrayVariable(parent.parent); + return fixObjectAndArrayVariableVariable(parent.parent); } // fix const { a:b } = foo; if (tokenBefore.value === ":") { + + // remove first unused variable in const { a:b } = foo; if (getTokenBeforeValue(parent) === "{" && getTokenAfterValue(parent) === ",") { - return fixer.removeRange([parent.range[0], getAfterToken(parent)]); + return fixer.removeRange([parent.range[0], getNextTokenEnd(parent)]); } - return fixer.removeRange([getBeforeToken(parent), id.range[1]]); + // remove unused variables in const { a: b, c: d } = foo; except first one + return fixer.removeRange([getPreviousTokenStart(parent), id.range[1]]); } } // remove unused varibales inside an array if (parentType === "ArrayPattern") { - if (parent.elements.length === 1 || hasSingleElement(parent)) { + if (hasSingleElement(parent)) { // fix [a, ...[b]] if (parent.parent.type === "RestElement") { @@ -1216,7 +1237,7 @@ module.exports = { * fix function foo([a]) {} * fix const { a: [b] } = foo; */ - return fixObjectAndArrayVariable(parent); + return fixObjectAndArrayVariableVariable(parent); } // if "a" is unused in [a, b ,c] fixes to [, b c] @@ -1230,35 +1251,45 @@ module.exports = { // fix [a, ...rest] if (parent.parent.type === "ArrayPattern") { - if (parent.parent.elements.length === 1 || hasSingleElement(parent.parent)) { + if (hasSingleElement(parent.parent)) { + + // fix [[...rest]] when there is only rest element if ( parent.parent.parent.type === "ArrayPattern" ) { return fixNestedArrayVariable(parent.parent); } + // fix 'const [...rest] = foo;' and 'function foo([...rest]) {}' return fixVariables(parent.parent); } - return fixer.removeRange([getBeforeToken(id, 1), id.range[1]]); + // fix [a, ...rest] + return fixer.removeRange([getPreviousTokenStart(id, 1), id.range[1]]); } // fix { a, ...rest} if (parent.parent.type === "ObjectPattern") { + + // fix 'const {...rest} = foo;' and 'function foo({...rest}) {}' if (parent.parent.properties.length === 1) { return fixVariables(parent.parent); } - return fixer.removeRange([getBeforeToken(id, 1), id.range[1]]); + // fix { a, ...rest} when there are multiple properties + return fixer.removeRange([getPreviousTokenStart(id, 1), id.range[1]]); } // fix function foo(...rest) {} - if (isTypeFunction(parent.parent)) { + if (isFunction(parent.parent)) { + + // remove unused rest in function parameter if there is only single parameter if (parent.parent.params.length === 1) { return fixer.removeRange(parent.range); } - return fixer.removeRange([getBeforeToken(parent), parent.range[1]]); + // remove unused rest in function parameter if there multiple parameter + return fixer.removeRange([getPreviousTokenStart(parent), parent.range[1]]); } } @@ -1278,17 +1309,20 @@ module.exports = { return fixNestedArrayVariable(parent.parent.parent); } + // fix 'const {a = aDefault} = foo;' and 'function foo({a = aDefault}) {}' return fixVariables(parent.parent.parent); } + // fix unused 'a' in {a = aDefault} if it is the first property if ( getTokenBeforeValue(parent.parent) === "{" && getTokenAfterValue(parent.parent) === "," ) { - return fixer.removeRange([parent.parent.range[0], getAfterToken(parent.parent)]); + return fixer.removeRange([parent.parent.range[0], getNextTokenEnd(parent.parent)]); } - return fixer.removeRange([getBeforeToken(parent.parent), parent.parent.range[1]]); + // fix unused 'b' in {a, b = aDefault} if it is not the first property + return fixer.removeRange([getPreviousTokenStart(parent.parent), parent.parent.range[1]]); } } @@ -1299,23 +1333,29 @@ module.exports = { // remove unused default import if (parentType === "ImportDefaultSpecifier") { + + // remove unused default import when there are not other imports if (!parent.parent.specifiers.some(e => e.type === "ImportSpecifier")) { return fixer.removeRange(parent.parent.range); } + // remove unused default import when there are other imports also return fixer.removeRange([id.range[0], tokenAfter.range[1]]); } - // remove unused imports + // remove unused imports when there is a single import if ( parentType === "ImportSpecifier" && parent.parent.specifiers.sort(e => e.type === "ImportSpecifier").length === 1 ) { + + // remove unused import when there is no default import if (!parent.parent.specifiers.some(e => e.type === "ImportDefaultSpecifier")) { return fixer.removeRange(parent.parent.range); } - return fixer.removeRange([getBeforeToken(id, 1), tokenAfter.range[1]]); + // remove unused import when there is a default import + return fixer.removeRange([getPreviousTokenStart(id, 1), tokenAfter.range[1]]); } // skip error in catch(error) variable @@ -1336,12 +1376,12 @@ module.exports = { // remove unused varible that is in a sequence inside function arguments and object pattern if (tokenAfter.value === ",") { - // fix function foo(a,b) {} + // fix function foo(a, b) {} if (tokenBefore.value === "(") { return fixer.removeRange([id.range[0], tokenAfter.range[1]]); } - // fix const {a,b} = foo; + // fix const {a, b} = foo; if (tokenBefore.value === "{") { return fixer.removeRange([id.range[0], tokenAfter.range[1]]); }