|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- /**
- * @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)$/;
- const FOR_IN_OF_TYPE = /^For(?:In|Of)Statement$/;
-
- /**
- * 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: {
- 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
- });
- });
- }
-
- /**
- * Validates variables inside of a node's scope.
- * @param {ASTNode} node The node to check.
- * @returns {void}
- * @private
- */
- function findVariables() {
- const scope = context.getScope();
-
- findVariablesInScope(scope);
- }
-
- const ruleDefinition = {
- "Program:exit"(node) {
- const 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();
- }
- };
- } else {
- ruleDefinition["FunctionExpression:exit"] =
- ruleDefinition["FunctionDeclaration:exit"] =
- ruleDefinition["ArrowFunctionExpression:exit"] = findVariables;
- }
-
- return ruleDefinition;
- }
- };
|