multiline-ternary.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. /**
  2. * @fileoverview Enforce newlines between operands of ternary expressions
  3. * @author Kai Cataldo
  4. */
  5. "use strict";
  6. const astUtils = require("./utils/ast-utils");
  7. //------------------------------------------------------------------------------
  8. // Rule Definition
  9. //------------------------------------------------------------------------------
  10. /** @type {import('../shared/types').Rule} */
  11. module.exports = {
  12. meta: {
  13. type: "layout",
  14. docs: {
  15. description: "Enforce newlines between operands of ternary expressions",
  16. recommended: false,
  17. url: "https://eslint.org/docs/rules/multiline-ternary"
  18. },
  19. schema: [
  20. {
  21. enum: ["always", "always-multiline", "never"]
  22. }
  23. ],
  24. messages: {
  25. expectedTestCons: "Expected newline between test and consequent of ternary expression.",
  26. expectedConsAlt: "Expected newline between consequent and alternate of ternary expression.",
  27. unexpectedTestCons: "Unexpected newline between test and consequent of ternary expression.",
  28. unexpectedConsAlt: "Unexpected newline between consequent and alternate of ternary expression."
  29. },
  30. fixable: "whitespace"
  31. },
  32. create(context) {
  33. const sourceCode = context.getSourceCode();
  34. const option = context.options[0];
  35. const multiline = option !== "never";
  36. const allowSingleLine = option === "always-multiline";
  37. //--------------------------------------------------------------------------
  38. // Public
  39. //--------------------------------------------------------------------------
  40. return {
  41. ConditionalExpression(node) {
  42. const questionToken = sourceCode.getTokenAfter(node.test, astUtils.isNotClosingParenToken);
  43. const colonToken = sourceCode.getTokenAfter(node.consequent, astUtils.isNotClosingParenToken);
  44. const firstTokenOfTest = sourceCode.getFirstToken(node);
  45. const lastTokenOfTest = sourceCode.getTokenBefore(questionToken);
  46. const firstTokenOfConsequent = sourceCode.getTokenAfter(questionToken);
  47. const lastTokenOfConsequent = sourceCode.getTokenBefore(colonToken);
  48. const firstTokenOfAlternate = sourceCode.getTokenAfter(colonToken);
  49. const areTestAndConsequentOnSameLine = astUtils.isTokenOnSameLine(lastTokenOfTest, firstTokenOfConsequent);
  50. const areConsequentAndAlternateOnSameLine = astUtils.isTokenOnSameLine(lastTokenOfConsequent, firstTokenOfAlternate);
  51. const hasComments = !!sourceCode.getCommentsInside(node).length;
  52. if (!multiline) {
  53. if (!areTestAndConsequentOnSameLine) {
  54. context.report({
  55. node: node.test,
  56. loc: {
  57. start: firstTokenOfTest.loc.start,
  58. end: lastTokenOfTest.loc.end
  59. },
  60. messageId: "unexpectedTestCons",
  61. fix(fixer) {
  62. if (hasComments) {
  63. return null;
  64. }
  65. const fixers = [];
  66. const areTestAndQuestionOnSameLine = astUtils.isTokenOnSameLine(lastTokenOfTest, questionToken);
  67. const areQuestionAndConsOnSameLine = astUtils.isTokenOnSameLine(questionToken, firstTokenOfConsequent);
  68. if (!areTestAndQuestionOnSameLine) {
  69. fixers.push(fixer.removeRange([lastTokenOfTest.range[1], questionToken.range[0]]));
  70. }
  71. if (!areQuestionAndConsOnSameLine) {
  72. fixers.push(fixer.removeRange([questionToken.range[1], firstTokenOfConsequent.range[0]]));
  73. }
  74. return fixers;
  75. }
  76. });
  77. }
  78. if (!areConsequentAndAlternateOnSameLine) {
  79. context.report({
  80. node: node.consequent,
  81. loc: {
  82. start: firstTokenOfConsequent.loc.start,
  83. end: lastTokenOfConsequent.loc.end
  84. },
  85. messageId: "unexpectedConsAlt",
  86. fix(fixer) {
  87. if (hasComments) {
  88. return null;
  89. }
  90. const fixers = [];
  91. const areConsAndColonOnSameLine = astUtils.isTokenOnSameLine(lastTokenOfConsequent, colonToken);
  92. const areColonAndAltOnSameLine = astUtils.isTokenOnSameLine(colonToken, firstTokenOfAlternate);
  93. if (!areConsAndColonOnSameLine) {
  94. fixers.push(fixer.removeRange([lastTokenOfConsequent.range[1], colonToken.range[0]]));
  95. }
  96. if (!areColonAndAltOnSameLine) {
  97. fixers.push(fixer.removeRange([colonToken.range[1], firstTokenOfAlternate.range[0]]));
  98. }
  99. return fixers;
  100. }
  101. });
  102. }
  103. } else {
  104. if (allowSingleLine && node.loc.start.line === node.loc.end.line) {
  105. return;
  106. }
  107. if (areTestAndConsequentOnSameLine) {
  108. context.report({
  109. node: node.test,
  110. loc: {
  111. start: firstTokenOfTest.loc.start,
  112. end: lastTokenOfTest.loc.end
  113. },
  114. messageId: "expectedTestCons",
  115. fix: fixer => (hasComments ? null : (
  116. fixer.replaceTextRange(
  117. [
  118. lastTokenOfTest.range[1],
  119. questionToken.range[0]
  120. ],
  121. "\n"
  122. )
  123. ))
  124. });
  125. }
  126. if (areConsequentAndAlternateOnSameLine) {
  127. context.report({
  128. node: node.consequent,
  129. loc: {
  130. start: firstTokenOfConsequent.loc.start,
  131. end: lastTokenOfConsequent.loc.end
  132. },
  133. messageId: "expectedConsAlt",
  134. fix: (fixer => (hasComments ? null : (
  135. fixer.replaceTextRange(
  136. [
  137. lastTokenOfConsequent.range[1],
  138. colonToken.range[0]
  139. ],
  140. "\n"
  141. )
  142. )))
  143. });
  144. }
  145. }
  146. }
  147. };
  148. }
  149. };