import assert from "assert";
import * as types from "ast-types";
import { printComments } from "./comments";
import FastPath from "./fast-path";
import { concat, fromString, Lines } from "./lines";
import { normalize as normalizeOptions } from "./options";
import { getReprinter } from "./patcher";
import * as util from "./util";
const namedTypes = types.namedTypes;
const isString = types.builtInTypes.string;
const isObject = types.builtInTypes.object;

export interface PrintResultType {
  code: string;
  map?: any;
  toString(): string;
}

interface PrintResultConstructor {
  new (code: any, sourceMap?: any): PrintResultType;
}

const PrintResult = function PrintResult(
  this: PrintResultType,
  code: any,
  sourceMap?: any,
) {
  assert.ok(this instanceof PrintResult);

  isString.assert(code);
  this.code = code;

  if (sourceMap) {
    isObject.assert(sourceMap);
    this.map = sourceMap;
  }
} as any as PrintResultConstructor;

const PRp: PrintResultType = PrintResult.prototype;
let warnedAboutToString = false;

PRp.toString = function () {
  if (!warnedAboutToString) {
    console.warn(
      "Deprecation warning: recast.print now returns an object with " +
        "a .code property. You appear to be treating the object as a " +
        "string, which might still work but is strongly discouraged.",
    );

    warnedAboutToString = true;
  }

  return this.code;
};

const emptyPrintResult = new PrintResult("");

interface PrinterType {
  print(ast: any): PrintResultType;
  printGenerically(ast: any): PrintResultType;
}

interface PrinterConstructor {
  new (config?: any): PrinterType;
}

const Printer = function Printer(this: PrinterType, config?: any) {
  assert.ok(this instanceof Printer);

  const explicitTabWidth = config && config.tabWidth;
  config = normalizeOptions(config);

  // It's common for client code to pass the same options into both
  // recast.parse and recast.print, but the Printer doesn't need (and
  // can be confused by) config.sourceFileName, so we null it out.
  config.sourceFileName = null;

  // Non-destructively modifies options with overrides, and returns a
  // new print function that uses the modified options.
  function makePrintFunctionWith(options: any, overrides: any) {
    options = Object.assign({}, options, overrides);
    return (path: any) => print(path, options);
  }

  function print(path: any, options: any) {
    assert.ok(path instanceof FastPath);
    options = options || {};

    if (options.includeComments) {
      return printComments(
        path,
        makePrintFunctionWith(options, {
          includeComments: false,
        }),
      );
    }

    const oldTabWidth = config.tabWidth;

    if (!explicitTabWidth) {
      const loc = path.getNode().loc;
      if (loc && loc.lines && loc.lines.guessTabWidth) {
        config.tabWidth = loc.lines.guessTabWidth();
      }
    }

    const reprinter = getReprinter(path);
    const lines = reprinter
      ? // Since the print function that we pass to the reprinter will
        // be used to print "new" nodes, it's tempting to think we
        // should pass printRootGenerically instead of print, to avoid
        // calling maybeReprint again, but that would be a mistake
        // because the new nodes might not be entirely new, but merely
        // moved from elsewhere in the AST. The print function is the
        // right choice because it gives us the opportunity to reprint
        // such nodes using their original source.
        reprinter(print)
      : genericPrint(
          path,
          config,
          options,
          makePrintFunctionWith(options, {
            includeComments: true,
            avoidRootParens: false,
          }),
        );

    config.tabWidth = oldTabWidth;

    return lines;
  }

  this.print = function (ast) {
    if (!ast) {
      return emptyPrintResult;
    }

    const lines = print(FastPath.from(ast), {
      includeComments: true,
      avoidRootParens: false,
    });

    return new PrintResult(
      lines.toString(config),
      util.composeSourceMaps(
        config.inputSourceMap,
        lines.getSourceMap(config.sourceMapName, config.sourceRoot),
      ),
    );
  };

  this.printGenerically = function (ast) {
    if (!ast) {
      return emptyPrintResult;
    }

    // Print the entire AST generically.
    function printGenerically(path: any) {
      return printComments(path, (path: any) =>
        genericPrint(
          path,
          config,
          {
            includeComments: true,
            avoidRootParens: false,
          },
          printGenerically,
        ),
      );
    }

    const path = FastPath.from(ast);
    const oldReuseWhitespace = config.reuseWhitespace;

    // Do not reuse whitespace (or anything else, for that matter)
    // when printing generically.
    config.reuseWhitespace = false;

    // TODO Allow printing of comments?
    const pr = new PrintResult(printGenerically(path).toString(config));
    config.reuseWhitespace = oldReuseWhitespace;
    return pr;
  };
} as any as PrinterConstructor;

export { Printer };

function genericPrint(path: any, config: any, options: any, printPath: any) {
  assert.ok(path instanceof FastPath);

  const node = path.getValue();
  const parts = [];
  const linesWithoutParens = genericPrintNoParens(path, config, printPath);

  if (!node || linesWithoutParens.isEmpty()) {
    return linesWithoutParens;
  }

  let shouldAddParens = false;
  const decoratorsLines = printDecorators(path, printPath);

  if (decoratorsLines.isEmpty()) {
    // Nodes with decorators can't have parentheses, so we can avoid
    // computing path.needsParens() except in this case.
    if (!options.avoidRootParens) {
      shouldAddParens = path.needsParens();
    }
  } else {
    parts.push(decoratorsLines);
  }

  if (shouldAddParens) {
    parts.unshift("(");
  }

  parts.push(linesWithoutParens);

  if (shouldAddParens) {
    parts.push(")");
  }

  return concat(parts);
}

