diff --git a/React/Base/RCTJSStackFrame.h b/React/Base/RCTJSStackFrame.h new file mode 100644 index 000000000..34c0fbe93 --- /dev/null +++ b/React/Base/RCTJSStackFrame.h @@ -0,0 +1,27 @@ +/** + * 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 + +@interface RCTJSStackFrame : NSObject + +@property (nonatomic, copy, readonly) NSString *methodName; +@property (nonatomic, copy, readonly) NSString *file; +@property (nonatomic, readonly) NSInteger lineNumber; +@property (nonatomic, readonly) NSInteger column; + +- (instancetype)initWithMethodName:(NSString *)methodName file:(NSString *)file lineNumber:(NSInteger)lineNumber column:(NSInteger)column; +- (NSDictionary *)toDictionary; + ++ (instancetype)stackFrameWithLine:(NSString *)line; ++ (instancetype)stackFrameWithDictionary:(NSDictionary *)dict; ++ (NSArray *)stackFramesWithLines:(NSString *)lines; ++ (NSArray *)stackFramesWithDictionaries:(NSArray *)dicts; + +@end diff --git a/React/Base/RCTJSStackFrame.m b/React/Base/RCTJSStackFrame.m new file mode 100644 index 000000000..fd2a615fe --- /dev/null +++ b/React/Base/RCTJSStackFrame.m @@ -0,0 +1,101 @@ +/** + * 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 "RCTJSStackFrame.h" +#import "RCTLog.h" + + +static NSRegularExpression *RCTJSStackFrameRegex() +{ + static dispatch_once_t onceToken; + static NSRegularExpression *_regex; + dispatch_once(&onceToken, ^{ + NSError *regexError; + _regex = [NSRegularExpression regularExpressionWithPattern:@"^([^@]+)@(.*):(\\d+):(\\d+)$" options:0 error:®exError]; + if (regexError) { + RCTLogError(@"Failed to build regex: %@", [regexError localizedDescription]); + } + }); + return _regex; +} + +@implementation RCTJSStackFrame + +- (instancetype)initWithMethodName:(NSString *)methodName file:(NSString *)file lineNumber:(NSInteger)lineNumber column:(NSInteger)column +{ + if (self = [super init]) { + _methodName = methodName; + _file = file; + _lineNumber = lineNumber; + _column = column; + } + return self; +} + +- (NSDictionary *)toDictionary +{ + return @{ + @"methodName": self.methodName, + @"file": self.file, + @"lineNumber": @(self.lineNumber), + @"column": @(self.column) + }; +} + ++ (instancetype)stackFrameWithLine:(NSString *)line +{ + NSTextCheckingResult *match = [RCTJSStackFrameRegex() firstMatchInString:line options:0 range:NSMakeRange(0, line.length)]; + if (!match) { + return nil; + } + + NSString *methodName = [line substringWithRange:[match rangeAtIndex:1]]; + NSString *file = [line substringWithRange:[match rangeAtIndex:2]]; + NSString *lineNumber = [line substringWithRange:[match rangeAtIndex:3]]; + NSString *column = [line substringWithRange:[match rangeAtIndex:4]]; + + return [[self alloc] initWithMethodName:methodName + file:file + lineNumber:[lineNumber integerValue] + column:[column integerValue]]; +} + ++ (instancetype)stackFrameWithDictionary:(NSDictionary *)dict +{ + return [[self alloc] initWithMethodName:dict[@"methodName"] + file:dict[@"file"] + lineNumber:[dict[@"lineNumber"] integerValue] + column:[dict[@"column"] integerValue]]; +} + ++ (NSArray *)stackFramesWithLines:(NSString *)lines +{ + NSMutableArray *stack = [NSMutableArray new]; + for (NSString *line in [lines componentsSeparatedByString:@"\n"]) { + RCTJSStackFrame *frame = [self stackFrameWithLine:line]; + if (frame) { + [stack addObject:frame]; + } + } + return stack; +} + ++ (NSArray *)stackFramesWithDictionaries:(NSArray *)dicts +{ + NSMutableArray *stack = [NSMutableArray new]; + for (NSDictionary *dict in dicts) { + RCTJSStackFrame *frame = [self stackFrameWithDictionary:dict]; + if (frame) { + [stack addObject:frame]; + } + } + return stack; +} + +@end diff --git a/React/Modules/RCTRedBox.m b/React/Modules/RCTRedBox.m index 92c3c541f..125b19f73 100644 --- a/React/Modules/RCTRedBox.m +++ b/React/Modules/RCTRedBox.m @@ -13,6 +13,7 @@ #import "RCTConvert.h" #import "RCTDefines.h" #import "RCTUtils.h" +#import "RCTJSStackFrame.h" #if RCT_DEBUG @@ -20,7 +21,7 @@ @protocol RCTRedBoxWindowActionDelegate -- (void)redBoxWindow:(RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(NSDictionary *)stackFrame; +- (void)redBoxWindow:(RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(RCTJSStackFrame *)stackFrame; - (void)reloadFromRedBoxWindow:(RCTRedBoxWindow *)redBoxWindow; @end @@ -33,7 +34,7 @@ { UITableView *_stackTraceTableView; NSString *_lastErrorMessage; - NSArray *_lastStackTrace; + NSArray *_lastStackTrace; } - (instancetype)initWithFrame:(CGRect)frame @@ -110,7 +111,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) [[NSNotificationCenter defaultCenter] removeObserver:self]; } -- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack isUpdate:(BOOL)isUpdate +- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack isUpdate:(BOOL)isUpdate { // Show if this is a new message, or if we're updating the previous message if ((self.hidden && !isUpdate) || (!self.hidden && isUpdate && [_lastErrorMessage isEqualToString:message])) { @@ -156,9 +157,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) fullStackTrace = [NSMutableString string]; } - for (NSDictionary *stackFrame in _lastStackTrace) { - [fullStackTrace appendString:[NSString stringWithFormat:@"%@\n", stackFrame[@"methodName"]]]; - if (stackFrame[@"file"]) { + for (RCTJSStackFrame *stackFrame in _lastStackTrace) { + [fullStackTrace appendString:[NSString stringWithFormat:@"%@\n", stackFrame.methodName]]; + if (stackFrame.file) { [fullStackTrace appendFormat:@" %@\n", [self formatFrameSource:stackFrame]]; } } @@ -167,15 +168,14 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) [pb setString:fullStackTrace]; } -- (NSString *)formatFrameSource:(NSDictionary *)stackFrame +- (NSString *)formatFrameSource:(RCTJSStackFrame *)stackFrame { NSString *lineInfo = [NSString stringWithFormat:@"%@:%zd", - [stackFrame[@"file"] lastPathComponent], - [stackFrame[@"lineNumber"] integerValue]]; + [stackFrame.file lastPathComponent], + stackFrame.lineNumber]; - NSInteger column = [stackFrame[@"column"] integerValue]; - if (column != 0) { - lineInfo = [lineInfo stringByAppendingFormat:@":%zd", column]; + if (stackFrame.column != 0) { + lineInfo = [lineInfo stringByAppendingFormat:@":%zd", stackFrame.column]; } return lineInfo; } @@ -200,7 +200,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) } UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; NSUInteger index = indexPath.row; - NSDictionary *stackFrame = _lastStackTrace[index]; + RCTJSStackFrame *stackFrame = _lastStackTrace[index]; return [self reuseCell:cell forStackFrame:stackFrame]; } @@ -223,7 +223,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) return cell; } -- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forStackFrame:(NSDictionary *)stackFrame +- (UITableViewCell *)reuseCell:(UITableViewCell *)cell forStackFrame:(RCTJSStackFrame *)stackFrame { if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"]; @@ -238,8 +238,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) cell.selectedBackgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.2]; } - cell.textLabel.text = stackFrame[@"methodName"]; - if (stackFrame[@"file"]) { + cell.textLabel.text = stackFrame.methodName; + if (stackFrame.file) { cell.detailTextLabel.text = [self formatFrameSource:stackFrame]; } else { cell.detailTextLabel.text = @""; @@ -266,7 +266,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) { if (indexPath.section == 1) { NSUInteger row = indexPath.row; - NSDictionary *stackFrame = _lastStackTrace[row]; + RCTJSStackFrame *stackFrame = _lastStackTrace[row]; [_actionDelegate redBoxWindow:self openStackFrameInEditor:stackFrame]; } [tableView deselectRowAtIndexPath:indexPath animated:YES]; @@ -341,9 +341,8 @@ RCT_EXPORT_MODULE() - (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack { - // TODO #11638796: convert rawStack into something useful - message = [NSString stringWithFormat:@"%@\n\n%@", message, rawStack]; - [self showErrorMessage:message withStack:nil isUpdate:NO]; + NSArray *stack = [RCTJSStackFrame stackFramesWithLines:rawStack]; + [self _showErrorMessage:message withStack:stack isUpdate:NO]; } - (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack @@ -357,6 +356,11 @@ RCT_EXPORT_MODULE() } - (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack isUpdate:(BOOL)isUpdate +{ + [self _showErrorMessage:message withStack:[RCTJSStackFrame stackFramesWithDictionaries:stack] isUpdate:isUpdate]; +} + +- (void)_showErrorMessage:(NSString *)message withStack:(NSArray *)stack isUpdate:(BOOL)isUpdate { dispatch_async(dispatch_get_main_queue(), ^{ if (!self->_window) { @@ -379,14 +383,14 @@ RCT_EXPORT_METHOD(dismiss) [self dismiss]; } -- (void)redBoxWindow:(RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(NSDictionary *)stackFrame +- (void)redBoxWindow:(RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(RCTJSStackFrame *)stackFrame { if (![_bridge.bundleURL.scheme hasPrefix:@"http"]) { RCTLogWarn(@"Cannot open stack frame in editor because you're not connected to the packager."); return; } - NSData *stackFrameJSON = [RCTJSONStringify(stackFrame, NULL) dataUsingEncoding:NSUTF8StringEncoding]; + NSData *stackFrameJSON = [RCTJSONStringify([stackFrame toDictionary], NULL) dataUsingEncoding:NSUTF8StringEncoding]; NSString *postLength = [NSString stringWithFormat:@"%tu", stackFrameJSON.length]; NSMutableURLRequest *request = [NSMutableURLRequest new]; request.URL = [NSURL URLWithString:@"/open-stack-frame" relativeToURL:_bridge.bundleURL]; diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 3873f67f1..665669678 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 000E6CEB1AB0E980000CDF4D /* RCTSourceCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */; }; + 008341F61D1DB34400876D9A /* RCTJSStackFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 008341F41D1DB34400876D9A /* RCTJSStackFrame.m */; }; 131B6AF41AF1093D00FFC3E0 /* RCTSegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */; }; 131B6AF51AF1093D00FFC3E0 /* RCTSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6AF31AF1093D00FFC3E0 /* RCTSegmentedControlManager.m */; }; 133CAE8E1B8E5CFD00F6AD92 /* RCTDatePicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 133CAE8D1B8E5CFD00F6AD92 /* RCTDatePicker.m */; }; @@ -117,6 +118,8 @@ /* Begin PBXFileReference section */ 000E6CE91AB0E97F000CDF4D /* RCTSourceCode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSourceCode.h; sourceTree = ""; }; 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSourceCode.m; sourceTree = ""; }; + 008341F41D1DB34400876D9A /* RCTJSStackFrame.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSStackFrame.m; sourceTree = ""; }; + 008341F51D1DB34400876D9A /* RCTJSStackFrame.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJSStackFrame.h; sourceTree = ""; }; 131B6AF01AF1093D00FFC3E0 /* RCTSegmentedControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSegmentedControl.h; sourceTree = ""; }; 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSegmentedControl.m; sourceTree = ""; }; 131B6AF21AF1093D00FFC3E0 /* RCTSegmentedControlManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSegmentedControlManager.h; sourceTree = ""; }; @@ -575,6 +578,8 @@ 83CBBA631A601ECA00E9B192 /* RCTJavaScriptExecutor.h */, 14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */, 14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */, + 008341F51D1DB34400876D9A /* RCTJSStackFrame.h */, + 008341F41D1DB34400876D9A /* RCTJSStackFrame.m */, 13A1F71C1A75392D00D3D453 /* RCTKeyCommands.h */, 13A1F71D1A75392D00D3D453 /* RCTKeyCommands.m */, 83CBBA4D1A601E3B00E9B192 /* RCTLog.h */, @@ -703,6 +708,7 @@ 14C2CA761B3AC64F00E6CBB2 /* RCTFrameUpdate.m in Sources */, 13B07FEF1A69327A00A75B9A /* RCTAlertManager.m in Sources */, 352DCFF01D19F4C20056D623 /* RCTI18nUtil.m in Sources */, + 008341F61D1DB34400876D9A /* RCTJSStackFrame.m in Sources */, 83CBBACC1A6023D300E9B192 /* RCTConvert.m in Sources */, 131B6AF41AF1093D00FFC3E0 /* RCTSegmentedControl.m in Sources */, 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */,