Repository: prettier/prettier (JavaScript)
Date: 2023-09-16 08:56:24+00:00
Task type: Bug Fix

Issue:
/* GraphQL */ comment tag does not format with typescript `as const`
**Prettier 3.0.0**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuc0DOMAEAlA8gIVwBUBlAfQEUBVAUWwE0yB1ASSIAkyBhXAORKKYAvJgD0AKkwBxAE4BDAA4ALCgBlM40ZgAGAHSiZMARwCucGQE9MJTMExolEBZgC++7ZjlpMkKBgDc+vq+GDgExOTUdPTCYpKyiirqmjr6hqbmVjZ2Dk6u7oFQIAA0IE4wAJboyKByMjIQAO4ACnUIaMggcgA2jXIWHaUARvJgANZwMCRyALZwqhVQcMgAZj1ocMOjE1MKcmCLAObIMDJmpRszFSdnmyBoR91wFCYQ8KvrdwBWaAAeJI9nq93kg1t0NqVTG84M0GgoOigvABaJZwAAm6JKIFOcgq3SOXAgMxmck6PW6WIeUEOTwAgjBThUhiZ4M1zAslh9wXclDAZt0mEoKvA0HswHASO1hRUAG7CiydMBoQYgGVmFhQDGwEhgGQVBQwWmakgwCxPLkQkAKBobJjyBSda1wDYyGXLUqLF0wWFyQ4ki13PYyF2dIZyIZwbpImAq62LGBMCpomBKZAADgADKUZHBTBUcz6-aTQZ9SjBw4nk6mkAAmUomDZEcMIsGWuAzCNojFo1RyakmX1wABiEBkJIZRzJLIgIBcLiAA)
<!-- prettier-ignore -->
```sh
--parser babel-ts
```

**Input:**
<!-- prettier-ignore -->
```tsx
const ROBOTS_QUERY_WITH_CONST = /* GraphQL */ `
  query S { shop }
` as const;

const ROBOTS_QUERY = /* GraphQL */ `
  query S { shop }
`;

```

**Output:**
<!-- prettier-ignore -->
```tsx
const ROBOTS_QUERY_WITH_CONST = /* GraphQL */ `
  query S { shop }
` as const;

const ROBOTS_QUERY = /* GraphQL */ `
  query S {
    shop
  }
`;

```

**Expected behavior:**

Should get the same formated result within the tagged string.

```
const ROBOTS_QUERY_WITH_CONST = /* GraphQL */ `
  query S { shop }
` as const;

const ROBOTS_QUERY = /* GraphQL */ `
  query S { shop }
`;

```

Of course, when it doesn't work with `as const`, you can drop that `as const` because it does not affect typescript's typing. However, there are places where dropping such `as const` might affect devs' experience




Issue discussion:
I don't think we need to support this.
@hanayashiki Hi, can you tell me why this is needed? Is there any meaning in adding `as const` to template literal in TypeScript?
> @hanayashiki Hi, can you tell me why this is needed? Is there any meaning in adding `as const` to template literal in TypeScript?

If you don't use as const, it will be inferred as "string" instead of a string literal.
thank you. I got that there are differences in the inference results. I think this is acceptable.

