import assert from "assert";
import * as types from "ast-types";
import { EOL as eol } from "os";
import { fromString } from "../lib/lines";
import { parse } from "../lib/parser";
import { Printer } from "../lib/printer";
import * as recast from "../main";
const n = types.namedTypes;
const b = types.builders;
const linesModule = require("../lib/lines");
const nodeMajorVersion = parseInt(process.versions.node, 10);

import * as tsParser from "../parsers/typescript";

describe("printer", function () {
  it("Printer", function testPrinter(done) {
    const code = testPrinter + "";
    const ast = parse(code);
    const printer = new Printer();

    assert.strictEqual(typeof printer.print, "function");
    assert.strictEqual(printer.print(null).code, "");

    let string = printer.printGenerically(ast).code;
    assert.ok(string.indexOf("done();") > 0);

    string = printer.print(ast).code;

    // TODO

    assert.ok(string.indexOf("// TODO") > 0);

    done();
  });

  const uselessSemicolons = [
    "function a() {",
    '  return "a";',
    "};",
    "",
    "function b() {",
    '  return "b";',
    "};",
  ].join(eol);

  it("EmptyStatements", function () {
    const ast = parse(uselessSemicolons);
    const printer = new Printer({ tabWidth: 2 });

    const reprinted = printer.print(ast).code;
    assert.strictEqual(typeof reprinted, "string");
    assert.strictEqual(reprinted, uselessSemicolons);

    const generic = printer.printGenerically(ast).code;
    const withoutTrailingSemicolons = uselessSemicolons.replace(/\};/g, "}");
    assert.strictEqual(typeof generic, "string");
    assert.strictEqual(generic, withoutTrailingSemicolons);
  });

  const importantSemicolons = [
    "var a = {};", // <--- this trailing semi-colon is very important
    "(function() {})();",
  ].join(eol);

  it("IffeAfterVariableDeclarationEndingInObjectLiteral", function () {
    const ast = parse(importantSemicolons);
    const printer = new Printer({ tabWidth: 2 });

    const reprinted = printer.printGenerically(ast).code;
    assert.strictEqual(typeof reprinted, "string");
    assert.strictEqual(reprinted, importantSemicolons);
  });

  const arrayExprWithTrailingComma = "[1, 2,];";
  const arrayExprWithoutTrailingComma = "[1, 2];";

  it("ArrayExpressionWithTrailingComma", function () {
    const ast = parse(arrayExprWithTrailingComma);
    const printer = new Printer({ tabWidth: 2 });

    const body = ast.program.body;
    const arrayExpr = body[0].expression;
    n.ArrayExpression.assert(arrayExpr);

    // This causes the array expression to be reprinted.
    const arrayExprOrig = arrayExpr.original;
    arrayExpr.original = null;

    assert.strictEqual(printer.print(ast).code, arrayExprWithoutTrailingComma);

    arrayExpr.original = arrayExprOrig;

    assert.strictEqual(printer.print(ast).code, arrayExprWithTrailingComma);
  });

  const arrayExprWithHoles = "[,,];";

  it("ArrayExpressionWithHoles", function () {
    const ast = parse(arrayExprWithHoles);
    const printer = new Printer({ tabWidth: 2 });

    const body = ast.program.body;
    const arrayExpr = body[0].expression;
    n.ArrayExpression.assert(arrayExpr);

    // This causes the array expression to be reprinted.
    const arrayExprOrig = arrayExpr.original;
    arrayExpr.original = null;

    assert.strictEqual(printer.print(ast).code, arrayExprWithHoles);

    arrayExpr.original = arrayExprOrig;

    assert.strictEqual(printer.print(ast).code, arrayExprWithHoles);
  });

  const objectExprWithTrailingComma = "({x: 1, y: 2,});";
  const objectExprWithoutTrailingComma =
    "({" + eol + "  x: 1," + eol + "  y: 2" + eol + "});";

  it("ArrayExpressionWithTrailingComma", function () {
    const ast = parse(objectExprWithTrailingComma);
    const printer = new Printer({ tabWidth: 2 });

    const body = ast.program.body;
    const objectExpr = body[0].expression;
    n.ObjectExpression.assert(objectExpr);

    // This causes the array expression to be reprinted.
    const objectExprOrig = objectExpr.original;
    objectExpr.original = null;

    assert.strictEqual(printer.print(ast).code, objectExprWithoutTrailingComma);

    objectExpr.original = objectExprOrig;

    assert.strictEqual(printer.print(ast).code, objectExprWithTrailingComma);
  });

  const switchCase = [
    "switch (test) {",
    "  default:",
    "  case a: break",
    "",
    "  case b:",
    "    break;",
    "}",
  ].join(eol);

  const switchCaseReprinted = [
    "if (test) {",
    "  switch (test) {",
    "  default:",
    "  case a: break",
    "  case b:",
    "    break;",
    "  }",
    "}",
  ].join(eol);

  const switchCaseGeneric = [
    "if (test) {",
    "  switch (test) {",
    "  default:",
    "  case a:",
    "    break;",
    "  case b:",
    "    break;",
    "  }",
    "}",
  ].join(eol);

  it("SwitchCase", function () {
    const ast = parse(switchCase);
    const printer = new Printer({ tabWidth: 2 });

    const body = ast.program.body;
    const switchStmt = body[0];
    n.SwitchStatement.assert(switchStmt);

    // This causes the switch statement to be reprinted.
    switchStmt.original = null;

    body[0] = b.ifStatement(
      b.identifier("test"),
      b.blockStatement([switchStmt]),
    );

    assert.strictEqual(printer.print(ast).code, switchCaseReprinted);

    assert.strictEqual(printer.printGenerically(ast).code, switchCaseGeneric);
  });

  const tryCatch = ["try {", "  a();", "} catch (e) {", "  b(e);", "}"].join(
    eol,
  );

  it("IndentTryCatch", function () {
    const ast = parse(tryCatch);
    const printer = new Printer({ tabWidth: 2 });
    const body = ast.program.body;
    const tryStmt = body[0];
    n.TryStatement.assert(tryStmt);

    // Force reprinting.
    assert.strictEqual(printer.printGenerically(ast).code, tryCatch);
  });

  const classBody = [
    "class A {",
    "  foo(x) { return x }",
    "  bar(y) { this.foo(y); }",
    "  baz(x, y) {",
    "    this.foo(x);",
    "    this.bar(y);",
    "  }",
    "}",
  ];

  const classBodyExpected = [
    "class A {",
    "  foo(x) { return x }",
    "  bar(y) { this.foo(y); }",
    "  baz(x, y) {",
    "    this.foo(x);",
    "    this.bar(y);",
    "  }",
    "  foo(x) { return x }",
    "}",
  ];

  it("MethodPrinting", function () {
    const code = classBody.join(eol);
    let ast;
    try {
      ast = parse(code);
    } catch (e) {
      // ES6 not supported, silently finish
      return;
    }
    const printer = new Printer({ tabWidth: 2 });
    const cb = ast.program.body[0].body;
    n.ClassBody.assert(cb);

    // Trigger reprinting of the class body.
    cb.body.push(cb.body[0]);

    assert.strictEqual(printer.print(ast).code, classBodyExpected.join(eol));
  });

  const multiLineParams = [
    "function f(/* first",
    "              xxx",
    "              param */ a,",
    "  // other params",
    "  b, c, // see?",
    "  d",
    ") {",
    "  return a + b + c + d;",
    "}",
  ];

  const multiLineParamsExpected = [
    "function f(",
    "  /* first",
    "     xxx",
    "     param */ a,",
    "  // other params",
    "  b,",
    "  // see?",
    "  c,",
    "  d",
    ") {",
    "  return a + b + c + d;",
    "}",
  ];

  it("MultiLineParams", function () {
    const code = multiLineParams.join(eol);
    const ast = parse(code);
    const printer = new Printer({ tabWidth: 2 });

    recast.visit(ast, {
      visitNode: function (path) {
        path.value.original = null;
        this.traverse(path);
      },
    });

    assert.strictEqual(
      printer.print(ast).code,
      multiLineParamsExpected.join(eol),
    );
  });

  it("SimpleVarPrinting", function () {
    const printer = new Printer({ tabWidth: 2 });
    const varDecl = b.variableDeclaration("var", [
      b.variableDeclarator(b.identifier("x"), null),
      b.variableDeclarator(b.identifier("y"), null),
      b.variableDeclarator(b.identifier("z"), null),
    ]);

    assert.strictEqual(
      printer.print(b.program([varDecl])).code,
      "var x, y, z;",
    );

    const z: any = varDecl.declarations.pop();
    varDecl.declarations.pop();
    varDecl.declarations.push(z);

    assert.strictEqual(printer.print(b.program([varDecl])).code, "var x, z;");
  });

  it("MultiLineVarPrinting", function () {
    const printer = new Printer({ tabWidth: 2 });
    const varDecl = b.variableDeclaration("var", [
      b.variableDeclarator(b.identifier("x"), null),
      b.variableDeclarator(
        b.identifier("y"),
        b.objectExpression([
          b.property("init", b.identifier("why"), b.literal("not")),
        ]),
      ),
      b.variableDeclarator(b.identifier("z"), null),
    ]);

    assert.strictEqual(
      printer.print(b.program([varDecl])).code,
      ["var x,", "    y = {", '      why: "not"', "    },", "    z;"].join(eol),
    );
  });

  it("ForLoopPrinting", function () {
    const printer = new Printer({ tabWidth: 2 });
    const loop = b.forStatement(
      b.variableDeclaration("var", [
        b.variableDeclarator(b.identifier("i"), b.literal(0)),
      ]),
      b.binaryExpression("<", b.identifier("i"), b.literal(3)),
      b.updateExpression("++", b.identifier("i"), /* prefix: */ false),
      b.expressionStatement(
        b.callExpression(b.identifier("log"), [b.identifier("i")]),
      ),
    );

    assert.strictEqual(
      printer.print(loop).code,
      "for (var i = 0; i < 3; i++)" + eol + "  log(i);",
    );
  });

  it("EmptyForLoopPrinting", function () {
    const printer = new Printer({ tabWidth: 2 });
    const loop = b.forStatement(
      b.variableDeclaration("var", [
        b.variableDeclarator(b.identifier("i"), b.literal(0)),
      ]),
      b.binaryExpression("<", b.identifier("i"), b.literal(3)),
      b.updateExpression("++", b.identifier("i"), /* prefix: */ false),
      b.emptyStatement(),
    );

    assert.strictEqual(
      printer.print(loop).code,
      "for (var i = 0; i < 3; i++)" + eol + "  ;",
    );
  });

  it("ForInLoopPrinting", function () {
    const printer = new Printer({ tabWidth: 2 });
    const loop = b.forInStatement(
      b.variableDeclaration("var", [
        b.variableDeclarator(b.identifier("key"), null),
      ]),
      b.identifier("obj"),
      b.expressionStatement(
        b.callExpression(b.identifier("log"), [b.identifier("key")]),
      ),
    );

    assert.strictEqual(
      printer.print(loop).code,
      "for (var key in obj)" + eol + "  log(key);",
    );
  });

  it("GuessTabWidth", function () {
    const code = ["function identity(x) {", "  return x;", "}"].join(eol);

    const guessedTwo = [
      "function identity(x) {",
      "  log(x);",
      "  return x;",
      "}",
    ].join(eol);

    const explicitFour = [
      "function identity(x) {",
      "    log(x);",
      "    return x;",
      "}",
    ].join(eol);

    const ast = parse(code);

    const funDecl = ast.program.body[0];
    n.FunctionDeclaration.assert(funDecl);

    const funBody = funDecl.body.body;

    funBody.unshift(
      b.expressionStatement(
        b.callExpression(b.identifier("log"), funDecl.params),
      ),
    );

    assert.strictEqual(new Printer().print(ast).code, guessedTwo);

    assert.strictEqual(
      new Printer({
        tabWidth: 4,
      }).print(ast).code,
      explicitFour,
    );
  });

  it("FunctionDefaultsAndRest", function () {
    const printer = new Printer();
    const funExpr = b.functionExpression(
      b.identifier("a"),
      [b.identifier("b"), b.identifier("c")],
      b.blockStatement([]),
      false,
      false,
    );

    funExpr.defaults = [null, b.literal(1)];
    funExpr.rest = b.identifier("d");

    assert.strictEqual(
      printer.print(funExpr).code,
      "function a(b, c = 1, ...d) {}",
    );

    const arrowFunExpr = b.arrowFunctionExpression(
      [b.identifier("b"), b.identifier("c")],
      b.blockStatement([]),
      false,
    );

    arrowFunExpr.defaults = [null, b.literal(1)];
    arrowFunExpr.rest = b.identifier("d");

    assert.strictEqual(
      printer.print(arrowFunExpr).code,
      "(b, c = 1, ...d) => {}",
    );
  });

  it("generically prints parsed code and generated code the same way", function () {
    const printer = new Printer();
    const ast = b.program([
      b.expressionStatement(b.literal(1)),
      b.expressionStatement(b.literal(2)),
    ]);

    assert.strictEqual(
      printer.printGenerically(parse("1; 2;")).code,
      printer.printGenerically(ast).code,
    );
  });

  it("ExportDeclaration semicolons", function () {
    const printer = new Printer();
    let code = "export var foo = 42;";
    let ast = parse(code);

    assert.strictEqual(printer.print(ast).code, code);
    assert.strictEqual(printer.printGenerically(ast).code, code);

    code = "export var foo = 42";
    ast = parse(code);

    assert.strictEqual(printer.print(ast).code, code);
    assert.strictEqual(printer.printGenerically(ast).code, code + ";");

    code = "export function foo() {}";
    ast = parse(code);

    assert.strictEqual(printer.print(ast).code, code);
    assert.strictEqual(printer.printGenerically(ast).code, code);

    code = 'export * from "./lib";';
    ast = parse(code);

    assert.strictEqual(printer.print(ast).code, code);
    assert.strictEqual(printer.printGenerically(ast).code, code);
  });

  it("empty ExportDeclaration", function () {
    const printer = new Printer();
    const code = "export {};";
    const ast = parse(code);

    assert.strictEqual(printer.print(ast).code, code);
    assert.strictEqual(printer.printGenerically(ast).code, code);
  });

  it("export default of IIFE", function () {
    const printer = new Printer();
    let ast = b.exportDefaultDeclaration(
      b.callExpression(
        b.functionExpression(null, [], b.blockStatement([])),
        [],
      ),
    );
    const code = printer.print(ast).code;
    ast = parse(code);

    assert.strictEqual(printer.print(ast).code, code);
    assert.strictEqual(printer.printGenerically(ast).code, code);
  });

  const stmtListSpaces = [
    "",
    "var x = 1;",
    "",
    "",
    "// y summation",
    "var y = x + 1;",
    "var z = x + y;",
    "// after z",
    "",
    "console.log(x, y, z);",
    "",
    "",
  ].join(eol);

  const stmtListSpacesExpected = [
    "",
    "debugger;",
    "var x = 1;",
    "",
    "",
    "// y summation",
    "var y = x + 1;",
    "debugger;",
    "var z = x + y;",
    "// after z",
    "",
    "console.log(x, y, z);",
    "",
    "debugger;",
    "",
    "",
  ].join(eol);

  it("Statement list whitespace reuse", function () {
    const ast = parse(stmtListSpaces);
    const printer = new Printer({ tabWidth: 2 });
    const debugStmt = b.expressionStatement(b.identifier("debugger"));

    ast.program.body.splice(2, 0, debugStmt);
    ast.program.body.unshift(debugStmt);
    ast.program.body.push(debugStmt);

    assert.strictEqual(printer.print(ast).code, stmtListSpacesExpected);

    const funDecl = b.functionDeclaration(
      b.identifier("foo"),
      [],
      b.blockStatement(ast.program.body),
    );

    assert.strictEqual(
      printer.print(funDecl).code,
      linesModule
        .concat([
          "function foo() {" + eol,
          linesModule
            .fromString(stmtListSpacesExpected.replace(/^\s+|\s+$/g, ""))
            .indent(2),
          eol + "}",
        ])
        .toString(),
    );
  });

  it("should print static methods with the static keyword", function () {
    const printer = new Printer({ tabWidth: 4 });
    const ast = parse(["class A {", "  static foo() {}", "}"].join(eol));

    const classBody = ast.program.body[0].body;
    n.ClassBody.assert(classBody);

    const foo = classBody.body[0];
    n.MethodDefinition.assert(foo);

    classBody.body.push(foo);

    foo.key.name = "formerlyFoo";

    assert.strictEqual(
      printer.print(ast).code,
      [
        "class A {",
        "    static formerlyFoo() {}",
        "    static formerlyFoo() {}",
        "}",
      ].join(eol),
    );
  });

  it("should print string literals with the specified delimiter", function () {
    const ast = parse(
      [
        "var obj = {",
        "    \"foo's\": 'bar',",
        "    '\"bar\\'s\"': /regex/m",
        "};",
      ].join(eol),
    );

    const variableDeclaration = ast.program.body[0];
    n.VariableDeclaration.assert(variableDeclaration);

    const printer = new Printer({ quote: "single" });
    assert.strictEqual(
      printer.printGenerically(ast).code,
      [
        "var obj = {",
        "    'foo\\'s': 'bar',",
        "    '\"bar\\'s\"': /regex/m",
        "};",
      ].join(eol),
    );

    const printer2 = new Printer({ quote: "double" });
    assert.strictEqual(
      printer2.printGenerically(ast).code,
      [
        "var obj = {",
        '    "foo\'s": "bar",',
        '    "\\"bar\'s\\"": /regex/m',
        "};",
      ].join(eol),
    );

    const printer3 = new Printer({ quote: "auto" });
    assert.strictEqual(
      printer3.printGenerically(ast).code,
      [
        "var obj = {",
        '    "foo\'s": "bar",',
        "    '\"bar\\'s\"': /regex/m",
        "};",
      ].join(eol),
    );
  });

  it("pretty-prints U+2028 and U+2029 as Unicode escapes", function () {
    const ast = parse('"\\u2028";');
    const printer = new Printer();
    assert.strictEqual(printer.printGenerically(ast).code, '"\\u2028";');

    const ast2 = parse('"\\u2029";');
    const printer2 = new Printer();
    assert.strictEqual(printer2.printGenerically(ast2).code, '"\\u2029";');
  });

  it("should print block comments at head of class once", function () {
    // Given.
    const ast = parse(
      [
        "/**",
        " * This class was in an IIFE and returned an instance of itself.",
        " */",
        "function SimpleClass() {",
        "};",
      ].join(eol),
    );

    const classIdentifier = b.identifier("SimpleClass");
    const exportsExpression = b.memberExpression(
      b.identifier("module"),
      b.identifier("exports"),
      false,
    );
    const assignmentExpression = b.assignmentExpression(
      "=",
      exportsExpression,
      classIdentifier,
    );
    const exportStatement = b.expressionStatement(assignmentExpression);

    ast.program.body.push(exportStatement);

    // When.
    const printedClass = new Printer().print(ast).code;

    // Then.
    assert.strictEqual(
      printedClass,
      [
        "/**",
        " * This class was in an IIFE and returned an instance of itself.",
        " */",
        "function SimpleClass() {",
        "}",
        "module.exports = SimpleClass;",
      ].join(eol),
    );
  });

  it("should support computed properties", function () {
    let code = [
      "class A {",
      '  ["a"]() {}',
      '  [ID("b")]() {}',
      "  [0]() {}",
      "  [ID(1)]() {}",
      '  get ["a"]() {}',
      '  get [ID("b")]() {}',
      "  get [0]() {}",
      "  get [ID(1)]() {}",
      '  set ["a"](x) {}',
      '  set [ID("b")](x) {}',
      "  set [0](x) {}",
      "  set [ID(1)](x) {}",
      '  static ["a"]() {}',
      '  static [ID("b")]() {}',
      "  static [0]() {}",
      "  static [ID(1)]() {}",
      '  static get ["a"]() {}',
      '  static get [ID("b")]() {}',
      "  static get [0]() {}",
      "  static get [ID(1)]() {}",
      '  static set ["a"](x) {}',
      '  static set [ID("b")](x) {}',
      "  static set [0](x) {}",
      "  static set [ID(1)](x) {}",
      "}",
    ].join(eol);

    let ast = parse(code);

    const printer = new Printer({
      tabWidth: 2,
    });

    assert.strictEqual(printer.printGenerically(ast).code, code);

    code = [
      "var obj = {",
      '  ["a"]: 1,',
      '  [ID("b")]: 2,',
      "  [0]: 3,",
      "  [ID(1)]: 4,",
      '  ["a"]() {},',
      '  [ID("b")]() {},',
      "  [0]() {},",
      "  [ID(1)]() {},",
      '  get ["a"]() {},',
      '  get [ID("b")]() {},',
      "  get [0]() {},",
      "  get [ID(1)]() {},",
      '  set ["a"](x) {},',
      '  set [ID("b")](x) {},',
      "  set [0](x) {},",
      "  set [ID(1)](x) {}",
      "};",
    ].join(eol);

    ast = parse(code);

    assert.strictEqual(printer.printGenerically(ast).code, code);

    ast = parse(
      [
        "var o = {",
        "  // This foo will become a computed method name.",
        "  foo() { return bar }",
        "};",
      ].join(eol),
    );

    const objExpr = ast.program.body[0].declarations[0].init;
    n.ObjectExpression.assert(objExpr);

    assert.strictEqual(objExpr.properties[0].computed, false);
    objExpr.properties[0].computed = true;
    objExpr.properties[0].kind = "get";

    assert.strictEqual(
      recast.print(ast).code,
      [
        "var o = {",
        "  // This foo will become a computed method name.",
        "  get [foo]() { return bar }",
        "};",
      ].join(eol),
    );
  });

  it("prints trailing commas in object literals", function () {
    const code = ["({", "  foo: bar,", "  bar: foo,", "});"].join(eol);

    const ast = parse(code, {
      // Supports trailing commas whereas plain esprima does not.
      parser: require("esprima-fb"),
    });

    let printer = new Printer({
      tabWidth: 2,
      trailingComma: true,
    });

    let pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);

    // It should also work when using the `trailingComma` option as an object.
    printer = new Printer({
      tabWidth: 2,
      trailingComma: { objects: true },
    });

    pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints trailing commas in function calls", function () {
    const code = ["call(", "  1,", "  2,", ");"].join(eol);

    const ast = parse(code, {
      // Supports trailing commas whereas plain esprima does not.
      parser: require("esprima-fb"),
    });

    let printer = new Printer({
      tabWidth: 2,
      wrapColumn: 1,
      trailingComma: true,
    });

    let pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);

    // It should also work when using the `trailingComma` option as an object.
    printer = new Printer({
      tabWidth: 2,
      wrapColumn: 1,
      trailingComma: { parameters: true },
    });

    pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints trailing commas in array expressions", function () {
    const code = ["[", "  1,", "  2,", "];"].join(eol);

    const ast = parse(code, {
      // Supports trailing commas whereas plain esprima does not.
      parser: require("esprima-fb"),
    });

    let printer = new Printer({
      tabWidth: 2,
      wrapColumn: 1,
      trailingComma: true,
    });

    let pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);

    // It should also work when using the `trailingComma` option as an object.
    printer = new Printer({
      tabWidth: 2,
      wrapColumn: 1,
      trailingComma: { arrays: true },
    });

    pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints trailing commas in function definitions", function () {
    const code = ["function foo(", "  a,", "  b,", ") {}"].join(eol);

    const ast = parse(code, {
      // Supports trailing commas whereas plain esprima does not.
      parser: require("esprima-fb"),
    });

    let printer = new Printer({
      tabWidth: 2,
      wrapColumn: 1,
      trailingComma: true,
    });

    let pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);

    // It should also work when using the `trailingComma` option as an object.
    printer = new Printer({
      tabWidth: 2,
      wrapColumn: 1,
      trailingComma: { parameters: true },
    });

    pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  (nodeMajorVersion >= 6 ? it : xit)(
    "shouldn't print a trailing comma for a RestElement",
    function () {
      const code = ["function foo(", "  a,", "  b,", "  ...rest", ") {}"].join(
        eol,
      );

      const ast = parse(code, {
        // The flow parser and Babylon recognize `...rest` as a `RestElement`
        parser: require("@babel/parser"),
      });

      const printer = new Printer({
        tabWidth: 2,
        wrapColumn: 1,
        trailingComma: true,
      });

      const pretty = printer.printGenerically(ast).code;
      assert.strictEqual(pretty, code);
    },
  );

  it("shouldn't print a trailing comma for a RestElement in destructuring", function () {
    const code = [
      "const {",
      "  foo,",
      "  bar,",
      "  ...rest",
      "} = input;",
    ].join(eol);

    const ast = parse(code, {
      parser: require("@babel/parser"),
    });

    const printer = new Printer({
      tabWidth: 2,
      trailingComma: true,
    });

    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("should support AssignmentPattern and RestElement", function () {
    const code = [
      "function foo(a, [b, c] = d(a), ...[e, f, ...rest]) {",
      "  return [a, b, c, e, f, rest];",
      "}",
    ].join(eol);

    const ast = parse(code, {
      // Supports rest parameter destructuring whereas plain esprima
      // does not.
      parser: require("esprima-fb"),
    });

    const printer = new Printer({
      tabWidth: 2,
    });

    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("adds parenthesis around spread patterns", function () {
    const code = "(...rest) => rest;";

    let ast = b.program([
      b.expressionStatement(
        b.arrowFunctionExpression(
          [b.spreadElementPattern(b.identifier("rest"))],
          b.identifier("rest"),
          false,
        ),
      ),
    ]);

    const printer = new Printer({
      tabWidth: 2,
    });

    let pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);

    // Print RestElement the same way
    ast = b.program([
      b.expressionStatement(
        b.arrowFunctionExpression(
          [b.restElement(b.identifier("rest"))],
          b.identifier("rest"),
          false,
        ),
      ),
    ]);

    pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);

    // Do the same for the `rest` field.
    const arrowFunction = b.arrowFunctionExpression(
      [],
      b.identifier("rest"),
      false,
    );
    arrowFunction.rest = b.identifier("rest");
    ast = b.program([b.expressionStatement(arrowFunction)]);

    pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("adds parenthesis around single arrow function arg when options.arrowParensAlways is true", function () {
    const code = "(a) => {};";

    const fn = b.arrowFunctionExpression(
      [b.identifier("a")],
      b.blockStatement([]),
      false,
    );

    const ast = b.program([b.expressionStatement(fn)]);

    const printer = new Printer({
      arrowParensAlways: true,
    });
    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("adds parenthesis around arrow function when binding", function () {
    const code = "var a = (x => y).bind(z);";

    const fn = b.arrowFunctionExpression(
      [b.identifier("x")],
      b.identifier("y"),
    );

    const declaration = b.variableDeclaration("var", [
      b.variableDeclarator(
        b.identifier("a"),
        b.callExpression(b.memberExpression(fn, b.identifier("bind"), false), [
          b.identifier("z"),
        ]),
      ),
    ]);

    const ast = b.program([declaration]);

    const printer = new Printer();
    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("adds parenthesis around async arrow functions with args", function () {
    let code = "async () => {};";

    const fn = b.arrowFunctionExpression([], b.blockStatement([]), false);
    fn.async = true;

    const ast = b.program([b.expressionStatement(fn)]);

    const printer = new Printer({
      tabWidth: 2,
    });

    let pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);

    // No parenthesis for single params if they are identifiers
    code = "async foo => {};";
    fn.params = [b.identifier("foo")];

    pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);

    // Add parenthesis for destructuring
    code = "async ([a, b]) => {};";
    fn.params = [b.arrayPattern([b.identifier("a"), b.identifier("b")])];

    pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("adds parenthesis around arrow functions with single arg and a type", function () {
    const code = "(a: b) => {};";

    const arg = b.identifier("a");
    arg.typeAnnotation = b.typeAnnotation(
      b.genericTypeAnnotation(b.identifier("b"), null),
    );

    const fn = b.arrowFunctionExpression([arg], b.blockStatement([]), false);

    const ast = b.program([b.expressionStatement(fn)]);

    const printer = new Printer();
    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("adds parenthesis around arrow functions with single arg and a return type", function () {
    const code = "(a): void => {};";

    const arg = b.identifier("a");

    const fn = b.arrowFunctionExpression([arg], b.blockStatement([]), false);

    fn.returnType = b.typeAnnotation(b.voidTypeAnnotation());

    const ast = b.program([b.expressionStatement(fn)]);

    const printer = new Printer();
    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("adds parenthesis around arrow function body when returning object literal with typescript typecast", function () {
    const code = "() => ({} as object);";

    const fn = b.arrowFunctionExpression(
      [],
      b.tsAsExpression(b.objectExpression([]), b.tsObjectKeyword()),
      false,
    );

    const ast = b.program([b.expressionStatement(fn)]);

    const printer = new Printer();
    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("adds parenthesis around nullish expression, when nullish expression is BinaryExpression's child", function () {
    const code = "let a = (c?.b ?? 1) + 1;";

    const b = recast.types.builders;

    const ast = b.variableDeclaration("let", [
      b.variableDeclarator(
        b.identifier("a"),
        b.binaryExpression(
          "+",
          b.logicalExpression(
            "??",
            b.optionalMemberExpression(b.identifier("c"), b.identifier("b")),
            b.numericLiteral(1),
          ),
          b.numericLiteral(1),
        ),
      ),
    ]);

    const printer = new Printer();
    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints class property initializers with type annotations correctly", function () {
    const code = [
      "class A {",
      "  foo = (a: b): void => {};",
      "}",
    ].join(eol);

    const arg = b.identifier("a");
    arg.typeAnnotation = b.typeAnnotation(
      b.genericTypeAnnotation(b.identifier("b"), null),
    );

    const fn = b.arrowFunctionExpression([arg], b.blockStatement([]), false);
    fn.returnType = b.typeAnnotation(b.voidTypeAnnotation());

    const ast = b.program([
      b.classDeclaration(
        b.identifier("A"),
        b.classBody([b.classProperty(b.identifier("foo"), fn, null, false)]),
      ),
    ]);

    const printer = new Printer({
      tabWidth: 2,
    });

    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints ClassProperty correctly", function () {
    const code = [
      "class A {",
      "  foo: Type = Bar;",
      "}",
    ].join(eol);

    const ast = b.program([
      b.classDeclaration(
        b.identifier("A"),
        b.classBody([
          b.classProperty(
            b.identifier("foo"),
            b.identifier("Bar"),
            b.typeAnnotation(
              b.genericTypeAnnotation(b.identifier("Type"), null),
            ),
          ),
        ]),
      ),
    ]);

    const printer = new Printer({
      tabWidth: 2,
    });

    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints 'definite' ClassProperty correctly", function () {
    const code = [
      "class A {",
      "  foo!: string;",
      "}",
    ].join(eol);

    const ast = b.program([
      b.classDeclaration(
        b.identifier("A"),
        b.classBody([
          Object.assign(
            b.classProperty(
              b.identifier("foo"),
              null,
              b.typeAnnotation(b.stringTypeAnnotation()),
            ),
            { definite: true },
          ),
        ]),
      ),
    ]);

    const printer = new Printer({
      tabWidth: 2,
    });

    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints static ClassProperty correctly", function () {
    const code = [
      "class A {",
      "  static foo = Bar;",
      "}",
    ].join(eol);

    const ast = b.program([
      b.classDeclaration(
        b.identifier("A"),
        b.classBody([
          b.classProperty(b.identifier("foo"), b.identifier("Bar"), null, true),
        ]),
      ),
    ]);

    const printer = new Printer({
      tabWidth: 2,
    });

    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints ClassAccessorProperty correctly", function () {
    const code = [
      "class A {",
      "  accessor foo: Type = Bar;",
      "}",
    ].join(eol);

    const ast = b.program([
      b.classDeclaration(
        b.identifier("A"),
        b.classBody([
          b.classAccessorProperty.from({
            key: b.identifier("foo"),
            value: b.identifier("Bar"),
            typeAnnotation: b.tsTypeAnnotation(
              b.tsTypeReference(b.identifier("Type")),
            )
          })
        ]),
      ),
    ]);

    const printer = new Printer({
      tabWidth: 2,
    });

    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints 'definite' ClassAccessorProperty correctly", function () {
    const code = [
      "class A {",
      "  accessor foo!: string;",
      "}",
    ].join(eol);

    const ast = b.program([
      b.classDeclaration(
        b.identifier("A"),
        b.classBody([
          b.classAccessorProperty.from({
            key: b.identifier("foo"),
            typeAnnotation: b.tsTypeAnnotation(b.tsStringKeyword()),
            definite: true,
          }),
        ]),
      ),
    ]);

    const printer = new Printer({
      tabWidth: 2,
    });

    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints static ClassAccessorProperty correctly", function () {
    const code = [
      "class A {",
      "  static accessor foo = Bar;",
      "}",
    ].join(eol);

    const ast = b.program([
      b.classDeclaration(
        b.identifier("A"),
        b.classBody([
          b.classAccessorProperty.from({
            key: b.identifier("foo"),
            value: b.identifier("Bar"),
            static: true,
          }),
        ]),
      ),
    ]);

    const printer = new Printer({
      tabWidth: 2,
    });

    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints abstract ClassAccessorProperty correctly", function () {
    const code = [
      "class A {",
      "  abstract accessor foo = Bar;",
      "}",
    ].join(eol);

    const ast = b.program([
      b.classDeclaration(
        b.identifier("A"),
        b.classBody([
          b.classAccessorProperty.from({
            key: b.identifier("foo"),
            value: b.identifier("Bar"),
            abstract: true,
          }),
        ]),
      ),
    ]);

    const printer = new Printer({
      tabWidth: 2,
    });

    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints override ClassAccessorProperty correctly", function () {
    const code = [
      "class A {",
      "  override accessor foo = Bar;",
      "}",
    ].join(eol);

    const ast = b.program([
      b.classDeclaration(
        b.identifier("A"),
        b.classBody([
          b.classAccessorProperty.from({
            key: b.identifier("foo"),
            value: b.identifier("Bar"),
            override: true,
          }),
        ]),
      ),
    ]);

    const printer = new Printer({
      tabWidth: 2,
    });

    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints template expressions correctly", function () {
    let code = ["graphql`query`;"].join(eol);

    let ast = b.program([
      b.taggedTemplateStatement(
        b.identifier("graphql"),
        b.templateLiteral(
          [b.templateElement({ cooked: "query", raw: "query" }, false)],
          [],
        ),
      ),
    ]);

    const printer = new Printer({
      tabWidth: 2,
    });

    let pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);

    code = ["graphql`query${foo.getQuery()}field${bar}`;"].join(eol);

    ast = b.program([
      b.taggedTemplateStatement(
        b.identifier("graphql"),
        b.templateLiteral(
          [
            b.templateElement({ cooked: "query", raw: "query" }, false),
            b.templateElement({ cooked: "field", raw: "field" }, false),
            b.templateElement({ cooked: "", raw: "" }, true),
          ],
          [
            b.callExpression(
              b.memberExpression(
                b.identifier("foo"),
                b.identifier("getQuery"),
                false,
              ),
              [],
            ),
            b.identifier("bar"),
          ],
        ),
      ),
    ]);

    pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);

    code = [
      "graphql`",
      "  query {",
      "    ${foo.getQuery()},",
      "    field,",
      "    ${bar},",
      "  }",
      "`;",
    ].join(eol);

    ast = parse(code);
    pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("preserves newlines at the beginning/end of files", function () {
    const code = ["", "f();", ""].join(eol);

    const lines = fromString(code);
    const ast = parse(code, {
      esprima: {
        parse: function (source: string, options?: any) {
          const program = require("esprima").parse(source, options);
          n.Program.assert(program);
          // Expand ast.program.loc to include any
          // leading/trailing whitespace, to simulate the
          // behavior of some parsers, e.g. babel-core.
          lines.skipSpaces(program.loc.start, true, true);
          lines.skipSpaces(program.loc.end, false, true);
          return program;
        },
      },
    });

    ast.program.body.unshift(b.debuggerStatement());

    const printer = new Printer({
      tabWidth: 2,
    });

    assert.strictEqual(
      printer.print(ast).code,
      ["", "debugger;", "f();", ""].join(eol),
    );
  });

  it("respects options.lineTerminator", function () {
    const lines = ["var first = 1;", "var second = 2;"];
    const code = lines.join("\n");
    const ast = parse(code);

    assert.strictEqual(
      new Printer({
        lineTerminator: "\r\n",
      }).print(ast).code,
      lines.join("\r\n"),
    );
  });

  it("preserves indentation in unmodified template expressions", function () {
    const printer = new Printer({
      tabWidth: 2,
    });

    const code = [
      "var x = {",
      "  y: () => Relay.QL`",
      "    query {",
      "      ${foo},",
      "      field,",
      "    }",
      "  `",
      "};",
    ].join(eol);

    const ast = parse(code);
    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("preserves indentation in modified template expressions", function () {
    const code = [
      "const fragments = {",
      "  viewer: Relay.QL`",
      "    fragment on Viewer {   // 2 extraneous spaces.",
      "      actor {              // 2 extraneous spaces.",
      "        id,            // 2 extraneous spaces.",
      "        ${ foo},           // 3 extraneous spaces.",
      "        ${bar },              // Correct!",
      "        name,                // Correct!",
      "        ${baz},              // Correct!",
      "        address {          // 2 extraneous spaces.",
      "          id,          // 2 extraneous spaces.",
      "        },                 // 2 extraneous spaces.",
      "      }                    // 2 extraneous spaces.",
      "    }                      // 2 extraneous spaces.",
      "<~ This line should not be indented.",
      "  `,                       // 2 extraneous spaces.",
      "};",
    ].join(eol);

    const ast = parse(code);
    const printer = new Printer({
      tabWidth: 2,
    });

    recast.visit(ast, {
      visitTaggedTemplateExpression: function (path) {
        function replaceIdWithNodeId(path: any) {
          path.replace(path.value.replace(/\bid\b/g, "nodeID"));
        }

        path.get("quasi", "quasis").each(function (quasiPath: any) {
          replaceIdWithNodeId(quasiPath.get("value", "cooked"));
          replaceIdWithNodeId(quasiPath.get("value", "raw"));
        });

        this.traverse(path);
      },
    });

    const actual = printer.print(ast).code;
    const expected = code.replace(/\bid\b/g, "nodeID");

    assert.strictEqual(actual, expected);
  });

  it("prints commas for flow object types by default", function () {
    const code = [
      "type MyType = {",
      "    message: string,",
      "    isAwesome: boolean,",
      "};",
    ].join(eol);

    const ast = b.typeAlias(
      b.identifier("MyType"),
      null,
      b.objectTypeAnnotation([
        b.objectTypeProperty(
          b.identifier("message"),
          b.stringTypeAnnotation(),
          false,
        ),
        b.objectTypeProperty(
          b.identifier("isAwesome"),
          b.booleanTypeAnnotation(),
          false,
        ),
      ]),
    );

    const printer = new Printer();
    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("shouldn't print a trailing comma for single-line flow object types", function () {
    const code1 = "type MyType = { message: string };";
    const code2 = "type MyType = { [key: string]: string };";

    const ast1 = b.typeAlias(
      b.identifier("MyType"),
      null,
      b.objectTypeAnnotation([
        b.objectTypeProperty(
          b.identifier("message"),
          b.stringTypeAnnotation(),
          false,
        ),
      ]),
    );

    const ast2 = b.typeAlias(
      b.identifier("MyType"),
      null,
      b.objectTypeAnnotation(
        [],
        [
          b.objectTypeIndexer(
            b.identifier("key"),
            b.stringTypeAnnotation(),
            b.stringTypeAnnotation(),
          ),
        ],
      ),
    );

    const printer = new Printer({ trailingComma: true });
    const pretty1 = printer.printGenerically(ast1).code;
    const pretty2 = printer.printGenerically(ast2).code;
    assert.strictEqual(pretty1, code1);
    assert.strictEqual(pretty2, code2);
  });

  it("prints semicolons for flow object types when options.flowObjectCommas is falsy", function () {
    const code = [
      "type MyType = {",
      "    message: string;",
      "    isAwesome: boolean;",
      "};",
    ].join(eol);

    const ast = b.typeAlias(
      b.identifier("MyType"),
      null,
      b.objectTypeAnnotation([
        b.objectTypeProperty(
          b.identifier("message"),
          b.stringTypeAnnotation(),
          false,
        ),
        b.objectTypeProperty(
          b.identifier("isAwesome"),
          b.booleanTypeAnnotation(),
          false,
        ),
      ]),
    );

    const printer = new Printer({ flowObjectCommas: false });
    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints parens for nullable union/intersection types", function () {
    const code = "type MyType = ?(string | number);";

    const ast = b.typeAlias(
      b.identifier("MyType"),
      null,
      b.nullableTypeAnnotation(
        b.unionTypeAnnotation([
          b.stringTypeAnnotation(),
          b.numberTypeAnnotation(),
        ]),
      ),
    );

    const printer = new Printer({});
    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  (nodeMajorVersion >= 6 ? it : xit)(
    "uses the `arrayBracketSpacing` and the `objectCurlySpacing` option",
    function () {
      const babelParser = require("@babel/parser");
      const parseOptions = {
        parser: {
          parse: (source: string) =>
            babelParser.parse(source, {
              sourceType: "module",
              plugins: ["flow"],
            }),
        },
      };

      const testCaseList = [
        {
          printerConfig: {
            arrayBracketSpacing: false,
            objectCurlySpacing: false,
          },
          code: [
            'import {java, script} from "javascript";',
            "",
            "function foo(a) {",
            "    type MyType = {message: string};",
            "    return [1, 2, 3];",
            "}",
            "",
            "export {foo};",
          ].join(eol),
        },
        {
          printerConfig: {
            arrayBracketSpacing: true,
            objectCurlySpacing: false,
          },
          code: [
            'import {java, script} from "javascript";',
            "",
            "function foo(a) {",
            "    type MyType = {message: string};",
            "    return [ 1, 2, 3 ];",
            "}",
            "",
            "export {foo};",
          ].join(eol),
        },
        {
          printerConfig: {
            arrayBracketSpacing: false,
            objectCurlySpacing: true,
          },
          code: [
            'import { java, script } from "javascript";',
            "",
            "function foo(a) {",
            "    type MyType = { message: string };",
            "    return [1, 2, 3];",
            "}",
            "",
            "export { foo };",
          ].join(eol),
        },
        {
          printerConfig: {
            arrayBracketSpacing: true,
            objectCurlySpacing: true,
          },
          code: [
            'import { java, script } from "javascript";',
            "",
            "function foo(a) {",
            "    type MyType = { message: string };",
            "    return [ 1, 2, 3 ];",
            "}",
            "",
            "export { foo };",
          ].join(eol),
        },
      ];

      testCaseList.forEach(function (testCase) {
        const code = testCase.code;
        const printer = new Printer(testCase.printerConfig);

        const ast = parse(code, parseOptions);
        const pretty = printer.printGenerically(ast).code;

        assert.strictEqual(pretty, code);
      });
    },
  );

  it("prints no extra semicolons in for-loop heads (#377)", function () {
    function check(head: any, parser: any) {
      const source = "for (" + head + ") console.log(i);";
      const ast = recast.parse(source, { parser: parser });
      const loop = ast.program.body[0];
      assert.strictEqual(loop.type, "ForStatement");
      loop.body = b.blockStatement([]);

      const reprinted = recast.print(ast).code;

      const openParenIndex = reprinted.indexOf("(");
      assert.notStrictEqual(openParenIndex, -1);

      const closeParenIndex = reprinted.indexOf(")", openParenIndex);
      assert.notStrictEqual(closeParenIndex, -1);

      const newHead = reprinted.slice(openParenIndex + 1, closeParenIndex);

      assert.strictEqual(newHead.split(";").length, 3);
    }

    function checkWith(parser: any) {
      check("let i = 0; i < 1; i++", parser);
      check("let i = 0 ; i < 1; i++", parser);
      check("let i = 0; ; i++", parser);
      check("let i = 0 ; ; i++", parser);
      check("let i = 0; i < 1; ", parser);
      check("let i = 0 ; i < 1; ", parser);
      check("let i = 0; ; ", parser);
      check("let i = 0 ; ; ", parser);
    }

    checkWith(require("../parsers/esprima"));
    checkWith(require("../parsers/acorn"));

    if (nodeMajorVersion >= 6) {
      checkWith(require("../parsers/babel"));
      checkWith(require("../parsers/typescript"));
      checkWith(require("../parsers/flow"));
    }
  });

  it("parenthesizes NumericLiteral MemberExpression objects", function () {
    const nonBabelNode = b.memberExpression(b.literal(1), b.identifier("foo"));

    const babelNode = b.memberExpression(
      b.numericLiteral(1),
      b.identifier("foo"),
    );

    assert.strictEqual(recast.print(nonBabelNode).code, "(1).foo");

    assert.strictEqual(recast.print(babelNode).code, "(1).foo");
  });

  it("obeys 'optional' property of OptionalMemberExpression", function () {
    const node = b.optionalMemberExpression(
      b.identifier("foo"),
      b.identifier("bar"),
    );

    assert.strictEqual(recast.print(node).code, "foo?.bar");

    const nonOptionalNode = b.optionalMemberExpression(
      b.identifier("foo"),
      b.identifier("bar"),
      false,
      false,
    );

    assert.strictEqual(recast.print(nonOptionalNode).code, "foo.bar");
  });

  it("prints chained expression elements", function () {
    const node = b.chainExpression(
      b.memberExpression(b.identifier("foo"), b.identifier("bar"), false),
    );

    assert.strictEqual(recast.print(node).code, "foo.bar");
  });

  it("prints optional ChainExpressions", function () {
    const node = b.chainExpression(
      b.optionalMemberExpression(
        b.identifier("foo"),
        b.identifier("bar"),
        false,
        true,
      ),
    );

    assert.strictEqual(recast.print(node).code, "foo?.bar");
  });

  it("reprints various optional member/call expressions", function () {
    const parser = require("../parsers/babel");

    function check(code: string) {
      const ast = recast.parse(code, { parser });
      const exprStmt = ast.program.body[0];
      n.ExpressionStatement.assert(exprStmt);
      const expr = exprStmt.expression;
      const output = recast.prettyPrint(expr, { tabWidth: 2 }).code;
      assert.strictEqual(code, output);
    }

    check("a.b");
    check("a?.b");
    check("a?.b.c");
    check("a.b?.c");
    check("a?.b?.c");
    check("a?.(b)");
    check("a?.b(c)");
    check("a?.b?.(c)");
    check("a.b?.(c)");
    check("a.b?.(c)?.d");
    check("a.b?.(c)?.d(e)");
    check("a.b?.(c)?.d?.(e)");
    check("a?.b?.(c).d?.(e)");
    check("a?.b?.(c)?.d?.(e)");
    check("(a?.b)?.(c)?.d?.(e)");
    check("(a?.b?.(c)?.d)?.(e)");
  });

  it("prints numbers in bases other than 10 without converting them", function () {
    const code = [
      "let base10 = 6;",
      "let hex = 0xf00d;",
      "let binary = 0b1010;",
      "let octal = 0o744;",
    ].join(eol);
    const ast = parse(code);
    const printer = new Printer({});
    const pretty = printer.printGenerically(ast).code;

    assert.strictEqual(pretty, code);
  });

  it("reprints modified numeric literals", function () {
    const code = "3 + 4;";
    const ast = parse(code);
    const expr = ast.program.body[0].expression;
    const left = expr.left;
    const right = expr.right;

    left.value++;
    right.value++;

    assert.strictEqual(recast.print(ast).code, "4 + 5;");
  });

  it("prints flow tuple type annotations correctly, respecting array options", function () {
    const code = [
      "type MyTupleType = [",
      '  "tuple element 1",',
      '  "tuple element 2",',
      '  "tuple element 3",',
      "];",
    ].join(eol);

    const ast = b.program([
      b.typeAlias(
        b.identifier("MyTupleType"),
        null,
        b.tupleTypeAnnotation([
          b.stringLiteralTypeAnnotation("tuple element 1", "tuple element 1"),
          b.stringLiteralTypeAnnotation("tuple element 2", "tuple element 2"),
          b.stringLiteralTypeAnnotation("tuple element 3", "tuple element 3"),
        ]),
      ),
    ]);

    const printer = new Printer({
      tabWidth: 2,
      wrapColumn: 40,
      trailingComma: true,
    });

    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("adds parenthesis around object expression", function () {
    const code = "({}).x = 1;";

    const assignment = b.assignmentExpression(
      "=",
      b.memberExpression(b.objectExpression([]), b.identifier("x"), false),
      b.literal(1),
    );

    const ast = b.program([b.expressionStatement(assignment)]);

    const printer = new Printer({
      arrowParensAlways: true,
    });
    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("adds parenthesis around conditional", function () {
    const code = "new (typeof a ? b : c)();";
    const callee = recast.parse("typeof a ? b : c").program.body[0].expression;

    const newExpression = b.newExpression(callee, []);

    const ast = b.program([b.expressionStatement(newExpression)]);

    const printer = new Printer({
      arrowParensAlways: true,
    });
    const pretty = printer.print(ast).code;
    assert.strictEqual(pretty, code);
  });

  for (const operator of ["-", "+", "~", "!", "typeof", "void"] as const) {
    it(`adds parenthesis around '${operator}' unary expression in exponentiation expression`, function () {
      const code = `(${
        /[a-z]/.test(operator) ? `${operator} ` : operator
      }a) ** 2;`;
      const ast = b.program([
        b.expressionStatement(
          b.binaryExpression(
            "**",
            b.unaryExpression(operator, b.identifier("a"), true),
            b.literal(2),
          ),
        ),
      ]);

      const printer = new Printer();
      const pretty = printer.print(ast).code;
      assert.strictEqual(pretty, code);
    });
  }

  it("prints flow object type internal slots correctly", function () {
    const code = [
      "type MyObjectType = {",
      "  [myIndexer: string]: any,",
      "  (myParameter: any): any,",
      "  (myOptionalParameter?: any): any,",
      "  (myParameterWithRest: any, ...rest: any[]): any,",
      "  [[myInternalSlot]]: any,",
      "  static [[myStaticOptionalInternalSlot]]?: (arg: any) => any,",
      "  static [[myStaticMethodOptionalInternalSlot]]?(arg: any): any,",
      "  myProperty: any,",
      "};",
    ].join(eol);

    const ast = b.program([
      b.typeAlias(
        b.identifier("MyObjectType"),
        null,
        b.objectTypeAnnotation.from({
          properties: [
            b.objectTypeProperty(
              b.identifier("myProperty"),
              b.anyTypeAnnotation(),
              false,
            ),
          ],
          indexers: [
            b.objectTypeIndexer(
              b.identifier("myIndexer"),
              b.stringTypeAnnotation(),
              b.anyTypeAnnotation(),
            ),
          ],
          callProperties: [
            b.objectTypeCallProperty(
              b.functionTypeAnnotation(
                [
                  b.functionTypeParam(
                    b.identifier("myParameter"),
                    b.anyTypeAnnotation(),
                    false,
                  ),
                ],
                b.anyTypeAnnotation(),
                null,
                null,
              ),
            ),
            b.objectTypeCallProperty(
              b.functionTypeAnnotation(
                [
                  b.functionTypeParam(
                    b.identifier("myOptionalParameter"),
                    b.anyTypeAnnotation(),
                    true,
                  ),
                ],
                b.anyTypeAnnotation(),
                null,
                null,
              ),
            ),
            b.objectTypeCallProperty(
              b.functionTypeAnnotation(
                [
                  b.functionTypeParam(
                    b.identifier("myParameterWithRest"),
                    b.anyTypeAnnotation(),
                    false,
                  ),
                ],
                b.anyTypeAnnotation(),
                b.functionTypeParam(
                  b.identifier("rest"),
                  b.arrayTypeAnnotation(b.anyTypeAnnotation()),
                  false,
                ),
                null,
              ),
            ),
          ],
          internalSlots: [
            b.objectTypeInternalSlot.from({
              id: b.identifier("myInternalSlot"),
              value: b.anyTypeAnnotation(),
              static: false,
              method: false,
              optional: false,
            }),
            b.objectTypeInternalSlot.from({
              id: b.identifier("myStaticOptionalInternalSlot"),
              value: b.functionTypeAnnotation(
                [
                  b.functionTypeParam(
                    b.identifier("arg"),
                    b.anyTypeAnnotation(),
                    false,
                  ),
                ],
                b.anyTypeAnnotation(),
                null,
                null,
              ),
              static: true,
              method: false,
              optional: true,
            }),
            b.objectTypeInternalSlot.from({
              id: b.identifier("myStaticMethodOptionalInternalSlot"),
              value: b.functionTypeAnnotation(
                [
                  b.functionTypeParam(
                    b.identifier("arg"),
                    b.anyTypeAnnotation(),
                    false,
                  ),
                ],
                b.anyTypeAnnotation(),
                null,
                null,
              ),
              static: true,
              method: true,
              optional: true,
            }),
          ],
        }),
      ),
    ]);

    const printer = new Printer({
      tabWidth: 2,
      wrapColumn: 40,
      trailingComma: true,
    });

    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints flow type alias to function correctly", function () {
    const code = ["type MyTypeAlias = (x?: ?number) => string;"].join(eol);

    const ast = b.program([
      b.typeAlias(
        b.identifier("MyTypeAlias"),
        null,
        b.functionTypeAnnotation(
          [
            b.functionTypeParam(
              b.identifier("x"),
              b.nullableTypeAnnotation(b.numberTypeAnnotation()),
              true,
            ),
          ],
          b.stringTypeAnnotation(),
          null,
          null,
        ),
      ),
    ]);

    const printer = new Printer({
      tabWidth: 2,
      wrapColumn: 40,
      trailingComma: true,
    });

    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints class private methods and properties correctly", function () {
    const code = [
      "class MyClassWithPrivate {",
      "  #myPrivateProperty: any;",
      "  #myPrivatePropertyWithValue: any = value;",
      "  #myPrivateMethod() {}",
      "}",
    ].join(eol);

    const ast = b.program([
      b.classDeclaration(
        b.identifier("MyClassWithPrivate"),
        b.classBody([
          b.classPrivateProperty.from({
            key: b.privateName(b.identifier("myPrivateProperty")),
            typeAnnotation: b.typeAnnotation(b.anyTypeAnnotation()),
            value: null,
          }),
          b.classPrivateProperty.from({
            key: b.privateName(b.identifier("myPrivatePropertyWithValue")),
            typeAnnotation: b.typeAnnotation(b.anyTypeAnnotation()),
            value: b.identifier("value"),
          }),
          b.classPrivateMethod(
            b.privateName(b.identifier("myPrivateMethod")),
            [],
            b.blockStatement([]),
          ),
        ]),
      ),
    ]);

    const printer = new Printer({
      tabWidth: 2,
      wrapColumn: 40,
      trailingComma: true,
    });

    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints an interpreter directive correctly", function () {
    const code = ["#!/usr/bin/env node", 'console.log("Hello, world!");'].join(
      eol,
    );

    const ast = b.program.from({
      interpreter: b.interpreterDirective("/usr/bin/env node"),
      body: [
        b.expressionStatement(
          b.callExpression(
            b.memberExpression(b.identifier("console"), b.identifier("log")),
            [b.stringLiteral("Hello, world!")],
          ),
        ),
      ],
    });

    const pretty = new Printer().printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("prints an interface type annotation correctly", function () {
    const code = [
      "let myVar: interface extends MyOtherInterface { myProperty: any };",
    ].join(eol);

    const ast = b.program([
      b.variableDeclaration("let", [
        b.variableDeclarator(
          b.identifier.from({
            name: "myVar",
            typeAnnotation: b.typeAnnotation(
              b.interfaceTypeAnnotation(
                b.objectTypeAnnotation([
                  b.objectTypeProperty(
                    b.identifier("myProperty"),
                    b.anyTypeAnnotation(),
                    false,
                  ),
                ]),
                [b.interfaceExtends(b.identifier("MyOtherInterface"))],
              ),
            ),
          }),
        ),
      ]),
    ]);

    const pretty = new Printer().printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("object destructuring for function parameters", function () {
    const code = [
      "function myFunction(",
      "    {",
      '        yes = "y",',
      '        no: no = "n",',
      "        short,",
      "        long: longHand",
      "    }",
      ") {}",
    ].join(eol);

    const simplifiedDefaultArgProperty = b.property(
      "init",
      b.identifier("yes"),
      b.assignmentPattern(b.identifier("yes"), b.literal("y")),
    );
    simplifiedDefaultArgProperty.shorthand = true;

    const notSimplifiedDefaultArgProperty = b.property(
      "init",
      b.identifier("no"),
      b.assignmentPattern(b.identifier("no"), b.literal("n")),
    );

    const simplifiedProperty = b.property(
      "init",
      b.identifier("short"),
      b.identifier("short"),
    );
    simplifiedProperty.shorthand = true;

    const notSimplifiedProperty = b.property(
      "init",
      b.identifier("long"),
      b.identifier("longHand"),
    );

    const ast = b.program([
      b.functionDeclaration(
        b.identifier("myFunction"),
        [
          b.objectPattern([
            simplifiedDefaultArgProperty,
            notSimplifiedDefaultArgProperty,
            simplifiedProperty,
            notSimplifiedProperty,
          ]),
        ],
        b.blockStatement([]),
      ),
    ]);

    const pretty = new Printer().printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("does not print shorthand properties if key != value", function () {
    const code = ["var o = {", "    value: value0", "};"].join(eol);

    const ast = b.program([
      b.variableDeclaration("var", [
        b.variableDeclarator(
          b.identifier("o"),
          b.objectExpression([
            b.property.from({
              kind: "init",
              key: b.identifier("value"),
              value: b.identifier("value0"),
              shorthand: true,
            }),
          ]),
        ),
      ]),
    ]);

    const pretty = new Printer().printGenerically(ast).code;
    assert.strictEqual(pretty, code);
  });

  it("can pretty-print StaticBlock nodes in class bodies", function () {
    const code = [
      "class A {",
      "  static a = 1;",
      "  static #b = 2;",
      "",
      "  static {",
      "    ++this.a;",
      "    this.#b++;",
      "  }",
      "}",
    ].join(eol);

    const printer = new Printer({ tabWidth: 2 });
    const ast = parse(code, {
      parser: tsParser,
    });
    const pretty = printer.printGenerically(ast).code;

    assert.strictEqual(pretty, code);

    types.visit(ast, {
      visitStaticBlock(path) {
        assert.strictEqual(path.get("body", "length").value, 2);
        while (path.get("body").shift()) {}
        assert.strictEqual(path.get("body", "length").value, 0);
        return false;
      },
    });

    const emptyBlockReprinted = printer.print(ast).code;
    assert.strictEqual(
      emptyBlockReprinted,
      [
        "class A {",
        "  static a = 1;",
        "  static #b = 2;",
        "", // Empty line preserved because of conservative printer.print reprinting.
        "  static {}",
        "}",
      ].join(eol),
    );

    const emptyBlockPrettyPrinted = printer.printGenerically(ast).code;
    assert.strictEqual(
      emptyBlockPrettyPrinted,
      [
        "class A {",
        "  static a = 1;",
        "  static #b = 2;",
        "  static {}",
        "}",
      ].join(eol),
    );
  });

  it("can pretty-print ImportAttribute syntax", function () {
    const code = [
      'import * as noAssertions from "./module";',
      'import * as emptyAssert from "./module" assert {};',
      'import json from "./module" assert { type: "json" };',
      'import * as ns from "./module" assert { type: "reallyLongStringLiteralThatShouldTriggerReflowOntoMultipleLines" }',
    ].join(eol);

    const expectedPretty = [
      'import * as noAssertions from "./module";',
      'import * as emptyAssert from "./module";',
      'import json from "./module" assert { type: "json" };',
      "",
      'import * as ns from "./module" assert {',
      '  type: "reallyLongStringLiteralThatShouldTriggerReflowOntoMultipleLines"',
      "};",
    ].join(eol);

    const printer = new Printer({
      tabWidth: 2,
      wrapColumn: 60,
    });

    const ast = parse(code, {
      parser: tsParser,
    });

    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, expectedPretty);

    types.visit(ast, {
      visitImportAttribute(path) {
        this.traverse(path);
        const valuePath = path.get("value");
        const strLit = valuePath.value;
        types.namedTypes.StringLiteral.assert(strLit);
        if (strLit.value.startsWith("reallyLong")) {
          valuePath.replace(b.stringLiteral("shorter"));
        }
      },
    });

    const reprinted = printer.print(ast).code;
    assert.strictEqual(
      reprinted,
      [
        'import * as noAssertions from "./module";',
        'import * as emptyAssert from "./module" assert {};',
        'import json from "./module" assert { type: "json" };',
        'import * as ns from "./module" assert { type: "shorter" }',
      ].join(eol),
    );
  });

  it("can pretty-print RecordExpression syntax", function () {
    const code = [
      "const rec = #{",
      "  a: #{",
      "    b: 1234",
      "  },",
      "",
      "  c: #{",
      '    d: "dee"',
      "  }",
      "};",
    ].join(eol);

    const ast = parse(code, {
      parser: tsParser,
    });

    const printer = new Printer({
      tabWidth: 2,
      wrapColumn: 60,
    });

    const pretty = printer.printGenerically(ast).code;

    assert.strictEqual(pretty, code);
  });

  it("can pretty-print TupleExpression syntax", function () {
    const code = [
      "const keyArgs = #[",
      '  "query",',
      '  "type",',
      '  "@connection",',
      '  #["key", "filter"]',
      "];",
    ].join(eol);

    const ast = parse(code, {
      parser: tsParser,
    });

    const printer = new Printer({
      tabWidth: 2,
      wrapColumn: 20,
    });

    const pretty = printer.printGenerically(ast).code;

    assert.strictEqual(pretty, code);
  });

  it("can pretty-print ModuleExpression syntax", function () {
    const code = [
      'import { log } from "logger";',
      "export const url = import.meta.url;",
      "log(url);",
    ].join(eol);

    const ast = parse(code, {
      parser: tsParser,
    });

    const printer = new Printer({
      tabWidth: 2,
      wrapColumn: 20,
    });

    const pretty = printer.printGenerically(ast).code;
    assert.strictEqual(pretty, code);

    const reprinted = printer.print(b.moduleExpression(ast.program)).code;
    assert.strictEqual(
      reprinted,
      ["module {", ...code.split(eol).map((line) => "  " + line), "}"].join(
        eol,
      ),
    );
  });
});
