Skip to content
Merged
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
61 changes: 59 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,64 @@ 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();
} 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
Loading