Repository: prettier/prettier (JavaScript)
Date: 2024-10-08 06:55:23+00:00
Task type: Bug Fix

Issue:
No space after await in typescript parser (in some cases)
**Prettier 1.18.2**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEBDA7qgljABAChgCcBXOXAfgB0pc77izclcAzVAGwGc4BKAbhogANCAgAHGFmhdkoVESIR0ABQUJZKVADcIWACYiQAIyKowAazgwAyuPNYoAc2SM4ogBYwAthwDqHjhwXPZgcDYaOFjaOACeyOBcsqKOPEQwKmZO3qjI7NzuIABWXAAeAEJmltY2qN5wADKOcHmcPKIlpTaOThxwAIokEPCtBaL2RGkJMLHiwWBEWJJG4ouwfgYwHsgAHAAM40o8fmbiCavBcETaLaIAjkPwmRKaaFwAtFBwcPo-RkRwB5YAGZVDZXJIfLtEA8bxYVykQpcHp9QbDFqQtqFGCoYwbfRbZAAJlExGwHB6AGEIN4cgkoNBbiASDwACq4zRQuAAX25QA)
```sh
--parser typescript
```

**Input:**
```tsx
await (true ?
      true : false);

```

**Output:**
```tsx
await(true ? true : false);

```

**Expected behavior:**
```tsx
await (true ? true : false);

```

This is an interesting one, as I've found in my IDE if I select just that line and format I'll get the above unexpected output, but if I format the whole file I get the expected output.



