no-use-before-define.js 12 KB

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