1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8#import "ABI48_0_0RCTJSStackFrame.h"
9
10#import "ABI48_0_0RCTLog.h"
11#import "ABI48_0_0RCTUtils.h"
12
13/**
14 * The RegEx used to parse Error.stack.
15 *
16 * JavaScriptCore has the following format:
17 *
18 *   Exception: Error: argh
19 *     func1@/path/to/file.js:2:18
20 *     func2@/path/to/file.js:6:8
21 *     eval@[native code]
22 *     global code@/path/to/file.js:13:5
23 *
24 * Another supported format:
25 *
26 *  Error: argh
27 *   at func1 (/path/to/file.js:2:18)
28 *   at func2 (/path/to/file.js:6:8)
29 *   at eval (native)
30 *   at global (/path/to/file.js:13:5)
31 */
32static NSRegularExpression *ABI48_0_0RCTJSStackFrameRegex()
33{
34  static dispatch_once_t onceToken;
35  static NSRegularExpression *_regex;
36  dispatch_once(&onceToken, ^{
37    NSString *pattern =
38        @"\\s*(?:at)?\\s*" // Skip leading "at" and whitespace, noncapturing
39        @"(.+?)" // Capture the function name (group 1)
40        @"\\s*[@(]" // Skip whitespace, then @ or (
41        @"(.*):" // Capture the file name (group 2), then colon
42        @"(\\d+):(\\d+)" // Line and column number (groups 3 and 4)
43        @"\\)?$" // Optional closing paren and EOL
44        ;
45    NSError *regexError;
46    _regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:&regexError];
47    if (regexError) {
48      ABI48_0_0RCTLogError(@"Failed to build regex: %@", [regexError localizedDescription]);
49    }
50  });
51  return _regex;
52}
53
54@implementation ABI48_0_0RCTJSStackFrame
55
56- (instancetype)initWithMethodName:(NSString *)methodName
57                              file:(NSString *)file
58                        lineNumber:(NSInteger)lineNumber
59                            column:(NSInteger)column
60                          collapse:(BOOL)collapse
61{
62  if (self = [super init]) {
63    _methodName = methodName;
64    _file = file;
65    _lineNumber = lineNumber;
66    _column = column;
67    _collapse = collapse;
68  }
69  return self;
70}
71
72- (NSDictionary *)toDictionary
73{
74  return @{
75    @"methodName" : ABI48_0_0RCTNullIfNil(self.methodName),
76    @"file" : ABI48_0_0RCTNullIfNil(self.file),
77    @"lineNumber" : @(self.lineNumber),
78    @"column" : @(self.column),
79    @"collapse" : @(self.collapse)
80  };
81}
82
83+ (instancetype)stackFrameWithLine:(NSString *)line
84{
85  NSTextCheckingResult *match = [ABI48_0_0RCTJSStackFrameRegex() firstMatchInString:line
86                                                                   options:0
87                                                                     range:NSMakeRange(0, line.length)];
88  if (!match) {
89    return nil;
90  }
91
92  // methodName may not be present for e.g. anonymous functions
93  const NSRange methodNameRange = [match rangeAtIndex:1];
94  NSString *methodName = methodNameRange.location == NSNotFound ? nil : [line substringWithRange:methodNameRange];
95  NSString *file = [line substringWithRange:[match rangeAtIndex:2]];
96  NSString *lineNumber = [line substringWithRange:[match rangeAtIndex:3]];
97  NSString *column = [line substringWithRange:[match rangeAtIndex:4]];
98
99  return [[self alloc] initWithMethodName:methodName
100                                     file:file
101                               lineNumber:[lineNumber integerValue]
102                                   column:[column integerValue]
103                                 collapse:NO];
104}
105
106+ (instancetype)stackFrameWithDictionary:(NSDictionary *)dict
107{
108  return [[self alloc] initWithMethodName:ABI48_0_0RCTNilIfNull(dict[@"methodName"])
109                                     file:dict[@"file"]
110                               lineNumber:[ABI48_0_0RCTNilIfNull(dict[@"lineNumber"]) integerValue]
111                                   column:[ABI48_0_0RCTNilIfNull(dict[@"column"]) integerValue]
112                                 collapse:[ABI48_0_0RCTNilIfNull(dict[@"collapse"]) boolValue]];
113}
114
115+ (NSArray<ABI48_0_0RCTJSStackFrame *> *)stackFramesWithLines:(NSString *)lines
116{
117  NSMutableArray *stack = [NSMutableArray new];
118  for (NSString *line in [lines componentsSeparatedByString:@"\n"]) {
119    ABI48_0_0RCTJSStackFrame *frame = [self stackFrameWithLine:line];
120    if (frame) {
121      [stack addObject:frame];
122    }
123  }
124  return stack;
125}
126
127+ (NSArray<ABI48_0_0RCTJSStackFrame *> *)stackFramesWithDictionaries:(NSArray<NSDictionary *> *)dicts
128{
129  NSMutableArray *stack = [NSMutableArray new];
130  for (NSDictionary *dict in dicts) {
131    ABI48_0_0RCTJSStackFrame *frame = [self stackFrameWithDictionary:dict];
132    if (frame) {
133      [stack addObject:frame];
134    }
135  }
136  return stack;
137}
138
139- (NSString *)description
140{
141  return [NSString stringWithFormat:@"<%@: %p method name: %@; file name: %@; line: %ld; column: %ld>",
142                                    self.class,
143                                    self,
144                                    self.methodName,
145                                    self.file,
146                                    (long)self.lineNumber,
147                                    (long)self.column];
148}
149
150@end
151