operator-linebreak.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. /**
  2. * @fileoverview Operator linebreak - enforces operator linebreak style of two types: after and before
  3. * @author Benoît Zugmeyer
  4. * @deprecated in ESLint v8.53.0
  5. */
  6. "use strict";
  7. //------------------------------------------------------------------------------
  8. // Requirements
  9. //------------------------------------------------------------------------------
  10. const astUtils = require("./utils/ast-utils");
  11. //------------------------------------------------------------------------------
  12. // Rule Definition
  13. //------------------------------------------------------------------------------
  14. /** @type {import('../types').Rule.RuleModule} */
  15. module.exports = {
  16. meta: {
  17. deprecated: {
  18. message: "Formatting rules are being moved out of ESLint core.",
  19. url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
  20. deprecatedSince: "8.53.0",
  21. availableUntil: "11.0.0",
  22. replacedBy: [
  23. {
  24. message:
  25. "ESLint Stylistic now maintains deprecated stylistic core rules.",
  26. url: "https://eslint.style/guide/migration",
  27. plugin: {
  28. name: "@stylistic/eslint-plugin",
  29. url: "https://eslint.style",
  30. },
  31. rule: {
  32. name: "operator-linebreak",
  33. url: "https://eslint.style/rules/operator-linebreak",
  34. },
  35. },
  36. ],
  37. },
  38. type: "layout",
  39. docs: {
  40. description: "Enforce consistent linebreak style for operators",
  41. recommended: false,
  42. url: "https://eslint.org/docs/latest/rules/operator-linebreak",
  43. },
  44. schema: [
  45. {
  46. enum: ["after", "before", "none", null],
  47. },
  48. {
  49. type: "object",
  50. properties: {
  51. overrides: {
  52. type: "object",
  53. additionalProperties: {
  54. enum: ["after", "before", "none", "ignore"],
  55. },
  56. },
  57. },
  58. additionalProperties: false,
  59. },
  60. ],
  61. fixable: "code",
  62. messages: {
  63. operatorAtBeginning:
  64. "'{{operator}}' should be placed at the beginning of the line.",
  65. operatorAtEnd:
  66. "'{{operator}}' should be placed at the end of the line.",
  67. badLinebreak: "Bad line breaking before and after '{{operator}}'.",
  68. noLinebreak:
  69. "There should be no line break before or after '{{operator}}'.",
  70. },
  71. },
  72. create(context) {
  73. const usedDefaultGlobal = !context.options[0];
  74. const globalStyle = context.options[0] || "after";
  75. const options = context.options[1] || {};
  76. const styleOverrides = options.overrides
  77. ? Object.assign({}, options.overrides)
  78. : {};
  79. if (usedDefaultGlobal && !styleOverrides["?"]) {
  80. styleOverrides["?"] = "before";
  81. }
  82. if (usedDefaultGlobal && !styleOverrides[":"]) {
  83. styleOverrides[":"] = "before";
  84. }
  85. const sourceCode = context.sourceCode;
  86. //--------------------------------------------------------------------------
  87. // Helpers
  88. //--------------------------------------------------------------------------
  89. /**
  90. * Gets a fixer function to fix rule issues
  91. * @param {Token} operatorToken The operator token of an expression
  92. * @param {string} desiredStyle The style for the rule. One of 'before', 'after', 'none'
  93. * @returns {Function} A fixer function
  94. */
  95. function getFixer(operatorToken, desiredStyle) {
  96. return fixer => {
  97. const tokenBefore = sourceCode.getTokenBefore(operatorToken);
  98. const tokenAfter = sourceCode.getTokenAfter(operatorToken);
  99. const textBefore = sourceCode.text.slice(
  100. tokenBefore.range[1],
  101. operatorToken.range[0],
  102. );
  103. const textAfter = sourceCode.text.slice(
  104. operatorToken.range[1],
  105. tokenAfter.range[0],
  106. );
  107. const hasLinebreakBefore = !astUtils.isTokenOnSameLine(
  108. tokenBefore,
  109. operatorToken,
  110. );
  111. const hasLinebreakAfter = !astUtils.isTokenOnSameLine(
  112. operatorToken,
  113. tokenAfter,
  114. );
  115. let newTextBefore, newTextAfter;
  116. if (
  117. hasLinebreakBefore !== hasLinebreakAfter &&
  118. desiredStyle !== "none"
  119. ) {
  120. // If there is a comment before and after the operator, don't do a fix.
  121. if (
  122. sourceCode.getTokenBefore(operatorToken, {
  123. includeComments: true,
  124. }) !== tokenBefore &&
  125. sourceCode.getTokenAfter(operatorToken, {
  126. includeComments: true,
  127. }) !== tokenAfter
  128. ) {
  129. return null;
  130. }
  131. /*
  132. * If there is only one linebreak and it's on the wrong side of the operator, swap the text before and after the operator.
  133. * foo &&
  134. * bar
  135. * would get fixed to
  136. * foo
  137. * && bar
  138. */
  139. newTextBefore = textAfter;
  140. newTextAfter = textBefore;
  141. } else {
  142. const LINEBREAK_REGEX =
  143. astUtils.createGlobalLinebreakMatcher();
  144. // Otherwise, if no linebreak is desired and no comments interfere, replace the linebreaks with empty strings.
  145. newTextBefore =
  146. desiredStyle === "before" || textBefore.trim()
  147. ? textBefore
  148. : textBefore.replace(LINEBREAK_REGEX, "");
  149. newTextAfter =
  150. desiredStyle === "after" || textAfter.trim()
  151. ? textAfter
  152. : textAfter.replace(LINEBREAK_REGEX, "");
  153. // If there was no change (due to interfering comments), don't output a fix.
  154. if (
  155. newTextBefore === textBefore &&
  156. newTextAfter === textAfter
  157. ) {
  158. return null;
  159. }
  160. }
  161. if (
  162. newTextAfter === "" &&
  163. tokenAfter.type === "Punctuator" &&
  164. "+-".includes(operatorToken.value) &&
  165. tokenAfter.value === operatorToken.value
  166. ) {
  167. // To avoid accidentally creating a ++ or -- operator, insert a space if the operator is a +/- and the following token is a unary +/-.
  168. newTextAfter += " ";
  169. }
  170. return fixer.replaceTextRange(
  171. [tokenBefore.range[1], tokenAfter.range[0]],
  172. newTextBefore + operatorToken.value + newTextAfter,
  173. );
  174. };
  175. }
  176. /**
  177. * Checks the operator placement
  178. * @param {ASTNode} node The node to check
  179. * @param {ASTNode} rightSide The node that comes after the operator in `node`
  180. * @param {string} operator The operator
  181. * @private
  182. * @returns {void}
  183. */
  184. function validateNode(node, rightSide, operator) {
  185. /*
  186. * Find the operator token by searching from the right side, because between the left side and the operator
  187. * there could be additional tokens from type annotations. Search specifically for the token which
  188. * value equals the operator, in order to skip possible opening parentheses before the right side node.
  189. */
  190. const operatorToken = sourceCode.getTokenBefore(
  191. rightSide,
  192. token => token.value === operator,
  193. );
  194. const leftToken = sourceCode.getTokenBefore(operatorToken);
  195. const rightToken = sourceCode.getTokenAfter(operatorToken);
  196. const operatorStyleOverride = styleOverrides[operator];
  197. const style = operatorStyleOverride || globalStyle;
  198. const fix = getFixer(operatorToken, style);
  199. // if single line
  200. if (
  201. astUtils.isTokenOnSameLine(leftToken, operatorToken) &&
  202. astUtils.isTokenOnSameLine(operatorToken, rightToken)
  203. ) {
  204. // do nothing.
  205. } else if (
  206. operatorStyleOverride !== "ignore" &&
  207. !astUtils.isTokenOnSameLine(leftToken, operatorToken) &&
  208. !astUtils.isTokenOnSameLine(operatorToken, rightToken)
  209. ) {
  210. // lone operator
  211. context.report({
  212. node,
  213. loc: operatorToken.loc,
  214. messageId: "badLinebreak",
  215. data: {
  216. operator,
  217. },
  218. fix,
  219. });
  220. } else if (
  221. style === "before" &&
  222. astUtils.isTokenOnSameLine(leftToken, operatorToken)
  223. ) {
  224. context.report({
  225. node,
  226. loc: operatorToken.loc,
  227. messageId: "operatorAtBeginning",
  228. data: {
  229. operator,
  230. },
  231. fix,
  232. });
  233. } else if (
  234. style === "after" &&
  235. astUtils.isTokenOnSameLine(operatorToken, rightToken)
  236. ) {
  237. context.report({
  238. node,
  239. loc: operatorToken.loc,
  240. messageId: "operatorAtEnd",
  241. data: {
  242. operator,
  243. },
  244. fix,
  245. });
  246. } else if (style === "none") {
  247. context.report({
  248. node,
  249. loc: operatorToken.loc,
  250. messageId: "noLinebreak",
  251. data: {
  252. operator,
  253. },
  254. fix,
  255. });
  256. }
  257. }
  258. /**
  259. * Validates a binary expression using `validateNode`
  260. * @param {BinaryExpression|LogicalExpression|AssignmentExpression} node node to be validated
  261. * @returns {void}
  262. */
  263. function validateBinaryExpression(node) {
  264. validateNode(node, node.right, node.operator);
  265. }
  266. //--------------------------------------------------------------------------
  267. // Public
  268. //--------------------------------------------------------------------------
  269. return {
  270. BinaryExpression: validateBinaryExpression,
  271. LogicalExpression: validateBinaryExpression,
  272. AssignmentExpression: validateBinaryExpression,
  273. VariableDeclarator(node) {
  274. if (node.init) {
  275. validateNode(node, node.init, "=");
  276. }
  277. },
  278. PropertyDefinition(node) {
  279. if (node.value) {
  280. validateNode(node, node.value, "=");
  281. }
  282. },
  283. ConditionalExpression(node) {
  284. validateNode(node, node.consequent, "?");
  285. validateNode(node, node.alternate, ":");
  286. },
  287. };
  288. },
  289. };