From 98fa4de3433fcbc0e6ea9104e193dbc243d8cb8a Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 6 Apr 2021 18:38:37 +0200 Subject: [PATCH] fix(async-rewriter2): handle typeof + identifier properly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The source code comments hopefully explain well enough what’s going on here. --- .../src/async-writer-babel.spec.ts | 21 ++++++++++++++++++ .../src/stages/transform-maybe-await.ts | 22 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/packages/async-rewriter2/src/async-writer-babel.spec.ts b/packages/async-rewriter2/src/async-writer-babel.spec.ts index a6f09a9f30..643ac9aa72 100644 --- a/packages/async-rewriter2/src/async-writer-babel.spec.ts +++ b/packages/async-rewriter2/src/async-writer-babel.spec.ts @@ -14,6 +14,7 @@ describe('AsyncWriter', () => { let plainFn: sinon.SinonStub; let implicitlyAsyncMethod: sinon.SinonStub; let plainMethod: sinon.SinonStub; + let implicitlyAsyncValue: any; let ctx: any; let runTranspiledCode: (code: string, context?: any) => any; let runUntranspiledCode: (code: string, context?: any) => any; @@ -24,6 +25,7 @@ describe('AsyncWriter', () => { plainFn = sinon.stub(); implicitlyAsyncMethod = sinon.stub(); plainMethod = sinon.stub(); + implicitlyAsyncValue = undefined; asyncWriter = new AsyncWriter(); ctx = vm.createContext({ @@ -42,6 +44,11 @@ describe('AsyncWriter', () => { { [Symbol.for('@@mongosh.syntheticPromise')]: true }); }, plainMethod + }, + get implicitlyAsyncValue() { + return Object.assign( + Promise.resolve(implicitlyAsyncValue), + { [Symbol.for('@@mongosh.syntheticPromise')]: true }); } }); runTranspiledCode = (code: string, context?: any) => { @@ -345,6 +352,20 @@ describe('AsyncWriter', () => { expect(() => runTranspiledCode('const a = 42; const a = 43;')) .to.throw(/has already been declared/); }); + + it('supports typeof for un-defined variables', () => { + expect(runTranspiledCode('typeof nonexistent')).to.equal('undefined'); + }); + + it('supports typeof for implicitly awaited function calls', async() => { + implicitlyAsyncFn.resolves(0); + expect(await runTranspiledCode('typeof implicitlyAsyncFn()')).to.equal('number'); + }); + + it('supports typeof for implicitly awaited values', async() => { + implicitlyAsyncValue = 'abc'; + expect(await runTranspiledCode('typeof implicitlyAsyncValue')).to.equal('string'); + }); }); context('error handling', () => { diff --git a/packages/async-rewriter2/src/stages/transform-maybe-await.ts b/packages/async-rewriter2/src/stages/transform-maybe-await.ts index 81f6044533..7d25e214e9 100644 --- a/packages/async-rewriter2/src/stages/transform-maybe-await.ts +++ b/packages/async-rewriter2/src/stages/transform-maybe-await.ts @@ -41,6 +41,7 @@ export default ({ types: t }: { types: typeof BabelTypes }): babel.PluginObj<{ f const isGeneratedHelper = asNodeKey(Symbol('isGeneratedHelper')); const isOriginalBody = asNodeKey(Symbol('isOriginalBody')); const isAlwaysSyncFunction = asNodeKey(Symbol('isAlwaysSyncFunction')); + const isExpandedTypeof = asNodeKey(Symbol('isExpandedTypeof')); // Using this key, we store data on Function nodes that contains the identifiers // of helpers which are available inside the function. const identifierGroupKey = '@@mongosh.identifierGroup'; @@ -318,6 +319,27 @@ export default ({ types: t }: { types: typeof BabelTypes }): babel.PluginObj<{ f ...wrapperFunction ])); }, + UnaryExpression: { + enter(path) { + if (path.node.operator === 'typeof' && path.node.argument.type === 'Identifier' && !path.node[isExpandedTypeof]) { + // 'typeof foo'-style checks have a double use; they not only report + // the 'type' of an expression, but when used on identifiers, check + // whether they have been declared, and if not, return 'undefined'. + // This is annoying. We replace `typeof foo` with + // `typeof foo === 'undefined' ? 'undefined' : typeof foo`. + // The first `typeof foo` is marked as a generated helper and not + // transformed any further, the second is transformed as usual. + path.replaceWith(t.conditionalExpression( + t.binaryExpression( + '===', + { ...path.node, [isGeneratedHelper]: true, [isExpandedTypeof]: true }, + t.stringLiteral('undefined')), + t.stringLiteral('undefined'), + { ...path.node, [isExpandedTypeof]: true } + )); + } + } + }, Expression: { enter(path) { // Minor adjustment: When we encounter a 'shorthand' arrow function,