UNPKG

@prettier/plugin-php

Version:
1,869 lines (1,644 loc) 78.9 kB
import { util as prettierUtil, doc } from "prettier"; import { printAllComments, hasTrailingComment, hasLeadingComment, printDanglingComments, printComments, isBlockComment, hasLeadingOwnLineComment, } from "./comments.mjs"; import pathNeedsParens from "./needs-parens.mjs"; import { locStart, locEnd } from "./loc.mjs"; import { getLast, getPenultimate, lineShouldEndWithSemicolon, printNumber, shouldFlatten, maybeStripLeadingSlashFromUse, fileShouldEndWithHardline, hasDanglingComments, docShouldHaveTrailingNewline, isLookupNode, isFirstChildrenInlineNode, shouldPrintHardLineAfterStartInControlStructure, shouldPrintHardLineBeforeEndInControlStructure, getAlignment, isProgramLikeNode, getNodeKindIncludingLogical, useDoubleQuote, hasEmptyBody, isNextLineEmptyAfterNamespace, shouldPrintHardlineBeforeTrailingComma, isDocNode, getAncestorNode, isReferenceLikeNode, normalizeMagicMethodName, isSimpleCallArgument, } from "./util.mjs"; const { breakParent, join, line, lineSuffix, group, conditionalGroup, indent, dedent, ifBreak, hardline, softline, literalline, align, dedentToRoot, } = doc.builders; const { willBreak } = doc.utils; const { isNextLineEmptyAfterIndex, hasNewline, hasNewlineInRange, getNextNonSpaceNonCommentCharacterIndex, isNextLineEmpty, isPreviousLineEmpty, } = prettierUtil; /** * Determine if we should print a trailing comma based on the config & php version * * @param options {object} Prettier Options * @param requiredVersion {number} * @returns {boolean} */ function shouldPrintComma(options, requiredVersion) { if (!options.trailingCommaPHP) { return false; } return options.phpVersion >= requiredVersion; } function shouldPrintHardlineForOpenBrace(options) { switch (options.braceStyle) { case "1tbs": return false; case "psr-2": case "per-cs": default: return true; } } function genericPrint(path, options, print) { const { node } = path; if (typeof node === "string") { return node; } const printedWithoutParens = printNode(path, options, print); const parts = []; const needsParens = pathNeedsParens(path, options); if (needsParens) { parts.unshift("("); } parts.push(printedWithoutParens); if (needsParens) { parts.push(")"); } if (lineShouldEndWithSemicolon(path)) { parts.push(";"); } if (fileShouldEndWithHardline(path)) { parts.push(hardline); } return parts; } function printPropertyLookup(path, options, print, nullsafe = false) { return [nullsafe ? "?" : "", "->", print("offset")]; } function printNullsafePropertyLookup(path, options, print) { return printPropertyLookup(path, options, print, true); } function printStaticLookup(path, options, print) { const { node } = path; const needCurly = !["variable", "identifier"].includes(node.offset.kind); return ["::", needCurly ? "{" : "", print("offset"), needCurly ? "}" : ""]; } function printOffsetLookup(path, options, print) { const { node } = path; const shouldInline = (node.offset && node.offset.kind === "number") || getAncestorNode(path, "encapsed"); return [ "[", node.offset ? group([ indent([shouldInline ? "" : softline, print("offset")]), shouldInline ? "" : softline, ]) : "", "]", ]; } // We detect calls on member expressions specially to format a // common pattern better. The pattern we are looking for is this: // // $arr // ->map(function(x) { return $x + 1; }) // ->filter(function(x) { return $x > 10; }) // ->some(function(x) { return $x % 2; }); // // The way it is structured in the AST is via a nested sequence of // propertylookup, staticlookup, offsetlookup and call. // We need to traverse the AST and make groups out of it // to print it in the desired way. function printMemberChain(path, options, print) { // The first phase is to linearize the AST by traversing it down. // // Example: // a()->b->c()->d(); // has the AST structure // call (isLookupNode d ( // call (isLookupNode c ( // isLookupNode b ( // call (variable a) // ) // )) // )) // and we transform it into (notice the reversed order) // [identifier a, call, isLookupNode b, isLookupNode c, call, // isLookupNode d, call] const printedNodes = []; // Here we try to retain one typed empty line after each call expression or // the first group whether it is in parentheses or not // // Example: // $a // ->call() // // ->otherCall(); // // ($foo ? $a : $b) // ->call() // ->otherCall(); function shouldInsertEmptyLineAfter(node) { const { originalText } = options; const nextCharIndex = getNextNonSpaceNonCommentCharacterIndex( originalText, locEnd(node) ); const nextChar = originalText.charAt(nextCharIndex); // if it is cut off by a parenthesis, we only account for one typed empty // line after that parenthesis if (nextChar === ")") { return isNextLineEmptyAfterIndex( originalText, nextCharIndex + 1, options ); } return isNextLineEmpty(originalText, locEnd(node)); } function traverse(path) { const { node } = path; if ( node.kind === "call" && (isLookupNode(node.what) || node.what.kind === "call") ) { printedNodes.unshift({ node, printed: [ printAllComments( path, () => printArgumentsList(path, options, print), options ), shouldInsertEmptyLineAfter(node) ? hardline : "", ], }); path.call((what) => traverse(what), "what"); } else if (isLookupNode(node)) { // Print *lookup nodes as we standard print them outside member chain let printedMemberish = null; if (node.kind === "propertylookup") { printedMemberish = printPropertyLookup(path, options, print); } else if (node.kind === "nullsafepropertylookup") { printedMemberish = printNullsafePropertyLookup(path, options, print); } else if (node.kind === "staticlookup") { printedMemberish = printStaticLookup(path, options, print); } else { printedMemberish = printOffsetLookup(path, options, print); } printedNodes.unshift({ node, needsParens: pathNeedsParens(path, options), printed: printAllComments(path, () => printedMemberish, options), }); path.call((what) => traverse(what), "what"); } else { printedNodes.unshift({ node, printed: print(), }); } } const { node } = path; printedNodes.unshift({ node, printed: printArgumentsList(path, options, print), }); path.call((what) => traverse(what), "what"); // Restore parens around `propertylookup` and `staticlookup` nodes with call. // $value = ($object->foo)(); // $value = ($object::$foo)(); for (let i = 0; i < printedNodes.length; ++i) { if ( printedNodes[i].node.kind === "call" && printedNodes[i - 1] && ["propertylookup", "nullsafepropertylookup", "staticlookup"].includes( printedNodes[i - 1].node.kind ) && printedNodes[i - 1].needsParens ) { printedNodes[0].printed = ["(", printedNodes[0].printed]; printedNodes[i - 1].printed = [printedNodes[i - 1].printed, ")"]; } } // create groups from list of nodes, i.e. // [identifier a, call, isLookupNode b, isLookupNode c, call, // isLookupNode d, call] // will be grouped as // [ // [identifier a, Call], // [isLookupNode b, isLookupNode c, call], // [isLookupNode d, call] // ] // so that we can print it as // a() // ->b->c() // ->d(); const groups = []; let currentGroup = [printedNodes[0]]; let i = 1; for (; i < printedNodes.length; ++i) { if ( printedNodes[i].node.kind === "call" || (isLookupNode(printedNodes[i].node) && printedNodes[i].node.offset && printedNodes[i].node.offset.kind === "number") ) { currentGroup.push(printedNodes[i]); } else { break; } } if (printedNodes[0].node.kind !== "call") { for (; i + 1 < printedNodes.length; ++i) { if ( isLookupNode(printedNodes[i].node) && isLookupNode(printedNodes[i + 1].node) ) { currentGroup.push(printedNodes[i]); } else { break; } } } groups.push(currentGroup); currentGroup = []; // Then, each following group is a sequence of propertylookup followed by // a sequence of call. To compute it, we keep adding things to the // group until we have seen a call in the past and reach a // propertylookup let hasSeenCallExpression = false; for (; i < printedNodes.length; ++i) { if (hasSeenCallExpression && isLookupNode(printedNodes[i].node)) { // [0] should be appended at the end of the group instead of the // beginning of the next one if ( printedNodes[i].node.kind === "offsetlookup" && printedNodes[i].node.offset && printedNodes[i].node.offset.kind === "number" ) { currentGroup.push(printedNodes[i]); continue; } groups.push(currentGroup); currentGroup = []; hasSeenCallExpression = false; } if (printedNodes[i].node.kind === "call") { hasSeenCallExpression = true; } currentGroup.push(printedNodes[i]); if ( printedNodes[i].node.comments && hasTrailingComment(printedNodes[i].node) ) { groups.push(currentGroup); currentGroup = []; hasSeenCallExpression = false; } } if (currentGroup.length > 0) { groups.push(currentGroup); } // Merge next nodes when: // // 1. We have `$this` variable before // // Example: // $this->method()->property; // // 2. When we have offsetlookup after *lookup node // // Example: // $foo->Data['key']("foo") // ->method(); // // 3. expression statements with variable names shorter than the tab width // // Example: // $foo->bar() // ->baz() // ->buzz() function shouldNotWrap(groups) { const hasComputed = groups[1].length && groups[1][0].node.kind === "offsetlookup"; if (groups[0].length === 1) { const firstNode = groups[0][0].node; return ( (firstNode.kind === "variable" && (firstNode.name === "this" || (isExpressionStatement && isShort(firstNode.name)))) || isReferenceLikeNode(firstNode) ); } function isShort(name) { return name.length < options.tabWidth; } const lastNode = getLast(groups[0]).node; return ( isLookupNode(lastNode) && (lastNode.offset.kind === "identifier" || lastNode.offset.kind === "variable") && hasComputed ); } const isExpressionStatement = path.parent.kind === "expressionstatement"; const shouldMerge = groups.length >= 2 && !groups[1][0].node.comments && shouldNotWrap(groups); function printGroup(printedGroup) { const result = []; for (let i = 0; i < printedGroup.length; i++) { // Checks if the next node (i.e. the parent node) needs parens // and print accordingl y if (printedGroup[i + 1] && printedGroup[i + 1].needsParens) { result.push( "(", printedGroup[i].printed, printedGroup[i + 1].printed, ")" ); i++; } else { result.push(printedGroup[i].printed); } } return result; } function printIndentedGroup(groups) { if (groups.length === 0) { return ""; } return indent(group([hardline, join(hardline, groups.map(printGroup))])); } const printedGroups = groups.map(printGroup); const oneLine = printedGroups; // Indicates how many we should merge // // Example (true): // $this->method()->otherMethod( // 'argument' // ); // // Example (false): // $foo // ->method() // ->otherMethod(); const cutoff = shouldMerge ? 3 : 2; const flatGroups = groups.slice(0, cutoff).flat(); const hasComment = flatGroups.slice(1, -1).some((node) => hasLeadingComment(node.node)) || flatGroups.slice(0, -1).some((node) => hasTrailingComment(node.node)) || (groups[cutoff] && hasLeadingComment(groups[cutoff][0].node)); const hasEncapsedAncestor = getAncestorNode(path, "encapsed"); // If we only have a single `->`, we shouldn't do anything fancy and just // render everything concatenated together. // In `encapsed` node we always print in one line. if ((groups.length <= cutoff && !hasComment) || hasEncapsedAncestor) { return group(oneLine); } // Find out the last node in the first group and check if it has an // empty line after const lastNodeBeforeIndent = getLast( shouldMerge ? groups.slice(1, 2)[0] : groups[0] ).node; const shouldHaveEmptyLineBeforeIndent = lastNodeBeforeIndent.kind !== "call" && shouldInsertEmptyLineAfter(lastNodeBeforeIndent); const expanded = [ printGroup(groups[0]), shouldMerge ? groups.slice(1, 2).map(printGroup) : "", shouldHaveEmptyLineBeforeIndent ? hardline : "", printIndentedGroup(groups.slice(shouldMerge ? 2 : 1)), ]; const callExpressions = printedNodes.filter( (tuple) => tuple.node.kind === "call" ); // We don't want to print in one line if there's: // * A comment. // * 3 or more chained calls. // * Any group but the last one has a hard line. // If the last group is a function it's okay to inline if it fits. if ( hasComment || (callExpressions.length > 2 && callExpressions.some( (exp) => !exp.node.arguments.every((arg) => isSimpleCallArgument(arg)) )) || printedGroups.slice(0, -1).some(willBreak) ) { return group(expanded); } return [ // We only need to check `oneLine` because if `expanded` is chosen // that means that the parent group has already been broken // naturally willBreak(oneLine) || shouldHaveEmptyLineBeforeIndent ? breakParent : "", conditionalGroup([oneLine, expanded]), ]; } function couldGroupArg(arg) { return ( (arg.kind === "array" && (arg.items.length > 0 || arg.comments)) || arg.kind === "function" || arg.kind === "method" || arg.kind === "closure" ); } function shouldGroupLastArg(args) { const lastArg = getLast(args); const penultimateArg = getPenultimate(args); return ( !hasLeadingComment(lastArg) && !hasTrailingComment(lastArg) && couldGroupArg(lastArg) && // If the last two arguments are of the same type, // disable last element expansion. (!penultimateArg || penultimateArg.kind !== lastArg.kind) ); } function shouldGroupFirstArg(args) { if (args.length !== 2) { return false; } const [firstArg, secondArg] = args; return ( (!firstArg.comments || !firstArg.comments.length) && (firstArg.kind === "function" || firstArg.kind === "method" || firstArg.kind === "closure") && secondArg.kind !== "retif" && !couldGroupArg(secondArg) ); } function printArgumentsList(path, options, print, argumentsKey = "arguments") { const args = path.node[argumentsKey]; if (args.length === 0) { return [ "(", printDanglingComments(path, options, /* sameIndent */ true), ")", ]; } let anyArgEmptyLine = false; let hasEmptyLineFollowingFirstArg = false; const printedArguments = path.map(({ node: arg, isLast, isFirst }) => { const parts = [print()]; if (isLast) { // do nothing } else if (isNextLineEmpty(options.originalText, locEnd(arg))) { if (isFirst) { hasEmptyLineFollowingFirstArg = true; } anyArgEmptyLine = true; parts.push(",", hardline, hardline); } else { parts.push(",", line); } return parts; }, argumentsKey); const { node } = path; const lastArg = getLast(args); const maybeTrailingComma = (shouldPrintComma(options, 7.3) && ["call", "new", "unset", "isset"].includes(node.kind)) || (shouldPrintComma(options, 8.0) && ["function", "closure", "method", "arrowfunc", "attribute"].includes( node.kind )) ? indent([ lastArg && shouldPrintHardlineBeforeTrailingComma(lastArg) ? hardline : "", ",", ]) : ""; function allArgsBrokenOut() { return group( ["(", indent([line, ...printedArguments]), maybeTrailingComma, line, ")"], { shouldBreak: true } ); } const shouldGroupFirst = shouldGroupFirstArg(args); const shouldGroupLast = shouldGroupLastArg(args); if (shouldGroupFirst || shouldGroupLast) { const shouldBreak = (shouldGroupFirst ? printedArguments.slice(1).some(willBreak) : printedArguments.slice(0, -1).some(willBreak)) || anyArgEmptyLine; // We want to print the last argument with a special flag let printedExpanded; path.each(({ isLast, isFirst }) => { if (shouldGroupFirst && isFirst) { printedExpanded = [ print([], { expandFirstArg: true }), printedArguments.length > 1 ? "," : "", hasEmptyLineFollowingFirstArg ? hardline : line, hasEmptyLineFollowingFirstArg ? hardline : "", printedArguments.slice(1), ]; } if (shouldGroupLast && isLast) { printedExpanded = [ ...printedArguments.slice(0, -1), print([], { expandLastArg: true }), ]; } }, argumentsKey); const somePrintedArgumentsWillBreak = printedArguments.some(willBreak); const simpleConcat = ["(", ...printedExpanded, ")"]; return [ somePrintedArgumentsWillBreak ? breakParent : "", conditionalGroup( [ !somePrintedArgumentsWillBreak ? simpleConcat : ifBreak(allArgsBrokenOut(), simpleConcat), shouldGroupFirst ? [ "(", group(printedExpanded[0], { shouldBreak: true }), ...printedExpanded.slice(1), ")", ] : [ "(", ...printedArguments.slice(0, -1), group(getLast(printedExpanded), { shouldBreak: true, }), ")", ], group( [ "(", indent([line, ...printedArguments]), ifBreak(maybeTrailingComma), line, ")", ], { shouldBreak: true } ), ], { shouldBreak } ), ]; } return group( [ "(", indent([softline, ...printedArguments]), ifBreak(maybeTrailingComma), softline, ")", ], { shouldBreak: printedArguments.some(willBreak) || anyArgEmptyLine, } ); } function shouldInlineRetifFalseExpression(node) { return node.kind === "array" && node.items.length !== 0; } function shouldInlineLogicalExpression(node) { return node.right.kind === "array" && node.right.items.length !== 0; } // For binary expressions to be consistent, we need to group // subsequent operators with the same precedence level under a single // group. Otherwise they will be nested such that some of them break // onto new lines but not all. Operators with the same precedence // level should either all break or not. Because we group them by // precedence level and the AST is structured based on precedence // level, things are naturally broken up correctly, i.e. `&&` is // broken before `+`. function printBinaryExpression( path, print, options, isNested, isInsideParenthesis ) { let parts = []; const { node } = path; if (node.kind === "bin") { // Put all operators with the same precedence level in the same // group. The reason we only need to do this with the `left` // expression is because given an expression like `1 + 2 - 3`, it // is always parsed like `((1 + 2) - 3)`, meaning the `left` side // is where the rest of the expression will exist. Binary // expressions on the right side mean they have a difference // precedence level and should be treated as a separate group, so // print them normally. (This doesn't hold for the `**` operator, // which is unique in that it is right-associative.) if (shouldFlatten(node.type, node.left.type)) { // Flatten them out by recursively calling this function. parts = parts.concat( path.call( () => printBinaryExpression( path, print, options, /* isNested */ true, isInsideParenthesis ), "left" ) ); } else { parts.push(print("left")); } const shouldInline = shouldInlineLogicalExpression(node); const right = shouldInline ? [node.type, " ", print("right")] : [node.type, line, print("right")]; // If there's only a single binary expression, we want to create a group // in order to avoid having a small right part like -1 be on its own line. const { parent } = path; const shouldGroup = !(isInsideParenthesis && ["||", "&&"].includes(node.type)) && getNodeKindIncludingLogical(parent) !== getNodeKindIncludingLogical(node) && getNodeKindIncludingLogical(node.left) !== getNodeKindIncludingLogical(node) && getNodeKindIncludingLogical(node.right) !== getNodeKindIncludingLogical(node); const shouldNotHaveWhitespace = isDocNode(node.left) || (node.left.kind === "bin" && isDocNode(node.left.right)); parts.push( shouldNotHaveWhitespace ? "" : " ", shouldGroup ? group(right) : right ); // The root comments are already printed, but we need to manually print // the other ones since we don't call the normal print on bin, // only for the left and right parts if (isNested && node.comments) { parts = printAllComments(path, () => parts, options); } } else { // Our stopping case. Simply print the node normally. parts.push(print()); } return parts; } function printLookupNodes(path, options, print) { const { node } = path; switch (node.kind) { case "propertylookup": return printPropertyLookup(path, options, print); case "nullsafepropertylookup": return printNullsafePropertyLookup(path, options, print); case "staticlookup": return printStaticLookup(path, options, print); case "offsetlookup": return printOffsetLookup(path, options, print); /* c8 ignore next 2 */ default: throw new Error(`Have not implemented lookup kind ${node.kind} yet.`); } } function getEncapsedQuotes(node, { opening = true } = {}) { if (node.type === "heredoc") { return opening ? `<<<${node.label}` : node.label; } const quotes = { string: '"', shell: "`", }; if (quotes[node.type]) { return quotes[node.type]; } /* c8 ignore next */ throw new Error(`Unimplemented encapsed type ${node.type}`); } function printArrayItems(path, options, print) { const printedElements = []; let separatorParts = []; path.each(({ node }) => { printedElements.push(separatorParts); printedElements.push(group(print())); separatorParts = [",", line]; if (node && isNextLineEmpty(options.originalText, locEnd(node))) { separatorParts.push(softline); } }, "items"); return printedElements; } // Wrap parts into groups by indexes. // It is require to have same indent on lines for all parts into group. // The value of `alignment` option indicates how many spaces must be before each part. // // Example: // <div> // <?php // echo '1'; // echo '2'; // echo '3'; // ?> // </div> function wrapPartsIntoGroups(parts, indexes) { if (indexes.length === 0) { return parts; } let lastEnd = 0; return indexes.reduce((accumulator, index) => { const { start, end, alignment, before, after } = index; const printedPartsForGrouping = [ before || "", ...parts.slice(start, end), after || "", ]; const newArray = accumulator.concat( parts.slice(lastEnd, start), alignment ? dedentToRoot( group( align(new Array(alignment).join(" "), printedPartsForGrouping) ) ) : group(printedPartsForGrouping), end === parts.length - 1 ? parts.slice(end) : "" ); lastEnd = end; return newArray; }, []); } function printLines(path, options, print, childrenAttribute = "children") { const { node, parent: parentNode } = path; let lastInlineIndex = -1; const parts = []; const groupIndexes = []; path.map(() => { const { node: childNode, next: nextNode, isFirst: isFirstNode, isLast: isLastNode, index, } = path; const isInlineNode = childNode.kind === "inline"; const printedPath = print(); const canPrintBlankLine = !isLastNode && !isInlineNode && (nextNode && nextNode.kind === "case" ? !isFirstChildrenInlineNode(path) : nextNode && nextNode.kind !== "inline"); let printed = [ printedPath, canPrintBlankLine ? hardline : "", canPrintBlankLine && isNextLineEmpty(options.originalText, locEnd(childNode)) ? hardline : "", ]; const isBlockNestedNode = node.kind === "block" && parentNode && ["function", "closure", "method", "try", "catch"].includes( parentNode.kind ); let beforeCloseTagInlineNode = isBlockNestedNode && isFirstNode ? "" : " "; if (isInlineNode || (!isInlineNode && isLastNode && lastInlineIndex >= 0)) { const prevLastInlineIndex = lastInlineIndex; if (isInlineNode) { lastInlineIndex = index; } const shouldCreateGroup = (isInlineNode && !isFirstNode) || (!isInlineNode && isLastNode); if (shouldCreateGroup) { const start = (isInlineNode ? prevLastInlineIndex : lastInlineIndex) + 1; const end = isLastNode && !isInlineNode ? index + 1 : index; const prevInlineNode = path.siblings[isInlineNode ? prevLastInlineIndex : lastInlineIndex]; const alignment = prevInlineNode ? getAlignment(prevInlineNode.raw) : ""; const shouldBreak = end - start > 1; const before = shouldBreak ? (isBlockNestedNode && !prevInlineNode) || (isProgramLikeNode(node) && start === 0) ? "" : hardline : ""; const after = shouldBreak && childNode.kind !== "halt" ? isBlockNestedNode && isLastNode ? "" : hardline : ""; if (shouldBreak) { beforeCloseTagInlineNode = ""; } groupIndexes.push({ start, end, alignment, before, after }); } } if (isInlineNode) { const openTag = nextNode && nextNode.kind === "echo" && nextNode.shortForm ? "<?=" : "<?php"; const beforeInline = childNode.leadingComments && childNode.leadingComments.length ? [ isFirstNode && node.kind !== "namespace" && !isBlockNestedNode ? "<?php" : "", node.kind === "namespace" || !isBlockNestedNode ? hardline : "", printComments(childNode.leadingComments, options), hardline, "?>", ] : isProgramLikeNode(node) && isFirstNode && node.kind !== "namespace" ? "" : [beforeCloseTagInlineNode, "?>"]; //FIXME getNode is used to get ancestors, but it seems this means to get next sibling? const nextV = path.getNode(index + 1); const skipLastComment = nextV && nextV.children && nextV.children.length; const afterInline = childNode.comments && childNode.comments.length ? [ openTag, hardline, skipLastComment ? printComments(childNode.comments, options) : "", hardline, ] : isProgramLikeNode(node) && isLastNode ? "" : [openTag, " "]; printed = [beforeInline, printed, afterInline]; } parts.push(printed); }, childrenAttribute); const wrappedParts = wrapPartsIntoGroups(parts, groupIndexes); if (node.kind === "program" && !node.extra.parseAsEval) { const parts = []; const [firstNode] = node.children; const hasStartTag = !firstNode || firstNode.kind !== "inline"; if (hasStartTag) { const between = options.originalText.trim().match(/^<\?(php|=)(\s+)?\S/); const afterOpenTag = [ between && between[2] && between[2].includes("\n") ? [hardline, between[2].split("\n").length > 2 ? hardline : ""] : " ", node.comments ? printComments(node.comments, options) : "", ]; const shortEcho = firstNode && firstNode.kind === "echo" && firstNode.shortForm; parts.push([shortEcho ? "<?=" : "<?php", afterOpenTag]); } parts.push(wrappedParts); const hasEndTag = /\?>\n?$/.test(options.originalText); if (hasEndTag) { const lastNode = getLast(node.children); const beforeCloseTag = lastNode ? [ hasNewlineInRange( options.originalText.trimEnd(), locEnd(lastNode), locEnd(node) ) ? !( lastNode.kind === "inline" && lastNode.comments && lastNode.comments.length ) ? hardline : "" : " ", isNextLineEmpty(options.originalText, locEnd(lastNode)) ? hardline : "", ] : node.comments ? hardline : ""; parts.push(lineSuffix([beforeCloseTag, "?>"])); } return parts; } return wrappedParts; } function printStatements(path, options, print, childrenAttribute) { return path.map(({ node, isLast }) => { const parts = []; parts.push(print()); if (!isLast) { parts.push(hardline); if (isNextLineEmpty(options.originalText, locEnd(node))) { parts.push(hardline); } } return parts; }, childrenAttribute); } function printClassPart( path, options, print, part = "extends", beforePart = " ", afterPart = " " ) { const value = path.node[part]; const printedBeforePart = hasDanglingComments(value) ? [ hardline, path.call(() => printDanglingComments(path, options, true), part), hardline, ] : beforePart; const printedPartItems = Array.isArray(value) ? group( join( ",", path.map(({ node }) => { const printedPart = print(); // Check if any of the implements nodes have comments return hasDanglingComments(node) ? [ hardline, printDanglingComments(path, options, true), hardline, printedPart, ] : [afterPart, printedPart]; }, part) ) ) : [afterPart, print(part)]; return indent([ printedBeforePart, part, willBreak(printedBeforePart) ? indent(printedPartItems) : printedPartItems, ]); } function printAttrs(path, options, print, { inline = false } = {}) { const allAttrs = []; if (!path.node.attrGroups) { return []; } path.each(() => { const attrGroup = ["#["]; if (!inline && allAttrs.length > 0) { allAttrs.push(hardline); } attrGroup.push(softline); path.each(() => { const attrNode = path.node; if (attrGroup.length > 2) { attrGroup.push(",", line); } const attrStmt = [attrNode.name]; if (attrNode.args.length > 0) { attrStmt.push(printArgumentsList(path, options, print, "args")); } attrGroup.push(group(attrStmt)); }, "attrs"); allAttrs.push( group([ indent(attrGroup), ifBreak(shouldPrintComma(options, 8.0) ? "," : ""), softline, "]", inline ? ifBreak(softline, " ") : "", ]) ); }, "attrGroups"); if (allAttrs.length === 0) { return []; } return [...allAttrs, inline ? "" : hardline]; } function printClass(path, options, print) { const { node } = path; const isAnonymousClass = node.kind === "class" && node.isAnonymous; const attrs = printAttrs(path, options, print, { inline: isAnonymousClass }); const declaration = isAnonymousClass ? [] : [...attrs]; if (node.isFinal) { declaration.push("final "); } if (node.isAbstract) { declaration.push("abstract "); } if (node.isReadonly) { declaration.push("readonly "); } // `new` print `class` keyword with arguments declaration.push(isAnonymousClass ? "" : node.kind); if (node.name) { declaration.push(" ", print("name")); } if (node.kind === "enum" && node.valueType) { declaration.push(": ", print("valueType")); } // Only `class` can have `extends` and `implements` if (node.extends && node.implements) { declaration.push( conditionalGroup( [ [ printClassPart(path, options, print, "extends"), printClassPart(path, options, print, "implements"), ], [ printClassPart(path, options, print, "extends"), printClassPart(path, options, print, "implements", " ", hardline), ], [ printClassPart(path, options, print, "extends", hardline, " "), printClassPart( path, options, print, "implements", hardline, node.implements.length > 1 ? hardline : " " ), ], ], { shouldBreak: hasDanglingComments(node.extends), } ) ); } else { if (node.extends) { declaration.push( conditionalGroup([ printClassPart(path, options, print, "extends"), printClassPart(path, options, print, "extends", " ", hardline), printClassPart( path, options, print, "extends", hardline, node.extends.length > 1 ? hardline : " " ), ]) ); } if (node.implements) { declaration.push( conditionalGroup([ printClassPart(path, options, print, "implements"), printClassPart(path, options, print, "implements", " ", hardline), printClassPart( path, options, print, "implements", hardline, node.implements.length > 1 ? hardline : " " ), ]) ); } } const hasEmptyClassBody = node.body && node.body.length === 0 && !hasDanglingComments(node); const printedDeclaration = group([ group(declaration), shouldPrintHardlineForOpenBrace(options) && !hasEmptyClassBody ? isAnonymousClass ? line : hardline : " ", ]); const printedBody = [ "{", indent([ hasEmptyClassBody ? "" : hardline, printStatements(path, options, print, "body"), ]), printDanglingComments(path, options, true), hasEmptyClassBody ? "" : hardline, "}", ]; return [printedDeclaration, printedBody]; } function printFunction(path, options, print) { const { node } = path; const declAttrs = printAttrs(path, options, print, { inline: node.kind === "closure", }); const declaration = []; if (node.isFinal) { declaration.push("final "); } if (node.isAbstract) { declaration.push("abstract "); } if (node.visibility) { declaration.push(node.visibility, " "); } if (node.isStatic) { declaration.push("static "); } declaration.push("function "); if (node.byref) { declaration.push("&"); } if (node.name) { declaration.push(print("name")); } declaration.push(printArgumentsList(path, options, print)); if (node.uses && node.uses.length > 0) { declaration.push( group([" use ", printArgumentsList(path, options, print, "uses")]) ); } if (node.type) { declaration.push([ ": ", hasDanglingComments(node.type) ? [ path.call(() => printDanglingComments(path, options, true), "type"), " ", ] : "", node.nullable ? "?" : "", print("type"), ]); } const printedDeclaration = declaration; if (!node.body) { return [...declAttrs, printedDeclaration]; } const printedBody = [ "{", indent([hasEmptyBody(path) ? "" : hardline, print("body")]), hasEmptyBody(path) ? "" : hardline, "}", ]; const isClosure = node.kind === "closure"; if (isClosure) { return [...declAttrs, printedDeclaration, " ", printedBody]; } if (node.arguments.length === 0) { return [ ...declAttrs, printedDeclaration, shouldPrintHardlineForOpenBrace(options) && !hasEmptyBody(path) ? hardline : " ", printedBody, ]; } const willBreakDeclaration = declaration.some(willBreak); if (willBreakDeclaration) { return [...declAttrs, printedDeclaration, " ", printedBody]; } return [ ...declAttrs, conditionalGroup([ [ printedDeclaration, shouldPrintHardlineForOpenBrace(options) && !hasEmptyBody(path) ? hardline : " ", printedBody, ], [printedDeclaration, " ", printedBody], ]), ]; } function printBodyControlStructure( path, options, print, bodyProperty = "body" ) { const { node } = path; if (!node[bodyProperty]) { return ";"; } const printedBody = print(bodyProperty); return [ node.shortForm ? ":" : " {", indent( node[bodyProperty].kind !== "block" || (node[bodyProperty].children && node[bodyProperty].children.length > 0) || (node[bodyProperty].comments && node[bodyProperty].comments.length > 0) ? [ shouldPrintHardLineAfterStartInControlStructure(path) ? node.kind === "switch" ? " " : "" : hardline, printedBody, ] : "" ), node.kind === "if" && bodyProperty === "body" ? "" : [ shouldPrintHardLineBeforeEndInControlStructure(path) ? hardline : "", node.shortForm ? ["end", node.kind, ";"] : "}", ], ]; } function printAssignment( leftNode, printedLeft, operator, rightNode, printedRight, hasRef, options ) { if (!rightNode) { return printedLeft; } const printed = printAssignmentRight( leftNode, rightNode, printedRight, hasRef, options ); return group([printedLeft, operator, printed]); } function isLookupNodeChain(node) { if (!isLookupNode(node)) { return false; } if (node.what.kind === "variable" || isReferenceLikeNode(node.what)) { return true; } return isLookupNodeChain(node.what); } function printAssignmentRight( leftNode, rightNode, printedRight, hasRef, options ) { const ref = hasRef ? "&" : ""; if (hasLeadingOwnLineComment(options.originalText, rightNode)) { return indent([hardline, ref, printedRight]); } const pureRightNode = rightNode.kind === "cast" ? rightNode.expr : rightNode; const canBreak = (pureRightNode.kind === "bin" && !shouldInlineLogicalExpression(pureRightNode)) || (pureRightNode.kind === "retif" && ((!pureRightNode.trueExpr && !shouldInlineRetifFalseExpression(pureRightNode.falseExpr)) || (pureRightNode.test.kind === "bin" && !shouldInlineLogicalExpression(pureRightNode.test)))) || ((leftNode.kind === "variable" || leftNode.kind === "string" || isLookupNode(leftNode)) && ((pureRightNode.kind === "string" && !stringHasNewLines(pureRightNode)) || isLookupNodeChain(pureRightNode))); if (canBreak) { return group(indent([line, ref, printedRight])); } return [" ", ref, printedRight]; } function needsHardlineAfterDanglingComment(node) { if (!node.comments) { return false; } const lastDanglingComment = getLast( node.comments.filter((comment) => !comment.leading && !comment.trailing) ); return lastDanglingComment && !isBlockComment(lastDanglingComment); } function stringHasNewLines(node) { return node.raw.includes("\n"); } function isStringOnItsOwnLine(node, text) { return ( (node.kind === "string" || (node.kind === "encapsed" && (node.type === "string" || node.type === "shell"))) && stringHasNewLines(node) && !hasNewline(text, locStart(node), { backwards: true }) ); } function printComposedTypes(path, print, glue) { return group( path.map(({ isFirst }) => (isFirst ? [print()] : [glue, print()]), "types") ); } function printNode(path, options, print) { const { node } = path; switch (node.kind) { case "program": { return group([ printLines(path, options, print), printDanglingComments( path, options, /* sameIndent */ true, (c) => !c.printed ), ]); } case "expressionstatement": return print("expression"); case "block": return [ printLines(path, options, print), printDanglingComments(path, options, true), ]; case "declare": { const printDeclareArguments = (path) => { return join(", ", path.map(print, "directives")); }; if (["block", "short"].includes(node.mode)) { return [ "declare(", printDeclareArguments(path), ")", node.mode === "block" ? " {" : ":", node.children.length > 0 ? indent([hardline, printLines(path, options, print)]) : "", printDanglingComments(path, options), hardline, node.mode === "block" ? "}" : "enddeclare;", ]; } return [ "declare(", printDeclareArguments(path), ")", path.next?.kind === "inline" ? "" : ";", ]; } case "declaredirective": return [print("key"), "=", print("value")]; case "namespace": return [ "namespace ", node.name && typeof node.name === "string" ? [node.name, node.withBrackets ? " " : ""] : "", node.withBrackets ? "{" : ";", hasDanglingComments(node) ? [" ", printDanglingComments(path, options, true)] : "", node.children.length > 0 ? node.withBrackets ? indent([hardline, printLines(path, options, print)]) : [ node.children[0].kind === "inline" ? "" : [ hardline, isNextLineEmptyAfterNamespace(options.originalText, node) ? hardline : "", ], printLines(path, options, print), ] : "", node.withBrackets ? [hardline, "}"] : "", ]; case "usegroup": return group([ "use ", node.type ? [node.type, " "] : "", indent([ node.name ? [maybeStripLeadingSlashFromUse(node.name), "\\{", softline] : "", join([",", line], path.map(print, "items")), ]), node.name ? [ifBreak(shouldPrintComma(options, 7.2) ? "," : ""), softline, "}"] : "", ]); case "useitem": return [ node.type ? [node.type, " "] : "", maybeStripLeadingSlashFromUse(node.name), hasDanglingComments(node) ? [" ", printDanglingComments(path, options, true)] : "", node.alias ? [" as ", print("alias")] : "", ]; case "class": case "enum": case "interface": case "trait": return printClass(path, options, print); case "traitprecedence": return [ print("trait"), "::", print("method"), " insteadof ", join(", ", path.map(print, "instead")), ]; case "traitalias": return [ node.trait ? [print("trait"), "::"] : "", node.method ? print("method") : "", " as ", join(" ", [ ...(node.visibility ? [node.visibility] : []), ...(node.as ? [print("as")] : []), ]), ]; case "traituse": return group([ "use ", indent(group(join([",", line], path.map(print, "traits")))), node.adaptations ? [ " {", node.adaptations.length > 0 ? [ indent([ hardline, printStatements(path, options, print, "adaptations"), ]), hardline, ] : hasDanglingComments(node) ? [line, printDanglingComments(path, options, true), line] : "", "}", ] : "", ]); case "function": case "closure": case "method": return printFunction(path, options, print); case "arrowfunc": return [ node.parenthesizedExpression ? "(" : "", ...printAttrs(path, options, print, { inline: true }), node.isStatic ? "static " : "", "fn", printArgumentsList(path, options, print), node.type ? [": ", node.nullable ? "?" : "", print("type")] : "", " => ", print("body"), node.parenthesizedExpression ? ")" : "", ]; case "parameter": { let promoted = ""; if (node.flags === 1) { promoted = "public "; } else if (node.flags === 2) { promoted = "protected "; } else if (node.flags === 4) { promoted = "private "; } const name = [ ...printAttrs(path, options, print, { inline: true }), promoted, node.readonly ? "readonly " : "", node.nullable ? "?" : "", node.type ? [print("type"), " "] : "", node.byref ? "&" : "", node.variadic ? "..." : "", "$", print("name"), ]; if (node.value) { return group([ name, // see handleFunctionParameter() in ./comments.mjs - since there's // no node to attach comments that fall in between the parameter name // and value, we store them as dangling comments hasDanglingComments(node) ? " " : "", printDanglingComments(path, options, true), " =", printAssignmentRight( node.name, node.value, print("value"), false, options ), ]); } return name; } case "variadic": return ["...", print("what")]; case "property": return group([ node.readonly ? "readonly " : "", node.type ? [node.nullable ? "?" : "", print("type"), " "] : "", "$", print("name"), node.value ? [ " =", printAssignmentRight( node.name, node.value, print("value"), false, options ), ] : "", ]); case "propertystatement": { const attrs = []; path.each(() => { attrs.push(...printAttrs(path, options, print)); }, "properties"); const printed = path.map(print, "properties"); const hasValue = node.properties.some((property) => property.value); let firstProperty; if (printed.length === 1 && !node.properties[0].comments) { [firstProperty] = printed; } else if (printed.length > 0) { // Indent first property firstProperty = indent(printed[0]); } const hasVisibility = node.visibility || node.visibility === null; return group([ ...attrs, hasVisibility ? [node.visibility === null ? "var" : node.visibility, ""] : "", node.isStatic ? [hasVisibility ? " " : "", "static"] : "", firstProperty ? [" ", firstProperty] : "", indent( printed.slice(1).map((p) => [",", hasValue ? hardline : line, p]) ), ]); } case "if": { const parts = []; const body = printBodyControlStructure(path, options, print, "body"); const opening = group([ "if (", group([indent([softline, print("test")]), softline]), ")", body, ]); parts.push( opening, isFirstChildrenInlineNode(path) || !node.body ? "" : hardline ); if (node.alternate) { parts.push(node.shortForm ? "" : "} "); const commentOnOwnLine = (hasTrailingComment(node.body) && node.body.comments.some( (comment) => comment.trailing && !isBlockComment(comment) )) || needsHardlineAfterDanglingComment(node); const elseOnSameLine = !commentOnOwnLine; parts.push(elseOnSameLine ? "" : hardline); if (hasDanglingComments(node)) { parts.push( isNextLineEmpty(options.originalText, locEnd(node.body)) ? hardline : "", printDanglingComments(path, options, true), commentOnOwnLine ? hardline : " "