Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 57 additions & 2 deletions packages/async-rewriter2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ they reach their first `await` expression, and the fact that we can determine
which `Promise`s need `await`ing by marking them as such using decorators
on the API surface.

The transformation takes place in two main steps.
The transformation takes place in three main steps.

### Step one: IIFE wrapping

Expand Down Expand Up @@ -63,7 +63,62 @@ function foo() {
Note how identifiers remain accessible in the outside environment, including
top-level functions being hoisted to the outside.

### Step two: Async function wrapping
### Step two: Making certain exceptions uncatchable

In order to support Ctrl+C properly, we add a type of exception that is not
catchable by userland code.

For example,

```js
try {
foo3();
} catch {
bar3();
}
```

is transformed into

```js
try {
foo3();
} catch (_err) {
if (!err || !_err[Symbol.for('@@mongosh.uncatchable')]) {
bar3();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for the sake of correctness there should be the throw in the else branch

Suggested change
}
} else {
throw _err;
}

}
```

and

```js
try {
foo1();
} catch (err) {
bar1(err);
} finally {
baz();
}
```

into

```js
let _isCatchable;

try {
foo1();
} catch (err) {
_isCatchable = !err || !err[Symbol.for('@@mongosh.uncatchable')];

if (_isCatchable) bar1(err); else throw err;
} finally {
if (_isCatchable) baz();
}
```

### Step three: Async function wrapping

We perform three operations:

Expand Down
183 changes: 183 additions & 0 deletions packages/async-rewriter2/src/async-writer-babel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ describe('AsyncWriter', () => {
return Object.assign(
Promise.resolve(implicitlyAsyncValue),
{ [Symbol.for('@@mongosh.syntheticPromise')]: true });
},
throwUncatchable() {
throw Object.assign(
new Error('uncatchable!'),
{ [Symbol.for('@@mongosh.uncatchable')]: true });
}
});
runTranspiledCode = (code: string, context?: any) => {
Expand Down Expand Up @@ -760,4 +765,182 @@ describe('AsyncWriter', () => {
});
});
});

context('uncatchable exceptions', () => {
it('allows catching regular exceptions', () => {
const result = runTranspiledCode(`
(() => {
try {
throw new Error('generic error');
} catch (err) {
return ({ caught: err });
}
})();`);
expect(result.caught.message).to.equal('generic error');
});

it('allows catching regular exceptions with destructuring catch (object)', () => {
const result = runTranspiledCode(`
(() => {
try {
throw new Error('generic error');
} catch ({ message }) {
return ({ caught: message });
}
})();`);
expect(result.caught).to.equal('generic error');
});


it('allows catching regular exceptions with destructuring catch (array)', () => {
const result = runTranspiledCode(`
(() => {
try {
throw [ 'foo' ];
} catch ([message]) {
return ({ caught: message });
}
})();`);
expect(result.caught).to.equal('foo');
});

it('allows catching regular exceptions with destructuring catch (assignable)', () => {
const result = runTranspiledCode(`
(() => {
try {
throw [ 'foo' ];
} catch ([message]) {
message = 42;
return ({ caught: message });
}
})();`);
expect(result.caught).to.equal(42);
});

it('allows rethrowing regular exceptions', () => {
try {
runTranspiledCode(`
(() => {
try {
throw new Error('generic error');
} catch (err) {
throw err;
}
})();`);
expect.fail('missed exception');
} catch (err) {
expect(err.message).to.equal('generic error');
}
});

it('allows returning from finally', () => {
const result = runTranspiledCode(`
(() => {
try {
throw new Error('generic error');
} catch (err) {
return ({ caught: err });
} finally {
return 'finally';
}
})();`);
expect(result).to.equal('finally');
});

it('allows finally without catch', () => {
const result = runTranspiledCode(`
(() => {
try {
throw new Error('generic error');
} finally {
return 'finally';
}
})();`);
expect(result).to.equal('finally');
});

it('allows throwing primitives', () => {
const result = runTranspiledCode(`
(() => {
try {
throw null;
} catch (err) {
return ({ caught: err });
}
})();`);
expect(result.caught).to.equal(null);
});

it('allows throwing primitives with finally', () => {
const result = runTranspiledCode(`
(() => {
try {
throw null;
} catch (err) {
return ({ caught: err });
} finally {
return 'finally';
}
})();`);
expect(result).to.equal('finally');
});

it('does not catch uncatchable exceptions', () => {
try {
runTranspiledCode(`
(() => {
try {
throwUncatchable();
} catch (err) {
return ({ caught: err });
}
})();`);
expect.fail('missed exception');
} catch (err) {
expect(err.message).to.equal('uncatchable!');
}
});

it('does not catch uncatchable exceptions with empty catch clause', () => {
try {
runTranspiledCode(`
(() => {
try {
throwUncatchable();
} catch { }
})();`);
expect.fail('missed exception');
} catch (err) {
expect(err.message).to.equal('uncatchable!');
}
});

it('does not catch uncatchable exceptions with finalizer', () => {
try {
runTranspiledCode(`
(() => {
try {
throwUncatchable();
} catch { } finally { return; }
})();`);
expect.fail('missed exception');
} catch (err) {
expect(err.message).to.equal('uncatchable!');
}
});

it('does not catch uncatchable exceptions with only finalizer', () => {
try {
runTranspiledCode(`
(() => {
try {
throwUncatchable();
} finally { return; }
})();`);
expect.fail('missed exception');
} catch (err) {
expect(err.message).to.equal('uncatchable!');
}
});
});
});
4 changes: 3 additions & 1 deletion packages/async-rewriter2/src/async-writer-babel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
import * as babel from '@babel/core';
import runtimeSupport from './runtime-support.nocov';
import wrapAsFunctionPlugin from './stages/wrap-as-iife';
import uncatchableExceptionPlugin from './stages/uncatchable-exceptions';
import makeMaybeAsyncFunctionPlugin from './stages/transform-maybe-await';
import { AsyncRewriterErrors } from './error-codes';

