operator-linebreak.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. /**
  2. * @fileoverview Operator linebreak - enforces operator linebreak style of two types: after and before
  3. * @author Benoît Zugmeyer
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Rule Definition
  12. //------------------------------------------------------------------------------
  13. module.exports = {
  14. meta: {
  15. type: "layout",
  16. docs: {
  17. description: "enforce consistent linebreak style for operators",
  18. category: "Stylistic Issues",
  19. recommended: false,
  20. url: "https://eslint.org/docs/rules/operator-linebreak"
  21. },
  22. schema: [
  23. {
  24. enum: ["after", "before", "none", null]
  25. },
  26. {
  27. type: "object",
  28. properties: {
  29. overrides: {
  30. type: "object",
  31. additionalProperties: {
  32. enum: ["after", "before", "none", "ignore"]
  33. }
  34. }
  35. },
  36. additionalProperties: false
  37. }
  38. ],
  39. fixable: "code",
  40. messages: {
  41. operatorAtBeginning: "'{{operator}}' should be placed at the beginning of the line.",
  42. operatorAtEnd: "'{{operator}}' should be placed at the end of the line.",
  43. badLinebreak: "Bad line breaking before and after '{{operator}}'.",
  44. noLinebreak: "There should be no line break before or after '{{operator}}'."
  45. }
  46. },
  47. create(context) {
  48. const usedDefaultGlobal = !context.options[0];
  49. const globalStyle = context.options[0] || "after";
  50. const options = context.options[1] || {};
  51. const styleOverrides = options.overrides ? Object.assign({}, options.overrides) : {};
  52. if (usedDefaultGlobal && !styleOverrides["?"]) {
  53. styleOverrides["?"] = "before";
  54. }
  55. if (usedDefaultGlobal && !styleOverrides[":"]) {
  56. styleOverrides[":"] = "before";
  57. }
  58. const sourceCode = context.getSourceCode();
  59. //--------------------------------------------------------------------------
  60. // Helpers
  61. //--------------------------------------------------------------------------
  62. /**
  63. * Gets a fixer function to fix rule issues
  64. * @param {Token} operatorToken The operator token of an expression
  65. * @param {string} desiredStyle The style for the rule. One of 'before', 'after', 'none'
  66. * @returns {Function} A fixer function
  67. */
  68. function getFixer(operatorToken, desiredStyle) {
  69. return fixer => {
  70. const tokenBefore = sourceCode.getTokenBefore(operatorToken);
  71. const tokenAfter = sourceCode.getTokenAfter(operatorToken);
  72. const textBefore = sourceCode.text.slice(tokenBefore.range[1], operatorToken.range[0]);
  73. const textAfter = sourceCode.text.slice(operatorToken.range[1], tokenAfter.range[0]);
  74. const hasLinebreakBefore = !astUtils.isTokenOnSameLine(tokenBefore, operatorToken);
  75. const hasLinebreakAfter = !astUtils.isTokenOnSameLine(operatorToken, tokenAfter);
  76. let newTextBefore, newTextAfter;
  77. if (hasLinebreakBefore !== hasLinebreakAfter && desiredStyle !== "none") {
  78. // If there is a comment before and after the operator, don't do a fix.
  79. if (sourceCode.getTokenBefore(operatorToken, { includeComments: true }) !== tokenBefore &&
  80. sourceCode.getTokenAfter(operatorToken, { includeComments: true }) !== tokenAfter) {
  81. return null;
  82. }
  83. /*
  84. * If there is only one linebreak and it's on the wrong side of the operator, swap the text before and after the operator.
  85. * foo &&
  86. * bar
  87. * would get fixed to
  88. * foo
  89. * && bar
  90. */
  91. newTextBefore = textAfter;
  92. newTextAfter = textBefore;
  93. } else {
  94. const LINEBREAK_REGEX = astUtils.createGlobalLinebreakMatcher();
  95. // Otherwise, if no linebreak is desired and no comments interfere, replace the linebreaks with empty strings.
  96. newTextBefore = desiredStyle === "before" || textBefore.trim() ? textBefore : textBefore.replace(LINEBREAK_REGEX, "");
  97. newTextAfter = desiredStyle === "after" || textAfter.trim() ? textAfter : textAfter.replace(LINEBREAK_REGEX, "");
  98. // If there was no change (due to interfering comments), don't output a fix.
  99. if (newTextBefore === textBefore && newTextAfter === textAfter) {
  100. return null;
  101. }
  102. }
  103. if (newTextAfter === "" && tokenAfter.type === "Punctuator" && "+-".includes(operatorToken.value) && tokenAfter.value === operatorToken.value) {
  104. // To avoid accidentally creating a ++ or -- operator, insert a space if the operator is a +/- and the following token is a unary +/-.
  105. newTextAfter += " ";
  106. }
  107. return fixer.replaceTextRange([tokenBefore.range[1], tokenAfter.range[0]], newTextBefore + operatorToken.value + newTextAfter);
  108. };
  109. }
  110. /**
  111. * Checks the operator placement
  112. * @param {ASTNode} node The node to check
  113. * @param {ASTNode} leftSide The node that comes before the operator in `node`
  114. * @private
  115. * @returns {void}
  116. */
  117. function validateNode(node, leftSide) {
  118. /*
  119. * When the left part of a binary expression is a single expression wrapped in
  120. * parentheses (ex: `(a) + b`), leftToken will be the last token of the expression
  121. * and operatorToken will be the closing parenthesis.
  122. * The leftToken should be the last closing parenthesis, and the operatorToken
  123. * should be the token right after that.
  124. */
  125. const operatorToken = sourceCode.getTokenAfter(leftSide, astUtils.isNotClosingParenToken);
  126. const leftToken = sourceCode.getTokenBefore(operatorToken);
  127. const rightToken = sourceCode.getTokenAfter(operatorToken);
  128. const operator = operatorToken.value;
  129. const operatorStyleOverride = styleOverrides[operator];
  130. const style = operatorStyleOverride || globalStyle;
  131. const fix = getFixer(operatorToken, style);
  132. // if single line
  133. if (astUtils.isTokenOnSameLine(leftToken, operatorToken) &&
  134. astUtils.isTokenOnSameLine(operatorToken, rightToken)) {
  135. // do nothing.
  136. } else if (operatorStyleOverride !== "ignore" && !astUtils.isTokenOnSameLine(leftToken, operatorToken) &&
  137. !astUtils.isTokenOnSameLine(operatorToken, rightToken)) {
  138. // lone operator
  139. context.report({
  140. node,
  141. loc: operatorToken.loc,
  142. messageId: "badLinebreak",
  143. data: {
  144. operator
  145. },
  146. fix
  147. });
  148. } else if (style === "before" && astUtils.isTokenOnSameLine(leftToken, operatorToken)) {
  149. context.report({
  150. node,
  151. loc: operatorToken.loc,
  152. messageId: "operatorAtBeginning",
  153. data: {
  154. operator
  155. },
  156. fix
  157. });
  158. } else if (style === "after" && astUtils.isTokenOnSameLine(operatorToken, rightToken)) {
  159. context.report({
  160. node,
  161. loc: operatorToken.loc,
  162. messageId: "operatorAtEnd",
  163. data: {
  164. operator
  165. },
  166. fix
  167. });
  168. } else if (style === "none") {
  169. context.report({
  170. node,
  171. loc: operatorToken.loc,
  172. messageId: "noLinebreak",
  173. data: {
  174. operator
  175. },
  176. fix
  177. });
  178. }
  179. }
  180. /**
  181. * Validates a binary expression using `validateNode`
  182. * @param {BinaryExpression|LogicalExpression|AssignmentExpression} node node to be validated
  183. * @returns {void}
  184. */
  185. function validateBinaryExpression(node) {
  186. validateNode(node, node.left);
  187. }
  188. //--------------------------------------------------------------------------
  189. // Public
  190. //--------------------------------------------------------------------------
  191. return {
  192. BinaryExpression: validateBinaryExpression,
  193. LogicalExpression: validateBinaryExpression,
  194. AssignmentExpression: validateBinaryExpression,
  195. VariableDeclarator(node) {
  196. if (node.init) {
  197. validateNode(node, node.id);
  198. }
  199. },
  200. ConditionalExpression(node) {
  201. validateNode(node, node.test);
  202. validateNode(node, node.consequent);
  203. }
  204. };
  205. }
  206. };