Skip to content

Commit 03c6e90

Browse files
authored
chore(async-rewriter2): add error code for runtime error (#768)
1 parent 438f062 commit 03c6e90

File tree

5 files changed

+96
-25
lines changed

5 files changed

+96
-25
lines changed

packages/async-rewriter/src/error-codes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Note: Codes are shared with the new async rewriter
12

23
/**
34
* @mongoshErrors

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

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -219,13 +219,6 @@ describe('AsyncWriter', () => {
219219
expect(await ret).to.equal('bar');
220220
});
221221

222-
it('cannot implicitly await inside of class constructors', () => {
223-
implicitlyAsyncFn.resolves({ foo: 'bar' });
224-
expect(() => runTranspiledCode(`class A {
225-
constructor() { this.value = implicitlyAsyncFn().foo; }
226-
}; new A()`).value).to.throw('Result of expression "implicitlyAsyncFn()" cannot be used in this context');
227-
});
228-
229222
it('can implicitly await inside of functions', async() => {
230223
implicitlyAsyncFn.resolves({ foo: 'bar' });
231224
const ret = runTranspiledCode(`(function() {
@@ -257,16 +250,6 @@ describe('AsyncWriter', () => {
257250
expect(await ret).to.equal('bar');
258251
});
259252

260-
it('cannot implicitly await inside of plain generator functions', () => {
261-
implicitlyAsyncFn.resolves({ foo: 'bar' });
262-
expect(() => runTranspiledCode(`(function() {
263-
const gen = (function*() {
264-
yield implicitlyAsyncFn().foo;
265-
})();
266-
for (const value of gen) return value;
267-
})()`)).to.throw('Result of expression "implicitlyAsyncFn()" cannot be used in this context');
268-
});
269-
270253
it('can implicitly await inside of shorthand arrow functions', async() => {
271254
implicitlyAsyncFn.resolves({ foo: 'bar' });
272255
const ret = runTranspiledCode('(() => implicitlyAsyncFn().foo)()');
@@ -374,6 +357,36 @@ describe('AsyncWriter', () => {
374357
implicitlyAsyncValue = 'abc';
375358
expect(await runTranspiledCode('typeof implicitlyAsyncValue')).to.equal('string');
376359
});
360+
361+
context('invalid implicit awaits', () => {
362+
beforeEach(() => {
363+
runUntranspiledCode(asyncWriter.runtimeSupportCode());
364+
});
365+
366+
it('cannot implicitly await inside of class constructors', () => {
367+
implicitlyAsyncFn.resolves({ foo: 'bar' });
368+
expect(() => runTranspiledCode(`class A {
369+
constructor() { this.value = implicitlyAsyncFn().foo; }
370+
}; new A()`).value).to.throw('[ASYNC-10012] Result of expression "implicitlyAsyncFn()" cannot be used in this context');
371+
});
372+
373+
it('wrapping inside async functions makes class constructors work nicely', async() => {
374+
implicitlyAsyncFn.resolves({ foo: 'bar' });
375+
expect(await runTranspiledCode(`class A {
376+
constructor() { this.value = (async() => implicitlyAsyncFn().foo)(); }
377+
}; new A()`).value).to.equal('bar');
378+
});
379+
380+
it('cannot implicitly await inside of plain generator functions', () => {
381+
implicitlyAsyncFn.resolves({ foo: 'bar' });
382+
expect(() => runTranspiledCode(`(function() {
383+
const gen = (function*() {
384+
yield implicitlyAsyncFn().foo;
385+
})();
386+
for (const value of gen) return value;
387+
})()`)).to.throw('[ASYNC-10012] Result of expression "implicitlyAsyncFn()" cannot be used in this context');
388+
});
389+
});
377390
});
378391

379392
context('error handling', () => {

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as babel from '@babel/core';
33
import runtimeSupport from './runtime-support.nocov';
44
import wrapAsFunctionPlugin from './stages/wrap-as-iife';
55
import makeMaybeAsyncFunctionPlugin from './stages/transform-maybe-await';
6+
import { AsyncRewriterErrors } from './error-codes';
67

78
/**
89
* General notes for this package:
@@ -49,7 +50,12 @@ export default class AsyncWriter {
4950
require('@babel/plugin-transform-destructuring').default
5051
]);
5152
code = this.step(code, [wrapAsFunctionPlugin]);
52-
code = this.step(code, [makeMaybeAsyncFunctionPlugin]);
53+
code = this.step(code, [
54+
[
55+
makeMaybeAsyncFunctionPlugin,
56+
{ customErrorBuilder: babel.types.identifier('MongoshAsyncWriterError') }
57+
]
58+
]);
5359
return code;
5460
} catch (e) {
5561
e.message = e.message.replace('unknown: ', '');
@@ -58,6 +64,16 @@ export default class AsyncWriter {
5864
}
5965

6066
runtimeSupportCode(): string {
61-
return this.process(runtimeSupport);
67+
// The definition of MongoshAsyncWriterError is kept separately from other
68+
// code, as it is one of the few actually mongosh-specific pieces of code here.
69+
return this.process(`
70+
class MongoshAsyncWriterError extends Error {
71+
constructor(message, codeIdentifier) {
72+
const code = (${JSON.stringify(AsyncRewriterErrors)})[codeIdentifier];
73+
super(\`[\${code}] \${message}\`);
74+
this.code = code;
75+
}
76+
}
77+
${runtimeSupport}`);
6278
}
6379
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Note: Codes are shared with the old async rewriter for now, hence starting at 10012
2+
3+
/**
4+
* @mongoshErrors
5+
*/
6+
enum AsyncRewriterErrors {
7+
/**
8+
* Signals the use of a Mongosh API call in a place where it is not supported.
9+
* This occurs inside of constructors and (non-async) generator functions.
10+
*
11+
* Examples causing error:
12+
* ```
13+
* class SomeClass {
14+
* constructor() {
15+
* this.list = db.coll.find().toArray();
16+
* }
17+
* }
18+
*
19+
* function*() {
20+
* yield* db.coll.find().toArray();
21+
* }
22+
* ```
23+
*
24+
* **Solution: Do not use calls directly in such functions. If necessary, place these calls in an inner 'async' function.**
25+
*/
26+
SyntheticPromiseInAlwaysSyncContext = 'ASYNC-10012'
27+
}
28+
29+
export {
30+
AsyncRewriterErrors
31+
};

packages/async-rewriter2/src/stages/transform-maybe-await.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ export default ({ types: t }: { types: typeof BabelTypes }): babel.PluginObj<{ f
7575
const assertNotSyntheticPromiseTemplate = babel.template.statement(`
7676
function ANSP_IDENTIFIER(p, s) {
7777
if (p && p[SP_IDENTIFIER]) {
78-
throw new Error('Result of expression "' + s + '" cannot be used in this context');
78+
throw new CUSTOM_ERROR_BUILDER(
79+
'Result of expression "' + s + '" cannot be used in this context',
80+
'SyntheticPromiseInAlwaysSyncContext');
7981
}
8082
return p;
8183
}
@@ -96,10 +98,12 @@ export default ({ types: t }: { types: typeof BabelTypes }): babel.PluginObj<{ f
9698
}
9799
`);
98100

101+
const expressionHolderVariableTemplate = babel.template.statement(`
102+
let EXPRESSION_HOLDER_IDENTIFIER;`);
103+
99104
const wrapperFunctionTemplate = babel.template.statements(`
100105
let FUNCTION_STATE_IDENTIFIER = "sync",
101-
SYNC_RETURN_VALUE_IDENTIFIER,
102-
EXPRESSION_HOLDER_IDENTIFIER;
106+
SYNC_RETURN_VALUE_IDENTIFIER;
103107
104108
const ASYNC_RETURN_VALUE_IDENTIFIER = (ASYNC_TRY_CATCH_WRAPPER)();
105109
@@ -186,7 +190,7 @@ export default ({ types: t }: { types: typeof BabelTypes }): babel.PluginObj<{ f
186190
const functionState = path.scope.generateUidIdentifier('fs');
187191
const synchronousReturnValue = path.scope.generateUidIdentifier('srv');
188192
const asynchronousReturnValue = path.scope.generateUidIdentifier('arv');
189-
const expressionHolder = path.scope.generateUidIdentifier('ex');
193+
const expressionHolder = existingIdentifiers?.expressionHolder ?? path.scope.generateUidIdentifier('ex');
190194
const markSyntheticPromise = existingIdentifiers?.markSyntheticPromise ?? path.scope.generateUidIdentifier('msp');
191195
const isSyntheticPromise = existingIdentifiers?.isSyntheticPromise ?? path.scope.generateUidIdentifier('isp');
192196
const assertNotSyntheticPromise = existingIdentifiers?.assertNotSyntheticPromise ?? path.scope.generateUidIdentifier('ansp');
@@ -225,6 +229,12 @@ export default ({ types: t }: { types: typeof BabelTypes }): babel.PluginObj<{ f
225229
}),
226230
{ [isGeneratedHelper]: true }
227231
),
232+
Object.assign(
233+
expressionHolderVariableTemplate({
234+
EXPRESSION_HOLDER_IDENTIFIER: expressionHolder
235+
}),
236+
{ [isGeneratedHelper]: true }
237+
)
228238
];
229239
const promiseHelpers = existingIdentifiers ? [] : [
230240
...commonHelpers,
@@ -254,7 +264,8 @@ export default ({ types: t }: { types: typeof BabelTypes }): babel.PluginObj<{ f
254264
Object.assign(
255265
assertNotSyntheticPromiseTemplate({
256266
ANSP_IDENTIFIER: assertNotSyntheticPromise,
257-
SP_IDENTIFIER: syntheticPromiseSymbol
267+
SP_IDENTIFIER: syntheticPromiseSymbol,
268+
CUSTOM_ERROR_BUILDER: (this as any).opts.customErrorBuilder ?? t.identifier('Error')
258269
}),
259270
{ [isGeneratedHelper]: true }
260271
)
@@ -307,7 +318,6 @@ export default ({ types: t }: { types: typeof BabelTypes }): babel.PluginObj<{ f
307318
FUNCTION_STATE_IDENTIFIER: functionState,
308319
SYNC_RETURN_VALUE_IDENTIFIER: synchronousReturnValue,
309320
ASYNC_RETURN_VALUE_IDENTIFIER: asynchronousReturnValue,
310-
EXPRESSION_HOLDER_IDENTIFIER: expressionHolder,
311321
MSP_IDENTIFIER: markSyntheticPromise,
312322
ASYNC_TRY_CATCH_WRAPPER: asyncTryCatchWrapper
313323
});

0 commit comments

Comments
 (0)