build: fix async private method token fusion; inlineChat: adopt native private fields#308797
build: fix async private method token fusion; inlineChat: adopt native private fields#308797
Conversation
…ewrite In minified code, `async#method()` has no space between `async` and `#method`. The `#` naturally starts a new token, but when replaced with `$a`, the `$` is a valid identifier character — `async$a` fuses into a single identifier. This makes any `await` inside the method body a syntax error. Fix: when the character immediately before the `#` is an identifier character, prepend a space to the replacement text. Add 3 tests covering async, static async, and the readable variant.
Convert inlineChat module from TypeScript private (`private _field`) to native ES private fields (`#field`) for better encapsulation and V8 performance (after the private-to-property build step mangles them).
There was a problem hiding this comment.
Pull request overview
This PR addresses a build-time bug in the private-field rewrite (token fusion for minified async#method patterns) and modernizes the inline chat implementation by switching from TypeScript private _field members to native ES #private fields.
Changes:
- Fix
convertPrivateFieldsto insert a separating space when aPrivateIdentifierfollows an identifier character (preventsasync$atoken fusion). - Add/extend unit tests covering async private method minified forms.
- Convert inline chat browser code to use native
#privatefields for state and injected services.
Show a summary per file
| File | Description |
|---|---|
| build/next/private-to-property.ts | Prevents token fusion by conditionally prefixing replacements with a space based on preceding character. |
| build/next/test/private-to-property.test.ts | Adds tests intended to cover async/static-async private method minified cases. |
| src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts | Migrates internal state/options/logging to native #private fields. |
| src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts | Migrates widget internals and injected services to #private fields. |
| src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts | Migrates service internals/events/session map to #private fields. |
| src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts | Migrates overlay widgets’ DOM/state/history interactions to #private fields. |
| src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts | Migrates disposable store to #private field. |
| src/vs/workbench/contrib/inlineChat/browser/inlineChatHistoryService.ts | Migrates storage/history handling to #private fields/method. |
| src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts | Migrates widget state/editor refs to #private fields and methods. |
| src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts | Migrates controller state/services and internal helpers to #private fields/methods. |
Copilot's findings
- Files reviewed: 10/10 changed files
- Comments generated: 1
| test('static async private method — no token fusion', () => { | ||
| const code = 'class Foo{static async#init(){await Promise.resolve()}static go(){return Foo.#init()}}'; | ||
| const result = convertPrivateFields(code, 'test.js'); | ||
| assert.doesNotThrow(() => new Function(result.code), | ||
| 'static async private method must produce valid JS, got:\n' + result.code); |
There was a problem hiding this comment.
The “static async private method — no token fusion” test only checks that the transformed output parses via new Function(result.code). That can still pass even if async#init incorrectly fuses into an identifier (e.g. async$a) because the code remains syntactically valid, but Foo.$a won’t exist and/or await semantics won’t be preserved at runtime. Please extend this test to execute the transformed class (e.g. call Foo.go() and assert it resolves/returns as expected) so it actually detects token-fusion regressions for the static async case.
| test('static async private method — no token fusion', () => { | |
| const code = 'class Foo{static async#init(){await Promise.resolve()}static go(){return Foo.#init()}}'; | |
| const result = convertPrivateFields(code, 'test.js'); | |
| assert.doesNotThrow(() => new Function(result.code), | |
| 'static async private method must produce valid JS, got:\n' + result.code); | |
| test('static async private method — no token fusion', async () => { | |
| const code = 'class Foo{static async#init(){return await Promise.resolve(1)}static go(){return Foo.#init()}}'; | |
| const result = convertPrivateFields(code, 'test.js'); | |
| assert.doesNotThrow(() => new Function(result.code), | |
| 'static async private method must produce valid JS, got:\n' + result.code); | |
| const exec = new Function(` | |
| ${result.code} | |
| return Foo.go(); | |
| `); | |
| const value = await exec(); | |
| assert.strictEqual(value, 1); |
Summary
Fix async private method token fusion in the private-to-property build rewrite, and adopt native ES private fields in the inlineChat module.
Session Context
Key decisions from the development session:
#is an identifier character (e.g.async#run), the replacement is prefixed with a space (async $ainstead ofasync$a). This is safe because an extra space in a non-minified position never changes semantics, and in minified code it's the minimal fix to prevent the$from fusing with the preceding keyword.isIdentifierCharhelper: Checksa-z,A-Z,0-9,_,$— the characters that can continue an identifier in JS. Placed outsideconvertPrivateFieldsas a module-level utility since it's pure and stateless.async#method, minifiedstatic async#method, and non-minifiedasync #method(which already had a space and passed before the fix).esbuild.transform()syntax check added in the prior PR is what caught this bug during the build — it works as intended.Changes
Commit 1:
build: fix async private method token fusionprivate-to-property.ts: When emitting a replacement for aPrivateIdentifier, check if the preceding character is an identifier char. If so, prepend a space to prevent token fusion (e.g.async#run→async $anotasync$a).private-to-property.ts: AddisIdentifierChar()helper.test/private-to-property.test.ts: 3 new tests for async, static async, and readable async private methods.Commit 2:
inlineChat: adopt native ES private fieldsprivate _fieldto#fieldpattern.