|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- /**
- * @fileoverview enforce consistent line breaks inside function parentheses
- * @author Teddy Katz
- */
- "use strict";
-
- //------------------------------------------------------------------------------
- // Requirements
- //------------------------------------------------------------------------------
-
- const astUtils = require("./utils/ast-utils");
-
- //------------------------------------------------------------------------------
- // Rule Definition
- //------------------------------------------------------------------------------
-
- module.exports = {
- meta: {
- type: "layout",
-
- docs: {
- description: "enforce consistent line breaks inside function parentheses",
- category: "Stylistic Issues",
- recommended: false,
- url: "https://eslint.org/docs/rules/function-paren-newline"
- },
-
- fixable: "whitespace",
-
- schema: [
- {
- oneOf: [
- {
- enum: ["always", "never", "consistent", "multiline", "multiline-arguments"]
- },
- {
- type: "object",
- properties: {
- minItems: {
- type: "integer",
- minimum: 0
- }
- },
- additionalProperties: false
- }
- ]
- }
- ],
-
- messages: {
- expectedBefore: "Expected newline before ')'.",
- expectedAfter: "Expected newline after '('.",
- expectedBetween: "Expected newline between arguments/params.",
- unexpectedBefore: "Unexpected newline before ')'.",
- unexpectedAfter: "Unexpected newline after '('."
- }
- },
-
- create(context) {
- const sourceCode = context.getSourceCode();
- const rawOption = context.options[0] || "multiline";
- const multilineOption = rawOption === "multiline";
- const multilineArgumentsOption = rawOption === "multiline-arguments";
- const consistentOption = rawOption === "consistent";
- let minItems;
-
- if (typeof rawOption === "object") {
- minItems = rawOption.minItems;
- } else if (rawOption === "always") {
- minItems = 0;
- } else if (rawOption === "never") {
- minItems = Infinity;
- } else {
- minItems = null;
- }
-
- //----------------------------------------------------------------------
- // Helpers
- //----------------------------------------------------------------------
-
- /**
- * Determines whether there should be newlines inside function parens
- * @param {ASTNode[]} elements The arguments or parameters in the list
- * @param {boolean} hasLeftNewline `true` if the left paren has a newline in the current code.
- * @returns {boolean} `true` if there should be newlines inside the function parens
- */
- function shouldHaveNewlines(elements, hasLeftNewline) {
- if (multilineArgumentsOption && elements.length === 1) {
- return hasLeftNewline;
- }
- if (multilineOption || multilineArgumentsOption) {
- return elements.some((element, index) => index !== elements.length - 1 && element.loc.end.line !== elements[index + 1].loc.start.line);
- }
- if (consistentOption) {
- return hasLeftNewline;
- }
- return elements.length >= minItems;
- }
-
- /**
- * Validates parens
- * @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
- * @param {ASTNode[]} elements The arguments or parameters in the list
- * @returns {void}
- */
- function validateParens(parens, elements) {
- const leftParen = parens.leftParen;
- const rightParen = parens.rightParen;
- const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen);
- const tokenBeforeRightParen = sourceCode.getTokenBefore(rightParen);
- const hasLeftNewline = !astUtils.isTokenOnSameLine(leftParen, tokenAfterLeftParen);
- const hasRightNewline = !astUtils.isTokenOnSameLine(tokenBeforeRightParen, rightParen);
- const needsNewlines = shouldHaveNewlines(elements, hasLeftNewline);
-
- if (hasLeftNewline && !needsNewlines) {
- context.report({
- node: leftParen,
- messageId: "unexpectedAfter",
- fix(fixer) {
- return sourceCode.getText().slice(leftParen.range[1], tokenAfterLeftParen.range[0]).trim()
-
- // If there is a comment between the ( and the first element, don't do a fix.
- ? null
- : fixer.removeRange([leftParen.range[1], tokenAfterLeftParen.range[0]]);
- }
- });
- } else if (!hasLeftNewline && needsNewlines) {
- context.report({
- node: leftParen,
- messageId: "expectedAfter",
- fix: fixer => fixer.insertTextAfter(leftParen, "\n")
- });
- }
-
- if (hasRightNewline && !needsNewlines) {
- context.report({
- node: rightParen,
- messageId: "unexpectedBefore",
- fix(fixer) {
- return sourceCode.getText().slice(tokenBeforeRightParen.range[1], rightParen.range[0]).trim()
-
- // If there is a comment between the last element and the ), don't do a fix.
- ? null
- : fixer.removeRange([tokenBeforeRightParen.range[1], rightParen.range[0]]);
- }
- });
- } else if (!hasRightNewline && needsNewlines) {
- context.report({
- node: rightParen,
- messageId: "expectedBefore",
- fix: fixer => fixer.insertTextBefore(rightParen, "\n")
- });
- }
- }
-
- /**
- * Validates a list of arguments or parameters
- * @param {Object} parens An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token
- * @param {ASTNode[]} elements The arguments or parameters in the list
- * @returns {void}
- */
- function validateArguments(parens, elements) {
- const leftParen = parens.leftParen;
- const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParen);
- const hasLeftNewline = !astUtils.isTokenOnSameLine(leftParen, tokenAfterLeftParen);
- const needsNewlines = shouldHaveNewlines(elements, hasLeftNewline);
-
- for (let i = 0; i <= elements.length - 2; i++) {
- const currentElement = elements[i];
- const nextElement = elements[i + 1];
- const hasNewLine = currentElement.loc.end.line !== nextElement.loc.start.line;
-
- if (!hasNewLine && needsNewlines) {
- context.report({
- node: currentElement,
- messageId: "expectedBetween",
- fix: fixer => fixer.insertTextBefore(nextElement, "\n")
- });
- }
- }
- }
-
- /**
- * Gets the left paren and right paren tokens of a node.
- * @param {ASTNode} node The node with parens
- * @returns {Object} An object with keys `leftParen` for the left paren token, and `rightParen` for the right paren token.
- * Can also return `null` if an expression has no parens (e.g. a NewExpression with no arguments, or an ArrowFunctionExpression
- * with a single parameter)
- */
- function getParenTokens(node) {
- switch (node.type) {
- case "NewExpression":
- if (!node.arguments.length && !(
- astUtils.isOpeningParenToken(sourceCode.getLastToken(node, { skip: 1 })) &&
- astUtils.isClosingParenToken(sourceCode.getLastToken(node))
- )) {
-
- // If the NewExpression does not have parens (e.g. `new Foo`), return null.
- return null;
- }
-
- // falls through
-
- case "CallExpression":
- return {
- leftParen: sourceCode.getTokenAfter(node.callee, astUtils.isOpeningParenToken),
- rightParen: sourceCode.getLastToken(node)
- };
-
- case "FunctionDeclaration":
- case "FunctionExpression": {
- const leftParen = sourceCode.getFirstToken(node, astUtils.isOpeningParenToken);
- const rightParen = node.params.length
- ? sourceCode.getTokenAfter(node.params[node.params.length - 1], astUtils.isClosingParenToken)
- : sourceCode.getTokenAfter(leftParen);
-
- return { leftParen, rightParen };
- }
-
- case "ArrowFunctionExpression": {
- const firstToken = sourceCode.getFirstToken(node, { skip: (node.async ? 1 : 0) });
-
- if (!astUtils.isOpeningParenToken(firstToken)) {
-
- // If the ArrowFunctionExpression has a single param without parens, return null.
- return null;
- }
-
- return {
- leftParen: firstToken,
- rightParen: sourceCode.getTokenBefore(node.body, astUtils.isClosingParenToken)
- };
- }
-
- case "ImportExpression": {
- const leftParen = sourceCode.getFirstToken(node, 1);
- const rightParen = sourceCode.getLastToken(node);
-
- return { leftParen, rightParen };
- }
-
- default:
- throw new TypeError(`unexpected node with type ${node.type}`);
- }
- }
-
- //----------------------------------------------------------------------
- // Public
- //----------------------------------------------------------------------
-
- return {
- [[
- "ArrowFunctionExpression",
- "CallExpression",
- "FunctionDeclaration",
- "FunctionExpression",
- "ImportExpression",
- "NewExpression"
- ]](node) {
- const parens = getParenTokens(node);
- let params;
-
- if (node.type === "ImportExpression") {
- params = [node.source];
- } else if (astUtils.isFunction(node)) {
- params = node.params;
- } else {
- params = node.arguments;
- }
-
- if (parens) {
- validateParens(parens, params);
-
- if (multilineArgumentsOption) {
- validateArguments(parens, params);
- }
- }
- }
- };
- }
- };
|