Skip to content

Commit fadcbb4

Browse files
authored
fix(async-rewriter2): properly stringify functions MONGOSH-664 (#753)
Add a copy of the original source code at the beginning of transformed functions, so that `Function.prototype.toString` works properly for them.
1 parent 5cccf81 commit fadcbb4

File tree

5 files changed

+73
-0
lines changed

5 files changed

+73
-0
lines changed

packages/async-rewriter2/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ made for readability).
9292

9393
```js
9494
(() => {
95+
// Keep a copy of the original source code for Function.prototype.toString.
96+
'<async_rewriter>(() => {\n return db.test.find().toArray();\n})</>';
9597
const _syntheticPromise = Symbol.for("@@mongosh.syntheticPromise");
9698

9799
function _markSyntheticPromise(p) {

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,30 @@ describe('AsyncWriter', () => {
578578
expect(plainFn).to.have.been.calledWith(6, 6, set);
579579
});
580580
});
581+
582+
context('Function.prototype.toString', () => {
583+
it('returns the original function source', () => {
584+
expect(runTranspiledCode('Function.prototype.toString.call(() => {})'))
585+
.to.equal('() => {}');
586+
expect(runTranspiledCode('Function.prototype.toString.call(function () {})'))
587+
.to.equal('function () {}');
588+
expect(runTranspiledCode('Function.prototype.toString.call(async function () {})'))
589+
.to.equal('async function () {}');
590+
expect(runTranspiledCode('Function.prototype.toString.call(function* () {})'))
591+
.to.equal('function* () {}');
592+
expect(runTranspiledCode('Function.prototype.toString.call(async function* () {})'))
593+
.to.equal('async function* () {}');
594+
expect(runTranspiledCode('Function.prototype.toString.call((class { method() {} }).prototype.method)'))
595+
.to.equal('method() {}');
596+
});
597+
598+
it('lets us not worry about special characters', () => {
599+
expect(runTranspiledCode('Function.prototype.toString.call(() => {\n method();\n})'))
600+
.to.equal('() => {\n method();\n }');
601+
expect(runTranspiledCode('Function.prototype.toString.call(() => { const 八 = 8; })'))
602+
.to.equal('() => {\n const 八 = 8;\n }');
603+
});
604+
});
581605
});
582606

583607
context('error messages', () => {

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,16 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
362362
return;
363363
}
364364

365+
const originalSource = path.parent.start !== undefined ?
366+
(this.file as any).code.slice(path.parent.start, path.parent.end) :
367+
'function () { [unknown code] }';
368+
// Encode using UTF-16 + hex encoding so we don't have to worry about
369+
// special characters.
370+
const encodedOriginalSource =
371+
[...originalSource].map(char => char.charCodeAt(0).toString(16).padStart(4, '0')).join('');
372+
const originalSourceNode = t.expressionStatement(
373+
t.stringLiteral(`<async_rewriter>${encodedOriginalSource}</>`));
374+
365375
// A parent function might have a set of existing helper methods.
366376
// If it does, we re-use the functionally equivalent ones.
367377
const existingIdentifiers: AsyncFunctionIdentifiers | null =
@@ -436,6 +446,7 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
436446
// a re-throwing try/catch around the body so that we can perform
437447
// error message adjustment through the CatchClause handler below.
438448
path.replaceWith(t.blockStatement([
449+
originalSourceNode,
439450
...promiseHelpers,
440451
rethrowTemplate({
441452
ORIGINAL_CODE: path.node.body
@@ -464,6 +475,7 @@ export const makeMaybeAsyncFunctionPlugin = ({ types: t }: { types: typeof Babel
464475

465476
// Generate the wrapper function. See the README for a full code snippet.
466477
path.replaceWith(t.blockStatement([
478+
originalSourceNode,
467479
...promiseHelpers,
468480
...wrapperFunction
469481
]));

packages/async-rewriter2/src/runtime-support.nocov.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,4 +468,17 @@ module.exports = '(' + function() {
468468
const array = Array.prototype.filter.call(this, func, thisArg);
469469
return new (this.constructor)(array);
470470
};
471+
472+
// Special addition: Function.prototype.toString!
473+
const origFptS = Function.prototype.toString;
474+
Function.prototype.toString = function() {
475+
const source = origFptS.call(this, arguments);
476+
const match = source.match(/^[^"]*"<async_rewriter>(?<encoded>[a-z0-9]+)<\/>";/);
477+
if (match) {
478+
// Decode using hex + UTF-16
479+
return String.fromCharCode(
480+
...match.groups.encoded.match(/.{4}/g).map(hex => parseInt(hex, 16)));
481+
}
482+
return source;
483+
};
471484
} + ')();';

packages/cli-repl/test/e2e.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,28 @@ describe('e2e', function() {
342342
shell.assertNoErrors();
343343
});
344344

345+
it('rewrites async properly for mapReduce', async() => {
346+
// This is being run under the new async rewriter because the old one
347+
// did not support mapReduce at all (because of needing 'this').
348+
// Once the new async rewriter is the default, this block can be removed.
349+
shell = TestShell.start({
350+
args: [ await testServer.connectionString() ],
351+
env: { ...process.env, MONGOSH_ASYNC_REWRITER2: '1' }
352+
});
353+
await shell.waitForPrompt();
354+
shell.assertNoErrors();
355+
356+
await shell.executeLine(`use ${dbName}`);
357+
await shell.executeLine('db.test.insertMany([{i:1},{i:2},{i:3},{i:4}]);');
358+
const result = await shell.executeLine(`db.test.mapReduce(function() {
359+
emit(this.i % 2, this.i);
360+
}, function(key, values) {
361+
return Array.sum(values);
362+
}, { out: { inline: 1 } }).results`);
363+
expect(result).to.include('{ _id: 0, value: 6 }');
364+
expect(result).to.include('{ _id: 1, value: 4 }');
365+
});
366+
345367
it('expands explain output indefinitely', async() => {
346368
await shell.executeLine('explainOutput = db.test.find().explain()');
347369
await shell.executeLine('explainOutput.a = {b:{c:{d:{e:{f:{g:{h:{i:{j:{}}}}}}}}}}');

0 commit comments

Comments
 (0)