diff --git a/React/Executors/RCTJSCErrorHandling.h b/React/Executors/RCTJSCErrorHandling.h new file mode 100644 index 000000000..dbc0d52d6 --- /dev/null +++ b/React/Executors/RCTJSCErrorHandling.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "RCTDefines.h" + +typedef struct RCTJSCWrapper RCTJSCWrapper; + +/** + Translates a given exception into an NSError. + + @param exception The JavaScript exception object to translate into an NSError. This must be + a JavaScript Error object, otherwise no stack trace information will be available. + + @return The translated NSError object + + - The JS exception's name property is incorporated in the NSError's localized description + - The JS exception's message property is the NSError's failure reason + - The JS exception's unsymbolicated stack trace is available via the NSError userInfo's RCTJSExceptionUnsymbolicatedStackTraceKey + */ +RCT_EXTERN NSError *RCTNSErrorFromJSError(JSValue *exception); + +/** + Translates a given exception into an NSError. + + @see RCTNSErrorFromJSError for details + */ +RCT_EXTERN NSError *RCTNSErrorFromJSErrorRef(JSValueRef exception, JSGlobalContextRef ctx, RCTJSCWrapper *jscWrapper); diff --git a/React/Executors/RCTJSCErrorHandling.m b/React/Executors/RCTJSCErrorHandling.m new file mode 100644 index 000000000..000781e07 --- /dev/null +++ b/React/Executors/RCTJSCErrorHandling.m @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#include "RCTJSCErrorHandling.h" + +#import "RCTAssert.h" +#import "RCTJSStackFrame.h" +#import "RCTJSCWrapper.h" + +NSString *const RCTJSExceptionUnsymbolicatedStackTraceKey = @"RCTJSExceptionUnsymbolicatedStackTraceKey"; + +NSError *RCTNSErrorFromJSError(JSValue *exception) +{ + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"Unhandled JS Exception: %@", [exception[@"name"] toString] ?: @"Unknown"]; + NSString *const exceptionMessage = [exception[@"message"] toString]; + if ([exceptionMessage length]) { + userInfo[NSLocalizedFailureReasonErrorKey] = exceptionMessage; + } + NSString *const stack = [exception[@"stack"] toString]; + if ([stack length]) { + NSArray *const unsymbolicatedFrames = [RCTJSStackFrame stackFramesWithLines:stack]; + userInfo[RCTJSStackTraceKey] = unsymbolicatedFrames; + } + return [NSError errorWithDomain:RCTErrorDomain code:1 userInfo:userInfo]; +} + +NSError *RCTNSErrorFromJSErrorRef(JSValueRef exceptionRef, JSGlobalContextRef ctx, RCTJSCWrapper *jscWrapper) +{ + JSContext *context = [jscWrapper->JSContext contextWithJSGlobalContextRef:ctx]; + JSValue *exception = [jscWrapper->JSValue valueWithJSValueRef:exceptionRef inContext:context]; + return RCTNSErrorFromJSError(exception); +} diff --git a/React/Executors/RCTJSCExecutor.mm b/React/Executors/RCTJSCExecutor.mm index 22e674dcd..36dbc57ed 100644 --- a/React/Executors/RCTJSCExecutor.mm +++ b/React/Executors/RCTJSCExecutor.mm @@ -30,9 +30,9 @@ #import "RCTRedBox.h" #import "RCTSourceCode.h" #import "RCTJSCWrapper.h" +#import "RCTJSCErrorHandling.h" NSString *const RCTJSCThreadName = @"com.facebook.react.JavaScript"; - NSString *const RCTJavaScriptContextCreatedNotification = @"RCTJavaScriptContextCreatedNotification"; static NSString *const RCTJSCProfilerEnabledDefaultsKey = @"RCTJSCProfilerEnabled"; @@ -163,81 +163,6 @@ RCT_NOT_IMPLEMENTED(-(instancetype)init) RCT_EXPORT_MODULE() -static NSString *RCTJSValueToNSString(RCTJSCWrapper *jscWrapper, JSContextRef context, JSValueRef value, JSValueRef *exception) -{ - JSStringRef JSString = jscWrapper->JSValueToStringCopy(context, value, exception); - if (!JSString) { - return nil; - } - - CFStringRef string = jscWrapper->JSStringCopyCFString(kCFAllocatorDefault, JSString); - jscWrapper->JSStringRelease(JSString); - - return (__bridge_transfer NSString *)string; -} - -static NSString *RCTJSValueToJSONString(RCTJSCWrapper *jscWrapper, JSContextRef context, JSValueRef value, JSValueRef *exception, unsigned indent) -{ - JSStringRef jsString = jscWrapper->JSValueCreateJSONString(context, value, indent, exception); - CFStringRef string = jscWrapper->JSStringCopyCFString(kCFAllocatorDefault, jsString); - jscWrapper->JSStringRelease(jsString); - - return (__bridge_transfer NSString *)string; -} - -static NSError *RCTNSErrorFromJSError(RCTJSCWrapper *jscWrapper, JSContextRef context, JSValueRef jsError) -{ - NSMutableDictionary *errorInfo = [NSMutableDictionary new]; - - NSString *description = jsError ? RCTJSValueToNSString(jscWrapper, context, jsError, NULL) : @"Unknown JS error"; - errorInfo[NSLocalizedDescriptionKey] = [@"Unhandled JS Exception: " stringByAppendingString:description]; - - NSString *details = jsError ? RCTJSValueToJSONString(jscWrapper, context, jsError, NULL, 0) : nil; - if (details) { - errorInfo[NSLocalizedFailureReasonErrorKey] = details; - - // Format stack as used in RCTFormatError - id json = RCTJSONParse(details, NULL); - if ([json isKindOfClass:[NSDictionary class]]) { - if (json[@"stack"]) { - NSError *regexError; - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^([^@]+)@(.*):(\\d+):(\\d+)$" options:0 error:®exError]; - if (regexError) { - RCTLogError(@"Failed to build regex: %@", [regexError localizedDescription]); - } - - NSMutableArray *stackTrace = [NSMutableArray array]; - for (NSString *stackLine in [json[@"stack"] componentsSeparatedByString:@"\n"]) { - NSTextCheckingResult *result = [regex firstMatchInString:stackLine options:0 range:NSMakeRange(0, stackLine.length)]; - if (result) { - [stackTrace addObject:@{ - @"methodName": [stackLine substringWithRange:[result rangeAtIndex:1]], - @"file": [stackLine substringWithRange:[result rangeAtIndex:2]], - @"lineNumber": [stackLine substringWithRange:[result rangeAtIndex:3]], - @"column": [stackLine substringWithRange:[result rangeAtIndex:4]] - }]; - } - } - if ([stackTrace count]) { - errorInfo[RCTJSStackTraceKey] = stackTrace; - } - } - - // Fall back to just logging the line number - if (!errorInfo[RCTJSStackTraceKey] && json[@"line"]) { - errorInfo[RCTJSStackTraceKey] = @[@{ - @"methodName": @"", - @"file": RCTNullIfNil(json[@"sourceURL"]), - @"lineNumber": RCTNullIfNil(json[@"line"]), - @"column": @0, - }]; - } - } - } - - return [NSError errorWithDomain:RCTErrorDomain code:1 userInfo:errorInfo]; -} - #if RCT_DEV static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) @@ -699,7 +624,7 @@ static void installBasicSynchronousHooksOnContext(JSContext *context) id objcValue; if (errorJSRef || error) { if (!error) { - error = RCTNSErrorFromJSError(jscWrapper, contextJSRef, errorJSRef); + error = RCTNSErrorFromJSError([jscWrapper->JSValue valueWithJSValueRef:errorJSRef inContext:context]); } } else { // We often return `null` from JS when there is nothing for native side. [JSValue toValue] @@ -799,11 +724,12 @@ static NSError *executeApplicationScript(NSData *script, NSURL *sourceURL, RCTJS JSValueRef jsError = NULL; JSStringRef execJSString = jscWrapper->JSStringCreateWithUTF8CString((const char *)script.bytes); JSStringRef bundleURL = jscWrapper->JSStringCreateWithUTF8CString(sourceURL.absoluteString.UTF8String); - JSValueRef result = jscWrapper->JSEvaluateScript(ctx, execJSString, NULL, bundleURL, 0, &jsError); + jscWrapper->JSEvaluateScript(ctx, execJSString, NULL, bundleURL, 0, &jsError); jscWrapper->JSStringRelease(bundleURL); jscWrapper->JSStringRelease(execJSString); [performanceLogger markStopForTag:RCTPLScriptExecution]; - NSError *error = result ? nil : RCTNSErrorFromJSError(jscWrapper, ctx, jsError); + + NSError *error = jsError ? RCTNSErrorFromJSErrorRef(jsError, ctx, jscWrapper) : nil; RCT_PROFILE_END_EVENT(0, @"js_call"); return error; } @@ -864,7 +790,7 @@ static NSError *executeApplicationScript(NSData *script, NSURL *sourceURL, RCTJS jscWrapper->JSStringRelease(JSName); if (jsError) { - error = RCTNSErrorFromJSError(jscWrapper, ctx, jsError); + error = RCTNSErrorFromJSErrorRef(jsError, ctx, jscWrapper); } } RCT_PROFILE_END_EVENT(0, @"js_call,json_call"); @@ -904,7 +830,7 @@ static void executeRandomAccessModule(RCTJSCExecutor *executor, uint32_t moduleI if (!result) { dispatch_async(dispatch_get_main_queue(), ^{ - RCTFatal(RCTNSErrorFromJSError(jscWrapper, ctx, jsError)); + RCTFatal(RCTNSErrorFromJSErrorRef(jsError, ctx, jscWrapper)); [executor invalidate]; }); } diff --git a/React/Modules/RCTRedBox.m b/React/Modules/RCTRedBox.m index f5434b5ef..002032e3a 100644 --- a/React/Modules/RCTRedBox.m +++ b/React/Modules/RCTRedBox.m @@ -378,32 +378,31 @@ RCT_EXPORT_MODULE() if (details) { combinedMessage = [NSString stringWithFormat:@"%@\n\n%@", message, details]; } - [self showErrorMessage:combinedMessage]; + [self showErrorMessage:combinedMessage withStack:nil isUpdate:NO]; } - (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack { NSArray *stack = [RCTJSStackFrame stackFramesWithLines:rawStack]; - [self _showErrorMessage:message withStack:stack isUpdate:NO]; + [self showErrorMessage:message withStack:stack isUpdate:NO]; } -- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack +- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack { [self showErrorMessage:message withStack:stack isUpdate:NO]; } -- (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack +- (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack { [self showErrorMessage:message withStack:stack isUpdate:YES]; } -- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack isUpdate:(BOOL)isUpdate +- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack isUpdate:(BOOL)isUpdate { - [self _showErrorMessage:message withStack:[RCTJSStackFrame stackFramesWithDictionaries:stack] isUpdate:isUpdate]; -} + if (![[stack firstObject] isKindOfClass:[RCTJSStackFrame class]]) { + stack = [RCTJSStackFrame stackFramesWithDictionaries:stack]; + } -- (void)_showErrorMessage:(NSString *)message withStack:(NSArray *)stack isUpdate:(BOOL)isUpdate -{ dispatch_async(dispatch_get_main_queue(), ^{ if (!self->_window) { self->_window = [[RCTRedBoxWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index a52f195b6..cacd9c319 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -82,6 +82,7 @@ 391E86A41C623EC800009732 /* RCTTouchEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 391E86A21C623EC800009732 /* RCTTouchEvent.m */; }; 3D1E68DB1CABD13900DD7465 /* RCTDisplayLink.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D1E68D91CABD13900DD7465 /* RCTDisplayLink.m */; }; 3D37B5821D522B190042D5B5 /* RCTFont.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3D37B5811D522B190042D5B5 /* RCTFont.mm */; }; + 3DC724321D8BF99A00808C32 /* RCTJSCErrorHandling.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DC724311D8BF99A00808C32 /* RCTJSCErrorHandling.m */; }; 3EDCA8A51D3591E700450C31 /* RCTErrorInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EDCA8A41D3591E700450C31 /* RCTErrorInfo.m */; }; 58114A161AAE854800E7D092 /* RCTPicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A131AAE854800E7D092 /* RCTPicker.m */; }; 58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A151AAE854800E7D092 /* RCTPickerManager.m */; }; @@ -282,6 +283,8 @@ 3D37B5811D522B190042D5B5 /* RCTFont.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTFont.mm; sourceTree = ""; }; 3DB910701C74B21600838BBE /* RCTWebSocketProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebSocketProxy.h; sourceTree = ""; }; 3DB910711C74B21600838BBE /* RCTWebSocketProxyDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebSocketProxyDelegate.h; sourceTree = ""; }; + 3DC724301D8BF99A00808C32 /* RCTJSCErrorHandling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJSCErrorHandling.h; sourceTree = ""; }; + 3DC724311D8BF99A00808C32 /* RCTJSCErrorHandling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSCErrorHandling.m; sourceTree = ""; }; 3EDCA8A21D3591E700450C31 /* RCTErrorCustomizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTErrorCustomizer.h; sourceTree = ""; }; 3EDCA8A31D3591E700450C31 /* RCTErrorInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTErrorInfo.h; sourceTree = ""; }; 3EDCA8A41D3591E700450C31 /* RCTErrorInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTErrorInfo.m; sourceTree = ""; }; @@ -362,6 +365,8 @@ 134FCB381A6E7F0800051CC8 /* Executors */ = { isa = PBXGroup; children = ( + 3DC724301D8BF99A00808C32 /* RCTJSCErrorHandling.h */, + 3DC724311D8BF99A00808C32 /* RCTJSCErrorHandling.m */, 134FCB391A6E7F0800051CC8 /* RCTJSCExecutor.h */, 134FCB3A1A6E7F0800051CC8 /* RCTJSCExecutor.mm */, 85C199EC1CD2407900DAD810 /* RCTJSCWrapper.h */, @@ -806,6 +811,7 @@ B95154321D1B34B200FE7B80 /* RCTActivityIndicatorView.m in Sources */, 13B0801A1A69489C00A75B9A /* RCTNavigator.m in Sources */, 137327E71AA5CF210034F82E /* RCTTabBar.m in Sources */, + 3DC724321D8BF99A00808C32 /* RCTJSCErrorHandling.m in Sources */, 13F17A851B8493E5007D4C75 /* RCTRedBox.m in Sources */, 83392EB31B6634E10013B15F /* RCTModalHostViewController.m in Sources */, 14435CE51AAC4AE100FC20F4 /* RCTMap.m in Sources */,