Start tracking suppressed errors in addition to the cause in underlying error object.

Change-Id: I82806154c1fceee612d06fbc94ccfe2a34268e74
Review-Link: https://gwt-review.googlesource.com/#/c/22020/
diff --git a/user/super/com/google/gwt/emul/java/lang/Throwable.java b/user/super/com/google/gwt/emul/java/lang/Throwable.java
index 3ea313d..cbee36f 100644
--- a/user/super/com/google/gwt/emul/java/lang/Throwable.java
+++ b/user/super/com/google/gwt/emul/java/lang/Throwable.java
@@ -21,8 +21,8 @@
 
 import java.io.PrintStream;
 import java.io.Serializable;
+import java.util.Arrays;
 
-import javaemul.internal.JsUtils;
 import javaemul.internal.annotations.DoNotInline;
 
 import jsinterop.annotations.JsMethod;
@@ -141,23 +141,39 @@
   private void setBackingJsObject(Object backingJsObject) {
     this.backingJsObject = backingJsObject;
     linkBack(backingJsObject);
-    linkBackingCause();
   }
 
-  private void linkBack(Object error) {
-    if (error != null) {
+  private native void linkBack(Object error) /*-{
+
+    if (error instanceof Object) {
       try {
         // This may throw exception in strict mode.
-        ((HasJavaThrowable) error).setJavaThrowable(this);
-      } catch (Throwable ignored) { }
-    }
-  }
+        error.__java$exception = this;
 
-  private void linkBackingCause() {
-    if (cause == null || !(backingJsObject instanceof NativeError)) {
-      return;
+        if (navigator.userAgent.toLowerCase().indexOf("msie") != -1 && $doc.documentMode < 9) {
+          return;
+        }
+
+        var throwable = this;
+        Object.defineProperties(error, {
+          cause: {
+            get: function() {
+              var cause = throwable.@Throwable::getCause()();
+              return cause && cause.@Throwable::getBackingJsObject()();
+            }
+          },
+          suppressed: {
+            get: function() {
+              return throwable.@Throwable::getBackingSuppressed()();
+            }
+          }
+        });
+      } catch (ignored) {}
     }
-    JsUtils.setProperty(backingJsObject, "cause", cause.backingJsObject);
+  }-*/;
+
+  private Object[] getBackingSuppressed() {
+    return Arrays.stream(getSuppressed()).map(t -> t.backingJsObject).toArray();
   }
 
   /**
@@ -245,7 +261,6 @@
     checkState(this.cause == null, "Can't overwrite cause");
     checkCriticalArgument(cause != this, "Self-causation not permitted");
     this.cause = cause;
-    linkBackingCause();
     return this;
   }
 
@@ -323,9 +338,6 @@
   @JsType(isNative = true, name = "?", namespace = JsPackage.GLOBAL)
   private interface HasJavaThrowable {
     @JsProperty(name = "__java$exception")
-    void setJavaThrowable(Throwable t);
-
-    @JsProperty(name = "__java$exception")
     Throwable getJavaThrowable();
   }
 }
diff --git a/user/test/com/google/gwt/emultest/java/lang/JsExceptionTest.java b/user/test/com/google/gwt/emultest/java/lang/JsExceptionTest.java
index 0c2abbb..511520d 100644
--- a/user/test/com/google/gwt/emultest/java/lang/JsExceptionTest.java
+++ b/user/test/com/google/gwt/emultest/java/lang/JsExceptionTest.java
@@ -129,11 +129,25 @@
   @JsType(isNative = true, namespace = "<window>")
   private static class TypeError { }
 
-  protected static void assertTypeError(RuntimeException e) {
+  private static void assertTypeError(RuntimeException e) {
     assertTrue(getBackingJsObject(e) instanceof TypeError);
     assertTrue(e.toString().contains("TypeError"));
   }
 
+  public void testFrozenObject() {
+    try {
+      Object e = new TypeError();
+      freeze(e);
+      throwNative(e);
+      fail();
+    } catch (RuntimeException e) {
+      e = (RuntimeException) javaNativeJavaSandwich(e);
+    }
+  }
+
+  @JsMethod(name = "Object.freeze", namespace = JsPackage.GLOBAL)
+  private static native void freeze(Object o);
+
   public void testSvgError() {
     try {
       throwSvgError();
diff --git a/user/test/com/google/gwt/emultest/java/lang/ThrowableTest.java b/user/test/com/google/gwt/emultest/java/lang/ThrowableTest.java
index ae01e46..3a6475b 100644
--- a/user/test/com/google/gwt/emultest/java/lang/ThrowableTest.java
+++ b/user/test/com/google/gwt/emultest/java/lang/ThrowableTest.java
@@ -16,6 +16,7 @@
 package com.google.gwt.emultest.java.lang;
 
 import com.google.gwt.testing.TestUtils;
+import java.io.IOException;
 import jsinterop.annotations.JsType;
 
 /** Unit tests for the GWT emulation of java.lang.Throwable class. */
@@ -111,7 +112,7 @@
   }
 
   public void testLinkedBackingObjects() {
-    if (TestUtils.isJvm()) {
+    if (isJvmOrIe8()) {
       return;
     }
     Throwable rootCause = new Throwable("Root cause");
@@ -121,7 +122,7 @@
   }
 
   public void testLinkedBackingObjects_initCause() {
-    if (TestUtils.isJvm()) {
+    if (isJvmOrIe8()) {
       return;
     }
     Throwable rootCause = new Throwable("Root cause");
@@ -132,7 +133,7 @@
   }
 
   public void testLinkedBackingObjects_noCause() {
-    if (TestUtils.isJvm()) {
+    if (isJvmOrIe8()) {
       return;
     }
     Throwable subError = new Throwable("Sub-error");
@@ -140,6 +141,45 @@
     assertNull(getBackingJsObject(subError).getCause());
   }
 
+  public void testLinkedSuppressedErrors_suppressedAddedViaInit() {
+    if (isJvmOrIe8()) {
+      return;
+    }
+    final Throwable suppressed = new Throwable();
+    Throwable e =
+        new Throwable() {
+          {
+            addSuppressed(suppressed);
+          }
+        };
+
+    assertEquals(getBackingJsObject(suppressed), getBackingJsObject(e).getSuppressed()[0]);
+  }
+
+  public void testLinkedSuppressedErrors_tryWithResources() {
+    if (isJvmOrIe8()) {
+      return;
+    }
+
+    class FailingResource implements AutoCloseable {
+      @Override
+      public void close() throws IOException {
+        throw new IOException("onClose");
+      }
+    }
+
+    RuntimeException e = new RuntimeException("try");
+    try (FailingResource r = new FailingResource()) {
+      throw e;
+    } catch (Exception expected) { }
+
+    assertEquals(
+        getBackingJsObject(e.getSuppressed()[0]), getBackingJsObject(e).getSuppressed()[0]);
+  }
+
+  private static boolean isJvmOrIe8() {
+    return TestUtils.isJvm() || System.getProperty("user.agent", "safari").equals("ie8");
+  }
 
   @JsType(isNative = true, name = "Error", namespace = "<window>")
   private static class JsError { }
diff --git a/user/test/com/google/gwt/emultest/java/lang/ThrowableTestBase.java b/user/test/com/google/gwt/emultest/java/lang/ThrowableTestBase.java
index 967eb5f..0397350 100644
--- a/user/test/com/google/gwt/emultest/java/lang/ThrowableTestBase.java
+++ b/user/test/com/google/gwt/emultest/java/lang/ThrowableTestBase.java
@@ -16,11 +16,10 @@
 package com.google.gwt.emultest.java.lang;
 
 import com.google.gwt.junit.client.GWTTestCase;
-import javaemul.internal.JsUtils;
 import jsinterop.annotations.JsFunction;
 import jsinterop.annotations.JsMethod;
-import jsinterop.annotations.JsOverlay;
 import jsinterop.annotations.JsPackage;
+import jsinterop.annotations.JsProperty;
 import jsinterop.annotations.JsType;
 
 /**
@@ -90,14 +89,10 @@
   /** A JavaScript object backing a Throwable. */
   @JsType(isNative = true, name = "*", namespace = JsPackage.GLOBAL)
   interface BackingJsObject {
-    @JsOverlay
-    default Object getCause() {
-      return JsUtils.getProperty(this, "cause");
-    }
+    @JsProperty
+    Object getCause();
 
-    @JsOverlay
-    default Object[] getSuppressed() {
-      return JsUtils.getProperty(this, "suppressed");
-    }
+    @JsProperty
+    Object[] getSuppressed();
   }
 }