// Note that the `options` parameter of this function is what other
// functions in this file call the `config` object (that is, the
// configuration object originally passed into the Printer constructor).
// Its properties are documented in lib/options.js.
function genericPrintNoParens(path: any, options: any, print: any) {
  const n = path.getValue();

  if (!n) {
    return fromString("");
  }

  if (typeof n === "string") {
    return fromString(n, options);
  }

  namedTypes.Printable.assert(n);

  const parts: (string | Lines)[] = [];

  switch (n.type) {
    case "File":
      return path.call(print, "program");

    case "Program":
      // Babel 6
      if (n.directives) {
        path.each(function (childPath: any) {
          parts.push(print(childPath), ";\n");
        }, "directives");
      }

      if (n.interpreter) {
        parts.push(path.call(print, "interpreter"));
      }

      parts.push(
        path.call(
          (bodyPath: any) => printStatementSequence(bodyPath, options, print),
          "body",
        ),
      );

      return concat(parts);

    case "Noop": // Babel extension.
    case "EmptyStatement":
      return fromString("");

    case "ExpressionStatement":
      return concat([path.call(print, "expression"), ";"]);

    case "ParenthesizedExpression": // Babel extension.
      return concat(["(", path.call(print, "expression"), ")"]);

    case "BinaryExpression":
    case "LogicalExpression":
    case "AssignmentExpression":
      return fromString(" ").join([
        path.call(print, "left"),
        n.operator,
        path.call(print, "right"),
      ]);

    case "AssignmentPattern":
      return concat([
        path.call(print, "left"),
        " = ",
        path.call(print, "right"),
      ]);

    case "MemberExpression":
    case "OptionalMemberExpression": {
      parts.push(path.call(print, "object"));

      const property = path.call(print, "property");

      // Like n.optional, except with defaults applied, so optional
      // defaults to true for OptionalMemberExpression nodes.
      const optional = types.getFieldValue(n, "optional");

      if (n.computed) {
        parts.push(optional ? "?.[" : "[", property, "]");
      } else {
        parts.push(optional ? "?." : ".", property);
      }

      return concat(parts);
    }

    case "ChainExpression":
      return path.call(print, "expression");

    case "MetaProperty":
      return concat([
        path.call(print, "meta"),
        ".",
        path.call(print, "property"),
      ]);

    case "BindExpression":
      if (n.object) {
        parts.push(path.call(print, "object"));
      }

      parts.push("::", path.call(print, "callee"));

      return concat(parts);

    case "Path":
      return fromString(".").join(n.body);

    case "Identifier":
      return concat([
        fromString(n.name, options),
        n.optional ? "?" : "",
        path.call(print, "typeAnnotation"),
      ]);

    case "SpreadElement":
    case "SpreadElementPattern":
    case "RestProperty": // Babel 6 for ObjectPattern
    case "SpreadProperty":
    case "SpreadPropertyPattern":
    case "ObjectTypeSpreadProperty":
    case "RestElement":
      return concat([
        "...",
        path.call(print, "argument"),
        path.call(print, "typeAnnotation"),
      ]);

    case "FunctionDeclaration":
    case "FunctionExpression":
    case "TSDeclareFunction":
      if (n.declare) {
        parts.push("declare ");
      }

      if (n.async) {
        parts.push("async ");
      }

      parts.push("function");

      if (n.generator) parts.push("*");

      if (n.id) {
        parts.push(
          " ",
          path.call(print, "id"),
          path.call(print, "typeParameters"),
        );
      } else {
        if (n.typeParameters) {
          parts.push(path.call(print, "typeParameters"));
        }
      }

      parts.push(
        "(",
        printFunctionParams(path, options, print),
        ")",
        path.call(print, "returnType"),
      );

      if (n.body) {
        parts.push(" ", path.call(print, "body"));
      }

      return concat(parts);

    case "ArrowFunctionExpression":
      if (n.async) {
        parts.push("async ");
      }

      if (n.typeParameters) {
        parts.push(path.call(print, "typeParameters"));
      }

      if (
        !options.arrowParensAlways &&
        n.params.length === 1 &&
        !n.rest &&
        n.params[0].type === "Identifier" &&
        !n.params[0].typeAnnotation &&
        !n.returnType
      ) {
        parts.push(path.call(print, "params", 0));
      } else {
        parts.push(
          "(",
          printFunctionParams(path, options, print),
          ")",
          path.call(print, "returnType"),
        );
      }

      parts.push(" => ", path.call(print, "body"));

      return concat(parts);

    case "MethodDefinition":
      return printMethod(path, options, print);

    case "YieldExpression":
      parts.push("yield");

      if (n.delegate) parts.push("*");

      if (n.argument) parts.push(" ", path.call(print, "argument"));

      return concat(parts);

    case "AwaitExpression":
      parts.push("await");

      if (n.all) parts.push("*");

      if (n.argument) parts.push(" ", path.call(print, "argument"));

      return concat(parts);

    case "ModuleExpression":
      return concat([
        "module {\n",
        path.call(print, "body").indent(options.tabWidth),
        "\n}",
      ]);

    case "ModuleDeclaration":
      parts.push("module", path.call(print, "id"));

      if (n.source) {
        assert.ok(!n.body);
        parts.push("from", path.call(print, "source"));
      } else {
        parts.push(path.call(print, "body"));
      }

      return fromString(" ").join(parts);

    case "ImportSpecifier":
      if (n.importKind && n.importKind !== "value") {
        parts.push(n.importKind + " ");
      }
      if (n.imported) {
        parts.push(path.call(print, "imported"));
        if (n.local && n.local.name !== n.imported.name) {
          parts.push(" as ", path.call(print, "local"));
        }
      } else if (n.id) {
        parts.push(path.call(print, "id"));
        if (n.name) {
          parts.push(" as ", path.call(print, "name"));
        }
      }

      return concat(parts);

    case "ExportSpecifier":
      if (n.exportKind && n.exportKind !== "value") {
        parts.push(n.exportKind + " ");
      }
      if (n.local) {
        parts.push(path.call(print, "local"));
        if (n.exported && n.exported.name !== n.local.name) {
          parts.push(" as ", path.call(print, "exported"));
        }
      } else if (n.id) {
        parts.push(path.call(print, "id"));
        if (n.name) {
          parts.push(" as ", path.call(print, "name"));
        }
      }

      return concat(parts);

    case "ExportBatchSpecifier":
      return fromString("*");

    case "ImportNamespaceSpecifier":
      parts.push("* as ");
      if (n.local) {
        parts.push(path.call(print, "local"));
      } else if (n.id) {
        parts.push(path.call(print, "id"));
      }
      return concat(parts);

    case "ImportDefaultSpecifier":
      if (n.local) {
        return path.call(print, "local");
      }
      return path.call(print, "id");

    case "TSExportAssignment":
      return concat(["export = ", path.call(print, "expression")]);

    case "ExportDeclaration":
    case "ExportDefaultDeclaration":
    case "ExportNamedDeclaration":
      return printExportDeclaration(path, options, print);

    case "ExportAllDeclaration":
      parts.push("export *");

      if (n.exported) {
        parts.push(" as ", path.call(print, "exported"));
      }

      parts.push(" from ", path.call(print, "source"), ";");

      return concat(parts);

    case "TSNamespaceExportDeclaration":
      parts.push("export as namespace ", path.call(print, "id"));
      return maybeAddSemicolon(concat(parts));

    case "ExportNamespaceSpecifier":
      return concat(["* as ", path.call(print, "exported")]);

    case "ExportDefaultSpecifier":
      return path.call(print, "exported");

    case "Import":
      return fromString("import", options);

    // Recast and ast-types currently support dynamic import(...) using
    // either this dedicated ImportExpression type or a CallExpression
    // whose callee has type Import.
    // https://github.com/benjamn/ast-types/pull/365#issuecomment-605214486
    case "ImportExpression":
      return concat(["import(", path.call(print, "source"), ")"]);

    case "ImportDeclaration": {
      parts.push("import ");

      if (n.importKind && n.importKind !== "value") {
        parts.push(n.importKind + " ");
      }

      if (n.specifiers && n.specifiers.length > 0) {
        const unbracedSpecifiers: any[] = [];
        const bracedSpecifiers: any[] = [];

        path.each(function (specifierPath: any) {
          const spec = specifierPath.getValue();
          if (spec.type === "ImportSpecifier") {
            bracedSpecifiers.push(print(specifierPath));
          } else if (
            spec.type === "ImportDefaultSpecifier" ||
            spec.type === "ImportNamespaceSpecifier"
          ) {
            unbracedSpecifiers.push(print(specifierPath));
          }
        }, "specifiers");

        unbracedSpecifiers.forEach((lines, i) => {
          if (i > 0) {
            parts.push(", ");
          }
          parts.push(lines);
        });

        if (bracedSpecifiers.length > 0) {
          let lines = fromString(", ").join(bracedSpecifiers);
          if (lines.getLineLength(1) > options.wrapColumn) {
            lines = concat([
              fromString(",\n").join(bracedSpecifiers).indent(options.tabWidth),
              ",",
            ]);
          }

          if (unbracedSpecifiers.length > 0) {
            parts.push(", ");
          }

          if (lines.length > 1) {
            parts.push("{\n", lines, "\n}");
          } else if (options.objectCurlySpacing) {
            parts.push("{ ", lines, " }");
          } else {
            parts.push("{", lines, "}");
          }
        }

        parts.push(" from ");
      }

      parts.push(
        path.call(print, "source"),
        maybePrintImportAssertions(path, options, print),
        ";",
      );

      return concat(parts);
    }

    case "ImportAttribute":
      return concat([path.call(print, "key"), ": ", path.call(print, "value")]);

    case "StaticBlock":
      parts.push("static ");
    // Intentionally fall through to BlockStatement below.

    case "BlockStatement": {
      const naked = path.call(
        (bodyPath: any) => printStatementSequence(bodyPath, options, print),
        "body",
      );

      if (naked.isEmpty()) {
        if (!n.directives || n.directives.length === 0) {
          parts.push("{}");
          return concat(parts);
        }
      }

      parts.push("{\n");
      // Babel 6
      if (n.directives) {
        path.each(function (childPath: any) {
          parts.push(
            maybeAddSemicolon(print(childPath).indent(options.tabWidth)),
            n.directives.length > 1 || !naked.isEmpty() ? "\n" : "",
          );
        }, "directives");
      }
      parts.push(naked.indent(options.tabWidth));
      parts.push("\n}");

      return concat(parts);
    }

    case "ReturnStatement": {
      parts.push("return");

      if (n.argument) {
        const argLines = path.call(print, "argument");
        if (
          argLines.startsWithComment() ||
          (argLines.length > 1 &&
            namedTypes.JSXElement &&
            namedTypes.JSXElement.check(n.argument))
        ) {
          parts.push(" (\n", argLines.indent(options.tabWidth), "\n)");
        } else {
          parts.push(" ", argLines);
        }
      }

      parts.push(";");

      return concat(parts);
    }

    case "CallExpression":
    case "OptionalCallExpression":
      parts.push(path.call(print, "callee"));

      if (n.typeParameters) {
        parts.push(path.call(print, "typeParameters"));
      }

      if (n.typeArguments) {
        parts.push(path.call(print, "typeArguments"));
      }

      // Like n.optional, but defaults to true for OptionalCallExpression
      // nodes that are missing an n.optional property (unusual),
      // according to the OptionalCallExpression definition in ast-types.
      if (types.getFieldValue(n, "optional")) {
        parts.push("?.");
      }

      parts.push(printArgumentsList(path, options, print));

      return concat(parts);

    case "RecordExpression":
      parts.push("#");
    // Intentionally fall through to printing the object literal...
    case "ObjectExpression":
    case "ObjectPattern":
    case "ObjectTypeAnnotation": {
      const isTypeAnnotation = n.type === "ObjectTypeAnnotation";
      const separator = options.flowObjectCommas
        ? ","
        : isTypeAnnotation
        ? ";"
        : ",";
      const fields = [];
      let allowBreak = false;

      if (isTypeAnnotation) {
        fields.push("indexers", "callProperties");
        if (n.internalSlots != null) {
          fields.push("internalSlots");
        }
      }

      fields.push("properties");

      let len = 0;
      fields.forEach(function (field) {
        len += n[field].length;
      });

      const oneLine = (isTypeAnnotation && len === 1) || len === 0;
      const leftBrace = n.exact ? "{|" : "{";
      const rightBrace = n.exact ? "|}" : "}";
      parts.push(oneLine ? leftBrace : leftBrace + "\n");
      const leftBraceIndex = parts.length - 1;

      let i = 0;
      fields.forEach(function (field) {
        path.each(function (childPath: any) {
          let lines = print(childPath);

          if (!oneLine) {
            lines = lines.indent(options.tabWidth);
          }

          const multiLine = !isTypeAnnotation && lines.length > 1;
          if (multiLine && allowBreak) {
            // Similar to the logic for BlockStatement.
            parts.push("\n");
          }

          parts.push(lines);

          if (i < len - 1) {
            // Add an extra line break if the previous object property
            // had a multi-line value.
            parts.push(separator + (multiLine ? "\n\n" : "\n"));
            allowBreak = !multiLine;
          } else if (len !== 1 && isTypeAnnotation) {
            parts.push(separator);
          } else if (
            !oneLine &&
            util.isTrailingCommaEnabled(options, "objects") &&
            childPath.getValue().type !== "RestElement"
          ) {
            parts.push(separator);
          }
          i++;
        }, field);
      });

      if (n.inexact) {
        const line = fromString("...", options);
        if (oneLine) {
          if (len > 0) {
            parts.push(separator, " ");
          }
          parts.push(line);
        } else {
          // No trailing separator after ... to maintain parity with prettier.
          parts.push("\n", line.indent(options.tabWidth));
        }
      }

      parts.push(oneLine ? rightBrace : "\n" + rightBrace);

      if (i !== 0 && oneLine && options.objectCurlySpacing) {
        parts[leftBraceIndex] = leftBrace + " ";
        parts[parts.length - 1] = " " + rightBrace;
      }

      if (n.typeAnnotation) {
        parts.push(path.call(print, "typeAnnotation"));
      }

      return concat(parts);
    }

    case "PropertyPattern":
      return concat([
        path.call(print, "key"),
        ": ",
        path.call(print, "pattern"),
      ]);

    case "ObjectProperty": // Babel 6
    case "Property": {
      // Non-standard AST node type.
      if (n.method || n.kind === "get" || n.kind === "set") {
        return printMethod(path, options, print);
      }

      if (n.shorthand && n.value.type === "AssignmentPattern") {
        return path.call(print, "value");
      }

      const key = path.call(print, "key");
      if (n.computed) {
        parts.push("[", key, "]");
      } else {
        parts.push(key);
      }

      if (!n.shorthand || n.key.name !== n.value.name) {
        parts.push(": ", path.call(print, "value"));
      }

      return concat(parts);
    }

    case "ClassMethod": // Babel 6
    case "ObjectMethod": // Babel 6
    case "ClassPrivateMethod":
    case "TSDeclareMethod":
      return printMethod(path, options, print);

    case "PrivateName":
      return concat(["#", path.call(print, "id")]);

    case "Decorator":
      return concat(["@", path.call(print, "expression")]);

    case "TupleExpression":
      parts.push("#");
    // Intentionally fall through to printing the tuple elements...
    case "ArrayExpression":
    case "ArrayPattern": {
      const elems: any[] = n.elements;
      const len = elems.length;
      const printed = path.map(print, "elements");
      const joined = fromString(", ").join(printed);
      const oneLine = joined.getLineLength(1) <= options.wrapColumn;

      if (oneLine) {
        if (options.arrayBracketSpacing) {
          parts.push("[ ");
        } else {
          parts.push("[");
        }
      } else {
        parts.push("[\n");
      }

      path.each(function (elemPath: any) {
        const i = elemPath.getName();
        const elem = elemPath.getValue();
        if (!elem) {
          // If the array expression ends with a hole, that hole
          // will be ignored by the interpreter, but if it ends with
          // two (or more) holes, we need to write out two (or more)
          // commas so that the resulting code is interpreted with
          // both (all) of the holes.
          parts.push(",");
        } else {
          let lines = printed[i];
          if (oneLine) {
            if (i > 0) parts.push(" ");
          } else {
            lines = lines.indent(options.tabWidth);
          }
          parts.push(lines);
          if (
            i < len - 1 ||
            (!oneLine && util.isTrailingCommaEnabled(options, "arrays"))
          )
            parts.push(",");
          if (!oneLine) parts.push("\n");
        }
      }, "elements");

      if (oneLine && options.arrayBracketSpacing) {
        parts.push(" ]");
      } else {
        parts.push("]");
      }

      if (n.typeAnnotation) {
        parts.push(path.call(print, "typeAnnotation"));
      }

      return concat(parts);
    }

    case "SequenceExpression":
      return fromString(", ").join(path.map(print, "expressions"));

    case "ThisExpression":
      return fromString("this");

    case "Super":
      return fromString("super");

    case "NullLiteral": // Babel 6 Literal split
      return fromString("null");

    case "RegExpLiteral": // Babel 6 Literal split
      return fromString(
        getPossibleRaw(n) || `/${n.pattern}/${n.flags || ""}`,
        options,
      );

    case "BigIntLiteral": // Babel 7 Literal split
      return fromString(getPossibleRaw(n) || n.value + "n", options);

    case "NumericLiteral": // Babel 6 Literal Split
      return fromString(getPossibleRaw(n) || n.value, options);

    case "DecimalLiteral":
      return fromString(getPossibleRaw(n) || n.value + "m", options);

    case "StringLiteral":
        return fromString(nodeStr(n.value, options));

    case "BooleanLiteral": // Babel 6 Literal split
    case "Literal":
      return fromString(
        getPossibleRaw(n) ||
          (typeof n.value === "string" ? nodeStr(n.value, options) : n.value),
        options,
      );

    case "Directive": // Babel 6
      return path.call(print, "value");

    case "DirectiveLiteral": // Babel 6
      return fromString(
        getPossibleRaw(n) || nodeStr(n.value, options),
        options,
      );

    case "InterpreterDirective":
      return fromString(`#!${n.value}\n`, options);

    case "ModuleSpecifier":
      if (n.local) {
        throw new Error("The ESTree ModuleSpecifier type should be abstract");
      }

      // The Esprima ModuleSpecifier type is just a string-valued
      // Literal identifying the imported-from module.
      return fromString(nodeStr(n.value, options), options);

    case "UnaryExpression":
      parts.push(n.operator);
      if (/[a-z]$/.test(n.operator)) parts.push(" ");
      parts.push(path.call(print, "argument"));
      return concat(parts);

    case "UpdateExpression":
      parts.push(path.call(print, "argument"), n.operator);

      if (n.prefix) parts.reverse();

      return concat(parts);

    case "ConditionalExpression":
      return concat([
        path.call(print, "test"),
        " ? ",
        path.call(print, "consequent"),
        " : ",
        path.call(print, "alternate"),
      ]);

    case "NewExpression": {
      parts.push("new ", path.call(print, "callee"));
      if (n.typeParameters) {
        parts.push(path.call(print, "typeParameters"));
      }
      if (n.typeArguments) {
        parts.push(path.call(print, "typeArguments"));
      }
      const args = n.arguments;
      if (args) {
        parts.push(printArgumentsList(path, options, print));
      }

      return concat(parts);
    }

    case "VariableDeclaration": {
      if (n.declare) {
        parts.push("declare ");
      }

      parts.push(n.kind, " ");

      let maxLen = 0;
      const printed = path.map(function (childPath: any) {
        const lines = print(childPath);
        maxLen = Math.max(lines.length, maxLen);
        return lines;
      }, "declarations");

      if (maxLen === 1) {
        parts.push(fromString(", ").join(printed));
      } else if (printed.length > 1) {
        parts.push(
          fromString(",\n")
            .join(printed)
            .indentTail(n.kind.length + 1),
        );
      } else {
        parts.push(printed[0]);
      }

      // We generally want to terminate all variable declarations with a
      // semicolon, except when they are children of for loops.
      const parentNode = path.getParentNode();
      if (
        !namedTypes.ForStatement.check(parentNode) &&
        !namedTypes.ForInStatement.check(parentNode) &&
        !(
          namedTypes.ForOfStatement &&
          namedTypes.ForOfStatement.check(parentNode)
        ) &&
        !(
          namedTypes.ForAwaitStatement &&
          namedTypes.ForAwaitStatement.check(parentNode)
        )
      ) {
        parts.push(";");
      }

      return concat(parts);
    }

    case "VariableDeclarator":
      return n.init
        ? fromString(" = ").join([
            path.call(print, "id"),
            path.call(print, "init"),
          ])
        : path.call(print, "id");

    case "WithStatement":
      return concat([
        "with (",
        path.call(print, "object"),
        ") ",
        path.call(print, "body"),
      ]);

    case "IfStatement": {
      const con = adjustClause(path.call(print, "consequent"), options);
      parts.push("if (", path.call(print, "test"), ")", con);

      if (n.alternate)
        parts.push(
          endsWithBrace(con) ? " else" : "\nelse",
          adjustClause(path.call(print, "alternate"), options),
        );

      return concat(parts);
    }

    case "ForStatement": {
      // TODO Get the for (;;) case right.
      const init = path.call(print, "init");
      const sep = init.length > 1 ? ";\n" : "; ";
      const forParen = "for (";
      const indented = fromString(sep)
        .join([init, path.call(print, "test"), path.call(print, "update")])
        .indentTail(forParen.length);
      const head = concat([forParen, indented, ")"]);
      let clause = adjustClause(path.call(print, "body"), options);

      parts.push(head);

      if (head.length > 1) {
        parts.push("\n");
        clause = clause.trimLeft();
      }

      parts.push(clause);

      return concat(parts);
    }

    case "WhileStatement":
      return concat([
        "while (",
        path.call(print, "test"),
        ")",
        adjustClause(path.call(print, "body"), options),
      ]);

    case "ForInStatement":
      // Note: esprima can't actually parse "for each (".
      return concat([
        n.each ? "for each (" : "for (",
        path.call(print, "left"),
        " in ",
        path.call(print, "right"),
        ")",
        adjustClause(path.call(print, "body"), options),
      ]);

    case "ForOfStatement":
    case "ForAwaitStatement":
      parts.push("for ");

      if (n.await || n.type === "ForAwaitStatement") {
        parts.push("await ");
      }

      parts.push(
        "(",
        path.call(print, "left"),
        " of ",
        path.call(print, "right"),
        ")",
        adjustClause(path.call(print, "body"), options),
      );

      return concat(parts);

    case "DoWhileStatement": {
      const doBody = concat([
        "do",
        adjustClause(path.call(print, "body"), options),
      ]);

      parts.push(doBody);

      if (endsWithBrace(doBody)) parts.push(" while");
      else parts.push("\nwhile");

      parts.push(" (", path.call(print, "test"), ");");

      return concat(parts);
    }

    case "DoExpression": {
      const statements = path.call(
        (bodyPath: any) => printStatementSequence(bodyPath, options, print),
        "body",
      );

      return concat(["do {\n", statements.indent(options.tabWidth), "\n}"]);
    }

    case "BreakStatement":
      parts.push("break");
      if (n.label) parts.push(" ", path.call(print, "label"));
      parts.push(";");
      return concat(parts);

    case "ContinueStatement":
      parts.push("continue");
      if (n.label) parts.push(" ", path.call(print, "label"));
      parts.push(";");
      return concat(parts);

    case "LabeledStatement":
      return concat([
        path.call(print, "label"),
        ":\n",
        path.call(print, "body"),
      ]);

    case "TryStatement":
      parts.push("try ", path.call(print, "block"));

      if (n.handler) {
        parts.push(" ", path.call(print, "handler"));
      } else if (n.handlers) {
        path.each(function (handlerPath: any) {
          parts.push(" ", print(handlerPath));
        }, "handlers");
      }

      if (n.finalizer) {
        parts.push(" finally ", path.call(print, "finalizer"));
      }

      return concat(parts);

    case "CatchClause":
      parts.push("catch ");

      if (n.param) {
        parts.push("(", path.call(print, "param"));
      }

      if (n.guard) {
        // Note: esprima does not recognize conditional catch clauses.
        parts.push(" if ", path.call(print, "guard"));
      }

      if (n.param) {
        parts.push(") ");
      }

      parts.push(path.call(print, "body"));

      return concat(parts);

    case "ThrowStatement":
      return concat(["throw ", path.call(print, "argument"), ";"]);

    case "SwitchStatement":
      return concat([
        "switch (",
        path.call(print, "discriminant"),
        ") {\n",
        fromString("\n").join(path.map(print, "cases")),
        "\n}",
      ]);

    // Note: ignoring n.lexical because it has no printing consequences.

    case "SwitchCase":
      if (n.test) parts.push("case ", path.call(print, "test"), ":");
      else parts.push("default:");

      if (n.consequent.length > 0) {
        parts.push(
          "\n",
          path
            .call(
              (consequentPath: any) =>
                printStatementSequence(consequentPath, options, print),
              "consequent",
            )
            .indent(options.tabWidth),
        );
      }

      return concat(parts);

    case "DebuggerStatement":
      return fromString("debugger;");

    // JSX extensions below.

    case "JSXAttribute":
      parts.push(path.call(print, "name"));
      if (n.value) parts.push("=", path.call(print, "value"));
      return concat(parts);

    case "JSXIdentifier":
      return fromString(n.name, options);

    case "JSXNamespacedName":
      return fromString(":").join([
        path.call(print, "namespace"),
        path.call(print, "name"),
      ]);

    case "JSXMemberExpression":
      return fromString(".").join([
        path.call(print, "object"),
        path.call(print, "property"),
      ]);

    case "JSXSpreadAttribute":
      return concat(["{...", path.call(print, "argument"), "}"]);

    case "JSXSpreadChild":
      return concat(["{...", path.call(print, "expression"), "}"]);

    case "JSXExpressionContainer":
      return concat(["{", path.call(print, "expression"), "}"]);

    case "JSXElement":
    case "JSXFragment": {
      const openingPropName =
        "opening" + (n.type === "JSXElement" ? "Element" : "Fragment");
      const closingPropName =
        "closing" + (n.type === "JSXElement" ? "Element" : "Fragment");
      const openingLines = path.call(print, openingPropName);

      if (n[openingPropName].selfClosing) {
        assert.ok(
          !n[closingPropName],
          "unexpected " +
            closingPropName +
            " element in self-closing " +
            n.type,
        );
        return openingLines;
      }

      const childLines = concat(
        path.map(function (childPath: any) {
          const child = childPath.getValue();

          if (
            namedTypes.Literal.check(child) &&
            typeof child.value === "string"
          ) {
            if (/\S/.test(child.value)) {
              return child.value.replace(/^\s+|\s+$/g, "");
            } else if (/\n/.test(child.value)) {
              return "\n";
            }
          }

          return print(childPath);
        }, "children"),
      ).indentTail(options.tabWidth);

      const closingLines = path.call(print, closingPropName);

      return concat([openingLines, childLines, closingLines]);
    }

    case "JSXOpeningElement": {
      parts.push("<", path.call(print, "name"));
      const attrParts: any[] = [];

      path.each(function (attrPath: any) {
        attrParts.push(" ", print(attrPath));
      }, "attributes");

      let attrLines = concat(attrParts);

      const needLineWrap =
        attrLines.length > 1 || attrLines.getLineLength(1) > options.wrapColumn;

      if (needLineWrap) {
        attrParts.forEach(function (part, i) {
          if (part === " ") {
            assert.strictEqual(i % 2, 0);
            attrParts[i] = "\n";
          }
        });

        attrLines = concat(attrParts).indentTail(options.tabWidth);
      }

      parts.push(attrLines, n.selfClosing ? " />" : ">");

      return concat(parts);
    }

    case "JSXClosingElement":
      return concat(["</", path.call(print, "name"), ">"]);

    case "JSXOpeningFragment":
      return fromString("<>");

    case "JSXClosingFragment":
      return fromString("</>");

    case "JSXText":
      return fromString(n.value, options);

    case "JSXEmptyExpression":
      return fromString("");

    case "TypeAnnotatedIdentifier":
      return concat([
        path.call(print, "annotation"),
        " ",
        path.call(print, "identifier"),
      ]);

    case "ClassBody":
      if (n.body.length === 0) {
        return fromString("{}");
      }

      return concat([
        "{\n",
        path
          .call(
            (bodyPath: any) => printStatementSequence(bodyPath, options, print),
            "body",
          )
          .indent(options.tabWidth),
        "\n}",
      ]);

    case "ClassPropertyDefinition":
      parts.push("static ", path.call(print, "definition"));
      if (!namedTypes.MethodDefinition.check(n.definition)) parts.push(";");
      return concat(parts);

    case "ClassProperty": {
      if (n.declare) {
        parts.push("declare ");
      }

      const access = n.accessibility || n.access;
      if (typeof access === "string") {
        parts.push(access, " ");
      }

      if (n.static) {
        parts.push("static ");
      }

      if (n.abstract) {
        parts.push("abstract ");
      }

      if (n.readonly) {
        parts.push("readonly ");
      }

      let key = path.call(print, "key");

      if (n.computed) {
        key = concat(["[", key, "]"]);
      }

      if (n.variance) {
        key = concat([printVariance(path, print), key]);
      }

      parts.push(key);

      if (n.optional) {
        parts.push("?");
      }

      if (n.definite) {
        parts.push("!");
      }

      if (n.typeAnnotation) {
        parts.push(path.call(print, "typeAnnotation"));
      }

      if (n.value) {
        parts.push(" = ", path.call(print, "value"));
      }

      parts.push(";");
      return concat(parts);
    }

    case "ClassPrivateProperty":
      if (n.static) {
        parts.push("static ");
      }

      parts.push(path.call(print, "key"));

      if (n.typeAnnotation) {
        parts.push(path.call(print, "typeAnnotation"));
      }

      if (n.value) {
        parts.push(" = ", path.call(print, "value"));
      }

      parts.push(";");
      return concat(parts);

    case "ClassAccessorProperty": {
      parts.push(
        ...printClassMemberModifiers(n),
        "accessor ",
      );

      if (n.computed) {
        parts.push("[", path.call(print, "key"), "]");
      } else {
        parts.push(path.call(print, "key"));
      }

      if (n.optional) {
        parts.push("?");
      }

      if (n.definite) {
        parts.push("!");
      }

      if (n.typeAnnotation) {
        parts.push(path.call(print, "typeAnnotation"));
      }

      if (n.value) {
        parts.push(" = ", path.call(print, "value"));
      }

      parts.push(";");

      return concat(parts);
    }

    case "ClassDeclaration":
    case "ClassExpression":
    case "DeclareClass":
      if (n.declare) {
        parts.push("declare ");
      }

      if (n.abstract) {
        parts.push("abstract ");
      }

      parts.push("class");

      if (n.id) {
        parts.push(" ", path.call(print, "id"));
      }

      if (n.typeParameters) {
        parts.push(path.call(print, "typeParameters"));
      }

      if (n.superClass) {
        // ClassDeclaration and ClassExpression only
        parts.push(
          " extends ",
          path.call(print, "superClass"),
          path.call(print, "superTypeParameters"),
        );
      }

      if (n.extends && n.extends.length > 0) {
        // DeclareClass only
        parts.push(
          " extends ",
          fromString(", ").join(path.map(print, "extends")),
        );
      }

      if (n["implements"] && n["implements"].length > 0) {
        parts.push(
          " implements ",
          fromString(", ").join(path.map(print, "implements")),
        );
      }

      parts.push(" ", path.call(print, "body"));

      if (n.type === "DeclareClass") {
        return printFlowDeclaration(path, parts);
      } else {
        return concat(parts);
      }

    case "TemplateElement":
      return fromString(n.value.raw, options).lockIndentTail();

    case "TemplateLiteral": {
      const expressions = path.map(print, "expressions");
      parts.push("`");

      path.each(function (childPath: any) {
        const i = childPath.getName();
        parts.push(print(childPath));
        if (i < expressions.length) {
          parts.push("${", expressions[i], "}");
        }
      }, "quasis");

      parts.push("`");

      return concat(parts).lockIndentTail();
    }

    case "TaggedTemplateExpression":
      return concat([path.call(print, "tag"), path.call(print, "quasi")]);

    // These types are unprintable because they serve as abstract
    // supertypes for other (printable) types.
    case "Node":
    case "Printable":
    case "SourceLocation":
    case "Position":
    case "Statement":
    case "Function":
    case "Pattern":
    case "Expression":
    case "Declaration":
    case "Specifier":
    case "NamedSpecifier":
    case "Comment": // Supertype of Block and Line
    case "Flow": // Supertype of all Flow AST node types
    case "FlowType": // Supertype of all Flow types
    case "FlowPredicate": // Supertype of InferredPredicate and DeclaredPredicate
    case "MemberTypeAnnotation": // Flow
    case "Type": // Flow
    case "TSHasOptionalTypeParameterInstantiation":
    case "TSHasOptionalTypeParameters":
    case "TSHasOptionalTypeAnnotation":
    case "ChainElement": // Supertype of MemberExpression and CallExpression
      throw new Error("unprintable type: " + JSON.stringify(n.type));

    case "CommentBlock": // Babel block comment.
    case "Block": // Esprima block comment.
      return concat(["/*", fromString(n.value, options), "*/"]);

    case "CommentLine": // Babel line comment.
    case "Line": // Esprima line comment.
      return concat(["//", fromString(n.value, options)]);

    // Type Annotations for Facebook Flow, typically stripped out or
    // transformed away before printing.
    case "TypeAnnotation":
      if (n.typeAnnotation) {
        if (n.typeAnnotation.type !== "FunctionTypeAnnotation") {
          parts.push(": ");
        }
        parts.push(path.call(print, "typeAnnotation"));
        return concat(parts);
      }

      return fromString("");

    case "ExistentialTypeParam":
    case "ExistsTypeAnnotation":
      return fromString("*", options);

    case "EmptyTypeAnnotation":
      return fromString("empty", options);

    case "AnyTypeAnnotation":
      return fromString("any", options);

    case "MixedTypeAnnotation":
      return fromString("mixed", options);

    case "ArrayTypeAnnotation":
      return concat([path.call(print, "elementType"), "[]"]);

    case "TupleTypeAnnotation": {
      const printed = path.map(print, "types");
      const joined = fromString(", ").join(printed);
      const oneLine = joined.getLineLength(1) <= options.wrapColumn;
      if (oneLine) {
        if (options.arrayBracketSpacing) {
          parts.push("[ ");
        } else {
          parts.push("[");
        }
      } else {
        parts.push("[\n");
      }

      path.each(function (elemPath: any) {
        const i = elemPath.getName();
        const elem = elemPath.getValue();
        if (!elem) {
          // If the array expression ends with a hole, that hole
          // will be ignored by the interpreter, but if it ends with
          // two (or more) holes, we need to write out two (or more)
          // commas so that the resulting code is interpreted with
          // both (all) of the holes.
          parts.push(",");
        } else {
          let lines = printed[i];
          if (oneLine) {
            if (i > 0) parts.push(" ");
          } else {
            lines = lines.indent(options.tabWidth);
          }
          parts.push(lines);
          if (
            i < n.types.length - 1 ||
            (!oneLine && util.isTrailingCommaEnabled(options, "arrays"))
          )
            parts.push(",");
          if (!oneLine) parts.push("\n");
        }
      }, "types");

      if (oneLine && options.arrayBracketSpacing) {
        parts.push(" ]");
      } else {
        parts.push("]");
      }

      return concat(parts);
    }

    case "BooleanTypeAnnotation":
      return fromString("boolean", options);

    case "BooleanLiteralTypeAnnotation":
      assert.strictEqual(typeof n.value, "boolean");
      return fromString("" + n.value, options);

    case "InterfaceTypeAnnotation":
      parts.push("interface");
      if (n.extends && n.extends.length > 0) {
        parts.push(
          " extends ",
          fromString(", ").join(path.map(print, "extends")),
        );
      }
      parts.push(" ", path.call(print, "body"));
      return concat(parts);

    case "DeclareFunction":
      return printFlowDeclaration(path, [
        "function ",
        path.call(print, "id"),
        ";",
      ]);

    case "DeclareModule":
      return printFlowDeclaration(path, [
        "module ",
        path.call(print, "id"),
        " ",
        path.call(print, "body"),
      ]);

    case "DeclareModuleExports":
      return printFlowDeclaration(path, [
        "module.exports",
        path.call(print, "typeAnnotation"),
      ]);

    case "DeclareVariable":
      return printFlowDeclaration(path, ["var ", path.call(print, "id"), ";"]);

    case "DeclareExportDeclaration":
    case "DeclareExportAllDeclaration":
      return concat(["declare ", printExportDeclaration(path, options, print)]);

    case "EnumDeclaration":
      return concat([
        "enum ",
        path.call(print, "id"),
        path.call(print, "body"),
      ]);

    case "EnumBooleanBody":
    case "EnumNumberBody":
    case "EnumStringBody":
    case "EnumSymbolBody": {
      if (n.type === "EnumSymbolBody" || n.explicitType) {
        parts.push(
          " of ",
          // EnumBooleanBody => boolean, etc.
          n.type.slice(4, -4).toLowerCase(),
        );
      }

      parts.push(
        " {\n",
        fromString("\n")
          .join(path.map(print, "members"))
          .indent(options.tabWidth),
        "\n}",
      );

      return concat(parts);
    }

    case "EnumDefaultedMember":
      return concat([path.call(print, "id"), ","]);

    case "EnumBooleanMember":
    case "EnumNumberMember":
    case "EnumStringMember":
      return concat([
        path.call(print, "id"),
        " = ",
        path.call(print, "init"),
        ",",
      ]);

    case "InferredPredicate":
      return fromString("%checks", options);

    case "DeclaredPredicate":
      return concat(["%checks(", path.call(print, "value"), ")"]);

    case "FunctionTypeAnnotation": {
      // FunctionTypeAnnotation is ambiguous:
      // declare function(a: B): void; OR
      // const A: (a: B) => void;
      const parent = path.getParentNode(0);
      const isArrowFunctionTypeAnnotation = !(
        namedTypes.ObjectTypeCallProperty.check(parent) ||
        (namedTypes.ObjectTypeInternalSlot.check(parent) && parent.method) ||
        namedTypes.DeclareFunction.check(path.getParentNode(2))
      );

      const needsColon =
        isArrowFunctionTypeAnnotation &&
        !namedTypes.FunctionTypeParam.check(parent) &&
        !namedTypes.TypeAlias.check(parent);

      if (needsColon) {
        parts.push(": ");
      }

      const hasTypeParameters = !!n.typeParameters;
      const needsParens =
        hasTypeParameters || n.params.length !== 1 || n.params[0].name;

      parts.push(
        hasTypeParameters ? path.call(print, "typeParameters") : "",
        needsParens ? "(" : "",
        printFunctionParams(path, options, print),
        needsParens ? ")" : "",
      );

      // The returnType is not wrapped in a TypeAnnotation, so the colon
      // needs to be added separately.
      if (n.returnType) {
        parts.push(
          isArrowFunctionTypeAnnotation ? " => " : ": ",
          path.call(print, "returnType"),
        );
      }

      return concat(parts);
    }

    case "FunctionTypeParam": {
      const name = path.call(print, "name");
      parts.push(name);
      if (n.optional) {
        parts.push("?");
      }
      if (name.infos[0].line) {
        parts.push(": ");
      }
      parts.push(path.call(print, "typeAnnotation"));

      return concat(parts);
    }

    case "GenericTypeAnnotation":
      return concat([
        path.call(print, "id"),
        path.call(print, "typeParameters"),
      ]);

    case "DeclareInterface":
      parts.push("declare ");
    // Fall through to InterfaceDeclaration...

    case "InterfaceDeclaration":
    case "TSInterfaceDeclaration":
      if (n.declare) {
        parts.push("declare ");
      }

      parts.push(
        "interface ",
        path.call(print, "id"),
        path.call(print, "typeParameters"),
        " ",
      );

      if (n["extends"] && n["extends"].length > 0) {
        parts.push(
          "extends ",
          fromString(", ").join(path.map(print, "extends")),
          " ",
        );
      }

      if (n.body) {
        parts.push(path.call(print, "body"));
      }

      return concat(parts);

    case "ClassImplements":
    case "InterfaceExtends":
      return concat([
        path.call(print, "id"),
        path.call(print, "typeParameters"),
      ]);

    case "IntersectionTypeAnnotation":
      return fromString(" & ").join(path.map(print, "types"));

    case "NullableTypeAnnotation":
      return concat(["?", path.call(print, "typeAnnotation")]);

    case "NullLiteralTypeAnnotation":
      return fromString("null", options);

    case "ThisTypeAnnotation":
      return fromString("this", options);

    case "NumberTypeAnnotation":
      return fromString("number", options);

    case "ObjectTypeCallProperty":
      return path.call(print, "value");

    case "ObjectTypeIndexer":
      if (n.static) {
        parts.push("static ");
      }

      parts.push(printVariance(path, print), "[");

      if (n.id) {
        parts.push(path.call(print, "id"), ": ");
      }

      parts.push(path.call(print, "key"), "]: ", path.call(print, "value"));

      return concat(parts);

    case "ObjectTypeProperty":
      return concat([
        printVariance(path, print),
        path.call(print, "key"),
        n.optional ? "?" : "",
        ": ",
        path.call(print, "value"),
      ]);

    case "ObjectTypeInternalSlot":
      return concat([
        n.static ? "static " : "",
        "[[",
        path.call(print, "id"),
        "]]",
        n.optional ? "?" : "",
        n.value.type !== "FunctionTypeAnnotation" ? ": " : "",
        path.call(print, "value"),
      ]);

    case "QualifiedTypeIdentifier":
      return concat([
        path.call(print, "qualification"),
        ".",
        path.call(print, "id"),
      ]);

    case "StringLiteralTypeAnnotation":
      return fromString(nodeStr(n.value, options), options);

    case "NumberLiteralTypeAnnotation":
    case "NumericLiteralTypeAnnotation":
      assert.strictEqual(typeof n.value, "number");
      return fromString(JSON.stringify(n.value), options);

    case "BigIntLiteralTypeAnnotation":
      return fromString(n.raw, options);

    case "StringTypeAnnotation":
      return fromString("string", options);

    case "DeclareTypeAlias":
      parts.push("declare ");
    // Fall through to TypeAlias...

    case "TypeAlias":
      return concat([
        "type ",
        path.call(print, "id"),
        path.call(print, "typeParameters"),
        " = ",
        path.call(print, "right"),
        ";",
      ]);

    case "DeclareOpaqueType":
      parts.push("declare ");
    // Fall through to OpaqueType...

    case "OpaqueType":
      parts.push(
        "opaque type ",
        path.call(print, "id"),
        path.call(print, "typeParameters"),
      );

      if (n["supertype"]) {
        parts.push(": ", path.call(print, "supertype"));
      }

      if (n["impltype"]) {
        parts.push(" = ", path.call(print, "impltype"));
      }

      parts.push(";");

      return concat(parts);

    case "TypeCastExpression":
      return concat([
        "(",
        path.call(print, "expression"),
        path.call(print, "typeAnnotation"),
        ")",
      ]);

    case "TypeParameterDeclaration":
    case "TypeParameterInstantiation":
      return concat([
        "<",
        fromString(", ").join(path.map(print, "params")),
        ">",
      ]);

    case "Variance":
      if (n.kind === "plus") {
        return fromString("+");
      }

      if (n.kind === "minus") {
        return fromString("-");
      }

      return fromString("");

    case "TypeParameter":
      if (n.variance) {
        parts.push(printVariance(path, print));
      }

      parts.push(path.call(print, "name"));

      if (n.bound) {
        parts.push(path.call(print, "bound"));
      }

      if (n["default"]) {
        parts.push("=", path.call(print, "default"));
      }

      return concat(parts);

    case "TypeofTypeAnnotation":
      return concat([
        fromString("typeof ", options),
        path.call(print, "argument"),
      ]);

    case "IndexedAccessType":
    case "OptionalIndexedAccessType":
      return concat([
        path.call(print, "objectType"),
        n.optional ? "?." : "",
        "[",
        path.call(print, "indexType"),
        "]",
      ]);

    case "UnionTypeAnnotation":
      return fromString(" | ").join(path.map(print, "types"));

    case "VoidTypeAnnotation":
      return fromString("void", options);

    case "NullTypeAnnotation":
      return fromString("null", options);

    case "SymbolTypeAnnotation":
      return fromString("symbol", options);

    case "BigIntTypeAnnotation":
      return fromString("bigint", options);

    // Type Annotations for TypeScript (when using Babylon as parser)
    case "TSType":
      throw new Error("unprintable type: " + JSON.stringify(n.type));

    case "TSNumberKeyword":
      return fromString("number", options);

    case "TSBigIntKeyword":
      return fromString("bigint", options);

    case "TSObjectKeyword":
      return fromString("object", options);

    case "TSBooleanKeyword":
      return fromString("boolean", options);

    case "TSStringKeyword":
      return fromString("string", options);

    case "TSSymbolKeyword":
      return fromString("symbol", options);

    case "TSAnyKeyword":
      return fromString("any", options);

    case "TSVoidKeyword":
      return fromString("void", options);

    case "TSIntrinsicKeyword":
      return fromString("intrinsic", options);

    case "TSThisType":
      return fromString("this", options);

    case "TSNullKeyword":
      return fromString("null", options);

    case "TSUndefinedKeyword":
      return fromString("undefined", options);

    case "TSUnknownKeyword":
      return fromString("unknown", options);

    case "TSNeverKeyword":
      return fromString("never", options);

    case "TSArrayType":
      return concat([path.call(print, "elementType"), "[]"]);

    case "TSLiteralType":
      return path.call(print, "literal");

    case "TSUnionType":
      return fromString(" | ").join(path.map(print, "types"));

    case "TSIntersectionType":
      return fromString(" & ").join(path.map(print, "types"));

    case "TSConditionalType":
      parts.push(
        path.call(print, "checkType"),
        " extends ",
        path.call(print, "extendsType"),
        " ? ",
        path.call(print, "trueType"),
        " : ",
        path.call(print, "falseType"),
      );

      return concat(parts);

    case "TSInferType":
      parts.push("infer ", path.call(print, "typeParameter"));

      return concat(parts);

    case "TSParenthesizedType":
      return concat(["(", path.call(print, "typeAnnotation"), ")"]);

    case "TSFunctionType":
      return concat([
        path.call(print, "typeParameters"),
        "(",
        printFunctionParams(path, options, print),
        ") => ",
        path.call(print, "typeAnnotation", "typeAnnotation"),
      ]);

    case "TSConstructorType":
      return concat([
        "new ",
        path.call(print, "typeParameters"),
        "(",
        printFunctionParams(path, options, print),
        ") => ",
        path.call(print, "typeAnnotation", "typeAnnotation"),
      ]);

    case "TSMappedType": {
      parts.push(
        n.readonly ? "readonly " : "",
        "[",
        path.call(print, "typeParameter"),
        "]",
        n.optional ? "?" : "",
      );

      if (n.typeAnnotation) {
        parts.push(": ", path.call(print, "typeAnnotation"), ";");
      }

      return concat(["{\n", concat(parts).indent(options.tabWidth), "\n}"]);
    }

    case "TSTupleType":
      return concat([
        "[",
        fromString(", ").join(path.map(print, "elementTypes")),
        "]",
      ]);

    case "TSNamedTupleMember":
      parts.push(path.call(print, "label"));

      if (n.optional) {
        parts.push("?");
      }

      parts.push(": ", path.call(print, "elementType"));

      return concat(parts);

    case "TSRestType":
      return concat(["...", path.call(print, "typeAnnotation")]);

    case "TSOptionalType":
      return concat([path.call(print, "typeAnnotation"), "?"]);

    case "TSIndexedAccessType":
      return concat([
        path.call(print, "objectType"),
        "[",
        path.call(print, "indexType"),
        "]",
      ]);

    case "TSTypeOperator":
      return concat([
        path.call(print, "operator"),
        " ",
        path.call(print, "typeAnnotation"),
      ]);

    case "TSTypeLiteral": {
      const members = fromString("\n").join(
        path.map(print, "members").map((member: Lines) => {
          if (lastNonSpaceCharacter(member) !== ";") {
            return member.concat(";");
          }
          return member;
        })
      );

      if (members.isEmpty()) {
        return fromString("{}", options);
      }

      parts.push("{\n", members.indent(options.tabWidth), "\n}");

      return concat(parts);
    }

    case "TSEnumMember":
      parts.push(path.call(print, "id"));
      if (n.initializer) {
        parts.push(" = ", path.call(print, "initializer"));
      }
      return concat(parts);

    case "TSTypeQuery":
      return concat(["typeof ", path.call(print, "exprName")]);

    case "TSParameterProperty":
      if (n.accessibility) {
        parts.push(n.accessibility, " ");
      }

      if (n.export) {
        parts.push("export ");
      }

      if (n.static) {
        parts.push("static ");
      }

      if (n.readonly) {
        parts.push("readonly ");
      }

      parts.push(path.call(print, "parameter"));

      return concat(parts);

    case "TSTypeReference":
      return concat([
        path.call(print, "typeName"),
        path.call(print, "typeParameters"),
      ]);

    case "TSQualifiedName":
      return concat([path.call(print, "left"), ".", path.call(print, "right")]);

    case "TSAsExpression":
    case "TSSatisfiesExpression":
    {
      const expression = path.call(print, "expression");
      parts.push(
        expression,
        n.type === "TSSatisfiesExpression" ? " satisfies " : " as ",
        path.call(print, "typeAnnotation"),
      );
      return concat(parts);
    }

    case "TSTypeCastExpression":
      return concat([
        path.call(print, "expression"),
        path.call(print, "typeAnnotation"),
      ]);

    case "TSNonNullExpression":
      return concat([path.call(print, "expression"), "!"]);

    case "TSTypeAnnotation":
      return concat([": ", path.call(print, "typeAnnotation")]);

    case "TSIndexSignature":
      return concat([
        n.readonly ? "readonly " : "",
        "[",
        path.map(print, "parameters"),
        "]",
        path.call(print, "typeAnnotation"),
      ]);

    case "TSPropertySignature":
      parts.push(printVariance(path, print), n.readonly ? "readonly " : "");

      if (n.computed) {
        parts.push("[", path.call(print, "key"), "]");
      } else {
        parts.push(path.call(print, "key"));
      }

      parts.push(n.optional ? "?" : "", path.call(print, "typeAnnotation"));

      return concat(parts);

    case "TSMethodSignature":
      if (n.computed) {
        parts.push("[", path.call(print, "key"), "]");
      } else {
        parts.push(path.call(print, "key"));
      }

      if (n.optional) {
        parts.push("?");
      }

      parts.push(
        path.call(print, "typeParameters"),
        "(",
        printFunctionParams(path, options, print),
        ")",
        path.call(print, "typeAnnotation"),
      );

      return concat(parts);

    case "TSTypePredicate":
      if (n.asserts) {
        parts.push("asserts ");
      }

      parts.push(path.call(print, "parameterName"));

      if (n.typeAnnotation) {
        parts.push(
          " is ",
          path.call(print, "typeAnnotation", "typeAnnotation"),
        );
      }

      return concat(parts);

    case "TSCallSignatureDeclaration":
      return concat([
        path.call(print, "typeParameters"),
        "(",
        printFunctionParams(path, options, print),
        ")",
        path.call(print, "typeAnnotation"),
      ]);

    case "TSConstructSignatureDeclaration":
      if (n.typeParameters) {
        parts.push("new", path.call(print, "typeParameters"));
      } else {
        parts.push("new ");
      }

      parts.push(
        "(",
        printFunctionParams(path, options, print),
        ")",
        path.call(print, "typeAnnotation"),
      );

      return concat(parts);

    case "TSTypeAliasDeclaration":
      return concat([
        n.declare ? "declare " : "",
        "type ",
        path.call(print, "id"),
        path.call(print, "typeParameters"),
        " = ",
        path.call(print, "typeAnnotation"),
        ";",
      ]);

    case "TSTypeParameter": {
      parts.push(path.call(print, "name"));

      // ambiguous because of TSMappedType
      const parent = path.getParentNode(0);
      const isInMappedType = namedTypes.TSMappedType.check(parent);

      if (n.constraint) {
        parts.push(
          isInMappedType ? " in " : " extends ",
          path.call(print, "constraint"),
        );
      }

      if (n["default"]) {
        parts.push(" = ", path.call(print, "default"));
      }

      return concat(parts);
    }

    case "TSTypeAssertion": {
      parts.push(
        "<",
        path.call(print, "typeAnnotation"),
        "> ",
        path.call(print, "expression"),
      );
      return concat(parts);
    }

    case "TSTypeParameterDeclaration":
    case "TSTypeParameterInstantiation":
      return concat([
        "<",
        fromString(", ").join(path.map(print, "params")),
        ">",
      ]);

    case "TSEnumDeclaration": {
      parts.push(
        n.declare ? "declare " : "",
        n.const ? "const " : "",
        "enum ",
        path.call(print, "id"),
      );

      const memberLines = fromString(",\n").join(path.map(print, "members"));

      if (memberLines.isEmpty()) {
        parts.push(" {}");
      } else {
        parts.push(" {\n", memberLines.indent(options.tabWidth), "\n}");
      }

      return concat(parts);
    }

    case "TSExpressionWithTypeArguments":
      return concat([
        path.call(print, "expression"),
        path.call(print, "typeParameters"),
      ]);

    case "TSInterfaceBody": {
      const lines = fromString("\n").join(
        path.map(print, "body").map((element: Lines) => {
          if (lastNonSpaceCharacter(element) !== ";") {
            return element.concat(";");
          }
          return element;
        })
      );
      if (lines.isEmpty()) {
        return fromString("{}", options);
      }
      return concat(["{\n", lines.indent(options.tabWidth), "\n}"]);
    }

    case "TSImportType":
      parts.push("import(", path.call(print, "argument"), ")");

      if (n.qualifier) {
        parts.push(".", path.call(print, "qualifier"));
      }

      if (n.typeParameters) {
        parts.push(path.call(print, "typeParameters"));
      }

      return concat(parts);

    case "TSImportEqualsDeclaration":
      if (n.isExport) {
        parts.push("export ");
      }

      parts.push(
        "import ",
        path.call(print, "id"),
        " = ",
        path.call(print, "moduleReference"),
      );

      return maybeAddSemicolon(concat(parts));

    case "TSExternalModuleReference":
      return concat(["require(", path.call(print, "expression"), ")"]);

    case "TSModuleDeclaration": {
      const parent = path.getParentNode();

      if (parent.type === "TSModuleDeclaration") {
        parts.push(".");
      } else {
        if (n.declare) {
          parts.push("declare ");
        }

        if (!n.global) {
          const isExternal =
            n.id.type === "StringLiteral" ||
            (n.id.type === "Literal" && typeof n.id.value === "string");

          if (isExternal) {
            parts.push("module ");
          } else if (n.loc && n.loc.lines && n.id.loc) {
            const prefix = n.loc.lines.sliceString(n.loc.start, n.id.loc.start);

            // These keywords are fundamentally ambiguous in the
            // Babylon parser, and not reflected in the AST, so
            // the best we can do is to match the original code,
            // when possible.
            if (prefix.indexOf("module") >= 0) {
              parts.push("module ");
            } else {
              parts.push("namespace ");
            }
          } else {
            parts.push("namespace ");
          }
        }
      }

      parts.push(path.call(print, "id"));

      if (n.body) {
        parts.push(" ");
        parts.push(path.call(print, "body"));
      }

      return concat(parts);
    }

    case "TSModuleBlock": {
      const naked = path.call(
        (bodyPath: any) => printStatementSequence(bodyPath, options, print),
        "body",
      );

      if (naked.isEmpty()) {
        parts.push("{}");
      } else {
        parts.push("{\n", naked.indent(options.tabWidth), "\n}");
      }

      return concat(parts);
    }

    case "TSInstantiationExpression": {
      parts.push(
        path.call(print, "expression"),
        path.call(print, "typeParameters"),
      );

      return concat(parts);
    }


    // https://github.com/babel/babel/pull/10148
    case "V8IntrinsicIdentifier":
      return concat(["%", path.call(print, "name")]);

    // https://github.com/babel/babel/pull/13191
    case "TopicReference":
      return fromString("#");

    // Unhandled types below. If encountered, nodes of these types should
    // be either left alone or desugared into AST types that are fully
    // supported by the pretty-printer.
    case "ClassHeritage": // TODO
    case "ComprehensionBlock": // TODO
    case "ComprehensionExpression": // TODO
    case "Glob": // TODO
    case "GeneratorExpression": // TODO
    case "LetStatement": // TODO
    case "LetExpression": // TODO
    case "GraphExpression": // TODO
    case "GraphIndexExpression": // TODO
    case "XMLDefaultDeclaration":
    case "XMLAnyName":
    case "XMLQualifiedIdentifier":
    case "XMLFunctionQualifiedIdentifier":
    case "XMLAttributeSelector":
    case "XMLFilterExpression":
    case "XML":
    case "XMLElement":
    case "XMLList":
    case "XMLEscape":
    case "XMLText":
    case "XMLStartTag":
    case "XMLEndTag":
    case "XMLPointTag":
    case "XMLName":
    case "XMLAttribute":
    case "XMLCdata":
    case "XMLComment":
    case "XMLProcessingInstruction":
    default:
      debugger;
      throw new Error("unknown type: " + JSON.stringify(n.type));
  }
}

