Repository: prettier/prettier (JavaScript)
Date: 2021-12-09 12:29:50+00:00
Task type: Bug Fix

Issue:
Invalid shorthand for ExportSpecifier
Sorry for not following the usual issue formatting but this issue is only available via the API:

```js
// Actual:   export {specifier1};
// Expected: export {specifier1 as specifier2};
console.log(require("prettier").format(".", {
  parser: () => ({
    type: "Program",
    body: [
      {
        type: "ExportNamedDeclaration",
        declaration: null,
        specifiers: [
          {
            type: "ExportSpecifier",
            local: {
              // start: 0, end: 0,
              type: "Identifier",
              name: "specifier1",
            },
            exported: {
              // start: 1, end: 1,
              type: "Identifier",
              name: "specifier2",
            },
          },
        ],
        source: null,
      },
    ],
    sourceType: "module",
  }),
}));
```

Uncommenting the `start` and `end` properties solves the issue. I understand that `prettier` looks into the `start` and `end` properties to inspect the original formatting of the code. But I submit to you that these properties should only affect the formatting of the generated code and not its correctness. This can be motivated by the fact that the `start` and `end` properties are not part of the [estree specification](https://github.com/estree/estree).



Issue discussion:
Location info is required to use Prettier.
But we can check specifier name first

Solution patch:
diff --git a/src/language-js/loc.js b/src/language-js/loc.js
index 969025b5a760..7c94dab011c2 100644
--- a/src/language-js/loc.js
+++ b/src/language-js/loc.js
@@ -32,7 +32,8 @@ function locEnd(node) {
  * @returns {boolean}
  */
 function hasSameLocStart(nodeA, nodeB) {
-  return locStart(nodeA) === locStart(nodeB);
+  const nodeAStart = locStart(nodeA);
+  return Number.isInteger(nodeAStart) && nodeAStart === locStart(nodeB);
 }
 
 /**
@@ -41,7 +42,8 @@ function hasSameLocStart(nodeA, nodeB) {
  * @returns {boolean}
  */
 function hasSameLocEnd(nodeA, nodeB) {
-  return locEnd(nodeA) === locEnd(nodeB);
+  const nodeAEnd = locEnd(nodeA);
+  return Number.isInteger(nodeAEnd) && nodeAEnd === locEnd(nodeB);
 }
 
 /**
diff --git a/src/language-js/print/module.js b/src/language-js/print/module.js
index 4c71a00e7049..11282ada81dd 100644
--- a/src/language-js/print/module.js
+++ b/src/language-js/print/module.js
@@ -11,6 +11,8 @@ const {
   CommentCheckFlags,
   shouldPrintComma,
   needsHardlineAfterDanglingComment,
+  isStringLiteral,
+  rawText,
 } = require("../utils.js");
 const { locStart, hasSameLoc } = require("../loc.js");
 const {
@@ -294,6 +296,8 @@ function printModuleSpecifier(path, options, print) {
   const isImport = type.startsWith("Import");
   const leftSideProperty = isImport ? "imported" : "local";
   const rightSideProperty = isImport ? "local" : "exported";
+  const leftSideNode = node[leftSideProperty];
+  const rightSideNode = node[rightSideProperty];
   let left = "";
   let right = "";
   if (
@@ -301,16 +305,11 @@ function printModuleSpecifier(path, options, print) {
     type === "ImportNamespaceSpecifier"
   ) {
     left = "*";
-  } else if (node[leftSideProperty]) {
+  } else if (leftSideNode) {
     left = print(leftSideProperty);
   }
 
-  if (
-    node[rightSideProperty] &&
-    (!node[leftSideProperty] ||
-      // import {a as a} from '.'
-      !hasSameLoc(node[leftSideProperty], node[rightSideProperty]))
-  ) {
+  if (rightSideNode && !isShorthandSpecifier(node)) {
     right = print(rightSideProperty);
   }
 
@@ -318,6 +317,43 @@ function printModuleSpecifier(path, options, print) {
   return parts;
 }
 
+function isShorthandSpecifier(specifier) {
+  if (
+    specifier.type !== "ImportSpecifier" &&
+    specifier.type !== "ExportSpecifier"
+  ) {
+    return false;
+  }
+
+  const {
+    local,
+    [specifier.type === "ImportSpecifier" ? "imported" : "exported"]:
+      importedOrExported,
+  } = specifier;
+
+  if (
+    local.type !== importedOrExported.type ||
+    !hasSameLoc(local, importedOrExported)
+  ) {
+    return false;
+  }
+
+  if (isStringLiteral(local)) {
+    return (
+      local.value === importedOrExported.value &&
+      rawText(local) === rawText(importedOrExported)
+    );
+  }
+
+  switch (local.type) {
+    case "Identifier":
+      return local.name === importedOrExported.name;
+    default:
+      /* istanbul ignore next */
+      return false;
+  }
+}
+
 module.exports = {
   printImportDeclaration,
   printExportDeclaration,


Test patch:
diff --git a/tests/integration/__tests__/__snapshots__/format-ast.js.snap b/tests/integration/__tests__/__snapshots__/format-ast.js.snap
new file mode 100644
index 000000000000..422eaeb0b421
--- /dev/null
+++ b/tests/integration/__tests__/__snapshots__/format-ast.js.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`formatAST Shorthand specifier 1`] = `
+"export { specifier1 as specifier2 };
+"
+`;
+
+exports[`formatAST Shorthand specifier 2 1`] = `
+"export { specifier1 as specifier2 };
+"
+`;
+
+exports[`formatAST Shorthand specifier 3 1`] = `
+"export { \\"specifier\\" as specifier };
+"
+`;
+
+exports[`formatAST Shorthand specifier 4 1`] = `
+"export { \\"specifier\\" as \\"specifier\\" };
+"
+`;
diff --git a/tests/integration/__tests__/format-ast.js b/tests/integration/__tests__/format-ast.js
new file mode 100644
index 000000000000..4ca37ee2fdce
--- /dev/null
+++ b/tests/integration/__tests__/format-ast.js
@@ -0,0 +1,97 @@
+"use strict";
+
+const {
+  __debug: { formatAST },
+} = require("prettier-local");
+
+describe("formatAST", () => {
+  const formatExportSpecifier = (specifier) => {
+    const { formatted } = formatAST(
+      {
+        type: "Program",
+        body: [
+          {
+            type: "ExportNamedDeclaration",
+            specifiers: [specifier],
+          },
+        ],
+      },
+      { parser: "meriyah" }
+    );
+
+    return formatted;
+  };
+
+  test("Shorthand specifier", () => {
+    expect(
+      formatExportSpecifier({
+        type: "ExportSpecifier",
+        local: {
+          type: "Identifier",
+          name: "specifier1",
+        },
+        exported: {
+          type: "Identifier",
+          name: "specifier2",
+        },
+      })
+    ).toMatchSnapshot();
+  });
+
+  test("Shorthand specifier 2", () => {
+    expect(
+      formatExportSpecifier({
+        type: "ExportSpecifier",
+        local: {
+          type: "Identifier",
+          name: "specifier1",
+          range: [0, 0],
+        },
+        exported: {
+          type: "Identifier",
+          name: "specifier2",
+          range: [0, 0],
+        },
+      })
+    ).toMatchSnapshot();
+  });
+
+  test("Shorthand specifier 3", () => {
+    expect(
+      formatExportSpecifier({
+        type: "ExportSpecifier",
+        local: {
+          type: "Literal",
+          value: "specifier",
+          raw: '"specifier"',
+          range: [0, 0],
+        },
+        exported: {
+          type: "Identifier",
+          name: "specifier",
+          range: [0, 0],
+        },
+      })
+    ).toMatchSnapshot();
+  });
+
+  test("Shorthand specifier 4", () => {
+    expect(
+      formatExportSpecifier({
+        type: "ExportSpecifier",
+        local: {
+          type: "Literal",
+          value: "specifier",
+          raw: '"specifier"',
+          range: [0, 0],
+        },
+        exported: {
+          type: "Literal",
+          value: "specifier",
+          raw: "'specifier'",
+          range: [0, 0],
+        },
+      })
+    ).toMatchSnapshot();
+  });
+});