Skip to content

Commit dacbc9f

Browse files
authored
feat(async-rewriter2): throw for async in sync contexts MONGOSH-654 (#755)
In always-synchronous contexts like class constructors and synchronous generator functions, throw an exception if a synthetic Promise is encountered, since there is no way to actually act as if it is a synchronous expression.
1 parent fadcbb4 commit dacbc9f

File tree

2 files changed

+82
-28
lines changed

2 files changed

+82
-28
lines changed

packages/async-rewriter2/src/async-writer-babel.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,11 @@ describe('AsyncWriter', () => {
212212
expect(await ret).to.equal('bar');
213213
});
214214

215-
it('cannot implicitly await inside of class constructors', async() => {
215+
it('cannot implicitly await inside of class constructors', () => {
216216
implicitlyAsyncFn.resolves({ foo: 'bar' });
217-
expect(runTranspiledCode(`class A {
217+
expect(() => runTranspiledCode(`class A {
218218
constructor() { this.value = implicitlyAsyncFn().foo; }
219-
}; new A()`).value).to.equal(undefined);
219+
}; new A()`).value).to.throw('Result of expression "implicitlyAsyncFn()" cannot be used in this context');
220220
});
221221

222222
it('can implicitly await inside of functions', async() => {
@@ -250,14 +250,14 @@ describe('AsyncWriter', () => {
250250
expect(await ret).to.equal('bar');
251251
});
252252

253-
it('cannot implicitly await inside of plain generator functions', async() => {
253+
it('cannot implicitly await inside of plain generator functions', () => {
254254
implicitlyAsyncFn.resolves({ foo: 'bar' });
255-
expect(runTranspiledCode(`(function() {
255+
expect(() => runTranspiledCode(`(function() {
256256
const gen = (function*() {
257257
yield implicitlyAsyncFn().foo;
258258
})();
259259
for (const value of gen) return value;
260-
})()`)).to.equal(undefined);
260+
})()`)).to.throw('Result of expression "implicitlyAsyncFn()" cannot be used in this context');
261261
});
262262

263263
it('can implicitly await inside of shorthand arrow functions', async() => {

packages/async-rewriter2/src/async-writer-babel.ts

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ interface AsyncFunctionIdentifiers {
213213
isSyntheticPromise: babel.types.Identifier;
214214
syntheticPromiseSymbol: babel.types.Identifier;
215215
demangleError: babel.types.Identifier;
216+
assertNotSyntheticPromise: babel.types.Identifier;
216217
}
217218
/**
218219
* The second step that performs the heavy lifting of turning regular functions
@@ -239,6 +240,7 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
239240
const isGeneratedInnerFunction = asNodeKey(Symbol('isGeneratedInnerFunction'));
240241
const isGeneratedHelper = asNodeKey(Symbol('isGeneratedHelper'));
241242
const isOriginalBody = asNodeKey(Symbol('isOriginalBody'));
243+
const isAlwaysSyncFunction = asNodeKey(Symbol('isAlwaysSyncFunction'));
242244
// Using this key, we store data on Function nodes that contains the identifiers
243245
// of helpers which are available inside the function.
244246
const identifierGroupKey = '@@mongosh.identifierGroup';
@@ -269,6 +271,15 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
269271
}
270272
`);
271273

274+
const assertNotSyntheticPromiseTemplate = babel.template.statement(`
275+
function ANSP_IDENTIFIER(p, s) {
276+
if (p && p[SP_IDENTIFIER]) {
277+
throw new Error('Result of expression "' + s + '" cannot be used in this context');
278+
}
279+
return p;
280+
}
281+
`);
282+
272283
const asyncTryCatchWrapperTemplate = babel.template.expression(`
273284
async () => {
274285
try {
@@ -307,6 +318,10 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
307318
allowAwaitOutsideFunction: true
308319
});
309320

321+
const assertNotSyntheticExpressionTemplate = babel.template.expression(`
322+
ANSP_IDENTIFIER(NODE, ORIGINAL_SOURCE)
323+
`);
324+
310325
const rethrowTemplate = babel.template.statement(`
311326
try {
312327
ORIGINAL_CODE;
@@ -350,17 +365,6 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
350365
if (path.parentPath.node[isGeneratedInnerFunction]) return;
351366
// Don't wrap helper functions with async-rewriter-generated code.
352367
if (path.parentPath.node[isGeneratedHelper]) return;
353-
// Don't wrap generator functions. There is no good way to handle them.
354-
if (path.parentPath.node.generator && !path.parentPath.node.async) return;
355-
// Finally, do not wrap constructor functions. This is not a technical
356-
// necessity, but rather a result of the fact that we can't handle
357-
// asynchronicity in constructors well (e.g.: What happens when you
358-
// subclass a class with a constructor that returns asynchronously?).
359-
if (path.parentPath.isClassMethod() &&
360-
path.parentPath.node.key.type === 'Identifier' &&
361-
path.parentPath.node.key.name === 'constructor') {
362-
return;
363-
}
364368

365369
const originalSource = path.parent.start !== undefined ?
366370
(this.file as any).code.slice(path.parent.start, path.parent.end) :
@@ -384,6 +388,7 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
384388
const expressionHolder = path.scope.generateUidIdentifier('ex');
385389
const markSyntheticPromise = existingIdentifiers?.markSyntheticPromise ?? path.scope.generateUidIdentifier('msp');
386390
const isSyntheticPromise = existingIdentifiers?.isSyntheticPromise ?? path.scope.generateUidIdentifier('isp');
391+
const assertNotSyntheticPromise = existingIdentifiers?.assertNotSyntheticPromise ?? path.scope.generateUidIdentifier('ansp');
387392
const syntheticPromiseSymbol = existingIdentifiers?.syntheticPromiseSymbol ?? path.scope.generateUidIdentifier('sp');
388393
const demangleError = existingIdentifiers?.demangleError ?? path.scope.generateUidIdentifier('de');
389394
const identifiersGroup: AsyncFunctionIdentifiers = {
@@ -393,6 +398,7 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
393398
expressionHolder,
394399
markSyntheticPromise,
395400
isSyntheticPromise,
401+
assertNotSyntheticPromise,
396402
syntheticPromiseSymbol,
397403
demangleError
398404
};
@@ -410,14 +416,17 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
410416
// Note that the last check potentially triggers getters and Proxy methods
411417
// and we may want to replace it by something a bit more sophisticated.
412418
// All of the top-level AST nodes here are marked as generated helpers.
413-
const promiseHelpers = existingIdentifiers ? [] : [
419+
const commonHelpers = existingIdentifiers ? [] : [
414420
Object.assign(
415421
syntheticPromiseSymbolTemplate({
416422
SP_IDENTIFIER: syntheticPromiseSymbol,
417423
SYMBOL_CONSTRUCTOR: symbolConstructor
418424
}),
419425
{ [isGeneratedHelper]: true }
420426
),
427+
];
428+
const promiseHelpers = existingIdentifiers ? [] : [
429+
...commonHelpers,
421430
Object.assign(
422431
markSyntheticPromiseTemplate({
423432
MSP_IDENTIFIER: markSyntheticPromise,
@@ -439,6 +448,16 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
439448
{ [isGeneratedHelper]: true }
440449
)
441450
];
451+
const syncFnHelpers = [
452+
...commonHelpers,
453+
Object.assign(
454+
assertNotSyntheticPromiseTemplate({
455+
ANSP_IDENTIFIER: assertNotSyntheticPromise,
456+
SP_IDENTIFIER: syntheticPromiseSymbol
457+
}),
458+
{ [isGeneratedHelper]: true }
459+
)
460+
];
442461

443462
if (path.parentPath.node.async) {
444463
// If we are in an async function, no async wrapping is necessary.
@@ -455,6 +474,25 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
455474
return;
456475
}
457476

477+
// If we are in a non-async generator function, or a class constructor,
478+
// we throw errors for implicitly asynchronous expressions, because there
479+
// is just no good way to handle them (e.g.: What happens when you
480+
// subclass a class with a constructor that returns asynchronously?).
481+
if (path.parentPath.node.generator ||
482+
(path.parentPath.isClassMethod() &&
483+
path.parentPath.node.key.type === 'Identifier' &&
484+
path.parentPath.node.key.name === 'constructor')) {
485+
Object.assign(path.parentPath.node, { [isAlwaysSyncFunction]: true });
486+
path.replaceWith(t.blockStatement([
487+
originalSourceNode,
488+
...syncFnHelpers,
489+
rethrowTemplate({
490+
ORIGINAL_CODE: path.node.body
491+
})
492+
]));
493+
return;
494+
}
495+
458496
const asyncTryCatchWrapper = Object.assign(
459497
asyncTryCatchWrapperTemplate({
460498
FUNCTION_STATE_IDENTIFIER: functionState,
@@ -493,9 +531,11 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
493531
},
494532
exit(path) {
495533
// We have seen an expression. If we're not inside an async function,
496-
// we don't care.
534+
// or a function that we explicitly marked as needing always-synchronous
535+
// treatment, we don't care.
497536
if (!path.getFunctionParent()) return;
498-
if (!path.getFunctionParent().node.async) return;
537+
if (!path.getFunctionParent().node.async &&
538+
!path.getFunctionParent().node[isAlwaysSyncFunction]) return;
499539
// identifierGroup holds the list of helper identifiers available
500540
// inside this function.
501541
let identifierGroup: AsyncFunctionIdentifiers;
@@ -534,8 +574,8 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
534574

535575
// If there is a [isGeneratedHelper] between the function we're in
536576
// and this node, that means we've already handled this node.
537-
if (path.findParent(
538-
path => path.isFunction() || (path.isSequenceExpression() && !!path.node[isGeneratedHelper])
577+
if (path.find(
578+
path => path.isFunction() || !!path.node[isGeneratedHelper]
539579
).node[isGeneratedHelper]) {
540580
return;
541581
}
@@ -586,6 +626,25 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
586626
return;
587627
}
588628

629+
const { expressionHolder, isSyntheticPromise, assertNotSyntheticPromise } = identifierGroup;
630+
const prettyOriginalString = limitStringLength(
631+
path.node.start !== undefined ?
632+
(this.file as any).code.slice(path.node.start, path.node.end) :
633+
'<unknown>', 24);
634+
635+
if (!path.getFunctionParent().node.async) {
636+
// Transform expression `foo` into `assertNotSyntheticPromise(foo, 'foo')`.
637+
path.replaceWith(Object.assign(
638+
assertNotSyntheticExpressionTemplate({
639+
ORIGINAL_SOURCE: t.stringLiteral(prettyOriginalString),
640+
NODE: path.node,
641+
ANSP_IDENTIFIER: assertNotSyntheticPromise
642+
}),
643+
{ [isGeneratedHelper]: true }
644+
));
645+
return;
646+
}
647+
589648
// Transform expression `foo` into
590649
// `('\uFEFFfoo\uFEFF', ex = foo, isSyntheticPromise(ex) ? await ex : ex)`
591650
// The first part of the sequence expression is used to identify this
@@ -597,13 +656,8 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
597656
// user code accidentally being recognized as the original source code.
598657
// We limit the string length so that long expressions (e.g. those
599658
// containing functions) are not included in full length.
600-
const { expressionHolder, isSyntheticPromise } = identifierGroup;
601659
const originalSource = t.stringLiteral(
602-
'\ufeff' + limitStringLength(
603-
path.node.start !== undefined ?
604-
(this.file as any).code.slice(path.node.start, path.node.end) :
605-
'<unknown>', 24) +
606-
'\ufeff');
660+
'\ufeff' + prettyOriginalString + '\ufeff');
607661
path.replaceWith(Object.assign(
608662
awaitSyntheticPromiseTemplate({
609663
ORIGINAL_SOURCE: originalSource,

0 commit comments

Comments
 (0)