| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667 |
- /**
- * Copyright (c) 2015-present, Facebook, Inc.
- * All rights reserved.
- *
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- */
- #import "FBClassChainQueryParser.h"
- #import "FBErrorBuilder.h"
- #import "FBElementTypeTransformer.h"
- #import "FBExceptions.h"
- #import "NSPredicate+FBFormat.h"
- NS_ASSUME_NONNULL_BEGIN
- @interface FBBaseClassChainToken : NSObject
- @property (nonatomic) NSString *asString;
- @property (nonatomic) NSUInteger previousItemsCountToOverride;
- @end
- @interface FBClassNameToken : FBBaseClassChainToken
- @end
- @interface FBStarToken : FBBaseClassChainToken
- @end
- @interface FBDescendantMarkerToken : FBBaseClassChainToken
- @end
- @interface FBSplitterToken : FBBaseClassChainToken
- @end
- @interface FBOpeningBracketToken : FBBaseClassChainToken
- @end
- @interface FBClosingBracketToken : FBBaseClassChainToken
- @end
- @interface FBNumberToken : FBBaseClassChainToken
- @end
- @interface FBAbstractPredicateToken : FBBaseClassChainToken
- @property (nonatomic) BOOL isParsingCompleted;
- + (NSString *)enclosingMarker;
- @end
- @interface FBSelfPredicateToken : FBAbstractPredicateToken
- @end
- @interface FBDescendantPredicateToken : FBAbstractPredicateToken
- @end
- NS_ASSUME_NONNULL_END
- @implementation FBBaseClassChainToken
- - (id)init
- {
- self = [super init];
- if (self) {
- _asString = @"";
- _previousItemsCountToOverride = 0;
- }
- return self;
- }
- - (instancetype)initWithStringValue:(NSString *)stringValue
- {
- self = [super init];
- if (self) {
- _asString = stringValue;
- }
- return self;
- }
- + (NSCharacterSet *)allowedCharacters
- {
- // This method is expected to be overriden by subclasses
- return [NSCharacterSet characterSetWithCharactersInString:@""];
- }
- + (NSUInteger)maxLength
- {
- // This method is expected to be overriden by subclasses
- return ULONG_MAX;
- }
- - (NSArray<Class> *)followingTokens
- {
- // This method is expected to be overriden by subclasses
- return @[];
- }
- + (BOOL)canConsumeCharacter:(unichar)character
- {
- return [self.allowedCharacters characterIsMember:character];
- }
- - (void)appendChar:(unichar)character
- {
- NSMutableString *value = [NSMutableString stringWithString:self.asString];
- [value appendFormat:@"%C", character];
- self.asString = value.copy;;
- }
- - (nullable FBBaseClassChainToken*)followingTokenBasedOn:(unichar)character
- {
- for (Class matchingTokenClass in self.followingTokens) {
- if ([matchingTokenClass canConsumeCharacter:character]) {
- return [[[matchingTokenClass alloc] init] nextTokenWithCharacter:character];
- }
- }
- return nil;
- }
- - (nullable FBBaseClassChainToken*)nextTokenWithCharacter:(unichar)character
- {
- if ([self.class canConsumeCharacter:character] && self.asString.length < [self.class maxLength]) {
- [self appendChar:character];
- return self;
- }
- return [self followingTokenBasedOn:character];
- }
- @end
- @implementation FBClassNameToken
- + (NSCharacterSet *)allowedCharacters
- {
- return [NSCharacterSet letterCharacterSet];
- }
- - (NSArray<Class> *)followingTokens
- {
- return @[FBSplitterToken.class, FBOpeningBracketToken.class];
- }
- @end
- static NSString *const STAR_TOKEN = @"*";
- @implementation FBStarToken
- + (NSCharacterSet *)allowedCharacters
- {
- return [NSCharacterSet characterSetWithCharactersInString:STAR_TOKEN];
- }
- - (NSArray<Class> *)followingTokens
- {
- return @[FBSplitterToken.class, FBOpeningBracketToken.class];
- }
- - (nullable FBBaseClassChainToken*)nextTokenWithCharacter:(unichar)character
- {
- if ([self.class.allowedCharacters characterIsMember:character]) {
- if (self.asString.length >= 1) {
- FBDescendantMarkerToken *nextToken = [[FBDescendantMarkerToken alloc] initWithStringValue:[NSString stringWithFormat:@"%@%@", STAR_TOKEN, STAR_TOKEN]];
- nextToken.previousItemsCountToOverride = 1;
- return nextToken;
- }
- [self appendChar:character];
- return self;
- }
- return [self followingTokenBasedOn:character];
- }
- @end
- static NSString *const DESCENDANT_MARKER = @"**/";
- @implementation FBDescendantMarkerToken
- + (NSCharacterSet *)allowedCharacters
- {
- return [NSCharacterSet characterSetWithCharactersInString:@"*/"];
- }
- - (NSArray<Class> *)followingTokens
- {
- return @[FBClassNameToken.class, FBStarToken.class];
- }
- + (NSUInteger)maxLength
- {
- return 3;
- }
- - (nullable FBBaseClassChainToken*)nextTokenWithCharacter:(unichar)character
- {
- if ([self.class.allowedCharacters characterIsMember:character] && self.asString.length <= self.class.maxLength) {
- if (self.asString.length > 0 && ![DESCENDANT_MARKER hasPrefix:self.asString]) {
- return nil;
- }
- if (self.asString.length < self.class.maxLength) {
- [self appendChar:character];
- return self;
- }
- }
- return [self followingTokenBasedOn:character];
- }
- @end
- @implementation FBSplitterToken
- + (NSCharacterSet *)allowedCharacters
- {
- return [NSCharacterSet characterSetWithCharactersInString:@"/"];
- }
- - (NSArray<Class> *)followingTokens
- {
- return @[FBStarToken.class, FBClassNameToken.class];
- }
- + (NSUInteger)maxLength
- {
- return 1;
- }
- @end
- @implementation FBOpeningBracketToken
- + (NSCharacterSet *)allowedCharacters
- {
- return [NSCharacterSet characterSetWithCharactersInString:@"["];
- }
- - (NSArray<Class> *)followingTokens
- {
- return @[FBNumberToken.class, FBSelfPredicateToken.class, FBDescendantPredicateToken.class];
- }
- + (NSUInteger)maxLength
- {
- return 1;
- }
- @end
- @implementation FBNumberToken
- + (NSCharacterSet *)allowedCharacters
- {
- NSMutableCharacterSet *result = [NSMutableCharacterSet new];
- [result formUnionWithCharacterSet:[NSCharacterSet decimalDigitCharacterSet]];
- [result addCharactersInString:@"-"];
- return result.copy;
- }
- - (NSArray<Class> *)followingTokens
- {
- return @[FBClosingBracketToken.class];
- }
- @end
- @implementation FBClosingBracketToken
- + (NSCharacterSet *)allowedCharacters
- {
- return [NSCharacterSet characterSetWithCharactersInString:@"]"];
- }
- - (NSArray<Class> *)followingTokens
- {
- return @[FBSplitterToken.class, FBOpeningBracketToken.class];
- }
- + (NSUInteger)maxLength
- {
- return 1;
- }
- @end
- static NSString *const FBAbstractMethodInvocationException = @"FBAbstractMethodInvocationException";
- @implementation FBAbstractPredicateToken
- - (id)init
- {
- self = [super init];
- if (self) {
- _isParsingCompleted = NO;
- }
- return self;
- }
- + (NSString *)enclosingMarker
- {
- NSString *errMsg = [NSString stringWithFormat:@"The + (NSString *)enclosingMarker method is expected to be overriden by %@ class", NSStringFromClass(self.class)];
- @throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil];
- }
- + (NSCharacterSet *)allowedCharacters
- {
- return [NSCharacterSet illegalCharacterSet].invertedSet;
- }
- - (NSArray<Class> *)followingTokens
- {
- return @[FBClosingBracketToken.class];
- }
- + (BOOL)canConsumeCharacter:(unichar)character
- {
- return [[NSCharacterSet characterSetWithCharactersInString:self.class.enclosingMarker] characterIsMember:character];
- }
- - (void)stripLastChar
- {
- if (self.asString.length > 0) {
- self.asString = [self.asString substringToIndex:self.asString.length - 1];
- }
- }
- - (nullable FBBaseClassChainToken*)nextTokenWithCharacter:(unichar)character
- {
- NSString *currentChar = [NSString stringWithFormat:@"%C", character];
- if (!self.isParsingCompleted && [self.class.allowedCharacters characterIsMember:character]) {
- if (0 == self.asString.length) {
- if ([self.class.enclosingMarker isEqualToString:currentChar]) {
- // Do not include enclosing character
- return self;
- }
- } else if ([self.class.enclosingMarker isEqualToString:currentChar]) {
- [self appendChar:character];
- self.isParsingCompleted = YES;
- return self;
- }
- [self appendChar:character];
- return self;
- }
- if (self.isParsingCompleted) {
- if ([currentChar isEqualToString:self.class.enclosingMarker]) {
- // Escaped enclosing character has been detected. Do not finish parsing
- self.isParsingCompleted = NO;
- return self;
- } else {
- // Do not include enclosing character
- [self stripLastChar];
- }
- }
- return [self followingTokenBasedOn:character];
- }
- @end
- @implementation FBSelfPredicateToken
- + (NSString *)enclosingMarker
- {
- return @"`";
- }
- @end
- @implementation FBDescendantPredicateToken
- + (NSString *)enclosingMarker
- {
- return @"$";
- }
- @end
- @implementation FBClassChainItem
- - (instancetype)initWithType:(XCUIElementType)type position:(NSNumber *)position predicates:(NSArray<FBAbstractPredicateItem *> *)predicates isDescendant:(BOOL)isDescendant
- {
- self = [super init];
- if (self) {
- _type = type;
- _position = position;
- _predicates = predicates;
- _isDescendant = isDescendant;
- }
- return self;
- }
- @end
- @implementation FBClassChain
- - (instancetype)initWithElements:(NSArray<FBClassChainItem *> *)elements
- {
- self = [super init];
- if (self) {
- _elements = elements;
- }
- return self;
- }
- @end
- @implementation FBClassChainQueryParser
- static NSNumberFormatter *numberFormatter = nil;
- + (void)initialize {
- if (nil == numberFormatter) {
- numberFormatter = [[NSNumberFormatter alloc] init];
- numberFormatter.numberStyle = NSNumberFormatterDecimalStyle;
- }
- }
- + (NSError *)tokenizationErrorWithIndex:(NSUInteger)index originalQuery:(NSString *)originalQuery
- {
- NSString *description = [NSString stringWithFormat:@"Cannot parse class chain query '%@'. Unexpected character detected at position %@:\n%@ <----", originalQuery, @(index + 1), [originalQuery substringToIndex:index + 1]];
- return [[FBErrorBuilder.builder withDescription:description] build];
- }
- + (nullable NSArray<FBBaseClassChainToken *> *)tokenizedQueryWithQuery:(NSString*)classChainQuery error:(NSError **)error
- {
- NSUInteger queryStringLength = classChainQuery.length;
- FBBaseClassChainToken *token;
- unichar firstCharacter = [classChainQuery characterAtIndex:0];
- if ([classChainQuery hasPrefix:DESCENDANT_MARKER]) {
- token = [[FBDescendantMarkerToken alloc] initWithStringValue:DESCENDANT_MARKER];
- } else if ([FBClassNameToken canConsumeCharacter:firstCharacter]) {
- token = [[FBClassNameToken alloc] initWithStringValue:[NSString stringWithFormat:@"%C", firstCharacter]];
- } else if ([FBStarToken canConsumeCharacter:firstCharacter]) {
- token = [[FBStarToken alloc] initWithStringValue:[NSString stringWithFormat:@"%C", firstCharacter]];
- } else {
- if (error) {
- *error = [self.class tokenizationErrorWithIndex:0 originalQuery:classChainQuery];
- }
- return nil;
- }
- NSMutableArray *result = [NSMutableArray array];
- FBBaseClassChainToken *nextToken = token;
- for (NSUInteger charIdx = token.asString.length; charIdx < queryStringLength; ++charIdx) {
- nextToken = [token nextTokenWithCharacter:[classChainQuery characterAtIndex:charIdx]];
- if (nil == nextToken) {
- if (error) {
- *error = [self.class tokenizationErrorWithIndex:charIdx originalQuery:classChainQuery];
- }
- return nil;
- }
- if (nextToken != token) {
- [result addObject:token];
- if (nextToken.previousItemsCountToOverride > 0 && result.count > 0) {
- NSUInteger itemsCountToOverride = nextToken.previousItemsCountToOverride <= result.count ? nextToken.previousItemsCountToOverride : result.count;
- [result removeObjectsInRange:NSMakeRange(result.count - itemsCountToOverride, itemsCountToOverride)];
- }
- token = nextToken;
- }
- }
- if (nextToken) {
- if (nextToken.previousItemsCountToOverride > 0 && result.count > 0) {
- NSUInteger itemsCountToOverride = nextToken.previousItemsCountToOverride <= result.count ? nextToken.previousItemsCountToOverride : result.count;
- [result removeObjectsInRange:NSMakeRange(result.count - itemsCountToOverride, itemsCountToOverride)];
- }
- [result addObject:nextToken];
- }
-
- FBBaseClassChainToken *lastToken = [result lastObject];
- if (!([lastToken isKindOfClass:FBClosingBracketToken.class] ||
- [lastToken isKindOfClass:FBClassNameToken.class] ||
- [lastToken isKindOfClass:FBStarToken.class])) {
- if (error) {
- *error = [self.class tokenizationErrorWithIndex:queryStringLength - 1 originalQuery:classChainQuery];
- }
- return nil;
- }
-
- return result.copy;
- }
- + (NSError *)compilationErrorWithQuery:(NSString *)originalQuery description:(NSString *)description
- {
- NSString *fullDescription = [NSString stringWithFormat:@"Cannot parse class chain query '%@'. %@", originalQuery, description];
- return [[FBErrorBuilder.builder withDescription:fullDescription] build];
- }
- + (nullable FBClassChain*)compiledQueryWithTokenizedQuery:(NSArray<FBBaseClassChainToken *> *)tokenizedQuery
- originalQuery:(NSString *)originalQuery
- error:(NSError **)error
- {
- NSMutableArray *result = [NSMutableArray array];
- XCUIElementType chainElementType = XCUIElementTypeAny;
- NSNumber *chainElementPosition = nil;
- BOOL isTypeSet = NO;
- BOOL isPositionSet = NO;
- BOOL isDescendantSet = NO;
- NSMutableArray<FBAbstractPredicateItem *> *predicates = [NSMutableArray array];
- for (FBBaseClassChainToken *token in tokenizedQuery) {
- if ([token isKindOfClass:FBClassNameToken.class]) {
- if (isTypeSet) {
- if (error) {
- NSString *description = [NSString stringWithFormat:@"Unexpected token '%@'. The type name can be set only once.", token.asString];
- *error = [self.class compilationErrorWithQuery:originalQuery description:description];
- }
- return nil;
- }
- @try {
- chainElementType = [FBElementTypeTransformer elementTypeWithTypeName:token.asString];
- isTypeSet = YES;
- } @catch (NSException *e) {
- if ([e.name isEqualToString:FBInvalidArgumentException]) {
- if (error) {
- NSString *description = [NSString stringWithFormat:@"'%@' class name is unknown to WDA", token.asString];
- *error = [self.class compilationErrorWithQuery:originalQuery description:description];
- }
- return nil;
- }
- @throw e;
- }
- } else if ([token isKindOfClass:FBStarToken.class]) {
- if (isTypeSet) {
- if (error) {
- NSString *description = [NSString stringWithFormat:@"Unexpected token '%@'. The type name can be set only once.", token.asString];
- *error = [self.class compilationErrorWithQuery:originalQuery description:description];
- }
- return nil;
- }
- chainElementType = XCUIElementTypeAny;
- isTypeSet = YES;
- } else if ([token isKindOfClass:FBDescendantMarkerToken.class]) {
- if (isDescendantSet) {
- NSString *description = [NSString stringWithFormat:@"Unexpected token '%@'. Descendant markers cannot be duplicated.", token.asString];
- if (error) {
- *error = [self.class compilationErrorWithQuery:originalQuery description:description];
- }
- return nil;
- }
- isTypeSet = NO;
- isPositionSet = NO;
- [predicates removeAllObjects];
- isDescendantSet = YES;
- } else if ([token isKindOfClass:FBAbstractPredicateToken.class]) {
- if (isPositionSet) {
- NSString *description = [NSString stringWithFormat:@"Predicate value '%@' must be set before position value.", token.asString];
- if (error) {
- *error = [self.class compilationErrorWithQuery:originalQuery description:description];
- }
- return nil;
- }
- if (!((FBAbstractPredicateToken *)token).isParsingCompleted) {
- NSString *description = [NSString stringWithFormat:@"Cannot find the end of '%@' predicate value.", token.asString];
- if (error) {
- *error = [self.class compilationErrorWithQuery:originalQuery description:description];
- }
- return nil;
- }
- NSPredicate *value = [NSPredicate fb_snapshotBlockPredicateWithPredicate:[NSPredicate predicateWithFormat:token.asString]];
- if ([token isKindOfClass:FBSelfPredicateToken.class]) {
- [predicates addObject:[[FBSelfPredicateItem alloc] initWithValue:value]];
- } else if ([token isKindOfClass:FBDescendantPredicateToken.class]) {
- [predicates addObject:[[FBDescendantPredicateItem alloc] initWithValue:value]];
- }
- } else if ([token isKindOfClass:FBNumberToken.class]) {
- if (isPositionSet) {
- NSString *description = [NSString stringWithFormat:@"Position value '%@' is expected to be set only once.", token.asString];
- if (error) {
- *error = [self.class compilationErrorWithQuery:originalQuery description:description];
- }
- return nil;
- }
- NSNumber *position = [numberFormatter numberFromString:token.asString];
- if (nil == position || 0 == position.intValue) {
- NSString *description = [NSString stringWithFormat:@"Position value '%@' is expected to be a valid integer number not equal to zero.", token.asString];
- if (error) {
- *error = [self.class compilationErrorWithQuery:originalQuery description:description];
- }
- return nil;
- }
- chainElementPosition = position;
- isPositionSet = YES;
- } else if ([token isKindOfClass:FBSplitterToken.class]) {
- if (!isPositionSet) {
- chainElementPosition = nil;
- }
- if (isDescendantSet) {
- if (isTypeSet) {
- [result addObject:[[FBClassChainItem alloc] initWithType:chainElementType position:chainElementPosition predicates:predicates.copy isDescendant:YES]];
- isDescendantSet = NO;
- }
- } else {
- [result addObject:[[FBClassChainItem alloc] initWithType:chainElementType position:chainElementPosition predicates:predicates.copy isDescendant:NO]];
- }
- isTypeSet = NO;
- isPositionSet = NO;
- [predicates removeAllObjects];
- }
- }
- if (!isPositionSet) {
- chainElementPosition = nil;
- }
- if (isDescendantSet) {
- if (isTypeSet) {
- [result addObject:[[FBClassChainItem alloc] initWithType:chainElementType position:chainElementPosition predicates:predicates.copy isDescendant:YES]];
- } else {
- if (error) {
- NSString *description = @"Descendants lookup modifier '**/' should be followed with the actual element type";
- *error = [self.class compilationErrorWithQuery:originalQuery description:description];
- }
- return nil;
- }
- } else {
- [result addObject:[[FBClassChainItem alloc] initWithType:chainElementType position:chainElementPosition predicates:predicates.copy isDescendant:NO]];
- }
- return [[FBClassChain alloc] initWithElements:result.copy];
- }
- + (FBClassChain *)parseQuery:(NSString*)classChainQuery error:(NSError **)error
- {
- NSAssert(classChainQuery.length > 0, @"Query length should be greater than zero", nil);
- NSArray *tokenizedQuery = [self.class tokenizedQueryWithQuery:classChainQuery error:error];
- if (nil == tokenizedQuery) {
- return nil;
- }
- return [self.class compiledQueryWithTokenizedQuery:tokenizedQuery originalQuery:classChainQuery error:error];
- }
- @end
- @implementation FBAbstractPredicateItem
- - (instancetype)initWithValue:(NSPredicate *)value
- {
- self = [super init];
- if (self) {
- _value = value;
- }
- return self;
- }
- @end
- @implementation FBSelfPredicateItem
- @end
- @implementation FBDescendantPredicateItem
- @end
|