function printDecorators(path: any, printPath: any) {
  const parts: any[] = [];
  const node = path.getValue();

  if (
    node.decorators &&
    node.decorators.length > 0 &&
    // If the parent node is an export declaration, it will be
    // responsible for printing node.decorators.
    !util.getParentExportDeclaration(path)
  ) {
    path.each(function (decoratorPath: any) {
      parts.push(printPath(decoratorPath), "\n");
    }, "decorators");
  } else if (
    util.isExportDeclaration(node) &&
    node.declaration &&
    node.declaration.decorators
  ) {
    // Export declarations are responsible for printing any decorators
    // that logically apply to node.declaration.
    path.each(
      function (decoratorPath: any) {
        parts.push(printPath(decoratorPath), "\n");
      },
      "declaration",
      "decorators",
    );
  }

  return concat(parts);
}

function printStatementSequence(path: any, options: any, print: any) {
  const filtered: any[] = [];
  let sawComment = false;
  let sawStatement = false;

  path.each(function (stmtPath: any) {
    const stmt = stmtPath.getValue();

    // Just in case the AST has been modified to contain falsy
    // "statements," it's safer simply to skip them.
    if (!stmt) {
      return;
    }

    // Skip printing EmptyStatement nodes to avoid leaving stray
    // semicolons lying around.
    if (
      stmt.type === "EmptyStatement" &&
      !(stmt.comments && stmt.comments.length > 0)
    ) {
      return;
    }

    if (namedTypes.Comment.check(stmt)) {
      // The pretty printer allows a dangling Comment node to act as
      // a Statement when the Comment can't be attached to any other
      // non-Comment node in the tree.
      sawComment = true;
    } else if (namedTypes.Statement.check(stmt)) {
      sawStatement = true;
    } else {
      // When the pretty printer encounters a string instead of an
      // AST node, it just prints the string. This behavior can be
      // useful for fine-grained formatting decisions like inserting
      // blank lines.
      isString.assert(stmt);
    }

    // We can't hang onto stmtPath outside of this function, because
    // it's just a reference to a mutable FastPath object, so we have
    // to go ahead and print it here.
    filtered.push({
      node: stmt,
      printed: print(stmtPath),
    });
  });

  if (sawComment) {
    assert.strictEqual(
      sawStatement,
      false,
      "Comments may appear as statements in otherwise empty statement " +
        "lists, but may not coexist with non-Comment nodes.",
    );
  }

  let prevTrailingSpace: any = null;
  const len = filtered.length;
  const parts: any[] = [];

  filtered.forEach(function (info, i) {
    const printed = info.printed;
    const stmt = info.node;
    const multiLine = printed.length > 1;
    const notFirst = i > 0;
    const notLast = i < len - 1;
    let leadingSpace;
    let trailingSpace;
    const lines = stmt && stmt.loc && stmt.loc.lines;
    const trueLoc =
      lines && options.reuseWhitespace && util.getTrueLoc(stmt, lines);

    if (notFirst) {
      if (trueLoc) {
        const beforeStart = lines.skipSpaces(trueLoc.start, true);
        const beforeStartLine = beforeStart ? beforeStart.line : 1;
        const leadingGap = trueLoc.start.line - beforeStartLine;
        leadingSpace = Array(leadingGap + 1).join("\n");
      } else {
        leadingSpace = multiLine ? "\n\n" : "\n";
      }
    } else {
      leadingSpace = "";
    }

    if (notLast) {
      if (trueLoc) {
        const afterEnd = lines.skipSpaces(trueLoc.end);
        const afterEndLine = afterEnd ? afterEnd.line : lines.length;
        const trailingGap = afterEndLine - trueLoc.end.line;
        trailingSpace = Array(trailingGap + 1).join("\n");
      } else {
        trailingSpace = multiLine ? "\n\n" : "\n";
      }
    } else {
      trailingSpace = "";
    }

    parts.push(maxSpace(prevTrailingSpace, leadingSpace), printed);

    if (notLast) {
      prevTrailingSpace = trailingSpace;
    } else if (trailingSpace) {
      parts.push(trailingSpace);
    }
  });

  return concat(parts);
}

