"use strict";

const {
  breakParent,
  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,
  isPreviousLineEmpty,
} = 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":
    case "per-cs":
    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 parts;
}

function printPropertyLookup(path, options, print, nullsafe = false) {
  return [nullsafe ? "?" : "", "->", path.call(print, "offset")];
}

function printNullsafePropertyLookup(path, options, print) {
  return printPropertyLookup(path, options, print, true);
}

function printStaticLookup(path, options, print) {
  const node = path.getValue();
  const needCurly = !["variable", "identifier"].includes(node.offset.kind);

  return [
    "::",
    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 [
    "[",
    node.offset
      ? group([
          indent([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: [
          comments.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: 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", "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 &&
      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.getParentNode().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) => 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 = [
    printGroup(groups[0]),
    shouldMerge ? 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 [
    // 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 [
      "(",
      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 parts;
  }, argumentsKey);

  const node = path.getValue();
  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;
    let i = 0;

    path.each((argPath) => {
      if (shouldGroupFirst && i === 0) {
        printedExpanded = [
          argPath.call((p) => print(p, { expandFirstArg: true })),
          printedArguments.length > 1 ? "," : "",
          hasEmptyLineFollowingFirstArg ? hardline : line,
          hasEmptyLineFollowingFirstArg ? hardline : "",
          printedArguments.slice(1),
        ];
      }

      if (shouldGroupLast && i === args.length - 1) {
        printedExpanded = [
          ...printedArguments.slice(0, -1),
          argPath.call((p) => print(p, { expandLastArg: true })),
        ];
      }

      i++;
    }, 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.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
      ? [node.type, " ", path.call(print, "right")]
      : [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, () => 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 "nullsafepropertylookup":
      return printNullsafePropertyLookup(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(separatorParts);
    printedElements.push(group(print(childPath)));

    separatorParts = [",", line];

    if (
      childPath.getValue() &&
      isNextLineEmpty(options.originalText, childPath.getValue(), options)
    ) {
      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 = 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 = [
      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
          ? [
              isFirstNode && node.kind !== "namespace" && !isBlockNestedNode
                ? "<?php"
                : "",
              node.kind === "namespace" || !isBlockNestedNode ? hardline : "",
              comments.printComments(childNode.leadingComments, options),
              hardline,
              "?>",
            ]
          : isProgramLikeNode(node) && isFirstNode && node.kind !== "namespace"
          ? ""
          : [beforeCloseTagInlineNode, "?>"];

      const nextV = path.getNode(index + 1);
      const skipLastComment = nextV && nextV.children && nextV.children.length;

      const afterInline =
        childNode.comments && childNode.comments.length
          ? [
              openTag,
              hardline,
              skipLastComment
                ? comments.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 ? 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(),
              options.locEnd(lastNode),
              options.locEnd(node)
            )
              ? !(
                  lastNode.kind === "inline" &&
                  lastNode.comments &&
                  lastNode.comments.length
                )
                ? hardline
                : ""
              : " ",
            isNextLineEmpty(options.originalText, lastNode, options)
              ? hardline
              : "",
          ]
        : node.comments
        ? hardline
        : "";

      parts.push(lineSuffix([beforeCloseTag, "?>"]));
    }

    return parts;
  }

  return wrappedParts;
}

function printStatements(path, options, print, childrenAttribute) {
  return 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 parts;
  }, childrenAttribute);
}

function printClassPart(
  path,
  options,
  print,
  part = "extends",
  beforePart = " ",
  afterPart = " "
) {
  const node = path.getValue();
  const printedBeforePart = hasDanglingComments(node[part])
    ? [
        hardline,
        path.call(
          (partPath) => comments.printDanglingComments(partPath, options, true),
          part
        ),
        hardline,
      ]
    : beforePart;
  const printedPartItems = Array.isArray(node[part])
    ? group(
        join(
          ",",
          path.map((itemPartPath) => {
            const printedPart = print(itemPartPath);
            // Check if any of the implements nodes have comments
            return hasDanglingComments(itemPartPath.getValue())
              ? [
                  hardline,
                  comments.printDanglingComments(itemPartPath, options, true),
                  hardline,
                  printedPart,
                ]
              : [afterPart, printedPart];
          }, part)
        )
      )
    : [afterPart, path.call(print, part)];

  return indent([
    printedBeforePart,
    part,
    willBreak(printedBeforePart) ? indent(printedPartItems) : printedPartItems,
  ]);
}

function printAttrs(path, options, print, { inline = false } = {}) {
  const allAttrs = [];
  if (!path.getValue().attrGroups) {
    return [];
  }
  path.each((agPath) => {
    const attrGroup = ["#["];
    if (!inline && allAttrs.length > 0) {
      allAttrs.push(hardline);
    }
    attrGroup.push(softline);
    agPath.each((attrPath) => {
      const attrNode = attrPath.getValue();
      if (attrGroup.length > 2) {
        attrGroup.push(",", line);
      }
      const attrStmt = [attrNode.name];
      if (attrNode.args.length > 0) {
        attrStmt.push(printArgumentsList(attrPath, 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.getValue();
  const isAnonymousClass = node.kind === "class" && node.isAnonymous;
  const attrs = printAttrs(path, options, print, { inline: isAnonymousClass });
  const declaration = isAnonymousClass ? [] : [...attrs];

  if (node.isReadonly) {
    declaration.push("readonly ");
  }

  if (node.isFinal) {
    declaration.push("final ");
  }

  if (node.isAbstract) {
    declaration.push("abstract ");
  }

  // `new` print `class` keyword with arguments
  declaration.push(isAnonymousClass ? "" : node.kind);

  if (node.name) {
    declaration.push(" ", path.call(print, "name"));
  }

  if (node.kind === "enum" && node.valueType) {
    declaration.push(": ", path.call(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 printedDeclaration = group([
    group(declaration),
    shouldPrintHardlineForOpenBrace(options)
      ? isAnonymousClass
        ? line
        : hardline
      : " ",
  ]);

  const hasEmptyClassBody =
    node.body && node.body.length === 0 && !hasDanglingComments(node);
  const printedBody = [
    "{",
    indent([
      hasEmptyClassBody ? "" : hardline,
      printStatements(path, options, print, "body"),
    ]),
    comments.printDanglingComments(path, options, true),
    isAnonymousClass && hasEmptyClassBody ? softline : hardline,
    "}",
  ];

  return [printedDeclaration, printedBody];
}

function printFunction(path, options, print) {
  const node = path.getValue();
  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(path.call(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(
              (typePath) =>
                comments.printDanglingComments(typePath, options, true),
              "type"
            ),
            " ",
          ]
        : "",
      node.nullable ? "?" : "",
      path.call(print, "type"),
    ]);
  }

  const printedDeclaration = declaration;

  if (!node.body) {
    return [...declAttrs, printedDeclaration];
  }

  const isClosure = node.kind === "closure";
  const printedBody = [
    "{",
    indent([hasEmptyBody(path) ? "" : hardline, path.call(print, "body")]),
    isClosure && hasEmptyBody(path) ? "" : hardline,
    "}",
  ];

  if (isClosure) {
    return [...declAttrs, printedDeclaration, " ", printedBody];
  }

  if (node.arguments.length === 0) {
    return [
      ...declAttrs,
      printedDeclaration,
      shouldPrintHardlineForOpenBrace(options) ? hardline : " ",
      printedBody,
    ];
  }

  const willBreakDeclaration = declaration.some(willBreak);

  if (willBreakDeclaration) {
    return [printedDeclaration, " ", printedBody];
  }

  return [
    ...declAttrs,
    conditionalGroup([
      [
        printedDeclaration,
        shouldPrintHardlineForOpenBrace(options) ? hardline : " ",
        printedBody,
      ],
      [printedDeclaration, " ", printedBody],
    ]),
  ];
}

function printBodyControlStructure(
  path,
  options,
  print,
  bodyProperty = "body"
) {
  const node = path.getValue();

  if (!node[bodyProperty]) {
    return ";";
  }

  const printedBody = path.call(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 (
    comments.hasLeadingOwnLineComment(options.originalText, rightNode, options)
  ) {
    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 && !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 printComposedTypes(path, print, glue) {
  return group(
    path.map(
      (uPath, i) => (i === 0 ? [path.call(print)] : [glue, path.call(print)]),
      "types"
    )
  );
}

function printNode(path, options, print) {
  const node = path.getValue();

  switch (node.kind) {
    case "program": {
      return group([
        printLines(path, options, print),
        comments.printDanglingComments(
          path,
          options,
          /* sameIndent */ true,
          (c) => !c.printed
        ),
      ]);
    }
    case "expressionstatement":
      return path.call(print, "expression");
    case "block":
      return [
        printLines(path, options, print),
        comments.printDanglingComments(path, options, true),
      ];
    case "declare": {
      const printDeclareArguments = (path) => {
        return join(
          ", ",
          path.map((directive) => print(directive), "directives")
        );
      };

      if (["block", "short"].includes(node.mode)) {
        return [
          "declare(",
          printDeclareArguments(path),
          ")",
          node.mode === "block" ? " {" : ":",
          node.children.length > 0
            ? indent([hardline, printLines(path, options, print)])
            : "",
          comments.printDanglingComments(path, options),
          hardline,
          node.mode === "block" ? "}" : "enddeclare;",
        ];
      }

      const nextNode = getNextNode(path, node);

      return [
        "declare(",
        printDeclareArguments(path),
        ")",
        nextNode && nextNode.kind === "inline" ? "" : ";",
      ];
    }
    case "declaredirective":
      return [path.call(print, "key"), "=", path.call(print, "value")];
    case "namespace":
      return [
        "namespace ",
        node.name && typeof node.name === "string"
          ? [node.name, node.withBrackets ? " " : ""]
          : "",
        node.withBrackets ? "{" : ";",
        hasDanglingComments(node)
          ? [" ", comments.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,
                        options.locStart
                      )
                        ? 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((item) => print(item), "items")
          ),
        ]),
        node.name
          ? [
              ifBreak(shouldPrintComma(options, "7.2") ? "," : ""),
              softline,
              "}",
            ]
          : "",
      ]);
    case "useitem":
      return [
        node.type ? [node.type, " "] : "",
        maybeStripLeadingSlashFromUse(node.name),
        hasDanglingComments(node)
          ? [" ", comments.printDanglingComments(path, options, true)]
          : "",
        node.alias ? [" as ", path.call(print, "alias")] : "",
      ];
    case "class":
    case "enum":
    case "interface":
    case "trait":
      return printClass(path, options, print);
    case "traitprecedence":
      return [
        path.call(print, "trait"),
        "::",
        path.call(print, "method"),
        " insteadof ",
        join(", ", path.map(print, "instead")),
      ];
    case "traitalias":
      return [
        node.trait ? [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([
        "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,
                    comments.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 ? "?" : "", path.call(print, "type")]
          : "",
        " => ",
        path.call(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 ? [path.call(print, "type"), " "] : "",
        node.byref ? "&" : "",
        node.variadic ? "..." : "",
        "$",
        path.call(print, "name"),
      ];

      if (node.value) {
        return group([
          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),
          " =",
          printAssignmentRight(
            node.name,
            node.value,
            path.call(print, "value"),
            false,
            options
          ),
        ]);
      }

      return name;
    }
    case "variadic":
      return ["...", path.call(print, "what")];
    case "property":
      return group([
        node.readonly ? "readonly " : "",
        node.type
          ? [node.nullable ? "?" : "", path.call(print, "type"), " "]
          : "",
        "$",
        path.call(print, "name"),
        node.value
          ? [
              " =",
              printAssignmentRight(
                node.name,
                node.value,
                path.call(print, "value"),
                false,
                options
              ),
            ]
          : "",
      ]);
    case "propertystatement": {
      const attrs = [];
      path.map(
        (propPath) => attrs.push(...printAttrs(propPath, options, print)),
        "properties"
      );
      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([
        ...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, 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(
            isNextLineEmpty(options.originalText, node.body, options)
              ? hardline
              : "",
            comments.printDanglingComments(path, options, true),
            commentOnOwnLine ? hardline : " "
          );
        }

        parts.push(
          "else",
          group(
            node.alternate.kind === "if"
              ? path.call(print, "alternate")
              : printBodyControlStructure(path, options, print, "alternate")
          )
        );
      } else {
        parts.push(node.body ? (node.shortForm ? "endif;" : "}") : "");
      }

      return parts;
    }
    case "do":
      return [
        "do",
        printBodyControlStructure(path, options, print, "body"),
        " while (",
        group([indent([softline, path.call(print, "test")]), softline]),
        ")",
      ];
    case "while":
    case "switch":
      return group([
        node.kind,
        " (",
        group([indent([softline, path.call(print, "test")]), softline]),
        ")",
        printBodyControlStructure(path, options, print, "body"),
      ]);
    case "for": {
      const body = printBodyControlStructure(path, options, print, "body");

      // We want to keep dangling comments above the loop to stay consistent.
      // Any comment positioned between the for statement and the parentheses
      // is going to be printed before the statement.
      const dangling = comments.printDanglingComments(
        path,
        options,
        /* sameLine */ true
      );
      const printedComments = dangling ? [dangling, softline] : "";

      if (!node.init.length && !node.test.length && !node.increment.length) {
        return [printedComments, group(["for (;;)", body])];
      }

      return [
        printedComments,
        group([
          "for (",
          group([
            indent([
              softline,
              group(join([",", line], path.map(print, "init"))),
              ";",
              line,
              group(join([",", line], path.map(print, "test"))),
              ";",
              line,
              group(join([",", line], path.map(print, "increment"))),
            ]),
            softline,
          ]),
          ")",
          body,
        ]),
      ];
    }
    case "foreach": {
      const body = printBodyControlStructure(path, options, print, "body");

      // We want to keep dangling comments above the loop to stay consistent.
      // Any comment positioned between the for statement and the parentheses
      // is going to be printed before the statement.
      const dangling = comments.printDanglingComments(
        path,
        options,
        /* sameLine */ true
      );
      const printedComments = dangling ? [dangling, softline] : "";

      return [
        printedComments,
        group([
          "foreach (",
          group([
            indent([
              softline,
              path.call(print, "source"),
              line,
              "as ",
              group(
                node.key
                  ? indent(
                      join(
                        [" =>", line],
                        [path.call(print, "key"), path.call(print, "value")]
                      )
                    )
                  : path.call(print, "value")
              ),
            ]),
            softline,
          ]),
          ")",
          body,
        ]),
      ];
    }
    case "try": {
      const parts = [];

      parts.push(
        "try",
        printBodyControlStructure(path, options, print, "body")
      );

      if (node.catches) {
        parts.push(path.map(print, "catches"));
      }

      if (node.always) {
        parts.push(
          " finally",
          printBodyControlStructure(path, options, print, "always")
        );
      }

      return parts;
    }
    case "catch": {
      return [
        " catch",
        node.what
          ? [
              " (",
              join(" | ", path.map(print, "what")),
              node.variable ? [" ", path.call(print, "variable")] : "",
              ")",
            ]
          : "",
        printBodyControlStructure(path, options, print, "body"),
      ];
    }
    case "case":
      return [
        node.test
          ? [
              "case ",
              node.test.comments
                ? indent(path.call(print, "test"))
                : path.call(print, "test"),
              ":",
            ]
          : "default:",
        node.body
          ? node.body.children && node.body.children.length
            ? indent([
                isFirstChildrenInlineNode(path) ? "" : hardline,
                path.call(print, "body"),
              ])
            : ""
          : "",
      ];
    case "break":
    case "continue":
      if (node.level) {
        if (node.level.kind === "number" && node.level.value !== "1") {
          return [`${node.kind} `, path.call(print, "level")];
        }

        return node.kind;
      }

      return node.kind;
    case "call": {
      // Multiline strings as single arguments
      if (
        node.arguments.length === 1 &&
        isStringOnItsOwnLine(node.arguments[0], options.originalText, options)
      ) {
        return [
          path.call(print, "what"),
          "(",
          join(", ", path.map(print, "arguments")),
          ")",
        ];
      }

      // chain: Call (*LookupNode (Call (*LookupNode (...))))
      if (isLookupNode(node.what)) {
        return printMemberChain(path, options, print);
      }

      return [
        path.call(print, "what"),
        printArgumentsList(path, options, print),
      ];
    }
    case "new": {
      const isAnonymousClassNode =
        node.what && node.what.kind === "class" && node.what.isAnonymous;

      // Multiline strings as single arguments
      if (
        !isAnonymousClassNode &&
        node.arguments.length === 1 &&
        isStringOnItsOwnLine(node.arguments[0], options.originalText, options)
      ) {
        return [
          "new ",
          ...path.call(printAttrs, "what"),
          path.call(print, "what"),
          "(",
          join(", ", path.map(print, "arguments")),
          ")",
        ];
      }

      const parts = [];

      parts.push("new ");

      if (isAnonymousClassNode) {
        parts.push(
          node.what.leadingComments &&
            node.what.leadingComments[0].kind === "commentblock"
            ? [comments.printComments(node.what.leadingComments, options), " "]
            : "",
          ...path.call(
            (pa) => printAttrs(pa, options, print, { inline: true }),
            "what"
          ),
          "class",
          node.arguments.length > 0
            ? [" ", printArgumentsList(path, options, print)]
            : "",
          group(path.call(print, "what"))
        );
      } else {
        const isExpression = ["call", "offsetlookup"].includes(node.what.kind);
        const printed = [
          isExpression ? "(" : "",
          path.call(print, "what"),
          isExpression ? ")" : "",
          printArgumentsList(path, options, print),
        ];

        parts.push(hasLeadingComment(node.what) ? indent(printed) : printed);
      }

      return parts;
    }
    case "clone":
      return [
        "clone ",
        node.what.comments
          ? indent(path.call(print, "what"))
          : path.call(print, "what"),
      ];
    case "propertylookup":
    case "nullsafepropertylookup":
    case "staticlookup":
    case "offsetlookup": {
      const parent = path.getParentNode();

      let firstNonMemberParent;
      let i = 0;

      do {
        firstNonMemberParent = path.getParentNode(i);
        i++;
      } while (firstNonMemberParent && isLookupNode(firstNonMemberParent));

      const hasEncapsedAncestor = getAncestorNode(path, "encapsed");
      const shouldInline =
        hasEncapsedAncestor ||
        (firstNonMemberParent &&
          (firstNonMemberParent.kind === "new" ||
            (firstNonMemberParent.kind === "assign" &&
              firstNonMemberParent.left.kind !== "variable"))) ||
        node.kind === "offsetlookup" ||
        ((isReferenceLikeNode(node.what) || node.what.kind === "variable") &&
          ["identifier", "variable", "encapsedpart"].includes(
            node.offset.kind
          ) &&
          parent &&
          !isLookupNode(parent));

      return [
        path.call(print, "what"),
        shouldInline
          ? printLookupNodes(path, options, print)
          : group(indent([softline, printLookupNodes(path, options, print)])),
      ];
    }
    case "exit":
      return group([
        node.useDie ? "die" : "exit",
        "(",
        node.expression
          ? isStringOnItsOwnLine(node.expression, options.originalText, options)
            ? path.call(print, "expression")
            : [indent([softline, path.call(print, "expression")]), softline]
          : comments.printDanglingComments(path, options),
        ")",
      ]);
    case "global":
      return group([
        "global ",
        indent(join([",", line], path.map(print, "items"))),
      ]);
    case "include":
      return [
        node.require ? "require" : "include",
        node.once ? "_once" : "",
        " ",
        node.target.comments
          ? indent(path.call(print, "target"))
          : path.call(print, "target"),
      ];
    case "label":
      return [path.call(print, "name"), ":"];
    case "goto":
      return ["goto ", path.call(print, "label")];
    case "throw":
      return [
        "throw ",
        node.what.comments
          ? indent(path.call(print, "what"))
          : path.call(print, "what"),
      ];
    case "silent":
      return ["@", path.call(print, "expr")];
    case "halt":
      return [
        hasDanglingComments(node)
          ? [
              comments.printDanglingComments(
                path,
                options,
                /* sameIndent */ true
              ),
              hardline,
            ]
          : "",
        "__halt_compiler();",
        node.after,
      ];
    case "eval":
      return group([
        "eval(",
        isStringOnItsOwnLine(node.source, options.originalText, options)
          ? path.call(print, "source")
          : [indent([softline, path.call(print, "source")]), softline],
        ")",
      ]);
    case "echo": {
      const printedArguments = path.map((childPath) => {
        return print(childPath);
      }, "expressions");

      let firstVariable;

      if (printedArguments.length === 1 && !node.expressions[0].comments) {
        [firstVariable] = printedArguments;
      } else if (printedArguments.length > 0) {
        firstVariable =
          isDocNode(node.expressions[0]) || node.expressions[0].comments
            ? indent(printedArguments[0])
            : dedent(printedArguments[0]);
      }

      return group([
        node.shortForm ? "" : "echo ",
        firstVariable ? firstVariable : "",
        indent(printedArguments.slice(1).map((p) => [",", line, p])),
      ]);
    }
    case "print": {
      return [
        "print ",
        node.expression.comments
          ? indent(path.call(print, "expression"))
          : path.call(print, "expression"),
      ];
    }
    case "return": {
      const parts = [];

      parts.push("return");

      if (node.expr) {
        const printedExpr = path.call(print, "expr");

        parts.push(" ", node.expr.comments ? indent(printedExpr) : printedExpr);
      }

      if (hasDanglingComments(node)) {
        parts.push(
          " ",
          comments.printDanglingComments(path, options, /* sameIndent */ true)
        );
      }

      return parts;
    }
    case "isset":
    case "unset":
      return group([
        node.kind,
        printArgumentsList(path, options, print, "variables"),
      ]);
    case "empty":
      return group([
        "empty(",
        indent([softline, path.call(print, "expression")]),
        softline,
        ")",
      ]);
    case "variable": {
      const parent = path.getParentNode();
      const parentParent = path.getParentNode(1);
      const ampersand = parent.kind === "assign" ? "" : node.byref ? "&" : "";
      const dollar =
        (parent.kind === "encapsedpart" &&
          parent.syntax === "simple" &&
          parent.curly) ||
        (parentParent &&
          parent.kind === "offsetlookup" &&
          parentParent.kind === "encapsedpart" &&
          parentParent.syntax === "simple" &&
          parentParent.curly)
          ? ""
          : "$";
      const openCurly = node.curly ? "{" : "";
      const closeCurly = node.curly ? "}" : "";

      return [
        ampersand,
        dollar,
        openCurly,
        path.call(print, "name"),
        closeCurly,
      ];
    }
    case "constantstatement":
    case "classconstant": {
      const attrs = printAttrs(path, options, print);
      const printed = path.map((childPath) => print(childPath), "constants");

      let firstVariable;

      if (printed.length === 1 && !node.constants[0].comments) {
        [firstVariable] = printed;
      } else if (printed.length > 0) {
        // Indent first item
        firstVariable = indent(printed[0]);
      }

      return group([
        ...attrs,
        node.final ? "final " : "",
        node.visibility ? [node.visibility, " "] : "",
        "const",
        firstVariable ? [" ", firstVariable] : "",
        indent(printed.slice(1).map((p) => [",", hardline, p])),
      ]);
    }
    case "constant":
      return printAssignment(
        node.name,
        path.call(print, "name"),
        " =",
        node.value,
        path.call(print, "value"),
        false,
        options
      );
    case "static": {
      const printed = path.map((childPath) => {
        return print(childPath);
      }, "variables");

      const hasValue = node.variables.some((item) => item.defaultValue);

      let firstVariable;

      if (printed.length === 1 && !node.variables[0].comments) {
        [firstVariable] = printed;
      } else if (printed.length > 0) {
        // Indent first item
        firstVariable = indent(printed[0]);
      }

      return group([
        "static",
        firstVariable ? [" ", firstVariable] : "",
        indent(
          printed.slice(1).map((p) => [",", hasValue ? hardline : line, p])
        ),
      ]);
    }
    case "staticvariable": {
      return printAssignment(
        node.variable,
        path.call(print, "variable"),
        " =",
        node.defaultValue,
        path.call(print, "defaultValue"),
        false,
        options
      );
    }
    case "list":
    case "array": {
      const useShortForm =
        (node.kind === "array" && isMinVersion(options.phpVersion, "5.4")) ||
        (node.kind === "list" &&
          (node.shortForm || isMinVersion(options.phpVersion, "7.1")));
      const open = useShortForm ? "[" : [node.kind, "("];
      const close = useShortForm ? "]" : ")";

      if (node.items.length === 0) {
        if (!hasDanglingComments(node)) {
          return [open, close];
        }

        return group([
          open,
          comments.printDanglingComments(path, options),
          softline,
          close,
        ]);
      }

      const lastElem = getLast(node.items);

      // PHP allows you to have empty elements in an array which
      // changes its length based on the number of commas. The algorithm
      // is that if the last argument is null, we need to force insert
      // a comma to ensure PHP recognizes it.
      //   [,] === $arr;
      //   [1,] === $arr;
      //   [1,,] === $arr;
      //
      // Note that getLast returns null if the array is empty, but
      // we already check for an empty array just above so we are safe
      const needsForcedTrailingComma = lastElem && lastElem.kind === "noop";

      const [firstProperty] = node.items
        .filter((node) => node.kind !== "noop")
        .sort((a, b) => options.locStart(a) - options.locStart(b));
      const isAssociative = !!(firstProperty && firstProperty.key);
      const shouldBreak =
        isAssociative &&
        firstProperty &&
        hasNewlineInRange(
          options.originalText,
          options.locStart(node),
          options.locStart(firstProperty)
        );

      return group(
        [
          open,
          indent([softline, printArrayItems(path, options, print)]),
          needsForcedTrailingComma ? "," : "",
          ifBreak(
            !needsForcedTrailingComma && shouldPrintComma(options, "5.0")
              ? [
                  lastElem && shouldPrintHardlineBeforeTrailingComma(lastElem)
                    ? hardline
                    : "",
                  ",",
                ]
              : ""
          ),
          comments.printDanglingComments(path, options, true),
          softline,
          close,
        ],
        { shouldBreak }
      );
    }
    case "entry": {
      const ref = node.byRef ? "&" : "";
      const unpack = node.unpack ? "..." : "";
      return node.key
        ? printAssignment(
            node.key,
            path.call(print, "key"),
            " =>",
            node.value,
            path.call(print, "value"),
            ref,
            options
          )
        : [ref, unpack, path.call(print, "value")];
    }
    case "yield": {
      const printedKeyAndValue = [
        node.key ? [path.call(print, "key"), " => "] : "",
        path.call(print, "value"),
      ];

      return [
        "yield",
        node.key || node.value ? " " : "",
        node.value && node.value.comments
          ? indent(printedKeyAndValue)
          : printedKeyAndValue,
      ];
    }
    case "yieldfrom":
      return [
        "yield from ",
        node.value.comments
          ? indent(path.call(print, "value"))
          : path.call(print, "value"),
      ];
    case "unary":
      return [node.type, path.call(print, "what")];
    case "pre":
      return [node.type + node.type, path.call(print, "what")];
    case "post":
      return [path.call(print, "what"), node.type + node.type];
    case "cast":
      return [
        "(",
        node.type,
        ") ",
        node.expr.comments
          ? indent(path.call(print, "expr"))
          : path.call(print, "expr"),
      ];
    case "assignref":
    case "assign": {
      const hasRef = node.kind === "assignref";

      return printAssignment(
        node.left,
        path.call(print, "left"),
        [" ", hasRef ? "=" : node.operator],
        node.right,
        path.call(print, "right"),
        hasRef,
        options
      );
    }
    case "bin": {
      const parent = path.getParentNode();
      const parentParent = path.getParentNode(1);
      const isInsideParenthesis =
        node !== parent.body &&
        (parent.kind === "if" ||
          parent.kind === "while" ||
          parent.kind === "switch" ||
          parent.kind === "do");

      const parts = printBinaryExpression(
        path,
        print,
        options,
        /* isNested */ false,
        isInsideParenthesis
      );

      //   if (
      //     $this->hasPlugin('dynamicImports') && $this->lookahead()->type === tt->parenLeft
      //   ) {
      //
      // looks super weird, we want to break the children if the parent breaks
      //
      //   if (
      //     $this->hasPlugin('dynamicImports') &&
      //     $this->lookahead()->type === tt->parenLeft
      //   ) {
      if (isInsideParenthesis) {
        return parts;
      }

      // Break between the parens in unaries or in a member expression, i.e.
      //
      //   (
      //     a &&
      //     b &&
      //     c
      //   )->call()
      if (
        parent.kind === "unary" ||
        (isLookupNode(parent) && parent.kind !== "offsetlookup")
      ) {
        return group([indent([softline, ...parts]), softline]);
      }

      // Avoid indenting sub-expressions in some cases where the first sub-expression is already
      // indented accordingly. We should indent sub-expressions where the first case isn't indented.
      const shouldNotIndent =
        (node !== parent.body && parent.kind === "for") ||
        (parent.kind === "retif" &&
          parentParent &&
          parentParent.kind !== "return");

      const shouldIndentIfInlining = [
        "assign",
        "property",
        "constant",
        "staticvariable",
        "entry",
      ].includes(parent.kind);

      const samePrecedenceSubExpression =
        node.left.kind === "bin" && shouldFlatten(node.type, node.left.type);

      if (
        shouldNotIndent ||
        (shouldInlineLogicalExpression(node) && !samePrecedenceSubExpression) ||
        (!shouldInlineLogicalExpression(node) && shouldIndentIfInlining)
      ) {
        return group(parts);
      }

      const rest = parts.slice(1);

      return group([
        // Don't include the initial expression in the indentation
        // level. The first item is guaranteed to be the first
        // left-most expression.
        parts.length > 0 ? parts[0] : "",
        indent(rest),
      ]);
    }
    case "retif": {
      const parts = [];
      const parent = path.getParentNode();

      // Find the outermost non-retif parent, and the outermost retif parent.
      let currentParent;
      let i = 0;

      do {
        currentParent = path.getParentNode(i);
        i++;
      } while (currentParent && currentParent.kind === "retif");
      const firstNonRetifParent = currentParent || parent;

      const printedFalseExpr =
        node.falseExpr.kind === "bin"
          ? indent(path.call(print, "falseExpr"))
          : path.call(print, "falseExpr");
      const part = [
        node.trueExpr ? line : " ",
        "?",
        node.trueExpr
          ? [
              " ",
              node.trueExpr.kind === "bin"
                ? indent(path.call(print, "trueExpr"))
                : path.call(print, "trueExpr"),
              line,
            ]
          : "",
        ":",
        node.trueExpr
          ? [" ", printedFalseExpr]
          : [
              shouldInlineRetifFalseExpression(node.falseExpr) ? " " : line,
              printedFalseExpr,
            ],
      ];

      parts.push(part);

      // We want a whole chain of retif to all break if any of them break.
      const maybeGroup = (doc) =>
        parent === firstNonRetifParent ? group(doc) : doc;

      // Break the closing parens to keep the chain right after it:
      // ($a
      //   ? $b
      //   : $c
      // )->call()
      const parentParent = path.getParentNode(1);
      const pureParent =
        parent.kind === "cast" && parentParent ? parentParent : parent;
      const breakLookupNodes = [
        "propertylookup",
        "nullsafepropertylookup",
        "staticlookup",
      ];
      const breakClosingParens = breakLookupNodes.includes(pureParent.kind);

      const printedTest = path.call(print, "test");

      if (!node.trueExpr) {
        const printed = [
          printedTest,
          pureParent.kind === "bin" ||
          ["print", "echo", "return", "include"].includes(
            firstNonRetifParent.kind
          )
            ? indent(parts)
            : parts,
        ];

        // Break between the parens in unaries or in a lookup nodes, i.e.
        //
        //   (
        //     a ?:
        //     b ?:
        //     c
        //   )->call()
        if (
          (pureParent.kind === "call" && pureParent.what === node) ||
          pureParent.kind === "unary" ||
          (isLookupNode(pureParent) && pureParent.kind !== "offsetlookup")
        ) {
          return group([indent([softline, printed]), softline]);
        }

        return maybeGroup(printed);
      }

      return maybeGroup([
        node.test.kind === "retif" ? indent(printedTest) : printedTest,
        indent(parts),
        breakClosingParens ? softline : "",
      ]);
    }
    case "boolean":
      return node.value ? "true" : "false";
    case "number":
      return printNumber(node.value);
    case "string": {
      const parent = path.getParentNode();

      if (parent.kind === "encapsedpart") {
        const parentParent = path.getParentNode(1);
        let closingTagIndentation = 0;
        const flexible = isMinVersion(options.phpVersion, "7.3");
        let linebreak = literalline;
        if (parentParent.type === "heredoc") {
          linebreak = flexible ? hardline : literalline;
          const lines = parentParent.raw.split("\n");
          closingTagIndentation = lines[lines.length - 1].search(/\S/);
          if (closingTagIndentation === -1) {
            closingTagIndentation = lines[lines.length - 2].search(/\S/);
          }
        }
        return join(
          linebreak,
          node.raw
            .split("\n")
            .map((s, i) =>
              i > 0 || node.loc.start.column === 0
                ? s.substring(closingTagIndentation)
                : s
            )
        );
      }

      const quote = useDoubleQuote(node, options) ? '"' : "'";

      let stringValue = node.raw;

      if (node.raw[0] === "b") {
        stringValue = stringValue.slice(1);
      }

      // We need to strip out the quotes from the raw value
      if (['"', "'"].includes(stringValue[0])) {
        stringValue = stringValue.substr(1);
      }

      if (['"', "'"].includes(stringValue[stringValue.length - 1])) {
        stringValue = stringValue.substr(0, stringValue.length - 1);
      }

      return [
        node.raw[0] === "b" ? "b" : "",
        quote,
        join(literalline, stringValue.split("\n")),
        quote,
      ];
    }
    case "intersectiontype": {
      return printComposedTypes(path, print, "&");
    }
    case "uniontype": {
      return printComposedTypes(path, print, "|");
    }
    case "encapsedpart": {
      const open =
        (node.syntax === "simple" && node.curly) || node.syntax === "complex"
          ? [node.curly ? "$" : "", "{"]
          : "";
      const close =
        (node.syntax === "simple" && node.curly) || node.syntax === "complex"
          ? "}"
          : "";

      return [open, path.call(print, "expression"), close];
    }
    case "encapsed":
      switch (node.type) {
        case "string":
        case "shell":
        case "heredoc": {
          const flexible = isMinVersion(options.phpVersion, "7.3");
          const linebreak = flexible ? hardline : literalline;
          return [
            getEncapsedQuotes(node),
            // Respect `indent` for `heredoc` nodes
            node.type === "heredoc" ? linebreak : "",
            ...path.map(print, "value"),
            getEncapsedQuotes(node, { opening: false }),
            node.type === "heredoc" && docShouldHaveTrailingNewline(path)
              ? hardline
              : "",
          ];
        }
        // istanbul ignore next
        default:
          return `Have not implemented kind ${node.type} yet.`;
      }
    case "inline":
      return join(
        literalline,
        node.raw.replace("___PSEUDO_INLINE_PLACEHOLDER___", "").split("\n")
      );
    case "magic":
      return node.value;
    case "nowdoc": {
      const flexible = isMinVersion(options.phpVersion, "7.3");
      const linebreak = flexible ? hardline : literalline;
      return [
        "<<<'",
        node.label,
        "'",
        linebreak,
        join(linebreak, node.value.split("\n")),
        linebreak,
        node.label,
        docShouldHaveTrailingNewline(path) ? hardline : "",
      ];
    }
    case "name":
      return [node.resolution === "rn" ? "namespace\\" : "", node.name];
    case "literal":
      return path.call(print, "value");
    case "parentreference":
      return "parent";
    case "selfreference":
      return "self";
    case "staticreference":
      return "static";
    case "typereference":
      return node.name;
    case "nullkeyword":
      return "null";
    case "identifier": {
      const parent = path.getParentNode();

      if (parent.kind === "method") {
        node.name = normalizeMagicMethodName(node.name);
      }

      return path.call(print, "name");
    }
    case "match": {
      const lastArmIndex = node.arms.length - 1;
      const arms = path.map((armPath, armIdx) => {
        const armNode = armPath.getValue();

        const maybeLeadingComment = comments.hasLeadingComment(armNode)
          ? [comments.printComments(armNode.leadingComments, options), hardline]
          : [];
        const maybeTrailingComma =
          armIdx < lastArmIndex || options.trailingCommaPHP ? "," : "";
        const maybeTrailingComment = comments.hasTrailingComment(armNode)
          ? [
              " ",
              comments.printComments(
                armNode.comments.filter((c) => c.trailing),
                options
              ),
            ]
          : [];

        const conds =
          armNode.conds === null
            ? "default"
            : armPath.map(
                (condPath, condIdx) =>
                  [",", line, print(condPath)].slice(condIdx === 0 ? 2 : 0),
                "conds"
              );
        const body = armPath.call(print, "body");
        const maybeEmptyLineBetweenArms =
          armIdx > 0 &&
          isPreviousLineEmpty(options.originalText, armNode, options)
            ? hardline
            : "";

        return [
          "",
          hardline,
          maybeEmptyLineBetweenArms,
          ...maybeLeadingComment,
          group([
            group([conds, indent(line)]),
            "=> ",
            body,
            maybeTrailingComma,
            ...maybeTrailingComment,
          ]),
        ].slice(armIdx > 0 ? 0 : 1);
      }, "arms");
      return group([
        "match (",
        group([softline, indent(path.call(print, "cond")), softline]),
        ") {",
        group(indent([...arms])),
        " ",
        softline,
        "}",
      ]);
    }

    case "noop":
      return node.comments
        ? comments.printComments(path.getValue().comments, options)
        : "";
    case "namedargument":
      return [node.name, ": ", path.call(print, "value")];

    case "enumcase":
      return group([
        "case ",
        path.call(print, "name"),
        node.value
          ? [
              " =",
              printAssignmentRight(
                node.name,
                node.value,
                path.call(print, "value"),
                false,
                options
              ),
            ]
          : "",
      ]);
    case "variadicplaceholder":
      return "...";

    case "error":
    default:
      // istanbul ignore next
      return `Have not implemented kind ${node.kind} yet.`;
  }
}

module.exports = genericPrint;
