Skip to content

Commit 80c19eb

Browse files
authored
Merge pull request #308797 from microsoft/joh/private-fields-async-fix
build: fix async private method token fusion; inlineChat: adopt native private fields
2 parents d786cb0 + 8f9f550 commit 80c19eb

File tree

10 files changed

+760
-623
lines changed

10 files changed

+760
-623
lines changed

build/next/private-to-property.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export function convertPrivateFields(code: string, filename: string): ConvertPri
115115
lastEnd = edit.end;
116116
}
117117
parts.push(code.substring(lastEnd));
118-
return { code: parts.join(''), classCount, fieldCount: fieldCount, editCount: edits.length, elapsed: Date.now() - t1, edits };
118+
return { code: parts.join(''), classCount, fieldCount: fieldCount, editCount: edits.length, elapsed: Date.now() - t1, edits };
119119

120120
// --- AST walking ---
121121

@@ -209,10 +209,15 @@ return { code: parts.join(''), classCount, fieldCount: fieldCount, editCount: ed
209209
if (ts.isPrivateIdentifier(child)) {
210210
const resolved = resolvePrivateName(child.text);
211211
if (resolved !== undefined) {
212+
const start = child.getStart(sourceFile);
212213
edits.push({
213-
start: child.getStart(sourceFile),
214+
start,
214215
end: child.getEnd(),
215-
newText: resolved
216+
// In minified code, `async#run()` has no space before `#`.
217+
// The `#` naturally starts a new token, but `$` does not —
218+
// `async$a` would fuse into one identifier. Insert a space
219+
// when the preceding character is an identifier character.
220+
newText: (start > 0 && isIdentifierChar(code.charCodeAt(start - 1))) ? ' ' + resolved : resolved
216221
});
217222
}
218223
return;
@@ -234,6 +239,11 @@ return { code: parts.join(''), classCount, fieldCount: fieldCount, editCount: ed
234239
}
235240
}
236241

242+
function isIdentifierChar(ch: number): boolean {
243+
// a-z, A-Z, 0-9, _, $
244+
return (ch >= 97 && ch <= 122) || (ch >= 65 && ch <= 90) || (ch >= 48 && ch <= 57) || ch === 95 || ch === 36;
245+
}
246+
237247
/**
238248
* Adjusts a source map to account for text edits applied to the generated JS.
239249
*

build/next/test/private-to-property.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,54 @@ suite('convertPrivateFields', () => {
292292
assert.deepStrictEqual(result.edits, []);
293293
});
294294

295+
test('async private method — replacement must not merge with async keyword', async () => {
296+
// In minified output, there is no space between `async` and `#method`:
297+
// class Foo{async#run(){await Promise.resolve(1)}}
298+
// Replacing `#run` with `$a` naively produces `async$a()` which is a
299+
// single identifier, not `async $a()`. The `await` inside then becomes
300+
// invalid because the method is no longer async.
301+
const code = 'class Foo{async#run(){return await Promise.resolve(1)}call(){return this.#run()}}';
302+
const result = convertPrivateFields(code, 'test.js');
303+
assert.ok(!result.code.includes('#run'), 'should replace #run');
304+
// The replacement must NOT fuse with `async` into a single token
305+
assert.doesNotThrow(() => new Function(result.code), 'transformed code must be valid JS');
306+
// Verify it actually executes (the async method should still work)
307+
const exec = new Function(`
308+
${result.code}
309+
return new Foo().call();
310+
`);
311+
const val = await exec();
312+
assert.strictEqual(val, 1);
313+
});
314+
315+
test('async private method — space inserted in declaration and not in usage', () => {
316+
// More readable version: ensure that `async #method()` becomes
317+
// `async $a()` (with space), while `this.#method()` becomes
318+
// `this.$a()` (no extra space needed since `.` separates tokens).
319+
const code = [
320+
'class Foo {',
321+
' async #doWork() { return await 42; }',
322+
' run() { return this.#doWork(); }',
323+
'}',
324+
].join('\n');
325+
const result = convertPrivateFields(code, 'test.js');
326+
assert.ok(!result.code.includes('#doWork'), 'should replace #doWork');
327+
assert.doesNotThrow(() => new Function(result.code), 'transformed code must be valid JS');
328+
});
329+
330+
test('static async private method — no token fusion', async () => {
331+
const code = 'class Foo{static async#init(){return await Promise.resolve(1)}static go(){return Foo.#init()}}';
332+
const result = convertPrivateFields(code, 'test.js');
333+
assert.doesNotThrow(() => new Function(result.code),
334+
'static async private method must produce valid JS, got:\n' + result.code);
335+
const exec = new Function(`
336+
${result.code}
337+
return Foo.go();
338+
`);
339+
const value = await exec();
340+
assert.strictEqual(value, 1);
341+
});
342+
295343
test('heritage clause — extends expression resolves outer private field, not inner', () => {
296344
const code = [
297345
'class Outer {',

0 commit comments

Comments
 (0)