Bug 1441333: Part 3 - Add helper to create a JS error with a saved stack.
There's no standard way to create a JS error with full stack and location
information from a saved stack. Since we need this functionality in order to
reject promises with useful Error objects, this patch adds a simple helper to
make that possible.
MozReview-Commit-ID: FyGuo4UjfsQ
--- a/dom/base/ChromeUtils.cpp
+++ b/dom/base/ChromeUtils.cpp
@@ -674,10 +674,73 @@ ChromeUtils::GetCallerLocation(const Glo
// FirstSubsumedFrame gets us a stack which stops at the first principal which
// is subsumed by the given principal. That means that we may have a lot of
// privileged frames that we don't care about at the top of the stack, though.
// We need to filter those out to get the frame we actually want.
aRetval.set(js::GetFirstSubsumedSavedFrame(cx, principals, frame, kSkipSelfHosted));
}
+/* static */ void
+ChromeUtils::CreateError(const GlobalObject& aGlobal, const nsAString& aMessage,
+ JS::Handle<JSObject*> aStack,
+ JS::MutableHandle<JSObject*> aRetVal, ErrorResult& aRv)
+{
+ if (aStack && !JS::IsSavedFrame(aStack)) {
+ aRv.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ JSContext* cx = aGlobal.Context();
+
+ auto cleanup = MakeScopeExit([&]() {
+ aRv.NoteJSContextException(cx);
+ });
+
+ JS::RootedObject retVal(cx);
+ {
+ JS::RootedString fileName(cx, JS_GetEmptyString(cx));
+ uint32_t line = 0;
+ uint32_t column = 0;
+
+ Maybe<JSAutoCompartment> ac;
+ JS::RootedObject stack(cx);
+ if (aStack) {
+ stack = UncheckedUnwrap(aStack);
+ ac.emplace(cx, stack);
+
+ if (JS::GetSavedFrameLine(cx, stack, &line) != JS::SavedFrameResult::Ok ||
+ JS::GetSavedFrameColumn(cx, stack, &column) != JS::SavedFrameResult::Ok ||
+ JS::GetSavedFrameSource(cx, stack, &fileName) != JS::SavedFrameResult::Ok) {
+ return;
+ }
+ }
+
+ JS::RootedString message(cx);
+ {
+ JS::RootedValue msgVal(cx);
+ if (!xpc::NonVoidStringToJsval(cx, aMessage, &msgVal)) {
+ return;
+ }
+ message = msgVal.toString();
+ }
+
+ JS::Rooted<JS::Value> err(cx);
+ if (!JS::CreateError(cx, JSEXN_ERR, stack,
+ fileName, line, column,
+ nullptr, message, &err)) {
+ return;
+ }
+
+ MOZ_ASSERT(err.isObject());
+ retVal = &err.toObject();
+ }
+
+ if (aStack && !JS_WrapObject(cx, &retVal)) {
+ return;
+ }
+
+ cleanup.release();
+ aRetVal.set(retVal);
+}
+
} // namespace dom
} // namespace mozilla
--- a/dom/base/ChromeUtils.h
+++ b/dom/base/ChromeUtils.h
@@ -165,14 +165,19 @@ public:
JS::Handle<JSObject*> target,
const nsAString& id,
const nsAString& resourceURI,
ErrorResult& aRv);
static void
GetCallerLocation(const GlobalObject& global, nsIPrincipal* principal,
JS::MutableHandle<JSObject*> aRetval);
+
+ static void
+ CreateError(const GlobalObject& global, const nsAString& message,
+ JS::Handle<JSObject*> stack,
+ JS::MutableHandle<JSObject*> aRetVal, ErrorResult& aRv);
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_ChromeUtils__
--- a/dom/webidl/ChromeUtils.webidl
+++ b/dom/webidl/ChromeUtils.webidl
@@ -296,16 +296,25 @@ partial namespace ChromeUtils {
void defineModuleGetter(object target, DOMString id, DOMString resourceURI);
/**
* Returns the scripted location of the first ancestor stack frame with a
* principal which is subsumed by the given principal. If no such frame
* exists on the call stack, returns null.
*/
object? getCallerLocation(Principal principal);
+
+ /**
+ * Creates a JS Error object with the given message and stack.
+ *
+ * If a stack object is provided, the error object is created in the global
+ * that it belongs to.
+ */
+ [Throws]
+ object createError(DOMString message, optional object? stack = null);
};
/**
* Used by principals and the script security manager to represent origin
* attributes. The first dictionary is designed to contain the full set of
* OriginAttributes, the second is used for pattern-matching (i.e. does this
* OriginAttributesDictionary match the non-empty attributes in this pattern).
*
--- a/js/xpconnect/tests/unit/test_getCallerLocation.js
+++ b/js/xpconnect/tests/unit/test_getCallerLocation.js
@@ -53,9 +53,32 @@ add_task(async function() {
});
equal(messages[0].stack, frame, "reportError stack frame");
equal(messages[0].message, '[JavaScript Error: "Meh" {file: "thing.js" line: 5}]\nit@thing.js:5:14\n');
Assert.throws(() => { Cu.reportError("Meh", {}); },
err => err.result == Cr.NS_ERROR_INVALID_ARG,
"reportError should throw when passed a non-SavedFrame object");
+
+
+ // createError
+
+ Assert.throws(() => { ChromeUtils.createError("Meh", {}); },
+ err => err.result == Cr.NS_ERROR_INVALID_ARG,
+ "createError should throw when passed a non-SavedFrame object");
+
+ let cloned = Cu.cloneInto(frame, sandbox);
+ let error = ChromeUtils.createError("Meh", cloned);
+
+ equal(String(cloned), String(frame),
+ "Cloning a SavedStack preserves its stringification");
+
+ equal(Cu.getGlobalForObject(error), sandbox,
+ "createError creates errors in the global of the SavedFrame");
+ equal(error.stack, String(cloned),
+ "createError creates errors with the correct stack");
+
+ equal(error.message, "Meh", "Error message");
+ equal(error.fileName, "thing.js", "Error filename");
+ equal(error.lineNumber, 5, "Error line");
+ equal(error.columnNumber, 14, "Error column");
});