no-use-before-define.js 11 KB

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