Skip to content

Commit

Permalink
Make JS stack traces of J2CL exceptions a lot more precise and make t…
Browse files Browse the repository at this point in the history
…he underlying JavaScript error message reflect Throwable.toString.

This will reduce down number extra frames to 1 frame which would be the Exception constructor (which is dropped in Java and pure JavaScript errors).

For browsers that use V8 (Chrome, Opera etc and soon IE) we could get down to no extra frames by using Error.captureStackTrace however that would add 2 frames to all other browsers and doesn't seem worthwhile at the moment. We will re-evaluate later.

For the error message part:
Throwable.toString() is sometimes overridden to provide more details about the exception (e.g. test infra). We are now including that in the backing JavaScript Error.
That couldn't be done before because calling toString before execution of the child constructor was dangerous.

PiperOrigin-RevId: 272791957
  • Loading branch information
gkdn authored and copybara-github committed Oct 4, 2019
1 parent 0123e1f commit 1e58670
Show file tree
Hide file tree
Showing 24 changed files with 950 additions and 217 deletions.
272 changes: 272 additions & 0 deletions jre/java/java/lang/Throwable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package java.lang;

import static javaemul.internal.InternalPreconditions.checkCriticalArgument;
import static javaemul.internal.InternalPreconditions.checkNotNull;
import static javaemul.internal.InternalPreconditions.checkState;

import java.io.PrintStream;
import java.io.Serializable;
import javaemul.internal.JsUtils;
import javaemul.internal.annotations.DoNotInline;
import jsinterop.annotations.JsMethod;
import jsinterop.annotations.JsNonNull;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsProperty;
import jsinterop.annotations.JsType;

