From ebe0a19b90a43da29d09292c7b7f6a643afd76e0 Mon Sep 17 00:00:00 2001 From: Jaapio Date: Sat, 19 Nov 2022 20:51:27 +0100 Subject: [PATCH] Fix regression in main branch The type resolver did support some odd format being a nullable type combined with compound or intersection. The new implementation didn't as phpstan doesn't support this. By now this behavoir is restored with a deprecation notice that we will remove it in the future. --- phpunit.xml.dist | 2 +- src/PseudoTypes/ConstExpression.php | 7 +-- src/TypeResolver.php | 64 ++++++++++++++++++--- tests/unit/TypeResolverTest.php | 86 +++++++++++++++++++---------- 4 files changed, 117 insertions(+), 42 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 09d5306..69debb7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,7 +4,7 @@ bootstrap="vendor/autoload.php" colors="true" verbose="true" - convertDeprecationsToExceptions="true" + convertDeprecationsToExceptions="false" forceCoversAnnotation="true" > diff --git a/src/PseudoTypes/ConstExpression.php b/src/PseudoTypes/ConstExpression.php index c2c42bc..930dfdc 100644 --- a/src/PseudoTypes/ConstExpression.php +++ b/src/PseudoTypes/ConstExpression.php @@ -13,7 +13,6 @@ namespace phpDocumentor\Reflection\PseudoTypes; -use phpDocumentor\Reflection\Fqsen; use phpDocumentor\Reflection\PseudoType; use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\Types\Mixed_; @@ -23,16 +22,16 @@ /** @psalm-immutable */ final class ConstExpression implements PseudoType { - private Fqsen $owner; + private Type $owner; private string $expression; - public function __construct(Fqsen $owner, string $expression) + public function __construct(Type $owner, string $expression) { $this->owner = $owner; $this->expression = $expression; } - public function getOwner(): Fqsen + public function getOwner(): Type { return $this->owner; } diff --git a/src/TypeResolver.php b/src/TypeResolver.php index 6532655..d15822d 100644 --- a/src/TypeResolver.php +++ b/src/TypeResolver.php @@ -102,8 +102,11 @@ use function sprintf; use function strpos; use function strtolower; +use function trigger_error; use function trim; +use const E_USER_DEPRECATED; + final class TypeResolver { /** @var string Definition of the NAMESPACE operator in PHP */ @@ -200,15 +203,13 @@ public function resolve(string $type, ?Context $context = null): Type $context = new Context(''); } - try { - $tokens = $this->lexer->tokenize($type); - $tokenIterator = new TokenIterator($tokens); - $ast = $this->typeParser->parse($tokenIterator); - } catch (ParserException $e) { - throw new RuntimeException($e->getMessage(), 0, $e); - } + $tokens = $this->lexer->tokenize($type); + $tokenIterator = new TokenIterator($tokens); + + $ast = $this->parse($tokenIterator); + $type = $this->createType($ast, $context); - return $this->createType($ast, $context); + return $this->tryParseRemainingCompoundTypes($tokenIterator, $context, $type); } public function createType(?TypeNode $type, Context $context): Type @@ -403,7 +404,7 @@ private function createFromConst(ConstTypeNode $type, Context $context): Type case $type->constExpr instanceof ConstFetchNode: return new ConstExpression( - $this->fqsenResolver->resolve($type->constExpr->className, $context), + $this->resolve($type->constExpr->className, $context), $type->constExpr->name ); @@ -551,4 +552,49 @@ private function createArray(array $typeNodes, Context $context): Array_ throw new RuntimeException('An array can have only integers or strings as keys'); } + + private function parse(TokenIterator $tokenIterator): TypeNode + { + try { + $ast = $this->typeParser->parse($tokenIterator); + } catch (ParserException $e) { + throw new RuntimeException($e->getMessage(), 0, $e); + } + + return $ast; + } + + /** + * Will try to parse unsupported type notations by phpstan + * + * The phpstan parser doesn't support the illegal nullable combinations like this library does. + * This method will warn the user about those notations but for bc purposes we will still have it here. + */ + private function tryParseRemainingCompoundTypes(TokenIterator $tokenIterator, Context $context, Type $type): Type + { + trigger_error( + 'Legacy nullable type detected, please update your code as + you are using nullable types in a docblock. support will be removed in v2.0.0', + E_USER_DEPRECATED + ); + $continue = true; + while ($continue) { + $continue = false; + while ($tokenIterator->tryConsumeTokenType(Lexer::TOKEN_UNION)) { + $ast = $this->parse($tokenIterator); + $type2 = $this->createType($ast, $context); + $type = new Compound([$type, $type2]); + $continue = true; + } + + while ($tokenIterator->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) { + $ast = $this->typeParser->parse($tokenIterator); + $type2 = $this->createType($ast, $context); + $type = new Intersection([$type, $type2]); + $continue = true; + } + } + + return $type; + } } diff --git a/tests/unit/TypeResolverTest.php b/tests/unit/TypeResolverTest.php index 52e3666..b669f46 100644 --- a/tests/unit/TypeResolverTest.php +++ b/tests/unit/TypeResolverTest.php @@ -424,32 +424,6 @@ public function testResolvingCompoundTypedArrayTypes(): void $this->assertInstanceOf(Object_::class, $secondType->getValueType()); } - /** - * @uses \phpDocumentor\Reflection\Types\Context - * @uses \phpDocumentor\Reflection\Types\Compound - * @uses \phpDocumentor\Reflection\Types\String_ - * @uses \phpDocumentor\Reflection\Types\Nullable - * @uses \phpDocumentor\Reflection\Types\Null_ - * @uses \phpDocumentor\Reflection\Types\Boolean - * @uses \phpDocumentor\Reflection\Fqsen - * @uses \phpDocumentor\Reflection\FqsenResolver - * - * @covers ::__construct - * @covers ::resolve - * @covers :: - */ - public function testResolvingNullableCompoundTypes(): void - { - $this->markTestSkipped('Invalid type definition'); - $fixture = new TypeResolver(); - - // Note that in PHP types it is illegal to use shorthand nullable - // syntax with unions. This would be 'string|boolean|null' instead. - $resolvedType = $fixture->resolve('?string|null|?boolean'); - - $this->assertSame('?string|null|?bool', (string) $resolvedType); - } - /** * @uses \phpDocumentor\Reflection\Types\Context * @uses \phpDocumentor\Reflection\Types\Compound @@ -876,6 +850,7 @@ public function testArrayKeyValueSpecification(): void * @dataProvider genericsProvider * @dataProvider callableProvider * @dataProvider constExpressions + * @dataProvider illegalLegacyFormatProvider * @testdox create type from $type */ public function testTypeBuilding(string $type, Type $expected): void @@ -1093,11 +1068,66 @@ public function constExpressions(): array ], [ 'Foo::FOO_CONSTANT', - new ConstExpression(new Fqsen('\\phpDocumentor\\Foo'), 'FOO_CONSTANT'), + new ConstExpression(new Object_(new Fqsen('\\phpDocumentor\\Foo')), 'FOO_CONSTANT'), ], [ 'Foo::FOO_*', - new ConstExpression(new Fqsen('\\phpDocumentor\\Foo'), 'FOO_*'), + new ConstExpression(new Object_(new Fqsen('\\phpDocumentor\\Foo')), 'FOO_*'), + ], + [ + 'self::*|null', + new Compound([new ConstExpression(new Self_(), '*'), new Null_()]), + ], + ]; + } + + /** + * @return array + */ + public function illegalLegacyFormatProvider(): array + { + return [ + [ + '?string|bool', + new Compound([new Nullable(new String_()), new Boolean()]), + ], + [ + '?string|?bool', + new Compound([new Nullable(new String_()), new Nullable(new Boolean())]), + ], + [ + '?string|?bool|null', + new Compound([new Nullable(new String_()), new Nullable(new Boolean()), new Null_()]), + ], + [ + '?string|bool|Foo', + new Compound([ + new Nullable(new String_()), + new Boolean(), + new Object_(new Fqsen('\\phpDocumentor\\Foo')), + ]), + ], + [ + '?string&bool', + new Intersection([new Nullable(new String_()), new Boolean()]), + ], + [ + '?string&bool|Foo', + new Intersection( + [ + new Nullable(new String_()), + new Compound([new Boolean(), new Object_(new Fqsen('\\phpDocumentor\\Foo'))]), + ] + ), + ], + [ + '?string&?bool|null', + new Compound( + [ + new Intersection([new Nullable(new String_()), new Nullable(new Boolean())]), + new Null_(), + ] + ), ], ]; }