no-var.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. /**
  2. * @fileoverview Rule to check for the usage of var.
  3. * @author Jamund Ferguson
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Helpers
  12. //------------------------------------------------------------------------------
  13. /**
  14. * Check whether a given variable is a global variable or not.
  15. * @param {eslint-scope.Variable} variable The variable to check.
  16. * @returns {boolean} `true` if the variable is a global variable.
  17. */
  18. function isGlobal(variable) {
  19. return Boolean(variable.scope) && variable.scope.type === "global";
  20. }
  21. /**
  22. * Finds the nearest function scope or global scope walking up the scope
  23. * hierarchy.
  24. * @param {eslint-scope.Scope} scope The scope to traverse.
  25. * @returns {eslint-scope.Scope} a function scope or global scope containing the given
  26. * scope.
  27. */
  28. function getEnclosingFunctionScope(scope) {
  29. let currentScope = scope;
  30. while (currentScope.type !== "function" && currentScope.type !== "global") {
  31. currentScope = currentScope.upper;
  32. }
  33. return currentScope;
  34. }
  35. /**
  36. * Checks whether the given variable has any references from a more specific
  37. * function expression (i.e. a closure).
  38. * @param {eslint-scope.Variable} variable A variable to check.
  39. * @returns {boolean} `true` if the variable is used from a closure.
  40. */
  41. function isReferencedInClosure(variable) {
  42. const enclosingFunctionScope = getEnclosingFunctionScope(variable.scope);
  43. return variable.references.some(
  44. reference =>
  45. getEnclosingFunctionScope(reference.from) !==
  46. enclosingFunctionScope,
  47. );
  48. }
  49. /**
  50. * Checks whether the given node is the assignee of a loop.
  51. * @param {ASTNode} node A VariableDeclaration node to check.
  52. * @returns {boolean} `true` if the declaration is assigned as part of loop
  53. * iteration.
  54. */
  55. function isLoopAssignee(node) {
  56. return (
  57. (node.parent.type === "ForOfStatement" ||
  58. node.parent.type === "ForInStatement") &&
  59. node === node.parent.left
  60. );
  61. }
  62. /**
  63. * Checks whether the given variable declaration is immediately initialized.
  64. * @param {ASTNode} node A VariableDeclaration node to check.
  65. * @returns {boolean} `true` if the declaration has an initializer.
  66. */
  67. function isDeclarationInitialized(node) {
  68. return node.declarations.every(declarator => declarator.init !== null);
  69. }
  70. const SCOPE_NODE_TYPE =
  71. /^(?:Program|BlockStatement|SwitchStatement|ForStatement|ForInStatement|ForOfStatement)$/u;
  72. /**
  73. * Gets the scope node which directly contains a given node.
  74. * @param {ASTNode} node A node to get. This is a `VariableDeclaration` or
  75. * an `Identifier`.
  76. * @returns {ASTNode} A scope node. This is one of `Program`, `BlockStatement`,
  77. * `SwitchStatement`, `ForStatement`, `ForInStatement`, and
  78. * `ForOfStatement`.
  79. */
  80. function getScopeNode(node) {
  81. for (
  82. let currentNode = node;
  83. currentNode;
  84. currentNode = currentNode.parent
  85. ) {
  86. if (SCOPE_NODE_TYPE.test(currentNode.type)) {
  87. return currentNode;
  88. }
  89. }
  90. /* c8 ignore next */
  91. return null;
  92. }
  93. /**
  94. * Checks whether a given variable is redeclared or not.
  95. * @param {eslint-scope.Variable} variable A variable to check.
  96. * @returns {boolean} `true` if the variable is redeclared.
  97. */
  98. function isRedeclared(variable) {
  99. return variable.defs.length >= 2;
  100. }
  101. /**
  102. * Checks whether a given variable is used from outside of the specified scope.
  103. * @param {ASTNode} scopeNode A scope node to check.
  104. * @returns {Function} The predicate function which checks whether a given
  105. * variable is used from outside of the specified scope.
  106. */
  107. function isUsedFromOutsideOf(scopeNode) {
  108. /**
  109. * Checks whether a given reference is inside of the specified scope or not.
  110. * @param {eslint-scope.Reference} reference A reference to check.
  111. * @returns {boolean} `true` if the reference is inside of the specified
  112. * scope.
  113. */
  114. function isOutsideOfScope(reference) {
  115. const scope = scopeNode.range;
  116. const id = reference.identifier.range;
  117. return id[0] < scope[0] || id[1] > scope[1];
  118. }
  119. return function (variable) {
  120. return variable.references.some(isOutsideOfScope);
  121. };
  122. }
  123. /**
  124. * Creates the predicate function which checks whether a variable has their references in TDZ.
  125. *
  126. * The predicate function would return `true`:
  127. *
  128. * - if a reference is before the declarator. E.g. (var a = b, b = 1;)(var {a = b, b} = {};)
  129. * - if a reference is in the expression of their default value. E.g. (var {a = a} = {};)
  130. * - if a reference is in the expression of their initializer. E.g. (var a = a;)
  131. * @param {ASTNode} node The initializer node of VariableDeclarator.
  132. * @returns {Function} The predicate function.
  133. * @private
  134. */
  135. function hasReferenceInTDZ(node) {
  136. const initStart = node.range[0];
  137. const initEnd = node.range[1];
  138. return variable => {
  139. const id = variable.defs[0].name;
  140. const idStart = id.range[0];
  141. const defaultValue =
  142. id.parent.type === "AssignmentPattern" ? id.parent.right : null;
  143. const defaultStart = defaultValue && defaultValue.range[0];
  144. const defaultEnd = defaultValue && defaultValue.range[1];
  145. return variable.references.some(reference => {
  146. const start = reference.identifier.range[0];
  147. const end = reference.identifier.range[1];
  148. return (
  149. !reference.init &&
  150. (start < idStart ||
  151. (defaultValue !== null &&
  152. start >= defaultStart &&
  153. end <= defaultEnd) ||
  154. (!astUtils.isFunction(node) &&
  155. start >= initStart &&
  156. end <= initEnd))
  157. );
  158. });
  159. };
  160. }
  161. /**
  162. * Checks whether a given variable has name that is allowed for 'var' declarations,
  163. * but disallowed for `let` declarations.
  164. * @param {eslint-scope.Variable} variable The variable to check.
  165. * @returns {boolean} `true` if the variable has a disallowed name.
  166. */
  167. function hasNameDisallowedForLetDeclarations(variable) {
  168. return variable.name === "let";
  169. }
  170. //------------------------------------------------------------------------------
  171. // Rule Definition
  172. //------------------------------------------------------------------------------
  173. /** @type {import('../types').Rule.RuleModule} */
  174. module.exports = {
  175. meta: {
  176. type: "suggestion",
  177. dialects: ["typescript", "javascript"],
  178. language: "javascript",
  179. docs: {
  180. description: "Require `let` or `const` instead of `var`",
  181. recommended: false,
  182. url: "https://eslint.org/docs/latest/rules/no-var",
  183. },
  184. schema: [],
  185. fixable: "code",
  186. messages: {
  187. unexpectedVar: "Unexpected var, use let or const instead.",
  188. },
  189. },
  190. create(context) {
  191. const sourceCode = context.sourceCode;
  192. /**
  193. * Checks whether the variables which are defined by the given declarator node have their references in TDZ.
  194. * @param {ASTNode} declarator The VariableDeclarator node to check.
  195. * @returns {boolean} `true` if one of the variables which are defined by the given declarator node have their references in TDZ.
  196. */
  197. function hasSelfReferenceInTDZ(declarator) {
  198. if (!declarator.init) {
  199. return false;
  200. }
  201. const variables = sourceCode.getDeclaredVariables(declarator);
  202. return variables.some(hasReferenceInTDZ(declarator.init));
  203. }
  204. /**
  205. * Checks whether it can fix a given variable declaration or not.
  206. * It cannot fix if the following cases:
  207. *
  208. * - A variable is a global variable.
  209. * - A variable is declared on a SwitchCase node.
  210. * - A variable is redeclared.
  211. * - A variable is used from outside the scope.
  212. * - A variable is used from a closure within a loop.
  213. * - A variable might be used before it is assigned within a loop.
  214. * - A variable might be used in TDZ.
  215. * - A variable is declared in statement position (e.g. a single-line `IfStatement`)
  216. * - A variable has name that is disallowed for `let` declarations.
  217. *
  218. * ## A variable is declared on a SwitchCase node.
  219. *
  220. * If this rule modifies 'var' declarations on a SwitchCase node, it
  221. * would generate the warnings of 'no-case-declarations' rule. And the
  222. * 'eslint:recommended' preset includes 'no-case-declarations' rule, so
  223. * this rule doesn't modify those declarations.
  224. *
  225. * ## A variable is redeclared.
  226. *
  227. * The language spec disallows redeclarations of `let` declarations.
  228. * Those variables would cause syntax errors.
  229. *
  230. * ## A variable is used from outside the scope.
  231. *
  232. * The language spec disallows accesses from outside of the scope for
  233. * `let` declarations. Those variables would cause reference errors.
  234. *
  235. * ## A variable is used from a closure within a loop.
  236. *
  237. * A `var` declaration within a loop shares the same variable instance
  238. * across all loop iterations, while a `let` declaration creates a new
  239. * instance for each iteration. This means if a variable in a loop is
  240. * referenced by any closure, changing it from `var` to `let` would
  241. * change the behavior in a way that is generally unsafe.
  242. *
  243. * ## A variable might be used before it is assigned within a loop.
  244. *
  245. * Within a loop, a `let` declaration without an initializer will be
  246. * initialized to null, while a `var` declaration will retain its value
  247. * from the previous iteration, so it is only safe to change `var` to
  248. * `let` if we can statically determine that the variable is always
  249. * assigned a value before its first access in the loop body. To keep
  250. * the implementation simple, we only convert `var` to `let` within
  251. * loops when the variable is a loop assignee or the declaration has an
  252. * initializer.
  253. * @param {ASTNode} node A variable declaration node to check.
  254. * @returns {boolean} `true` if it can fix the node.
  255. */
  256. function canFix(node) {
  257. const variables = sourceCode.getDeclaredVariables(node);
  258. const scopeNode = getScopeNode(node);
  259. if (
  260. node.parent.type === "SwitchCase" ||
  261. node.declarations.some(hasSelfReferenceInTDZ) ||
  262. variables.some(isGlobal) ||
  263. variables.some(isRedeclared) ||
  264. variables.some(isUsedFromOutsideOf(scopeNode)) ||
  265. variables.some(hasNameDisallowedForLetDeclarations)
  266. ) {
  267. return false;
  268. }
  269. if (astUtils.isInLoop(node)) {
  270. if (variables.some(isReferencedInClosure)) {
  271. return false;
  272. }
  273. if (!isLoopAssignee(node) && !isDeclarationInitialized(node)) {
  274. return false;
  275. }
  276. }
  277. if (
  278. !isLoopAssignee(node) &&
  279. !(
  280. node.parent.type === "ForStatement" &&
  281. node.parent.init === node
  282. ) &&
  283. !astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type)
  284. ) {
  285. // If the declaration is not in a block, e.g. `if (foo) var bar = 1;`, then it can't be fixed.
  286. return false;
  287. }
  288. return true;
  289. }
  290. /**
  291. * Reports a given variable declaration node.
  292. * @param {ASTNode} node A variable declaration node to report.
  293. * @returns {void}
  294. */
  295. function report(node) {
  296. context.report({
  297. node,
  298. messageId: "unexpectedVar",
  299. fix(fixer) {
  300. const varToken = sourceCode.getFirstToken(node, {
  301. filter: t => t.value === "var",
  302. });
  303. return canFix(node)
  304. ? fixer.replaceText(varToken, "let")
  305. : null;
  306. },
  307. });
  308. }
  309. return {
  310. "VariableDeclaration:exit"(node) {
  311. if (node.kind !== "var") {
  312. return;
  313. }
  314. if (
  315. node.parent.type === "TSModuleBlock" &&
  316. node.parent.parent.type === "TSModuleDeclaration" &&
  317. node.parent.parent.global
  318. ) {
  319. return;
  320. }
  321. report(node);
  322. },
  323. };
  324. },
  325. };