[gjs: 1/3] jsapi-util: Print error cause, if available, when logging error
- From: Philip Chimento <pchimento src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs: 1/3] jsapi-util: Print error cause, if available, when logging error
- Date: Sun, 13 Feb 2022 20:05:54 +0000 (UTC)
commit 33d58646d43b84d4c0ffc3681b89d125d5ccdfc6
Author: Philip Chimento <philip chimento gmail com>
Date: Sat Feb 12 11:50:54 2022 -0800
jsapi-util: Print error cause, if available, when logging error
A new feature in SpiderMonkey 91 is the Error.cause property. If there is
a cause property on an error that we log with gjs_log_exception(), append
it to the original error's log message.
Closes: #454
gjs/atoms.h | 1 +
gjs/jsapi-util.cpp | 95 ++++++++++++++++++++++++++++--------
installed-tests/js/testExceptions.js | 45 +++++++++++++++++
3 files changed, 120 insertions(+), 21 deletions(-)
---
diff --git a/gjs/atoms.h b/gjs/atoms.h
index 1e0b72fca..955165017 100644
--- a/gjs/atoms.h
+++ b/gjs/atoms.h
@@ -18,6 +18,7 @@ class JSTracer;
// clang-format off
#define FOR_EACH_ATOM(macro) \
+ macro(cause, "cause") \
macro(code, "code") \
macro(column_number, "columnNumber") \
macro(connect_after, "connect_after") \
diff --git a/gjs/jsapi-util.cpp b/gjs/jsapi-util.cpp
index 6f46ca2f5..4168f3acb 100644
--- a/gjs/jsapi-util.cpp
+++ b/gjs/jsapi-util.cpp
@@ -25,7 +25,9 @@
#include <js/ErrorReport.h>
#include <js/Exception.h>
#include <js/GCAPI.h> // for JS_MaybeGC, NonIncrementalGC, GCRe...
+#include <js/GCHashTable.h> // for GCHashSet
#include <js/GCVector.h> // for RootedVector
+#include <js/HashTable.h> // for DefaultHasher
#include <js/Object.h> // for GetClass
#include <js/RootingAPI.h>
#include <js/String.h>
@@ -34,11 +36,16 @@
#include <js/ValueArray.h>
#include <jsapi.h> // for JS_GetPropertyById, JS_InstanceOf
#include <jsfriendapi.h> // for ProtoKeyToClass
+#include <mozilla/HashTable.h> // for HashSet::AddPtr
#include "gjs/atoms.h"
#include "gjs/context-private.h"
#include "gjs/jsapi-util.h"
+namespace js {
+class SystemAllocPolicy;
+}
+
static void
throw_property_lookup_error(JSContext *cx,
JS::HandleObject obj,
@@ -410,11 +417,72 @@ static std::string format_syntax_error_location(JSContext* cx,
return out.str();
}
+using CauseSet = JS::GCHashSet<JSObject*, js::DefaultHasher<JSObject*>,
+ js::SystemAllocPolicy>;
+
+static std::string format_exception_with_cause(
+ JSContext* cx, JS::HandleObject exc_obj,
+ JS::MutableHandle<CauseSet> seen_causes) {
+ std::ostringstream out;
+ const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+
+ JS::UniqueChars utf8_stack;
+ // Check both the internal SavedFrame object and the stack property.
+ // GErrors will not have the former, and internal errors will not
+ // have the latter.
+ JS::RootedObject saved_frame(cx, JS::ExceptionStackOrNull(exc_obj));
+ JS::RootedString str(cx);
+ if (saved_frame) {
+ JS::BuildStackString(cx, nullptr, saved_frame, &str, 0);
+ } else {
+ JS::RootedValue stack(cx);
+ if (JS_GetPropertyById(cx, exc_obj, atoms.stack(), &stack) &&
+ stack.isString())
+ str = stack.toString();
+ }
+ if (str)
+ utf8_stack = JS_EncodeStringToUTF8(cx, str);
+ if (utf8_stack)
+ out << '\n' << utf8_stack.get();
+ JS_ClearPendingException(cx);
+
+ // COMPAT: use JS::GetExceptionCause, mozjs 91.6 and later, on Error objects
+ // in order to avoid side effects
+ JS::RootedValue v_cause(cx);
+ if (!JS_GetPropertyById(cx, exc_obj, atoms.cause(), &v_cause))
+ JS_ClearPendingException(cx);
+ if (v_cause.isUndefined())
+ return out.str();
+
+ JS::RootedObject cause(cx);
+ if (v_cause.isObject()) {
+ cause = &v_cause.toObject();
+ CauseSet::AddPtr entry = seen_causes.lookupForAdd(cause);
+ if (entry)
+ return out.str(); // cause has been printed already, ref cycle
+ if (!seen_causes.add(entry, cause))
+ return out.str(); // out of memory, just stop here
+ }
+
+ out << "Caused by: ";
+ JS::RootedString exc_str(cx, exception_to_string(cx, v_cause));
+ if (exc_str) {
+ JS::UniqueChars utf8_exception = JS_EncodeStringToUTF8(cx, exc_str);
+ if (utf8_exception)
+ out << utf8_exception.get();
+ }
+ JS_ClearPendingException(cx);
+
+ if (v_cause.isObject())
+ out << format_exception_with_cause(cx, cause, seen_causes);
+
+ return out.str();
+}
+
static std::string format_exception_log_message(JSContext* cx,
JS::HandleValue exc,
JS::HandleString message) {
std::ostringstream out;
- const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
if (message) {
JS::UniqueChars utf8_message = JS_EncodeStringToUTF8(cx, message);
@@ -440,30 +508,15 @@ static std::string format_exception_log_message(JSContext* cx,
// We log syntax errors differently, because the stack for those
// includes only the referencing module, but we want to print out the
// file name, line number, and column number from the exception.
+ // We assume that syntax errors have no cause property, and are not the
+ // cause of other exceptions, so no recursion.
out << format_syntax_error_location(cx, exc_obj);
return out.str();
}
- JS::UniqueChars utf8_stack;
- // Check both the internal SavedFrame object and the stack property.
- // GErrors will not have the former, and internal errors will not
- // have the latter.
- JS::RootedObject saved_frame(cx, JS::ExceptionStackOrNull(exc_obj));
- JS::RootedString str(cx);
- if (saved_frame) {
- JS::BuildStackString(cx, nullptr, saved_frame, &str, 0);
- } else {
- JS::RootedValue stack(cx);
- if (JS_GetPropertyById(cx, exc_obj, atoms.stack(), &stack) &&
- stack.isString())
- str = stack.toString();
- }
- if (str)
- utf8_stack = JS_EncodeStringToUTF8(cx, str);
- if (utf8_stack)
- out << '\n' << utf8_stack.get();
- JS_ClearPendingException(cx);
-
+ JS::Rooted<CauseSet> seen_causes(cx);
+ seen_causes.putNew(exc_obj);
+ out << format_exception_with_cause(cx, exc_obj, &seen_causes);
return out.str();
}
diff --git a/installed-tests/js/testExceptions.js b/installed-tests/js/testExceptions.js
index fbc32bc20..17c61da2f 100644
--- a/installed-tests/js/testExceptions.js
+++ b/installed-tests/js/testExceptions.js
@@ -166,6 +166,51 @@ describe('logError', function () {
logError(e);
}
});
+
+ it('logs an error with cause', function marker() {
+ GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+ 'JS ERROR: Error: an error\nmarker@*Caused by: Gio.IOErrorEnum: another error\nmarker2@*');
+ function marker2() {
+ return new Gio.IOErrorEnum({message: 'another error', code: 0});
+ }
+ logError(new Error('an error', {cause: marker2()}));
+ });
+
+ it('logs a GError with cause', function marker() {
+ GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+ 'JS ERROR: Gio.IOErrorEnum: an error\nmarker@*Caused by: Error: another error\nmarker2@*');
+ function marker2() {
+ return new Error('another error');
+ }
+ const e = new Gio.IOErrorEnum({message: 'an error', code: 0});
+ e.cause = marker2();
+ logError(e);
+ });
+
+ it('logs an error with non-object cause', function () {
+ GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+ 'JS ERROR: Error: an error\n*Caused by: 3');
+ logError(new Error('an error', {cause: 3}));
+ });
+
+ it('logs an error with a cause tree', function () {
+ GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+ 'JS ERROR: Error: one\n*Caused by: Error: two\n*Caused by: Error: three\n*');
+ const three = new Error('three');
+ const two = new Error('two', {cause: three});
+ logError(new Error('one', {cause: two}));
+ });
+
+ it('logs an error with cyclical causes', function () {
+ // We cannot assert here with GLib.test_expect_message that the * at the
+ // end of the string doesn't match more causes, but at least the idea is
+ // that it shouldn't go into an infinite loop
+ GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+ 'JS ERROR: Error: one\n*Caused by: Error: two\n*');
+ const one = new Error('one');
+ one.cause = new Error('two', {cause: one});
+ logError(one);
+ });
});
describe('Exception from function with too few arguments', function () {
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]