| /** | |
|  * @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; | |
|     let allowNamedExports = false; | |
| 
 | |
|     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; | |
|         allowNamedExports = !!options.allowNamedExports; | |
|     } | |
| 
 | |
|     return { functions, classes, variables, allowNamedExports }; | |
| } | |
| 
 | |
| /** | |
|  * 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 location is inside of the range of a class static initializer. | |
|  * Static initializers are static blocks and initializers of static fields. | |
|  * @param {ASTNode} node `ClassBody` node to check static initializers. | |
|  * @param {number} location A location to check. | |
|  * @returns {boolean} `true` if the location is inside of a class static initializer. | |
|  */ | |
| function isInClassStaticInitializerRange(node, location) { | |
|     return node.body.some(classMember => ( | |
|         ( | |
|             classMember.type === "StaticBlock" && | |
|             isInRange(classMember, location) | |
|         ) || | |
|         ( | |
|             classMember.type === "PropertyDefinition" && | |
|             classMember.static && | |
|             classMember.value && | |
|             isInRange(classMember.value, location) | |
|         ) | |
|     )); | |
| } | |
| 
 | |
| /** | |
|  * Checks whether a given scope is the scope of a class static initializer. | |
|  * Static initializers are static blocks and initializers of static fields. | |
|  * @param {eslint-scope.Scope} scope A scope to check. | |
|  * @returns {boolean} `true` if the scope is a class static initializer scope. | |
|  */ | |
| function isClassStaticInitializerScope(scope) { | |
|     if (scope.type === "class-static-block") { | |
|         return true; | |
|     } | |
| 
 | |
|     if (scope.type === "class-field-initializer") { | |
| 
 | |
|         // `scope.block` is PropertyDefinition#value node | |
|         const propertyDefinition = scope.block.parent; | |
| 
 | |
|         return propertyDefinition.static; | |
|     } | |
| 
 | |
|     return false; | |
| } | |
| 
 | |
| /** | |
|  * Checks whether a given reference is evaluated in an execution context | |
|  * that isn't the one where the variable it refers to is defined. | |
|  * Execution contexts are: | |
|  * - top-level | |
|  * - functions | |
|  * - class field initializers (implicit functions) | |
|  * - class static blocks (implicit functions) | |
|  * Static class field initializers and class static blocks are automatically run during the class definition evaluation, | |
|  * and therefore we'll consider them as a part of the parent execution context. | |
|  * Example: | |
|  * | |
|  *   const x = 1; | |
|  * | |
|  *   x; // returns `false` | |
|  *   () => x; // returns `true` | |
|  * | |
|  *   class C { | |
|  *       field = x; // returns `true` | |
|  *       static field = x; // returns `false` | |
|  * | |
|  *       method() { | |
|  *           x; // returns `true` | |
|  *       } | |
|  * | |
|  *       static method() { | |
|  *           x; // returns `true` | |
|  *       } | |
|  * | |
|  *       static { | |
|  *           x; // returns `false` | |
|  *       } | |
|  *   } | |
|  * @param {eslint-scope.Reference} reference A reference to check. | |
|  * @returns {boolean} `true` if the reference is from a separate execution context. | |
|  */ | |
| function isFromSeparateExecutionContext(reference) { | |
|     const variable = reference.resolved; | |
|     let scope = reference.from; | |
| 
 | |
|     // Scope#variableScope represents execution context | |
|     while (variable.scope.variableScope !== scope.variableScope) { | |
|         if (isClassStaticInitializerScope(scope.variableScope)) { | |
|             scope = scope.variableScope.upper; | |
|         } else { | |
|             return true; | |
|         } | |
|     } | |
| 
 | |
|     return false; | |
| } | |
| 
 | |