function maxSpace(s1: any, s2: any) {
  if (!s1 && !s2) {
    return fromString("");
  }

  if (!s1) {
    return fromString(s2);
  }

  if (!s2) {
    return fromString(s1);
  }

  const spaceLines1 = fromString(s1);
  const spaceLines2 = fromString(s2);

  if (spaceLines2.length > spaceLines1.length) {
    return spaceLines2;
  }

  return spaceLines1;
}

function printClassMemberModifiers(node: any): string[] {
  const parts = [];

  if (node.declare) {
    parts.push("declare ");
  }

  const access = node.accessibility || node.access;
  if (typeof access === "string") {
    parts.push(access, " ");
  }

  if (node.static) {
    parts.push("static ");
  }

  if (node.override) {
    parts.push("override ");
  }

  if (node.abstract) {
    parts.push("abstract ");
  }

  if (node.readonly) {
    parts.push("readonly ");
  }

  return parts;
}

function printMethod(path: any, options: any, print: any) {
  const node = path.getNode();
  const kind = node.kind;
  const parts = [];

  let nodeValue = node.value;
  if (!namedTypes.FunctionExpression.check(nodeValue)) {
    nodeValue = node;
  }

  parts.push(...printClassMemberModifiers(node));

  if (nodeValue.async) {
    parts.push("async ");
  }

  if (nodeValue.generator) {
    parts.push("*");
  }

  if (kind === "get" || kind === "set") {
    parts.push(kind, " ");
  }

  let key = path.call(print, "key");
  if (node.computed) {
    key = concat(["[", key, "]"]);
  }

  parts.push(key);

  if (node.optional) {
    parts.push("?");
  }

  if (node === nodeValue) {
    parts.push(
      path.call(print, "typeParameters"),
      "(",
      printFunctionParams(path, options, print),
      ")",
      path.call(print, "returnType"),
    );

    if (node.body) {
      parts.push(" ", path.call(print, "body"));
    } else {
      parts.push(";");
    }
  } else {
    parts.push(
      path.call(print, "value", "typeParameters"),
      "(",
      path.call(
        (valuePath: any) => printFunctionParams(valuePath, options, print),
        "value",
      ),
      ")",
      path.call(print, "value", "returnType"),
    );

    if (nodeValue.body) {
      parts.push(" ", path.call(print, "value", "body"));
    } else {
      parts.push(";");
    }
  }

  return concat(parts);
}

