diff --git a/src/com/google/javascript/jscomp/Es6ConvertSuperConstructorCalls.java b/src/com/google/javascript/jscomp/Es6ConvertSuperConstructorCalls.java index 412b2086c8e..0e092affbf8 100644 --- a/src/com/google/javascript/jscomp/Es6ConvertSuperConstructorCalls.java +++ b/src/com/google/javascript/jscomp/Es6ConvertSuperConstructorCalls.java @@ -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) { @@ -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. @@ -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(); @@ -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."); } diff --git a/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java b/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java index e6d4a6d30ea..078f71677cc 100644 --- a/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java +++ b/test/com/google/javascript/jscomp/Es6ToEs3ConverterTest.java @@ -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); diff --git a/test/com/google/javascript/jscomp/runtime_tests/class_test.js b/test/com/google/javascript/jscomp/runtime_tests/class_test.js index cd7ed3755a1..ba427fb7a4f 100644 --- a/test/com/google/javascript/jscomp/runtime_tests/class_test.js +++ b/test/com/google/javascript/jscomp/runtime_tests/class_test.js @@ -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); + } +} \ No newline at end of file