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).
This commit is contained in:
Nick Lockwood
2015-07-29 05:54:59 -07:00
parent e0ea046092
commit 95b9dd3a88
7 changed files with 397 additions and 235 deletions

View File

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

View File

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

View File

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

View File

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