Solution patch:
diff --git a/changelog_unreleased/typescript/15408.md b/changelog_unreleased/typescript/15408.md
new file mode 100644
index 000000000000..abade71c2386
--- /dev/null
+++ b/changelog_unreleased/typescript/15408.md
@@ -0,0 +1,21 @@
+#### Support embedded formatting in template literals annotated with `as const` (#15408 by @sosukesuzuki)
+
+<!-- prettier-ignore -->
+```tsx
+// Input
+const GQL_QUERY_WITH_CONST = /* GraphQL */ `
+  query S { shop }
+` as const;
+
+// Prettier stable
+const GQL_QUERY_WITH_CONST = /* GraphQL */ `
+  query S { shop }
+` as const;
+
+// Prettier main
+const GQL_QUERY_WITH_CONST = /* GraphQL */ `
+  query S {
+    shop
+  }
+` as const;
+```
diff --git a/src/language-js/embed/graphql.js b/src/language-js/embed/graphql.js
index 0172b07d8e6e..89839165e4fa 100644
--- a/src/language-js/embed/graphql.js
+++ b/src/language-js/embed/graphql.js
@@ -107,7 +107,7 @@ function printGraphqlComments(lines) {
  */
 function isGraphQL({ node, parent }) {
   return (
-    hasLanguageComment(node, "GraphQL") ||
+    hasLanguageComment({ node, parent }, "GraphQL") ||
     (parent &&
       ((parent.type === "TaggedTemplateExpression" &&
         ((parent.tag.type === "MemberExpression" &&
diff --git a/src/language-js/embed/html.js b/src/language-js/embed/html.js
index 8d301dccb9b1..8ee78ab1e49c 100644
--- a/src/language-js/embed/html.js
+++ b/src/language-js/embed/html.js
@@ -102,7 +102,7 @@ async function printEmbedHtmlLike(parser, textToDoc, print, path, options) {
  */
 function isHtml(path) {
   return (
-    hasLanguageComment(path.node, "HTML") ||
+    hasLanguageComment(path, "HTML") ||
     path.match(
       (node) => node.type === "TemplateLiteral",
       (node, name) =>
diff --git a/src/language-js/embed/utils.js b/src/language-js/embed/utils.js
index aab006fbdf4b..075c04eafb98 100644
--- a/src/language-js/embed/utils.js
+++ b/src/language-js/embed/utils.js
@@ -53,7 +53,7 @@ function isAngularComponentTemplate(path) {
   );
 }
 
-function hasLanguageComment(node, languageName) {
+function hasLeadingBlockCommentWithName(node, languageName) {
   // This checks for a leading comment that is exactly `/* GraphQL */`
   // In order to be in line with other implementations of this comment tag
   // we will not trim the comment value and we will expect exactly one space on
@@ -65,9 +65,27 @@ function hasLanguageComment(node, languageName) {
     ({ value }) => value === ` ${languageName} `,
   );
 }
+function hasLanguageComment({ node, parent }, languageName) {
+  return (
+    hasLeadingBlockCommentWithName(node, languageName) ||
+    (isAsConstExpression(parent) &&
+      hasLeadingBlockCommentWithName(parent, languageName))
+  );
+}
+
+function isAsConstExpression(node) {
+  return (
+    node.type === "AsConstExpression" ||
+    (node.type === "TSAsExpression" &&
+      node.typeAnnotation.type === "TSTypeReference" &&
+      node.typeAnnotation.typeName.type === "Identifier" &&
+      node.typeAnnotation.typeName.name === "const")
+  );
+}
 
 export {
   isAngularComponentStyles,
   isAngularComponentTemplate,
   hasLanguageComment,
+  isAsConstExpression,
 };


Test patch:
diff --git a/tests/format/flow/as-const/__snapshots__/jsfmt.spec.js.snap b/tests/format/flow/as-const/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000000..39860b333915
--- /dev/null
+++ b/tests/format/flow/as-const/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`embedded.js [babel-flow] format 1`] = `
+"Unexpected token (3:11)
+  1 | const GQL_QUERY_WITH_CONST = /* GraphQL */ \`
+  2 |   query S { shop }
+> 3 | \` as const;
+    |           ^
+  4 |
+  5 | const HTML_WITH_CONST = /* HTML */ \`
+  6 | <div>"
+`;
+
+exports[`embedded.js format 1`] = `
+====================================options=====================================
+parsers: ["flow"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+const GQL_QUERY_WITH_CONST = /* GraphQL */ \`
+  query S { shop }
+\` as const;
+
+const HTML_WITH_CONST = /* HTML */ \`
+<div>
+<h1>foo</h1>
+  <p>foo</p>
+</div>
+\` as const;
+
+=====================================output=====================================
+const GQL_QUERY_WITH_CONST = /* GraphQL */ \`
+  query S {
+    shop
+  }
+\` as const;
+
+const HTML_WITH_CONST = /* HTML */ \`
+  <div>
+    <h1>foo</h1>
+    <p>foo</p>
+  </div>
+\` as const;
+
+================================================================================
+`;
diff --git a/tests/format/flow/as-const/embedded.js b/tests/format/flow/as-const/embedded.js
new file mode 100644
index 000000000000..b33624693bf4
--- /dev/null
+++ b/tests/format/flow/as-const/embedded.js
@@ -0,0 +1,10 @@
+const GQL_QUERY_WITH_CONST = /* GraphQL */ `
+  query S { shop }
+` as const;
+
+const HTML_WITH_CONST = /* HTML */ `
+<div>
+<h1>foo</h1>
+  <p>foo</p>
+</div>
+` as const;
diff --git a/tests/format/flow/as-const/jsfmt.spec.js b/tests/format/flow/as-const/jsfmt.spec.js
new file mode 100644
index 000000000000..76e7e7410985
--- /dev/null
+++ b/tests/format/flow/as-const/jsfmt.spec.js
@@ -0,0 +1,5 @@
+run_spec(import.meta, ["flow"], {
+  errors: {
+    "babel-flow": ["embedded.js"],
+  },
+});
diff --git a/tests/format/typescript/as/__snapshots__/jsfmt.spec.js.snap b/tests/format/typescript/as/__snapshots__/jsfmt.spec.js.snap
index c1b8120aa4de..e31f35ce94f1 100644
--- a/tests/format/typescript/as/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/format/typescript/as/__snapshots__/jsfmt.spec.js.snap
@@ -133,6 +133,40 @@ const iter2 = createIterator(
 ================================================================================
 `;
 
+exports[`as-const-embedded.ts format 1`] = `
+====================================options=====================================
+parsers: ["typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+const GQL_QUERY_WITH_CONST = /* GraphQL */ \`
+  query S { shop }
+\` as const;
+
+const HTML_WITH_CONST = /* HTML */ \`
+<div>
+<h1>foo</h1>
+  <p>foo</p>
+</div>
+\` as const;
+
+=====================================output=====================================
+const GQL_QUERY_WITH_CONST = /* GraphQL */ \`
+  query S {
+    shop
+  }
+\` as const;
+
+const HTML_WITH_CONST = /* HTML */ \`
+  <div>
+    <h1>foo</h1>
+    <p>foo</p>
+  </div>
+\` as const;
+
+================================================================================
+`;
+
 exports[`assignment.ts format 1`] = `
 ====================================options=====================================
 parsers: ["typescript"]
diff --git a/tests/format/typescript/as/as-const-embedded.ts b/tests/format/typescript/as/as-const-embedded.ts
new file mode 100644
index 000000000000..b33624693bf4
--- /dev/null
+++ b/tests/format/typescript/as/as-const-embedded.ts
@@ -0,0 +1,10 @@
+const GQL_QUERY_WITH_CONST = /* GraphQL */ `
+  query S { shop }
+` as const;
+
+const HTML_WITH_CONST = /* HTML */ `
+<div>
+<h1>foo</h1>
+  <p>foo</p>
+</div>
+` as const;