one-var.js 21 KB

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