From 95b9dd3a88043a97a00c09543138284a63072589 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Wed, 29 Jul 2015 05:54:59 -0700 Subject: [PATCH] Added support for method argument nullability Summary: This diff adds support for enforcing nullability in the arguments for exported methods. We previously supported use of the nullable/nonnull attributes on method arguments, but didn't do anything to ensure that they were respected. Now, if an argument is marked as nonnull, and a null value is sent for that argument, it will display a redbox. In future, nonnull will be assumed by default, but for now we assume that un-annotated arguments can be null (to avoid breaking existing code). --- .../UIExplorer.xcodeproj/project.pbxproj | 12 +- .../RCTMethodArgumentTests.m | 131 +++++++++ .../RCTModuleMethodTests.m | 135 +++------ React/Base/RCTLog.h | 11 +- React/Base/RCTLog.m | 59 ++-- React/Base/RCTModuleMethod.h | 19 +- React/Base/RCTModuleMethod.m | 265 ++++++++++-------- 7 files changed, 397 insertions(+), 235 deletions(-) create mode 100644 Examples/UIExplorer/UIExplorerUnitTests/RCTMethodArgumentTests.m diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index 78f2f9977..5b9e04288 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -15,13 +15,14 @@ 134A8A2A1AACED7A00945AAE /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 134A8A251AACED6A00945AAE /* libRCTGeolocation.a */; }; 138D6A171B53CD440074A87E /* RCTCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 138D6A151B53CD440074A87E /* RCTCacheTests.m */; }; 138D6A181B53CD440074A87E /* RCTShadowViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 138D6A161B53CD440074A87E /* RCTShadowViewTests.m */; }; + 1393D0381B68CD1300E1B601 /* RCTModuleMethodTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1393D0371B68CD1300E1B601 /* RCTModuleMethodTests.m */; }; 139FDEDB1B0651FB00C62182 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDED91B0651EA00C62182 /* libRCTWebSocket.a */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 13DB03481B5D2ED500C27245 /* RCTJSONTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */; }; - 13DF61B61B67A45000EDB188 /* RCTModuleMethodTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DF61B51B67A45000EDB188 /* RCTModuleMethodTests.m */; }; + 13DF61B61B67A45000EDB188 /* RCTMethodArgumentTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */; }; 141FC1211B222EBB004D5FFB /* IntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 141FC1201B222EBB004D5FFB /* IntegrationTests.m */; }; 143BC5A11B21E45C00462512 /* UIExplorerSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */; }; 144D21241B2204C5006DB32B /* RCTClipRectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 144D21231B2204C5006DB32B /* RCTClipRectTests.m */; }; @@ -167,6 +168,7 @@ 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = ../../Libraries/Geolocation/RCTGeolocation.xcodeproj; sourceTree = ""; }; 138D6A151B53CD440074A87E /* RCTCacheTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCacheTests.m; sourceTree = ""; }; 138D6A161B53CD440074A87E /* RCTShadowViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTShadowViewTests.m; sourceTree = ""; }; + 1393D0371B68CD1300E1B601 /* RCTModuleMethodTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModuleMethodTests.m; sourceTree = ""; }; 139FDECA1B0651EA00C62182 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = ../../Libraries/WebSocket/RCTWebSocket.xcodeproj; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* UIExplorer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UIExplorer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = UIExplorer/AppDelegate.h; sourceTree = ""; }; @@ -177,7 +179,7 @@ 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = UIExplorer/main.m; sourceTree = ""; }; 13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../../Libraries/Settings/RCTSettings.xcodeproj; sourceTree = ""; }; 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSONTests.m; sourceTree = ""; }; - 13DF61B51B67A45000EDB188 /* RCTModuleMethodTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModuleMethodTests.m; sourceTree = ""; }; + 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMethodArgumentTests.m; sourceTree = ""; }; 141FC1201B222EBB004D5FFB /* IntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IntegrationTests.m; sourceTree = ""; }; 143BC57E1B21E18100462512 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 143BC5811B21E18100462512 /* testLayoutExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testLayoutExampleSnapshot_1@2x.png"; sourceTree = ""; }; @@ -362,7 +364,8 @@ 1497CFA91B21F5E400C1F8F2 /* RCTEventDispatcherTests.m */, 1300627E1B59179B0043FE5A /* RCTGzipTests.m */, 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */, - 13DF61B51B67A45000EDB188 /* RCTModuleMethodTests.m */, + 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */, + 1393D0371B68CD1300E1B601 /* RCTModuleMethodTests.m */, 138D6A161B53CD440074A87E /* RCTShadowViewTests.m */, 1497CFAA1B21F5E400C1F8F2 /* RCTSparseArrayTests.m */, 1497CFAB1B21F5E400C1F8F2 /* RCTUIManagerTests.m */, @@ -791,6 +794,7 @@ files = ( 1497CFB01B21F5E400C1F8F2 /* RCTConvert_UIFontTests.m in Sources */, 144D21241B2204C5006DB32B /* RCTClipRectTests.m in Sources */, + 1393D0381B68CD1300E1B601 /* RCTModuleMethodTests.m in Sources */, 1497CFB21B21F5E400C1F8F2 /* RCTSparseArrayTests.m in Sources */, 1300627F1B59179B0043FE5A /* RCTGzipTests.m in Sources */, 1497CFAF1B21F5E400C1F8F2 /* RCTConvert_NSURLTests.m in Sources */, @@ -801,7 +805,7 @@ 138D6A171B53CD440074A87E /* RCTCacheTests.m in Sources */, 13DB03481B5D2ED500C27245 /* RCTJSONTests.m in Sources */, 1497CFAC1B21F5E400C1F8F2 /* RCTAllocationTests.m in Sources */, - 13DF61B61B67A45000EDB188 /* RCTModuleMethodTests.m in Sources */, + 13DF61B61B67A45000EDB188 /* RCTMethodArgumentTests.m in Sources */, 138D6A181B53CD440074A87E /* RCTShadowViewTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTMethodArgumentTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTMethodArgumentTests.m new file mode 100644 index 000000000..8c4ce86e8 --- /dev/null +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTMethodArgumentTests.m @@ -0,0 +1,131 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import +#import + +#import "RCTModuleMethod.h" + +@interface RCTMethodArgumentTests : XCTestCase + +@end + +@implementation RCTMethodArgumentTests + +extern void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **argTypes); + +- (void)testOneArgument +{ + NSArray *arguments; + NSString *methodName = @"foo:(NSInteger)foo"; + RCTParseObjCMethodName(&methodName, &arguments); + XCTAssertEqualObjects(methodName, @"foo:"); + XCTAssertEqual(arguments.count, (NSUInteger)1); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[0]).type, @"NSInteger"); +} + +- (void)testTwoArguments +{ + NSArray *arguments; + NSString *methodName = @"foo:(NSInteger)foo bar:(BOOL)bar"; + RCTParseObjCMethodName(&methodName, &arguments); + XCTAssertEqualObjects(methodName, @"foo:bar:"); + XCTAssertEqual(arguments.count, (NSUInteger)2); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[0]).type, @"NSInteger"); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[1]).type, @"BOOL"); +} + +- (void)testSpaces +{ + NSArray *arguments; + NSString *methodName = @"foo : (NSInteger)foo bar : (BOOL) bar"; + RCTParseObjCMethodName(&methodName, &arguments); + XCTAssertEqualObjects(methodName, @"foo:bar:"); + XCTAssertEqual(arguments.count, (NSUInteger)2); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[0]).type, @"NSInteger"); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[1]).type, @"BOOL"); +} + +- (void)testNewlines +{ + NSArray *arguments; + NSString *methodName = @"foo : (NSInteger)foo\nbar : (BOOL) bar"; + RCTParseObjCMethodName(&methodName, &arguments); + XCTAssertEqualObjects(methodName, @"foo:bar:"); + XCTAssertEqual(arguments.count, (NSUInteger)2); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[0]).type, @"NSInteger"); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[1]).type, @"BOOL"); +} + +- (void)testUnnamedArgs +{ + NSArray *arguments; + NSString *methodName = @"foo:(NSInteger)foo:(BOOL)bar"; + RCTParseObjCMethodName(&methodName, &arguments); + XCTAssertEqualObjects(methodName, @"foo::"); + XCTAssertEqual(arguments.count, (NSUInteger)2); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[0]).type, @"NSInteger"); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[1]).type, @"BOOL"); +} + +- (void)testUntypedUnnamedArgs +{ + NSArray *arguments; + NSString *methodName = @"foo:foo:bar:bar"; + RCTParseObjCMethodName(&methodName, &arguments); + XCTAssertEqualObjects(methodName, @"foo:::"); + XCTAssertEqual(arguments.count, (NSUInteger)3); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[0]).type, @"id"); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[1]).type, @"id"); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[2]).type, @"id"); +} + +- (void)testAttributes +{ + NSArray *arguments; + NSString *methodName = @"foo:(__attribute__((nonnull)) NSString *)foo bar:(__unused BOOL)bar"; + RCTParseObjCMethodName(&methodName, &arguments); + XCTAssertEqualObjects(methodName, @"foo:bar:"); + XCTAssertEqual(arguments.count, (NSUInteger)2); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[0]).type, @"NSString"); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[1]).type, @"BOOL"); +} + +- (void)testNullability +{ + NSArray *arguments; + NSString *methodName = @"foo:(nullable NSString *)foo bar:(nonnull NSNumber *)bar baz:(id)baz"; + RCTParseObjCMethodName(&methodName, &arguments); + XCTAssertEqualObjects(methodName, @"foo:bar:baz:"); + XCTAssertEqual(arguments.count, (NSUInteger)3); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[0]).type, @"NSString"); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[1]).type, @"NSNumber"); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[2]).type, @"id"); + XCTAssertEqual(((RCTMethodArgument *)arguments[0]).nullability, RCTNullable); + XCTAssertEqual(((RCTMethodArgument *)arguments[1]).nullability, RCTNonnullable); + XCTAssertEqual(((RCTMethodArgument *)arguments[2]).nullability, RCTNullabilityUnspecified); +} + +- (void)testSemicolonStripping +{ + NSArray *arguments; + NSString *methodName = @"foo:(NSString *)foo bar:(BOOL)bar;"; + RCTParseObjCMethodName(&methodName, &arguments); + XCTAssertEqualObjects(methodName, @"foo:bar:"); + XCTAssertEqual(arguments.count, (NSUInteger)2); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[0]).type, @"NSString"); + XCTAssertEqualObjects(((RCTMethodArgument *)arguments[1]).type, @"BOOL"); +} + +@end diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m index 716397667..cd8ec7d29 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m @@ -1,15 +1,22 @@ -// -// RCTModuleMethodTests.m -// UIExplorer -// -// Created by Nick Lockwood on 28/07/2015. -// Copyright (c) 2015 Facebook. All rights reserved. -// +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ #import #import #import "RCTModuleMethod.h" +#import "RCTLog.h" @interface RCTModuleMethodTests : XCTestCase @@ -17,94 +24,40 @@ @implementation RCTModuleMethodTests -extern void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **argTypes); +- (void)doFooWithBar:(__unused NSString *)bar { } -- (void)testOneArgument +- (void)testNonnull { - NSArray *argTypes; - NSString *methodName = @"foo:(NSInteger)foo"; - RCTParseObjCMethodName(&methodName, &argTypes); - XCTAssertEqualObjects(methodName, @"foo:"); - XCTAssertEqual(argTypes.count, (NSUInteger)1); - XCTAssertEqualObjects(argTypes[0], @"NSInteger"); -} + NSString *methodName = @"doFooWithBar:(nonnull NSString *)bar"; + RCTModuleMethod *method = [[RCTModuleMethod alloc] initWithObjCMethodName:methodName + JSMethodName:nil + moduleClass:[self class]]; -- (void)testTwoArguments -{ - NSArray *argTypes; - NSString *methodName = @"foo:(NSInteger)foo bar:(BOOL)bar"; - RCTParseObjCMethodName(&methodName, &argTypes); - XCTAssertEqualObjects(methodName, @"foo:bar:"); - XCTAssertEqual(argTypes.count, (NSUInteger)2); - XCTAssertEqualObjects(argTypes[0], @"NSInteger"); - XCTAssertEqualObjects(argTypes[1], @"BOOL"); -} + { + __block BOOL loggedError = NO; + RCTPerformBlockWithLogFunction(^{ + [method invokeWithBridge:nil module:self arguments:@[@"Hello World"]]; + }, ^(RCTLogLevel level, + __unused NSString *fileName, + __unused NSNumber *lineNumber, + __unused NSString *message) { + loggedError = (level == RCTLogLevelError); + }); + XCTAssertFalse(loggedError); + } -- (void)testSpaces -{ - NSArray *argTypes; - NSString *methodName = @"foo : (NSInteger)foo bar : (BOOL) bar"; - RCTParseObjCMethodName(&methodName, &argTypes); - XCTAssertEqualObjects(methodName, @"foo:bar:"); - XCTAssertEqual(argTypes.count, (NSUInteger)2); - XCTAssertEqualObjects(argTypes[0], @"NSInteger"); - XCTAssertEqualObjects(argTypes[1], @"BOOL"); -} - -- (void)testNewlines -{ - NSArray *argTypes; - NSString *methodName = @"foo : (NSInteger)foo\nbar : (BOOL) bar"; - RCTParseObjCMethodName(&methodName, &argTypes); - XCTAssertEqualObjects(methodName, @"foo:bar:"); - XCTAssertEqual(argTypes.count, (NSUInteger)2); - XCTAssertEqualObjects(argTypes[0], @"NSInteger"); - XCTAssertEqualObjects(argTypes[1], @"BOOL"); -} - -- (void)testUnnamedArgs -{ - NSArray *argTypes; - NSString *methodName = @"foo:(NSInteger)foo:(BOOL)bar"; - RCTParseObjCMethodName(&methodName, &argTypes); - XCTAssertEqualObjects(methodName, @"foo::"); - XCTAssertEqual(argTypes.count, (NSUInteger)2); - XCTAssertEqualObjects(argTypes[0], @"NSInteger"); - XCTAssertEqualObjects(argTypes[1], @"BOOL"); -} - -- (void)testUntypedUnnamedArgs -{ - NSArray *argTypes; - NSString *methodName = @"foo:foo:bar:bar"; - RCTParseObjCMethodName(&methodName, &argTypes); - XCTAssertEqualObjects(methodName, @"foo:::"); - XCTAssertEqual(argTypes.count, (NSUInteger)3); - XCTAssertEqualObjects(argTypes[0], @"id"); - XCTAssertEqualObjects(argTypes[1], @"id"); - XCTAssertEqualObjects(argTypes[2], @"id"); -} - -- (void)testAttributes -{ - NSArray *argTypes; - NSString *methodName = @"foo:(__attribute__((nonnull)) NSString *)foo bar:(__unused BOOL)bar"; - RCTParseObjCMethodName(&methodName, &argTypes); - XCTAssertEqualObjects(methodName, @"foo:bar:"); - XCTAssertEqual(argTypes.count, (NSUInteger)2); - XCTAssertEqualObjects(argTypes[0], @"NSString"); - XCTAssertEqualObjects(argTypes[1], @"BOOL"); -} - -- (void)testSemicolonStripping -{ - NSArray *argTypes; - NSString *methodName = @"foo:(NSString *)foo bar:(BOOL)bar;"; - RCTParseObjCMethodName(&methodName, &argTypes); - XCTAssertEqualObjects(methodName, @"foo:bar:"); - XCTAssertEqual(argTypes.count, (NSUInteger)2); - XCTAssertEqualObjects(argTypes[0], @"NSString"); - XCTAssertEqualObjects(argTypes[1], @"BOOL"); + { + __block BOOL loggedError = NO; + RCTPerformBlockWithLogFunction(^{ + [method invokeWithBridge:nil module:self arguments:@[[NSNull null]]]; + }, ^(RCTLogLevel level, + __unused NSString *fileName, + __unused NSNumber *lineNumber, + __unused NSString *message) { + loggedError = (level == RCTLogLevelError); + }); + XCTAssertTrue(loggedError); + } } @end diff --git a/React/Base/RCTLog.h b/React/Base/RCTLog.h index 11b957fd3..a746a3f96 100644 --- a/React/Base/RCTLog.h +++ b/React/Base/RCTLog.h @@ -60,7 +60,7 @@ RCT_EXTERN NSString *RCTFormatLog( extern RCTLogFunction RCTDefaultLogFunction; /** - * These methods get and set the current logging threshold. This is the level + * These methods get and set the global logging threshold. This is the level * below which logs will be ignored. Default is RCTLogLevelInfo for debug and * RCTLogLevelError for production. */ @@ -68,7 +68,7 @@ RCT_EXTERN void RCTSetLogThreshold(RCTLogLevel threshold); RCT_EXTERN RCTLogLevel RCTGetLogThreshold(void); /** - * These methods get and set the current logging function called by the RCTLogXX + * These methods get and set the global logging function called by the RCTLogXX * macros. You can use these to replace the standard behavior with custom log * functionality. */ @@ -82,6 +82,13 @@ RCT_EXTERN RCTLogFunction RCTGetLogFunction(void); */ RCT_EXTERN void RCTAddLogFunction(RCTLogFunction logFunction); +/** + * This method temporarily overrides the log function while performing the + * specified block. This is useful for testing purposes (to detect if a given + * function logs something) or to suppress or override logging temporarily. + */ +RCT_EXTERN void RCTPerformBlockWithLogFunction(void (^block)(void), RCTLogFunction logFunction); + /** * This method adds a conditional prefix to any messages logged within the scope * of the passed block. This is useful for adding additional context to log diff --git a/React/Base/RCTLog.m b/React/Base/RCTLog.m index 25ad49e85..521ca5ddd 100644 --- a/React/Base/RCTLog.m +++ b/React/Base/RCTLog.m @@ -22,7 +22,7 @@ @end -static NSString *const RCTLogPrefixStack = @"RCTLogPrefixStack"; +static NSString *const RCTLogFunctionStack = @"RCTLogFunctionStack"; const char *RCTLogLevels[] = { "info", @@ -107,17 +107,42 @@ void RCTAddLogFunction(RCTLogFunction logFunction) } } -void RCTPerformBlockWithLogPrefix(void (^block)(void), NSString *prefix) +/** + * returns the topmost stacked log function for the current thread, which + * may not be the same as the current value of RCTCurrentLogFunction. + */ +static RCTLogFunction RCTGetLocalLogFunction() { NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; - NSMutableArray *prefixStack = threadDictionary[RCTLogPrefixStack]; - if (!prefixStack) { - prefixStack = [[NSMutableArray alloc] init]; - threadDictionary[RCTLogPrefixStack] = prefixStack; + NSArray *functionStack = threadDictionary[RCTLogFunctionStack]; + RCTLogFunction logFunction = [functionStack lastObject]; + if (logFunction) { + return logFunction; } - [prefixStack addObject:prefix]; + return RCTCurrentLogFunction; +} + +void RCTPerformBlockWithLogFunction(void (^block)(void), RCTLogFunction logFunction) +{ + NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; + NSMutableArray *functionStack = threadDictionary[RCTLogFunctionStack]; + if (!functionStack) { + functionStack = [[NSMutableArray alloc] init]; + threadDictionary[RCTLogFunctionStack] = functionStack; + } + [functionStack addObject:logFunction]; block(); - [prefixStack removeLastObject]; + [functionStack removeLastObject]; +} + +void RCTPerformBlockWithLogPrefix(void (^block)(void), NSString *prefix) +{ + RCTLogFunction logFunction = RCTGetLocalLogFunction(); + if (logFunction) { + RCTPerformBlockWithLogFunction(block, ^(RCTLogLevel level, NSString *fileName, NSNumber *lineNumber, NSString *message) { + logFunction(level, fileName, lineNumber, [prefix stringByAppendingString:message]); + }); + } } NSString *RCTFormatLog( @@ -165,8 +190,8 @@ void _RCTLogFormat( int lineNumber, NSString *format, ...) { - - BOOL log = RCT_DEBUG || (RCTCurrentLogFunction != nil); + RCTLogFunction logFunction = RCTGetLocalLogFunction(); + BOOL log = RCT_DEBUG || (logFunction != nil); if (log && level >= RCTCurrentLogThreshold) { // Get message @@ -175,18 +200,10 @@ void _RCTLogFormat( NSString *message = [[NSString alloc] initWithFormat:format arguments:args]; va_end(args); - // Add prefix - NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; - NSArray *prefixStack = threadDictionary[RCTLogPrefixStack]; - NSString *prefix = [prefixStack lastObject]; - if (prefix) { - message = [prefix stringByAppendingString:message]; - } - // Call log function - RCTCurrentLogFunction( - level, fileName ? @(fileName) : nil, (lineNumber >= 0) ? @(lineNumber) : nil, message - ); + if (logFunction) { + logFunction(level, fileName ? @(fileName) : nil, (lineNumber >= 0) ? @(lineNumber) : nil, message); + } #if RCT_DEBUG // Red box is only available in debug mode diff --git a/React/Base/RCTModuleMethod.h b/React/Base/RCTModuleMethod.h index 09a7514b4..f99f3ac49 100644 --- a/React/Base/RCTModuleMethod.h +++ b/React/Base/RCTModuleMethod.h @@ -16,12 +16,25 @@ typedef NS_ENUM(NSUInteger, RCTJavaScriptFunctionKind) { RCTJavaScriptFunctionKindAsync, }; +typedef NS_ENUM(NSUInteger, RCTNullability) { + RCTNullabilityUnspecified, + RCTNullable, + RCTNonnullable, +}; + +@interface RCTMethodArgument : NSObject + +@property (nonatomic, copy, readonly) NSString *type; +@property (nonatomic, readonly) RCTNullability nullability; + +@end + @interface RCTModuleMethod : NSObject @property (nonatomic, copy, readonly) NSString *JSMethodName; -@property (nonatomic, strong, readonly) Class moduleClass; -@property (nonatomic, assign, readonly) SEL selector; -@property (nonatomic, assign, readonly) RCTJavaScriptFunctionKind functionKind; +@property (nonatomic, readonly) Class moduleClass; +@property (nonatomic, readonly) SEL selector; +@property (nonatomic, readonly) RCTJavaScriptFunctionKind functionKind; - (instancetype)initWithObjCMethodName:(NSString *)objCMethodName JSMethodName:(NSString *)JSMethodName diff --git a/React/Base/RCTModuleMethod.m b/React/Base/RCTModuleMethod.m index 75fb45b73..4a2c6938d 100644 --- a/React/Base/RCTModuleMethod.m +++ b/React/Base/RCTModuleMethod.m @@ -17,6 +17,22 @@ #import "RCTLog.h" #import "RCTUtils.h" +typedef void (^RCTArgumentBlock)(RCTBridge *, NSInvocation *, NSUInteger, id); + +@implementation RCTMethodArgument + +- (instancetype)initWithType:(NSString *)type + nullability:(RCTNullability)nullability +{ + if ((self = [super init])) { + _type = [type copy]; + _nullability = nullability; + } + return self; +} + +@end + @interface RCTBridge (RCTModuleMethod) - (void)_invokeAndProcessModule:(NSString *)module @@ -36,16 +52,17 @@ RCT_NOT_IMPLEMENTED(-init) void RCTParseObjCMethodName(NSString **, NSArray **); -void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **argTypes) +void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) { static NSRegularExpression *typeNameRegex; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSString *unusedPattern = @"(?:__unused|__attribute__\\(\\(unused\\)\\))"; NSString *constPattern = @"(?:const)"; - NSString *nullabilityPattern = @"(?:__nullable|__nonnull|nullable|nonnull|__attribute__\\(\\(nonnull\\)\\)|__attribute__\\(\\(nullable\\)\\))"; - NSString *annotationPattern = [NSString stringWithFormat:@"(?:(?:%@|%@|%@)\\s*)", - unusedPattern, constPattern, nullabilityPattern]; + NSString *nullablePattern = @"(?:__nullable|nullable|__attribute__\\(\\(nullable\\)\\))"; + NSString *nonnullPattern = @"(?:__nonnull|nonnull|__attribute__\\(\\(nonnull\\)\\))"; + NSString *annotationPattern = [NSString stringWithFormat:@"(?:(?:%@|%@|(%@)|(%@))\\s*)", + unusedPattern, constPattern, nullablePattern, nonnullPattern]; NSString *pattern = [NSString stringWithFormat:@"(?<=:)(\\s*\\(%1$@?(\\w+?)(?:\\s*\\*)?%1$@?\\))?\\s*\\w+", annotationPattern]; typeNameRegex = [[NSRegularExpression alloc] initWithPattern:pattern options:0 error:NULL]; @@ -54,12 +71,15 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **argTypes) // Extract argument types NSString *methodName = *objCMethodName; NSRange methodRange = {0, methodName.length}; - NSMutableArray *arguments = [NSMutableArray array]; + NSMutableArray *args = [NSMutableArray array]; [typeNameRegex enumerateMatchesInString:methodName options:0 range:methodRange usingBlock:^(NSTextCheckingResult *result, __unused NSMatchingFlags flags, __unused BOOL *stop) { - NSRange range = [result rangeAtIndex:2]; - [arguments addObject:range.length ? [methodName substringWithRange:range] : @"id"]; + NSRange typeRange = [result rangeAtIndex:4]; + NSString *type = typeRange.length ? [methodName substringWithRange:typeRange] : @"id"; + RCTNullability nullability = [result rangeAtIndex:2].length ? RCTNullable : + [result rangeAtIndex:3].length ? RCTNonnullable : RCTNullabilityUnspecified; + [args addObject:[[RCTMethodArgument alloc] initWithType:type nullability:nullability]]; }]; - *argTypes = [arguments copy]; + *arguments = [args copy]; // Remove the parameter types and names methodName = [typeNameRegex stringByReplacingMatchesInString:methodName options:0 @@ -84,8 +104,8 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **argTypes) { if ((self = [super init])) { - NSArray *argTypes; - RCTParseObjCMethodName(&objCMethodName, &argTypes); + NSArray *arguments; + RCTParseObjCMethodName(&objCMethodName, &arguments); _moduleClass = moduleClass; _selector = NSSelectorFromString(objCMethodName); @@ -112,7 +132,7 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **argTypes) #define RCT_ARG_BLOCK(_logic) \ [argumentBlocks addObject:^(__unused RCTBridge *bridge, NSInvocation *invocation, NSUInteger index, id json) { \ _logic \ - [invocation setArgument:&value atIndex:index]; \ + [invocation setArgument:&value atIndex:(index) + 2]; \ }]; void (^addBlockArgument)(void) = ^{ @@ -143,12 +163,13 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **argTypes) }; for (NSUInteger i = 2; i < numberOfArguments; i++) { - const char *argumentType = [_methodSignature getArgumentTypeAtIndex:i]; + const char *objcType = [_methodSignature getArgumentTypeAtIndex:i]; - NSString *selName = argTypes[i - 2]; - SEL selector = NSSelectorFromString([selName stringByAppendingString:@":"]); + RCTMethodArgument *argument = arguments[i - 2]; + NSString *typeName = argument.type; + SEL selector = NSSelectorFromString([typeName stringByAppendingString:@":"]); if ([RCTConvert respondsToSelector:selector]) { - switch (argumentType[0]) { + switch (objcType[0]) { #define RCT_CONVERT_CASE(_value, _type) \ case _value: { \ @@ -157,109 +178,125 @@ case _value: { \ break; \ } - RCT_CONVERT_CASE(':', SEL) - RCT_CONVERT_CASE('*', const char *) - RCT_CONVERT_CASE('c', char) - RCT_CONVERT_CASE('C', unsigned char) - RCT_CONVERT_CASE('s', short) - RCT_CONVERT_CASE('S', unsigned short) - RCT_CONVERT_CASE('i', int) - RCT_CONVERT_CASE('I', unsigned int) - RCT_CONVERT_CASE('l', long) - RCT_CONVERT_CASE('L', unsigned long) - RCT_CONVERT_CASE('q', long long) - RCT_CONVERT_CASE('Q', unsigned long long) - RCT_CONVERT_CASE('f', float) - RCT_CONVERT_CASE('d', double) - RCT_CONVERT_CASE('B', BOOL) - RCT_CONVERT_CASE('@', id) - RCT_CONVERT_CASE('^', void *) + RCT_CONVERT_CASE(':', SEL) + RCT_CONVERT_CASE('*', const char *) + RCT_CONVERT_CASE('c', char) + RCT_CONVERT_CASE('C', unsigned char) + RCT_CONVERT_CASE('s', short) + RCT_CONVERT_CASE('S', unsigned short) + RCT_CONVERT_CASE('i', int) + RCT_CONVERT_CASE('I', unsigned int) + RCT_CONVERT_CASE('l', long) + RCT_CONVERT_CASE('L', unsigned long) + RCT_CONVERT_CASE('q', long long) + RCT_CONVERT_CASE('Q', unsigned long long) + RCT_CONVERT_CASE('f', float) + RCT_CONVERT_CASE('d', double) + RCT_CONVERT_CASE('B', BOOL) + RCT_CONVERT_CASE('@', id) + RCT_CONVERT_CASE('^', void *) - case '{': { - [argumentBlocks addObject:^(__unused RCTBridge *bridge, NSInvocation *invocation, NSUInteger index, id json) { - NSMethodSignature *methodSignature = [RCTConvert methodSignatureForSelector:selector]; - void *returnValue = malloc(methodSignature.methodReturnLength); - NSInvocation *_invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; - [_invocation setTarget:[RCTConvert class]]; - [_invocation setSelector:selector]; - [_invocation setArgument:&json atIndex:2]; - [_invocation invoke]; - [_invocation getReturnValue:returnValue]; + case '{': { + [argumentBlocks addObject:^(__unused RCTBridge *bridge, NSInvocation *invocation, NSUInteger index, id json) { + NSMethodSignature *methodSignature = [RCTConvert methodSignatureForSelector:selector]; + void *returnValue = malloc(methodSignature.methodReturnLength); + NSInvocation *_invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [_invocation setTarget:[RCTConvert class]]; + [_invocation setSelector:selector]; + [_invocation setArgument:&json atIndex:2]; + [_invocation invoke]; + [_invocation getReturnValue:returnValue]; - [invocation setArgument:returnValue atIndex:index]; + [invocation setArgument:returnValue atIndex:index + 2]; - free(returnValue); - }]; - break; - } - - default: - defaultCase(argumentType); + free(returnValue); + }]; + break; } - } else if ([selName isEqualToString:@"RCTResponseSenderBlock"]) { - addBlockArgument(); - } else if ([selName isEqualToString:@"RCTResponseErrorBlock"]) { - RCT_ARG_BLOCK( - if (RCT_DEBUG && json && ![json isKindOfClass:[NSNumber class]]) { - RCTLogError(@"Argument %tu (%@) of %@.%@ should be a number", index, - json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); - return; - } - - // Marked as autoreleasing, because NSInvocation doesn't retain arguments - __autoreleasing id value = (json ? ^(NSError *error) { - [bridge _invokeAndProcessModule:@"BatchedBridge" - method:@"invokeCallbackAndReturnFlushedQueue" - arguments:@[json, @[RCTJSErrorFromNSError(error)]]]; - } : ^(__unused NSError *error) {}); - ) - } else if ([selName isEqualToString:@"RCTPromiseResolveBlock"]) { - RCTAssert(i == numberOfArguments - 2, - @"The RCTPromiseResolveBlock must be the second to last parameter in -[%@ %@]", - _moduleClass, objCMethodName); - RCT_ARG_BLOCK( - if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { - RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise resolver ID", index, - json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); - return; - } - - // Marked as autoreleasing, because NSInvocation doesn't retain arguments - __autoreleasing RCTPromiseResolveBlock value = (^(id result) { - NSArray *arguments = result ? @[result] : @[]; - [bridge _invokeAndProcessModule:@"BatchedBridge" - method:@"invokeCallbackAndReturnFlushedQueue" - arguments:@[json, arguments]]; - }); - ) - _functionKind = RCTJavaScriptFunctionKindAsync; - } else if ([selName isEqualToString:@"RCTPromiseRejectBlock"]) { - RCTAssert(i == numberOfArguments - 1, - @"The RCTPromiseRejectBlock must be the last parameter in -[%@ %@]", - _moduleClass, objCMethodName); - RCT_ARG_BLOCK( - if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { - RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise rejecter ID", index, - json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); - return; - } - - // Marked as autoreleasing, because NSInvocation doesn't retain arguments - __autoreleasing RCTPromiseRejectBlock value = (^(NSError *error) { - NSDictionary *errorJSON = RCTJSErrorFromNSError(error); - [bridge _invokeAndProcessModule:@"BatchedBridge" - method:@"invokeCallbackAndReturnFlushedQueue" - arguments:@[json, @[errorJSON]]]; - }); - ) - _functionKind = RCTJavaScriptFunctionKindAsync; - } else { - - // Unknown argument type - RCTLogError(@"Unknown argument type '%@' in method %@. Extend RCTConvert" - " to support this type.", selName, [self methodName]); + default: + defaultCase(objcType); } + } else if ([typeName isEqualToString:@"RCTResponseSenderBlock"]) { + addBlockArgument(); + } else if ([typeName isEqualToString:@"RCTResponseErrorBlock"]) { + RCT_ARG_BLOCK( + + if (RCT_DEBUG && json && ![json isKindOfClass:[NSNumber class]]) { + RCTLogError(@"Argument %tu (%@) of %@.%@ should be a number", index, + json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); + return; + } + + // Marked as autoreleasing, because NSInvocation doesn't retain arguments + __autoreleasing id value = (json ? ^(NSError *error) { + [bridge _invokeAndProcessModule:@"BatchedBridge" + method:@"invokeCallbackAndReturnFlushedQueue" + arguments:@[json, @[RCTJSErrorFromNSError(error)]]]; + } : ^(__unused NSError *error) {}); + ) + } else if ([typeName isEqualToString:@"RCTPromiseResolveBlock"]) { + RCTAssert(i == numberOfArguments - 2, + @"The RCTPromiseResolveBlock must be the second to last parameter in -[%@ %@]", + _moduleClass, objCMethodName); + RCT_ARG_BLOCK( + if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { + RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise resolver ID", index, + json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); + return; + } + + // Marked as autoreleasing, because NSInvocation doesn't retain arguments + __autoreleasing RCTPromiseResolveBlock value = (^(id result) { + [bridge _invokeAndProcessModule:@"BatchedBridge" + method:@"invokeCallbackAndReturnFlushedQueue" + arguments:@[json, result ? @[result] : @[]]]; + }); + ) + _functionKind = RCTJavaScriptFunctionKindAsync; + } else if ([typeName isEqualToString:@"RCTPromiseRejectBlock"]) { + RCTAssert(i == numberOfArguments - 1, + @"The RCTPromiseRejectBlock must be the last parameter in -[%@ %@]", + _moduleClass, objCMethodName); + RCT_ARG_BLOCK( + if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { + RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise rejecter ID", index, + json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); + return; + } + + // Marked as autoreleasing, because NSInvocation doesn't retain arguments + __autoreleasing RCTPromiseRejectBlock value = (^(NSError *error) { + NSDictionary *errorJSON = RCTJSErrorFromNSError(error); + [bridge _invokeAndProcessModule:@"BatchedBridge" + method:@"invokeCallbackAndReturnFlushedQueue" + arguments:@[json, @[errorJSON]]]; + }); + ) + _functionKind = RCTJavaScriptFunctionKindAsync; + } else { + + // Unknown argument type + RCTLogError(@"Unknown argument type '%@' in method %@. Extend RCTConvert" + " to support this type.", typeName, [self methodName]); + } + + if (RCT_DEBUG) { + RCTNullability nullability = argument.nullability; + if (nullability == RCTNonnullable) { + RCTArgumentBlock oldBlock = argumentBlocks[i - 2]; + argumentBlocks[i - 2] = ^(RCTBridge *bridge, NSInvocation *invocation, NSUInteger index, id json) { + if (json == nil || json == (id)kCFNull) { + RCTLogError(@"Argument %tu of %@.%@ must not be null", index, + RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); + id null = nil; + [invocation setArgument:&null atIndex:index + 2]; + } else { + oldBlock(bridge, invocation, index, json); + } + }; + } + } } _argumentBlocks = [argumentBlocks copy]; @@ -305,8 +342,8 @@ case _value: { \ NSUInteger index = 0; for (id json in arguments) { id arg = RCTNilIfNull(json); - void (^block)(RCTBridge *, NSInvocation *, NSUInteger, id) = _argumentBlocks[index]; - block(bridge, invocation, index + 2, arg); + RCTArgumentBlock block = _argumentBlocks[index]; + block(bridge, invocation, index, arg); index++; }