operator-linebreak.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  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. /** @type {import('../shared/types').Rule} */
  14. module.exports = {
  15. meta: {
  16. type: "layout",
  17. docs: {
  18. description: "Enforce consistent linebreak style for operators",
  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} rightSide The node that comes after the operator in `node`
  114. * @param {string} operator The operator
  115. * @private
  116. * @returns {void}
  117. */
  118. function validateNode(node, rightSide, operator) {
  119. /*
  120. * Find the operator token by searching from the right side, because between the left side and the operator
  121. * there could be additional tokens from type annotations. Search specifically for the token which
  122. * value equals the operator, in order to skip possible opening parentheses before the right side node.
  123. */
  124. const operatorToken = sourceCode.getTokenBefore(rightSide, token => token.value === operator);
  125. const leftToken = sourceCode.getTokenBefore(operatorToken);
  126. const rightToken = sourceCode.getTokenAfter(operatorToken);
  127. const operatorStyleOverride = styleOverrides[operator];
  128. const style = operatorStyleOverride || globalStyle;
  129. const fix = getFixer(operatorToken, style);
  130. // if single line
  131. if (astUtils.isTokenOnSameLine(leftToken, operatorToken) &&
  132. astUtils.isTokenOnSameLine(operatorToken, rightToken)) {
  133. // do nothing.
  134. } else if (operatorStyleOverride !== "ignore" && !astUtils.isTokenOnSameLine(leftToken, operatorToken) &&
  135. !astUtils.isTokenOnSameLine(operatorToken, rightToken)) {
  136. // lone operator
  137. context.report({
  138. node,
  139. loc: operatorToken.loc,
  140. messageId: "badLinebreak",
  141. data: {
  142. operator
  143. },
  144. fix
  145. });
  146. } else if (style === "before" && astUtils.isTokenOnSameLine(leftToken, operatorToken)) {
  147. context.report({
  148. node,
  149. loc: operatorToken.loc,
  150. messageId: "operatorAtBeginning",
  151. data: {
  152. operator
  153. },
  154. fix
  155. });
  156. } else if (style === "after" && astUtils.isTokenOnSameLine(operatorToken, rightToken)) {
  157. context.report({
  158. node,
  159. loc: operatorToken.loc,
  160. messageId: "operatorAtEnd",
  161. data: {
  162. operator
  163. },
  164. fix
  165. });
  166. } else if (style === "none") {
  167. context.report({
  168. node,
  169. loc: operatorToken.loc,
  170. messageId: "noLinebreak",
  171. data: {
  172. operator
  173. },
  174. fix
  175. });
  176. }
  177. }
  178. /**
  179. * Validates a binary expression using `validateNode`
  180. * @param {BinaryExpression|LogicalExpression|AssignmentExpression} node node to be validated
  181. * @returns {void}
  182. */
  183. function validateBinaryExpression(node) {
  184. validateNode(node, node.right, node.operator);
  185. }
  186. //--------------------------------------------------------------------------
  187. // Public
  188. //--------------------------------------------------------------------------
  189. return {
  190. BinaryExpression: validateBinaryExpression,
  191. LogicalExpression: validateBinaryExpression,
  192. AssignmentExpression: validateBinaryExpression,
  193. VariableDeclarator(node) {
  194. if (node.init) {
  195. validateNode(node, node.init, "=");
  196. }
  197. },
  198. PropertyDefinition(node) {
  199. if (node.value) {
  200. validateNode(node, node.value, "=");
  201. }
  202. },
  203. ConditionalExpression(node) {
  204. validateNode(node, node.consequent, "?");
  205. validateNode(node, node.alternate, ":");
  206. }
  207. };
  208. }
  209. };