/**
* See <a
* href="http://java.sun.com/j2se/1.5.0/docs/api/java/lang/Throwable.html">the
* official Java API doc</a> for details.
*/
public class Throwable implements Serializable {

private String detailMessage;
private Throwable cause;
private Throwable[] suppressedExceptions;
private StackTraceElement[] stackTrace = new StackTraceElement[0];
private boolean disableSuppression;
private boolean writableStackTrace = true;

@JsProperty
private Object backingJsObject;

public Throwable() {
fillInStackTrace();
}

public Throwable(String message) {
this.detailMessage = message;
fillInStackTrace();
}

public Throwable(String message, Throwable cause) {
this.cause = cause;
this.detailMessage = message;
fillInStackTrace();
}

public Throwable(Throwable cause) {
this.detailMessage = (cause == null) ? null : cause.toString();
this.cause = cause;
fillInStackTrace();
}

/**
* Constructor that allows subclasses disabling exception suppression and stack traces.
* Those features should only be disabled in very specific cases.
*/
protected Throwable(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
this.cause = cause;
this.detailMessage = message;
this.writableStackTrace = writableStackTrace;
this.disableSuppression = !enableSuppression;
if (writableStackTrace) {
fillInStackTrace();
}
}

Throwable(Object backingJsObject) {
this(String.valueOf(backingJsObject));
}

// Called by transpiler. Do not remove!
void privateInitError(Object error) {
setBackingJsObject(error);
}

public Object getBackingJsObject() {
return backingJsObject;
}

private void setBackingJsObject(Object backingJsObject) {
this.backingJsObject = backingJsObject;
linkBack(backingJsObject);
linkBackingCause();
}

private void linkBack(Object error) {
if (error != null) {
try {
// This may throw exception in strict mode.
((HasJavaThrowable) error).setJavaThrowable(this);
} catch (Throwable ignored) { }
}
}

private void linkBackingCause() {
if (cause == null || !(backingJsObject instanceof NativeError)) {
return;
}
JsUtils.setProperty(backingJsObject, "cause", cause.backingJsObject);
}

/** Call to add an exception that was suppressed. Used by try-with-resources. */
public final void addSuppressed(Throwable exception) {
checkNotNull(exception, "Cannot suppress a null exception.");
checkCriticalArgument(exception != this, "Exception can not suppress itself.");

if (disableSuppression) {
return;
}

if (suppressedExceptions == null) {
suppressedExceptions = new Throwable[] {exception};
} else {
// TRICK: This is not correct Java (would give an OOBE, but it works in JS and
// this code will only be executed in JS.
suppressedExceptions[suppressedExceptions.length] = exception;
}
}

/**
* Populates the stack trace information for the Throwable.
*
* @return this
*/
@DoNotInline
public Throwable fillInStackTrace() {
if (writableStackTrace) {
// Note that when this called from ctor, transpiler hasn't initialized backingJsObject yet.
if (backingJsObject instanceof NativeError) {
NativeError.captureStackTrace((NativeError) backingJsObject);
}

// Invalidate the cached trace
this.stackTrace = null;
}
return this;
}

public Throwable getCause() {
return cause;
}

public String getLocalizedMessage() {
return getMessage();
}

public String getMessage() {
return detailMessage;
}

/** Returns the stack trace for the Throwable if it is available. */
public StackTraceElement[] getStackTrace() {
if (stackTrace == null) {
stackTrace = constructJavaStackTrace();
}
return stackTrace;
}

@SuppressWarnings("unusable-by-js")
private native StackTraceElement[] constructJavaStackTrace();

/** Returns the array of Exception that this one suppressedExceptions. */
public final Throwable[] getSuppressed() {
if (suppressedExceptions == null) {
suppressedExceptions = new Throwable[0];
}

return suppressedExceptions;
}

public Throwable initCause(Throwable cause) {
checkState(this.cause == null, "Can't overwrite cause");
checkCriticalArgument(cause != this, "Self-causation not permitted");
this.cause = cause;
linkBackingCause();
return this;
}

public void printStackTrace() {
printStackTrace(System.err);
}

public void printStackTrace(PrintStream out) {
printStackTraceImpl(out, "", "");
}

private void printStackTraceImpl(PrintStream out, String prefix, String ident) {
out.println(ident + prefix + this);
printStackTraceItems(out, ident);

for (Throwable t : getSuppressed()) {
t.printStackTraceImpl(out, "Suppressed: ", "\t" + ident);
}

Throwable theCause = getCause();
if (theCause != null) {
theCause.printStackTraceImpl(out, "Caused by: ", ident);
}
}

private void printStackTraceItems(PrintStream out, String ident) {
for (StackTraceElement element : getStackTrace()) {
out.println(ident + "\tat " + element);
}
}

public void setStackTrace(StackTraceElement[] stackTrace) {
int length = stackTrace.length;
StackTraceElement[] copy = new StackTraceElement[length];
for (int i = 0; i < length; ++i) {
copy[i] = checkNotNull(stackTrace[i]);
}
this.stackTrace = copy;
}

@Override
public String toString() {
String className = getClass().getName();
String message = getLocalizedMessage();
return message == null ? className : className + ": " + message;
}

@JsMethod
public static @JsNonNull Throwable of(Object e) {
// If the JS error is already mapped to a Java Exception, use it.
if (e != null) {
Throwable throwable = ((HasJavaThrowable) e).getJavaThrowable();
if (throwable != null) {
return throwable;
}
}

// If the JS error is being seen for the first time, map it best corresponding Java exception.
return e instanceof NativeTypeError ? new NullPointerException(e) : new JsException(e);
}

@JsType(isNative = true, name = "Error", namespace = "<window>")
private static class NativeError {
static native void captureStackTrace(Object error);
}

@JsType(isNative = true, name = "TypeError", namespace = "<window>")
private static class NativeTypeError {}

@SuppressWarnings("unusable-by-js")
@JsType(isNative = true, name = "?", namespace = JsPackage.GLOBAL)
private interface HasJavaThrowable {
@JsProperty(name = "__java$exception")
void setJavaThrowable(Throwable t);

@JsProperty(name = "__java$exception")
Throwable getJavaThrowable();
}
}
25 changes: 1 addition & 24 deletions jre/java/java/lang/Throwable.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,7 @@
// limitations under the License.

/**
* @public
*/
Throwable.prototype.m_captureStackTrace___$p_java_lang_Throwable = function() {
// Only supporting modern browsers so generating stack by traversing callees
// is not necessary.
};

/**
* @return {Array<StackTraceElement>}
* @return {!Array<!StackTraceElement>}
* @public
*/
Throwable.prototype.m_constructJavaStackTrace___$p_java_lang_Throwable =
Expand All @@ -37,18 +29,3 @@ Throwable.prototype.m_constructJavaStackTrace___$p_java_lang_Throwable =
}
return stackTraceElements;
};

