Added unit tests for RCTModuleMethod parsing, and fixed some edge cases

This commit is contained in:
Nick Lockwood 2015-07-28 13:09:22 -07:00
parent e964e741ef
commit b812b0ee2e
5 changed files with 177 additions and 41 deletions

View File

@ -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 = "<group>"; };
13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../../Libraries/Settings/RCTSettings.xcodeproj; sourceTree = "<group>"; };
13DB03471B5D2ED500C27245 /* RCTJSONTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJSONTests.m; sourceTree = "<group>"; };
13DF61B51B67A45000EDB188 /* RCTModuleMethodTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModuleMethodTests.m; sourceTree = "<group>"; };
141FC1201B222EBB004D5FFB /* IntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IntegrationTests.m; sourceTree = "<group>"; };
143BC57E1B21E18100462512 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
143BC5811B21E18100462512 /* testLayoutExampleSnapshot_1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testLayoutExampleSnapshot_1@2x.png"; sourceTree = "<group>"; };
@ -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;

View File

@ -0,0 +1,110 @@
//
// RCTModuleMethodTests.m
// UIExplorer
//
// Created by Nick Lockwood on 28/07/2015.
// Copyright (c) 2015 Facebook. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#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

View File

@ -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),

View File

@ -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;

View File

@ -11,6 +11,7 @@
#import <objc/message.h>
#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]);
}
}