one-var.js 21 KB

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