function-paren-newline.js 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. /**
  2. * @fileoverview enforce consistent line breaks inside function parentheses
  3. * @author Teddy Katz
  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: "function-paren-newline",
  33. url: "https://eslint.style/rules/function-paren-newline",
  34. },
  35. },
  36. ],
  37. },
  38. type: "layout",
  39. docs: {
  40. description:
  41. "Enforce consistent line breaks inside function parentheses",
  42. recommended: false,
  43. url: "https://eslint.org/docs/latest/rules/function-paren-newline",
  44. },
  45. fixable: "whitespace",
  46. schema: [
  47. {
  48. oneOf: [
  49. {
  50. enum: [
  51. "always",
  52. "never",
  53. "consistent",
  54. "multiline",
  55. "multiline-arguments",
  56. ],
  57. },
  58. {
  59. type: "object",
  60. properties: {
  61. minItems: {
  62. type: "integer",
  63. minimum: 0,
  64. },
  65. },
  66. additionalProperties: false,
  67. },
  68. ],
  69. },
  70. ],
  71. messages: {
  72. expectedBefore: "Expected newline before ')'.",
  73. expectedAfter: "Expected newline after '('.",
  74. expectedBetween: "Expected newline between arguments/params.",
  75. unexpectedBefore: "Unexpected newline before ')'.",
  76. unexpectedAfter: "Unexpected newline after '('.",
  77. },
  78. },
  79. create(context) {
  80. const sourceCode = context.sourceCode;
  81. const rawOption = context.options[0] || "multiline";
  82. const multilineOption = rawOption === "multiline";
  83. const multilineArgumentsOption = rawOption === "multiline-arguments";
  84. const consistentOption = rawOption === "consistent";
  85. let minItems;
  86. if (typeof rawOption === "object") {
  87. minItems = rawOption.minItems;
  88. } else if (rawOption === "always") {
  89. minItems = 0;
  90. } else if (rawOption === "never") {
  91. minItems = Infinity;
  92. } else {
  93. minItems = null;
  94. }
  95. //----------------------------------------------------------------------
  96. // Helpers
  97. //----------------------------------------------------------------------
  98. /**
  99. * Determines whether there should be newlines inside function parens
  100. * @param {ASTNode[]} elements The arguments or parameters in the list
  101. * @param {boolean} hasLeftNewline `true` if the left paren has a newline in the current code.
  102. * @returns {boolean} `true` if there should be newlines inside the function parens
  103. */
  104. function shouldHaveNewlines(elements, hasLeftNewline) {
  105. if (multilineArgumentsOption && elements.length === 1) {
  106. return hasLeftNewline;
  107. }
  108. if (multilineOption || multilineArgumentsOption) {
  109. return elements.some(
  110. (element, index) =>
  111. index !== elements.length - 1 &&
  112. element.loc.end.line !==
  113. elements[index + 1].loc.start.line,
  114. );
  115. }
  116. if (consistentOption) {
  117. return hasLeftNewline;
  118. }
  119. return elements.length >= minItems;
  120. }
  121. /**
  122. * Validates parens
  123. * @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
  124. * @param {ASTNode[]} elements The arguments or parameters in the list
  125. * @returns {void}
  126. */
  127. function validateParens(parens, elements) {
  128. const leftParen = parens.leftParen;
  129. const rightParen = parens.rightParen;
  130. const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen);
  131. const tokenBeforeRightParen = sourceCode.getTokenBefore(rightParen);
  132. const hasLeftNewline = !astUtils.isTokenOnSameLine(
  133. leftParen,
  134. tokenAfterLeftParen,
  135. );
  136. const hasRightNewline = !astUtils.isTokenOnSameLine(
  137. tokenBeforeRightParen,
  138. rightParen,
  139. );
  140. const needsNewlines = shouldHaveNewlines(elements, hasLeftNewline);
  141. if (hasLeftNewline && !needsNewlines) {
  142. context.report({
  143. node: leftParen,
  144. messageId: "unexpectedAfter",
  145. fix(fixer) {
  146. return sourceCode
  147. .getText()
  148. .slice(
  149. leftParen.range[1],
  150. tokenAfterLeftParen.range[0],
  151. )
  152. .trim()
  153. ? // If there is a comment between the ( and the first element, don't do a fix.
  154. null
  155. : fixer.removeRange([
  156. leftParen.range[1],
  157. tokenAfterLeftParen.range[0],
  158. ]);
  159. },
  160. });
  161. } else if (!hasLeftNewline && needsNewlines) {
  162. context.report({
  163. node: leftParen,
  164. messageId: "expectedAfter",
  165. fix: fixer => fixer.insertTextAfter(leftParen, "\n"),
  166. });
  167. }
  168. if (hasRightNewline && !needsNewlines) {
  169. context.report({
  170. node: rightParen,
  171. messageId: "unexpectedBefore",
  172. fix(fixer) {
  173. return sourceCode
  174. .getText()
  175. .slice(
  176. tokenBeforeRightParen.range[1],
  177. rightParen.range[0],
  178. )
  179. .trim()
  180. ? // If there is a comment between the last element and the ), don't do a fix.
  181. null
  182. : fixer.removeRange([
  183. tokenBeforeRightParen.range[1],
  184. rightParen.range[0],
  185. ]);
  186. },
  187. });
  188. } else if (!hasRightNewline && needsNewlines) {
  189. context.report({
  190. node: rightParen,
  191. messageId: "expectedBefore",
  192. fix: fixer => fixer.insertTextBefore(rightParen, "\n"),
  193. });
  194. }
  195. }
  196. /**
  197. * Validates a list of arguments or parameters
  198. * @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
  199. * @param {ASTNode[]} elements The arguments or parameters in the list
  200. * @returns {void}
  201. */
  202. function validateArguments(parens, elements) {
  203. const leftParen = parens.leftParen;
  204. const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen);
  205. const hasLeftNewline = !astUtils.isTokenOnSameLine(
  206. leftParen,
  207. tokenAfterLeftParen,
  208. );
  209. const needsNewlines = shouldHaveNewlines(elements, hasLeftNewline);
  210. for (let i = 0; i <= elements.length - 2; i++) {
  211. const currentElement = elements[i];
  212. const nextElement = elements[i + 1];
  213. const hasNewLine =
  214. currentElement.loc.end.line !== nextElement.loc.start.line;
  215. if (!hasNewLine && needsNewlines) {
  216. context.report({
  217. node: currentElement,
  218. messageId: "expectedBetween",
  219. fix: fixer => fixer.insertTextBefore(nextElement, "\n"),
  220. });
  221. }
  222. }
  223. }
  224. /**
  225. * Gets the left paren and right paren tokens of a node.
  226. * @param {ASTNode} node The node with parens
  227. * @throws {TypeError} Unexpected node type.
  228. * @returns {Object} An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token.
  229. * Can also return `null` if an expression has no parens (e.g. a NewExpression with no arguments, or an ArrowFunctionExpression
  230. * with a single parameter)
  231. */
  232. function getParenTokens(node) {
  233. switch (node.type) {
  234. case "NewExpression":
  235. if (
  236. !node.arguments.length &&
  237. !(
  238. astUtils.isOpeningParenToken(
  239. sourceCode.getLastToken(node, { skip: 1 }),
  240. ) &&
  241. astUtils.isClosingParenToken(
  242. sourceCode.getLastToken(node),
  243. ) &&
  244. node.callee.range[1] < node.range[1]
  245. )
  246. ) {
  247. // If the NewExpression does not have parens (e.g. `new Foo`), return null.
  248. return null;
  249. }
  250. // falls through
  251. case "CallExpression":
  252. return {
  253. leftParen: sourceCode.getTokenAfter(
  254. node.callee,
  255. astUtils.isOpeningParenToken,
  256. ),
  257. rightParen: sourceCode.getLastToken(node),
  258. };
  259. case "FunctionDeclaration":
  260. case "FunctionExpression": {
  261. const leftParen = sourceCode.getFirstToken(
  262. node,
  263. astUtils.isOpeningParenToken,
  264. );
  265. const rightParen = node.params.length
  266. ? sourceCode.getTokenAfter(
  267. node.params.at(-1),
  268. astUtils.isClosingParenToken,
  269. )
  270. : sourceCode.getTokenAfter(leftParen);
  271. return { leftParen, rightParen };
  272. }
  273. case "ArrowFunctionExpression": {
  274. const firstToken = sourceCode.getFirstToken(node, {
  275. skip: node.async ? 1 : 0,
  276. });
  277. if (!astUtils.isOpeningParenToken(firstToken)) {
  278. // If the ArrowFunctionExpression has a single param without parens, return null.
  279. return null;
  280. }
  281. const rightParen = node.params.length
  282. ? sourceCode.getTokenAfter(
  283. node.params.at(-1),
  284. astUtils.isClosingParenToken,
  285. )
  286. : sourceCode.getTokenAfter(firstToken);
  287. return {
  288. leftParen: firstToken,
  289. rightParen,
  290. };
  291. }
  292. case "ImportExpression": {
  293. const leftParen = sourceCode.getFirstToken(node, 1);
  294. const rightParen = sourceCode.getLastToken(node);
  295. return { leftParen, rightParen };
  296. }
  297. default:
  298. throw new TypeError(
  299. `unexpected node with type ${node.type}`,
  300. );
  301. }
  302. }
  303. //----------------------------------------------------------------------
  304. // Public
  305. //----------------------------------------------------------------------
  306. return {
  307. [[
  308. "ArrowFunctionExpression",
  309. "CallExpression",
  310. "FunctionDeclaration",
  311. "FunctionExpression",
  312. "ImportExpression",
  313. "NewExpression",
  314. ]](node) {
  315. const parens = getParenTokens(node);
  316. let params;
  317. if (node.type === "ImportExpression") {
  318. params = [node.source];
  319. } else if (astUtils.isFunction(node)) {
  320. params = node.params;
  321. } else {
  322. params = node.arguments;
  323. }
  324. if (parens) {
  325. validateParens(parens, params);
  326. if (multilineArgumentsOption) {
  327. validateArguments(parens, params);
  328. }
  329. }
  330. },
  331. };
  332. },
  333. };