Files
test-repo/src/language-js/print/function-parameters.js
T

306 lines
8.8 KiB
JavaScript

import { ArgExpansionBailout } from "../../common/errors.js";
import {
group,
hardline,
ifBreak,
indent,
line,
softline,
} from "../../document/builders.js";
import { removeLines, willBreak } from "../../document/utils.js";
import { printDanglingComments } from "../../main/comments/print.js";
import getNextNonSpaceNonCommentCharacter from "../../utils/get-next-non-space-non-comment-character.js";
import isNonEmptyArray from "../../utils/is-non-empty-array.js";
import { locEnd } from "../loc.js";
import {
getFunctionParameters,
hasComment,
hasRestParameter,
isArrayExpression,
isFlowObjectTypePropertyAFunction,
isNextLineEmpty,
isObjectExpression,
isObjectType,
isSimpleType,
isTestCall,
isTypeAnnotationAFunction,
iterateFunctionParametersPath,
shouldPrintComma,
} from "../utils/index.js";
/** @import AstPath from "../../common/ast-path.js" */
function printFunctionParameters(
path,
options,
print,
shouldExpandArgument,
shouldPrintTypeParameters,
) {
const functionNode = path.node;
const parameters = getFunctionParameters(functionNode);
const typeParametersDoc =
shouldPrintTypeParameters && functionNode.typeParameters
? print("typeParameters")
: "";
if (parameters.length === 0) {
return [
typeParametersDoc,
"(",
printDanglingComments(path, options, {
filter: (comment) =>
getNextNonSpaceNonCommentCharacter(
options.originalText,
locEnd(comment),
) === ")",
}),
")",
];
}
const { parent } = path;
const isParametersInTestCall = isTestCall(parent);
const shouldHugParameters = shouldHugTheOnlyFunctionParameter(functionNode);
const printed = [];
iterateFunctionParametersPath(path, (parameterPath, index) => {
const isLastParameter = index === parameters.length - 1;
if (isLastParameter && functionNode.rest) {
printed.push("...");
}
printed.push(print());
if (isLastParameter) {
return;
}
printed.push(",");
if (isParametersInTestCall || shouldHugParameters) {
printed.push(" ");
} else if (isNextLineEmpty(parameters[index], options)) {
printed.push(hardline, hardline);
} else {
printed.push(line);
}
});
// If the parent is a call with the first/last argument expansion and this is the
// params of the first/last argument, we don't want the arguments to break and instead
// want the whole expression to be on a new line.
//
// Good: Bad:
// verylongcall( verylongcall((
// (a, b) => { a,
// } b,
// ) ) => {
// })
if (shouldExpandArgument && !isDecoratedFunction(path)) {
if (willBreak(typeParametersDoc) || willBreak(printed)) {
// Removing lines in this case leads to broken or ugly output
throw new ArgExpansionBailout();
}
return group([
removeLines(typeParametersDoc),
"(",
removeLines(printed),
")",
]);
}
// Single object destructuring should hug
//
// function({
// a,
// b,
// c
// }) {}
const hasNotParameterDecorator = parameters.every(
(node) => !isNonEmptyArray(node.decorators),
);
if (shouldHugParameters && hasNotParameterDecorator) {
return [typeParametersDoc, "(", ...printed, ")"];
}
// don't break in specs, eg; `it("should maintain parens around done even when long", (done) => {})`
if (isParametersInTestCall) {
return [typeParametersDoc, "(", ...printed, ")"];
}
const isFlowShorthandWithOneArg =
(isFlowObjectTypePropertyAFunction(parent) ||
isTypeAnnotationAFunction(parent) ||
parent.type === "TypeAlias" ||
parent.type === "UnionTypeAnnotation" ||
parent.type === "IntersectionTypeAnnotation" ||
(parent.type === "FunctionTypeAnnotation" &&
parent.returnType === functionNode)) &&
parameters.length === 1 &&
parameters[0].name === null &&
// `type q = (this: string) => void;`
functionNode.this !== parameters[0] &&
parameters[0].typeAnnotation &&
functionNode.typeParameters === null &&
isSimpleType(parameters[0].typeAnnotation) &&
!functionNode.rest;
if (isFlowShorthandWithOneArg) {
if (
options.arrowParens === "always" ||
functionNode.type === "HookTypeAnnotation"
) {
return ["(", ...printed, ")"];
}
return printed;
}
return [
typeParametersDoc,
"(",
indent([softline, ...printed]),
ifBreak(
!hasRestParameter(functionNode) && shouldPrintComma(options, "all")
? ","
: "",
),
softline,
")",
];
}
function shouldHugTheOnlyFunctionParameter(node) {
if (!node) {
return false;
}
const parameters = getFunctionParameters(node);
if (parameters.length !== 1) {
return false;
}
const [parameter] = parameters;
return (
!hasComment(parameter) &&
(parameter.type === "ObjectPattern" ||
parameter.type === "ArrayPattern" ||
(parameter.type === "Identifier" &&
parameter.typeAnnotation &&
(parameter.typeAnnotation.type === "TypeAnnotation" ||
parameter.typeAnnotation.type === "TSTypeAnnotation") &&
isObjectType(parameter.typeAnnotation.typeAnnotation)) ||
(parameter.type === "FunctionTypeParam" &&
isObjectType(parameter.typeAnnotation) &&
parameter !== node.rest) ||
(parameter.type === "AssignmentPattern" &&
(parameter.left.type === "ObjectPattern" ||
parameter.left.type === "ArrayPattern") &&
(parameter.right.type === "Identifier" ||
(isObjectExpression(parameter.right) &&
parameter.right.properties.length === 0) ||
(isArrayExpression(parameter.right) &&
parameter.right.elements.length === 0))))
);
}
function getReturnTypeNode(functionNode) {
let returnTypeNode;
if (functionNode.returnType) {
returnTypeNode = functionNode.returnType;
if (returnTypeNode.typeAnnotation) {
returnTypeNode = returnTypeNode.typeAnnotation;
}
} else if (functionNode.typeAnnotation) {
returnTypeNode = functionNode.typeAnnotation;
}
return returnTypeNode;
}
// When parameters are grouped, the return type annotation breaks first.
function shouldGroupFunctionParameters(functionNode, returnTypeDoc) {
const returnTypeNode = getReturnTypeNode(functionNode);
if (!returnTypeNode) {
return false;
}
const typeParameters = functionNode.typeParameters?.params;
if (typeParameters) {
if (typeParameters.length > 1) {
return false;
}
if (typeParameters.length === 1) {
const typeParameter = typeParameters[0];
if (typeParameter.constraint || typeParameter.default) {
return false;
}
}
}
return (
getFunctionParameters(functionNode).length === 1 &&
(isObjectType(returnTypeNode) || willBreak(returnTypeDoc))
);
}
/**
* The "decorated function" pattern.
* The arrow function should be kept hugged even if its signature breaks.
*
* ```
* const decoratedFn = decorator(param1, param2)((
* ...
* ) => {
* ...
* });
* ```
* @param {AstPath} path
*/
function isDecoratedFunction(path) {
return path.match(
(node) =>
node.type === "ArrowFunctionExpression" &&
node.body.type === "BlockStatement",
(node, name) => {
if (
node.type === "CallExpression" &&
name === "arguments" &&
node.arguments.length === 1 &&
node.callee.type === "CallExpression"
) {
const decorator = node.callee.callee;
return (
decorator.type === "Identifier" ||
(decorator.type === "MemberExpression" &&
!decorator.computed &&
decorator.object.type === "Identifier" &&
decorator.property.type === "Identifier")
);
}
return false;
},
(node, name) =>
(node.type === "VariableDeclarator" && name === "init") ||
(node.type === "ExportDefaultDeclaration" && name === "declaration") ||
(node.type === "TSExportAssignment" && name === "expression") ||
(node.type === "AssignmentExpression" &&
name === "right" &&
node.left.type === "MemberExpression" &&
node.left.object.type === "Identifier" &&
node.left.object.name === "module" &&
node.left.property.type === "Identifier" &&
node.left.property.name === "exports"),
(node) =>
node.type !== "VariableDeclaration" ||
(node.kind === "const" && node.declarations.length === 1),
);
}
function shouldBreakFunctionParameters(functionNode) {
const parameters = getFunctionParameters(functionNode);
return (
parameters.length > 1 &&
parameters.some((parameter) => parameter.type === "TSParameterProperty")
);
}
export {
printFunctionParameters,
shouldBreakFunctionParameters,
shouldGroupFunctionParameters,
shouldHugTheOnlyFunctionParameter,
};