blob: a98f6c87ff8c988fc774885c4916f3ce280eb52e [file] [log] [blame]
/**
* @fileoverview Rule to flag use of variables before they are defined
* @author Ilya Volodin
*/
"use strict";
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const SENTINEL_TYPE = /^(?:(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|CatchClause|ImportDeclaration|ExportNamedDeclaration)$/u;
const FOR_IN_OF_TYPE = /^For(?:In|Of)Statement$/u;
/**
* Parses a given value as options.
*
* @param {any} options - A value to parse.
* @returns {Object} The parsed options.
*/
function parseOptions(options) {
let functions = true;
let classes = true;
let variables = true;
if (typeof options === "string") {
functions = (options !== "nofunc");
} else if (typeof options === "object" && options !== null) {
functions = options.functions !== false;
classes = options.classes !== false;
variables = options.variables !== false;
}
return { functions, classes, variables };
}
/**
* Checks whether or not a given variable is a function declaration.
*
* @param {eslint-scope.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 {eslint-scope.Variable} variable - A variable to check.
* @param {eslint-scope.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 variable declaration in an upper function scope.
* @param {eslint-scope.Variable} variable - A variable to check.
* @param {eslint-scope.Reference} reference - A reference to check.
* @returns {boolean} `true` if the variable is a variable declaration.
*/
function isOuterVariable(variable, reference) {
return (
variable.defs[0].type === "Variable" &&
variable.scope.variableScope !== reference.from.variableScope
);
}
/**
* 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.
*
* This returns `true` in the following cases:
*
* var a = a
* var [a = a] = list
* var {a = a} = obj
* for (var a in a) {}
* for (var a of a) {}
*
* @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;
}
let node = variable.identifiers[0].parent;
const location = reference.identifier.range[1];
while (node) {
if (node.type === "VariableDeclarator") {
if (isInRange(node.init, location)) {
return true;
}
if (FOR_IN_OF_TYPE.test(node.parent.parent.type) &&
isInRange(node.parent.parent.right, 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 = {
meta: {
type: "problem",
docs: {
description: "disallow the use of variables before they are defined",
category: "Variables",
recommended: false,
url: "https://eslint.org/docs/rules/no-use-before-define"
},
schema: [
{
oneOf: [
{
enum: ["nofunc"]
},
{
type: "object",
properties: {
functions: { type: "boolean" },
classes: { type: "boolean" },
variables: { type: "boolean" }
},
additionalProperties: false
}
]
}
]
},
create(context) {
const options = parseOptions(context.options[0]);
/**
* Determines whether a given use-before-define case should be reported according to the options.
* @param {eslint-scope.Variable} variable The variable that gets used before being defined
* @param {eslint-scope.Reference} reference The reference to the variable
* @returns {boolean} `true` if the usage should be reported
*/
function isForbidden(variable, reference) {
if (isFunction(variable)) {
return options.functions;
}
if (isOuterClass(variable, reference)) {
return options.classes;
}
if (isOuterVariable(variable, reference)) {
return options.variables;
}
return true;
}
/**
* Finds and validates all variables in a given scope.
* @param {Scope} scope The scope object.
* @returns {void}
* @private
*/
function findVariablesInScope(scope) {
scope.references.forEach(reference => {
const 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)) ||
!isForbidden(variable, reference)
) {
return;
}
// Reports.
context.report({
node: reference.identifier,
message: "'{{name}}' was used before it was defined.",
data: reference.identifier
});
});
scope.childScopes.forEach(findVariablesInScope);
}
return {
Program() {
findVariablesInScope(context.getScope());
}
};
}
};