Replaced RegExp method parser with recursive descent

Summary:
public

This diff replaces the RegEx module method parser with a handwritten recursive descent parser that's faster and easier to maintain.

The new parser is ~8 times faster when tested on the UIManager.managerChildren() method, and uses ~1/10 as much RAM.

The new parser also supports lightweight generics, and is more tolerant of white space.

(This means that you now can – and should – use types like `NSArray<NSString *> *` for your exported properties and method arguments, instead of `NSStringArray`).

Reviewed By: jspahrsummers

Differential Revision: D2736636

fb-gh-sync-id: f6a11431935fa8acc8ac36f3471032ec9a1c8490
This commit is contained in:
Nick Lockwood
2015-12-10 10:09:04 -08:00
committed by facebook-github-bot-4
parent ce7c0b735f
commit 88ac40666c
20 changed files with 413 additions and 170 deletions

View File

@@ -15,6 +15,7 @@
#import "RCTBridge.h"
#import "RCTConvert.h"
#import "RCTLog.h"
#import "RCTParserUtils.h"
#import "RCTUtils.h"
typedef BOOL (^RCTArgumentBlock)(RCTBridge *, NSUInteger, id);
@@ -50,7 +51,7 @@ typedef BOOL (^RCTArgumentBlock)(RCTBridge *, NSUInteger, id);
Class _moduleClass;
NSInvocation *_invocation;
NSArray<RCTArgumentBlock> *_argumentBlocks;
NSString *_objCMethodName;
NSString *_methodSignature;
SEL _selector;
NSDictionary *_profileArgs;
}
@@ -68,77 +69,106 @@ static void RCTLogArgumentError(RCTModuleMethod *method, NSUInteger index,
RCT_NOT_IMPLEMENTED(- (instancetype)init)
void RCTParseObjCMethodName(NSString **, NSArray<RCTMethodArgument *> **);
void RCTParseObjCMethodName(NSString **objCMethodName, NSArray<RCTMethodArgument *> **arguments)
// returns YES if the selector ends in a colon (indicating that there is at
// least one argument, and maybe more selector parts) or NO if it doesn't.
static BOOL RCTParseSelectorPart(const char **input, NSMutableString *selector)
{
static NSRegularExpression *typeNameRegex;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *unusedPattern = @"(?:__unused|__attribute__\\(\\(unused\\)\\))";
NSString *constPattern = @"(?:const)";
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];
});
// Extract argument types
NSString *methodName = *objCMethodName;
NSRange methodRange = {0, methodName.length};
NSMutableArray *args = [NSMutableArray array];
[typeNameRegex enumerateMatchesInString:methodName options:0 range:methodRange usingBlock:^(NSTextCheckingResult *result, __unused NSMatchingFlags flags, __unused BOOL *stop) {
NSRange typeRange = [result rangeAtIndex:5];
NSString *type = typeRange.length ? [methodName substringWithRange:typeRange] : @"id";
BOOL unused = ([result rangeAtIndex:2].length > 0);
RCTNullability nullability = [result rangeAtIndex:3].length ? RCTNullable :
[result rangeAtIndex:4].length ? RCTNonnullable : RCTNullabilityUnspecified;
[args addObject:[[RCTMethodArgument alloc] initWithType:type
nullability:nullability
unused:unused]];
}];
*arguments = [args 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];
NSString *selectorPart;
if (RCTParseIdentifier(input, &selectorPart)) {
[selector appendString:selectorPart];
}
*objCMethodName = methodName;
RCTSkipWhitespace(input);
if (RCTReadChar(input, ':')) {
[selector appendString:@":"];
RCTSkipWhitespace(input);
return YES;
}
return NO;
}
- (instancetype)initWithObjCMethodName:(NSString *)objCMethodName
JSMethodName:(NSString *)JSMethodName
moduleClass:(Class)moduleClass
static BOOL RCTParseUnused(const char **input)
{
return RCTReadString(input, "__unused") ||
RCTReadString(input, "__attribute__((unused))");
}
static RCTNullability RCTParseNullability(const char **input)
{
if (RCTReadString(input, "nullable")) {
return RCTNullable;
} else if (RCTReadString(input, "nonnull")) {
return RCTNonnullable;
}
return RCTNullabilityUnspecified;
}
SEL RCTParseMethodSignature(NSString *, NSArray<RCTMethodArgument *> **);
SEL RCTParseMethodSignature(NSString *methodSignature, NSArray<RCTMethodArgument *> **arguments)
{
const char *input = methodSignature.UTF8String;
RCTSkipWhitespace(&input);
NSMutableArray *args;
NSMutableString *selector = [NSMutableString new];
while (RCTParseSelectorPart(&input, selector)) {
if (!args) {
args = [NSMutableArray new];
}
// Parse type
if (RCTReadChar(&input, '(')) {
RCTSkipWhitespace(&input);
BOOL unused = RCTParseUnused(&input);
RCTSkipWhitespace(&input);
RCTNullability nullability = RCTParseNullability(&input);
RCTSkipWhitespace(&input);
NSString *type = RCTParseType(&input);
[args addObject:[[RCTMethodArgument alloc] initWithType:type
nullability:nullability
unused:unused]];
RCTSkipWhitespace(&input);
RCTReadChar(&input, ')');
RCTSkipWhitespace(&input);
} else {
// Type defaults to id if unspecified
[args addObject:[[RCTMethodArgument alloc] initWithType:@"id"
nullability:RCTNullable
unused:NO]];
}
// Argument name
RCTParseIdentifier(&input, NULL);
RCTSkipWhitespace(&input);
}
*arguments = [args copy];
return NSSelectorFromString(selector);
}
- (instancetype)initWithMethodSignature:(NSString *)methodSignature
JSMethodName:(NSString *)JSMethodName
moduleClass:(Class)moduleClass
{
if ((self = [super init])) {
_moduleClass = moduleClass;
_objCMethodName = [objCMethodName copy];
_methodSignature = [methodSignature copy];
_JSMethodName = JSMethodName.length > 0 ? JSMethodName : ({
NSString *methodName = objCMethodName;
NSString *methodName = methodSignature;
NSRange colonRange = [methodName rangeOfString:@":"];
if (colonRange.location != NSNotFound) {
methodName = [methodName substringToIndex:colonRange.location];
}
methodName = [methodName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
RCTAssert(methodName.length, @"%@ is not a valid JS function name, please"
" supply an alternative using RCT_REMAP_METHOD()", objCMethodName);
" supply an alternative using RCT_REMAP_METHOD()", methodSignature);
methodName;
});
if ([_objCMethodName rangeOfString:@"RCTPromise"].length) {
if ([_methodSignature rangeOfString:@"RCTPromise"].length) {
_functionType = RCTFunctionTypePromise;
} else {
_functionType = RCTFunctionTypeNormal;
@@ -151,15 +181,12 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray<RCTMethodArgument
- (void)processMethodSignature
{
NSArray<RCTMethodArgument *> *arguments;
NSString *objCMethodName = _objCMethodName;
RCTParseObjCMethodName(&objCMethodName, &arguments);
_selector = NSSelectorFromString(objCMethodName);
RCTAssert(_selector, @"%@ is not a valid selector", objCMethodName);
_selector = RCTParseMethodSignature(_methodSignature, &arguments);
RCTAssert(_selector, @"%@ is not a valid selector", _methodSignature);
// Create method invocation
NSMethodSignature *methodSignature = [_moduleClass instanceMethodSignatureForSelector:_selector];
RCTAssert(methodSignature, @"%@ is not a recognized Objective-C method.", objCMethodName);
RCTAssert(methodSignature, @"%@ is not a recognized Objective-C method.", _methodSignature);
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocation.selector = _selector;
_invocation = invocation;
@@ -203,7 +230,7 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray<RCTMethodArgument
BOOL isNullableType = NO;
RCTMethodArgument *argument = arguments[i - 2];
NSString *typeName = argument.type;
SEL selector = NSSelectorFromString([typeName stringByAppendingString:@":"]);
SEL selector = RCTConvertSelectorForType(typeName);
if ([RCTConvert respondsToSelector:selector]) {
switch (objcType[0]) {
@@ -296,7 +323,7 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray<RCTMethodArgument
} else if ([typeName isEqualToString:@"RCTPromiseResolveBlock"]) {
RCTAssert(i == numberOfArguments - 2,
@"The RCTPromiseResolveBlock must be the second to last parameter in -[%@ %@]",
_moduleClass, objCMethodName);
_moduleClass, _methodSignature);
RCT_ARG_BLOCK(
if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) {
RCTLogArgumentError(weakSelf, index, json, "should be a promise resolver function");
@@ -310,7 +337,7 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray<RCTMethodArgument
} else if ([typeName isEqualToString:@"RCTPromiseRejectBlock"]) {
RCTAssert(i == numberOfArguments - 1,
@"The RCTPromiseRejectBlock must be the last parameter in -[%@ %@]",
_moduleClass, objCMethodName);
_moduleClass, _methodSignature);
RCT_ARG_BLOCK(
if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) {
RCTLogArgumentError(weakSelf, index, json, "should be a promise rejecter function");