no-use-before-define.js 7.5 KB

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