Skip to content

Commit

Permalink
Class for JS stack frames instead of dictionaries
Browse files Browse the repository at this point in the history
Summary:
Currently React Native codebase treats JS stack traces as array of dictionaries.

This diff switches the Red Box to use new `RCTJSStackFrame` for internal data representation, while keeping the exposed API unchanged. The next step would be to replace the rest of manual parsing and usage of dictionaries.

The new class has ability to parse the stack from raw strings or dictionaries.

Depends on D3429031

Reviewed By: javache

Differential Revision: D3473199

fbshipit-source-id: 90d2a4f5e8e054b75c99905f35c2ee54927bb311
  • Loading branch information
frantic authored and Facebook Github Bot 9 committed Jul 11, 2016
1 parent 13a19a8 commit e565056
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 22 deletions.
27 changes: 27 additions & 0 deletions React/Base/RCTJSStackFrame.h
Original file line number Diff line number Diff line change
@@ -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 <Foundation/Foundation.h>

@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<RCTJSStackFrame *> *)stackFramesWithLines:(NSString *)lines;
+ (NSArray<RCTJSStackFrame *> *)stackFramesWithDictionaries:(NSArray<NSDictionary *> *)dicts;

@end
101 changes: 101 additions & 0 deletions React/Base/RCTJSStackFrame.m
Original file line number Diff line number Diff line change
@@ -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:&regexError];
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<RCTJSStackFrame *> *)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<RCTJSStackFrame *> *)stackFramesWithDictionaries:(NSArray<NSDictionary *> *)dicts
{
NSMutableArray *stack = [NSMutableArray new];
for (NSDictionary *dict in dicts) {
RCTJSStackFrame *frame = [self stackFrameWithDictionary:dict];
if (frame) {
[stack addObject:frame];
}
}
return stack;
}

@end
48 changes: 26 additions & 22 deletions React/Modules/RCTRedBox.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@
#import "RCTConvert.h"
#import "RCTDefines.h"
#import "RCTUtils.h"
#import "RCTJSStackFrame.h"

#if RCT_DEBUG

@class RCTRedBoxWindow;

@protocol RCTRedBoxWindowActionDelegate <NSObject>

- (void)redBoxWindow:(RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(NSDictionary *)stackFrame;
- (void)redBoxWindow:(RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(RCTJSStackFrame *)stackFrame;
- (void)reloadFromRedBoxWindow:(RCTRedBoxWindow *)redBoxWindow;

@end
Expand All @@ -33,7 +34,7 @@ @implementation RCTRedBoxWindow
{
UITableView *_stackTraceTableView;
NSString *_lastErrorMessage;
NSArray<NSDictionary *> *_lastStackTrace;
NSArray<RCTJSStackFrame *> *_lastStackTrace;
}

- (instancetype)initWithFrame:(CGRect)frame
Expand Down Expand Up @@ -110,7 +111,7 @@ - (void)dealloc
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack isUpdate:(BOOL)isUpdate
- (void)showErrorMessage:(NSString *)message withStack:(NSArray<RCTJSStackFrame *> *)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])) {
Expand Down Expand Up @@ -156,9 +157,9 @@ - (void)copyStack
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]];
}
}
Expand All @@ -167,15 +168,14 @@ - (void)copyStack
[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;
}
Expand All @@ -200,7 +200,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N
}
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
NSUInteger index = indexPath.row;
NSDictionary *stackFrame = _lastStackTrace[index];
RCTJSStackFrame *stackFrame = _lastStackTrace[index];
return [self reuseCell:cell forStackFrame:stackFrame];
}

Expand All @@ -223,7 +223,7 @@ - (UITableViewCell *)reuseCell:(UITableViewCell *)cell forErrorMessage:(NSString
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"];
Expand All @@ -238,8 +238,8 @@ - (UITableViewCell *)reuseCell:(UITableViewCell *)cell forStackFrame:(NSDictiona
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 = @"";
Expand All @@ -266,7 +266,7 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath
{
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];
Expand Down Expand Up @@ -341,9 +341,8 @@ - (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details

- (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<RCTJSStackFrame *> *stack = [RCTJSStackFrame stackFramesWithLines:rawStack];
[self _showErrorMessage:message withStack:stack isUpdate:NO];
}

- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack
Expand All @@ -357,6 +356,11 @@ - (void)updateErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *
}

- (void)showErrorMessage:(NSString *)message withStack:(NSArray<NSDictionary *> *)stack isUpdate:(BOOL)isUpdate
{
[self _showErrorMessage:message withStack:[RCTJSStackFrame stackFramesWithDictionaries:stack] isUpdate:isUpdate];
}

- (void)_showErrorMessage:(NSString *)message withStack:(NSArray<RCTJSStackFrame *> *)stack isUpdate:(BOOL)isUpdate
{
dispatch_async(dispatch_get_main_queue(), ^{
if (!self->_window) {
Expand All @@ -379,14 +383,14 @@ - (void)invalidate
[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];
Expand Down
6 changes: 6 additions & 0 deletions React/React.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -117,6 +118,8 @@
/* Begin PBXFileReference section */
000E6CE91AB0E97F000CDF4D /* RCTSourceCode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSourceCode.h; sourceTree = "<group>"; };
000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSourceCode.m; sourceTree = "<group>"; };
008341F41D1DB34400876D9A /* RCTJSStackFrame.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSStackFrame.m; sourceTree = "<group>"; };
008341F51D1DB34400876D9A /* RCTJSStackFrame.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJSStackFrame.h; sourceTree = "<group>"; };
131B6AF01AF1093D00FFC3E0 /* RCTSegmentedControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSegmentedControl.h; sourceTree = "<group>"; };
131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSegmentedControl.m; sourceTree = "<group>"; };
131B6AF21AF1093D00FFC3E0 /* RCTSegmentedControlManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSegmentedControlManager.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down

0 comments on commit e565056

Please sign in to comment.