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 12const SENTINEL_TYPE = /^(?:(?:Function|Class)(?:Declaration|Expression)|ArrowFunctionExpression|CatchClause|ImportDeclaration|ExportNamedDeclaration)$/u; 13const 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 */ 20function parseOptions(options) { 21 let functions = true; 22 let classes = true; 23 let variables = true; 24 25 if (typeof options === "string") { 26 functions = (options !== "nofunc"); 27 } else if (typeof options === "object" && options !== null) { 28 functions = options.functions !== false; 29 classes = options.classes !== false; 30 variables = options.variables !== false; 31 } 32 33 return { functions, classes, variables }; 34} 35 36/** 37 * Checks whether or not a given variable is a function declaration. 38 * @param {eslint-scope.Variable} variable A variable to check. 39 * @returns {boolean} `true` if the variable is a function declaration. 40 */ 41function isFunction(variable) { 42 return variable.defs[0].type === "FunctionName"; 43} 44 45/** 46 * Checks whether or not a given variable is a class declaration in an upper function scope. 47 * @param {eslint-scope.Variable} variable A variable to check. 48 * @param {eslint-scope.Reference} reference A reference to check. 49 * @returns {boolean} `true` if the variable is a class declaration. 50 */ 51function isOuterClass(variable, reference) { 52 return ( 53 variable.defs[0].type === "ClassName" && 54 variable.scope.variableScope !== reference.from.variableScope 55 ); 56} 57 58/** 59 * Checks whether or not a given variable is a variable declaration in an upper function scope. 60 * @param {eslint-scope.Variable} variable A variable to check. 61 * @param {eslint-scope.Reference} reference A reference to check. 62 * @returns {boolean} `true` if the variable is a variable declaration. 63 */ 64function isOuterVariable(variable, reference) { 65 return ( 66 variable.defs[0].type === "Variable" && 67 variable.scope.variableScope !== reference.from.variableScope 68 ); 69} 70 71/** 72 * Checks whether or not a given location is inside of the range of a given node. 73 * @param {ASTNode} node An node to check. 74 * @param {number} location A location to check. 75 * @returns {boolean} `true` if the location is inside of the range of the node. 76 */ 77function isInRange(node, location) { 78 return node && node.range[0] <= location && location <= node.range[1]; 79} 80 81/** 82 * Checks whether or not a given reference is inside of the initializers of a given variable. 83 * 84 * This returns `true` in the following cases: 85 * 86 * var a = a 87 * var [a = a] = list 88 * var {a = a} = obj 89 * for (var a in a) {} 90 * for (var a of a) {} 91 * @param {Variable} variable A variable to check. 92 * @param {Reference} reference A reference to check. 93 * @returns {boolean} `true` if the reference is inside of the initializers. 94 */ 95function isInInitializer(variable, reference) { 96 if (variable.scope !== reference.from) { 97 return false; 98 } 99 100 let node = variable.identifiers[0].parent; 101 const location = reference.identifier.range[1]; 102 103 while (node) { 104 if (node.type === "VariableDeclarator") { 105 if (isInRange(node.init, location)) { 106 return true; 107 } 108 if (FOR_IN_OF_TYPE.test(node.parent.parent.type) && 109 isInRange(node.parent.parent.right, location) 110 ) { 111 return true; 112 } 113 break; 114 } else if (node.type === "AssignmentPattern") { 115 if (isInRange(node.right, location)) { 116 return true; 117 } 118 } else if (SENTINEL_TYPE.test(node.type)) { 119 break; 120 } 121 122 node = node.parent; 123 } 124 125 return false; 126} 127 128//------------------------------------------------------------------------------ 129// Rule Definition 130//------------------------------------------------------------------------------ 131 132module.exports = { 133 meta: { 134 type: "problem", 135 136 docs: { 137 description: "disallow the use of variables before they are defined", 138 category: "Variables", 139 recommended: false, 140 url: "https://eslint.org/docs/rules/no-use-before-define" 141 }, 142 143 schema: [ 144 { 145 oneOf: [ 146 { 147 enum: ["nofunc"] 148 }, 149 { 150 type: "object", 151 properties: { 152 functions: { type: "boolean" }, 153 classes: { type: "boolean" }, 154 variables: { type: "boolean" } 155 }, 156 additionalProperties: false 157 } 158 ] 159 } 160 ], 161 162 messages: { 163 usedBeforeDefined: "'{{name}}' was used before it was defined." 164 } 165 }, 166 167 create(context) { 168 const options = parseOptions(context.options[0]); 169 170 /** 171 * Determines whether a given use-before-define case should be reported according to the options. 172 * @param {eslint-scope.Variable} variable The variable that gets used before being defined 173 * @param {eslint-scope.Reference} reference The reference to the variable 174 * @returns {boolean} `true` if the usage should be reported 175 */ 176 function isForbidden(variable, reference) { 177 if (isFunction(variable)) { 178 return options.functions; 179 } 180 if (isOuterClass(variable, reference)) { 181 return options.classes; 182 } 183 if (isOuterVariable(variable, reference)) { 184 return options.variables; 185 } 186 return true; 187 } 188 189 /** 190 * Finds and validates all variables in a given scope. 191 * @param {Scope} scope The scope object. 192 * @returns {void} 193 * @private 194 */ 195 function findVariablesInScope(scope) { 196 scope.references.forEach(reference => { 197 const variable = reference.resolved; 198 199 /* 200 * Skips when the reference is: 201 * - initialization's. 202 * - referring to an undefined variable. 203 * - referring to a global environment variable (there're no identifiers). 204 * - located preceded by the variable (except in initializers). 205 * - allowed by options. 206 */ 207 if (reference.init || 208 !variable || 209 variable.identifiers.length === 0 || 210 (variable.identifiers[0].range[1] < reference.identifier.range[1] && !isInInitializer(variable, reference)) || 211 !isForbidden(variable, reference) 212 ) { 213 return; 214 } 215 216 // Reports. 217 context.report({ 218 node: reference.identifier, 219 messageId: "usedBeforeDefined", 220 data: reference.identifier 221 }); 222 }); 223 224 scope.childScopes.forEach(findVariablesInScope); 225 } 226 227 return { 228 Program() { 229 findVariablesInScope(context.getScope()); 230 } 231 }; 232 } 233}; 234