one-var.js 21 KB

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