function printArgumentsList(path: any, options: any, print: any) {
  const printed = path.map(print, "arguments");
  const trailingComma = util.isTrailingCommaEnabled(options, "parameters");

  let joined = fromString(", ").join(printed);
  if (joined.getLineLength(1) > options.wrapColumn) {
    joined = fromString(",\n").join(printed);
    return concat([
      "(\n",
      joined.indent(options.tabWidth),
      trailingComma ? ",\n)" : "\n)",
    ]);
  }

  return concat(["(", joined, ")"]);
}

function printFunctionParams(path: any, options: any, print: any) {
  const fun = path.getValue();

  let params;
  let printed: Array<Lines> = [];
  if (fun.params) {
    params = fun.params;
    printed = path.map(print, "params");
  } else if (fun.parameters) {
    params = fun.parameters;
    printed = path.map(print, "parameters");
  }

  if (fun.defaults) {
    path.each(function (defExprPath: any) {
      const i = defExprPath.getName();
      const p = printed[i];
      if (p && defExprPath.getValue()) {
        printed[i] = concat([p, " = ", print(defExprPath)]);
      }
    }, "defaults");
  }

  if (fun.rest) {
    printed.push(concat(["...", path.call(print, "rest")]));
  }

  let joined = fromString(", ").join(printed);
  if (joined.length > 1 || joined.getLineLength(1) > options.wrapColumn) {
    joined = fromString(",\n").join(printed);
    if (
      util.isTrailingCommaEnabled(options, "parameters") &&
      !fun.rest &&
      params[params.length - 1].type !== "RestElement"
    ) {
      joined = concat([joined, ",\n"]);
    } else {
      joined = concat([joined, "\n"]);
    }
    return concat(["\n", joined.indent(options.tabWidth)]);
  }

  return joined;
}

