|
|
|
/**
|
|
|
|
* @fileoverview Rule to flag use of variables before they are defined
|
|
|
|
* @author Ilya Volodin
|
|
|
|
* @copyright 2013 Ilya Volodin. All rights reserved.
|
|
|
|
*/
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
// Helpers
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
var SENTINEL_TYPE = /^(?:(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|CatchClause|ImportDeclaration|ExportNamedDeclaration)$/;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parses a given value as options.
|
|
|
|
*
|
|
|
|
* @param {any} options - A value to parse.
|
|
|
|
* @returns {object} The parsed options.
|
|
|
|
*/
|
|
|
|
function parseOptions(options) {
|
|
|
|
var functions = true;
|
|
|
|
var classes = true;
|
|
|
|
|
|
|
|
if (typeof options === "string") {
|
|
|
|
functions = (options !== "nofunc");
|
|
|
|
} else if (typeof options === "object" && options !== null) {
|
|
|
|
functions = options.functions !== false;
|
|
|
|
classes = options.classes !== false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {functions: functions, classes: classes};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @returns {boolean} `false`.
|
|
|
|
*/
|
|
|
|
function alwaysFalse() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether or not a given variable is a function declaration.
|
|
|
|
*
|
|
|
|
* @param {escope.Variable} variable - A variable to check.
|
|
|
|
* @returns {boolean} `true` if the variable is a function declaration.
|
|
|
|
*/
|
|
|
|
function isFunction(variable) {
|
|
|
|
return variable.defs[0].type === "FunctionName";
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether or not a given variable is a class declaration in an upper function scope.
|
|
|
|
*
|
|
|
|
* @param {escope.Variable} variable - A variable to check.
|
|
|
|
* @param {escope.Reference} reference - A reference to check.
|
|
|
|
* @returns {boolean} `true` if the variable is a class declaration.
|
|
|
|
*/
|
|
|
|
function isOuterClass(variable, reference) {
|
|
|
|
return (
|
|
|
|
variable.defs[0].type === "ClassName" &&
|
|
|
|
variable.scope.variableScope !== reference.from.variableScope
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether or not a given variable is a function declaration or a class declaration in an upper function scope.
|
|
|
|
*
|
|
|
|
* @param {escope.Variable} variable - A variable to check.
|
|
|
|
* @param {escope.Reference} reference - A reference to check.
|
|
|
|
* @returns {boolean} `true` if the variable is a function declaration or a class declaration.
|
|
|
|
*/
|
|
|
|
function isFunctionOrOuterClass(variable, reference) {
|
|
|
|
return isFunction(variable, reference) || isOuterClass(variable, reference);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether or not a given location is inside of the range of a given node.
|
|
|
|
*
|
|
|
|
* @param {ASTNode} node - An node to check.
|
|
|
|
* @param {number} location - A location to check.
|
|
|
|
* @returns {boolean} `true` if the location is inside of the range of the node.
|
|
|
|
*/
|
|
|
|
function isInRange(node, location) {
|
|
|
|
return node && node.range[0] <= location && location <= node.range[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether or not a given reference is inside of the initializers of a given variable.
|
|
|
|
*
|
|
|
|
* @param {Variable} variable - A variable to check.
|
|
|
|
* @param {Reference} reference - A reference to check.
|
|
|
|
* @returns {boolean} `true` if the reference is inside of the initializers.
|
|
|
|
*/
|
|
|
|
function isInInitializer(variable, reference) {
|
|
|
|
if (variable.scope !== reference.from) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
var node = variable.identifiers[0].parent;
|
|
|
|
var location = reference.identifier.range[1];
|
|
|
|
|
|
|
|
while (node) {
|
|
|
|
if (node.type === "VariableDeclarator") {
|
|
|
|
if (isInRange(node.init, location)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
} else if (node.type === "AssignmentPattern") {
|
|
|
|
if (isInRange(node.right, location)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} else if (SENTINEL_TYPE.test(node.type)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
node = node.parent;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
// Rule Definition
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
module.exports = function(context) {
|
|
|
|
var options = parseOptions(context.options[0]);
|
|
|
|
|
|
|
|
// Defines a function which checks whether or not a reference is allowed according to the option.
|
|
|
|
var isAllowed;
|
|
|
|
|
|
|
|
if (options.functions && options.classes) {
|
|
|
|
isAllowed = alwaysFalse;
|
|
|
|
} else if (options.functions) {
|
|
|
|
isAllowed = isOuterClass;
|
|
|
|
} else if (options.classes) {
|
|
|
|
isAllowed = isFunction;
|
|
|
|
} else {
|
|
|
|
isAllowed = isFunctionOrOuterClass;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Finds and validates all variables in a given scope.
|
|
|
|
* @param {Scope} scope The scope object.
|
|
|
|
* @returns {void}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
function findVariablesInScope(scope) {
|
|
|
|
scope.references.forEach(function(reference) {
|
|
|
|
var variable = reference.resolved;
|
|
|
|
|
|
|
|
// Skips when the reference is:
|
|
|
|
// - initialization's.
|
|
|
|
// - referring to an undefined variable.
|
|
|
|
// - referring to a global environment variable (there're no identifiers).
|
|
|
|
// - located preceded by the variable (except in initializers).
|
|
|
|
// - allowed by options.
|
|
|
|
if (reference.init ||
|
|
|
|
!variable ||
|
|
|
|
variable.identifiers.length === 0 ||
|
|
|
|
(variable.identifiers[0].range[1] < reference.identifier.range[1] && !isInInitializer(variable, reference)) ||
|
|
|
|
isAllowed(variable, reference)
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reports.
|
|
|
|
context.report({
|
|
|
|
node: reference.identifier,
|
|
|
|
message: "'{{name}}' was used before it was defined",
|
|
|
|
data: reference.identifier
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validates variables inside of a node's scope.
|
|
|
|
* @param {ASTNode} node The node to check.
|
|
|
|
* @returns {void}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
function findVariables() {
|
|
|
|
var scope = context.getScope();
|
|
|
|
|
|
|
|
findVariablesInScope(scope);
|
|
|
|
}
|
|
|
|
|
|
|
|
var ruleDefinition = {
|
|
|
|
"Program:exit": function(node) {
|
|
|
|
var scope = context.getScope(),
|
|
|
|
ecmaFeatures = context.parserOptions.ecmaFeatures || {};
|
|
|
|
|
|
|
|
findVariablesInScope(scope);
|
|
|
|
|
|
|
|
// both Node.js and Modules have an extra scope
|
|
|
|
if (ecmaFeatures.globalReturn || node.sourceType === "module") {
|
|
|
|
findVariablesInScope(scope.childScopes[0]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
if (context.parserOptions.ecmaVersion >= 6) {
|
|
|
|
ruleDefinition["BlockStatement:exit"] =
|
|
|
|
ruleDefinition["SwitchStatement:exit"] = findVariables;
|
|
|
|
|
|
|
|
ruleDefinition["ArrowFunctionExpression:exit"] = function(node) {
|
|
|
|
if (node.body.type !== "BlockStatement") {
|
|
|
|
findVariables(node);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
ruleDefinition["FunctionExpression:exit"] =
|
|
|
|
ruleDefinition["FunctionDeclaration:exit"] =
|
|
|
|
ruleDefinition["ArrowFunctionExpression:exit"] = findVariables;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ruleDefinition;
|
|
|
|
};
|
|
|
|
|
|
|
|
module.exports.schema = [
|
|
|
|
{
|
|
|
|
"oneOf": [
|
|
|
|
{
|
|
|
|
"enum": ["nofunc"]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"type": "object",
|
|
|
|
"properties": {
|
|
|
|
"functions": {"type": "boolean"},
|
|
|
|
"classes": {"type": "boolean"}
|
|
|
|
},
|
|
|
|
"additionalProperties": false
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
];
|