diff --git a/AppledocTests/Parsing/ObjectiveCConstantStateTests.mm b/AppledocTests/Parsing/ObjectiveCConstantStateTests.mm index f1e91460..c1856225 100644 --- a/AppledocTests/Parsing/ObjectiveCConstantStateTests.mm +++ b/AppledocTests/Parsing/ObjectiveCConstantStateTests.mm @@ -336,6 +336,106 @@ static void runWithState(void(^handler)(ObjectiveCConstantState *state)) { }); }); }); + + describe(@"edge cases / limitations for supporting descriptors", ^{ + describe(@"requires at least one non-descriptor looking token before starting accepting destriptors", ^{ + it(@"should detect types with double underscore prefix", ^{ + runWithState(^(ObjectiveCConstantState *state) { + runWithString(@"__type1 __type2 __name;", ^(id parser, id tokens) { + // setup + id store = [OCMockObject mockForClass:[Store class]]; + [[store expect] setCurrentSourceInfo:OCMOCK_ANY]; + [[store expect] beginConstant]; + [[store expect] beginConstantTypes]; + [[store expect] appendType:@"__type1"]; + [[store expect] appendType:@"__type2"]; + [[store expect] endCurrentObject]; // types + [[store expect] appendConstantName:@"__name"]; + [[store expect] endCurrentObject]; // constant + [[parser expect] popState]; + ObjectiveCParseData *data = [ObjectiveCParseData dataWithStream:tokens parser:parser store:store]; + // execute + [state parseWithData:data]; + // verify + ^{ [store verify]; } should_not raise_exception(); + ^{ [parser verify]; } should_not raise_exception(); + }); + }); + }); + + it(@"should detect types with uppercase letters", ^{ + runWithState(^(ObjectiveCConstantState *state) { + runWithString(@"TYPE1 TYPE2 NAME;", ^(id parser, id tokens) { + // setup + id store = [OCMockObject mockForClass:[Store class]]; + [[store expect] setCurrentSourceInfo:OCMOCK_ANY]; + [[store expect] beginConstant]; + [[store expect] beginConstantTypes]; + [[store expect] appendType:@"TYPE1"]; + [[store expect] appendType:@"TYPE2"]; + [[store expect] endCurrentObject]; // types + [[store expect] appendConstantName:@"NAME"]; + [[store expect] endCurrentObject]; // constant + [[parser expect] popState]; + ObjectiveCParseData *data = [ObjectiveCParseData dataWithStream:tokens parser:parser store:store]; + // execute + [state parseWithData:data]; + // verify + ^{ [store verify]; } should_not raise_exception(); + ^{ [parser verify]; } should_not raise_exception(); + }); + }); + }); + + it(@"should detect types with double and name with uppercase letters", ^{ + runWithState(^(ObjectiveCConstantState *state) { + runWithString(@"__type1 __type2 NAME;", ^(id parser, id tokens) { + // setup + id store = [OCMockObject mockForClass:[Store class]]; + [[store expect] setCurrentSourceInfo:OCMOCK_ANY]; + [[store expect] beginConstant]; + [[store expect] beginConstantTypes]; + [[store expect] appendType:@"__type1"]; + [[store expect] appendType:@"__type2"]; + [[store expect] endCurrentObject]; // types + [[store expect] appendConstantName:@"NAME"]; + [[store expect] endCurrentObject]; // constant + [[parser expect] popState]; + ObjectiveCParseData *data = [ObjectiveCParseData dataWithStream:tokens parser:parser store:store]; + // execute + [state parseWithData:data]; + // verify + ^{ [store verify]; } should_not raise_exception(); + ^{ [parser verify]; } should_not raise_exception(); + }); + }); + }); + + it(@"should detect types with uppercase letters and name with double underscore prefix", ^{ + runWithState(^(ObjectiveCConstantState *state) { + runWithString(@"TYPE1 TYPE2 __name;", ^(id parser, id tokens) { + // setup + id store = [OCMockObject mockForClass:[Store class]]; + [[store expect] setCurrentSourceInfo:OCMOCK_ANY]; + [[store expect] beginConstant]; + [[store expect] beginConstantTypes]; + [[store expect] appendType:@"TYPE1"]; + [[store expect] appendType:@"TYPE2"]; + [[store expect] endCurrentObject]; // types + [[store expect] appendConstantName:@"__name"]; + [[store expect] endCurrentObject]; // constant + [[parser expect] popState]; + ObjectiveCParseData *data = [ObjectiveCParseData dataWithStream:tokens parser:parser store:store]; + // execute + [state parseWithData:data]; + // verify + ^{ [store verify]; } should_not raise_exception(); + ^{ [parser verify]; } should_not raise_exception(); + }); + }); + }); + }); + }); }); describe(@"fail cases", ^{ diff --git a/appledoc/Parsing/ObjectiveC/ObjectiveCConstantState.m b/appledoc/Parsing/ObjectiveC/ObjectiveCConstantState.m index fbab625c..3fe3af40 100644 --- a/appledoc/Parsing/ObjectiveC/ObjectiveCConstantState.m +++ b/appledoc/Parsing/ObjectiveC/ObjectiveCConstantState.m @@ -49,12 +49,10 @@ - (BOOL)parseConstantDefinition:(ObjectiveCParseData *)data { [data.store endCurrentObject]; // types [data.store appendConstantName:token.stringValue]; if (hasDescriptors) [data.store beginConstantDescriptors]; - return; } else if (lookahead < indexOfEndToken) { [data.store appendDescriptor:token.stringValue]; - } else if ([token matches:@";"]) { + } else { if (hasDescriptors) [data.store endCurrentObject]; // descriptors - return; } }]; if (found == NSNotFound) { @@ -76,11 +74,17 @@ - (BOOL)finalizeConstant:(ObjectiveCParseData *)data { #pragma mark - Helper methods - (NSUInteger)lookaheadIndexOfFirstPotentialDescriptorToken:(ObjectiveCParseData *)data { + // This one is a bit tricky: we want to leave at least 2 tokens before accepting descriptors: one for type and one for name. But if all tokens look like descriptor from start on, we should take them to be types - this would cover cases like (__unsafe_unretained etc). If all tokens look like descriptors, then just take the last as constant name and all prior as types. LogParDebug(@"Scanning tokens for constant descriptors."); - __block NSInteger remainingTypeAndNameTokens = 2; - NSUInteger result = [data lookaheadIndexOfFirstPotentialDescriptorWithEndDelimiters:@";" block:^(PKToken *token, BOOL *allowDescriptor) { - // require at least 2 tokens, one for type and one for name! - if (--remainingTypeAndNameTokens >= 0) *allowDescriptor = NO; + __block NSUInteger numberOfSuccessiveDescriptorLikeTokens = 0; + NSUInteger result = [data lookaheadIndexOfFirstPotentialDescriptorWithEndDelimiters:@";" block:^(PKToken *token, NSUInteger lookahead, BOOL *isDescriptor) { + BOOL looksLikeDescriptor = [data doesStringLookLikeDescriptor:token.stringValue]; + if (looksLikeDescriptor) + numberOfSuccessiveDescriptorLikeTokens++; + else + numberOfSuccessiveDescriptorLikeTokens = 0; + if (lookahead < 2) return; // require at least 2 tokens from the start + if (looksLikeDescriptor && numberOfSuccessiveDescriptorLikeTokens <= lookahead) *isDescriptor = YES; }]; return result; } diff --git a/appledoc/Parsing/ObjectiveC/ObjectiveCParseData.h b/appledoc/Parsing/ObjectiveC/ObjectiveCParseData.h index 3f8e5b6d..7087561e 100644 --- a/appledoc/Parsing/ObjectiveC/ObjectiveCParseData.h +++ b/appledoc/Parsing/ObjectiveC/ObjectiveCParseData.h @@ -11,6 +11,8 @@ @class TokensStream; @class ObjectiveCParser; +typedef void(^GBDescriptorsLookaheadBlock)(PKToken *token, NSUInteger lookahead, BOOL *isDescriptor); + /** Provides data to ObjectiveCParserState objects. The main reason for introducing this object is to reduce the number of parameters otherwise required for states. Instead of passing individual parameters around, we're now using parameter object. @@ -20,7 +22,7 @@ + (id)dataWithStream:(TokensStream *)stream parser:(ObjectiveCParser *)parser store:(Store *)store; - (NSUInteger)lookaheadIndexOfFirstEndDelimiter:(id)end; -- (NSUInteger)lookaheadIndexOfFirstPotentialDescriptorWithEndDelimiters:(id)end block:(void(^)(PKToken *token, BOOL *allowDescriptor))handler; +- (NSUInteger)lookaheadIndexOfFirstPotentialDescriptorWithEndDelimiters:(id)end block:(GBDescriptorsLookaheadBlock)handler; - (BOOL)doesStringLookLikeDescriptor:(NSString *)string; @property (nonatomic, readonly, strong) Store *store; diff --git a/appledoc/Parsing/ObjectiveC/ObjectiveCParseData.m b/appledoc/Parsing/ObjectiveC/ObjectiveCParseData.m index 3feba47c..9520fcbd 100644 --- a/appledoc/Parsing/ObjectiveC/ObjectiveCParseData.m +++ b/appledoc/Parsing/ObjectiveC/ObjectiveCParseData.m @@ -55,7 +55,7 @@ - (NSUInteger)lookaheadIndexOfFirstEndDelimiter:(id)end { return result; } -- (NSUInteger)lookaheadIndexOfFirstPotentialDescriptorWithEndDelimiters:(id)end block:(void(^)(PKToken *token, BOOL *allowDescriptor))handler { +- (NSUInteger)lookaheadIndexOfFirstPotentialDescriptorWithEndDelimiters:(id)end block:(GBDescriptorsLookaheadBlock)handler { __block NSUInteger result = NSNotFound; [self.stream lookAheadWithBlock:^(PKToken *token, NSUInteger lookahead, BOOL *stop) { if ([token matches:end]) { @@ -63,13 +63,9 @@ - (NSUInteger)lookaheadIndexOfFirstPotentialDescriptorWithEndDelimiters:(id)end return; } - // Give client a chance to skip tokens prior to start testing for descriptors. - BOOL canTokenBeTestedForDescriptor = YES; - handler(token, &canTokenBeTestedForDescriptor); - if (!canTokenBeTestedForDescriptor) return; - - // If we encounter possible descriptor, use this index. - if ([self doesStringLookLikeDescriptor:token.stringValue]) { + BOOL isDescriptor = NO; + handler(token, lookahead, &isDescriptor); + if (isDescriptor) { result = lookahead; *stop = YES; return; diff --git a/appledoc/Parsing/ObjectiveC/ObjectiveCPropertyState.m b/appledoc/Parsing/ObjectiveC/ObjectiveCPropertyState.m index a5a66a77..62d4b03c 100644 --- a/appledoc/Parsing/ObjectiveC/ObjectiveCPropertyState.m +++ b/appledoc/Parsing/ObjectiveC/ObjectiveCPropertyState.m @@ -106,16 +106,17 @@ - (BOOL)finalizeProperty:(ObjectiveCParseData *)data { } - (NSUInteger)lookaheadIndexOfFirstPotentialDescriptor:(ObjectiveCParseData *)data { + // Require at least one token for type and one for name. Note that we should take all asterisks as types while here! LogParDebug(@"Scanning tokens for property descriptors."); - __block NSInteger remainingTypeAndNameTokens = 2; - NSUInteger result = [data lookaheadIndexOfFirstPotentialDescriptorWithEndDelimiters:@";" block:^(PKToken *token, BOOL *allowDescriptor) { - // Require at least one token for type and one for name. Note that we should take all asterisks as types while here! - if (remainingTypeAndNameTokens >= 0) { - *allowDescriptor = NO; - if ([token matches:@"*"]) return; - if (--remainingTypeAndNameTokens < 0) *allowDescriptor = YES; + __block BOOL wasPreviousTokenPossiblePropertyName = YES; + NSUInteger result = [data lookaheadIndexOfFirstPotentialDescriptorWithEndDelimiters:@";" block:^(PKToken *token, NSUInteger lookahead, BOOL *isDescriptor) { + if ([token matches:@"*"]) { + wasPreviousTokenPossiblePropertyName = NO; return; } + if (lookahead < 2) return; + if (wasPreviousTokenPossiblePropertyName && [data doesStringLookLikeDescriptor:token.stringValue]) *isDescriptor = YES; + wasPreviousTokenPossiblePropertyName = YES; }]; return result; }