operator-linebreak.js 10.0 KB

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