diff options
Diffstat (limited to 'tools/eslint/lib/rules/no-unused-vars.js')
-rw-r--r-- | tools/eslint/lib/rules/no-unused-vars.js | 218 |
1 files changed, 145 insertions, 73 deletions
diff --git a/tools/eslint/lib/rules/no-unused-vars.js b/tools/eslint/lib/rules/no-unused-vars.js index d6ec43bf1d6..6b3b7f334e2 100644 --- a/tools/eslint/lib/rules/no-unused-vars.js +++ b/tools/eslint/lib/rules/no-unused-vars.js @@ -27,9 +27,14 @@ module.exports = function(context) { } } + //-------------------------------------------------------------------------- + // Helpers + //-------------------------------------------------------------------------- + + /** * Determines if a given variable is being exported from a module. - * @param {Variable} variable EScope variable object. + * @param {Variable} variable - EScope variable object. * @returns {boolean} True if the variable is exported, false if not. * @private */ @@ -39,12 +44,14 @@ module.exports = function(context) { if (definition) { - definition = definition.node; - if (definition.type === "VariableDeclarator") { - definition = definition.parent; + var node = definition.node; + if (node.type === "VariableDeclarator") { + node = node.parent; + } else if (definition.type === "Parameter" && node.type === "FunctionDeclaration") { + return false; } - return definition.parent.type.indexOf("Export") === 0; + return node.parent.type.indexOf("Export") === 0; } else { return false; } @@ -52,7 +59,7 @@ module.exports = function(context) { /** * Determines if a reference is a read operation. - * @param {Reference} ref - an escope Reference + * @param {Reference} ref - An escope Reference * @returns {Boolean} whether the given reference represents a read operation * @private */ @@ -61,120 +68,185 @@ module.exports = function(context) { } /** - * Determine if an identifier is referencing the enclosing function name. - * @param {Reference} ref The reference to check. + * Determine if an identifier is referencing an enclosing function name. + * @param {Reference} ref - The reference to check. + * @param {ASTNode[]} nodes - The candidate function nodes. * @returns {boolean} True if it's a self-reference, false if not. * @private */ - function isSelfReference(ref) { + function isSelfReference(ref, nodes) { + var scope = ref.from; + + while (scope != null) { + if (nodes.indexOf(scope.block) >= 0) { + return true; + } - if (ref.from.type === "function" && ref.from.block.id) { - return ref.identifier.name === ref.from.block.id.name; + scope = scope.upper; } return false; } /** - * Determines if a reference should be counted as a read. A reference should - * be counted only if it's a read and it's not a reference to the containing - * function declaration name. - * @param {Reference} ref The reference to check. - * @returns {boolean} True if it's a value read reference, false if not. + * Determines if the variable is used. + * @param {Variable} variable - The variable to check. + * @param {Reference[]} references - The variable references to check. + * @returns {boolean} True if the variable is used + */ + function isUsedVariable(variable, references) { + var functionNodes = variable.defs.filter(function (def) { + return def.type === "FunctionName"; + }).map(function (def) { + return def.node; + }), + isFunctionDefinition = functionNodes.length > 0; + + return references.some(function (ref) { + return isReadRef(ref) && !(isFunctionDefinition && isSelfReference(ref, functionNodes)); + }); + } + + /** + * Gets unresolved references. + * They contains var's, function's, and explicit global variable's. + * If `config.vars` is not "all", returns empty map. + * @param {Scope} scope - the global scope. + * @returns {object} Unresolved references. Keys of the object is its variable name. Values of the object is an array of its references. * @private */ - function isValidReadRef(ref) { - return isReadRef(ref) && !isSelfReference(ref); + function collectUnresolvedReferences(scope) { + var unresolvedRefs = Object.create(null); + + if (config.vars === "all") { + for (var i = 0, l = scope.through.length; i < l; ++i) { + var ref = scope.through[i]; + var name = ref.identifier.name; + + if (isReadRef(ref)) { + if (!unresolvedRefs[name]) { + unresolvedRefs[name] = []; + } + unresolvedRefs[name].push(ref); + } + } + } + + return unresolvedRefs; } /** - * Gets an array of local variables without read references. - * @param {Scope} scope - an escope Scope object - * @returns {Variable[]} most of the local variables with no read references + * Gets an array of variables without read references. + * @param {Scope} scope - an escope Scope object. + * @param {object} unresolvedRefs - a map of each variable name and its references. + * @param {Variable[]} unusedVars - an array that saving result. + * @returns {Variable[]} unused variables of the scope and descendant scopes. * @private */ - function getUnusedLocals(scope) { - var unused = []; + function collectUnusedVariables(scope, unresolvedRefs, unusedVars) { var variables = scope.variables; + var childScopes = scope.childScopes; + var i, l; - if (scope.type !== "global" && scope.type !== "TDZ") { - for (var i = 0, l = variables.length; i < l; ++i) { + if (scope.type !== "TDZ" && (scope.type !== "global" || config.vars === "all")) { + for (i = 0, l = variables.length; i < l; ++i) { + var variable = variables[i]; + // skip a variable of class itself name in the class scope + if (scope.type === "class" && scope.block.id === variable.identifiers[0]) { + continue; + } // skip function expression names - if (scope.functionExpressionScope || variables[i].eslintUsed) { + if (scope.functionExpressionScope || variable.eslintUsed) { continue; } // skip implicit "arguments" variable - if (scope.type === "function" && variables[i].name === "arguments" && variables[i].identifiers.length === 0) { + if (scope.type === "function" && variable.name === "arguments" && variable.identifiers.length === 0) { continue; } - var def = variables[i].defs[0], - type = def.type; + // explicit global variables don't have definitions. + var def = variable.defs[0]; + if (def != null) { + var type = def.type; - // skip catch variables - if (type === "CatchClause") { - continue; - } + // skip catch variables + if (type === "CatchClause") { + continue; + } - // skip any setter argument - if (type === "Parameter" && def.node.parent.type === "Property" && def.node.parent.kind === "set") { - continue; - } + // skip any setter argument + if (type === "Parameter" && def.node.parent.type === "Property" && def.node.parent.kind === "set") { + continue; + } - // if "args" option is "none", skip any parameter - if (config.args === "none" && type === "Parameter") { - continue; - } + // if "args" option is "none", skip any parameter + if (config.args === "none" && type === "Parameter") { + continue; + } - // if "args" option is "after-used", skip all but the last parameter - if (config.args === "after-used" && type === "Parameter" && variables[i].defs[0].index < variables[i].defs[0].node.params.length - 1) { - continue; + // if "args" option is "after-used", skip all but the last parameter + if (config.args === "after-used" && type === "Parameter" && def.index < def.node.params.length - 1) { + continue; + } } - if (variables[i].references.filter(isValidReadRef).length === 0 && !isExported(variables[i])) { - unused.push(variables[i]); + // On global, variables without let/const/class are unresolved. + var references = (scope.type === "global" ? unresolvedRefs[variable.name] : null) || variable.references; + if (!isUsedVariable(variable, references) && !isExported(variable)) { + unusedVars.push(variable); } } } - return [].concat.apply(unused, scope.childScopes.map(getUnusedLocals)); + for (i = 0, l = childScopes.length; i < l; ++i) { + collectUnusedVariables(childScopes[i], unresolvedRefs, unusedVars); + } + + return unusedVars; } + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + return { "Program:exit": function(programNode) { var globalScope = context.getScope(); - var unused = getUnusedLocals(globalScope); - var i, l; - - // determine unused globals - if (config.vars === "all") { - var unresolvedRefs = globalScope.through.filter(isValidReadRef).map(function(ref) { - return ref.identifier.name; - }); - - for (i = 0, l = globalScope.variables.length; i < l; ++i) { - if (unresolvedRefs.indexOf(globalScope.variables[i].name) < 0 && - !globalScope.variables[i].eslintUsed && !isExported(globalScope.variables[i])) { - unused.push(globalScope.variables[i]); - } - } - } + var unresolvedRefs = collectUnresolvedReferences(globalScope); + var unusedVars = collectUnusedVariables(globalScope, unresolvedRefs, []); - for (i = 0, l = unused.length; i < l; ++i) { - if (unused[i].eslintExplicitGlobal) { - context.report(programNode, MESSAGE, unused[i]); - } else if (unused[i].defs.length > 0) { - - // TODO: Remove when https://github.com/estools/escope/issues/49 is resolved - if (unused[i].defs[0].type === "ClassName") { - continue; - } + for (var i = 0, l = unusedVars.length; i < l; ++i) { + var unusedVar = unusedVars[i]; - context.report(unused[i].identifiers[0], MESSAGE, unused[i]); + if (unusedVar.eslintExplicitGlobal) { + context.report(programNode, MESSAGE, unusedVar); + } else if (unusedVar.defs.length > 0) { + context.report(unusedVar.identifiers[0], MESSAGE, unusedVar); } } } }; }; + +module.exports.schema = [ + { + "oneOf": [ + { + "enum": ["all", "local"] + }, + { + "type": "object", + "properties": { + "vars": { + "enum": ["all", "local"] + }, + "args": { + "enum": ["all", "after-used", "none"] + } + } + } + ] + } +]; |