You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

181 lines
6.3 KiB

/**
* @fileoverview Rule to flag on declaring variables already declared in the outer scope
* @author Ilya Volodin
* @copyright 2013 Ilya Volodin. All rights reserved.
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = function(context) {
var options = {
hoist: (context.options[0] && context.options[0].hoist) || "functions"
};
/**
* Checks if a variable of the class name in the class scope of ClassDeclaration.
*
* ClassDeclaration creates two variables of its name into its outer scope and its class scope.
* So we should ignore the variable in the class scope.
*
* @param {Object} variable The variable to check.
* @returns {boolean} Whether or not the variable of the class name in the class scope of ClassDeclaration.
*/
function isDuplicatedClassNameVariable(variable) {
var block = variable.scope.block;
return block.type === "ClassDeclaration" && block.id === variable.identifiers[0];
}
/**
* Checks if a variable is inside the initializer of scopeVar.
*
* To avoid reporting at declarations such as `var a = function a() {};`.
* But it should report `var a = function(a) {};` or `var a = function() { function a() {} };`.
*
* @param {Object} variable The variable to check.
* @param {Object} scopeVar The scope variable to look for.
* @returns {boolean} Whether or not the variable is inside initializer of scopeVar.
*/
function isOnInitializer(variable, scopeVar) {
var outerScope = scopeVar.scope;
var outerDef = scopeVar.defs[0];
var outer = outerDef && outerDef.parent && outerDef.parent.range;
var innerScope = variable.scope;
var innerDef = variable.defs[0];
var inner = innerDef && innerDef.name.range;
return (
outer != null &&
inner != null &&
outer[0] < inner[0] &&
inner[1] < outer[1] &&
((innerDef.type === "FunctionName" && innerDef.node.type === "FunctionExpression") || innerDef.node.type === "ClassExpression") &&
outerScope === innerScope.upper
);
}
/**
* Get a range of a variable's identifier node.
* @param {Object} variable The variable to get.
* @returns {Array|undefined} The range of the variable's identifier node.
*/
function getNameRange(variable) {
var def = variable.defs[0];
return def && def.name.range;
}
/**
* Checks if a variable is in TDZ of scopeVar.
* @param {Object} variable The variable to check.
* @param {Object} scopeVar The variable of TDZ.
* @returns {boolean} Whether or not the variable is in TDZ of scopeVar.
*/
function isInTdz(variable, scopeVar) {
var outerDef = scopeVar.defs[0];
var inner = getNameRange(variable);
var outer = getNameRange(scopeVar);
return (
inner != null &&
outer != null &&
inner[1] < outer[0] &&
// Excepts FunctionDeclaration if is {"hoist":"function"}.
(options.hoist !== "functions" || outerDef == null || outerDef.node.type !== "FunctionDeclaration")
);
}
/**
* Checks if a variable is contained in the list of given scope variables.
* @param {Object} variable The variable to check.
* @param {Array} scopeVars The scope variables to look for.
* @returns {boolean} Whether or not the variable is contains in the list of scope variables.
*/
function isContainedInScopeVars(variable, scopeVars) {
return scopeVars.some(function (scopeVar) {
return (
scopeVar.identifiers.length > 0 &&
variable.name === scopeVar.name &&
!isDuplicatedClassNameVariable(scopeVar) &&
!isOnInitializer(variable, scopeVar) &&
!(options.hoist !== "all" && isInTdz(variable, scopeVar))
);
});
}
/**
* Checks if the given variables are shadowed in the given scope.
* @param {Array} variables The variables to look for
* @param {Object} scope The scope to be checked.
* @returns {Array} Variables which are not declared in the given scope.
*/
function checkShadowsInScope(variables, scope) {
var passedVars = [];
variables.forEach(function (variable) {
// "arguments" is a special case that has no identifiers (#1759)
if (variable.identifiers.length > 0 && isContainedInScopeVars(variable, scope.variables)) {
context.report(
variable.identifiers[0],
"{{name}} is already declared in the upper scope.",
{name: variable.name});
} else {
passedVars.push(variable);
}
});
return passedVars;
}
/**
* Checks the current context for shadowed variables.
* @param {Scope} scope - Fixme
* @returns {void}
*/
function checkForShadows(scope) {
var variables = scope.variables.filter(function(variable) {
return (
// Skip "arguments".
variable.identifiers.length > 0 &&
// Skip variables of a class name in the class scope of ClassDeclaration.
!isDuplicatedClassNameVariable(variable)
);
});
// iterate through the array of variables and find duplicates with the upper scope
var upper = scope.upper;
while (upper && variables.length) {
variables = checkShadowsInScope(variables, upper);
upper = upper.upper;
}
}
return {
"Program:exit": function () {
var globalScope = context.getScope(),
stack = globalScope.childScopes.slice(),
scope;
while (stack.length) {
scope = stack.pop();
stack.push.apply(stack, scope.childScopes);
checkForShadows(scope);
}
}
};
};
module.exports.schema = [
{
"type": "object",
"properties": {
"hoist": {
"enum": ["all", "functions", "never"]
}
}
}
];