Files
test-repo/src/language-js/print/call-arguments.js
T

428 lines
11 KiB
JavaScript

import { ArgExpansionBailout } from "../../common/errors.js";
import {
breakParent,
conditionalGroup,
group,
hardline,
ifBreak,
indent,
line,
softline,
willBreak,
} from "../../document/index.js";
import {
getCallArguments,
getCallArgumentSelector,
iterateCallArgumentsPath,
} from "../utilities/call-arguments.js";
import { CommentCheckFlags, hasComment } from "../utilities/comments.js";
import { getFunctionParameters } from "../utilities/function-parameters.js";
import { isFunctionCompositionArguments } from "../utilities/is-function-composition-arguments.js";
import { isLongCurriedCallExpression } from "../utilities/is-long-curried-call-expression.js";
import { isNextLineEmpty } from "../utilities/is-next-line-empty.js";
import { isObjectProperty } from "../utilities/is-object-property.js";
import { isSimpleCallArgument } from "../utilities/is-simple-call-argument.js";
import { isSimpleType } from "../utilities/is-simple-type.js";
import {
isArrayExpression,
isBinaryCastExpression,
isBinaryish,
isCallExpression,
isCallLikeExpression,
isJsxElement,
isObjectExpression,
isRegExpLiteral,
isStringLiteral,
} from "../utilities/node-types.js";
import { shouldPrintComma } from "../utilities/should-print-comma.js";
import { stripChainElementWrappers } from "../utilities/strip-chain-element-wrappers.js";
import { isConciselyPrintedArray } from "./array.js";
import { printDanglingCommentsInList } from "./miscellaneous.js";
/*
- `NewExpression`
- `ImportExpression`
- `OptionalCallExpression`
- `CallExpression`
- `TSImportType` (TypeScript)
- `TSExternalModuleReference` (TypeScript)
*/
function printCallArguments(path, options, print) {
const { node } = path;
const args = getCallArguments(node);
if (args.length === 0) {
return group(["(", printDanglingCommentsInList(path, options), ")"]);
}
const lastArgIndex = args.length - 1;
// useEffect(() => { ... }, [foo, bar, baz])
// useImperativeHandle(ref, () => { ... }, [foo, bar, baz])
if (isReactHookCallWithDepsArray(args)) {
const parts = ["("];
iterateCallArgumentsPath(path, (path, index) => {
parts.push(print());
if (index !== lastArgIndex) {
parts.push(", ");
}
});
parts.push(")");
return parts;
}
let anyArgEmptyLine = false;
const printedArguments = [];
iterateCallArgumentsPath(path, ({ node: arg }, index) => {
let argDoc = print();
if (index === lastArgIndex) {
// do nothing
} else if (isNextLineEmpty(arg, options)) {
anyArgEmptyLine = true;
argDoc = [argDoc, ",", hardline, hardline];
} else {
argDoc = [argDoc, ",", line];
}
printedArguments.push(argDoc);
});
const maybeTrailingComma =
// Angular does not allow trailing comma
path.root.type !== "NGRoot" &&
// Dynamic imports cannot have trailing commas
node.type !== "ImportExpression" &&
node.type !== "TSImportType" &&
node.type !== "TSExternalModuleReference" &&
shouldPrintComma(options, "all")
? ","
: "";
function allArgsBrokenOut() {
return group(
["(", indent([line, ...printedArguments]), maybeTrailingComma, line, ")"],
{ shouldBreak: true },
);
}
if (
anyArgEmptyLine ||
(path.parent.type !== "Decorator" && isFunctionCompositionArguments(args))
) {
return allArgsBrokenOut();
}
if (shouldExpandFirstArg(args)) {
const tailArgs = printedArguments.slice(1);
if (tailArgs.some(willBreak)) {
return allArgsBrokenOut();
}
let firstArg;
try {
firstArg = print(getCallArgumentSelector(node, 0), {
expandFirstArg: true,
});
} catch (caught) {
if (caught instanceof ArgExpansionBailout) {
return allArgsBrokenOut();
}
/* c8 ignore next */
throw caught;
}
if (willBreak(firstArg)) {
return [
breakParent,
conditionalGroup([
["(", group(firstArg, { shouldBreak: true }), ", ", ...tailArgs, ")"],
allArgsBrokenOut(),
]),
];
}
return conditionalGroup([
["(", firstArg, ", ", ...tailArgs, ")"],
["(", group(firstArg, { shouldBreak: true }), ", ", ...tailArgs, ")"],
allArgsBrokenOut(),
]);
}
if (shouldExpandLastArg(args, printedArguments, options)) {
const headArgs = printedArguments.slice(0, -1);
if (headArgs.some(willBreak)) {
return allArgsBrokenOut();
}
let lastArg;
try {
lastArg = print(getCallArgumentSelector(node, -1), {
expandLastArg: true,
});
} catch (caught) {
if (caught instanceof ArgExpansionBailout) {
return allArgsBrokenOut();
}
/* c8 ignore next */
throw caught;
}
if (willBreak(lastArg)) {
return [
breakParent,
conditionalGroup([
["(", ...headArgs, group(lastArg, { shouldBreak: true }), ")"],
allArgsBrokenOut(),
]),
];
}
return conditionalGroup([
["(", ...headArgs, lastArg, ")"],
["(", ...headArgs, group(lastArg, { shouldBreak: true }), ")"],
allArgsBrokenOut(),
]);
}
const contents = [
"(",
indent([softline, ...printedArguments]),
ifBreak(maybeTrailingComma),
softline,
")",
];
if (isLongCurriedCallExpression(path)) {
// By not wrapping the arguments in a group, the printer prioritizes
// breaking up these arguments rather than the args of the parent call.
return contents;
}
return group(contents, {
shouldBreak: printedArguments.some(willBreak) || anyArgEmptyLine,
});
}
function couldExpandArg(arg, arrowChainRecursion = false) {
if (
isObjectExpression(arg) &&
(arg.properties.length > 0 || hasComment(arg))
) {
return true;
}
if (isArrayExpression(arg) && (arg.elements.length > 0 || hasComment(arg))) {
return true;
}
if (
(isBinaryCastExpression(arg) || arg.type === "TSTypeAssertion") &&
couldExpandArg(arg.expression)
) {
return true;
}
if (
arg.type === "FunctionExpression" ||
arg.type === "DoExpression" ||
arg.type === "ModuleExpression"
) {
return true;
}
if (arg.type === "ArrowFunctionExpression") {
const { body } = arg;
if (
body.type === "BlockStatement" ||
isJsxElement(body) ||
isObjectExpression(body) ||
isArrayExpression(body)
) {
return true;
}
if (body.type === "ArrowFunctionExpression" && couldExpandArg(body, true)) {
return true;
}
if (!arrowChainRecursion) {
if (body.type === "ConditionalExpression") {
return true;
}
if (isCallExpression(stripChainElementWrappers(body))) {
return true;
}
}
}
return false;
}
function shouldExpandLastArg(args, argDocs, options) {
const lastArg = args.at(-1);
if (args.length === 1) {
const lastArgDoc = argDocs.at(-1);
if (lastArgDoc.label?.embed && lastArgDoc.label?.hug !== false) {
return true;
}
}
const penultimateArg = args.at(-2);
return (
!hasComment(lastArg, CommentCheckFlags.Leading) &&
!hasComment(lastArg, CommentCheckFlags.Trailing) &&
couldExpandArg(lastArg) &&
// If the last two arguments are of the same type,
// disable last element expansion.
(!penultimateArg || penultimateArg.type !== lastArg.type) &&
// useMemo(() => func(), [foo, bar, baz])
(args.length !== 2 ||
penultimateArg.type !== "ArrowFunctionExpression" ||
!isArrayExpression(lastArg)) &&
!(args.length > 1 && isConciselyPrintedArray(lastArg, options))
);
}
function shouldExpandFirstArg(args) {
if (args.length !== 2) {
return false;
}
const [firstArg, secondArg] = args;
if (
firstArg.type === "ModuleExpression" &&
isTypeModuleObjectExpression(secondArg)
) {
return true;
}
return (
!hasComment(firstArg) &&
(firstArg.type === "FunctionExpression" ||
(firstArg.type === "ArrowFunctionExpression" &&
firstArg.body.type === "BlockStatement")) &&
secondArg.type !== "FunctionExpression" &&
secondArg.type !== "ArrowFunctionExpression" &&
secondArg.type !== "ConditionalExpression" &&
isHopefullyShortCallArgument(secondArg) &&
!couldExpandArg(secondArg)
);
}
// A hack to fix most manifestations of
// https://github.com/prettier/prettier/issues/2456
// https://github.com/prettier/prettier/issues/5172
// https://github.com/prettier/prettier/issues/12892
// A proper (printWidth-aware) fix for those would require a complex change in the doc printer.
function isHopefullyShortCallArgument(node) {
if (node.type === "ParenthesizedExpression") {
return isHopefullyShortCallArgument(node.expression);
}
if (isBinaryCastExpression(node) || node.type === "TypeCastExpression") {
let { typeAnnotation } = node;
if (typeAnnotation.type === "TypeAnnotation") {
typeAnnotation = typeAnnotation.typeAnnotation;
}
if (typeAnnotation.type === "TSArrayType") {
typeAnnotation = typeAnnotation.elementType;
if (typeAnnotation.type === "TSArrayType") {
typeAnnotation = typeAnnotation.elementType;
}
}
if (
typeAnnotation.type === "GenericTypeAnnotation" ||
typeAnnotation.type === "TSTypeReference"
) {
const typeArguments =
typeAnnotation.type === "GenericTypeAnnotation"
? typeAnnotation.typeParameters
: typeAnnotation.typeArguments;
if (typeArguments?.params.length === 1) {
typeAnnotation = typeArguments.params[0];
}
}
return (
isSimpleType(typeAnnotation) && isSimpleCallArgument(node.expression, 1)
);
}
if (isCallLikeExpression(node) && getCallArguments(node).length > 1) {
return false;
}
if (isBinaryish(node)) {
return (
isSimpleCallArgument(node.left, 1) && isSimpleCallArgument(node.right, 1)
);
}
return isRegExpLiteral(node) || isSimpleCallArgument(node);
}
/**
* Checks if the arguments of a function are a call to a React Hook with a dependencies array.
*/
function isReactHookCallWithDepsArray(args) {
if (args.length === 2) {
/**
* useEffect(() => {
* // do something
* }, [dep1, dep2, dep2])
*/
return isValidHookCallbackAndDepsFormat(args, /* baseIndex */ 0);
}
if (args.length === 3) {
/**
* useImperativeHandle(ref, () => {
* // do something
* }, [dep1, dep2, dep2]);
*/
return (
args[0].type === "Identifier" &&
isValidHookCallbackAndDepsFormat(args, /* baseIndex */ 1)
);
}
return false;
}
function isValidHookCallbackAndDepsFormat(args, baseIndex) {
const maybeArrowFunction = args[baseIndex];
const maybeDepsArray = args[baseIndex + 1];
return (
maybeArrowFunction.type === "ArrowFunctionExpression" &&
getFunctionParameters(maybeArrowFunction).length === 0 &&
maybeArrowFunction.body.type === "BlockStatement" &&
maybeDepsArray.type === "ArrayExpression" &&
!args.some((arg) => hasComment(arg))
);
}
// `{ type: "module" }` and `{"type": "module"}`
function isTypeModuleObjectExpression(node) {
if (!(node.type === "ObjectExpression" && node.properties.length === 1)) {
return false;
}
const [property] = node.properties;
if (!isObjectProperty(property)) {
return false;
}
return (
!property.computed &&
((property.key.type === "Identifier" && property.key.name === "type") ||
(isStringLiteral(property.key) && property.key.value === "type")) &&
isStringLiteral(property.value) &&
property.value.value === "module"
);
}
export default printCallArguments;