Skip to content
This repository was archived by the owner on Jan 6, 2020. It is now read-only.

Hoist await and yield #6

Closed
wants to merge 1 commit into from
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
43 changes: 42 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ var buildHelper = template([
'}'
].join('\n'));

var hoistExpression = template('var ARG = EXP;');

var assertionVisitor = {
CallExpression: function (path, state) {
if (isThrowsMember(path.get('callee'))) {
Expand All @@ -40,14 +42,24 @@ var assertionVisitor = {
return;
}

var hoisted = hoistAwaitAndYield(path, arg0);

path.node.arguments[0] = wrapWithHelper({
HELPER_ID: t.identifier(this.avaThrowHelper()),
EXP: arg0,
EXP: hoisted ? hoisted.arg0 : arg0,
LINE: t.numericLiteral(arg0.loc.start.line),
COLUMN: t.numericLiteral(arg0.loc.start.column),
SOURCE: t.stringLiteral(state.file.code.substring(arg0.start, arg0.end)),
FILE: t.stringLiteral(state.file.opts.filename)
}).expression;

if (hoisted) {
path.replaceWithMultiple(
hoisted.expressions.concat(
t.expressionStatement(path.node)
Copy link
Contributor

Choose a reason for hiding this comment

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

You are replacing the Callxpression by an ExpressionStatement here. Would the following test code stll work, or would Babel crash or something because you are building an odd AST?

test('should foo...', async t => {
  t.is(
      t.throws(foo(await quux())).message,
     'Could not find X'
   );
});

My worry is that you can't AFAIK pass a statement as the argument of a function (here, the `t.is(){ call). But I don't know how Babel would react to that. Would you mind adding a test case and see how that pans out?
I would be fine with not entirely supporting this edge case, but it should at the very least not crash.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yea I doubt that'll work… in your example t.throws is inside an ArgumentExpression so we'd have to walk the path back to the test implementation and detect any ArgumentExpression nodes, then prepend the hosted statements to node's parent.

But then what about:

test(async t => {
  (function* () {
    const err = await t.throws(foo(yield))
    t.true(err.message === 'hahaha')
  })()
})

That scenario is further complicated by this plugin running after async/await has been transpiled into a generator…

Copy link
Member

Choose a reason for hiding this comment

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

So, what should we do about it? Just ignore it as an edge-case?

Copy link
Member Author

Choose a reason for hiding this comment

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

I haven't had the time to get back to this. We could explore whether we can run this plugin before any other presets and plugins. Alternatively the generator compilation (for Node.js v4 and up) should reduce to yield expressions.

Copy link
Member

@sindresorhus sindresorhus Nov 5, 2016

Choose a reason for hiding this comment

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

Relevant: babel/babel#3281


Alternatively the generator compilation (for Node.js v4 and up) should reduce to yield expressions.

I would be ok with only supporting Node.js 4 for this.

)
);
}
}
}
};
Expand Down Expand Up @@ -83,3 +95,32 @@ function isThrowsMember(path) {
path.get('property').isIdentifier({name: 'notThrows'})
);
}

function hoistAwaitAndYield(path, arg0) {
if (t.isAwaitExpression(arg0) || t.isYieldExpression(arg0)) {
var arg = path.scope.generateUidIdentifier('arg');
return {
expressions: [hoistExpression({ARG: arg, EXP: arg0})],
arg0: arg
};
}

if (t.isCallExpression(arg0)) {
var expressions = [];
arg0.arguments = arg0.arguments.map(function (arg) {
var hoisted = hoistAwaitAndYield(path, arg);
if (!hoisted) {
return arg;
}

expressions = expressions.concat(hoisted.expressions);
return hoisted.arg0;
});
return {
expressions: expressions,
arg0: arg0
};
}

return null;
}
64 changes: 58 additions & 6 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,24 +51,28 @@ const HELPER = `function _avaThrowsHelper(fn, data) {
}
}\n`;

function wrapped(throws, expression, line, column) {
function wrapped(throws, line, column, expression, source = expression) { // eslint-disable-line max-params
return `t.${throws}(_avaThrowsHelper(function () {
return ${expression};
}, {
line: ${line},
column: ${column},
source: "${expression}",
source: "${source}",
filename: "some-file.js"
}));`;
}

function indent(str) {
return str.replace(/\n/g, '\n ').trimRight();
}

test('creates a helper', t => {
const input = 't.throws(foo())';
const {code} = transform(input);

const expected = [
HELPER,
wrapped('throws', 'foo()', 1, 9)
wrapped('throws', 1, 9, 'foo()')
].join('\n');

t.is(code, expected);
Expand All @@ -81,14 +85,62 @@ test('creates the helper only once', t => {

const expected = [
HELPER,
wrapped('throws', 'foo()', 1, 9),
wrapped('throws', 'bar()', 2, 9)
wrapped('throws', 1, 9, 'foo()'),
wrapped('throws', 2, 9, 'bar()')
].join('\n');

t.is(code, expected);
addExample(input, code);
});

test('hoists await expressions', t => {
const input = `async function test() {
t.throws(foo(await bar(), await baz(), qux));
t.throws(await quux);
}`;
const {code} = transform(input);

const expected = `${HELPER}
async function test() {
var _arg = await bar();

var _arg2 = await baz();

${indent(wrapped('throws', 2, 11, 'foo(_arg, _arg2, qux)', 'foo(await bar(), await baz(), qux)'))}

var _arg3 = await quux;

${indent(wrapped('throws', 3, 11, '_arg3', 'await quux'))}
}`;

t.is(code, expected);
addExample(input, code);
});

test('hoists yield expressions', t => {
const input = `function* test() {
t.throws(foo(yield bar(), yield baz(), qux));
t.throws(yield quux);
}`;
const {code} = transform(input);

const expected = `${HELPER}
function* test() {
var _arg = yield bar();

var _arg2 = yield baz();

${indent(wrapped('throws', 2, 11, 'foo(_arg, _arg2, qux)', 'foo(yield bar(), yield baz(), qux)'))}

var _arg3 = yield quux;

${indent(wrapped('throws', 3, 11, '_arg3', 'yield quux'))}
}`;

t.is(code, expected);
addExample(input, code);
});

test('does nothing if it does not match', t => {
const input = 't.is(foo());';
const {code} = transform(input);
Expand All @@ -103,7 +155,7 @@ test('helps notThrows', t => {

const expected = [
HELPER,
wrapped('notThrows', 'baz()', 1, 12)
wrapped('notThrows', 1, 12, 'baz()')
].join('\n');

t.is(code, expected);
Expand Down