one-var.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. /**
  2. * @fileoverview A rule to control the use of single variable declarations.
  3. * @author Ian Christian Myers
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Helpers
  12. //------------------------------------------------------------------------------
  13. /**
  14. * Determines whether the given node is in a statement list.
  15. * @param {ASTNode} node node to check
  16. * @returns {boolean} `true` if the given node is in a statement list
  17. */
  18. function isInStatementList(node) {
  19. return astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type);
  20. }
  21. //------------------------------------------------------------------------------
  22. // Rule Definition
  23. //------------------------------------------------------------------------------
  24. /** @type {import('../types').Rule.RuleModule} */
  25. module.exports = {
  26. meta: {
  27. type: "suggestion",
  28. docs: {
  29. description:
  30. "Enforce variables to be declared either together or separately in functions",
  31. recommended: false,
  32. frozen: true,
  33. url: "https://eslint.org/docs/latest/rules/one-var",
  34. },
  35. fixable: "code",
  36. schema: [
  37. {
  38. oneOf: [
  39. {
  40. enum: ["always", "never", "consecutive"],
  41. },
  42. {
  43. type: "object",
  44. properties: {
  45. separateRequires: {
  46. type: "boolean",
  47. },
  48. var: {
  49. enum: ["always", "never", "consecutive"],
  50. },
  51. let: {
  52. enum: ["always", "never", "consecutive"],
  53. },
  54. const: {
  55. enum: ["always", "never", "consecutive"],
  56. },
  57. using: {
  58. enum: ["always", "never", "consecutive"],
  59. },
  60. awaitUsing: {
  61. enum: ["always", "never", "consecutive"],
  62. },
  63. },
  64. additionalProperties: false,
  65. },
  66. {
  67. type: "object",
  68. properties: {
  69. initialized: {
  70. enum: ["always", "never", "consecutive"],
  71. },
  72. uninitialized: {
  73. enum: ["always", "never", "consecutive"],
  74. },
  75. },
  76. additionalProperties: false,
  77. },
  78. ],
  79. },
  80. ],
  81. messages: {
  82. combineUninitialized:
  83. "Combine this with the previous '{{type}}' statement with uninitialized variables.",
  84. combineInitialized:
  85. "Combine this with the previous '{{type}}' statement with initialized variables.",
  86. splitUninitialized:
  87. "Split uninitialized '{{type}}' declarations into multiple statements.",
  88. splitInitialized:
  89. "Split initialized '{{type}}' declarations into multiple statements.",
  90. splitRequires:
  91. "Split requires to be separated into a single block.",
  92. combine: "Combine this with the previous '{{type}}' statement.",
  93. split: "Split '{{type}}' declarations into multiple statements.",
  94. },
  95. },
  96. create(context) {
  97. const MODE_ALWAYS = "always";
  98. const MODE_NEVER = "never";
  99. const MODE_CONSECUTIVE = "consecutive";
  100. const mode = context.options[0] || MODE_ALWAYS;
  101. const options = {};
  102. if (typeof mode === "string") {
  103. // simple options configuration with just a string
  104. options.var = { uninitialized: mode, initialized: mode };
  105. options.let = { uninitialized: mode, initialized: mode };
  106. options.const = { uninitialized: mode, initialized: mode };
  107. options.using = { uninitialized: mode, initialized: mode };
  108. options.awaitUsing = { uninitialized: mode, initialized: mode };
  109. } else if (typeof mode === "object") {
  110. // options configuration is an object
  111. options.separateRequires = !!mode.separateRequires;
  112. options.var = { uninitialized: mode.var, initialized: mode.var };
  113. options.let = { uninitialized: mode.let, initialized: mode.let };
  114. options.const = {
  115. uninitialized: mode.const,
  116. initialized: mode.const,
  117. };
  118. options.using = {
  119. uninitialized: mode.using,
  120. initialized: mode.using,
  121. };
  122. options.awaitUsing = {
  123. uninitialized: mode.awaitUsing,
  124. initialized: mode.awaitUsing,
  125. };
  126. if (Object.hasOwn(mode, "uninitialized")) {
  127. options.var.uninitialized = mode.uninitialized;
  128. options.let.uninitialized = mode.uninitialized;
  129. options.const.uninitialized = mode.uninitialized;
  130. options.using.uninitialized = mode.uninitialized;
  131. options.awaitUsing.uninitialized = mode.uninitialized;
  132. }
  133. if (Object.hasOwn(mode, "initialized")) {
  134. options.var.initialized = mode.initialized;
  135. options.let.initialized = mode.initialized;
  136. options.const.initialized = mode.initialized;
  137. options.using.initialized = mode.initialized;
  138. options.awaitUsing.initialized = mode.initialized;
  139. }
  140. }
  141. const sourceCode = context.sourceCode;
  142. //--------------------------------------------------------------------------
  143. // Helpers
  144. //--------------------------------------------------------------------------
  145. const functionStack = [];
  146. const blockStack = [];
  147. /**
  148. * Increments the blockStack counter.
  149. * @returns {void}
  150. * @private
  151. */
  152. function startBlock() {
  153. blockStack.push({
  154. let: { initialized: false, uninitialized: false },
  155. const: { initialized: false, uninitialized: false },
  156. using: { initialized: false, uninitialized: false },
  157. awaitUsing: { initialized: false, uninitialized: false },
  158. });
  159. }
  160. /**
  161. * Increments the functionStack counter.
  162. * @returns {void}
  163. * @private
  164. */
  165. function startFunction() {
  166. functionStack.push({ initialized: false, uninitialized: false });
  167. startBlock();
  168. }
  169. /**
  170. * Decrements the blockStack counter.
  171. * @returns {void}
  172. * @private
  173. */
  174. function endBlock() {
  175. blockStack.pop();
  176. }
  177. /**
  178. * Decrements the functionStack counter.
  179. * @returns {void}
  180. * @private
  181. */
  182. function endFunction() {
  183. functionStack.pop();
  184. endBlock();
  185. }
  186. /**
  187. * Check if a variable declaration is a require.
  188. * @param {ASTNode} decl variable declaration Node
  189. * @returns {bool} if decl is a require, return true; else return false.
  190. * @private
  191. */
  192. function isRequire(decl) {
  193. return (
  194. decl.init &&
  195. decl.init.type === "CallExpression" &&
  196. decl.init.callee.name === "require"
  197. );
  198. }
  199. /**
  200. * Records whether initialized/uninitialized/required variables are defined in current scope.
  201. * @param {string} statementType one of: "var", "let", "const", "using", or "awaitUsing"
  202. * @param {ASTNode[]} declarations List of declarations
  203. * @param {Object} currentScope The scope being investigated
  204. * @returns {void}
  205. * @private
  206. */
  207. function recordTypes(statementType, declarations, currentScope) {
  208. for (let i = 0; i < declarations.length; i++) {
  209. if (declarations[i].init === null) {
  210. if (
  211. options[statementType] &&
  212. options[statementType].uninitialized === MODE_ALWAYS
  213. ) {
  214. currentScope.uninitialized = true;
  215. }
  216. } else {
  217. if (
  218. options[statementType] &&
  219. options[statementType].initialized === MODE_ALWAYS
  220. ) {
  221. if (
  222. options.separateRequires &&
  223. isRequire(declarations[i])
  224. ) {
  225. currentScope.required = true;
  226. } else {
  227. currentScope.initialized = true;
  228. }
  229. }
  230. }
  231. }
  232. }
  233. /**
  234. * Determines the current scope (function or block)
  235. * @param {string} statementType one of: "var", "let", "const", "using", or "awaitUsing"
  236. * @returns {Object} The scope associated with statementType
  237. */
  238. function getCurrentScope(statementType) {
  239. let currentScope;
  240. if (statementType === "var") {
  241. currentScope = functionStack.at(-1);
  242. } else if (statementType === "let") {
  243. currentScope = blockStack.at(-1).let;
  244. } else if (statementType === "const") {
  245. currentScope = blockStack.at(-1).const;
  246. } else if (statementType === "using") {
  247. currentScope = blockStack.at(-1).using;
  248. } else if (statementType === "awaitUsing") {
  249. currentScope = blockStack.at(-1).awaitUsing;
  250. }
  251. return currentScope;
  252. }
  253. /**
  254. * Counts the number of initialized and uninitialized declarations in a list of declarations
  255. * @param {ASTNode[]} declarations List of declarations
  256. * @returns {Object} Counts of 'uninitialized' and 'initialized' declarations
  257. * @private
  258. */
  259. function countDeclarations(declarations) {
  260. const counts = { uninitialized: 0, initialized: 0 };
  261. for (let i = 0; i < declarations.length; i++) {
  262. if (declarations[i].init === null) {
  263. counts.uninitialized++;
  264. } else {
  265. counts.initialized++;
  266. }
  267. }
  268. return counts;
  269. }
  270. /**
  271. * Determines if there is more than one var statement in the current scope.
  272. * @param {string} statementType one of: "var", "let", "const", "using", or "awaitUsing"
  273. * @param {ASTNode[]} declarations List of declarations
  274. * @returns {boolean} Returns true if it is the first var declaration, false if not.
  275. * @private
  276. */
  277. function hasOnlyOneStatement(statementType, declarations) {
  278. const declarationCounts = countDeclarations(declarations);
  279. const currentOptions = options[statementType] || {};
  280. const currentScope = getCurrentScope(statementType);
  281. const hasRequires = declarations.some(isRequire);
  282. if (
  283. currentOptions.uninitialized === MODE_ALWAYS &&
  284. currentOptions.initialized === MODE_ALWAYS
  285. ) {
  286. if (currentScope.uninitialized || currentScope.initialized) {
  287. if (!hasRequires) {
  288. return false;
  289. }
  290. }
  291. }
  292. if (declarationCounts.uninitialized > 0) {
  293. if (
  294. currentOptions.uninitialized === MODE_ALWAYS &&
  295. currentScope.uninitialized
  296. ) {
  297. return false;
  298. }
  299. }
  300. if (declarationCounts.initialized > 0) {
  301. if (
  302. currentOptions.initialized === MODE_ALWAYS &&
  303. currentScope.initialized
  304. ) {
  305. if (!hasRequires) {
  306. return false;
  307. }
  308. }
  309. }
  310. if (currentScope.required && hasRequires) {
  311. return false;
  312. }
  313. recordTypes(statementType, declarations, currentScope);
  314. return true;
  315. }
  316. /**
  317. * Fixer to join VariableDeclaration's into a single declaration
  318. * @param {VariableDeclarator[]} declarations The `VariableDeclaration` to join
  319. * @returns {Function} The fixer function
  320. */
  321. function joinDeclarations(declarations) {
  322. const declaration = declarations[0];
  323. const body = Array.isArray(declaration.parent.parent.body)
  324. ? declaration.parent.parent.body
  325. : [];
  326. const currentIndex = body.findIndex(
  327. node => node.range[0] === declaration.parent.range[0],
  328. );
  329. const previousNode = body[currentIndex - 1];
  330. return function* joinDeclarationsFixer(fixer) {
  331. const type = sourceCode.getFirstToken(declaration.parent);
  332. const beforeType = sourceCode.getTokenBefore(type);
  333. if (
  334. previousNode &&
  335. previousNode.kind === declaration.parent.kind
  336. ) {
  337. if (beforeType.value === ";") {
  338. yield fixer.replaceText(beforeType, ",");
  339. } else {
  340. yield fixer.insertTextAfter(beforeType, ",");
  341. }
  342. if (declaration.parent.kind === "await using") {
  343. const usingToken = sourceCode.getTokenAfter(type);
  344. yield fixer.remove(usingToken);
  345. }
  346. yield fixer.replaceText(type, "");
  347. }
  348. };
  349. }
  350. /**
  351. * Fixer to split a VariableDeclaration into individual declarations
  352. * @param {VariableDeclaration} declaration The `VariableDeclaration` to split
  353. * @returns {Function|null} The fixer function
  354. */
  355. function splitDeclarations(declaration) {
  356. const { parent } = declaration;
  357. // don't autofix code such as: if (foo) var x, y;
  358. if (
  359. !isInStatementList(
  360. parent.type === "ExportNamedDeclaration"
  361. ? parent
  362. : declaration,
  363. )
  364. ) {
  365. return null;
  366. }
  367. return fixer =>
  368. declaration.declarations
  369. .map(declarator => {
  370. const tokenAfterDeclarator =
  371. sourceCode.getTokenAfter(declarator);
  372. if (tokenAfterDeclarator === null) {
  373. return null;
  374. }
  375. const afterComma = sourceCode.getTokenAfter(
  376. tokenAfterDeclarator,
  377. { includeComments: true },
  378. );
  379. if (tokenAfterDeclarator.value !== ",") {
  380. return null;
  381. }
  382. const exportPlacement =
  383. declaration.parent.type === "ExportNamedDeclaration"
  384. ? "export "
  385. : "";
  386. /*
  387. * `var x,y`
  388. * tokenAfterDeclarator ^^ afterComma
  389. */
  390. if (
  391. afterComma.range[0] ===
  392. tokenAfterDeclarator.range[1]
  393. ) {
  394. return fixer.replaceText(
  395. tokenAfterDeclarator,
  396. `; ${exportPlacement}${declaration.kind} `,
  397. );
  398. }
  399. /*
  400. * `var x,
  401. * tokenAfterDeclarator ^
  402. * y`
  403. * ^ afterComma
  404. */
  405. if (
  406. afterComma.loc.start.line >
  407. tokenAfterDeclarator.loc.end.line ||
  408. afterComma.type === "Line" ||
  409. afterComma.type === "Block"
  410. ) {
  411. let lastComment = afterComma;
  412. while (
  413. lastComment.type === "Line" ||
  414. lastComment.type === "Block"
  415. ) {
  416. lastComment = sourceCode.getTokenAfter(
  417. lastComment,
  418. { includeComments: true },
  419. );
  420. }
  421. return fixer.replaceTextRange(
  422. [
  423. tokenAfterDeclarator.range[0],
  424. lastComment.range[0],
  425. ],
  426. `;${sourceCode.text.slice(
  427. tokenAfterDeclarator.range[1],
  428. lastComment.range[0],
  429. )}${exportPlacement}${declaration.kind} `,
  430. );
  431. }
  432. return fixer.replaceText(
  433. tokenAfterDeclarator,
  434. `; ${exportPlacement}${declaration.kind}`,
  435. );
  436. })
  437. .filter(x => x);
  438. }
  439. /**
  440. * Checks a given VariableDeclaration node for errors.
  441. * @param {ASTNode} node The VariableDeclaration node to check
  442. * @returns {void}
  443. * @private
  444. */
  445. function checkVariableDeclaration(node) {
  446. const parent = node.parent;
  447. const type = node.kind;
  448. const key = type === "await using" ? "awaitUsing" : type;
  449. if (!options[key]) {
  450. return;
  451. }
  452. const declarations = node.declarations;
  453. const declarationCounts = countDeclarations(declarations);
  454. const mixedRequires =
  455. declarations.some(isRequire) && !declarations.every(isRequire);
  456. if (options[key].initialized === MODE_ALWAYS) {
  457. if (options.separateRequires && mixedRequires) {
  458. context.report({
  459. node,
  460. messageId: "splitRequires",
  461. });
  462. }
  463. }
  464. // consecutive
  465. const nodeIndex =
  466. (parent.body &&
  467. parent.body.length > 0 &&
  468. parent.body.indexOf(node)) ||
  469. 0;
  470. if (nodeIndex > 0) {
  471. const previousNode = parent.body[nodeIndex - 1];
  472. const isPreviousNodeDeclaration =
  473. previousNode.type === "VariableDeclaration";
  474. const declarationsWithPrevious = declarations.concat(
  475. previousNode.declarations || [],
  476. );
  477. if (
  478. isPreviousNodeDeclaration &&
  479. previousNode.kind === type &&
  480. !(
  481. declarationsWithPrevious.some(isRequire) &&
  482. !declarationsWithPrevious.every(isRequire)
  483. )
  484. ) {
  485. const previousDeclCounts = countDeclarations(
  486. previousNode.declarations,
  487. );
  488. if (
  489. options[key].initialized === MODE_CONSECUTIVE &&
  490. options[key].uninitialized === MODE_CONSECUTIVE
  491. ) {
  492. context.report({
  493. node,
  494. messageId: "combine",
  495. data: {
  496. type,
  497. },
  498. fix: joinDeclarations(declarations),
  499. });
  500. } else if (
  501. options[key].initialized === MODE_CONSECUTIVE &&
  502. declarationCounts.initialized > 0 &&
  503. previousDeclCounts.initialized > 0
  504. ) {
  505. context.report({
  506. node,
  507. messageId: "combineInitialized",
  508. data: {
  509. type,
  510. },
  511. fix: joinDeclarations(declarations),
  512. });
  513. } else if (
  514. options[key].uninitialized === MODE_CONSECUTIVE &&
  515. declarationCounts.uninitialized > 0 &&
  516. previousDeclCounts.uninitialized > 0
  517. ) {
  518. context.report({
  519. node,
  520. messageId: "combineUninitialized",
  521. data: {
  522. type,
  523. },
  524. fix: joinDeclarations(declarations),
  525. });
  526. }
  527. }
  528. }
  529. // always
  530. if (!hasOnlyOneStatement(key, declarations)) {
  531. if (
  532. options[key].initialized === MODE_ALWAYS &&
  533. options[key].uninitialized === MODE_ALWAYS
  534. ) {
  535. context.report({
  536. node,
  537. messageId: "combine",
  538. data: {
  539. type,
  540. },
  541. fix: joinDeclarations(declarations),
  542. });
  543. } else {
  544. if (
  545. options[key].initialized === MODE_ALWAYS &&
  546. declarationCounts.initialized > 0
  547. ) {
  548. context.report({
  549. node,
  550. messageId: "combineInitialized",
  551. data: {
  552. type,
  553. },
  554. fix: joinDeclarations(declarations),
  555. });
  556. }
  557. if (
  558. options[key].uninitialized === MODE_ALWAYS &&
  559. declarationCounts.uninitialized > 0
  560. ) {
  561. if (
  562. node.parent.left === node &&
  563. (node.parent.type === "ForInStatement" ||
  564. node.parent.type === "ForOfStatement")
  565. ) {
  566. return;
  567. }
  568. context.report({
  569. node,
  570. messageId: "combineUninitialized",
  571. data: {
  572. type,
  573. },
  574. fix: joinDeclarations(declarations),
  575. });
  576. }
  577. }
  578. }
  579. // never
  580. if (parent.type !== "ForStatement" || parent.init !== node) {
  581. const totalDeclarations =
  582. declarationCounts.uninitialized +
  583. declarationCounts.initialized;
  584. if (totalDeclarations > 1) {
  585. if (
  586. options[key].initialized === MODE_NEVER &&
  587. options[key].uninitialized === MODE_NEVER
  588. ) {
  589. // both initialized and uninitialized
  590. context.report({
  591. node,
  592. messageId: "split",
  593. data: {
  594. type,
  595. },
  596. fix: splitDeclarations(node),
  597. });
  598. } else if (
  599. options[key].initialized === MODE_NEVER &&
  600. declarationCounts.initialized > 0
  601. ) {
  602. // initialized
  603. context.report({
  604. node,
  605. messageId: "splitInitialized",
  606. data: {
  607. type,
  608. },
  609. fix: splitDeclarations(node),
  610. });
  611. } else if (
  612. options[key].uninitialized === MODE_NEVER &&
  613. declarationCounts.uninitialized > 0
  614. ) {
  615. // uninitialized
  616. context.report({
  617. node,
  618. messageId: "splitUninitialized",
  619. data: {
  620. type,
  621. },
  622. fix: splitDeclarations(node),
  623. });
  624. }
  625. }
  626. }
  627. }
  628. //--------------------------------------------------------------------------
  629. // Public API
  630. //--------------------------------------------------------------------------
  631. return {
  632. Program: startFunction,
  633. FunctionDeclaration: startFunction,
  634. FunctionExpression: startFunction,
  635. ArrowFunctionExpression: startFunction,
  636. StaticBlock: startFunction, // StaticBlock creates a new scope for `var` variables
  637. BlockStatement: startBlock,
  638. ForStatement: startBlock,
  639. ForInStatement: startBlock,
  640. ForOfStatement: startBlock,
  641. SwitchStatement: startBlock,
  642. VariableDeclaration: checkVariableDeclaration,
  643. "ForStatement:exit": endBlock,
  644. "ForOfStatement:exit": endBlock,
  645. "ForInStatement:exit": endBlock,
  646. "SwitchStatement:exit": endBlock,
  647. "BlockStatement:exit": endBlock,
  648. "Program:exit": endFunction,
  649. "FunctionDeclaration:exit": endFunction,
  650. "FunctionExpression:exit": endFunction,
  651. "ArrowFunctionExpression:exit": endFunction,
  652. "StaticBlock:exit": endFunction,
  653. };
  654. },
  655. };