| /** | |
|  * Checks whether or not a given reference is evaluated during the initialization of its 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) {} | |
|  *     var C = class { [C]; }; | |
|  *     var C = class { static foo = C; }; | |
|  *     var C = class { static { foo = C; } }; | |
|  *     class C extends C {} | |
|  *     class C extends (class { static foo = C; }) {} | |
|  *     class C { [C]; } | |
|  * @param {Reference} reference A reference to check. | |
|  * @returns {boolean} `true` if the reference is evaluated during the initialization. | |
|  */ | |
| function isEvaluatedDuringInitialization(reference) { | |
|     if (isFromSeparateExecutionContext(reference)) { | |
| 
 | |
|         /* | |
|          * Even if the reference appears in the initializer, it isn't evaluated during the initialization. | |
|          * For example, `const x = () => x;` is valid. | |
|          */ | |
|         return false; | |
|     } | |
| 
 | |
|     const location = reference.identifier.range[1]; | |
|     const definition = reference.resolved.defs[0]; | |
| 
 | |
|     if (definition.type === "ClassName") { | |
| 
 | |
|         // `ClassDeclaration` or `ClassExpression` | |
|         const classDefinition = definition.node; | |
| 
 | |
|         return ( | |
|             isInRange(classDefinition, location) && | |
| 
 | |
|             /* | |
|              * Class binding is initialized before running static initializers. | |
|              * For example, `class C { static foo = C; static { bar = C; } }` is valid. | |
|              */ | |
|             !isInClassStaticInitializerRange(classDefinition.body, location) | |
|         ); | |
|     } | |
| 
 | |
|     let node = definition.name.parent; | |
| 
 | |
|     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 | |
| //------------------------------------------------------------------------------ | |
|  | |
| /** @type {import('../shared/types').Rule} */ | |
| module.exports = { | |
|     meta: { | |
|         type: "problem", | |
| 
 | |
|         docs: { | |
|             description: "Disallow the use of variables before they are defined", | |
|             recommended: false, | |
|             url: "https://eslint.org/docs/latest/rules/no-use-before-define" | |
|         }, | |
| 
 | |
|         schema: [ | |
|             { | |
|                 oneOf: [ | |
|                     { | |
|                         enum: ["nofunc"] | |
|                     }, | |
|                     { | |
|                         type: "object", | |
|                         properties: { | |
|                             functions: { type: "boolean" }, | |
|                             classes: { type: "boolean" }, | |
|                             variables: { type: "boolean" }, | |
|                             allowNamedExports: { type: "boolean" } | |
|                         }, | |
|                         additionalProperties: false | |
|                     } | |
|                 ] | |
|             } | |
|         ], | |
| 
 | |
|         messages: { | |
|             usedBeforeDefined: "'{{name}}' was used before it was defined." | |
|         } | |
|     }, | |
| 
 | |
|     create(context) { | |
|         const options = parseOptions(context.options[0]); | |
|         const sourceCode = context.sourceCode; | |
| 
 | |
|         /** | |
|          * Determines whether a given reference should be checked. | |
|          * | |
|          * Returns `false` if the reference is: | |
|          * - initialization's (e.g., `let a = 1`). | |
|          * - referring to an undefined variable (i.e., if it's an unresolved reference). | |
|          * - referring to a variable that is defined, but not in the given source code | |
|          *   (e.g., global environment variable or `arguments` in functions). | |
|          * - allowed by options. | |
|          * @param {eslint-scope.Reference} reference The reference | |
|          * @returns {boolean} `true` if the reference should be checked | |
|          */ | |
|         function shouldCheck(reference) { | |
|             if (reference.init) { | |
|                 return false; | |
|             } | |
| 
 | |
|             const { identifier } = reference; | |
| 
 | |
|             if ( | |
|                 options.allowNamedExports && | |
|                 identifier.parent.type === "ExportSpecifier" && | |
|                 identifier.parent.local === identifier | |
|             ) { | |
|                 return false; | |
|             } | |
| 
 | |
|             const variable = reference.resolved; | |
| 
 | |
|             if (!variable || variable.defs.length === 0) { | |
|                 return false; | |
|             } | |
| 
 | |
|             const definitionType = variable.defs[0].type; | |
| 
 | |
|             if (!options.functions && definitionType === "FunctionName") { | |
|                 return false; | |
|             } | |
| 
 | |
|             if ( | |
|                 ( | |
|                     !options.variables && definitionType === "Variable" || | |
|                     !options.classes && definitionType === "ClassName" | |
|                 ) && | |
| 
 | |
|                 // don't skip checking the reference if it's in the same execution context, because of TDZ | |
|                 isFromSeparateExecutionContext(reference) | |
|             ) { | |
|                 return false; | |
|             } | |
| 
 | |
|             return true; | |
|         } | |
| 
 | |
|         /** | |
|          * Finds and validates all references in a given scope and its child scopes. | |
|          * @param {eslint-scope.Scope} scope The scope object. | |
|          * @returns {void} | |
|          */ | |
|         function checkReferencesInScope(scope) { | |
|             scope.references.filter(shouldCheck).forEach(reference => { | |
|                 const variable = reference.resolved; | |
|                 const definitionIdentifier = variable.defs[0].name; | |
| 
 | |
|                 if ( | |
|                     reference.identifier.range[1] < definitionIdentifier.range[1] || | |
|                     isEvaluatedDuringInitialization(reference) | |
|                 ) { | |
|                     context.report({ | |
|                         node: reference.identifier, | |
|                         messageId: "usedBeforeDefined", | |
|                         data: reference.identifier | |
|                     }); | |
|                 } | |
|             }); | |
| 
 | |
|             scope.childScopes.forEach(checkReferencesInScope); | |
|         } | |
| 
 | |
|         return { | |
|             Program(node) { | |
|                 checkReferencesInScope(sourceCode.getScope(node)); | |
|             } | |
|         }; | |
|     } | |
| };
 |