function maybePrintImportAssertions(
  path: any,
  options: any,
  print: any,
): Lines {
  const n = path.getValue();
  if (n.assertions && n.assertions.length > 0) {
    const parts: (string | Lines)[] = [" assert {"];
    const printed = path.map(print, "assertions");
    const flat = fromString(", ").join(printed);
    if (flat.length > 1 || flat.getLineLength(1) > options.wrapColumn) {
      parts.push(
        "\n",
        fromString(",\n").join(printed).indent(options.tabWidth),
        "\n}",
      );
    } else {
      parts.push(" ", flat, " }");
    }
    return concat(parts);
  }
  return fromString("");
}

function printExportDeclaration(path: any, options: any, print: any) {
  const decl = path.getValue();
  const parts: (string | Lines)[] = ["export "];
  if (decl.exportKind && decl.exportKind === "type") {
    if (!decl.declaration) {
      parts.push("type ");
    }
  }
  const shouldPrintSpaces = options.objectCurlySpacing;

  namedTypes.Declaration.assert(decl);

  if (decl["default"] || decl.type === "ExportDefaultDeclaration") {
    parts.push("default ");
  }

  if (decl.declaration) {
    parts.push(path.call(print, "declaration"));
  } else if (decl.specifiers) {
    if (
      decl.specifiers.length === 1 &&
      decl.specifiers[0].type === "ExportBatchSpecifier"
    ) {
      parts.push("*");
    } else if (decl.specifiers.length === 0) {
      parts.push("{}");
    } else if (decl.specifiers[0].type === "ExportDefaultSpecifier") {
      const unbracedSpecifiers: any[] = [];
      const bracedSpecifiers: any[] = [];

      path.each(function (specifierPath: any) {
        const spec = specifierPath.getValue();
        if (spec.type === "ExportDefaultSpecifier") {
          unbracedSpecifiers.push(print(specifierPath));
        } else {
          bracedSpecifiers.push(print(specifierPath));
        }
      }, "specifiers");

      unbracedSpecifiers.forEach((lines, i) => {
        if (i > 0) {
          parts.push(", ");
        }
        parts.push(lines);
      });

      if (bracedSpecifiers.length > 0) {
        let lines = fromString(", ").join(bracedSpecifiers);
        if (lines.getLineLength(1) > options.wrapColumn) {
          lines = concat([
            fromString(",\n").join(bracedSpecifiers).indent(options.tabWidth),
            ",",
          ]);
        }

        if (unbracedSpecifiers.length > 0) {
          parts.push(", ");
        }

        if (lines.length > 1) {
          parts.push("{\n", lines, "\n}");
        } else if (options.objectCurlySpacing) {
          parts.push("{ ", lines, " }");
        } else {
          parts.push("{", lines, "}");
        }
      }
    } else {
      parts.push(
        shouldPrintSpaces ? "{ " : "{",
        fromString(", ").join(path.map(print, "specifiers")),
        shouldPrintSpaces ? " }" : "}",
      );
    }

    if (decl.source) {
      parts.push(
        " from ",
        path.call(print, "source"),
        maybePrintImportAssertions(path, options, print),
      );
    }
  }

  let lines = concat(parts);
  if (
    lastNonSpaceCharacter(lines) !== ";" &&
    !(
      decl.declaration &&
      (decl.declaration.type === "FunctionDeclaration" ||
        decl.declaration.type === "ClassDeclaration" ||
        decl.declaration.type === "TSModuleDeclaration" ||
        decl.declaration.type === "TSInterfaceDeclaration" ||
        decl.declaration.type === "TSEnumDeclaration")
    )
  ) {
    lines = concat([lines, ";"]);
  }
  return lines;
}

