one-var.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  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. /*
  282. * `var x,y`
  283. * tokenAfterDeclarator ^^ afterComma
  284. */
  285. if (afterComma.range[0] === tokenAfterDeclarator.range[1]) {
  286. return fixer.replaceText(tokenAfterDeclarator, `; ${declaration.kind} `);
  287. }
  288. /*
  289. * `var x,
  290. * tokenAfterDeclarator ^
  291. * y`
  292. * ^ afterComma
  293. */
  294. if (
  295. afterComma.loc.start.line > tokenAfterDeclarator.loc.end.line ||
  296. afterComma.type === "Line" ||
  297. afterComma.type === "Block"
  298. ) {
  299. let lastComment = afterComma;
  300. while (lastComment.type === "Line" || lastComment.type === "Block") {
  301. lastComment = sourceCode.getTokenAfter(lastComment, { includeComments: true });
  302. }
  303. return fixer.replaceTextRange(
  304. [tokenAfterDeclarator.range[0], lastComment.range[0]],
  305. `;${sourceCode.text.slice(tokenAfterDeclarator.range[1], lastComment.range[0])}${declaration.kind} `
  306. );
  307. }
  308. return fixer.replaceText(tokenAfterDeclarator, `; ${declaration.kind}`);
  309. }).filter(x => x);
  310. }
  311. /**
  312. * Checks a given VariableDeclaration node for errors.
  313. * @param {ASTNode} node The VariableDeclaration node to check
  314. * @returns {void}
  315. * @private
  316. */
  317. function checkVariableDeclaration(node) {
  318. const parent = node.parent;
  319. const type = node.kind;
  320. if (!options[type]) {
  321. return;
  322. }
  323. const declarations = node.declarations;
  324. const declarationCounts = countDeclarations(declarations);
  325. const mixedRequires = declarations.some(isRequire) && !declarations.every(isRequire);
  326. if (options[type].initialized === MODE_ALWAYS) {
  327. if (options.separateRequires && mixedRequires) {
  328. context.report({
  329. node,
  330. messageId: "splitRequires"
  331. });
  332. }
  333. }
  334. // consecutive
  335. const nodeIndex = (parent.body && parent.body.length > 0 && parent.body.indexOf(node)) || 0;
  336. if (nodeIndex > 0) {
  337. const previousNode = parent.body[nodeIndex - 1];
  338. const isPreviousNodeDeclaration = previousNode.type === "VariableDeclaration";
  339. const declarationsWithPrevious = declarations.concat(previousNode.declarations || []);
  340. if (
  341. isPreviousNodeDeclaration &&
  342. previousNode.kind === type &&
  343. !(declarationsWithPrevious.some(isRequire) && !declarationsWithPrevious.every(isRequire))
  344. ) {
  345. const previousDeclCounts = countDeclarations(previousNode.declarations);
  346. if (options[type].initialized === MODE_CONSECUTIVE && options[type].uninitialized === MODE_CONSECUTIVE) {
  347. context.report({
  348. node,
  349. messageId: "combine",
  350. data: {
  351. type
  352. },
  353. fix: joinDeclarations(declarations)
  354. });
  355. } else if (options[type].initialized === MODE_CONSECUTIVE && declarationCounts.initialized > 0 && previousDeclCounts.initialized > 0) {
  356. context.report({
  357. node,
  358. messageId: "combineInitialized",
  359. data: {
  360. type
  361. },
  362. fix: joinDeclarations(declarations)
  363. });
  364. } else if (options[type].uninitialized === MODE_CONSECUTIVE &&
  365. declarationCounts.uninitialized > 0 &&
  366. previousDeclCounts.uninitialized > 0) {
  367. context.report({
  368. node,
  369. messageId: "combineUninitialized",
  370. data: {
  371. type
  372. },
  373. fix: joinDeclarations(declarations)
  374. });
  375. }
  376. }
  377. }
  378. // always
  379. if (!hasOnlyOneStatement(type, declarations)) {
  380. if (options[type].initialized === MODE_ALWAYS && options[type].uninitialized === MODE_ALWAYS) {
  381. context.report({
  382. node,
  383. messageId: "combine",
  384. data: {
  385. type
  386. },
  387. fix: joinDeclarations(declarations)
  388. });
  389. } else {
  390. if (options[type].initialized === MODE_ALWAYS && declarationCounts.initialized > 0) {
  391. context.report({
  392. node,
  393. messageId: "combineInitialized",
  394. data: {
  395. type
  396. },
  397. fix: joinDeclarations(declarations)
  398. });
  399. }
  400. if (options[type].uninitialized === MODE_ALWAYS && declarationCounts.uninitialized > 0) {
  401. if (node.parent.left === node && (node.parent.type === "ForInStatement" || node.parent.type === "ForOfStatement")) {
  402. return;
  403. }
  404. context.report({
  405. node,
  406. messageId: "combineUninitialized",
  407. data: {
  408. type
  409. },
  410. fix: joinDeclarations(declarations)
  411. });
  412. }
  413. }
  414. }
  415. // never
  416. if (parent.type !== "ForStatement" || parent.init !== node) {
  417. const totalDeclarations = declarationCounts.uninitialized + declarationCounts.initialized;
  418. if (totalDeclarations > 1) {
  419. if (options[type].initialized === MODE_NEVER && options[type].uninitialized === MODE_NEVER) {
  420. // both initialized and uninitialized
  421. context.report({
  422. node,
  423. messageId: "split",
  424. data: {
  425. type
  426. },
  427. fix: splitDeclarations(node)
  428. });
  429. } else if (options[type].initialized === MODE_NEVER && declarationCounts.initialized > 0) {
  430. // initialized
  431. context.report({
  432. node,
  433. messageId: "splitInitialized",
  434. data: {
  435. type
  436. },
  437. fix: splitDeclarations(node)
  438. });
  439. } else if (options[type].uninitialized === MODE_NEVER && declarationCounts.uninitialized > 0) {
  440. // uninitialized
  441. context.report({
  442. node,
  443. messageId: "splitUninitialized",
  444. data: {
  445. type
  446. },
  447. fix: splitDeclarations(node)
  448. });
  449. }
  450. }
  451. }
  452. }
  453. //--------------------------------------------------------------------------
  454. // Public API
  455. //--------------------------------------------------------------------------
  456. return {
  457. Program: startFunction,
  458. FunctionDeclaration: startFunction,
  459. FunctionExpression: startFunction,
  460. ArrowFunctionExpression: startFunction,
  461. BlockStatement: startBlock,
  462. ForStatement: startBlock,
  463. ForInStatement: startBlock,
  464. ForOfStatement: startBlock,
  465. SwitchStatement: startBlock,
  466. VariableDeclaration: checkVariableDeclaration,
  467. "ForStatement:exit": endBlock,
  468. "ForOfStatement:exit": endBlock,
  469. "ForInStatement:exit": endBlock,
  470. "SwitchStatement:exit": endBlock,
  471. "BlockStatement:exit": endBlock,
  472. "Program:exit": endFunction,
  473. "FunctionDeclaration:exit": endFunction,
  474. "FunctionExpression:exit": endFunction,
  475. "ArrowFunctionExpression:exit": endFunction
  476. };
  477. }
  478. };