blob: d03bd25d609f0d0c3b32dfe70315a072893527d7 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/showcase/common/protocol_alerter.h"
#import <objc/runtime.h>
#include "base/logging.h"
#import "base/strings/sys_string_conversions.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Opaque value to use as an associated object key.
char kAssociatedProtocolNameKey;
}
@interface NSInvocation (Description)
// Returns a string description of the invocation consisting of the selector
// name interspersed with argument values.
- (NSString*)crsc_description;
@end
@interface ProtocolAlerter () {
NSSet<Protocol*>* _protocols;
// Selectors for which no logging should be done.
NSMutableSet<NSValue*>* _ignoredSelectors;
}
@end
@implementation ProtocolAlerter
@synthesize baseViewController = _baseViewController;
- (instancetype)initWithProtocols:(NSArray<Protocol*>*)protocols {
if (!protocols)
return nil;
// NSProxy isn't a subclass of NSObject, and has no superclass, so
// there's no [super init] to call.
_protocols = [[NSSet<Protocol*> alloc] initWithArray:protocols];
_ignoredSelectors = [NSMutableSet set];
return self;
}
- (void)ignoreSelector:(SEL)sel {
[_ignoredSelectors addObject:[NSValue valueWithPointer:sel]];
}
#pragma mark - NSProxy
- (NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
for (Protocol* protocol in _protocols) {
for (NSNumber* required in @[ @(YES), @(NO) ]) {
struct objc_method_description method =
protocol_getMethodDescription(protocol,
sel,
required.boolValue,
YES /* an instance method */);
if (method.name != NULL) {
NSMethodSignature* signature =
[NSMethodSignature signatureWithObjCTypes:method.types];
// Tag the method signature with the protocol name.
objc_setAssociatedObject(signature,
&kAssociatedProtocolNameKey,
NSStringFromProtocol(protocol),
OBJC_ASSOCIATION_COPY_NONATOMIC);
return signature;
}
}
}
return nil;
}
- (void)forwardInvocation:(NSInvocation*)invocation {
if ([_ignoredSelectors
containsObject:[NSValue valueWithPointer:invocation.selector]]) {
return;
}
// Instead of actually doing anything the protocol method would normally
// do, instead just generate a title and description and display an alert or
// log a message.
NSString* protocolName = objc_getAssociatedObject(
[self methodSignatureForSelector:invocation.selector],
&kAssociatedProtocolNameKey);
NSString* description = [invocation crsc_description];
if (self.baseViewController) {
[self showAlertWithTitle:protocolName message:description];
} else {
VLOG(0) << "Alerter -- protocol:"
<< base::SysNSStringToUTF8(protocolName);
VLOG(0) << "Alerter -- invocation:"
<< base::SysNSStringToUTF8(description);
}
}
#pragma mark - NSObject
- (BOOL)conformsToProtocol:(Protocol*)aProtocol {
for (Protocol* protocol in _protocols) {
// Handle protocols that conform to other protocols.
if (protocol_conformsToProtocol(protocol, aProtocol))
return YES;
}
return NO;
}
- (BOOL)respondsToSelector:(SEL)aSelector {
return [self methodSignatureForSelector:aSelector] != nil;
}
#pragma mark - Private
// Helper to show simple alert.
- (void)showAlertWithTitle:(NSString*)title message:(NSString*)message {
UIAlertController* alertController =
[UIAlertController alertControllerWithTitle:title
message:message
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* action =
[UIAlertAction actionWithTitle:@"Done"
style:UIAlertActionStyleCancel
handler:nil];
[action setAccessibilityLabel:@"protocol_alerter_done"];
[alertController addAction:action];
[self.baseViewController presentViewController:alertController
animated:YES
completion:nil];
}
@end
@implementation NSInvocation (Description)
- (NSString*)crsc_description {
NSInteger arguments = self.methodSignature.numberOfArguments;
NSString* selector = NSStringFromSelector(self.selector);
// NSInvocation's first two arguments are |self| and |_cmd|; if they are the
// only ones, then the invocation has no actual arguments.
if (arguments == 2)
return selector;
// Get the parts of the selector name by splitting on /:/, and dropping the
// last (empty) part.
NSArray* keywords = [[selector componentsSeparatedByString:@":"]
subarrayWithRange:NSMakeRange(0, arguments - 2)];
NSMutableString* description = [[NSMutableString alloc] init];
NSInteger argumentIndex = 2;
for (NSString* keyword in keywords) {
// Insert a space before each keyword after the first one.
if (description.length)
[description appendString:@" "];
[description appendString:keyword];
[description appendString:@":"];
[description
appendString:[self crsc_argumentDescriptionAtIndex:argumentIndex]];
argumentIndex++;
}
return description;
}
// Return a string describing the argument value at |index|.
// (|index| is in NSInvocation's argument array).
- (NSString*)crsc_argumentDescriptionAtIndex:(NSInteger)index {
const char* type = [self.methodSignature getArgumentTypeAtIndex:index];
switch (*type) {
case '@':
return [self crsc_objectDescriptionAtIndex:index];
case 'q':
return [self crsc_longLongDescriptionAtIndex:index];
case 'Q':
return [self crsc_unsignedLongLongDescriptionAtIndex:index];
// Add cases as needed here.
default:
return [NSString stringWithFormat:@"<Unknown Type:%s>", type];
}
}
// Return a string describing an argument at |index| that's known to be an
// objective-C object.
- (NSString*)crsc_objectDescriptionAtIndex:(NSInteger)index {
__unsafe_unretained id object;
[self getArgument:&object atIndex:index];
if (!object)
return @"nil";
NSString* description = [object description];
NSString* className = NSStringFromClass([object class]);
if (!description) {
return
[NSString stringWithFormat:@"<%@ object, no description>", className];
}
// Wrap strings in @" ... ".
if ([object isKindOfClass:[NSString class]])
return [NSString stringWithFormat:@"@\"%@\"", description];
// Remove the address of objects from their descriptions, so (for example):
// <NSObject: 0xc00lf0ccac1a>
// becomes just:
// <NSObject>
NSRange range = NSMakeRange(0, description.length);
NSString* classPlusAddress =
[className stringByAppendingString:@": 0x[0-9a-c]+"];
return [description
stringByReplacingOccurrencesOfString:classPlusAddress
withString:className
options:NSRegularExpressionSearch
range:range];
}
// Returns a string describing an argument at |index| that is known to be a long
// long.
- (NSString*)crsc_longLongDescriptionAtIndex:(NSInteger)index {
long long value;
[self getArgument:&value atIndex:index];
return [NSString stringWithFormat:@"%lld", value];
}
// Returns a string describing an argument at |index| that is known to be an
// unsigned long long.
- (NSString*)crsc_unsignedLongLongDescriptionAtIndex:(NSInteger)index {
unsigned long long value;
[self getArgument:&value atIndex:index];
return [NSString stringWithFormat:@"%llu", value];
}
@end