[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview Rule to flag use of variables before they are defined
|
---|
| 3 | * @author Ilya Volodin
|
---|
| 4 | */
|
---|
| 5 |
|
---|
| 6 | "use strict";
|
---|
| 7 |
|
---|
| 8 | //------------------------------------------------------------------------------
|
---|
| 9 | // Helpers
|
---|
| 10 | //------------------------------------------------------------------------------
|
---|
| 11 |
|
---|
| 12 | const SENTINEL_TYPE = /^(?:(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|CatchClause|ImportDeclaration|ExportNamedDeclaration)$/u;
|
---|
| 13 | const FOR_IN_OF_TYPE = /^For(?:In|Of)Statement$/u;
|
---|
| 14 |
|
---|
| 15 | /**
|
---|
| 16 | * Parses a given value as options.
|
---|
| 17 | * @param {any} options A value to parse.
|
---|
| 18 | * @returns {Object} The parsed options.
|
---|
| 19 | */
|
---|
| 20 | function parseOptions(options) {
|
---|
| 21 | let functions = true;
|
---|
| 22 | let classes = true;
|
---|
| 23 | let variables = true;
|
---|
| 24 | let allowNamedExports = false;
|
---|
| 25 |
|
---|
| 26 | if (typeof options === "string") {
|
---|
| 27 | functions = (options !== "nofunc");
|
---|
| 28 | } else if (typeof options === "object" && options !== null) {
|
---|
| 29 | functions = options.functions !== false;
|
---|
| 30 | classes = options.classes !== false;
|
---|
| 31 | variables = options.variables !== false;
|
---|
| 32 | allowNamedExports = !!options.allowNamedExports;
|
---|
| 33 | }
|
---|
| 34 |
|
---|
| 35 | return { functions, classes, variables, allowNamedExports };
|
---|
| 36 | }
|
---|
| 37 |
|
---|
| 38 | /**
|
---|
| 39 | * Checks whether or not a given location is inside of the range of a given node.
|
---|
| 40 | * @param {ASTNode} node An node to check.
|
---|
| 41 | * @param {number} location A location to check.
|
---|
| 42 | * @returns {boolean} `true` if the location is inside of the range of the node.
|
---|
| 43 | */
|
---|
| 44 | function isInRange(node, location) {
|
---|
| 45 | return node && node.range[0] <= location && location <= node.range[1];
|
---|
| 46 | }
|
---|
| 47 |
|
---|
| 48 | /**
|
---|
| 49 | * Checks whether or not a given location is inside of the range of a class static initializer.
|
---|
| 50 | * Static initializers are static blocks and initializers of static fields.
|
---|
| 51 | * @param {ASTNode} node `ClassBody` node to check static initializers.
|
---|
| 52 | * @param {number} location A location to check.
|
---|
| 53 | * @returns {boolean} `true` if the location is inside of a class static initializer.
|
---|
| 54 | */
|
---|
| 55 | function isInClassStaticInitializerRange(node, location) {
|
---|
| 56 | return node.body.some(classMember => (
|
---|
| 57 | (
|
---|
| 58 | classMember.type === "StaticBlock" &&
|
---|
| 59 | isInRange(classMember, location)
|
---|
| 60 | ) ||
|
---|
| 61 | (
|
---|
| 62 | classMember.type === "PropertyDefinition" &&
|
---|
| 63 | classMember.static &&
|
---|
| 64 | classMember.value &&
|
---|
| 65 | isInRange(classMember.value, location)
|
---|
| 66 | )
|
---|
| 67 | ));
|
---|
| 68 | }
|
---|
| 69 |
|
---|
| 70 | /**
|
---|
| 71 | * Checks whether a given scope is the scope of a class static initializer.
|
---|
| 72 | * Static initializers are static blocks and initializers of static fields.
|
---|
| 73 | * @param {eslint-scope.Scope} scope A scope to check.
|
---|
| 74 | * @returns {boolean} `true` if the scope is a class static initializer scope.
|
---|
| 75 | */
|
---|
| 76 | function isClassStaticInitializerScope(scope) {
|
---|
| 77 | if (scope.type === "class-static-block") {
|
---|
| 78 | return true;
|
---|
| 79 | }
|
---|
| 80 |
|
---|
| 81 | if (scope.type === "class-field-initializer") {
|
---|
| 82 |
|
---|
| 83 | // `scope.block` is PropertyDefinition#value node
|
---|
| 84 | const propertyDefinition = scope.block.parent;
|
---|
| 85 |
|
---|
| 86 | return propertyDefinition.static;
|
---|
| 87 | }
|
---|
| 88 |
|
---|
| 89 | return false;
|
---|
| 90 | }
|
---|
| 91 |
|
---|
| 92 | /**
|
---|
| 93 | * Checks whether a given reference is evaluated in an execution context
|
---|
| 94 | * that isn't the one where the variable it refers to is defined.
|
---|
| 95 | * Execution contexts are:
|
---|
| 96 | * - top-level
|
---|
| 97 | * - functions
|
---|
| 98 | * - class field initializers (implicit functions)
|
---|
| 99 | * - class static blocks (implicit functions)
|
---|
| 100 | * Static class field initializers and class static blocks are automatically run during the class definition evaluation,
|
---|
| 101 | * and therefore we'll consider them as a part of the parent execution context.
|
---|
| 102 | * Example:
|
---|
| 103 | *
|
---|
| 104 | * const x = 1;
|
---|
| 105 | *
|
---|
| 106 | * x; // returns `false`
|
---|
| 107 | * () => x; // returns `true`
|
---|
| 108 | *
|
---|
| 109 | * class C {
|
---|
| 110 | * field = x; // returns `true`
|
---|
| 111 | * static field = x; // returns `false`
|
---|
| 112 | *
|
---|
| 113 | * method() {
|
---|
| 114 | * x; // returns `true`
|
---|
| 115 | * }
|
---|
| 116 | *
|
---|
| 117 | * static method() {
|
---|
| 118 | * x; // returns `true`
|
---|
| 119 | * }
|
---|
| 120 | *
|
---|
| 121 | * static {
|
---|
| 122 | * x; // returns `false`
|
---|
| 123 | * }
|
---|
| 124 | * }
|
---|
| 125 | * @param {eslint-scope.Reference} reference A reference to check.
|
---|
| 126 | * @returns {boolean} `true` if the reference is from a separate execution context.
|
---|
| 127 | */
|
---|
| 128 | function isFromSeparateExecutionContext(reference) {
|
---|
| 129 | const variable = reference.resolved;
|
---|
| 130 | let scope = reference.from;
|
---|
| 131 |
|
---|
| 132 | // Scope#variableScope represents execution context
|
---|
| 133 | while (variable.scope.variableScope !== scope.variableScope) {
|
---|
| 134 | if (isClassStaticInitializerScope(scope.variableScope)) {
|
---|
| 135 | scope = scope.variableScope.upper;
|
---|
| 136 | } else {
|
---|
| 137 | return true;
|
---|
| 138 | }
|
---|
| 139 | }
|
---|
| 140 |
|
---|
| 141 | return false;
|
---|
| 142 | }
|
---|
| 143 |
|
---|
| 144 | /**
|
---|
| 145 | * Checks whether or not a given reference is evaluated during the initialization of its variable.
|
---|
| 146 | *
|
---|
| 147 | * This returns `true` in the following cases:
|
---|
| 148 | *
|
---|
| 149 | * var a = a
|
---|
| 150 | * var [a = a] = list
|
---|
| 151 | * var {a = a} = obj
|
---|
| 152 | * for (var a in a) {}
|
---|
| 153 | * for (var a of a) {}
|
---|
| 154 | * var C = class { [C]; };
|
---|
| 155 | * var C = class { static foo = C; };
|
---|
| 156 | * var C = class { static { foo = C; } };
|
---|
| 157 | * class C extends C {}
|
---|
| 158 | * class C extends (class { static foo = C; }) {}
|
---|
| 159 | * class C { [C]; }
|
---|
| 160 | * @param {Reference} reference A reference to check.
|
---|
| 161 | * @returns {boolean} `true` if the reference is evaluated during the initialization.
|
---|
| 162 | */
|
---|
| 163 | function isEvaluatedDuringInitialization(reference) {
|
---|
| 164 | if (isFromSeparateExecutionContext(reference)) {
|
---|
| 165 |
|
---|
| 166 | /*
|
---|
| 167 | * Even if the reference appears in the initializer, it isn't evaluated during the initialization.
|
---|
| 168 | * For example, `const x = () => x;` is valid.
|
---|
| 169 | */
|
---|
| 170 | return false;
|
---|
| 171 | }
|
---|
| 172 |
|
---|
| 173 | const location = reference.identifier.range[1];
|
---|
| 174 | const definition = reference.resolved.defs[0];
|
---|
| 175 |
|
---|
| 176 | if (definition.type === "ClassName") {
|
---|
| 177 |
|
---|
| 178 | // `ClassDeclaration` or `ClassExpression`
|
---|
| 179 | const classDefinition = definition.node;
|
---|
| 180 |
|
---|
| 181 | return (
|
---|
| 182 | isInRange(classDefinition, location) &&
|
---|
| 183 |
|
---|
| 184 | /*
|
---|
| 185 | * Class binding is initialized before running static initializers.
|
---|
| 186 | * For example, `class C { static foo = C; static { bar = C; } }` is valid.
|
---|
| 187 | */
|
---|
| 188 | !isInClassStaticInitializerRange(classDefinition.body, location)
|
---|
| 189 | );
|
---|
| 190 | }
|
---|
| 191 |
|
---|
| 192 | let node = definition.name.parent;
|
---|
| 193 |
|
---|
| 194 | while (node) {
|
---|
| 195 | if (node.type === "VariableDeclarator") {
|
---|
| 196 | if (isInRange(node.init, location)) {
|
---|
| 197 | return true;
|
---|
| 198 | }
|
---|
| 199 | if (FOR_IN_OF_TYPE.test(node.parent.parent.type) &&
|
---|
| 200 | isInRange(node.parent.parent.right, location)
|
---|
| 201 | ) {
|
---|
| 202 | return true;
|
---|
| 203 | }
|
---|
| 204 | break;
|
---|
| 205 | } else if (node.type === "AssignmentPattern") {
|
---|
| 206 | if (isInRange(node.right, location)) {
|
---|
| 207 | return true;
|
---|
| 208 | }
|
---|
| 209 | } else if (SENTINEL_TYPE.test(node.type)) {
|
---|
| 210 | break;
|
---|
| 211 | }
|
---|
| 212 |
|
---|
| 213 | node = node.parent;
|
---|
| 214 | }
|
---|
| 215 |
|
---|
| 216 | return false;
|
---|
| 217 | }
|
---|
| 218 |
|
---|
| 219 | //------------------------------------------------------------------------------
|
---|
| 220 | // Rule Definition
|
---|
| 221 | //------------------------------------------------------------------------------
|
---|
| 222 |
|
---|
| 223 | /** @type {import('../shared/types').Rule} */
|
---|
| 224 | module.exports = {
|
---|
| 225 | meta: {
|
---|
| 226 | type: "problem",
|
---|
| 227 |
|
---|
| 228 | docs: {
|
---|
| 229 | description: "Disallow the use of variables before they are defined",
|
---|
| 230 | recommended: false,
|
---|
| 231 | url: "https://eslint.org/docs/latest/rules/no-use-before-define"
|
---|
| 232 | },
|
---|
| 233 |
|
---|
| 234 | schema: [
|
---|
| 235 | {
|
---|
| 236 | oneOf: [
|
---|
| 237 | {
|
---|
| 238 | enum: ["nofunc"]
|
---|
| 239 | },
|
---|
| 240 | {
|
---|
| 241 | type: "object",
|
---|
| 242 | properties: {
|
---|
| 243 | functions: { type: "boolean" },
|
---|
| 244 | classes: { type: "boolean" },
|
---|
| 245 | variables: { type: "boolean" },
|
---|
| 246 | allowNamedExports: { type: "boolean" }
|
---|
| 247 | },
|
---|
| 248 | additionalProperties: false
|
---|
| 249 | }
|
---|
| 250 | ]
|
---|
| 251 | }
|
---|
| 252 | ],
|
---|
| 253 |
|
---|
| 254 | messages: {
|
---|
| 255 | usedBeforeDefined: "'{{name}}' was used before it was defined."
|
---|
| 256 | }
|
---|
| 257 | },
|
---|
| 258 |
|
---|
| 259 | create(context) {
|
---|
| 260 | const options = parseOptions(context.options[0]);
|
---|
| 261 | const sourceCode = context.sourceCode;
|
---|
| 262 |
|
---|
| 263 | /**
|
---|
| 264 | * Determines whether a given reference should be checked.
|
---|
| 265 | *
|
---|
| 266 | * Returns `false` if the reference is:
|
---|
| 267 | * - initialization's (e.g., `let a = 1`).
|
---|
| 268 | * - referring to an undefined variable (i.e., if it's an unresolved reference).
|
---|
| 269 | * - referring to a variable that is defined, but not in the given source code
|
---|
| 270 | * (e.g., global environment variable or `arguments` in functions).
|
---|
| 271 | * - allowed by options.
|
---|
| 272 | * @param {eslint-scope.Reference} reference The reference
|
---|
| 273 | * @returns {boolean} `true` if the reference should be checked
|
---|
| 274 | */
|
---|
| 275 | function shouldCheck(reference) {
|
---|
| 276 | if (reference.init) {
|
---|
| 277 | return false;
|
---|
| 278 | }
|
---|
| 279 |
|
---|
| 280 | const { identifier } = reference;
|
---|
| 281 |
|
---|
| 282 | if (
|
---|
| 283 | options.allowNamedExports &&
|
---|
| 284 | identifier.parent.type === "ExportSpecifier" &&
|
---|
| 285 | identifier.parent.local === identifier
|
---|
| 286 | ) {
|
---|
| 287 | return false;
|
---|
| 288 | }
|
---|
| 289 |
|
---|
| 290 | const variable = reference.resolved;
|
---|
| 291 |
|
---|
| 292 | if (!variable || variable.defs.length === 0) {
|
---|
| 293 | return false;
|
---|
| 294 | }
|
---|
| 295 |
|
---|
| 296 | const definitionType = variable.defs[0].type;
|
---|
| 297 |
|
---|
| 298 | if (!options.functions && definitionType === "FunctionName") {
|
---|
| 299 | return false;
|
---|
| 300 | }
|
---|
| 301 |
|
---|
| 302 | if (
|
---|
| 303 | (
|
---|
| 304 | !options.variables && definitionType === "Variable" ||
|
---|
| 305 | !options.classes && definitionType === "ClassName"
|
---|
| 306 | ) &&
|
---|
| 307 |
|
---|
| 308 | // don't skip checking the reference if it's in the same execution context, because of TDZ
|
---|
| 309 | isFromSeparateExecutionContext(reference)
|
---|
| 310 | ) {
|
---|
| 311 | return false;
|
---|
| 312 | }
|
---|
| 313 |
|
---|
| 314 | return true;
|
---|
| 315 | }
|
---|
| 316 |
|
---|
| 317 | /**
|
---|
| 318 | * Finds and validates all references in a given scope and its child scopes.
|
---|
| 319 | * @param {eslint-scope.Scope} scope The scope object.
|
---|
| 320 | * @returns {void}
|
---|
| 321 | */
|
---|
| 322 | function checkReferencesInScope(scope) {
|
---|
| 323 | scope.references.filter(shouldCheck).forEach(reference => {
|
---|
| 324 | const variable = reference.resolved;
|
---|
| 325 | const definitionIdentifier = variable.defs[0].name;
|
---|
| 326 |
|
---|
| 327 | if (
|
---|
| 328 | reference.identifier.range[1] < definitionIdentifier.range[1] ||
|
---|
| 329 | isEvaluatedDuringInitialization(reference)
|
---|
| 330 | ) {
|
---|
| 331 | context.report({
|
---|
| 332 | node: reference.identifier,
|
---|
| 333 | messageId: "usedBeforeDefined",
|
---|
| 334 | data: reference.identifier
|
---|
| 335 | });
|
---|
| 336 | }
|
---|
| 337 | });
|
---|
| 338 |
|
---|
| 339 | scope.childScopes.forEach(checkReferencesInScope);
|
---|
| 340 | }
|
---|
| 341 |
|
---|
| 342 | return {
|
---|
| 343 | Program(node) {
|
---|
| 344 | checkReferencesInScope(sourceCode.getScope(node));
|
---|
| 345 | }
|
---|
| 346 | };
|
---|
| 347 | }
|
---|
| 348 | };
|
---|