diff --git a/composer.json b/composer.json index a0ae92f0..d8d96754 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ "phpstan/phpstan-mockery": "^1.1", "phpstan/extension-installer": "^1.1", "phpstan/phpstan-webmozart-assert": "^1.2", - "psalm/phar": "^5.26" + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 8ee5b3e7..d70bafc4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "69aa0af5bad8d21df103f96a6e42e6dc", + "content-hash": "2af32665428eb750a10f182d03577049", "packages": [ { "name": "doctrine/deprecations", @@ -2391,6 +2391,75 @@ ], "time": "2020-09-28T06:39:44+00:00" }, + { + "name": "shipmonk/dead-code-detector", + "version": "0.5.1", + "source": { + "type": "git", + "url": "https://github.com/shipmonk-rnd/dead-code-detector.git", + "reference": "bcfa0a0ca5c2610422a141d6430727738dbcda36" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shipmonk-rnd/dead-code-detector/zipball/bcfa0a0ca5c2610422a141d6430727738dbcda36", + "reference": "bcfa0a0ca5c2610422a141d6430727738dbcda36", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^1.11.7" + }, + "require-dev": { + "doctrine/orm": "^2.19 || ^3.0", + "editorconfig-checker/editorconfig-checker": "^10.3.0", + "ergebnis/composer-normalize": "^2.28", + "nette/application": "^3.1", + "nette/component-model": "^3.0", + "nette/utils": "^3.0 || ^4.0", + "nikic/php-parser": "^4.19", + "phpstan/phpstan-phpunit": "^1.1.1", + "phpstan/phpstan-strict-rules": "^1.2.3", + "phpstan/phpstan-symfony": "^1.4", + "phpunit/phpunit": "^9.5.20", + "shipmonk/composer-dependency-analyser": "^1.6", + "shipmonk/name-collision-detector": "^2.0.0", + "shipmonk/phpstan-rules": "^3.1", + "slevomat/coding-standard": "^8.15.0", + "symfony/contracts": "^2.5 || ^3.0", + "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/routing": "^5.4 || ^6.0 || ^7.0" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "ShipMonk\\PHPStan\\DeadCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Dead code detector to find unused PHP code via PHPStan extension.", + "keywords": [ + "PHPStan", + "dead code", + "static analysis", + "unused code" + ], + "support": { + "issues": "https://github.com/shipmonk-rnd/dead-code-detector/issues", + "source": "https://github.com/shipmonk-rnd/dead-code-detector/tree/0.5.1" + }, + "time": "2024-11-05T09:28:57+00:00" + }, { "name": "theseer/tokenizer", "version": "1.3.1", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..e4d26e22 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,21 @@ +parameters: + ignoreErrors: + - + message: "#^Unused phpDocumentor\\\\Reflection\\\\DocBlock\\\\ExampleFinder\\:\\:getExampleDirectories$#" + count: 1 + path: src/DocBlock/ExampleFinder.php + + - + message: "#^Unused phpDocumentor\\\\Reflection\\\\DocBlock\\\\ExampleFinder\\:\\:setExampleDirectories$#" + count: 1 + path: src/DocBlock/ExampleFinder.php + + - + message: "#^Unused phpDocumentor\\\\Reflection\\\\DocBlock\\\\ExampleFinder\\:\\:setSourceDirectory$#" + count: 1 + path: src/DocBlock/ExampleFinder.php + + - + message: "#^Unused phpDocumentor\\\\Reflection\\\\DocBlock\\\\Tags\\\\Factory\\\\MethodParameterFactory\\:\\:formatNull$#" + count: 1 + path: src/DocBlock/Tags/Factory/MethodParameterFactory.php diff --git a/phpstan.neon b/phpstan.neon index c2365775..cde1bc0b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,17 @@ +includes: + - phpstan-baseline.neon + parameters: level: max ignoreErrors: - '#Method phpDocumentor\\Reflection\\DocBlock\\StandardTagFactory::createTag\(\) should return phpDocumentor\\Reflection\\DocBlock\\Tag but returns mixed#' - '#Offset 2 on array\{string, 28, int\} on left side of \?\? always exists and is not nullable\.#' + - + path: src/DocBlockFactoryInterface.php + identifier: shipmonk.deadMethod + - + path: src/DocBlock/TagFactory.php + identifier: shipmonk.deadMethod paths: - src + - tests/unit diff --git a/phpunit.xml.dist b/phpunit.xml.dist index baf0ad63..4dde640b 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 75caea28..e911f7e8 100644 --- a/src/DocBlock/StandardTagFactory.php +++ b/src/DocBlock/StandardTagFactory.php @@ -22,14 +22,14 @@ use phpDocumentor\Reflection\DocBlock\Tags\Factory\Factory; use phpDocumentor\Reflection\DocBlock\Tags\Factory\ImplementsFactory; use phpDocumentor\Reflection\DocBlock\Tags\Factory\MethodFactory; +use phpDocumentor\Reflection\DocBlock\Tags\Factory\MixinFactory; use phpDocumentor\Reflection\DocBlock\Tags\Factory\ParamFactory; 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; @@ -151,11 +151,11 @@ public static function createInstance(FqsenResolver $fqsenResolver): self new PropertyReadFactory($typeResolver, $descriptionFactory), new PropertyWriteFactory($typeResolver, $descriptionFactory), new MethodFactory($typeResolver, $descriptionFactory), + new MixinFactory($typeResolver, $descriptionFactory), 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 71cd61d9..35a981e5 100644 --- a/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php +++ b/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php @@ -18,6 +18,7 @@ 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 @@ public function __construct(PHPStanFactory ...$factories) 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 @@ public function create(string $tagLine, ?TypeContext $context = null): Tag } } 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/MethodParameterFactory.php b/src/DocBlock/Tags/Factory/MethodParameterFactory.php index da968967..d98237cb 100644 --- a/src/DocBlock/Tags/Factory/MethodParameterFactory.php +++ b/src/DocBlock/Tags/Factory/MethodParameterFactory.php @@ -34,7 +34,7 @@ public function format($defaultValue): string { $method = 'format' . ucfirst(gettype($defaultValue)); if (method_exists($this, $method)) { - return ' = ' . $this->{$method}($defaultValue); + return $this->{$method}($defaultValue); } return ''; 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 e23444a6..329a9979 100644 --- a/src/DocBlock/Tags/Factory/TemplateExtendsFactory.php +++ b/src/DocBlock/Tags/Factory/TemplateCovariantFactory.php @@ -6,11 +6,12 @@ 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 @@ /** * @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 @@ public function __construct(TypeResolver $typeResolver, DescriptionFactory $desc 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 bb3d11da..00000000 --- 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/src/DocBlock/Tags/MethodParameter.php b/src/DocBlock/Tags/MethodParameter.php index ceb87024..68c7ca98 100644 --- a/src/DocBlock/Tags/MethodParameter.php +++ b/src/DocBlock/Tags/MethodParameter.php @@ -84,7 +84,7 @@ public function __toString(): string '$' . $this->getName() . ( $this->defaultValue !== self::NO_DEFAULT_VALUE ? - (new MethodParameterFactory())->format($this->defaultValue) : + ' = ' . (new MethodParameterFactory())->format($this->defaultValue) : '' ); } diff --git a/src/DocBlock/Tags/TagWithType.php b/src/DocBlock/Tags/TagWithType.php index 3611436a..36f43be9 100644 --- a/src/DocBlock/Tags/TagWithType.php +++ b/src/DocBlock/Tags/TagWithType.php @@ -13,17 +13,10 @@ namespace phpDocumentor\Reflection\DocBlock\Tags; -use InvalidArgumentException; use phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\Exception\CannotCreateTag; use phpDocumentor\Reflection\Type; -use function in_array; -use function sprintf; -use function strlen; -use function substr; -use function trim; - abstract class TagWithType extends BaseTag { /** @var ?Type */ @@ -42,43 +35,6 @@ final public static function create(string $body): Tag throw new CannotCreateTag('Typed tag cannot be created'); } - /** - * @return string[] - */ - protected static function extractTypeFromBody(string $body): array - { - $type = ''; - $nestingLevel = 0; - for ($i = 0, $iMax = strlen($body); $i < $iMax; $i++) { - $character = $body[$i]; - - if ($nestingLevel === 0 && trim($character) === '') { - break; - } - - $type .= $character; - if (in_array($character, ['<', '(', '[', '{'])) { - $nestingLevel++; - continue; - } - - if (in_array($character, ['>', ')', ']', '}'])) { - $nestingLevel--; - continue; - } - } - - if ($nestingLevel < 0 || $nestingLevel > 0) { - throw new InvalidArgumentException( - sprintf('Could not find type in %s, please check for malformed notations', $body) - ); - } - - $description = trim(substr($body, strlen($type))); - - return [$type, $description]; - } - public function __toString(): string { if ($this->description) { diff --git a/src/DocBlock/Tags/Template.php b/src/DocBlock/Tags/Template.php index cfd6b699..5009879d 100644 --- a/src/DocBlock/Tags/Template.php +++ b/src/DocBlock/Tags/Template.php @@ -13,9 +13,9 @@ namespace phpDocumentor\Reflection\DocBlock\Tags; -use Doctrine\Deprecations\Deprecation; use phpDocumentor\Reflection\DocBlock\Description; use phpDocumentor\Reflection\DocBlock\Tag; +use phpDocumentor\Reflection\Exception\CannotCreateTag; use phpDocumentor\Reflection\Type; /** @@ -51,14 +51,7 @@ public function __construct( */ public static function create(string $body): ?Tag { - Deprecation::trigger( - 'phpdocumentor/reflection-docblock', - 'https://github.com/phpDocumentor/ReflectionDocBlock/issues/361', - 'Create using static factory is deprecated, this method should not be called directly - by library consumers', - ); - - return null; + throw new CannotCreateTag('Template tag cannot be created'); } public function getTemplateName(): string diff --git a/tests/unit/Assets/CustomParam.php b/tests/unit/Assets/CustomParam.php index 57f6f7f7..80b3955a 100644 --- a/tests/unit/Assets/CustomParam.php +++ b/tests/unit/Assets/CustomParam.php @@ -10,7 +10,10 @@ final class CustomParam implements Tag { + /** @var string|null */ public $myParam; + + /** @var FqsenResolver|null */ public $fqsenResolver; public function getName() : string @@ -18,7 +21,7 @@ public function getName() : string return 'spy'; } - public static function create($body, FqsenResolver $fqsenResolver = null, ?string $myParam = null) + public static function create(string $body, FqsenResolver $fqsenResolver = null, ?string $myParam = null) { $tag = new self(); $tag->fqsenResolver = $fqsenResolver; diff --git a/tests/unit/Assets/CustomServiceClass.php b/tests/unit/Assets/CustomServiceClass.php index a5d0e785..c5542602 100644 --- a/tests/unit/Assets/CustomServiceClass.php +++ b/tests/unit/Assets/CustomServiceClass.php @@ -7,11 +7,10 @@ use phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\DocBlock\Tags\Formatter; use phpDocumentor\Reflection\DocBlock\Tags\Formatter\PassthroughFormatter; -use phpDocumentor\Reflection\FqsenResolver; -use phpDocumentor\Reflection\DocBlock\Tags\Factory\StaticMethod; final class CustomServiceClass implements Tag { + /** @var Formatter|null */ public $formatter; public function getName() : string @@ -19,7 +18,7 @@ public function getName() : string return 'spy'; } - public static function create($body, PassthroughFormatter $formatter = null) + public static function create(string $body, PassthroughFormatter $formatter = null) { $tag = new self(); $tag->formatter = $formatter; diff --git a/tests/unit/Assets/CustomServiceInterface.php b/tests/unit/Assets/CustomServiceInterface.php index ac8f612c..36bd753c 100644 --- a/tests/unit/Assets/CustomServiceInterface.php +++ b/tests/unit/Assets/CustomServiceInterface.php @@ -11,6 +11,7 @@ final class CustomServiceInterface implements Tag { + /** @var Formatter|null */ public $formatter; public function getName() : string @@ -18,7 +19,7 @@ public function getName() : string return 'spy'; } - public static function create($body, Formatter $formatter = null) + public static function create(string $body, Formatter $formatter = null) { $tag = new self(); $tag->formatter = $formatter; diff --git a/tests/unit/Assets/CustomTagFactory.php b/tests/unit/Assets/CustomTagFactory.php index df4a3f69..7a3b3a9e 100644 --- a/tests/unit/Assets/CustomTagFactory.php +++ b/tests/unit/Assets/CustomTagFactory.php @@ -20,6 +20,7 @@ class CustomTagFactory implements Factory { + /** @var CustomServiceClass|null */ public $class; public function create(string $tagLine, ?Context $context = null, CustomServiceClass $class = null): Tag diff --git a/tests/unit/DocBlock/DescriptionFactoryTest.php b/tests/unit/DocBlock/DescriptionFactoryTest.php index 7fc90eff..00304cad 100644 --- a/tests/unit/DocBlock/DescriptionFactoryTest.php +++ b/tests/unit/DocBlock/DescriptionFactoryTest.php @@ -13,7 +13,6 @@ namespace phpDocumentor\Reflection\DocBlock; -use Exception; use Mockery as m; use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag; use phpDocumentor\Reflection\DocBlock\Tags\Link as LinkTag; @@ -216,7 +215,7 @@ public function testDescriptionWithBrokenInlineTags(): void $tagFactory->shouldReceive('create') ->once() ->with('@see $name', $context) - ->andReturn(InvalidTag::create('$name', 'see', new Exception())); + ->andReturn(InvalidTag::create('$name', 'see')); $factory = new DescriptionFactory($tagFactory); $description = $factory->create($contents, $context); diff --git a/tests/unit/DocBlock/StandardTagFactoryTest.php b/tests/unit/DocBlock/StandardTagFactoryTest.php index 0b3aa466..3f8311f5 100644 --- a/tests/unit/DocBlock/StandardTagFactoryTest.php +++ b/tests/unit/DocBlock/StandardTagFactoryTest.php @@ -15,6 +15,7 @@ use InvalidArgumentException; use Mockery as m; +use Name\Spaced\Tag; use phpDocumentor\Reflection\Assets\CustomParam; use phpDocumentor\Reflection\Assets\CustomServiceClass; use phpDocumentor\Reflection\Assets\CustomServiceInterface; @@ -134,10 +135,10 @@ public function testCreatingASpecificTag(): void public function testAnEmptyContextIsCreatedIfNoneIsProvided(): void { $fqsen = '\Tag'; - $resolver = m::mock(FqsenResolver::class) - ->shouldReceive('resolve') + $resolver = m::mock(FqsenResolver::class); + $resolver->allows('resolve') ->with('Tag', m::type(Context::class)) - ->andReturn(new Fqsen($fqsen)) + ->andReturns(new Fqsen($fqsen)) ->getMock(); $descriptionFactory = m::mock(DescriptionFactory::class); $descriptionFactory->shouldIgnoreMissing(); @@ -246,18 +247,14 @@ public function testPassingYourOwnSetOfTagHandlersWithEmptyComment(): void 'The tag "@my-täg " does not seem to be wellformed, please check it for errors' ); - $typeResolver = new TypeResolver(); $fqsenResolver = new FqsenResolver(); - $tagFactory = StandardTagFactory::createInstance($fqsenResolver); - $descriptionFactory = new DescriptionFactory($tagFactory); $context = new Context(''); $tagFactory = StandardTagFactory::createInstance( $fqsenResolver, - ['my-täg' => Author::class] ); - $tag = $tagFactory->create('@my-täg ', $context); + $tagFactory->create('@my-täg ', $context); } /** @@ -291,6 +288,7 @@ public function testAddParameterToServiceLocator(): void $tagFactory->addParameter('myParam', 'myValue'); $spy = $tagFactory->create('@spy'); + self::assertInstanceOf(CustomParam::class, $spy); $this->assertSame($resolver, $spy->fqsenResolver); $this->assertSame('myValue', $spy->myParam); } @@ -311,6 +309,7 @@ public function testAddServiceToServiceLocator(): void $spy = $tagFactory->create('@spy'); + self::assertInstanceOf(CustomServiceClass::class, $spy); $this->assertSame($service, $spy->formatter); } @@ -331,6 +330,7 @@ public function testInjectConcreteServiceForInterfaceToServiceLocator(): void $spy = $tagFactory->create('@spy'); + self::assertInstanceOf(CustomServiceInterface::class, $spy); $this->assertSame($service, $spy->formatter); } @@ -366,7 +366,10 @@ public function testHandlerRegistrationFailsIfProvidedTagNameIsNamespaceButNotFu $resolver = m::mock(FqsenResolver::class); $tagFactory = StandardTagFactory::createInstance($resolver); // phpcs:ignore SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly.ReferenceViaFullyQualifiedName - $tagFactory->registerTagHandler(\Name\Spaced\Tag::class, Author::class); + $tagFactory->registerTagHandler( + Tag::class, // @phpstan-ignore class.notFound + Author::class + ); } /** @@ -380,6 +383,7 @@ public function testHandlerRegistrationFailsIfProvidedHandlerIsEmpty(): void $this->expectException('InvalidArgumentException'); $resolver = m::mock(FqsenResolver::class); $tagFactory = StandardTagFactory::createInstance($resolver); + //@phpstan-ignore argument.type $tagFactory->registerTagHandler('my-tag', ''); } @@ -394,6 +398,8 @@ public function testHandlerRegistrationFailsIfProvidedHandlerIsNotAnExistingClas $this->expectException('InvalidArgumentException'); $resolver = m::mock(FqsenResolver::class); $tagFactory = StandardTagFactory::createInstance($resolver); + + //@phpstan-ignore argument.type $tagFactory->registerTagHandler('my-tag', 'IDoNotExist'); } @@ -408,6 +414,8 @@ public function testHandlerRegistrationFailsIfProvidedHandlerDoesNotImplementThe $this->expectException('InvalidArgumentException'); $resolver = m::mock(FqsenResolver::class); $tagFactory = StandardTagFactory::createInstance($resolver); + + //@phpstan-ignore argument.type $tagFactory->registerTagHandler('my-tag', 'stdClass'); } diff --git a/tests/unit/DocBlock/Tags/AuthorTest.php b/tests/unit/DocBlock/Tags/AuthorTest.php index a1ce4321..5525fdd6 100644 --- a/tests/unit/DocBlock/Tags/AuthorTest.php +++ b/tests/unit/DocBlock/Tags/AuthorTest.php @@ -82,6 +82,7 @@ public function testHasTheAuthorName(): void $fixture = new Author($expected, 'mike@phpdoc.org'); + self::assertInstanceOf(Author::class, $fixture); $this->assertSame($expected, $fixture->getAuthorName()); } @@ -95,6 +96,7 @@ public function testHasTheAuthorMailAddress(): void $fixture = new Author('Mike van Riel', $expected); + self::assertInstanceOf(Author::class, $fixture); $this->assertSame($expected, $fixture->getEmail()); } @@ -156,6 +158,7 @@ public function testFactoryMethod(string $input, string $output, string $name, s { $fixture = Author::create($input); + self::assertInstanceOf(Author::class, $fixture); $this->assertSame($output, (string) $fixture); $this->assertSame($name, $fixture->getAuthorName()); $this->assertSame($email, $fixture->getEmail()); diff --git a/tests/unit/DocBlock/Tags/ExampleTest.php b/tests/unit/DocBlock/Tags/ExampleTest.php index 77bf5a34..6fe97d1b 100644 --- a/tests/unit/DocBlock/Tags/ExampleTest.php +++ b/tests/unit/DocBlock/Tags/ExampleTest.php @@ -23,6 +23,7 @@ class ExampleTest extends TestCase public function testExampleWithoutContent(): void { $tag = Example::create('"example1.php"'); + self::assertInstanceOf(Example::class, $tag); $this->assertEquals('"example1.php"', $tag->getContent()); $this->assertEquals('', $tag->getDescription()); $this->assertEquals('example', $tag->getName()); @@ -39,6 +40,7 @@ public function testExampleWithoutContent(): void public function testWithDescription(): void { $tag = Example::create('"example1.php" some text'); + self::assertInstanceOf(Example::class, $tag); $this->assertEquals('example1.php', $tag->getFilePath()); $this->assertEquals('some text', $tag->getDescription()); } @@ -54,6 +56,7 @@ public function testWithDescription(): void public function testStartlineIsParsed(): void { $tag = Example::create('"example1.php" 10'); + self::assertInstanceOf(Example::class, $tag); $this->assertEquals('example1.php', $tag->getFilePath()); $this->assertEquals(10, $tag->getStartingLine()); } @@ -70,6 +73,7 @@ public function testStartlineIsParsed(): void public function testAllowOmittingLineCount(): void { $tag = Example::create('"example1.php" 10 some text'); + self::assertInstanceOf(Example::class, $tag); $this->assertEquals('example1.php', $tag->getFilePath()); $this->assertEquals(10, $tag->getStartingLine()); $this->assertEquals('some text', $tag->getDescription()); @@ -87,6 +91,7 @@ public function testAllowOmittingLineCount(): void public function testLengthIsParsed(): void { $tag = Example::create('"example1.php" 10 5'); + self::assertInstanceOf(Example::class, $tag); $this->assertEquals('example1.php', $tag->getFilePath()); $this->assertEquals(10, $tag->getStartingLine()); $this->assertEquals(5, $tag->getLineCount()); @@ -159,6 +164,7 @@ public function testFactoryMethod( string $content ): void { $tag = Example::create($input); + self::assertInstanceOf(Example::class, $tag); $this->assertSame($filePath, $tag->getFilePath()); $this->assertSame($startLine, $tag->getStartingLine()); $this->assertSame($lineCount, $tag->getLineCount()); diff --git a/tests/unit/DocBlock/Tags/Factory/AbstractPHPStanFactoryTest.php b/tests/unit/DocBlock/Tags/Factory/AbstractPHPStanFactoryTest.php new file mode 100644 index 00000000..27eb7e9f --- /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 00000000..60e9adbb --- /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 00000000..e175bbce --- /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 00000000..36510853 --- /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/ParamFactoryTest.php b/tests/unit/DocBlock/Tags/Factory/ParamFactoryTest.php index aa8c14e1..d5687769 100644 --- a/tests/unit/DocBlock/Tags/Factory/ParamFactoryTest.php +++ b/tests/unit/DocBlock/Tags/Factory/ParamFactoryTest.php @@ -48,7 +48,7 @@ public function testParamIsCreated(string $input, Tag $expected): void } /** - * @return array + * @return array> */ public function paramInputProvider(): array { diff --git a/tests/unit/DocBlock/Tags/Factory/TagFactoryTestCase.php b/tests/unit/DocBlock/Tags/Factory/TagFactoryTestCase.php index f7876b2b..d8b9900c 100644 --- a/tests/unit/DocBlock/Tags/Factory/TagFactoryTestCase.php +++ b/tests/unit/DocBlock/Tags/Factory/TagFactoryTestCase.php @@ -27,23 +27,16 @@ use PHPStan\PhpDocParser\ParserConfig; use PHPUnit\Framework\TestCase; -use function class_exists; use function property_exists; abstract class TagFactoryTestCase extends TestCase { public function parseTag(string $tag): PhpDocTagNode { - if (class_exists(ParserConfig::class)) { - $config = new ParserConfig([]); - $lexer = new Lexer($config); - $constParser = new ConstExprParser($config); - $phpDocParser = new PhpDocParser($config, new TypeParser($config, $constParser), $constParser); - } else { - $lexer = new Lexer(); - $constParser = new ConstExprParser(); - $phpDocParser = new PhpDocParser(new TypeParser($constParser), $constParser); - } + $config = new ParserConfig(['indexes' => true, 'lines' => true]); + $lexer = new Lexer($config); + $constParser = new ConstExprParser($config); + $phpDocParser = new PhpDocParser($config, new TypeParser($config, $constParser), $constParser); $tagNode = $phpDocParser->parseTag(new TokenIterator($lexer->tokenize($tag))); if (property_exists($tagNode->value, 'description') === true) { diff --git a/tests/unit/DocBlock/Tags/Factory/TemplateCovariantFactoryTest.php b/tests/unit/DocBlock/Tags/Factory/TemplateCovariantFactoryTest.php new file mode 100644 index 00000000..dcc70a5d --- /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 00000000..964d1f98 --- /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 00000000..6171fbb4 --- /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) + ); + } +} diff --git a/tests/unit/DocBlock/Tags/InvalidTagTest.php b/tests/unit/DocBlock/Tags/InvalidTagTest.php index cb25754c..dc252cda 100644 --- a/tests/unit/DocBlock/Tags/InvalidTagTest.php +++ b/tests/unit/DocBlock/Tags/InvalidTagTest.php @@ -10,6 +10,7 @@ use Throwable; use function fopen; +use function is_string; use function serialize; use function unserialize; @@ -57,9 +58,12 @@ public function testCreationWithErrorContainingClosure(): void self::assertSame('name', $tag->getName()); self::assertSame('@name Body', $tag->render()); self::assertSame($parentException, $tag->getException()); + + self::assertSame($e, $tag->getException()->getPrevious()); $trace = $tag->getException()->getPrevious()->getTrace(); if (isset($trace[0]['args'])) { // Not set by default on 7.4 + self::assertTrue(is_string($trace[0]['args'][0])); self::assertStringStartsWith('(Closure at', $trace[0]['args'][0]); self::assertStringContainsString(__FILE__, $trace[0]['args'][0]); } @@ -70,7 +74,7 @@ public function testCreationWithErrorContainingClosure(): void private function throwExceptionFromClosureWithClosureArgument(): void { - $function = static function (): void { + $function = static function (?callable $foo = null): void { throw new InvalidArgumentException(); }; @@ -87,9 +91,11 @@ public function testCreationWithErrorContainingResource(): void self::assertSame('name', $tag->getName()); self::assertSame('@name Body', $tag->render()); self::assertSame($parentException, $tag->getException()); + self::assertSame($e, $tag->getException()->getPrevious()); $trace = $tag->getException()->getPrevious()->getTrace(); if (isset($trace[0]['args'])) { // Not set by default on 7.4 + self::assertTrue(is_string($trace[0]['args'][0])); self::assertStringStartsWith( 'resource(stream)', $trace[0]['args'][0] @@ -102,7 +108,7 @@ public function testCreationWithErrorContainingResource(): void private function throwExceptionWithResourceArgument(): void { - $function = static function (): void { + $function = static function ($file): void { throw new InvalidArgumentException(); }; diff --git a/tests/unit/DocBlock/Tags/MethodParameterTest.php b/tests/unit/DocBlock/Tags/MethodParameterTest.php index a5fb2f06..89e0bdc2 100644 --- a/tests/unit/DocBlock/Tags/MethodParameterTest.php +++ b/tests/unit/DocBlock/Tags/MethodParameterTest.php @@ -81,6 +81,10 @@ public function testIfTagCanBeRenderedUsingMethodParameterWithDefaultValue( sprintf('%s $argument = %s', $type, $defaultValueStr), (string) $fixture ); + $this->assertSame( + $defaultValueStr, + $fixture->getDefaultValue() + ); } /** diff --git a/tests/unit/DocBlock/Tags/MethodTest.php b/tests/unit/DocBlock/Tags/MethodTest.php index c80b8178..2cdf842d 100644 --- a/tests/unit/DocBlock/Tags/MethodTest.php +++ b/tests/unit/DocBlock/Tags/MethodTest.php @@ -225,6 +225,19 @@ public function testStringRepresentationIsReturnedWithoutDescription(): void ); } + /** + * @covers ::__construct + * @covers ::getReturnType + */ + public function testReturnsReference(): void + { + $expected = new String_(); + + $fixture = new Method('myMethod', [], $expected); + + $this->assertFalse($fixture->returnsReference()); + } + /** * @covers ::create */ diff --git a/tests/unit/DocBlock/Tags/ParamTest.php b/tests/unit/DocBlock/Tags/ParamTest.php index 1a628f73..c89df399 100644 --- a/tests/unit/DocBlock/Tags/ParamTest.php +++ b/tests/unit/DocBlock/Tags/ParamTest.php @@ -153,4 +153,19 @@ public function testStringRepresentationIsReturned(): void $this->assertSame('string ...$myParameter Description', (string) $fixture); } + + /** + * @uses \phpDocumentor\Reflection\DocBlock\Description + * + * @covers ::__construct + * @covers \phpDocumentor\Reflection\DocBlock\Tags\Param::isReference + */ + public function testIsReference(): void + { + $expected = new Description('Description'); + + $fixture = new Param('1.0', null, false, $expected); + + $this->assertFalse($fixture->isReference()); + } } diff --git a/tests/unit/DocBlock/Tags/SinceTest.php b/tests/unit/DocBlock/Tags/SinceTest.php index cd221474..1db40bc7 100644 --- a/tests/unit/DocBlock/Tags/SinceTest.php +++ b/tests/unit/DocBlock/Tags/SinceTest.php @@ -158,6 +158,7 @@ public function testFactoryMethod(): void $fixture = Since::create('1.0 My Description', $descriptionFactory, $context); + self::assertInstanceOf(Since::class, $fixture); $this->assertSame('1.0 My Description', (string) $fixture); $this->assertSame($version, $fixture->getVersion()); $this->assertSame($description, $fixture->getDescription()); @@ -178,6 +179,7 @@ public function testFactoryMethodCreatesEmptySinceTag(): void $fixture = Since::create('', $descriptionFactory, new Context('')); + self::assertInstanceOf(Since::class, $fixture); $this->assertSame('', (string) $fixture); $this->assertSame(null, $fixture->getVersion()); $this->assertSame(null, $fixture->getDescription()); diff --git a/tests/unit/DocBlock/Tags/SourceTest.php b/tests/unit/DocBlock/Tags/SourceTest.php index 52defd1a..d45924e3 100644 --- a/tests/unit/DocBlock/Tags/SourceTest.php +++ b/tests/unit/DocBlock/Tags/SourceTest.php @@ -227,6 +227,8 @@ public function testExceptionIsThrownIfStartingLineIsNotInteger(): void public function testExceptionIsThrownIfLineCountIsNotIntegerOrNull(): void { $this->expectException('InvalidArgumentException'); + + //@phpstan-ignore argument.type new Source('1', []); } } diff --git a/tests/unit/DocBlock/Tags/VersionTest.php b/tests/unit/DocBlock/Tags/VersionTest.php index 9a156f9d..03def31d 100644 --- a/tests/unit/DocBlock/Tags/VersionTest.php +++ b/tests/unit/DocBlock/Tags/VersionTest.php @@ -158,6 +158,7 @@ public function testFactoryMethod(): void $fixture = Version::create('1.0 My Description', $descriptionFactory, $context); + self::assertInstanceOf(Version::class, $fixture); $this->assertSame('1.0 My Description', (string) $fixture); $this->assertSame($version, $fixture->getVersion()); $this->assertSame($description, $fixture->getDescription()); @@ -178,6 +179,7 @@ public function testFactoryMethodCreatesEmptyVersionTag(): void $fixture = Version::create('', $descriptionFactory, new Context('')); + self::assertInstanceOf(Version::class, $fixture); $this->assertSame('', (string) $fixture); $this->assertSame(null, $fixture->getVersion()); $this->assertSame(null, $fixture->getDescription()); diff --git a/tests/unit/DocBlockFactoryTest.php b/tests/unit/DocBlockFactoryTest.php index 762a7acc..6f6ee89c 100644 --- a/tests/unit/DocBlockFactoryTest.php +++ b/tests/unit/DocBlockFactoryTest.php @@ -200,7 +200,7 @@ public function testTagsAreInterpretedUsingFactory(): void } /** - * @return string[] + * @return array> */ public function provideSummaryAndDescriptions(): array { diff --git a/tests/unit/DocBlockTest.php b/tests/unit/DocBlockTest.php index f58f7461..29b46d83 100644 --- a/tests/unit/DocBlockTest.php +++ b/tests/unit/DocBlockTest.php @@ -108,8 +108,9 @@ public function testDocBlockCanHaveTags(): void public function testDocBlockAllowsOnlyTags(): void { $this->expectException('InvalidArgumentException'); - $tags = [null]; - $fixture = new DocBlock('', null, $tags); + $tags = [null]; + // @phpstan-ignore argument.type + new DocBlock('', null, $tags); } /** diff --git a/tests/unit/PregSplitTest.php b/tests/unit/PregSplitTest.php index 15ca30d5..96b3d5f8 100644 --- a/tests/unit/PregSplitTest.php +++ b/tests/unit/PregSplitTest.php @@ -41,8 +41,12 @@ public function testSimplePregSplit(): void public function testPregSplitThrowsOnError(): void { //We need to disable the error handler for phpunit... because we expect some errors here - $this->errorHandler = set_error_handler(static function (): void { - }, E_WARNING); + $this->errorHandler = set_error_handler( + static function (int $i, string $s, string $s2, int $x, ?array $trace = null): bool { + return true; + }, + E_WARNING + ); $this->expectException(PcreException::class); Utils::pregSplit('~InvalidRegular)Expression~', 'some word');