function printFlowDeclaration(path: any, parts: any) {
  const parentExportDecl = util.getParentExportDeclaration(path);

  if (parentExportDecl) {
    assert.strictEqual(parentExportDecl.type, "DeclareExportDeclaration");
  } else {
    // If the parent node has type DeclareExportDeclaration, then it
    // will be responsible for printing the "declare" token. Otherwise
    // it needs to be printed with this non-exported declaration node.
    parts.unshift("declare ");
  }

  return concat(parts);
}

function printVariance(path: any, print: any) {
  return path.call(function (variancePath: any) {
    const value = variancePath.getValue();

    if (value) {
      if (value === "plus") {
        return fromString("+");
      }

      if (value === "minus") {
        return fromString("-");
      }

      return print(variancePath);
    }

    return fromString("");
  }, "variance");
}

function adjustClause(clause: any, options: any) {
  if (clause.length > 1) return concat([" ", clause]);

  return concat(["\n", maybeAddSemicolon(clause).indent(options.tabWidth)]);
}

function lastNonSpaceCharacter(lines: any) {
  const pos = lines.lastPos();
  do {
    const ch = lines.charAt(pos);
    if (/\S/.test(ch)) return ch;
  } while (lines.prevPos(pos));
}

function endsWithBrace(lines: any) {
  return lastNonSpaceCharacter(lines) === "}";
}

