Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

229 řádky
9.1 KiB

  1. /**
  2. * @fileoverview enforce consistent line breaks inside function parentheses
  3. * @author Teddy Katz
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("../util/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Rule Definition
  12. //------------------------------------------------------------------------------
  13. module.exports = {
  14. meta: {
  15. docs: {
  16. description: "enforce consistent line breaks inside function parentheses",
  17. category: "Stylistic Issues",
  18. recommended: false,
  19. url: "https://eslint.org/docs/rules/function-paren-newline"
  20. },
  21. fixable: "whitespace",
  22. schema: [
  23. {
  24. oneOf: [
  25. {
  26. enum: ["always", "never", "consistent", "multiline"]
  27. },
  28. {
  29. type: "object",
  30. properties: {
  31. minItems: {
  32. type: "integer",
  33. minimum: 0
  34. }
  35. },
  36. additionalProperties: false
  37. }
  38. ]
  39. }
  40. ],
  41. messages: {
  42. expectedBefore: "Expected newline before ')'.",
  43. expectedAfter: "Expected newline after '('.",
  44. unexpectedBefore: "Unexpected newline before '('.",
  45. unexpectedAfter: "Unexpected newline after ')'."
  46. }
  47. },
  48. create(context) {
  49. const sourceCode = context.getSourceCode();
  50. const rawOption = context.options[0] || "multiline";
  51. const multilineOption = rawOption === "multiline";
  52. const consistentOption = rawOption === "consistent";
  53. let minItems;
  54. if (typeof rawOption === "object") {
  55. minItems = rawOption.minItems;
  56. } else if (rawOption === "always") {
  57. minItems = 0;
  58. } else if (rawOption === "never") {
  59. minItems = Infinity;
  60. } else {
  61. minItems = null;
  62. }
  63. //----------------------------------------------------------------------
  64. // Helpers
  65. //----------------------------------------------------------------------
  66. /**
  67. * Determines whether there should be newlines inside function parens
  68. * @param {ASTNode[]} elements The arguments or parameters in the list
  69. * @param {boolean} hasLeftNewline `true` if the left paren has a newline in the current code.
  70. * @returns {boolean} `true` if there should be newlines inside the function parens
  71. */
  72. function shouldHaveNewlines(elements, hasLeftNewline) {
  73. if (multilineOption) {
  74. return elements.some((element, index) => index !== elements.length - 1 && element.loc.end.line !== elements[index + 1].loc.start.line);
  75. }
  76. if (consistentOption) {
  77. return hasLeftNewline;
  78. }
  79. return elements.length >= minItems;
  80. }
  81. /**
  82. * Validates a list of arguments or parameters
  83. * @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
  84. * @param {ASTNode[]} elements The arguments or parameters in the list
  85. * @returns {void}
  86. */
  87. function validateParens(parens, elements) {
  88. const leftParen = parens.leftParen;
  89. const rightParen = parens.rightParen;
  90. const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen);
  91. const tokenBeforeRightParen = sourceCode.getTokenBefore(rightParen);
  92. const hasLeftNewline = !astUtils.isTokenOnSameLine(leftParen, tokenAfterLeftParen);
  93. const hasRightNewline = !astUtils.isTokenOnSameLine(tokenBeforeRightParen, rightParen);
  94. const needsNewlines = shouldHaveNewlines(elements, hasLeftNewline);
  95. if (hasLeftNewline && !needsNewlines) {
  96. context.report({
  97. node: leftParen,
  98. messageId: "unexpectedAfter",
  99. fix(fixer) {
  100. return sourceCode.getText().slice(leftParen.range[1], tokenAfterLeftParen.range[0]).trim()
  101. // If there is a comment between the ( and the first element, don't do a fix.
  102. ? null
  103. : fixer.removeRange([leftParen.range[1], tokenAfterLeftParen.range[0]]);
  104. }
  105. });
  106. } else if (!hasLeftNewline && needsNewlines) {
  107. context.report({
  108. node: leftParen,
  109. messageId: "expectedAfter",
  110. fix: fixer => fixer.insertTextAfter(leftParen, "\n")
  111. });
  112. }
  113. if (hasRightNewline && !needsNewlines) {
  114. context.report({
  115. node: rightParen,
  116. messageId: "unexpectedBefore",
  117. fix(fixer) {
  118. return sourceCode.getText().slice(tokenBeforeRightParen.range[1], rightParen.range[0]).trim()
  119. // If there is a comment between the last element and the ), don't do a fix.
  120. ? null
  121. : fixer.removeRange([tokenBeforeRightParen.range[1], rightParen.range[0]]);
  122. }
  123. });
  124. } else if (!hasRightNewline && needsNewlines) {
  125. context.report({
  126. node: rightParen,
  127. messageId: "expectedBefore",
  128. fix: fixer => fixer.insertTextBefore(rightParen, "\n")
  129. });
  130. }
  131. }
  132. /**
  133. * Gets the left paren and right paren tokens of a node.
  134. * @param {ASTNode} node The node with parens
  135. * @returns {Object} An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token.
  136. * Can also return `null` if an expression has no parens (e.g. a NewExpression with no arguments, or an ArrowFunctionExpression
  137. * with a single parameter)
  138. */
  139. function getParenTokens(node) {
  140. switch (node.type) {
  141. case "NewExpression":
  142. if (!node.arguments.length && !(
  143. astUtils.isOpeningParenToken(sourceCode.getLastToken(node, { skip: 1 })) &&
  144. astUtils.isClosingParenToken(sourceCode.getLastToken(node))
  145. )) {
  146. // If the NewExpression does not have parens (e.g. `new Foo`), return null.
  147. return null;
  148. }
  149. // falls through
  150. case "CallExpression":
  151. return {
  152. leftParen: sourceCode.getTokenAfter(node.callee, astUtils.isOpeningParenToken),
  153. rightParen: sourceCode.getLastToken(node)
  154. };
  155. case "FunctionDeclaration":
  156. case "FunctionExpression": {
  157. const leftParen = sourceCode.getFirstToken(node, astUtils.isOpeningParenToken);
  158. const rightParen = node.params.length
  159. ? sourceCode.getTokenAfter(node.params[node.params.length - 1], astUtils.isClosingParenToken)
  160. : sourceCode.getTokenAfter(leftParen);
  161. return { leftParen, rightParen };
  162. }
  163. case "ArrowFunctionExpression": {
  164. const firstToken = sourceCode.getFirstToken(node);
  165. if (!astUtils.isOpeningParenToken(firstToken)) {
  166. // If the ArrowFunctionExpression has a single param without parens, return null.
  167. return null;
  168. }
  169. return {
  170. leftParen: firstToken,
  171. rightParen: sourceCode.getTokenBefore(node.body, astUtils.isClosingParenToken)
  172. };
  173. }
  174. default:
  175. throw new TypeError(`unexpected node with type ${node.type}`);
  176. }
  177. }
  178. /**
  179. * Validates the parentheses for a node
  180. * @param {ASTNode} node The node with parens
  181. * @returns {void}
  182. */
  183. function validateNode(node) {
  184. const parens = getParenTokens(node);
  185. if (parens) {
  186. validateParens(parens, astUtils.isFunction(node) ? node.params : node.arguments);
  187. }
  188. }
  189. //----------------------------------------------------------------------
  190. // Public
  191. //----------------------------------------------------------------------
  192. return {
  193. ArrowFunctionExpression: validateNode,
  194. CallExpression: validateNode,
  195. FunctionDeclaration: validateNode,
  196. FunctionExpression: validateNode,
  197. NewExpression: validateNode
  198. };
  199. }
  200. };