/**
* @param {*} e
* @return {*}
* @public
*/
Throwable.m_fixIE__java_lang_Object = function(e) {
if (!('stack' in e)) {
try {
throw e;
} catch (ignored) {
}
}
return e;
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,19 @@
/** Helper class for comparing stack traces */
class StacktraceAsserter {

private static final ImmutableList<String> JAVA_START_FRAMES_FOR_TRIMMING =
private static final int EXTRA_J2CL_FRAME_COUNT = 1;

private static final ImmutableList<String> EXTRA_J2CL_FRAMES =
ImmutableList.of(
"at sun.reflect.", "at java.lang.reflect.", "at org.junit.", "at com.google.testing.");
"at java.lang.Throwable.fillInStackTrace.*",
"at java.lang.RuntimeException.*",
"at .*\\$MyJsException.*");

private static final ImmutableList<String> JS_START_FRAMES_FOR_TRIMMING =
private static final ImmutableList<String> JAVA_TEST_INFRA_FRAMES =
ImmutableList.of(
"at java.lang.Throwable.", "at java.lang.Exception.", "at java.lang.RuntimeException.");
"at sun.reflect.", "at java.lang.reflect.", "at org.junit.", "at com.google.testing.");

private static final ImmutableList<String> JS_FILES_FOR_TRIMMING =
private static final ImmutableList<String> JS_TEST_INFRA_FRAMES =
ImmutableList.of(
"javascript/closure/testing/testcase.js",
"javascript/closure/testing/testrunner.js",
Expand Down Expand Up @@ -178,9 +182,9 @@ private Stacktrace parseStackTrace(List<String> stacktrace) {
Builder stacktraceBuilder = Stacktrace.newStacktraceBuilder();

String message = stacktrace.get(0).trim();
int i;
for (i = 1; i < stacktrace.size(); i++) {
String line = stacktrace.get(i).trim();
int frameStart;
for (frameStart = 1; frameStart < stacktrace.size(); frameStart++) {
String line = stacktrace.get(frameStart).trim();
if (!line.startsWith("at ")) {
message += "\n" + line;
} else {
Expand All @@ -189,15 +193,17 @@ private Stacktrace parseStackTrace(List<String> stacktrace) {
}
stacktraceBuilder.message(message);

for (; i < stacktrace.size(); i++) {
for (int i = frameStart; i < stacktrace.size(); i++) {
final String line = stacktrace.get(i).trim();
List<String> startTokenList =
testMode == TestMode.JAVA ? JAVA_START_FRAMES_FOR_TRIMMING : JS_START_FRAMES_FOR_TRIMMING;
boolean skip = startTokenList.stream().anyMatch(s -> line.startsWith(s));

boolean skip = false;
if (testMode.isJ2cl()) {
// in J2cl we skip certain js files
skip |= JS_FILES_FOR_TRIMMING.stream().anyMatch(s -> line.contains(s));
if (i < frameStart + EXTRA_J2CL_FRAME_COUNT) {
skip |= EXTRA_J2CL_FRAMES.stream().anyMatch(s -> line.matches(s));
}
skip |= JS_TEST_INFRA_FRAMES.stream().anyMatch(s -> line.contains(s));
} else {
skip |= JAVA_TEST_INFRA_FRAMES.stream().anyMatch(s -> line.contains(s));
}

if (!skip) {
Expand All @@ -213,7 +219,7 @@ private List<String> extractStackTrace(List<String> logLines, String startLine)
// (they are not part of a normal j2cl_test log)
// Make sure we skip those here
int logIndex = 0;
if (testMode != TestMode.JAVA) {
if (testMode.isJ2cl()) {
for (; logIndex < logLines.size(); logIndex++) {
if (logLines
.get(logIndex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ j2cl_test_integration_test(
"//junit/generator/javatests/com/google/j2cl/junit/integration/stacktrace/data:AnonymousClassesStacktraceTest",
"//junit/generator/javatests/com/google/j2cl/junit/integration/stacktrace/data:CustomExceptionStacktraceTest",
"//junit/generator/javatests/com/google/j2cl/junit/integration/stacktrace/data:FillInStacktraceTest",
"//junit/generator/javatests/com/google/j2cl/junit/integration/stacktrace/data:JsExceptionStacktraceTest",
"//junit/generator/javatests/com/google/j2cl/junit/integration/stacktrace/data:JsExceptionNonJsConstructorStacktraceTest",
"//junit/generator/javatests/com/google/j2cl/junit/integration/stacktrace/data:LambdaStacktraceTest",
"//junit/generator/javatests/com/google/j2cl/junit/integration/stacktrace/data:NativeStacktraceTest",
"//junit/generator/javatests/com/google/j2cl/junit/integration/stacktrace/data:RecursiveStacktraceTest",
Expand Down
Loading

0 comments on commit 1e58670

Please sign in to comment.