Skip to content

Commit

Permalink
Workaround to handle transpilation of classes that extend native Erro…
Browse files Browse the repository at this point in the history
…r classes.

Fixes #2102

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=137538300
  • Loading branch information
brad4d authored and blickly committed Oct 29, 2016
1 parent d17a0c0 commit abe8cdd
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 55 deletions.
156 changes: 101 additions & 55 deletions src/com/google/javascript/jscomp/Es6ConvertSuperConstructorCalls.java
Expand Up @@ -26,6 +26,8 @@
/** Converts {@code super()} calls. This has to run after typechecking. */
public final class Es6ConvertSuperConstructorCalls
implements NodeTraversal.Callback, HotSwapCompilerPass {
private static final String TMP_ERROR = "$jscomp$tmp$error";

private final AbstractCompiler compiler;

public Es6ConvertSuperConstructorCalls(AbstractCompiler compiler) {
Expand All @@ -40,11 +42,11 @@ public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isSuper()) {
visitSuper(n, parent);
visitSuper(t, n, parent);
}
}

private void visitSuper(Node node, Node parent) {
private void visitSuper(NodeTraversal t, Node node, Node parent) {
// NOTE: When this pass runs:
// - ES6 classes have already been rewritten as ES5 functions.
// - All instances of super() that are not super constructor calls have been rewritten.
Expand All @@ -58,67 +60,112 @@ private void visitSuper(Node node, Node parent) {
// so just drop it.
NodeUtil.getEnclosingStatement(node).detach();
compiler.reportCodeChange();
} else if (parent.isCall()) {
visitSuperCall(node, parent);
} else {
visitSuperApplyCall(node, parent);
// super() or super.apply()
Node superCall = parent.isCall() ? parent : parent.getParent();
String superClassQName = getSuperClassQName(superCall);
Node newSuperCall = createNewSuperCall(superClassQName, superCall);
if (isNativeErrorType(t, superClassQName)) {
replaceNativeErrorSuperCall(superCall, newSuperCall);
} else {
superCall.getParent().replaceChild(superCall, newSuperCall);
}
compiler.reportCodeChange();
}
}

/**
* Converts {@code super(..args..)} to {@code SuperClass.call(this, ..args..)}
*
* @param superNode
* @param superCall
*/
private void visitSuperCall(Node superNode, Node superCall) {
private Node createNewSuperCall(String superClassQName, Node superCall) {
checkArgument(superCall.isCall(), superCall);
checkArgument(superCall.getFirstChild() == superNode, superCall);

Node superClassQName = createSuperClassQNameNode(superCall);
Node superClassDotCall = IR.getprop(superClassQName, IR.string("call"));
superClassDotCall.useSourceInfoFromForTree(superNode);
Node newSuperCall = NodeUtil.newCallNode(superClassDotCall, IR.thisNode());
newSuperCall.useSourceInfoIfMissingFromForTree(superCall);
superCall.removeFirstChild();
newSuperCall.addChildrenToBack(superCall.removeChildren());
superCall.getParent().replaceChild(superCall, newSuperCall);
compiler.reportCodeChange();
Node newSuperCall = superCall.cloneTree();
Node callee = newSuperCall.getFirstChild();

if (callee.isSuper()) {
// super(...) -> super.call(this, ...)
Node superClassDotCall =
IR.getprop(NodeUtil.newQName(compiler, superClassQName), IR.string("call"))
.useSourceInfoFromForTree(callee);
newSuperCall.replaceChild(callee, superClassDotCall);
newSuperCall.putBooleanProp(Node.FREE_CALL, false); // callee is now a getprop
newSuperCall.addChildAfter(IR.thisNode().useSourceInfoFrom(callee), superClassDotCall);
} else {
// super.apply(null|this, ...) -> SuperClass.apply(this, ...)
checkState(callee.isGetProp(), callee);
Node applyNode = checkNotNull(callee.getSecondChild());
checkState(applyNode.getString().equals("apply"), applyNode);

Node superDotApply = newSuperCall.getFirstChild();
Node superNode = superDotApply.getFirstChild();
superDotApply.replaceChild(
superNode,
NodeUtil.newQName(compiler, superClassQName).useSourceInfoFromForTree(superNode));
// super.apply(null, ...) is generated by spread transpilation
// super.apply(this, arguments) is used by Es6ConvertSuper in automatically-generated
// constructors.
Node nullOrThisNode = newSuperCall.getSecondChild();
if (!nullOrThisNode.isThis()) {
checkState(nullOrThisNode.isNull(), nullOrThisNode);
newSuperCall.replaceChild(nullOrThisNode, IR.thisNode().useSourceInfoFrom(nullOrThisNode));
}
}
return newSuperCall;
}

/**
* Converts {@code super.apply(null|this, ..args..)} to {@code SuperClass.apply(this, ..args..)}.
*
* @param superNode
* @param superDotApply
*/
private void visitSuperApplyCall(Node superNode, Node superDotApply) {
// super.apply(null, ...) is generated by spread transpilation
// super.apply(this, arguments) is used by Es6ConvertSuper in automatically-generated
// constructors.
checkArgument(superDotApply.isGetProp(), superDotApply);
Node applyNode = checkNotNull(superDotApply.getSecondChild());
checkState(applyNode.getString().equals("apply"), applyNode);

Node superCall = superDotApply.getParent();
checkState(superCall.isCall(), superCall);
checkState(superCall.getFirstChild() == superDotApply, superCall);

Node superClassQName = createSuperClassQNameNode(superCall);
superClassQName.useSourceInfoFromForTree(superNode);
superDotApply.replaceChild(superNode, superClassQName);

Node nullOrThisNode = superCall.getSecondChild();
if (nullOrThisNode.isNull()) {
Node thisNode = IR.thisNode().useSourceInfoFrom(nullOrThisNode);
superCall.replaceChild(nullOrThisNode, thisNode);
} else {
checkState(nullOrThisNode.isThis(), nullOrThisNode);
private void replaceNativeErrorSuperCall(Node superCall, Node newSuperCall) {
// The native error class constructors always return a new object instead of initializing
// `this`, so a workaround is needed.
Node superStatement = NodeUtil.getEnclosingStatement(superCall);
Node body = superStatement.getParent();
checkState(body.isBlock(), body);

// var $jscomp$tmp$error;
Node getError = IR.var(IR.name(TMP_ERROR)).useSourceInfoIfMissingFromForTree(superCall);
body.addChildBefore(getError, superStatement);

// Create an expression to initialize `this` from temporary Error object at the point
// where super.apply() was called.
// $jscomp$tmp$error = Error.call(this, ...),
Node getTmpError = IR.assign(IR.name(TMP_ERROR), newSuperCall);
// this.message = $jscomp$tmp$error.message,
Node copyMessage =
IR.assign(
IR.getprop(IR.thisNode(), IR.string("message")),
IR.getprop(IR.name(TMP_ERROR), IR.string("message")));

// Old versions of IE Don't set stack until the object is thrown, and won't set it then
// if it already exists on the object.
// ('stack' in $jscomp$tmp$error) && (this.stack = $jscomp$tmp$error.stack)
Node setStack =
IR.and(
IR.in(IR.string("stack"), IR.name(TMP_ERROR)),
IR.assign(
IR.getprop(IR.thisNode(), IR.string("stack")),
IR.getprop(IR.name(TMP_ERROR), IR.string("stack"))));
// TODO(bradfordcsmith): The spec says super() should return `this`, but Angular2 errors.ts
// currently depends on it returning the newly created Error object.
Node superErrorExpr =
IR.comma(IR.comma(IR.comma(getTmpError, copyMessage), setStack), IR.name(TMP_ERROR))
.useSourceInfoIfMissingFromForTree(superCall);
superCall.getParent().replaceChild(superCall, superErrorExpr);
}

private boolean isNativeErrorType(NodeTraversal t, String superClassName) {
switch (superClassName) {
// All Error classes listed in the ECMAScript spec as of 2016
case "Error":
case "EvalError":
case "RangeError":
case "ReferenceError":
case "SyntaxError":
case "TypeError":
case "URIError":
Var objectVar = t.getScope().getVar(superClassName);
return objectVar == null || objectVar.isExtern();
default:
return false;
}
compiler.reportCodeChange();
}

private Node createSuperClassQNameNode(Node superCall) {
private String getSuperClassQName(Node superCall) {
// Find the $jscomp.inherits() call and take the super class name from there.
Node enclosingConstructor = checkNotNull(NodeUtil.getEnclosingFunction(superCall));
String className = NodeUtil.getNameNode(enclosingConstructor).getQualifiedName();
Expand All @@ -129,10 +176,9 @@ private Node createSuperClassQNameNode(Node superCall) {
statement = statement.getNext()) {
String superClassName = getSuperClassNameIfIsInheritsStatement(statement, className);
if (superClassName != null) {
return NodeUtil.newQName(compiler, superClassName);
return superClassName;
}
}

throw new IllegalStateException("$jscomp.inherits() call not found.");
}

Expand Down
93 changes: 93 additions & 0 deletions test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java
Expand Up @@ -601,6 +601,99 @@ public void testExtends() {
"var C = function(var_args) {};"));
}

public void testExtendNonNativeError() {
test(
LINE_JOINER.join(
"class Error {",
" /** @param {string} msg */",
" constructor(msg) {",
" /** @const */ this.message = msg;",
" }",
"}",
"class C extends Error {}"), // autogenerated constructor
LINE_JOINER.join(
"/** @constructor @struct",
" * @param {string} msg",
" */",
"var Error = function(msg) {",
" /** @const */ this.message = msg;",
"};",
"/** @constructor @struct",
" * @extends {Error}",
" * @param {...?} var_args",
" */",
"var C = function(var_args) { Error.apply(this, arguments); };",
"$jscomp.inherits(C, Error);"));
test(
LINE_JOINER.join(
"",
"class Error {",
" /** @param {string} msg */",
" constructor(msg) {",
" /** @const */ this.message = msg;",
" }",
"}",
"class C extends Error {",
" constructor() {",
" super('C error');", // explicit super() call
" }",
"}"),
LINE_JOINER.join(
"/** @constructor @struct",
" * @param {string} msg",
" */",
"var Error = function(msg) {",
" /** @const */ this.message = msg;",
"};",
"/** @constructor @struct",
" * @extends {Error}",
" */",
"var C = function() { Error.call(this, 'C error'); };",
"$jscomp.inherits(C, Error);"));
}

public void testExtendNativeError() {
test(
"class C extends Error {}", // autogenerated constructor
LINE_JOINER.join(
"/** @constructor @struct",
" * @extends {Error}",
" * @param {...?} var_args",
" */",
"var C = function(var_args) {",
" var $jscomp$tmp$error;",
" $jscomp$tmp$error = Error.apply(this, arguments),",
" this.message = $jscomp$tmp$error.message,",
" ('stack' in $jscomp$tmp$error) && (this.stack = $jscomp$tmp$error.stack),",
" $jscomp$tmp$error;",
"};",
"$jscomp.inherits(C, Error);"));
test(
LINE_JOINER.join(
"",
"class C extends Error {",
" constructor() {",
" var self = super('C error') || this;", // explicit super() call in an expression
" }",
"}"),
LINE_JOINER.join(
"/** @constructor @struct",
" * @extends {Error}",
" */",
"var C = function() {",
" var $jscomp$tmp$error;",
" var self =",
" ($jscomp$tmp$error = Error.call(this, 'C error'),",
" this.message = $jscomp$tmp$error.message,",
" ('stack' in $jscomp$tmp$error) && (this.stack = $jscomp$tmp$error.stack),",
// TODO(bradfordcsmith): The spec says super() should return `this`, but Angular2
// errors.ts currently depends on it returning the newly created Error object.
" $jscomp$tmp$error)",
" || this;",
"};",
"$jscomp.inherits(C, Error);"));
}

public void testInvalidExtends() {
testError("class C extends foo() {}", Es6ToEs3Converter.DYNAMIC_EXTENDS_TYPE);
testError("class C extends function(){} {}", Es6ToEs3Converter.DYNAMIC_EXTENDS_TYPE);
Expand Down
52 changes: 52 additions & 0 deletions test/com/google/javascript/jscomp/runtime_tests/class_test.js
Expand Up @@ -155,3 +155,55 @@ function testImplicitSuperConstructorCall() {
assertEquals(1234, B.create().x);
}

/**
* Does the current environment set `stack` on Error objects either when
* they are created or when they are thrown?
* @returns {boolean}
*/
function thrownErrorHasStack() {
const e = new Error();
try {
throw e;
} finally {
return 'stack' in e;
}
}

function testExtendsError() {
class MyError extends Error {
constructor() {
super('my message');
}
}
try {
throw new MyError();
} catch (e) {
// IE11 and earlier don't set the stack field until the error is actually
// thrown. IE8 doesn't set it at all.
assertTrue(e instanceof MyError);
assertEquals('my message', e.message);
if (thrownErrorHasStack()) {
assertNonEmptyString(e.stack);
}
}
}

function testSupportsErrorExtensionHack() {
class MyError extends Error {
constructor() {
const superResult = super('my message');
this.superResult = superResult;
}
}
const e = new MyError();
if (MyError.toString().startsWith('class')) {
// Browser should have correct behavior for uncompiled code.
assertEquals(e, e.superResult);
} else {
// TODO(bradfordcsmith): The spec says super() should return `this`,
// but Angular2 errors.ts currently depends on incorrect compiler behavior
// that causes it to return a newly created Error object.
// https://github.com/angular/angular/issues/12575
assertNotEquals(e, e.superResult);
}
}

0 comments on commit abe8cdd

Please sign in to comment.