function swapQuotes(str: string) {
  return str.replace(/['"]/g, (m) => (m === '"' ? "'" : '"'));
}

function getPossibleRaw(
  node:
    | types.namedTypes.Literal
    | types.namedTypes.NumericLiteral
    | types.namedTypes.StringLiteral
    | types.namedTypes.RegExpLiteral
    | types.namedTypes.BigIntLiteral
    | types.namedTypes.DecimalLiteral,
): string | void {
  const value = types.getFieldValue(node, "value");
  const extra = types.getFieldValue(node, "extra");

  if (extra && typeof extra.raw === "string" && value == extra.rawValue) {
    return extra.raw;
  }

  if (node.type === "Literal") {
    const raw = (node as typeof extra).raw;
    if (typeof raw === "string" && value == raw) {
      return raw;
    }
  }
}

function jsSafeStringify(str: string) {
  return JSON.stringify(str).replace(/[\u2028\u2029]/g, function (m) {
    return "\\u" + m.charCodeAt(0).toString(16);
  });
}

function nodeStr(str: string, options: any) {
  isString.assert(str);
  switch (options.quote) {
    case "auto": {
      const double = jsSafeStringify(str);
      const single = swapQuotes(jsSafeStringify(swapQuotes(str)));
      return double.length > single.length ? single : double;
    }
    case "single":
      return swapQuotes(jsSafeStringify(swapQuotes(str)));
    case "double":
    default:
      return jsSafeStringify(str);
  }
}

function maybeAddSemicolon(lines: any) {
  const eoc = lastNonSpaceCharacter(lines);
  if (!eoc || "\n};".indexOf(eoc) < 0) return concat([lines, ";"]);
  return lines;
}
