one-var.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  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('../shared/types').Rule} */
  25. module.exports = {
  26. meta: {
  27. type: "suggestion",
  28. docs: {
  29. description: "Enforce variables to be declared either together or separately in functions",
  30. recommended: false,
  31. url: "https://eslint.org/docs/rules/one-var"
  32. },
  33. fixable: "code",
  34. schema: [
  35. {
  36. oneOf: [
  37. {
  38. enum: ["always", "never", "consecutive"]
  39. },
  40. {
  41. type: "object",
  42. properties: {
  43. separateRequires: {
  44. type: "boolean"
  45. },
  46. var: {
  47. enum: ["always", "never", "consecutive"]
  48. },
  49. let: {
  50. enum: ["always", "never", "consecutive"]
  51. },
  52. const: {
  53. enum: ["always", "never", "consecutive"]
  54. }
  55. },
  56. additionalProperties: false
  57. },
  58. {
  59. type: "object",
  60. properties: {
  61. initialized: {
  62. enum: ["always", "never", "consecutive"]
  63. },
  64. uninitialized: {
  65. enum: ["always", "never", "consecutive"]
  66. }
  67. },
  68. additionalProperties: false
  69. }
  70. ]
  71. }
  72. ],
  73. messages: {
  74. combineUninitialized: "Combine this with the previous '{{type}}' statement with uninitialized variables.",
  75. combineInitialized: "Combine this with the previous '{{type}}' statement with initialized variables.",
  76. splitUninitialized: "Split uninitialized '{{type}}' declarations into multiple statements.",
  77. splitInitialized: "Split initialized '{{type}}' declarations into multiple statements.",
  78. splitRequires: "Split requires to be separated into a single block.",
  79. combine: "Combine this with the previous '{{type}}' statement.",
  80. split: "Split '{{type}}' declarations into multiple statements."
  81. }
  82. },
  83. create(context) {
  84. const MODE_ALWAYS = "always";
  85. const MODE_NEVER = "never";
  86. const MODE_CONSECUTIVE = "consecutive";
  87. const mode = context.options[0] || MODE_ALWAYS;
  88. const options = {};
  89. if (typeof mode === "string") { // simple options configuration with just a string
  90. options.var = { uninitialized: mode, initialized: mode };
  91. options.let = { uninitialized: mode, initialized: mode };
  92. options.const = { uninitialized: mode, initialized: mode };
  93. } else if (typeof mode === "object") { // options configuration is an object
  94. options.separateRequires = !!mode.separateRequires;
  95. options.var = { uninitialized: mode.var, initialized: mode.var };
  96. options.let = { uninitialized: mode.let, initialized: mode.let };
  97. options.const = { uninitialized: mode.const, initialized: mode.const };
  98. if (Object.prototype.hasOwnProperty.call(mode, "uninitialized")) {
  99. options.var.uninitialized = mode.uninitialized;
  100. options.let.uninitialized = mode.uninitialized;
  101. options.const.uninitialized = mode.uninitialized;
  102. }
  103. if (Object.prototype.hasOwnProperty.call(mode, "initialized")) {
  104. options.var.initialized = mode.initialized;
  105. options.let.initialized = mode.initialized;
  106. options.const.initialized = mode.initialized;
  107. }
  108. }
  109. const sourceCode = context.getSourceCode();
  110. //--------------------------------------------------------------------------
  111. // Helpers
  112. //--------------------------------------------------------------------------
  113. const functionStack = [];
  114. const blockStack = [];
  115. /**
  116. * Increments the blockStack counter.
  117. * @returns {void}
  118. * @private
  119. */
  120. function startBlock() {
  121. blockStack.push({
  122. let: { initialized: false, uninitialized: false },
  123. const: { initialized: false, uninitialized: false }
  124. });
  125. }
  126. /**
  127. * Increments the functionStack counter.
  128. * @returns {void}
  129. * @private
  130. */
  131. function startFunction() {
  132. functionStack.push({ initialized: false, uninitialized: false });
  133. startBlock();
  134. }
  135. /**
  136. * Decrements the blockStack counter.
  137. * @returns {void}
  138. * @private
  139. */
  140. function endBlock() {
  141. blockStack.pop();
  142. }
  143. /**
  144. * Decrements the functionStack counter.
  145. * @returns {void}
  146. * @private
  147. */
  148. function endFunction() {
  149. functionStack.pop();
  150. endBlock();
  151. }
  152. /**
  153. * Check if a variable declaration is a require.
  154. * @param {ASTNode} decl variable declaration Node
  155. * @returns {bool} if decl is a require, return true; else return false.
  156. * @private
  157. */
  158. function isRequire(decl) {
  159. return decl.init && decl.init.type === "CallExpression" && decl.init.callee.name === "require";
  160. }
  161. /**
  162. * Records whether initialized/uninitialized/required variables are defined in current scope.
  163. * @param {string} statementType node.kind, one of: "var", "let", or "const"
  164. * @param {ASTNode[]} declarations List of declarations
  165. * @param {Object} currentScope The scope being investigated
  166. * @returns {void}
  167. * @private
  168. */
  169. function recordTypes(statementType, declarations, currentScope) {
  170. for (let i = 0; i < declarations.length; i++) {
  171. if (declarations[i].init === null) {
  172. if (options[statementType] && options[statementType].uninitialized === MODE_ALWAYS) {
  173. currentScope.uninitialized = true;
  174. }
  175. } else {
  176. if (options[statementType] && options[statementType].initialized === MODE_ALWAYS) {
  177. if (options.separateRequires && isRequire(declarations[i])) {
  178. currentScope.required = true;
  179. } else {
  180. currentScope.initialized = true;
  181. }
  182. }
  183. }
  184. }
  185. }
  186. /**
  187. * Determines the current scope (function or block)
  188. * @param {string} statementType node.kind, one of: "var", "let", or "const"
  189. * @returns {Object} The scope associated with statementType
  190. */
  191. function getCurrentScope(statementType) {
  192. let currentScope;
  193. if (statementType === "var") {
  194. currentScope = functionStack[functionStack.length - 1];
  195. } else if (statementType === "let") {
  196. currentScope = blockStack[blockStack.length - 1].let;
  197. } else if (statementType === "const") {
  198. currentScope = blockStack[blockStack.length - 1].const;
  199. }
  200. return currentScope;
  201. }
  202. /**
  203. * Counts the number of initialized and uninitialized declarations in a list of declarations
  204. * @param {ASTNode[]} declarations List of declarations
  205. * @returns {Object} Counts of 'uninitialized' and 'initialized' declarations
  206. * @private
  207. */
  208. function countDeclarations(declarations) {
  209. const counts = { uninitialized: 0, initialized: 0 };
  210. for (let i = 0; i < declarations.length; i++) {
  211. if (declarations[i].init === null) {
  212. counts.uninitialized++;
  213. } else {
  214. counts.initialized++;
  215. }
  216. }
  217. return counts;
  218. }
  219. /**
  220. * Determines if there is more than one var statement in the current scope.
  221. * @param {string} statementType node.kind, one of: "var", "let", or "const"
  222. * @param {ASTNode[]} declarations List of declarations
  223. * @returns {boolean} Returns true if it is the first var declaration, false if not.
  224. * @private
  225. */
  226. function hasOnlyOneStatement(statementType, declarations) {
  227. const declarationCounts = countDeclarations(declarations);
  228. const currentOptions = options[statementType] || {};
  229. const currentScope = getCurrentScope(statementType);
  230. const hasRequires = declarations.some(isRequire);
  231. if (currentOptions.uninitialized === MODE_ALWAYS && currentOptions.initialized === MODE_ALWAYS) {
  232. if (currentScope.uninitialized || currentScope.initialized) {
  233. if (!hasRequires) {
  234. return false;
  235. }
  236. }
  237. }
  238. if (declarationCounts.uninitialized > 0) {
  239. if (currentOptions.uninitialized === MODE_ALWAYS && currentScope.uninitialized) {
  240. return false;
  241. }
  242. }
  243. if (declarationCounts.initialized > 0) {
  244. if (currentOptions.initialized === MODE_ALWAYS && currentScope.initialized) {
  245. if (!hasRequires) {
  246. return false;
  247. }
  248. }
  249. }
  250. if (currentScope.required && hasRequires) {
  251. return false;
  252. }
  253. recordTypes(statementType, declarations, currentScope);
  254. return true;
  255. }
  256. /**
  257. * Fixer to join VariableDeclaration's into a single declaration
  258. * @param {VariableDeclarator[]} declarations The `VariableDeclaration` to join
  259. * @returns {Function} The fixer function
  260. */
  261. function joinDeclarations(declarations) {
  262. const declaration = declarations[0];
  263. const body = Array.isArray(declaration.parent.parent.body) ? declaration.parent.parent.body : [];
  264. const currentIndex = body.findIndex(node => node.range[0] === declaration.parent.range[0]);
  265. const previousNode = body[currentIndex - 1];
  266. return fixer => {
  267. const type = sourceCode.getTokenBefore(declaration);
  268. const prevSemi = sourceCode.getTokenBefore(type);
  269. const res = [];
  270. if (previousNode && previousNode.kind === sourceCode.getText(type)) {
  271. if (prevSemi.value === ";") {
  272. res.push(fixer.replaceText(prevSemi, ","));
  273. } else {
  274. res.push(fixer.insertTextAfter(prevSemi, ","));
  275. }
  276. res.push(fixer.replaceText(type, ""));
  277. }
  278. return res;
  279. };
  280. }
  281. /**
  282. * Fixer to split a VariableDeclaration into individual declarations
  283. * @param {VariableDeclaration} declaration The `VariableDeclaration` to split
  284. * @returns {Function|null} The fixer function
  285. */
  286. function splitDeclarations(declaration) {
  287. const { parent } = declaration;
  288. // don't autofix code such as: if (foo) var x, y;
  289. if (!isInStatementList(parent.type === "ExportNamedDeclaration" ? parent : declaration)) {
  290. return null;
  291. }
  292. return fixer => declaration.declarations.map(declarator => {
  293. const tokenAfterDeclarator = sourceCode.getTokenAfter(declarator);
  294. if (tokenAfterDeclarator === null) {
  295. return null;
  296. }
  297. const afterComma = sourceCode.getTokenAfter(tokenAfterDeclarator, { includeComments: true });
  298. if (tokenAfterDeclarator.value !== ",") {
  299. return null;
  300. }
  301. const exportPlacement = declaration.parent.type === "ExportNamedDeclaration" ? "export " : "";
  302. /*
  303. * `var x,y`
  304. * tokenAfterDeclarator ^^ afterComma
  305. */
  306. if (afterComma.range[0] === tokenAfterDeclarator.range[1]) {
  307. return fixer.replaceText(tokenAfterDeclarator, `; ${exportPlacement}${declaration.kind} `);
  308. }
  309. /*
  310. * `var x,
  311. * tokenAfterDeclarator ^
  312. * y`
  313. * ^ afterComma
  314. */
  315. if (
  316. afterComma.loc.start.line > tokenAfterDeclarator.loc.end.line ||
  317. afterComma.type === "Line" ||
  318. afterComma.type === "Block"
  319. ) {
  320. let lastComment = afterComma;
  321. while (lastComment.type === "Line" || lastComment.type === "Block") {
  322. lastComment = sourceCode.getTokenAfter(lastComment, { includeComments: true });
  323. }
  324. return fixer.replaceTextRange(
  325. [tokenAfterDeclarator.range[0], lastComment.range[0]],
  326. `;${sourceCode.text.slice(tokenAfterDeclarator.range[1], lastComment.range[0])}${exportPlacement}${declaration.kind} `
  327. );
  328. }
  329. return fixer.replaceText(tokenAfterDeclarator, `; ${exportPlacement}${declaration.kind}`);
  330. }).filter(x => x);
  331. }
  332. /**
  333. * Checks a given VariableDeclaration node for errors.
  334. * @param {ASTNode} node The VariableDeclaration node to check
  335. * @returns {void}
  336. * @private
  337. */
  338. function checkVariableDeclaration(node) {
  339. const parent = node.parent;
  340. const type = node.kind;
  341. if (!options[type]) {
  342. return;
  343. }
  344. const declarations = node.declarations;
  345. const declarationCounts = countDeclarations(declarations);
  346. const mixedRequires = declarations.some(isRequire) && !declarations.every(isRequire);
  347. if (options[type].initialized === MODE_ALWAYS) {
  348. if (options.separateRequires && mixedRequires) {
  349. context.report({
  350. node,
  351. messageId: "splitRequires"
  352. });
  353. }
  354. }
  355. // consecutive
  356. const nodeIndex = (parent.body && parent.body.length > 0 && parent.body.indexOf(node)) || 0;
  357. if (nodeIndex > 0) {
  358. const previousNode = parent.body[nodeIndex - 1];
  359. const isPreviousNodeDeclaration = previousNode.type === "VariableDeclaration";
  360. const declarationsWithPrevious = declarations.concat(previousNode.declarations || []);
  361. if (
  362. isPreviousNodeDeclaration &&
  363. previousNode.kind === type &&
  364. !(declarationsWithPrevious.some(isRequire) && !declarationsWithPrevious.every(isRequire))
  365. ) {
  366. const previousDeclCounts = countDeclarations(previousNode.declarations);
  367. if (options[type].initialized === MODE_CONSECUTIVE && options[type].uninitialized === MODE_CONSECUTIVE) {
  368. context.report({
  369. node,
  370. messageId: "combine",
  371. data: {
  372. type
  373. },
  374. fix: joinDeclarations(declarations)
  375. });
  376. } else if (options[type].initialized === MODE_CONSECUTIVE && declarationCounts.initialized > 0 && previousDeclCounts.initialized > 0) {
  377. context.report({
  378. node,
  379. messageId: "combineInitialized",
  380. data: {
  381. type
  382. },
  383. fix: joinDeclarations(declarations)
  384. });
  385. } else if (options[type].uninitialized === MODE_CONSECUTIVE &&
  386. declarationCounts.uninitialized > 0 &&
  387. previousDeclCounts.uninitialized > 0) {
  388. context.report({
  389. node,
  390. messageId: "combineUninitialized",
  391. data: {
  392. type
  393. },
  394. fix: joinDeclarations(declarations)
  395. });
  396. }
  397. }
  398. }
  399. // always
  400. if (!hasOnlyOneStatement(type, declarations)) {
  401. if (options[type].initialized === MODE_ALWAYS && options[type].uninitialized === MODE_ALWAYS) {
  402. context.report({
  403. node,
  404. messageId: "combine",
  405. data: {
  406. type
  407. },
  408. fix: joinDeclarations(declarations)
  409. });
  410. } else {
  411. if (options[type].initialized === MODE_ALWAYS && declarationCounts.initialized > 0) {
  412. context.report({
  413. node,
  414. messageId: "combineInitialized",
  415. data: {
  416. type
  417. },
  418. fix: joinDeclarations(declarations)
  419. });
  420. }
  421. if (options[type].uninitialized === MODE_ALWAYS && declarationCounts.uninitialized > 0) {
  422. if (node.parent.left === node && (node.parent.type === "ForInStatement" || node.parent.type === "ForOfStatement")) {
  423. return;
  424. }
  425. context.report({
  426. node,
  427. messageId: "combineUninitialized",
  428. data: {
  429. type
  430. },
  431. fix: joinDeclarations(declarations)
  432. });
  433. }
  434. }
  435. }
  436. // never
  437. if (parent.type !== "ForStatement" || parent.init !== node) {
  438. const totalDeclarations = declarationCounts.uninitialized + declarationCounts.initialized;
  439. if (totalDeclarations > 1) {
  440. if (options[type].initialized === MODE_NEVER && options[type].uninitialized === MODE_NEVER) {
  441. // both initialized and uninitialized
  442. context.report({
  443. node,
  444. messageId: "split",
  445. data: {
  446. type
  447. },
  448. fix: splitDeclarations(node)
  449. });
  450. } else if (options[type].initialized === MODE_NEVER && declarationCounts.initialized > 0) {
  451. // initialized
  452. context.report({
  453. node,
  454. messageId: "splitInitialized",
  455. data: {
  456. type
  457. },
  458. fix: splitDeclarations(node)
  459. });
  460. } else if (options[type].uninitialized === MODE_NEVER && declarationCounts.uninitialized > 0) {
  461. // uninitialized
  462. context.report({
  463. node,
  464. messageId: "splitUninitialized",
  465. data: {
  466. type
  467. },
  468. fix: splitDeclarations(node)
  469. });
  470. }
  471. }
  472. }
  473. }
  474. //--------------------------------------------------------------------------
  475. // Public API
  476. //--------------------------------------------------------------------------
  477. return {
  478. Program: startFunction,
  479. FunctionDeclaration: startFunction,
  480. FunctionExpression: startFunction,
  481. ArrowFunctionExpression: startFunction,
  482. StaticBlock: startFunction, // StaticBlock creates a new scope for `var` variables
  483. BlockStatement: startBlock,
  484. ForStatement: startBlock,
  485. ForInStatement: startBlock,
  486. ForOfStatement: startBlock,
  487. SwitchStatement: startBlock,
  488. VariableDeclaration: checkVariableDeclaration,
  489. "ForStatement:exit": endBlock,
  490. "ForOfStatement:exit": endBlock,
  491. "ForInStatement:exit": endBlock,
  492. "SwitchStatement:exit": endBlock,
  493. "BlockStatement:exit": endBlock,
  494. "Program:exit": endFunction,
  495. "FunctionDeclaration:exit": endFunction,
  496. "FunctionExpression:exit": endFunction,
  497. "ArrowFunctionExpression:exit": endFunction,
  498. "StaticBlock:exit": endFunction
  499. };
  500. }
  501. };