From bc32947554f94acd51561e0bba4585c5a74c56b5 Mon Sep 17 00:00:00 2001 From: Appcelerator Build Date: Thu, 1 Oct 2020 11:49:53 -0400 Subject: [PATCH] fix(ios): format js errors in cli output (#12147) Fixes TIMOB-27812 --- .../TitaniumKit/Sources/API/KrollBridge.m | 7 +- .../Sources/API/TiExceptionHandler.h | 10 ++ .../Sources/API/TiExceptionHandler.m | 149 +++++++++++------- 3 files changed, 112 insertions(+), 54 deletions(-) diff --git a/iphone/TitaniumKit/TitaniumKit/Sources/API/KrollBridge.m b/iphone/TitaniumKit/TitaniumKit/Sources/API/KrollBridge.m index 015876e6c56..e295d0d8633 100644 --- a/iphone/TitaniumKit/TitaniumKit/Sources/API/KrollBridge.m +++ b/iphone/TitaniumKit/TitaniumKit/Sources/API/KrollBridge.m @@ -876,7 +876,7 @@ - (KrollWrapper *)loadJavascriptText:(NSString *)data fromFile:(NSString *)filen KrollWrapper *module = [self loadCommonJSModule:data withSourceURL:url_]; if (![module respondsToSelector:@selector(replaceValue:forKey:notification:)]) { - @throw [NSException exceptionWithName:@"org.appcelerator.kroll" + @throw [NSException exceptionWithName:@"org.appcelerator.kroll.invalidmodule" reason:[NSString stringWithFormat:@"Module \"%@\" failed to leave a valid exports object", filename] userInfo:nil]; } @@ -1327,6 +1327,11 @@ - (id)require:(KrollContext *)kroll path:(NSString *)path break; } } + @catch (NSException *exception) { + if ([exception.name isEqualToString:@"org.appcelerator.kroll.invalidmodule"]) { + return nil; + } + } @finally { [self setCurrentURL:oldURL]; // Cache the resolved path for this request if we got a module diff --git a/iphone/TitaniumKit/TitaniumKit/Sources/API/TiExceptionHandler.h b/iphone/TitaniumKit/TitaniumKit/Sources/API/TiExceptionHandler.h index 8039df4c633..2355baf90d0 100644 --- a/iphone/TitaniumKit/TitaniumKit/Sources/API/TiExceptionHandler.h +++ b/iphone/TitaniumKit/TitaniumKit/Sources/API/TiExceptionHandler.h @@ -24,6 +24,11 @@ */ @property (nonatomic, readonly) NSString *sourceURL; +/** + * Returns the actual source code line as a string where the error happened. + */ +@property (nonatomic, readonly) NSString *sourceLine; + /** * Returns line number where error happened. */ @@ -54,6 +59,11 @@ */ @property (nonatomic, readonly) NSArray *nativeStack; +/** + * Returns the pre-formated and cleaned native stack trace. + */ +@property (nonatomic, readonly) NSArray *formattedNativeStack; + - (id)initWithMessage:(NSString *)message sourceURL:(NSString *)sourceURL lineNo:(NSInteger)lineNo; - (id)initWithDictionary:(NSDictionary *)dictionary; diff --git a/iphone/TitaniumKit/TitaniumKit/Sources/API/TiExceptionHandler.m b/iphone/TitaniumKit/TitaniumKit/Sources/API/TiExceptionHandler.m index 73aba0536bc..c4099464ee3 100644 --- a/iphone/TitaniumKit/TitaniumKit/Sources/API/TiExceptionHandler.m +++ b/iphone/TitaniumKit/TitaniumKit/Sources/API/TiExceptionHandler.m @@ -73,7 +73,7 @@ - (void)reportException:(NSException *)exception - (void)reportScriptError:(TiScriptError *)scriptError { - DebugLog(@"[ERROR] Script Error %@", [scriptError detailedDescription]); + DebugLog(@"[ERROR] %@", scriptError); id currentDelegate = _delegate; if (currentDelegate == nil) { @@ -94,45 +94,15 @@ - (void)reportScriptError:(JSValue *)error inJSContext:(JSContext *)context - (void)showScriptError:(TiScriptError *)error { - NSArray *exceptionStackTrace = [error valueForKey:@"nativeStack"]; - if (exceptionStackTrace == nil) { - exceptionStackTrace = [NSThread callStackSymbols]; - } - NSMutableDictionary *errorDict = [error.dictionaryValue mutableCopy]; [errorDict setObject:[NSNumber numberWithLong:error.column] forKey:@"column"]; [errorDict setObject:[NSNumber numberWithLong:error.lineNo] forKey:@"line"]; - if (exceptionStackTrace == nil) { - [[TiApp app] showModalError:[error description]]; - } else { - NSMutableArray *formattedStackTrace = [[[NSMutableArray alloc] init] autorelease]; - NSUInteger exceptionStackTraceLength = [exceptionStackTrace count]; - - // re-size stack trace and format results. Starting at index = 1 to not include showScriptError call - for (NSInteger i = 1; i < (exceptionStackTraceLength >= 20 ? 20 : exceptionStackTraceLength); i++) { - NSString *line = [self removeWhitespace:[exceptionStackTrace objectAtIndex:i]]; - - // remove stack index - line = [line substringFromIndex:[line rangeOfString:@" "].location + 1]; - - [formattedStackTrace addObject:line]; - } - NSString *stackTrace = [formattedStackTrace componentsJoinedByString:@"\n"]; - [errorDict setObject:stackTrace forKey:@"nativeStack"]; - - [[TiApp app] showModalError:[NSString stringWithFormat:@"%@\n\n%@", [error description], stackTrace]]; - } + NSString *stackTrace = [error.formattedNativeStack componentsJoinedByString:@"\n"]; + [errorDict setObject:stackTrace forKey:@"nativeStack"]; + [[TiApp app] showModalError:[error description]]; [[NSNotificationCenter defaultCenter] postNotificationName:kTiErrorNotification object:self userInfo:errorDict]; } -- (NSString *)removeWhitespace:(NSString *)line -{ - while ([line rangeOfString:@" "].length > 0) { - line = [line stringByReplacingOccurrencesOfString:@" " withString:@" "]; - } - return line; -} - #pragma mark - TiExceptionHandlerDelegate - (void)handleUncaughtException:(NSException *)exception @@ -151,11 +121,13 @@ @implementation TiScriptError @synthesize message = _message; @synthesize sourceURL = _sourceURL; +@synthesize sourceLine = _sourceLine; @synthesize lineNo = _lineNo; @synthesize column = _column; @synthesize dictionaryValue = _dictionaryValue; @synthesize backtrace = _backtrace; @synthesize nativeStack = _nativeStack; +@synthesize formattedNativeStack = _formattedNativeStack; - (id)initWithMessage:(NSString *)message sourceURL:(NSString *)sourceURL lineNo:(NSInteger)lineNo { @@ -194,36 +166,51 @@ - (void)dealloc { RELEASE_TO_NIL(_message); RELEASE_TO_NIL(_sourceURL); + RELEASE_TO_NIL(_sourceLine); RELEASE_TO_NIL(_backtrace); RELEASE_TO_NIL(_dictionaryValue); RELEASE_TO_NIL(_nativeStack); + RELEASE_TO_NIL(_formattedNativeStack); [super dealloc]; } - (NSString *)description { - if (self.sourceURL != nil) { - // attempt to load encrypted source code - NSURL *sourceURL = [NSURL URLWithString:self.sourceURL]; - NSData *data = [TiUtils loadAppResource:sourceURL]; - NSString *source = nil; - if (data == nil) { - source = [NSString stringWithContentsOfFile:[sourceURL path] encoding:NSUTF8StringEncoding error:NULL]; - } else { - source = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]; + NSMutableString *message = [[NSMutableString new] autorelease]; + NSString *encodedBundlePath = [NSString stringWithFormat:@"file://%@", [[NSBundle mainBundle].bundlePath stringByReplacingOccurrencesOfString:@" " withString:@"%20"]]; + + if (self.sourceURL) { + [message appendFormat:@"%@:%ld\n", [self.sourceURL stringByReplacingOccurrencesOfString:encodedBundlePath withString:@""], (long)self.lineNo]; + [message appendFormat:@"%@\n", self.sourceLine]; + NSString *columnIndicatorPadding = [@"" stringByPaddingToLength:self.column withString:@" " startingAtIndex:0]; + [message appendFormat:@"%@^\n", columnIndicatorPadding]; + } + + NSString *type = self.dictionaryValue[@"type"] != nil ? self.dictionaryValue[@"type"] : @"Error"; + [message appendFormat:@"%@: %@", type, self.message]; + + NSString *jsStack = [self.backtrace stringByReplacingOccurrencesOfString:encodedBundlePath withString:@""]; + NSArray *jsStackLines = [jsStack componentsSeparatedByCharactersInSet:NSCharacterSet.newlineCharacterSet]; + NSMutableString *formattedJsStack = [[NSMutableString new] autorelease]; + for (NSString *line in jsStackLines) { + NSRange atSymbolRange = [line rangeOfString:@"@"]; + NSInteger atSymbolIndex = atSymbolRange.location == NSNotFound ? -1 : atSymbolRange.location; + NSString *source = [line substringFromIndex:atSymbolIndex + 1]; + NSString *symbolName = @"Object."; + if (atSymbolIndex != -1) { + symbolName = [line substringWithRange:NSMakeRange(0, atSymbolIndex)]; } - NSArray *lines = [source componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; - NSString *line = [lines objectAtIndex:self.lineNo - 1]; - NSString *linePointer = [@"" stringByPaddingToLength:self.column withString:@" " startingAtIndex:0]; + // global code is our module wrapper code which can be ignored + if ([symbolName isEqualToString:@"global code"]) { + continue; + } + [formattedJsStack appendFormat:@"\n at %@ (%@)", symbolName, source]; + } + [message appendString:formattedJsStack]; - // remove bundle path from source paths - NSString *encodedBundlePath = [NSString stringWithFormat:@"file://%@", [[NSBundle mainBundle].bundlePath stringByReplacingOccurrencesOfString:@" " withString:@"%20"]]; - NSString *jsStack = [self.backtrace stringByReplacingOccurrencesOfString:encodedBundlePath withString:@""]; + [message appendFormat:@"\n\n %@", [self.formattedNativeStack componentsJoinedByString:@"\n "]]; - return [NSString stringWithFormat:@"/%@:%ld\n%@\n%@^\n%@\n%@", [self.sourceURL lastPathComponent], (long)self.lineNo, line, linePointer, self.message, jsStack]; - } else { - return [NSString stringWithFormat:@"%@", self.message]; - } + return message; } - (NSString *)detailedDescription @@ -231,6 +218,62 @@ - (NSString *)detailedDescription return _dictionaryValue != nil ? [_dictionaryValue description] : [self description]; } +- (NSArray *)formattedNativeStack +{ + if (_formattedNativeStack != nil) { + return _formattedNativeStack; + } + + NSArray *stackTrace = self.nativeStack; + if (stackTrace == nil) { + stackTrace = [NSThread callStackSymbols]; + } + NSMutableArray *formattedStackTrace = [[[NSMutableArray alloc] init] autorelease]; + NSUInteger stackTraceLength = [stackTrace count]; + // re-size stack trace and format results. starting at index = 2 to not include this method and callee + for (NSInteger i = 2; i < (stackTraceLength >= 20 ? 20 : stackTraceLength); i++) { + NSString *line = [self removeWhitespace:stackTrace[i]]; + // remove stack index + line = [line substringFromIndex:[line rangeOfString:@" "].location + 1]; + [formattedStackTrace addObject:line]; + } + _formattedNativeStack = [formattedStackTrace copy]; + + return _formattedNativeStack; +} + +- (NSString *)sourceLine +{ + if (_sourceURL == nil) { + return nil; + } + + if (_sourceLine != nil) { + return _sourceLine; + } + + NSURL *sourceURL = [NSURL URLWithString:self.sourceURL]; + NSData *data = [TiUtils loadAppResource:sourceURL]; + NSString *source = nil; + if (data == nil) { + source = [NSString stringWithContentsOfFile:[sourceURL path] encoding:NSUTF8StringEncoding error:NULL]; + } else { + source = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]; + } + NSArray *lines = [source componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; + _sourceLine = [[lines objectAtIndex:self.lineNo - 1] retain]; + + return _sourceLine; +} + +- (NSString *)removeWhitespace:(NSString *)line +{ + while ([line rangeOfString:@" "].length > 0) { + line = [line stringByReplacingOccurrencesOfString:@" " withString:@" "]; + } + return line; +} + @end //