UNPKG

@prettier/plugin-php

Version:
1,863 lines (1,643 loc) 79.3 kB
"use strict"; const { breakParent, concat, join, line, lineSuffix, group, conditionalGroup, indent, dedent, ifBreak, hardline, softline, literalline, align, dedentToRoot, } = require("prettier").doc.builders; const { willBreak } = require("prettier").doc.utils; const { isNextLineEmptyAfterIndex, hasNewline, hasNewlineInRange, } = require("prettier").util; const comments = require("./comments"); const pathNeedsParens = require("./needs-parens"); const { getLast, getPenultimate, isLastStatement, lineShouldEndWithSemicolon, printNumber, shouldFlatten, maybeStripLeadingSlashFromUse, fileShouldEndWithHardline, hasDanglingComments, hasLeadingComment, hasTrailingComment, docShouldHaveTrailingNewline, isLookupNode, isFirstChildrenInlineNode, shouldPrintHardLineAfterStartInControlStructure, shouldPrintHardLineBeforeEndInControlStructure, getAlignment, isProgramLikeNode, getNodeKindIncludingLogical, useDoubleQuote, hasEmptyBody, isNextLineEmptyAfterNamespace, shouldPrintHardlineBeforeTrailingComma, isDocNode, getAncestorNode, isReferenceLikeNode, getNextNode, normalizeMagicMethodName, getNextNonSpaceNonCommentCharacterIndex, isNextLineEmpty, } = require("./util"); function isMinVersion(actualVersion, requiredVersion) { return parseFloat(actualVersion) >= parseFloat(requiredVersion); } function shouldPrintComma(options, requiredVersion) { if (!options.trailingCommaPHP) { return false; } return isMinVersion(options.phpVersion, requiredVersion); } function shouldPrintHardlineForOpenBrace(options) { switch (options.braceStyle) { case "1tbs": return false; case "psr-2": default: return true; } } function genericPrint(path, options, print) { const node = path.getValue(); if (!node) { return ""; } else 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 concat(parts); } function printPropertyLookup(path, options, print) { return concat(["->", path.call(print, "offset")]); } function printStaticLookup(path, options, print) { const node = path.getValue(); const needCurly = !["variable", "identifier"].includes(node.offset.kind); return concat([ "::", needCurly ? "{" : "", path.call(print, "offset"), needCurly ? "}" : "", ]); } function printOffsetLookup(path, options, print) { const node = path.getValue(); const shouldInline = (node.offset && node.offset.kind === "number") || getAncestorNode(path, "encapsed"); return concat([ "[", node.offset ? group( concat([ indent( concat([shouldInline ? "" : softline, path.call(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, node, options ); 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, node, options); } function traverse(path) { const node = path.getValue(); if ( node.kind === "call" && (isLookupNode(node.what) || node.what.kind === "call") ) { printedNodes.unshift({ node, printed: concat([ comments.printAllComments( path, () => concat([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 === "staticlookup") { printedMemberish = printStaticLookup(path, options, print); } else { printedMemberish = printOffsetLookup(path, options, print); } printedNodes.unshift({ node, needsParens: pathNeedsParens(path, options), printed: comments.printAllComments( path, () => printedMemberish, options ), }); path.call((what) => traverse(what), "what"); } else { printedNodes.unshift({ node, printed: path.call(print), }); } } const node = path.getValue(); 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", "staticlookup"].includes( printedNodes[i - 1].node.kind ) && printedNodes[i - 1].needsParens ) { printedNodes[0].printed = concat(["(", printedNodes[0].printed]); printedNodes[i - 1].printed = concat([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 && 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(); // 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") || isReferenceLikeNode(firstNode) ); } const lastNode = getLast(groups[0]).node; return ( isLookupNode(lastNode) && (lastNode.offset.kind === "identifier" || lastNode.offset.kind === "variable") && hasComputed ); } 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 concat(result); } function printIndentedGroup(groups) { if (groups.length === 0) { return ""; } return indent( group(concat([hardline, join(hardline, groups.map(printGroup))])) ); } const printedGroups = groups.map(printGroup); const oneLine = concat(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) .reduce((res, group) => res.concat(group), []); const hasComment = flatGroups .slice(1, -1) .some((node) => comments.hasLeadingComment(node.node)) || flatGroups .slice(0, -1) .some((node) => comments.hasTrailingComment(node.node)) || (groups[cutoff] && comments.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 = concat([ printGroup(groups[0]), shouldMerge ? concat(groups.slice(1, 2).map(printGroup)) : "", shouldHaveEmptyLineBeforeIndent ? hardline : "", printIndentedGroup(groups.slice(shouldMerge ? 2 : 1)), ]); const callExpressionCount = printedNodes.filter( (tuple) => tuple.node.kind === "call" ).length; // 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 || callExpressionCount >= 3 || printedGroups.slice(0, -1).some(willBreak) ) { return group(expanded); } return concat([ // 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.getValue()[argumentsKey]; if (args.length === 0) { return concat([ "(", comments.printDanglingComments(path, options, /* sameIndent */ true), ")", ]); } let anyArgEmptyLine = false; let hasEmptyLineFollowingFirstArg = false; const lastArgIndex = args.length - 1; const printedArguments = path.map((argPath, index) => { const arg = argPath.getNode(); const parts = [print(argPath)]; if (index === lastArgIndex) { // do nothing } else if (isNextLineEmpty(options.originalText, arg, options)) { if (index === 0) { hasEmptyLineFollowingFirstArg = true; } anyArgEmptyLine = true; parts.push(",", hardline, hardline); } else { parts.push(",", line); } return concat(parts); }, argumentsKey); const node = path.getValue(); const lastArg = getLast(args); const maybeTrailingComma = ["call", "new", "unset", "isset"].includes(node.kind) && shouldPrintComma(options, "7.3") ? indent( concat([ lastArg && shouldPrintHardlineBeforeTrailingComma(lastArg) ? hardline : "", ",", ]) ) : ""; function allArgsBrokenOut() { return group( concat([ "(", indent(concat([line, concat(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; let i = 0; path.each((argPath) => { if (shouldGroupFirst && i === 0) { printedExpanded = [ concat([ argPath.call((p) => print(p, { expandFirstArg: true })), printedArguments.length > 1 ? "," : "", hasEmptyLineFollowingFirstArg ? hardline : line, hasEmptyLineFollowingFirstArg ? hardline : "", ]), ].concat(printedArguments.slice(1)); } if (shouldGroupLast && i === args.length - 1) { printedExpanded = printedArguments .slice(0, -1) .concat(argPath.call((p) => print(p, { expandLastArg: true }))); } i++; }, argumentsKey); const somePrintedArgumentsWillBreak = printedArguments.some(willBreak); const simpleConcat = concat(["(", concat(printedExpanded), ")"]); return concat([ somePrintedArgumentsWillBreak ? breakParent : "", conditionalGroup( [ !somePrintedArgumentsWillBreak ? simpleConcat : ifBreak(allArgsBrokenOut(), simpleConcat), shouldGroupFirst ? concat([ "(", group(printedExpanded[0], { shouldBreak: true }), concat(printedExpanded.slice(1)), ")", ]) : concat([ "(", concat(printedArguments.slice(0, -1)), group(getLast(printedExpanded), { shouldBreak: true, }), ")", ]), group( concat([ "(", indent(concat([line, concat(printedArguments)])), ifBreak(maybeTrailingComma), line, ")", ]), { shouldBreak: true } ), ], { shouldBreak } ), ]); } return group( concat([ "(", indent(concat([softline, concat(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.getValue(); 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( (left) => printBinaryExpression( left, print, options, /* isNested */ true, isInsideParenthesis ), "left" ) ); } else { parts.push(path.call(print, "left")); } const shouldInline = shouldInlineLogicalExpression(node); const right = shouldInline ? concat([node.type, " ", path.call(print, "right")]) : concat([node.type, line, path.call(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.getParentNode(); 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 = comments.printAllComments(path, () => concat(parts), options); } } else { // Our stopping case. Simply print the node normally. parts.push(path.call(print)); } return parts; } function printLookupNodes(path, options, print) { const node = path.getValue(); switch (node.kind) { case "propertylookup": return printPropertyLookup(path, options, print); case "staticlookup": return printStaticLookup(path, options, print); case "offsetlookup": return printOffsetLookup(path, options, print); /* istanbul ignore next */ default: return `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]; } /* istanbul ignore next */ return `Unimplemented encapsed type ${node.type}`; } function printArrayItems(path, options, print) { const printedElements = []; let separatorParts = []; path.each((childPath) => { printedElements.push(concat(separatorParts)); printedElements.push(group(print(childPath))); separatorParts = [",", line]; if ( childPath.getValue() && isNextLineEmpty(options.originalText, childPath.getValue(), options) ) { separatorParts.push(softline); } }, "items"); return concat(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 = concat([ before || "", concat(parts.slice(start, end)), after || "", ]); const newArray = accumulator.concat( parts.slice(lastEnd, start), alignment ? dedentToRoot( group( concat([ 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 = path.getValue(); const parentNode = path.getParentNode(); let lastInlineIndex = -1; const parts = []; const groupIndexes = []; path.map((childPath, index) => { const childNode = childPath.getValue(); const isInlineNode = childNode.kind === "inline"; const printedPath = print(childPath); const children = node[childrenAttribute]; const nextNode = children[index + 1]; const canPrintBlankLine = !isLastStatement(childPath) && !isInlineNode && (nextNode && nextNode.kind === "case" ? !isFirstChildrenInlineNode(path) : nextNode && nextNode.kind !== "inline"); let printed = concat([ printedPath, canPrintBlankLine ? hardline : "", canPrintBlankLine && isNextLineEmpty(options.originalText, childNode, options) ? hardline : "", ]); const isFirstNode = index === 0; const isLastNode = children.length - 1 === index; 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 = children[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 ? concat([ isFirstNode && node.kind !== "namespace" && !isBlockNestedNode ? openTag : "", node.kind === "namespace" || !isBlockNestedNode ? hardline : "", comments.printComments(childNode.leadingComments, options), hardline, "?>", ]) : isProgramLikeNode(node) && isFirstNode && node.kind !== "namespace" ? "" : concat([beforeCloseTagInlineNode, "?>"]); const afterInline = childNode.comments && childNode.comments.length ? concat([ openTag, hardline, comments.printComments(childNode.comments, options), hardline, "?>", ]) : isProgramLikeNode(node) && isLastNode ? "" : concat([openTag, " "]); printed = concat([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 = concat([ between && between[2] && between[2].includes("\n") ? concat([ hardline, between[2].split("\n").length > 2 ? hardline : "", ]) : " ", node.comments ? comments.printComments(node.comments, options) : "", ]); const shortEcho = firstNode && firstNode.kind === "echo" && firstNode.shortForm; parts.push(concat([shortEcho ? "<?=" : "<?php", afterOpenTag])); } parts.push(concat(wrappedParts)); const hasEndTag = options.originalText.trim().endsWith("?>"); if (hasEndTag) { const lastNode = getLast(node.children); const beforeCloseTag = lastNode ? concat([ hasNewlineInRange( options.originalText, options.locEnd(lastNode), options.locEnd(node) ) ? hardline : " ", isNextLineEmpty(options.originalText, lastNode, options) ? hardline : "", ]) : node.comments ? hardline : ""; parts.push(lineSuffix(concat([beforeCloseTag, "?>"]))); } return concat(parts); } return concat(wrappedParts); } function printStatements(path, options, print, childrenAttribute) { return concat( path.map((childPath) => { const parts = []; parts.push(print(childPath)); if (!isLastStatement(childPath)) { parts.push(hardline); if ( isNextLineEmpty(options.originalText, childPath.getValue(), options) ) { parts.push(hardline); } } return concat(parts); }, childrenAttribute) ); } function printClassPart( path, options, print, part = "extends", beforePart = " ", afterPart = " " ) { const node = path.getValue(); const printedBeforePart = hasDanglingComments(node[part]) ? concat([ hardline, path.call( (partPath) => comments.printDanglingComments(partPath, options, true), part ), hardline, ]) : beforePart; const printedPartItems = Array.isArray(node[part]) ? group( concat([ join( ",", path.map((itemPartPath) => { const printedPart = print(itemPartPath); // Check if any of the implements nodes have comments return hasDanglingComments(itemPartPath.getValue()) ? concat([ hardline, comments.printDanglingComments(itemPartPath, options, true), hardline, printedPart, ]) : concat([afterPart, printedPart]); }, part) ), ]) ) : concat([afterPart, path.call(print, part)]); return indent( concat([ printedBeforePart, part, willBreak(printedBeforePart) ? indent(printedPartItems) : printedPartItems, ]) ); } function printClass(path, options, print) { const node = path.getValue(); const declaration = []; if (node.isFinal) { declaration.push("final "); } if (node.isAbstract) { declaration.push("abstract "); } const isAnonymousClass = node.kind === "class" && node.isAnonymous; // `new` print `class` keyword with arguments declaration.push(isAnonymousClass ? "" : node.kind); if (node.name) { declaration.push(" ", path.call(print, "name")); } // Only `class` can have `extends` and `implements` if (node.extends && node.implements) { declaration.push( conditionalGroup( [ concat([ printClassPart(path, options, print, "extends"), printClassPart(path, options, print, "implements"), ]), concat([ printClassPart(path, options, print, "extends"), printClassPart(path, options, print, "implements", " ", hardline), ]), concat([ 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 printedDeclaration = group( concat([ group(concat(declaration)), shouldPrintHardlineForOpenBrace(options) ? isAnonymousClass ? line : hardline : " ", ]) ); const hasEmptyClassBody = node.body && node.body.length === 0 && !hasDanglingComments(node); const printedBody = concat([ "{", indent( concat([ hasEmptyClassBody ? "" : hardline, printStatements(path, options, print, "body"), ]) ), comments.printDanglingComments(path, options, true), isAnonymousClass && hasEmptyClassBody ? softline : hardline, "}", ]); return concat([printedDeclaration, printedBody]); } function printFunction(path, options, print) { const node = path.getValue(); 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(path.call(print, "name")); } declaration.push(printArgumentsList(path, options, print)); if (node.uses && node.uses.length > 0) { declaration.push( group(concat([" use ", printArgumentsList(path, options, print, "uses")])) ); } if (node.type) { declaration.push( concat([ ": ", hasDanglingComments(node.type) ? concat([ path.call( (typePath) => comments.printDanglingComments(typePath, options, true), "type" ), " ", ]) : "", node.nullable ? "?" : "", path.call(print, "type"), ]) ); } const printedDeclaration = concat(declaration); if (!node.body) { return printedDeclaration; } const isClosure = node.kind === "closure"; const printedBody = concat([ "{", indent( concat([hasEmptyBody(path) ? "" : hardline, path.call(print, "body")]) ), isClosure && hasEmptyBody(path) ? "" : hardline, "}", ]); if (isClosure) { return concat([printedDeclaration, " ", printedBody]); } if (node.arguments.length === 0) { return concat([ printedDeclaration, shouldPrintHardlineForOpenBrace(options) ? hardline : " ", printedBody, ]); } const willBreakDeclaration = declaration.some(willBreak); if (willBreakDeclaration) { return concat([printedDeclaration, " ", printedBody]); } return conditionalGroup([ concat([ printedDeclaration, shouldPrintHardlineForOpenBrace(options) ? hardline : " ", printedBody, ]), concat([printedDeclaration, " ", printedBody]), ]); } function printBodyControlStructure( path, options, print, bodyProperty = "body" ) { const node = path.getValue(); if (!node[bodyProperty]) { return ";"; } const printedBody = path.call(print, bodyProperty); return concat([ node.shortForm ? ":" : " {", indent( concat([ node[bodyProperty].kind !== "block" || (node[bodyProperty].children && node[bodyProperty].children.length > 0) || (node[bodyProperty].comments && node[bodyProperty].comments.length > 0) ? concat([ shouldPrintHardLineAfterStartInControlStructure(path) ? node.kind === "switch" ? " " : "" : hardline, printedBody, ]) : "", ]) ), node.kind === "if" && bodyProperty === "body" ? "" : concat([ shouldPrintHardLineBeforeEndInControlStructure(path) ? hardline : "", node.shortForm ? concat(["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(concat([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 ( comments.hasLeadingOwnLineComment(options.originalText, rightNode, options) ) { return indent(concat([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(concat([line, ref, printedRight]))); } return concat([" ", ref, printedRight]); } function needsHardlineAfterDanglingComment(node) { if (!node.comments) { return false; } const lastDanglingComment = getLast( node.comments.filter((comment) => !comment.leading && !comment.trailing) ); return lastDanglingComment && !comments.isBlockComment(lastDanglingComment); } function stringHasNewLines(node) { return node.raw.includes("\n"); } function isStringOnItsOwnLine(node, text, options) { return ( (node.kind === "string" || (node.kind === "encapsed" && (node.type === "string" || node.type === "shell"))) && stringHasNewLines(node) && !hasNewline(text, options.locStart(node), { backwards: true }) ); } function printNode(path, options, print) { const node = path.getValue(); switch (node.kind) { case "program": { return group( concat([ printLines(path, options, print), comments.printDanglingComments( path, options, /* sameIndent */ true, (c) => !c.printed ), ]) ); } case "expressionstatement": return path.call(print, "expression"); case "block": return concat([ printLines(path, options, print), comments.printDanglingComments(path, options, true), ]); case "declare": { const printDeclareArguments = (path) => { return join( ", ", path.map((directive) => concat([print(directive)]), "directives") ); }; if (["block", "short"].includes(node.mode)) { return concat([ "declare(", printDeclareArguments(path), ")", node.mode === "block" ? " {" : ":", node.children.length > 0 ? indent(concat([hardline, printLines(path, options, print)])) : "", comments.printDanglingComments(path, options), hardline, node.mode === "block" ? "}" : "enddeclare;", ]); } const nextNode = getNextNode(path, node); return concat([ "declare(", printDeclareArguments(path), ")", nextNode && nextNode.kind === "inline" ? "" : ";", ]); } case "declaredirective": return concat([path.call(print, "key"), "=", path.call(print, "value")]); case "namespace": return concat([ "namespace ", node.name && typeof node.name === "string" ? concat([node.name, node.withBrackets ? " " : ""]) : "", node.withBrackets ? "{" : ";", hasDanglingComments(node) ? concat([" ", comments.printDanglingComments(path, options, true)]) : "", node.children.length > 0 ? node.withBrackets ? indent(concat([hardline, printLines(path, options, print)])) : concat([ node.children[0].kind === "inline" ? "" : concat([ hardline, isNextLineEmptyAfterNamespace( options.originalText, node, options.locStart ) ? hardline : "", ]), printLines(path, options, print), ]) : "", node.withBrackets ? concat([hardline, "}"]) : "", ]); case "usegroup": return group( concat([ "use ", node.type ? concat([node.type, " "]) : "", indent( concat([ node.name ? concat([ maybeStripLeadingSlashFromUse(node.name), "\\{", softline, ]) : "", join( concat([",", line]), path.map((item) => concat([print(item)]), "items") ), ]) ), node.name ? concat([ ifBreak(shouldPrintComma(options, "7.2") ? "," : ""), softline, "}", ]) : "", ]) ); case "useitem": return concat([ node.type ? concat([node.type, " "]) : "", maybeStripLeadingSlashFromUse(node.name), hasDanglingComments(node) ? concat([" ", comments.printDanglingComments(path, options, true)]) : "", node.alias ? concat([" as ", path.call(print, "alias")]) : "", ]); case "class": case "interface": case "trait": return printClass(path, options, print); case "traitprecedence": return concat([ path.call(print, "trait"), "::", path.call(print, "method"), " insteadof ", join(", ", path.map(print, "instead")), ]); case "traitalias": return concat([ node.trait ? concat([path.call(print, "trait"), "::"]) : "", node.method ? path.call(print, "method") : "", " as ", join(" ", [ ...(node.visibility ? [node.visibility] : []), ...(node.as ? [path.call(print, "as")] : []), ]), ]); case "traituse": return group( concat([ "use ", indent(group(join(concat([",", line]), path.map(print, "traits")))), node.adaptations ? concat([ " {", node.adaptations.length > 0 ? concat([ indent( concat([ hardline, printStatements(path, options, print, "adaptations"), ]) ), hardline, ]) : hasDanglingComments(node) ? concat([ line, comments.printDanglingComments(path, options, true), line, ]) : "", "}", ]) : "", ]) ); case "function": case "closure": case "method": return printFunction(path, options, print); case "arrowfunc": return concat([ node.isStatic ? "static " : "", "fn", printArgumentsList(path, options, print), node.type ? concat([": ", node.nullable ? "?" : "", path.call(print, "type")]) : "", " => ", path.call(print, "body"), ]); case "parameter": { const name = concat([ node.nullable ? "?" : "", node.type ? concat([path.call(print, "type"), " "]) : "", node.byref ? "&" : "", node.variadic ? "..." : "", "$", path.call(print, "name"), ]); if (node.value) { return group( concat([ name, // see handleFunctionParameter() in ./comments.js - 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) ? " " : "", comments.printDanglingComments(path, options, true), concat([ " =", printAssignmentRight( node.name, node.value, path.call(print, "value"), false, options ), ]), ]) ); } return name; } case "variadic": return concat(["...", path.call(print, "what")]); case "property": return group( concat([ node.type ? concat([node.nullable ? "?" : "", path.call(print, "type"), " "]) : "", "$", path.call(print, "name"), node.value ? concat([ " =", printAssignmentRight( node.name, node.value, path.call(print, "value"), false, options ), ]) : "", ]) ); case "propertystatement": { const printed = path.map((childPath) => { return print(childPath); }, "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( concat([ hasVisibility ? concat([node.visibility === null ? "var" : node.visibility, ""]) : "", node.isStatic ? concat([hasVisibility ? " " : "", "static"]) : "", firstProperty ? concat([" ", firstProperty]) : "", indent( concat( printed .slice(1) .map((p) => concat([",", hasValue ? hardline : line, p])) ) ), ]) ); } case "if": { const parts = []; const body = printBodyControlStructure(path, options, print, "body"); const opening = group( concat([ "if (", group( concat([ indent(concat([softline, path.call(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 && !comments.isBlockComment(comment) )) || needsHardlineAfterDanglingComment(node); const elseOnSameLine = !commentOnOwnLine; parts.push(elseOnSameLine ? "" : hardline); if (hasDanglingComments(node)) { parts.push(