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. */ /** Converts {@code super()} calls. This has to run after typechecking. */
public final class Es6ConvertSuperConstructorCalls public final class Es6ConvertSuperConstructorCalls
implements NodeTraversal.Callback, HotSwapCompilerPass { implements NodeTraversal.Callback, HotSwapCompilerPass {
private static final String TMP_ERROR = "$jscomp$tmp$error";

private final AbstractCompiler compiler; private final AbstractCompiler compiler;


public Es6ConvertSuperConstructorCalls(AbstractCompiler compiler) { public Es6ConvertSuperConstructorCalls(AbstractCompiler compiler) {
Expand All @@ -40,11 +42,11 @@ public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
@Override @Override
public void visit(NodeTraversal t, Node n, Node parent) { public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isSuper()) { 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: // NOTE: When this pass runs:
// - ES6 classes have already been rewritten as ES5 functions. // - ES6 classes have already been rewritten as ES5 functions.
// - All instances of super() that are not super constructor calls have been rewritten. // - 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. // so just drop it.
NodeUtil.getEnclosingStatement(node).detach(); NodeUtil.getEnclosingStatement(node).detach();
compiler.reportCodeChange(); compiler.reportCodeChange();
} else if (parent.isCall()) {
visitSuperCall(node, parent);
} else { } 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();
} }
} }


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

Node callee = newSuperCall.getFirstChild();
Node superClassQName = createSuperClassQNameNode(superCall);
Node superClassDotCall = IR.getprop(superClassQName, IR.string("call")); if (callee.isSuper()) {
superClassDotCall.useSourceInfoFromForTree(superNode); // super(...) -> super.call(this, ...)
Node newSuperCall = NodeUtil.newCallNode(superClassDotCall, IR.thisNode()); Node superClassDotCall =
newSuperCall.useSourceInfoIfMissingFromForTree(superCall); IR.getprop(NodeUtil.newQName(compiler, superClassQName), IR.string("call"))
superCall.removeFirstChild(); .useSourceInfoFromForTree(callee);
newSuperCall.addChildrenToBack(superCall.removeChildren()); newSuperCall.replaceChild(callee, superClassDotCall);
superCall.getParent().replaceChild(superCall, newSuperCall); newSuperCall.putBooleanProp(Node.FREE_CALL, false); // callee is now a getprop
compiler.reportCodeChange(); 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;
} }


/** private void replaceNativeErrorSuperCall(Node superCall, Node newSuperCall) {
* Converts {@code super.apply(null|this, ..args..)} to {@code SuperClass.apply(this, ..args..)}. // The native error class constructors always return a new object instead of initializing
* // `this`, so a workaround is needed.
* @param superNode Node superStatement = NodeUtil.getEnclosingStatement(superCall);
* @param superDotApply Node body = superStatement.getParent();
*/ checkState(body.isBlock(), body);
private void visitSuperApplyCall(Node superNode, Node superDotApply) {
// super.apply(null, ...) is generated by spread transpilation // var $jscomp$tmp$error;
// super.apply(this, arguments) is used by Es6ConvertSuper in automatically-generated Node getError = IR.var(IR.name(TMP_ERROR)).useSourceInfoIfMissingFromForTree(superCall);
// constructors. body.addChildBefore(getError, superStatement);
checkArgument(superDotApply.isGetProp(), superDotApply);
Node applyNode = checkNotNull(superDotApply.getSecondChild()); // Create an expression to initialize `this` from temporary Error object at the point
checkState(applyNode.getString().equals("apply"), applyNode); // where super.apply() was called.

// $jscomp$tmp$error = Error.call(this, ...),
Node superCall = superDotApply.getParent(); Node getTmpError = IR.assign(IR.name(TMP_ERROR), newSuperCall);
checkState(superCall.isCall(), superCall); // this.message = $jscomp$tmp$error.message,
checkState(superCall.getFirstChild() == superDotApply, superCall); Node copyMessage =

IR.assign(
Node superClassQName = createSuperClassQNameNode(superCall); IR.getprop(IR.thisNode(), IR.string("message")),
superClassQName.useSourceInfoFromForTree(superNode); IR.getprop(IR.name(TMP_ERROR), IR.string("message")));
superDotApply.replaceChild(superNode, superClassQName);

// Old versions of IE Don't set stack until the object is thrown, and won't set it then
Node nullOrThisNode = superCall.getSecondChild(); // if it already exists on the object.
if (nullOrThisNode.isNull()) { // ('stack' in $jscomp$tmp$error) && (this.stack = $jscomp$tmp$error.stack)
Node thisNode = IR.thisNode().useSourceInfoFrom(nullOrThisNode); Node setStack =
superCall.replaceChild(nullOrThisNode, thisNode); IR.and(
} else { IR.in(IR.string("stack"), IR.name(TMP_ERROR)),
checkState(nullOrThisNode.isThis(), nullOrThisNode); 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. // Find the $jscomp.inherits() call and take the super class name from there.
Node enclosingConstructor = checkNotNull(NodeUtil.getEnclosingFunction(superCall)); Node enclosingConstructor = checkNotNull(NodeUtil.getEnclosingFunction(superCall));
String className = NodeUtil.getNameNode(enclosingConstructor).getQualifiedName(); String className = NodeUtil.getNameNode(enclosingConstructor).getQualifiedName();
Expand All @@ -129,10 +176,9 @@ private Node createSuperClassQNameNode(Node superCall) {
statement = statement.getNext()) { statement = statement.getNext()) {
String superClassName = getSuperClassNameIfIsInheritsStatement(statement, className); String superClassName = getSuperClassNameIfIsInheritsStatement(statement, className);
if (superClassName != null) { if (superClassName != null) {
return NodeUtil.newQName(compiler, superClassName); return superClassName;
} }
} }

throw new IllegalStateException("$jscomp.inherits() call not found."); 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) {};")); "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() { public void testInvalidExtends() {
testError("class C extends foo() {}", Es6ToEs3Converter.DYNAMIC_EXTENDS_TYPE); testError("class C extends foo() {}", Es6ToEs3Converter.DYNAMIC_EXTENDS_TYPE);
testError("class C extends function(){} {}", 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); 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.