From c1111a0b0a82911de509487c11d8d2ae530ac70a Mon Sep 17 00:00:00 2001 From: Jaapio Date: Sun, 4 Jan 2026 15:34:19 +0100 Subject: [PATCH] Add tests for factories --- phpunit.xml.dist | 59 ++++------ src/DocBlock/StandardTagFactory.php | 6 +- .../Tags/Factory/AbstractPHPStanFactory.php | 21 ++-- ...ctory.php => TemplateCovariantFactory.php} | 15 +-- .../Factory/TemplateImplementsFactory.php | 52 -------- .../Factory/AbstractPHPStanFactoryTest.php | 111 ++++++++++++++++++ .../Tags/Factory/ExtendsFactoryTest.php | 45 +++++++ .../Tags/Factory/ImplementsFactoryTest.php | 45 +++++++ .../Tags/Factory/MixinFactoryTest.php | 43 +++++++ .../Tags/Factory/TagFactoryTestCase.php | 2 +- .../Factory/TemplateCovariantFactoryTest.php | 74 ++++++++++++ .../Tags/Factory/TemplateFactoryTest.php | 80 +++++++++++++ .../Tags/Factory/ThrowsFactoryTest.php | 43 +++++++ 13 files changed, 488 insertions(+), 108 deletions(-) rename src/DocBlock/Tags/Factory/{TemplateExtendsFactory.php => TemplateCovariantFactory.php} (69%) delete mode 100644 src/DocBlock/Tags/Factory/TemplateImplementsFactory.php create mode 100644 tests/unit/DocBlock/Tags/Factory/AbstractPHPStanFactoryTest.php create mode 100644 tests/unit/DocBlock/Tags/Factory/ExtendsFactoryTest.php create mode 100644 tests/unit/DocBlock/Tags/Factory/ImplementsFactoryTest.php create mode 100644 tests/unit/DocBlock/Tags/Factory/MixinFactoryTest.php create mode 100644 tests/unit/DocBlock/Tags/Factory/TemplateCovariantFactoryTest.php create mode 100644 tests/unit/DocBlock/Tags/Factory/TemplateFactoryTest.php create mode 100644 tests/unit/DocBlock/Tags/Factory/ThrowsFactoryTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index baf0ad6..4dde640 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,39 +1,24 @@ - - - - - ./tests/unit - - - ./tests/integration - - - - - ./src/ - - - - - - - - - + + + + ./src/ + + + + + + + + + ./tests/unit + + + ./tests/integration + + + + + + diff --git a/src/DocBlock/StandardTagFactory.php b/src/DocBlock/StandardTagFactory.php index d282b44..e911f7e 100644 --- a/src/DocBlock/StandardTagFactory.php +++ b/src/DocBlock/StandardTagFactory.php @@ -28,9 +28,8 @@ use phpDocumentor\Reflection\DocBlock\Tags\Factory\PropertyFactory; use phpDocumentor\Reflection\DocBlock\Tags\Factory\PropertyReadFactory; use phpDocumentor\Reflection\DocBlock\Tags\Factory\PropertyWriteFactory; use phpDocumentor\Reflection\DocBlock\Tags\Factory\ReturnFactory; -use phpDocumentor\Reflection\DocBlock\Tags\Factory\TemplateExtendsFactory; +use phpDocumentor\Reflection\DocBlock\Tags\Factory\TemplateCovariantFactory; use phpDocumentor\Reflection\DocBlock\Tags\Factory\TemplateFactory; -use phpDocumentor\Reflection\DocBlock\Tags\Factory\TemplateImplementsFactory; use phpDocumentor\Reflection\DocBlock\Tags\Factory\ThrowsFactory; use phpDocumentor\Reflection\DocBlock\Tags\Factory\VarFactory; use phpDocumentor\Reflection\DocBlock\Tags\Generic; @@ -156,8 +155,7 @@ final class StandardTagFactory implements TagFactory new ImplementsFactory($typeResolver, $descriptionFactory), new ExtendsFactory($typeResolver, $descriptionFactory), new TemplateFactory($typeResolver, $descriptionFactory), - new TemplateImplementsFactory($typeResolver, $descriptionFactory), - new TemplateExtendsFactory($typeResolver, $descriptionFactory), + new TemplateCovariantFactory($typeResolver, $descriptionFactory), new ThrowsFactory($typeResolver, $descriptionFactory), ); diff --git a/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php b/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php index 71cd61d..35a981e 100644 --- a/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php +++ b/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php @@ -18,6 +18,7 @@ use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag; use phpDocumentor\Reflection\Types\Context as TypeContext; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\ConstExprParser; +use PHPStan\PhpDocParser\Parser\ParserException; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; @@ -59,13 +60,17 @@ class AbstractPHPStanFactory implements Factory public function create(string $tagLine, ?TypeContext $context = null): Tag { - $tokens = $this->tokenizeLine($tagLine . "\n"); - $ast = $this->parser->parseTag($tokens); - if (property_exists($ast->value, 'description') === true) { - $ast->value->setAttribute( - 'description', - rtrim($ast->value->description . $tokens->joinUntil(Lexer::TOKEN_END), "\n") - ); + try { + $tokens = $this->tokenizeLine($tagLine . "\n"); + $ast = $this->parser->parseTag($tokens); + if (property_exists($ast->value, 'description') === true) { + $ast->value->setAttribute( + 'description', + rtrim($ast->value->description . $tokens->joinUntil(Lexer::TOKEN_END), "\n") + ); + } + } catch (ParserException $e) { + return InvalidTag::create($tagLine, '')->withError($e); } if ($context === null) { @@ -80,6 +85,8 @@ class AbstractPHPStanFactory implements Factory } } catch (RuntimeException $e) { return InvalidTag::create((string) $ast->value, 'method')->withError($e); + } catch (ParserException $e) { + return InvalidTag::create((string) $ast->value, $ast->name)->withError($e); } return InvalidTag::create( diff --git a/src/DocBlock/Tags/Factory/TemplateExtendsFactory.php b/src/DocBlock/Tags/Factory/TemplateCovariantFactory.php similarity index 69% rename from src/DocBlock/Tags/Factory/TemplateExtendsFactory.php rename to src/DocBlock/Tags/Factory/TemplateCovariantFactory.php index e23444a..329a997 100644 --- a/src/DocBlock/Tags/Factory/TemplateExtendsFactory.php +++ b/src/DocBlock/Tags/Factory/TemplateCovariantFactory.php @@ -6,11 +6,12 @@ namespace phpDocumentor\Reflection\DocBlock\Tags\Factory; use phpDocumentor\Reflection\DocBlock\DescriptionFactory; use phpDocumentor\Reflection\DocBlock\Tag; -use phpDocumentor\Reflection\DocBlock\Tags\TemplateExtends; +use phpDocumentor\Reflection\DocBlock\Tags\TemplateCovariant; use phpDocumentor\Reflection\TypeResolver; use phpDocumentor\Reflection\Types\Context; -use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use Webmozart\Assert\Assert; use function is_string; @@ -18,7 +19,7 @@ use function is_string; /** * @internal This class is not part of the BC promise of this library. */ -final class TemplateExtendsFactory implements PHPStanFactory +final class TemplateCovariantFactory implements PHPStanFactory { private DescriptionFactory $descriptionFactory; private TypeResolver $typeResolver; @@ -31,21 +32,21 @@ final class TemplateExtendsFactory implements PHPStanFactory public function supports(PhpDocTagNode $node, Context $context): bool { - return $node->value instanceof ExtendsTagValueNode && $node->name === '@template-extends'; + return $node->value instanceof TemplateTagValueNode && $node->name === '@template-covariant'; } public function create(PhpDocTagNode $node, Context $context): Tag { $tagValue = $node->value; - Assert::isInstanceOf($tagValue, ExtendsTagValueNode::class); + Assert::isInstanceOf($tagValue, TemplateTagValueNode::class); $description = $tagValue->getAttribute('description'); if (is_string($description) === false) { $description = $tagValue->description; } - return new TemplateExtends( - $this->typeResolver->createType($tagValue->type, $context), + return new TemplateCovariant( + $this->typeResolver->createType(new IdentifierTypeNode($tagValue->name), $context), $this->descriptionFactory->create($description, $context) ); } diff --git a/src/DocBlock/Tags/Factory/TemplateImplementsFactory.php b/src/DocBlock/Tags/Factory/TemplateImplementsFactory.php deleted file mode 100644 index bb3d11d..0000000 --- a/src/DocBlock/Tags/Factory/TemplateImplementsFactory.php +++ /dev/null @@ -1,52 +0,0 @@ -descriptionFactory = $descriptionFactory; - $this->typeResolver = $typeResolver; - } - - public function supports(PhpDocTagNode $node, Context $context): bool - { - return $node->value instanceof ImplementsTagValueNode && $node->name === '@template-implements'; - } - - public function create(PhpDocTagNode $node, Context $context): Tag - { - $tagValue = $node->value; - Assert::isInstanceOf($tagValue, ImplementsTagValueNode::class); - - $description = $tagValue->getAttribute('description'); - if (is_string($description) === false) { - $description = $tagValue->description; - } - - return new TemplateImplements( - $this->typeResolver->createType($tagValue->type, $context), - $this->descriptionFactory->create($description, $context) - ); - } -} diff --git a/tests/unit/DocBlock/Tags/Factory/AbstractPHPStanFactoryTest.php b/tests/unit/DocBlock/Tags/Factory/AbstractPHPStanFactoryTest.php new file mode 100644 index 0000000..27eb7e9 --- /dev/null +++ b/tests/unit/DocBlock/Tags/Factory/AbstractPHPStanFactoryTest.php @@ -0,0 +1,111 @@ + + */ +class AbstractPHPStanFactoryTest extends TestCase +{ + /** + * Call Mockery::close after each test. + */ + public function tearDown(): void + { + m::close(); + } + + /** + * @covers ::create + */ + public function testCreateReturnsTagFromSupportingFactory(): void + { + $tag = m::mock(Tag::class); + $factory = m::mock(PHPStanFactory::class); + $factory->shouldReceive('supports')->andReturn(true); + $factory->shouldReceive('create')->andReturn($tag); + + $sut = new AbstractPHPStanFactory($factory); + + $result = $sut->create('@param string $param'); + + self::assertSame($tag, $result); + } + + /** + * @covers ::create + */ + public function testCreateReturnsInvalidTagWhenNoFactorySupports(): void + { + $factory = m::mock(PHPStanFactory::class); + $factory->shouldReceive('supports')->andReturn(false); + + $sut = new AbstractPHPStanFactory($factory); + + $result = $sut->create('@unknown string $param'); + + self::assertInstanceOf(InvalidTag::class, $result); + self::assertEquals('@unknown', $result->getName()); + } + + /** + * @covers ::create + */ + public function testCreateReturnsInvalidTagWithErrorOnFactoryRuntimeException(): void + { + $factory = m::mock(PHPStanFactory::class); + $factory->shouldReceive('supports')->andReturn(true); + $factory->shouldReceive('create')->andThrow(new RuntimeException('Factory error')); + + $sut = new AbstractPHPStanFactory($factory); + + $result = $sut->create('@param string $param'); + + self::assertInstanceOf(InvalidTag::class, $result); + self::assertInstanceOf(Exception::class, $result->getException()); + self::assertEquals('Factory error', $result->getException()->getMessage()); + } + + /** + * @covers ::create + */ + public function testCreateReturnsInvalidTagWithErrorOnFactoryParserException(): void + { + $exception = m::mock(ParserException::class); + $exception->shouldReceive('getMessage')->andReturn('Parser error'); + + $factory = m::mock(PHPStanFactory::class); + $factory->shouldReceive('supports')->andReturn(true); + $factory->shouldReceive('create')->andThrow($exception); + + $sut = new AbstractPHPStanFactory($factory); + + $result = $sut->create('@param string $param'); + + self::assertInstanceOf(InvalidTag::class, $result); + self::assertSame($exception, $result->getException()); + } +} diff --git a/tests/unit/DocBlock/Tags/Factory/ExtendsFactoryTest.php b/tests/unit/DocBlock/Tags/Factory/ExtendsFactoryTest.php new file mode 100644 index 0000000..60e9adb --- /dev/null +++ b/tests/unit/DocBlock/Tags/Factory/ExtendsFactoryTest.php @@ -0,0 +1,45 @@ +parseTag('@extends SomeClass'); + $factory = new ExtendsFactory($this->giveTypeResolver(), $this->givenDescriptionFactory()); + $context = new Context('global'); + + self::assertTrue($factory->supports($ast, $context)); + self::assertEquals( + new Extends_( + new Generic(new Fqsen('\\SomeClass'), [new Object_(new Fqsen('\\OtherType'))]), + new Description('') + ), + $factory->create($ast, $context) + ); + } +} diff --git a/tests/unit/DocBlock/Tags/Factory/ImplementsFactoryTest.php b/tests/unit/DocBlock/Tags/Factory/ImplementsFactoryTest.php new file mode 100644 index 0000000..e175bbc --- /dev/null +++ b/tests/unit/DocBlock/Tags/Factory/ImplementsFactoryTest.php @@ -0,0 +1,45 @@ +parseTag('@implements SomeClass'); + $factory = new ImplementsFactory($this->giveTypeResolver(), $this->givenDescriptionFactory()); + $context = new Context('global'); + + self::assertTrue($factory->supports($ast, $context)); + self::assertEquals( + new Implements_( + new Generic(new Fqsen('\\SomeClass'), [new Object_(new Fqsen('\\OtherType'))]), + new Description('') + ), + $factory->create($ast, $context) + ); + } +} diff --git a/tests/unit/DocBlock/Tags/Factory/MixinFactoryTest.php b/tests/unit/DocBlock/Tags/Factory/MixinFactoryTest.php new file mode 100644 index 0000000..3651085 --- /dev/null +++ b/tests/unit/DocBlock/Tags/Factory/MixinFactoryTest.php @@ -0,0 +1,43 @@ +parseTag('@mixin string'); + $factory = new MixinFactory($this->giveTypeResolver(), $this->givenDescriptionFactory()); + $context = new Context('global'); + + self::assertTrue($factory->supports($ast, $context)); + self::assertEquals( + new Mixin( + new String_(), + new Description('') + ), + $factory->create($ast, $context) + ); + } +} diff --git a/tests/unit/DocBlock/Tags/Factory/TagFactoryTestCase.php b/tests/unit/DocBlock/Tags/Factory/TagFactoryTestCase.php index 721a484..d8b9900 100644 --- a/tests/unit/DocBlock/Tags/Factory/TagFactoryTestCase.php +++ b/tests/unit/DocBlock/Tags/Factory/TagFactoryTestCase.php @@ -33,7 +33,7 @@ abstract class TagFactoryTestCase extends TestCase { public function parseTag(string $tag): PhpDocTagNode { - $config = new ParserConfig([]); + $config = new ParserConfig(['indexes' => true, 'lines' => true]); $lexer = new Lexer($config); $constParser = new ConstExprParser($config); $phpDocParser = new PhpDocParser($config, new TypeParser($config, $constParser), $constParser); diff --git a/tests/unit/DocBlock/Tags/Factory/TemplateCovariantFactoryTest.php b/tests/unit/DocBlock/Tags/Factory/TemplateCovariantFactoryTest.php new file mode 100644 index 0000000..dcc70a5 --- /dev/null +++ b/tests/unit/DocBlock/Tags/Factory/TemplateCovariantFactoryTest.php @@ -0,0 +1,74 @@ +parseTag($input); + $factory = new TemplateCovariantFactory($this->giveTypeResolver(), $this->givenDescriptionFactory()); + $context = new Context('global'); + + self::assertTrue($factory->supports($ast, $context)); + self::assertEquals( + $expected, + $factory->create($ast, $context) + ); + } + + /** + * @return array> + */ + public function templateCovariantInputProvider(): array + { + return [ + [ + '@template-covariant string', + new TemplateCovariant( + new String_(), + new Description('') + ), + ], + [ + '@template-covariant SomeClass Description', + new TemplateCovariant( + new Object_(new Fqsen('\SomeClass')), + new Description('Description') + ), + ], + [ + '@template-covariant SomeClass', + new TemplateCovariant( + new Object_(new Fqsen('\SomeClass')), + new Description('') + ), + ], + ]; + } +} diff --git a/tests/unit/DocBlock/Tags/Factory/TemplateFactoryTest.php b/tests/unit/DocBlock/Tags/Factory/TemplateFactoryTest.php new file mode 100644 index 0000000..964d1f9 --- /dev/null +++ b/tests/unit/DocBlock/Tags/Factory/TemplateFactoryTest.php @@ -0,0 +1,80 @@ +parseTag($input); + $factory = new TemplateFactory($this->giveTypeResolver(), $this->givenDescriptionFactory()); + $context = new Context('global'); + + self::assertTrue($factory->supports($ast, $context)); + self::assertEquals( + $expected, + $factory->create($ast, $context) + ); + } + + /** + * @return array> + */ + public function templateInputProvider(): array + { + return [ + [ + '@template T', + new Template( + 'T', + new Mixed_(), + new Mixed_(), + new Description('') + ), + ], + [ + '@template T of SomeClass Description', + new Template( + 'T', + new Object_(new Fqsen('\SomeClass')), + new Mixed_(), + new Description('Description') + ), + ], + [ + '@template T of SomeClass = Default', + new Template( + 'T', + new Object_(new Fqsen('\SomeClass')), + new Object_(new Fqsen('\Default')), + new Description('') + ), + ], + ]; + } +} diff --git a/tests/unit/DocBlock/Tags/Factory/ThrowsFactoryTest.php b/tests/unit/DocBlock/Tags/Factory/ThrowsFactoryTest.php new file mode 100644 index 0000000..6171fbb --- /dev/null +++ b/tests/unit/DocBlock/Tags/Factory/ThrowsFactoryTest.php @@ -0,0 +1,43 @@ +parseTag('@throws string'); + $factory = new ThrowsFactory($this->giveTypeResolver(), $this->givenDescriptionFactory()); + $context = new Context('global'); + + self::assertTrue($factory->supports($ast, $context)); + self::assertEquals( + new Throws( + new String_(), + new Description('') + ), + $factory->create($ast, $context) + ); + } +}