Permalink
Cannot retrieve contributors at this time
| /** | |
| * @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()); | |
| } | |
| }; | |
| } | |
| }; |