230 lines
7.4 KiB
JavaScript
230 lines
7.4 KiB
JavaScript
|
/**
|
||
|
* @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());
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
};
|