Issue discussion:
Not a bug. TS does not support top level `await` ([so far](https://github.com/microsoft/TypeScript/issues/25988)), so this works as expected.

On the other hand, when formatting a range, not the whole file, it's indeed should be considered a bug. Looks like a new label `area:ranges` needs to be introduced to mark issues like this one.
Indeed, 

```ts
const myFn = async () => {
  await (true ?
    true : false);  
}
```

formats as 

```ts
const myFn = async () => {
  await (true ? true : false);
};
```

[Playground](https://prettier.io/playground/#N4Igxg9gdgLgprEAuc0DOMAEBbAngMSkwF5MBDNXKMTACgEoSA+TYAHSPIHcyBLLWjABOAVziYA-B0wzMwsZiSYAZmQA2aOPQDcMjgF8OIADQgIABxi90yUGSFCIXAAr2EaZCDIA3CLwAmJiAARkJkYADWcDAAyubhvFAA5sjycKYAFjDYagDqGfxwaPFgcDHu-Lze-LieYGgepomaQjDOYUnYZMiqGukgAFZoAB4AQmGR0TFk2HAAMolwPeqapkPDMYlJanAAiiIQ8Mt9pvFCLZ4wuOZFYEK8lkHm97C5ATAZyAAcAAynjppcmFzJ5nkU4EJvEtTABHA7wdoWDwoCgAWigcDg-ixQSEcDhvDx7TInW6SF6qxAmmwvFSon6aC2O32hyW5JW-RgZGCb38H2QACZTMI+GotgBhCDYLqeKDQaEgESaAAq3ORFLg+n0QA)
Thank you @thorn0 , @kachkaev . I probably understood.

The following cases is a bug, right?

[Playground Link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEBDAzgTymABAMwFccYBLaAiCACgEpdgAdKXXVAd1VJl2pgCdCcXAH5cAobiTjBcWswC+IADQgIABzLR0yUKn78I7AAr6EOlKgBuEUgBMVIAEb9UYANZwYAZXVvSUADmyBJwqgAWMAC2ADYA6uHccOh+YHDe5tykVtyYyODoOqoB6HD8MMaugVGoyPioMaWqAFboAB4AQq4eXt6oUXAAMgFwdQ1NIK1t3gGBMXAAioQQ8GONYSB+-KX8+TCY6slg-KSajuonsHH2MOHIABwADKoXEKVxrur5F8llVqOqACOy3glQ0FjQ6AAtFA4HA7PDHPw4MDSMjKqhqrUkPV1qpSlFSCFZPjZvMlitRjjxhsYKgnNc7LdkAAmVQCLgxWYAYQgURq+Sg0ABIEIpQAKvSLLiJq4gnAAKJQBxIACszxAcsC6Tp5VZABYFAogA)

```
--parser=typescript
--range-start=24
--range-end=50
```

**Input:**
```ts
async function foo() {
  await (true ? true : true)
}
```

**Output:**
```ts
async function foo() {
  await(true ? true : true);
}
```
Yep, but given that top-level await is coming anyway, it makes sense to always parse `await` as a keyword. Moreover, in some situations, TS already parses it as a keyword outside async functions, e.g. in front of function calls: `await f()`, see [playground](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEBDA7qgljABAMwAoBKEAGhAgAcYtoBnZUVAJxYnQAVWFGVUAbhCwATciABGLVGADWcGAGUqMrFADmyGCwCucCgAsYAWwA2AdQM449FWDiLeOLAJwBPZOHqMKa+nBYYTml1Y1RkfFRTfwoAK3oADwAhaTkFRVRjOAAZNTgIqJiQeITFNXVTOABFHQh4Auj9EBUWfxZPGDcqGzAWLBpxKj7Yc1EYA2QADgAGCiGIf3NpKk8hmwCBfIoAR1r4YOo+NHoAWig4OBFL8RY4Xaxb4NRQ8KRIxop-YywtXSb6cqVGp1fJvQpNGCoCSjETjZAAJgo2mwpnKAGEIMYwp4oNAtiAdP4ACpQvjvfwAXwpQA), use the "Show AST" checkbox there.
I've done some poking around on this, hoping to put a PR, but this has some complex details to iron out. In short, this issue appears to be a consequence of where top-level await is syntactically allowed in typescript. Technically top-level await is only allowed inside module code ([more on this here](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#top-level-await)). 

So, prettier correctly handles the snippet in the issue if we make it a module

**Prettier 3.1.1**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEBDA7qgljABAChgCcBXOXAfgB0pc77izclcAzVAGwGc4BKAbhpwAHgAcIRPMAC+IADQgIomFmhdkoVESIR0ABS0J1KTpgCe6hQCMiqMAGs4MAMqoAtnAAyWKHGTtuOGtbBydnUTsfAHNkRiCQHjcsWNJ4kVE4IiwPWE4AFUyoLSw4YwCeBS5ojjgARRIIeH9OCpAAKy5hZ2q6hqakcviARz64PR1RYzQuAFpfOAATRfkQYmwOaIBhCDc3VGQ0Dg4VqqgomoBBGGIsKxJ4PUzvX2bAhQALGDcOAHV3nFKETAcGcRhwWAAbjgzAcwFxLCAIWQAJJQJawZxgLLKC5o5wwMw1V6tUQ6Hg-WyiA6k0qZCF+BQ+HiScaoKJ7YnxCJEZkHAkZLhYrDKFaknwwH5YBYwd7IAAcAAYFEQ4CMsCrWez9gMWvEYKgrJLpbKkAAmBQkHh5A1lXUKOBuKyLJYLTyoM4kNlwABiEj212iB1Q9wgIGk0iAA)
<!-- prettier-ignore -->
```sh
--parser typescript
```

**Input:**
<!-- prettier-ignore -->
```tsx
await (true ?
      true : false);
export {}
```

**Output:**
<!-- prettier-ignore -->
```tsx
await (true ? true : false);
export {};

```

In _non-module_ code, `await x` is still treated as an "AwaitExpression", and therefore still prints as expected, despite being invalid TS. However, `await (/* an expression in parentheses */)` is treated as a "CallExpression". _Technically_, it is valid typescript to create a function called `await` and call it (see [TS Playground link](https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABAQwO7JlAPAFQHwAUAHgFyI4CUZOiA3ogE4CmUIDSRiAvgNwBQaDFAIEKiALx463CqJ5A)), even though this fails at runtime for me in the browser. Therefore, with a bit of mental gymnastics, you can convince yourself the `await` in
```ts
function await<T>(x: T): T { return x };
await(() => { })();
```
_correctly_ does not have a space after it (see also the [typescript-eslint playground](https://typescript-eslint.io/play/#ts=5.2.2&showAST=es&fileType=.tsx&code=GYVwdgxgLglg9mABAQwO7JlAPAFQHwAUAHgFyI4CUZOiA3ogE4CmUIDSRiAvgNwCwAKDQYoBAhUQBePHW4VxPIA&eslintrc=N4KABGBEBOCuA2BTAzpAXGYBfEWg&tsconfig=N4KABGBEDGD2C2AHAlgGwKYCcDyiAuysAdgM6QBcYoEEkJemy0eAcgK6qoDCAFutAGsylBm3TgwAXxCSgA&tokens=false)). Ditto for some atrocity like this:
```ts
declare const await: any;
await?.property;
```

Speculating a bit, I think that the only time the behavior described in the issue could really happen is with illegal top-level await (outside of module code) that _also_ happens to be followed by parentheses (and perhaps a few other syntactic scenarios apart from simply `await x;`). That seems very unlikely to occur in real-world code.  However, it's quite likely to occur in _code snippets_, where the parser isn't interpreting the snippet as module code. See, for example, [this example in the wild](https://typescript-eslint.io/rules/no-floating-promises/#ignoreiife), whose source is typescript embedded in markdown formatted by prettier. I'm pretty sure this analysis also helps explain the findings in the other comments in this issue, where a range behaves differently than the whole file.

It seems that in order to "fix" the behavior in code snippets, one possibility would be to proactively interpret snippets as module code, which may not be a sound thing to do in general (I'm not sure). Or, maybe prettier is doing it right, and one should just not use top-level await in code snippets without inserting an `export {}`. Looking for guidance on how (or whether!) to proceed with this issue. 

Afterthought - you can also construct an analogous scenario with the yield keyword, like so
**Prettier 3.1.1**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEBPAlnANgEwAQAUBAlHgLwB8ewAvsQDpSMBmArlGDOtAFR4DmCOACcAhjAjCS1RnjwZs+IqUrU6jGiAA0ICAAcu0AM7JQo4cIgB3AArmEJlKKxXRqEzoBGYsAGs4MADKogC2cAAy6FBwyMzORnBePv5BeqJgUfzIMMKsiSAJIejZuflwAB56IuhhsM4AKiJQ5piOcVgJOkaZWHAAiqwQ8LHx+QBWRuWBPf2Dw0jtnSAAjnNwNpZ6jiCiRgC00XA4R9ogOaLoWJkAwhAhIaLIO1hYp91Q-L0AgjA56J6seA2ESRaIjDr5AAWMBCWAA6pD0PAjGkwHBAg4kegAG5I1BPMBGDwgbF5ACSUGOsECYGE6AMX0pgRgqF64KWeksCThYj0T05cASwmxMR0USFMA2on4D3Z+TSwiFTxZVSMtPpMFOnKiMDh6BwMEhyAAHAAGHTCOCrdCWqUyx4LUY6GCiTx6g1GpAAJh0rAS9VdbSdIDgIU8R2OOHCog+rGlcAAYpIHr9Mk9RICICAaDQgA)
<!-- prettier-ignore -->
```sh
--parser typescript
```

**Input:**
<!-- prettier-ignore -->
```tsx
yield (() => {})

function* generator() {
  yield (() => {})
}
```

**Output:**
<!-- prettier-ignore -->
```tsx
yield(() => {});

function* generator() {
  yield () => {};
}

```
@kirkwaiblinger no worries if this was too long ago and you don't remember, but how does this your explanation make sense in light that TypeScript adds `export {};` to the emitted JS, in effect forcing the JS file to be a module even if the TS file wasn't a module?
@abrahamguo I'm not quite sure what you're referring to here

Solution patch:
diff --git a/changelog_unreleased/typescript/16729.md b/changelog_unreleased/typescript/16729.md
new file mode 100644
index 000000000000..515790d69892
--- /dev/null
+++ b/changelog_unreleased/typescript/16729.md
@@ -0,0 +1,13 @@
+#### Support "Top-level await statements" (#16729 by @fisker)
+
+<!-- prettier-ignore -->
+```tsx
+// Input
+(await (await fetch()).json()).foo
+
+// Prettier stable
+await(await fetch()).json().foo;
+
+// Prettier main
+(await (await fetch()).json()).foo;
+```
diff --git a/src/language-js/parse/typescript.js b/src/language-js/parse/typescript.js
index 9cbd54e3f34a..86bf78d3f80c 100644
--- a/src/language-js/parse/typescript.js
+++ b/src/language-js/parse/typescript.js
@@ -3,6 +3,7 @@ import createError from "../../common/parser-create-error.js";
 import tryCombinations from "../../utils/try-combinations.js";
 import postprocess from "./postprocess/index.js";
 import createParser from "./utils/create-parser.js";
+import getSourceType from "./utils/get-source-type.js";
 import replaceHashbang from "./utils/replace-hashbang.js";
 
 /** @import {TSESTreeOptions} from "@typescript-eslint/typescript-estree" */
@@ -50,18 +51,34 @@ const isKnownFileType = (filepath) =>
 
 function getParseOptionsCombinations(text, options) {
   const filepath = options?.filepath;
+
+  let combinations = [{ ...baseParseOptions, filePath: filepath }];
+
+  const sourceType = getSourceType(options);
+  if (sourceType) {
+    combinations = combinations.map((parseOptions) => ({
+      ...parseOptions,
+      sourceType,
+    }));
+  } else {
+    /** @type {("module" | "script") []} */
+    const sourceTypes = ["module", "script"];
+    combinations = sourceTypes.flatMap((sourceType) =>
+      combinations.map((parseOptions) => ({ ...parseOptions, sourceType })),
+    );
+  }
+
   if (filepath && isKnownFileType(filepath)) {
-    return [{ ...baseParseOptions, filePath: filepath }];
+    return combinations;
   }
 
   const shouldEnableJsx = isProbablyJsx(text);
-  return [
-    { ...baseParseOptions, jsx: shouldEnableJsx },
-    { ...baseParseOptions, jsx: !shouldEnableJsx },
-  ];
+  return [shouldEnableJsx, !shouldEnableJsx].flatMap((jsx) =>
+    combinations.map((parseOptions) => ({ ...parseOptions, jsx })),
+  );
 }
 
-function parse(text, options) {
+function parse(text, options = {}) {
   const textToParse = replaceHashbang(text);
   const parseOptionsCombinations = getParseOptionsCombinations(text, options);
 
diff --git a/src/language-js/parse/utils/get-source-type.js b/src/language-js/parse/utils/get-source-type.js
index ce721f0c14b8..a8ac05c214ae 100644
--- a/src/language-js/parse/utils/get-source-type.js
+++ b/src/language-js/parse/utils/get-source-type.js
@@ -6,11 +6,11 @@ function getSourceType(options) {
   }
   filepath = filepath.toLowerCase();
 
-  if (filepath.endsWith(".cjs")) {
+  if (filepath.endsWith(".cjs") || filepath.endsWith(".cts")) {
     return "script";
   }
 
-  if (filepath.endsWith(".mjs")) {
+  if (filepath.endsWith(".mjs") || filepath.endsWith(".mts")) {
     return "module";
   }
 }


Test patch:
diff --git a/tests/format/js/top-level-await/__snapshots__/format.test.js.snap b/tests/format/js/top-level-await/__snapshots__/format.test.js.snap
index 249b632a6672..16fa177226e3 100644
--- a/tests/format/js/top-level-await/__snapshots__/format.test.js.snap
+++ b/tests/format/js/top-level-await/__snapshots__/format.test.js.snap
@@ -29,3 +29,45 @@ call(await something());
 
 ================================================================================
 `;
+
+exports[`test.cjs format 1`] = `
+====================================options=====================================
+parsers: ["babel", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+await(1);
+
+=====================================output=====================================
+await(1);
+
+================================================================================
+`;
+
+exports[`test.js format 1`] = `
+====================================options=====================================
+parsers: ["babel", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+await(1);
+
+=====================================output=====================================
+await 1;
+
+================================================================================
+`;
+
+exports[`test.mjs format 1`] = `
+====================================options=====================================
+parsers: ["babel", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+await(1);
+
+=====================================output=====================================
+await 1;
+
+================================================================================
+`;
diff --git a/tests/format/js/top-level-await/test.cjs b/tests/format/js/top-level-await/test.cjs
new file mode 100644
index 000000000000..f3d5b23bd46f
--- /dev/null
+++ b/tests/format/js/top-level-await/test.cjs
@@ -0,0 +1,1 @@
+await(1);
diff --git a/tests/format/js/top-level-await/test.js b/tests/format/js/top-level-await/test.js
new file mode 100644
index 000000000000..f3d5b23bd46f
--- /dev/null
+++ b/tests/format/js/top-level-await/test.js
@@ -0,0 +1,1 @@
+await(1);
diff --git a/tests/format/js/top-level-await/test.mjs b/tests/format/js/top-level-await/test.mjs
new file mode 100644
index 000000000000..f3d5b23bd46f
--- /dev/null
+++ b/tests/format/js/top-level-await/test.mjs
@@ -0,0 +1,1 @@
+await(1);
diff --git a/tests/format/jsx/top-level-await/__snapshots__/format.test.js.snap b/tests/format/jsx/top-level-await/__snapshots__/format.test.js.snap
new file mode 100644
index 000000000000..0c2b4cbb7813
--- /dev/null
+++ b/tests/format/jsx/top-level-await/__snapshots__/format.test.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`test.jsx format 1`] = `
+====================================options=====================================
+parsers: ["babel", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+await(1);
+
+=====================================output=====================================
+await 1;
+
+================================================================================
+`;
diff --git a/tests/format/jsx/top-level-await/format.test.js b/tests/format/jsx/top-level-await/format.test.js
new file mode 100644
index 000000000000..98ba37cc2f18
--- /dev/null
+++ b/tests/format/jsx/top-level-await/format.test.js
@@ -0,0 +1,1 @@
+runFormatTest(import.meta, ["babel", "typescript"]);
diff --git a/tests/format/jsx/top-level-await/test.jsx b/tests/format/jsx/top-level-await/test.jsx
new file mode 100644
index 000000000000..f3d5b23bd46f
--- /dev/null
+++ b/tests/format/jsx/top-level-await/test.jsx
@@ -0,0 +1,1 @@
+await(1);
diff --git a/tests/format/typescript/top-level-await/__snapshots__/format.test.js.snap b/tests/format/typescript/top-level-await/__snapshots__/format.test.js.snap
new file mode 100644
index 000000000000..84f20d71c583
--- /dev/null
+++ b/tests/format/typescript/top-level-await/__snapshots__/format.test.js.snap
@@ -0,0 +1,57 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`test.cts format 1`] = `
+====================================options=====================================
+parsers: ["typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+await(1);
+
+=====================================output=====================================
+await(1);
+
+================================================================================
+`;
+
+exports[`test.mts format 1`] = `
+====================================options=====================================
+parsers: ["typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+await(1);
+
+=====================================output=====================================
+await 1;
+
+================================================================================
+`;
+
+exports[`test.ts format 1`] = `
+====================================options=====================================
+parsers: ["typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+await(1);
+
+=====================================output=====================================
+await 1;
+
+================================================================================
+`;
+
+exports[`test.tsx format 1`] = `
+====================================options=====================================
+parsers: ["typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+await(1);
+
+=====================================output=====================================
+await 1;
+
+================================================================================
+`;
diff --git a/tests/format/typescript/top-level-await/format.test.js b/tests/format/typescript/top-level-await/format.test.js
new file mode 100644
index 000000000000..116dcbd78bc4
--- /dev/null
+++ b/tests/format/typescript/top-level-await/format.test.js
@@ -0,0 +1,1 @@
+runFormatTest(import.meta, ["typescript"]);
diff --git a/tests/format/typescript/top-level-await/test.cts b/tests/format/typescript/top-level-await/test.cts
new file mode 100644
index 000000000000..f3d5b23bd46f
--- /dev/null
+++ b/tests/format/typescript/top-level-await/test.cts
@@ -0,0 +1,1 @@
+await(1);
diff --git a/tests/format/typescript/top-level-await/test.mts b/tests/format/typescript/top-level-await/test.mts
new file mode 100644
index 000000000000..f3d5b23bd46f
--- /dev/null
+++ b/tests/format/typescript/top-level-await/test.mts
@@ -0,0 +1,1 @@
+await(1);
diff --git a/tests/format/typescript/top-level-await/test.ts b/tests/format/typescript/top-level-await/test.ts
new file mode 100644
index 000000000000..f3d5b23bd46f
--- /dev/null
+++ b/tests/format/typescript/top-level-await/test.ts
@@ -0,0 +1,1 @@
+await(1);
diff --git a/tests/format/typescript/top-level-await/test.tsx b/tests/format/typescript/top-level-await/test.tsx
new file mode 100644
index 000000000000..f3d5b23bd46f
--- /dev/null
+++ b/tests/format/typescript/top-level-await/test.tsx
@@ -0,0 +1,1 @@
+await(1);