/**
* General notes for this package:
*
* This package contains two babel plugins used in async rewriting, plus a helper
* This package contains three babel plugins used in async rewriting, plus a helper
* to apply these plugins to plain code.
*
* If you have not worked with babel plugins,
Expand Down Expand Up @@ -51,6 +52,7 @@ export default class AsyncWriter {
require('@babel/plugin-transform-destructuring').default
]);
code = this.step(code, [wrapAsFunctionPlugin]);
code = this.step(code, [uncatchableExceptionPlugin]);
code = this.step(code, [
[
makeMaybeAsyncFunctionPlugin,
Expand Down
106 changes: 106 additions & 0 deletions packages/async-rewriter2/src/stages/uncatchable-exceptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as babel from '@babel/core';
import * as BabelTypes from '@babel/types';

/**
* In this step, we transform try/catch statements so that there are specific
* types of exceptions that they cannot catch (marked by
* Symbol.for('@@mongosh.uncatchable')) or run finally blocks for.
*/
export default ({ types: t }: { types: typeof BabelTypes }): babel.PluginObj<{}> => {
// We mark already-visited try/catch statements using these symbols.
function asNodeKey(v: any): keyof babel.types.Node { return v; }
const isGeneratedTryCatch = asNodeKey(Symbol('isGeneratedTryCatch'));
const notUncatchableCheck = babel.template.expression(`
(!ERR_IDENTIFIER || !ERR_IDENTIFIER[Symbol.for('@@mongosh.uncatchable')])
`);

return {
visitor: {
TryStatement(path) {
if (path.node[isGeneratedTryCatch]) return;
const { block, finalizer } = path.node;
let catchParam: babel.types.Identifier;
let handler: babel.types.CatchClause;
const fallbackCatchParam = path.scope.generateUidIdentifier('err');

if (path.node.handler) {
if (path.node.handler.param?.type === 'Identifier') {
// Classic catch(err) { ... }. We're good, no need to change anything.
catchParam = path.node.handler.param;
handler = path.node.handler;
} else if (path.node.handler.param) {
// Destructuring catch({ ... }) { ... body ... }. Transform to
// catch(err) { let ... = err; ... body ... }.
catchParam = fallbackCatchParam;
handler = t.catchClause(catchParam, t.blockStatement([
t.variableDeclaration('let', [
t.variableDeclarator(path.node.handler.param, catchParam)
]),
path.node.handler.body
]));
} else {
//
catchParam = fallbackCatchParam;
handler = path.node.handler;
}
} else {
// try {} finally {} without 'catch' is valid -- if we encounter that,
// pretend that there is a dummy catch (err) { throw err; }
catchParam = fallbackCatchParam;
handler = t.catchClause(catchParam, t.blockStatement([
t.throwStatement(catchParam)
]));
}

if (!finalizer) {
// No finalizer -> no need to keep track of state outside the catch {}
// block itself. This is a bit simpler.
path.replaceWith(Object.assign(
t.tryStatement(
block,
t.catchClause(
catchParam,
t.blockStatement([
// if (!err[isUncatchableSymbol]) { ... } else throw err;
t.ifStatement(
notUncatchableCheck({ ERR_IDENTIFIER: catchParam }),
handler.body,
t.throwStatement(catchParam))
])
)
),
{ [isGeneratedTryCatch]: true }));
} else {
// finalizer -> need to store whether the exception was catchable
// (i.e. whether the finalizer is allowed to run) outside of the
// try/catch/finally block.
const isCatchable = path.scope.generateUidIdentifier('_isCatchable');
path.replaceWithMultiple([
t.variableDeclaration('let', [t.variableDeclarator(isCatchable)]),
Object.assign(
t.tryStatement(
block,
t.catchClause(
catchParam,
t.blockStatement([
// isCatchable = !err[isUncatchableSymbol]
t.expressionStatement(
t.assignmentExpression('=',
isCatchable,
notUncatchableCheck({ ERR_IDENTIFIER: catchParam }))),
// if (isCatchable) { ... } else throw err;
t.ifStatement(isCatchable, handler.body, t.throwStatement(catchParam))
]),
),
t.blockStatement([
// if (isCatchable) { ... }
t.ifStatement(isCatchable, finalizer)
])
),
{ [isGeneratedTryCatch]: true })
]);
}
}
}
};
};