diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index b2c4a8910..78f2f9977 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 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 */; }; 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 */; }; @@ -176,6 +177,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 = ""; }; 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 = ""; }; @@ -360,6 +362,7 @@ 1497CFA91B21F5E400C1F8F2 /* RCTEventDispatcherTests.m */, 1300627E1B59179B0043FE5A /* RCTGzipTests.m */, 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */, + 13DF61B51B67A45000EDB188 /* RCTModuleMethodTests.m */, 138D6A161B53CD440074A87E /* RCTShadowViewTests.m */, 1497CFAA1B21F5E400C1F8F2 /* RCTSparseArrayTests.m */, 1497CFAB1B21F5E400C1F8F2 /* RCTUIManagerTests.m */, @@ -798,6 +801,7 @@ 138D6A171B53CD440074A87E /* RCTCacheTests.m in Sources */, 13DB03481B5D2ED500C27245 /* RCTJSONTests.m in Sources */, 1497CFAC1B21F5E400C1F8F2 /* RCTAllocationTests.m in Sources */, + 13DF61B61B67A45000EDB188 /* RCTModuleMethodTests.m in Sources */, 138D6A181B53CD440074A87E /* RCTShadowViewTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m new file mode 100644 index 000000000..716397667 --- /dev/null +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m @@ -0,0 +1,110 @@ +// +// RCTModuleMethodTests.m +// UIExplorer +// +// Created by Nick Lockwood on 28/07/2015. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import +#import + +#import "RCTModuleMethod.h" + +@interface RCTModuleMethodTests : XCTestCase + +@end + +@implementation RCTModuleMethodTests + +extern void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **argTypes); + +- (void)testOneArgument +{ + NSArray *argTypes; + NSString *methodName = @"foo:(NSInteger)foo"; + RCTParseObjCMethodName(&methodName, &argTypes); + XCTAssertEqualObjects(methodName, @"foo:"); + XCTAssertEqual(argTypes.count, (NSUInteger)1); + XCTAssertEqualObjects(argTypes[0], @"NSInteger"); +} + +- (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"); +} + +- (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"); +} + +@end diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index 2d6bbab08..8acad2163 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -670,7 +670,7 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL } RCTProfileEndEvent(@"Invoke callback", @"objc_call", @{ - @"module": method.moduleClassName, + @"module": NSStringFromClass(method.moduleClass), @"method": method.JSMethodName, @"selector": NSStringFromSelector(method.selector), @"args": RCTJSONStringify(RCTNullIfNil(params), NULL), diff --git a/React/Base/RCTModuleMethod.h b/React/Base/RCTModuleMethod.h index 710b1b899..09a7514b4 100644 --- a/React/Base/RCTModuleMethod.h +++ b/React/Base/RCTModuleMethod.h @@ -18,8 +18,8 @@ typedef NS_ENUM(NSUInteger, RCTJavaScriptFunctionKind) { @interface RCTModuleMethod : NSObject -@property (nonatomic, copy, readonly) NSString *moduleClassName; @property (nonatomic, copy, readonly) NSString *JSMethodName; +@property (nonatomic, strong, readonly) Class moduleClass; @property (nonatomic, assign, readonly) SEL selector; @property (nonatomic, assign, readonly) RCTJavaScriptFunctionKind functionKind; diff --git a/React/Base/RCTModuleMethod.m b/React/Base/RCTModuleMethod.m index 00ed6c1f2..75fb45b73 100644 --- a/React/Base/RCTModuleMethod.m +++ b/React/Base/RCTModuleMethod.m @@ -11,6 +11,7 @@ #import +#import "RCTAssert.h" #import "RCTBridge.h" #import "RCTConvert.h" #import "RCTLog.h" @@ -34,52 +35,73 @@ RCT_NOT_IMPLEMENTED(-init) +void RCTParseObjCMethodName(NSString **, NSArray **); +void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **argTypes) +{ + 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 *pattern = [NSString stringWithFormat:@"(?<=:)(\\s*\\(%1$@?(\\w+?)(?:\\s*\\*)?%1$@?\\))?\\s*\\w+", + annotationPattern]; + typeNameRegex = [[NSRegularExpression alloc] initWithPattern:pattern options:0 error:NULL]; + }); + + // Extract argument types + NSString *methodName = *objCMethodName; + NSRange methodRange = {0, methodName.length}; + NSMutableArray *arguments = [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"]; + }]; + *argTypes = [arguments copy]; + + // Remove the parameter types and names + methodName = [typeNameRegex stringByReplacingMatchesInString:methodName options:0 + range:methodRange + withTemplate:@""]; + + // Remove whitespace + methodName = [methodName stringByReplacingOccurrencesOfString:@"\n" withString:@""]; + methodName = [methodName stringByReplacingOccurrencesOfString:@" " withString:@""]; + + // Strip trailing semicolon + if ([methodName hasSuffix:@";"]) { + methodName = [methodName substringToIndex:methodName.length - 1]; + } + + *objCMethodName = methodName; +} + - (instancetype)initWithObjCMethodName:(NSString *)objCMethodName JSMethodName:(NSString *)JSMethodName moduleClass:(Class)moduleClass { if ((self = [super init])) { - static NSRegularExpression *typeRegex; - static NSRegularExpression *selectorRegex; - if (!typeRegex) { - NSString *unusedPattern = @"(?:__unused|__attribute__\\(\\(unused\\)\\))"; - NSString *constPattern = @"(?:const)"; - NSString *nullabilityPattern = @"(?:__nullable|__nonnull|nullable|nonnull)"; - NSString *annotationPattern = [NSString stringWithFormat:@"(?:(?:%@|%@|%@)\\s*)", - unusedPattern, constPattern, nullabilityPattern]; - NSString *pattern = [NSString stringWithFormat:@"\\(%1$@?(\\w+?)(?:\\s*\\*)?%1$@?\\)", annotationPattern]; - typeRegex = [[NSRegularExpression alloc] initWithPattern:pattern options:0 error:NULL]; - selectorRegex = [[NSRegularExpression alloc] initWithPattern:@"(?<=:).*?(?=[a-zA-Z_]+:|$)" options:0 error:NULL]; - } - - NSMutableArray *argumentNames = [NSMutableArray array]; - [typeRegex enumerateMatchesInString:objCMethodName options:0 range:NSMakeRange(0, objCMethodName.length) usingBlock:^(NSTextCheckingResult *result, __unused NSMatchingFlags flags, __unused BOOL *stop) { - NSString *argumentName = [objCMethodName substringWithRange:[result rangeAtIndex:1]]; - [argumentNames addObject:argumentName]; - }]; - - // Remove the parameters' type and name - objCMethodName = [selectorRegex stringByReplacingMatchesInString:objCMethodName - options:0 - range:NSMakeRange(0, objCMethodName.length) - withTemplate:@""]; - // Remove any spaces since `selector : (Type)name` is a valid syntax - objCMethodName = [objCMethodName stringByReplacingOccurrencesOfString:@" " withString:@""]; + NSArray *argTypes; + RCTParseObjCMethodName(&objCMethodName, &argTypes); _moduleClass = moduleClass; - _moduleClassName = NSStringFromClass(_moduleClass); _selector = NSSelectorFromString(objCMethodName); + RCTAssert(_selector, @"%@ is not a valid selector", objCMethodName); + _JSMethodName = JSMethodName.length > 0 ? JSMethodName : ({ - NSString *methodName = NSStringFromSelector(_selector); + NSString *methodName = objCMethodName; NSRange colonRange = [methodName rangeOfString:@":"]; - if (colonRange.length) { + if (colonRange.location != NSNotFound) { methodName = [methodName substringToIndex:colonRange.location]; } + RCTAssert(methodName.length, @"%@ is not a valid JS function name, please" + " supply an alternative using RCT_REMAP_METHOD()", objCMethodName); methodName; }); - // Get method signature _methodSignature = [_moduleClass instanceMethodSignatureForSelector:_selector]; @@ -123,8 +145,8 @@ RCT_NOT_IMPLEMENTED(-init) for (NSUInteger i = 2; i < numberOfArguments; i++) { const char *argumentType = [_methodSignature getArgumentTypeAtIndex:i]; - NSString *argumentName = argumentNames[i - 2]; - SEL selector = NSSelectorFromString([argumentName stringByAppendingString:@":"]); + NSString *selName = argTypes[i - 2]; + SEL selector = NSSelectorFromString([selName stringByAppendingString:@":"]); if ([RCTConvert respondsToSelector:selector]) { switch (argumentType[0]) { @@ -174,9 +196,9 @@ case _value: { \ default: defaultCase(argumentType); } - } else if ([argumentName isEqualToString:@"RCTResponseSenderBlock"]) { + } else if ([selName isEqualToString:@"RCTResponseSenderBlock"]) { addBlockArgument(); - } else if ([argumentName isEqualToString:@"RCTResponseErrorBlock"]) { + } else if ([selName isEqualToString:@"RCTResponseErrorBlock"]) { RCT_ARG_BLOCK( if (RCT_DEBUG && json && ![json isKindOfClass:[NSNumber class]]) { @@ -192,10 +214,10 @@ case _value: { \ arguments:@[json, @[RCTJSErrorFromNSError(error)]]]; } : ^(__unused NSError *error) {}); ) - } else if ([argumentName isEqualToString:@"RCTPromiseResolveBlock"]) { + } else if ([selName isEqualToString:@"RCTPromiseResolveBlock"]) { RCTAssert(i == numberOfArguments - 2, @"The RCTPromiseResolveBlock must be the second to last parameter in -[%@ %@]", - _moduleClassName, objCMethodName); + _moduleClass, objCMethodName); RCT_ARG_BLOCK( if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise resolver ID", index, @@ -212,10 +234,10 @@ case _value: { \ }); ) _functionKind = RCTJavaScriptFunctionKindAsync; - } else if ([argumentName isEqualToString:@"RCTPromiseRejectBlock"]) { + } else if ([selName isEqualToString:@"RCTPromiseRejectBlock"]) { RCTAssert(i == numberOfArguments - 1, @"The RCTPromiseRejectBlock must be the last parameter in -[%@ %@]", - _moduleClassName, objCMethodName); + _moduleClass, objCMethodName); RCT_ARG_BLOCK( if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise rejecter ID", index, @@ -236,7 +258,7 @@ case _value: { \ // Unknown argument type RCTLogError(@"Unknown argument type '%@' in method %@. Extend RCTConvert" - " to support this type.", argumentName, [self methodName]); + " to support this type.", selName, [self methodName]); } }