diff --git a/composer.json b/composer.json index 7d76f16e..c4348f1f 100644 --- a/composer.json +++ b/composer.json @@ -15,10 +15,11 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpdocumentor/type-resolver": "^1.3", + "phpdocumentor/type-resolver": "1.x-dev@dev", "webmozart/assert": "^1.9.1", "phpdocumentor/reflection-common": "^2.2", - "ext-filter": "*" + "ext-filter": "*", + "phpstan/phpdoc-parser": "^1.7" }, "require-dev": { "mockery/mockery": "~1.3.5", diff --git a/composer.lock b/composer.lock index c4be8e91..55b451a9 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": "cbf52dda9a68fb6e5d4da2511d6b4c0d", + "content-hash": "829061026cc6b2284953cbe816406760", "packages": [ { "name": "phpdocumentor/reflection-common", @@ -61,26 +61,34 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.6.1", + "version": "1.x-dev", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "77a32518733312af16a44300404e945338981de3" + "reference": "662b17f56786e34354684f6c141c1909b1ee3ac0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3", - "reference": "77a32518733312af16a44300404e945338981de3", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/662b17f56786e34354684f6c141c1909b1ee3ac0", + "reference": "662b17f56786e34354684f6c141c1909b1ee3ac0", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.13" }, "require-dev": { "ext-tokenizer": "*", - "psalm/phar": "^4.8" + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -105,9 +113,54 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.x" + }, + "time": "2022-11-11T10:44:03+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.13.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "33aefcdab42900e36366d0feab6206e2dd68f947" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/33aefcdab42900e36366d0feab6206e2dd68f947", + "reference": "33aefcdab42900e36366d0feab6206e2dd68f947", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.13.0" }, - "time": "2022-03-15T21:29:03+00:00" + "time": "2022-10-21T09:57:39+00:00" }, { "name": "webmozart/assert", @@ -4004,7 +4057,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "phpdocumentor/type-resolver": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 27aa317b..03c89541 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -10,13 +10,10 @@ - + - - - diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fd7ab748..baf0ad63 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,8 +4,8 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.0/phpunit.xsd" colors="true" - convertDeprecationsToExceptions="true" - beStrictAboutOutputDuringTests="true" + convertDeprecationsToExceptions="false" + beStrictAboutOutputDuringTests="false" forceCoversAnnotation="true" verbose="true" bootstrap="vendor/autoload.php" diff --git a/src/DocBlock/DescriptionFactory.php b/src/DocBlock/DescriptionFactory.php index 1a519ec4..d0180184 100644 --- a/src/DocBlock/DescriptionFactory.php +++ b/src/DocBlock/DescriptionFactory.php @@ -13,6 +13,7 @@ namespace phpDocumentor\Reflection\DocBlock; +use phpDocumentor\Reflection\DocBlock\Tags\Factory\Factory; use phpDocumentor\Reflection\Types\Context as TypeContext; use phpDocumentor\Reflection\Utils; @@ -47,13 +48,13 @@ */ class DescriptionFactory { - /** @var TagFactory */ + /** @var Factory */ private $tagFactory; /** * Initializes this factory with the means to construct (inline) tags. */ - public function __construct(TagFactory $tagFactory) + public function __construct(Factory $tagFactory) { $this->tagFactory = $tagFactory; } diff --git a/src/DocBlock/StandardTagFactory.php b/src/DocBlock/StandardTagFactory.php index 8d765951..e5c071c1 100644 --- a/src/DocBlock/StandardTagFactory.php +++ b/src/DocBlock/StandardTagFactory.php @@ -17,6 +17,7 @@ use phpDocumentor\Reflection\DocBlock\Tags\Author; use phpDocumentor\Reflection\DocBlock\Tags\Covers; use phpDocumentor\Reflection\DocBlock\Tags\Deprecated; +use phpDocumentor\Reflection\DocBlock\Tags\Factory\Factory; use phpDocumentor\Reflection\DocBlock\Tags\Generic; use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag; use phpDocumentor\Reflection\DocBlock\Tags\Link as LinkTag; @@ -40,12 +41,15 @@ use ReflectionParameter; use Webmozart\Assert\Assert; +use function array_key_exists; use function array_merge; use function array_slice; use function call_user_func_array; use function count; use function get_class; +use function is_object; use function preg_match; +use function sprintf; use function strpos; use function trim; @@ -72,7 +76,7 @@ final class StandardTagFactory implements TagFactory public const REGEX_TAGNAME = '[\w\-\_\\\\:]+'; /** - * @var array> An array with a tag as a key, and an + * @var array|Factory> An array with a tag as a key, and an * FQCN to a class that handles it as an array value. */ private $tagHandlerMappings = [ @@ -162,18 +166,25 @@ public function addService(object $service, ?string $alias = null): void $this->serviceLocator[$alias ?: get_class($service)] = $service; } - public function registerTagHandler(string $tagName, string $handler): void + /** {@inheritDoc} */ + public function registerTagHandler(string $tagName, $handler): void { Assert::stringNotEmpty($tagName); - Assert::classExists($handler); - Assert::implementsInterface($handler, Tag::class); - if (strpos($tagName, '\\') && $tagName[0] !== '\\') { throw new InvalidArgumentException( 'A namespaced tag must have a leading backslash as it must be fully qualified' ); } + if (is_object($handler)) { + Assert::isInstanceOf($handler, Factory::class); + $this->tagHandlerMappings[$tagName] = $handler; + + return; + } + + Assert::classExists($handler); + Assert::implementsInterface($handler, Tag::class); $this->tagHandlerMappings[$tagName] = $handler; } @@ -210,6 +221,10 @@ private function createTag(string $body, string $name, TypeContext $context): Ta $this->getServiceLocatorWithDynamicParameters($context, $name, $body) ); + if (array_key_exists('tagLine', $arguments)) { + $arguments['tagLine'] = sprintf('@%s %s', $name, $body); + } + try { $callable = [$handlerClassName, 'create']; Assert::isCallable($callable); @@ -225,9 +240,9 @@ private function createTag(string $body, string $name, TypeContext $context): Ta /** * Determines the Fully Qualified Class Name of the Factory or Tag (containing a Factory Method `create`). * - * @return class-string + * @return class-string|Factory */ - private function findHandlerClassName(string $tagName, TypeContext $context): string + private function findHandlerClassName(string $tagName, TypeContext $context) { $handlerClassName = Generic::class; if (isset($this->tagHandlerMappings[$tagName])) { @@ -268,18 +283,18 @@ private function getArgumentsForParametersFromWiring(array $parameters, array $l } } + $parameterName = $parameter->getName(); if (isset($locator[$typeHint])) { - $arguments[] = $locator[$typeHint]; + $arguments[$parameterName] = $locator[$typeHint]; continue; } - $parameterName = $parameter->getName(); if (isset($locator[$parameterName])) { - $arguments[] = $locator[$parameterName]; + $arguments[$parameterName] = $locator[$parameterName]; continue; } - $arguments[] = null; + $arguments[$parameterName] = null; } return $arguments; @@ -289,12 +304,14 @@ private function getArgumentsForParametersFromWiring(array $parameters, array $l * Retrieves a series of ReflectionParameter objects for the static 'create' method of the given * tag handler class name. * - * @param class-string $handlerClassName + * @param class-string|Factory $handler * * @return ReflectionParameter[] */ - private function fetchParametersForHandlerFactoryMethod(string $handlerClassName): array + private function fetchParametersForHandlerFactoryMethod($handler): array { + $handlerClassName = is_object($handler) ? get_class($handler) : $handler; + if (!isset($this->tagHandlerParameterCache[$handlerClassName])) { $methodReflection = new ReflectionMethod($handlerClassName, 'create'); $this->tagHandlerParameterCache[$handlerClassName] = $methodReflection->getParameters(); diff --git a/src/DocBlock/TagFactory.php b/src/DocBlock/TagFactory.php index c0868dcb..a6f1ae8d 100644 --- a/src/DocBlock/TagFactory.php +++ b/src/DocBlock/TagFactory.php @@ -14,9 +14,9 @@ namespace phpDocumentor\Reflection\DocBlock; use InvalidArgumentException; -use phpDocumentor\Reflection\Types\Context as TypeContext; +use phpDocumentor\Reflection\DocBlock\Tags\Factory\Factory; -interface TagFactory +interface TagFactory extends Factory { /** * Adds a parameter to the service locator that can be injected in a tag's factory method. @@ -40,17 +40,6 @@ interface TagFactory */ public function addParameter(string $name, $value): void; - /** - * Factory method responsible for instantiating the correct sub type. - * - * @param string $tagLine The text for this tag, including description. - * - * @return Tag A new tag object. - * - * @throws InvalidArgumentException If an invalid tag line was presented. - */ - public function create(string $tagLine, ?TypeContext $context = null): Tag; - /** * Registers a service with the Service Locator using the FQCN of the class or the alias, if provided. * @@ -71,7 +60,7 @@ public function addService(object $service): void; * * @param string $tagName Name of tag to register a handler for. When registering a namespaced * tag, the full name, along with a prefixing slash MUST be provided. - * @param class-string $handler FQCN of handler. + * @param class-string|Factory $handler FQCN of handler. * * @throws InvalidArgumentException If the tag name is not a string. * @throws InvalidArgumentException If the tag name is namespaced (contains backslashes) but @@ -80,5 +69,5 @@ public function addService(object $service): void; * @throws InvalidArgumentException If the handler is not an existing class. * @throws InvalidArgumentException If the handler does not implement the {@see Tag} interface. */ - public function registerTagHandler(string $tagName, string $handler): void; + public function registerTagHandler(string $tagName, $handler): void; } diff --git a/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php b/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php new file mode 100644 index 00000000..7d96d314 --- /dev/null +++ b/src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php @@ -0,0 +1,68 @@ +lexer = new Lexer(); + $constParser = new ConstExprParser(); + $this->parser = new PhpDocParser(new TypeParser($constParser), $constParser); + $this->factories = $factories; + } + + public function create(string $tagLine, ?TypeContext $context = null): Tag + { + $tokens = $this->lexer->tokenize($tagLine); + $ast = $this->parser->parseTag(new TokenIterator($tokens)); + + if ($context === null) { + $context = new TypeContext(''); + } + + foreach ($this->factories as $factory) { + if ($factory->supports($ast, $context)) { + return $factory->create($ast, $context); + } + } + + return InvalidTag::create( + $ast->name, + (string) $ast->value + ); + } +} diff --git a/src/DocBlock/Tags/Factory/Factory.php b/src/DocBlock/Tags/Factory/Factory.php new file mode 100644 index 00000000..190d3ff8 --- /dev/null +++ b/src/DocBlock/Tags/Factory/Factory.php @@ -0,0 +1,41 @@ +descriptionFactory = $descriptionFactory; + $this->typeResolver = $typeResolver; + } + + public function create(PhpDocTagNode $node, Context $context): Tag + { + $tagValue = $node->value; + Assert::isInstanceOf($tagValue, MethodTagValueNode::class); + + return new Method( + $tagValue->methodName, + [], + $this->createReturnType($tagValue, $context), + $tagValue->isStatic, + $this->descriptionFactory->create($tagValue->description, $context), + false, + array_map( + function (MethodTagValueParameterNode $param) use ($context) { + return new MethodParameter( + trim($param->parameterName, '$'), + $param->type === null ? new Mixed_() : $this->typeResolver->createType( + $param->type, + $context + ), + $param->isReference, + $param->isVariadic, + (string) $param->defaultValue + ); + }, + $tagValue->parameters + ), + ); + } + + public function supports(PhpDocTagNode $node, Context $context): bool + { + return $node->value instanceof MethodTagValueNode; + } + + private function createReturnType(MethodTagValueNode $tagValue, Context $context): Type + { + if ($tagValue->returnType === null) { + return new Void_(); + } + + return $this->typeResolver->createType($tagValue->returnType, $context); + } +} diff --git a/src/DocBlock/Tags/Factory/PHPStanFactory.php b/src/DocBlock/Tags/Factory/PHPStanFactory.php new file mode 100644 index 00000000..cf04a06e --- /dev/null +++ b/src/DocBlock/Tags/Factory/PHPStanFactory.php @@ -0,0 +1,16 @@ +descriptionFactory = $descriptionFactory; + $this->typeResolver = $typeResolver; + } + + public function create(PhpDocTagNode $node, Context $context): Tag + { + $tagValue = $node->value; + Assert::isInstanceOf($tagValue, ParamTagValueNode::class); + + return new Param( + trim($tagValue->parameterName, '$'), + $this->typeResolver->createType($tagValue->type, $context), + $tagValue->isVariadic, + $this->descriptionFactory->create($tagValue->description, $context), + $tagValue->isReference + ); + } + + public function supports(PhpDocTagNode $node, Context $context): bool + { + return $node->value instanceof ParamTagValueNode; + } +} diff --git a/src/DocBlock/Tags/Factory/PropertyFactory.php b/src/DocBlock/Tags/Factory/PropertyFactory.php new file mode 100644 index 00000000..83a18e8a --- /dev/null +++ b/src/DocBlock/Tags/Factory/PropertyFactory.php @@ -0,0 +1,48 @@ +descriptionFactory = $descriptionFactory; + $this->typeResolver = $typeResolver; + } + + public function create(PhpDocTagNode $node, Context $context): Tag + { + $tagValue = $node->value; + Assert::isInstanceOf($tagValue, PropertyTagValueNode::class); + + return new Property( + trim($tagValue->propertyName, '$'), + $this->typeResolver->createType($tagValue->type, $context), + $this->descriptionFactory->create($tagValue->description, $context) + ); + } + + public function supports(PhpDocTagNode $node, Context $context): bool + { + return $node->value instanceof PropertyTagValueNode && $node->name === '@property'; + } +} diff --git a/src/DocBlock/Tags/Factory/PropertyReadFactory.php b/src/DocBlock/Tags/Factory/PropertyReadFactory.php new file mode 100644 index 00000000..be443ab6 --- /dev/null +++ b/src/DocBlock/Tags/Factory/PropertyReadFactory.php @@ -0,0 +1,48 @@ +typeResolver = $typeResolver; + $this->descriptionFactory = $descriptionFactory; + } + + public function create(PhpDocTagNode $node, Context $context): Tag + { + $tagValue = $node->value; + Assert::isInstanceOf($tagValue, PropertyTagValueNode::class); + + return new PropertyRead( + trim($tagValue->propertyName, '$'), + $this->typeResolver->createType($tagValue->type, $context), + $this->descriptionFactory->create($tagValue->description, $context) + ); + } + + public function supports(PhpDocTagNode $node, Context $context): bool + { + return $node->value instanceof PropertyTagValueNode && $node->name === '@property-read'; + } +} diff --git a/src/DocBlock/Tags/Factory/PropertyWriteFactory.php b/src/DocBlock/Tags/Factory/PropertyWriteFactory.php new file mode 100644 index 00000000..08cf7534 --- /dev/null +++ b/src/DocBlock/Tags/Factory/PropertyWriteFactory.php @@ -0,0 +1,48 @@ +descriptionFactory = $descriptionFactory; + $this->typeResolver = $typeResolver; + } + + public function create(PhpDocTagNode $node, Context $context): Tag + { + $tagValue = $node->value; + Assert::isInstanceOf($tagValue, PropertyTagValueNode::class); + + return new PropertyWrite( + trim($tagValue->propertyName, '$'), + $this->typeResolver->createType($tagValue->type, $context), + $this->descriptionFactory->create($tagValue->description, $context) + ); + } + + public function supports(PhpDocTagNode $node, Context $context): bool + { + return $node->value instanceof PropertyTagValueNode && $node->name === '@property-write'; + } +} diff --git a/src/DocBlock/Tags/Factory/ReturnFactory.php b/src/DocBlock/Tags/Factory/ReturnFactory.php new file mode 100644 index 00000000..8ebe5ad4 --- /dev/null +++ b/src/DocBlock/Tags/Factory/ReturnFactory.php @@ -0,0 +1,45 @@ +descriptionFactory = $descriptionFactory; + $this->typeResolver = $typeResolver; + } + + public function create(PhpDocTagNode $node, Context $context): Tag + { + $tagValue = $node->value; + Assert::isInstanceOf($tagValue, ReturnTagValueNode::class); + + return new Return_( + $this->typeResolver->createType($tagValue->type, $context), + $this->descriptionFactory->create($tagValue->description, $context) + ); + } + + public function supports(PhpDocTagNode $node, Context $context): bool + { + return $node->value instanceof ReturnTagValueNode; + } +} diff --git a/src/DocBlock/Tags/Factory/VarFactory.php b/src/DocBlock/Tags/Factory/VarFactory.php new file mode 100644 index 00000000..79ffdf12 --- /dev/null +++ b/src/DocBlock/Tags/Factory/VarFactory.php @@ -0,0 +1,48 @@ +descriptionFactory = $descriptionFactory; + $this->typeResolver = $typeResolver; + } + + public function create(PhpDocTagNode $node, Context $context): Tag + { + $tagValue = $node->value; + Assert::isInstanceOf($tagValue, VarTagValueNode::class); + + return new Var_( + trim($tagValue->variableName, '$'), + $this->typeResolver->createType($tagValue->type, $context), + $this->descriptionFactory->create($tagValue->description, $context) + ); + } + + public function supports(PhpDocTagNode $node, Context $context): bool + { + return $node->value instanceof VarTagValueNode; + } +} diff --git a/src/DocBlock/Tags/Method.php b/src/DocBlock/Tags/Method.php index 12996a42..02aa2b47 100644 --- a/src/DocBlock/Tags/Method.php +++ b/src/DocBlock/Tags/Method.php @@ -24,6 +24,7 @@ use Webmozart\Assert\Assert; use function array_keys; +use function array_map; use function explode; use function implode; use function is_string; @@ -31,9 +32,12 @@ use function sort; use function strpos; use function substr; +use function trigger_error; use function trim; use function var_export; +use const E_USER_DEPRECATED; + /** * Reflection class for an {@}method in a Docblock. */ @@ -45,12 +49,6 @@ final class Method extends BaseTag implements Factory\StaticMethod /** @var string */ private $methodName; - /** - * @phpstan-var array - * @var array> - */ - private $arguments; - /** @var bool */ private $isStatic; @@ -60,8 +58,12 @@ final class Method extends BaseTag implements Factory\StaticMethod /** @var bool */ private $returnsReference; + /** @var MethodParameter[] */ + private array $parameters; + /** * @param array> $arguments + * @param MethodParameter[] $parameters * @phpstan-param array $arguments */ public function __construct( @@ -70,7 +72,8 @@ public function __construct( ?Type $returnType = null, bool $static = false, ?Description $description = null, - bool $returnsReference = false + bool $returnsReference = false, + ?array $parameters = null ) { Assert::stringNotEmpty($methodName); @@ -78,20 +81,31 @@ public function __construct( $returnType = new Void_(); } + $arguments = $this->filterArguments($arguments); + $this->methodName = $methodName; - $this->arguments = $this->filterArguments($arguments); $this->returnType = $returnType; $this->isStatic = $static; $this->description = $description; $this->returnsReference = $returnsReference; + $this->parameters = $parameters ?? $this->fromLegacyArguments($arguments); } + /** + * @deprecated Create using static factory is deprecated, + * this method should not be called directly by library consumers + */ public static function create( string $body, ?TypeResolver $typeResolver = null, ?DescriptionFactory $descriptionFactory = null, ?TypeContext $context = null ): ?self { + trigger_error( + 'Create using static factory is deprecated, this method should not be called directly + by library consumers', + E_USER_DEPRECATED + ); Assert::stringNotEmpty($body); Assert::notNull($typeResolver); Assert::notNull($descriptionFactory); @@ -186,7 +200,14 @@ public static function create( } } - return new static($methodName, $arguments, $returnType, $static, $description, $returnsReference); + return new static( + $methodName, + $arguments, + $returnType, + $static, + $description, + $returnsReference + ); } /** @@ -198,12 +219,27 @@ public function getMethodName(): string } /** + * @deprecated Method deprecated, use {@see self::getParameters()} + * * @return array> * @phpstan-return array */ public function getArguments(): array { - return $this->arguments; + trigger_error('Method deprecated, use ::getParameters()', E_USER_DEPRECATED); + + return array_map( + static function (MethodParameter $methodParameter) { + return ['name' => $methodParameter->getName(), 'type' => $methodParameter->getType()]; + }, + $this->parameters + ); + } + + /** @return MethodParameter[] */ + public function getParameters(): array + { + return $this->parameters; } /** @@ -229,8 +265,11 @@ public function returnsReference(): bool public function __toString(): string { $arguments = []; - foreach ($this->arguments as $argument) { - $arguments[] = $argument['type'] . ' $' . $argument['name']; + foreach ($this->parameters as $parameter) { + $arguments[] = $parameter->getType() . ' ' . + ($parameter->isReference() ? '&' : '') . + ($parameter->isVariadic() ? '...' : '') . + '$' . $parameter->getName(); } $argumentStr = '(' . implode(', ', $arguments) . ')'; @@ -297,4 +336,28 @@ private static function stripRestArg(string $argument): string return $argument; } + + /** + * @param array{name: string, type: Type} $arguments + * @phpstan-param array $arguments + * + * @return MethodParameter[] + */ + private function fromLegacyArguments(array $arguments): array + { + trigger_error( + 'Create method parameters via legacy format is deprecated add parameters via the constructor', + E_USER_DEPRECATED + ); + + return array_map( + static function ($arg) { + return new MethodParameter( + $arg['name'], + $arg['type'] + ); + }, + $arguments + ); + } } diff --git a/src/DocBlock/Tags/MethodParameter.php b/src/DocBlock/Tags/MethodParameter.php new file mode 100644 index 00000000..15780a6f --- /dev/null +++ b/src/DocBlock/Tags/MethodParameter.php @@ -0,0 +1,72 @@ +type = $type; + $this->isReference = $isReference; + $this->isVariadic = $isVariadic; + $this->name = $name; + $this->defaultValue = $defaultValue; + } + + public function getName(): string + { + return $this->name; + } + + public function getType(): Type + { + return $this->type; + } + + public function isReference(): bool + { + return $this->isReference; + } + + public function isVariadic(): bool + { + return $this->isVariadic; + } + + public function getDefaultValue(): ?string + { + return $this->defaultValue; + } +} diff --git a/src/DocBlock/Tags/Param.php b/src/DocBlock/Tags/Param.php index 3399649b..cb14abeb 100644 --- a/src/DocBlock/Tags/Param.php +++ b/src/DocBlock/Tags/Param.php @@ -26,7 +26,9 @@ use function implode; use function strpos; use function substr; +use function trigger_error; +use const E_USER_DEPRECATED; use const PREG_SPLIT_DELIM_CAPTURE; /** @@ -58,12 +60,21 @@ public function __construct( $this->isReference = $isReference; } + /** + * @deprecated Create using static factory is deprecated, + * this method should not be called directly by library consumers + */ public static function create( string $body, ?TypeResolver $typeResolver = null, ?DescriptionFactory $descriptionFactory = null, ?TypeContext $context = null ): self { + trigger_error( + 'Create using static factory is deprecated, this method should not be called directly + by library consumers', + E_USER_DEPRECATED + ); Assert::stringNotEmpty($body); Assert::notNull($typeResolver); Assert::notNull($descriptionFactory); diff --git a/src/DocBlock/Tags/Property.php b/src/DocBlock/Tags/Property.php index 2521fb3f..b3d9df42 100644 --- a/src/DocBlock/Tags/Property.php +++ b/src/DocBlock/Tags/Property.php @@ -26,7 +26,9 @@ use function implode; use function strpos; use function substr; +use function trigger_error; +use const E_USER_DEPRECATED; use const PREG_SPLIT_DELIM_CAPTURE; /** @@ -47,12 +49,21 @@ public function __construct(?string $variableName, ?Type $type = null, ?Descript $this->description = $description; } + /** + * @deprecated Create using static factory is deprecated, + * this method should not be called directly by library consumers + */ public static function create( string $body, ?TypeResolver $typeResolver = null, ?DescriptionFactory $descriptionFactory = null, ?TypeContext $context = null ): self { + trigger_error( + 'Create using static factory is deprecated, this method should not be called directly + by library consumers', + E_USER_DEPRECATED + ); Assert::stringNotEmpty($body); Assert::notNull($typeResolver); Assert::notNull($descriptionFactory); diff --git a/src/DocBlock/Tags/PropertyRead.php b/src/DocBlock/Tags/PropertyRead.php index 9491b39c..ccffde63 100644 --- a/src/DocBlock/Tags/PropertyRead.php +++ b/src/DocBlock/Tags/PropertyRead.php @@ -26,7 +26,9 @@ use function implode; use function strpos; use function substr; +use function trigger_error; +use const E_USER_DEPRECATED; use const PREG_SPLIT_DELIM_CAPTURE; /** @@ -47,12 +49,21 @@ public function __construct(?string $variableName, ?Type $type = null, ?Descript $this->description = $description; } + /** + * @deprecated Create using static factory is deprecated, + * this method should not be called directly by library consumers + */ public static function create( string $body, ?TypeResolver $typeResolver = null, ?DescriptionFactory $descriptionFactory = null, ?TypeContext $context = null ): self { + trigger_error( + 'Create using static factory is deprecated, this method should not be called directly + by library consumers', + E_USER_DEPRECATED + ); Assert::stringNotEmpty($body); Assert::notNull($typeResolver); Assert::notNull($descriptionFactory); diff --git a/src/DocBlock/Tags/PropertyWrite.php b/src/DocBlock/Tags/PropertyWrite.php index 2bfdac6a..35dac201 100644 --- a/src/DocBlock/Tags/PropertyWrite.php +++ b/src/DocBlock/Tags/PropertyWrite.php @@ -26,7 +26,9 @@ use function implode; use function strpos; use function substr; +use function trigger_error; +use const E_USER_DEPRECATED; use const PREG_SPLIT_DELIM_CAPTURE; /** @@ -47,12 +49,21 @@ public function __construct(?string $variableName, ?Type $type = null, ?Descript $this->description = $description; } + /** + * @deprecated Create using static factory is deprecated, + * this method should not be called directly by library consumers + */ public static function create( string $body, ?TypeResolver $typeResolver = null, ?DescriptionFactory $descriptionFactory = null, ?TypeContext $context = null ): self { + trigger_error( + 'Create using static factory is deprecated, this method should not be called directly + by library consumers', + E_USER_DEPRECATED + ); Assert::stringNotEmpty($body); Assert::notNull($typeResolver); Assert::notNull($descriptionFactory); diff --git a/src/DocBlock/Tags/Return_.php b/src/DocBlock/Tags/Return_.php index f021b609..2a1efb73 100644 --- a/src/DocBlock/Tags/Return_.php +++ b/src/DocBlock/Tags/Return_.php @@ -20,6 +20,10 @@ use phpDocumentor\Reflection\Types\Context as TypeContext; use Webmozart\Assert\Assert; +use function trigger_error; + +use const E_USER_DEPRECATED; + /** * Reflection class for a {@}return tag in a Docblock. */ @@ -32,12 +36,21 @@ public function __construct(Type $type, ?Description $description = null) $this->description = $description; } + /** + * @deprecated Create using static factory is deprecated, + * this method should not be called directly by library consumers + */ public static function create( string $body, ?TypeResolver $typeResolver = null, ?DescriptionFactory $descriptionFactory = null, ?TypeContext $context = null ): self { + trigger_error( + 'Create using static factory is deprecated, this method should not be called directly + by library consumers', + E_USER_DEPRECATED + ); Assert::notNull($typeResolver); Assert::notNull($descriptionFactory); diff --git a/src/DocBlock/Tags/Var_.php b/src/DocBlock/Tags/Var_.php index fa1f9dbf..d8e75b99 100644 --- a/src/DocBlock/Tags/Var_.php +++ b/src/DocBlock/Tags/Var_.php @@ -26,7 +26,9 @@ use function implode; use function strpos; use function substr; +use function trigger_error; +use const E_USER_DEPRECATED; use const PREG_SPLIT_DELIM_CAPTURE; /** @@ -47,12 +49,21 @@ public function __construct(?string $variableName, ?Type $type = null, ?Descript $this->description = $description; } + /** + * @deprecated Create using static factory is deprecated, + * this method should not be called directly by library consumers + */ public static function create( string $body, ?TypeResolver $typeResolver = null, ?DescriptionFactory $descriptionFactory = null, ?TypeContext $context = null ): self { + trigger_error( + 'Create using static factory is deprecated, this method should not be called directly + by library consumers', + E_USER_DEPRECATED + ); Assert::stringNotEmpty($body); Assert::notNull($typeResolver); Assert::notNull($descriptionFactory); diff --git a/src/DocBlockFactory.php b/src/DocBlockFactory.php index 37f72dd2..46eb87f9 100644 --- a/src/DocBlockFactory.php +++ b/src/DocBlockFactory.php @@ -19,6 +19,14 @@ use phpDocumentor\Reflection\DocBlock\StandardTagFactory; use phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\DocBlock\TagFactory; +use phpDocumentor\Reflection\DocBlock\Tags\Factory\AbstractPHPStanFactory; +use phpDocumentor\Reflection\DocBlock\Tags\Factory\Factory; +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\VarFactory; use Webmozart\Assert\Assert; use function array_shift; @@ -38,7 +46,7 @@ final class DocBlockFactory implements DocBlockFactoryInterface /** @var DocBlock\DescriptionFactory */ private $descriptionFactory; - /** @var DocBlock\TagFactory */ + /** @var TagFactory */ private $tagFactory; /** @@ -47,22 +55,39 @@ final class DocBlockFactory implements DocBlockFactoryInterface public function __construct(DescriptionFactory $descriptionFactory, TagFactory $tagFactory) { $this->descriptionFactory = $descriptionFactory; - $this->tagFactory = $tagFactory; + $this->tagFactory = $tagFactory; } /** * Factory method for easy instantiation. * - * @param array> $additionalTags + * @param array|Factory> $additionalTags */ - public static function createInstance(array $additionalTags = []): self + public static function createInstance(array $additionalTags = []): DocBlockFactoryInterface { - $fqsenResolver = new FqsenResolver(); - $tagFactory = new StandardTagFactory($fqsenResolver); + $fqsenResolver = new FqsenResolver(); + $tagFactory = new StandardTagFactory($fqsenResolver); $descriptionFactory = new DescriptionFactory($tagFactory); + $typeResolver = new TypeResolver($fqsenResolver); + + $phpstanTagFactory = new AbstractPHPStanFactory( + new ParamFactory($typeResolver, $descriptionFactory), + new VarFactory($typeResolver, $descriptionFactory), + new ReturnFactory($typeResolver, $descriptionFactory), + new PropertyFactory($typeResolver, $descriptionFactory), + new PropertyReadFactory($typeResolver, $descriptionFactory), + new PropertyWriteFactory($typeResolver, $descriptionFactory), + ); $tagFactory->addService($descriptionFactory); - $tagFactory->addService(new TypeResolver($fqsenResolver)); + $tagFactory->addService($typeResolver); + $tagFactory->registerTagHandler('param', $phpstanTagFactory); + $tagFactory->registerTagHandler('var', $phpstanTagFactory); + $tagFactory->registerTagHandler('return', $phpstanTagFactory); + $tagFactory->registerTagHandler('property', $phpstanTagFactory); + $tagFactory->registerTagHandler('property-read', $phpstanTagFactory); + $tagFactory->registerTagHandler('property-write', $phpstanTagFactory); + $tagFactory->registerTagHandler('method', $phpstanTagFactory); $docBlockFactory = new self($descriptionFactory, $tagFactory); foreach ($additionalTags as $tagName => $tagHandler) { @@ -111,9 +136,9 @@ public function create($docblock, ?Types\Context $context = null, ?Location $loc } /** - * @param class-string $handler + * @param class-string|Factory $handler */ - public function registerTagHandler(string $tagName, string $handler): void + public function registerTagHandler(string $tagName, $handler): void { $this->tagFactory->registerTagHandler($tagName, $handler); } @@ -138,6 +163,7 @@ private function stripDocComment(string $comment): string } // phpcs:disable + /** * Splits the DocBlock into a template marker, summary, description and block of tags. * @@ -149,7 +175,7 @@ private function stripDocComment(string $comment): string * * @author Richard van Velzen (@_richardJ) Special thanks to Richard for the regex responsible for the split. */ - private function splitDocBlock(string $comment) : array + private function splitDocBlock(string $comment): array { // phpcs:enable // Performance improvement cheat: if the first character is an @ then only tags are in this DocBlock. This @@ -227,7 +253,7 @@ private function splitDocBlock(string $comment) : array /** * Creates the tag objects. * - * @param string $tags Tag block to parse. + * @param string $tags Tag block to parse. * @param Types\Context $context Context of the parsed Tag * * @return DocBlock\Tag[] @@ -240,7 +266,7 @@ private function parseTagBlock(string $tags, Types\Context $context): array } $result = []; - $lines = $this->splitTagBlockIntoTagLines($tags); + $lines = $this->splitTagBlockIntoTagLines($tags); foreach ($lines as $key => $tagLine) { $result[$key] = $this->tagFactory->create(trim($tagLine), $context); } diff --git a/src/DocBlockFactoryInterface.php b/src/DocBlockFactoryInterface.php index 9995c0c0..cacc382e 100644 --- a/src/DocBlockFactoryInterface.php +++ b/src/DocBlockFactoryInterface.php @@ -14,7 +14,7 @@ interface DocBlockFactoryInterface * * @param array> $additionalTags */ - public static function createInstance(array $additionalTags = []): DocBlockFactory; + public static function createInstance(array $additionalTags = []): self; /** * @param string|object $docblock diff --git a/tests/integration/InterpretingDocBlocksTest.php b/tests/integration/InterpretingDocBlocksTest.php index 2c8358cf..5ebc29c6 100644 --- a/tests/integration/InterpretingDocBlocksTest.php +++ b/tests/integration/InterpretingDocBlocksTest.php @@ -17,7 +17,11 @@ use phpDocumentor\Reflection\DocBlock\Description; use phpDocumentor\Reflection\DocBlock\StandardTagFactory; use phpDocumentor\Reflection\DocBlock\Tag; +use phpDocumentor\Reflection\DocBlock\Tags\Param; use phpDocumentor\Reflection\DocBlock\Tags\See; +use phpDocumentor\Reflection\Types\Array_; +use phpDocumentor\Reflection\Types\Integer; +use phpDocumentor\Reflection\Types\String_; use PHPUnit\Framework\TestCase; /** @@ -83,7 +87,7 @@ public function testInterpretingASimpleDocBlock(): void str_replace( PHP_EOL, "\n", - $descriptionText + $descriptionText ), $description->render() ); @@ -134,7 +138,7 @@ public function testDescriptionsCanEscapeAtSignsAndClosingBraces(): void str_replace( PHP_EOL, "\n", - <<<'DESCRIPTION' + <<<'DESCRIPTION' You can escape the @-sign by surrounding it with braces, for example: @. And escape a closing brace within an inline tag by adding an opening brace in front of it like this: }. @@ -149,4 +153,36 @@ public function testDescriptionsCanEscapeAtSignsAndClosingBraces(): void $foundDescription ); } + + public function testMultilineTags(): void + { + $docCommment = << \$store + */ +DOC; + $factory = DocBlockFactory::createInstance(); + $docblock = $factory->create($docCommment); + + self::assertEquals( + [ + new Param( + 'store', + new Array_( + new String_(), + new Integer() + ), + false, + new Description(''), + ), + ], + $docblock->getTags() + ); + + } } diff --git a/tests/unit/Assets/CustomTagFactory.php b/tests/unit/Assets/CustomTagFactory.php new file mode 100644 index 00000000..df4a3f69 --- /dev/null +++ b/tests/unit/Assets/CustomTagFactory.php @@ -0,0 +1,31 @@ +class = $class; + + return new Generic('custom'); + } +} diff --git a/tests/unit/DocBlock/StandardTagFactoryTest.php b/tests/unit/DocBlock/StandardTagFactoryTest.php index 2e380d91..5a954d52 100644 --- a/tests/unit/DocBlock/StandardTagFactoryTest.php +++ b/tests/unit/DocBlock/StandardTagFactoryTest.php @@ -18,6 +18,7 @@ use phpDocumentor\Reflection\Assets\CustomParam; use phpDocumentor\Reflection\Assets\CustomServiceClass; use phpDocumentor\Reflection\Assets\CustomServiceInterface; +use phpDocumentor\Reflection\Assets\CustomTagFactory; use phpDocumentor\Reflection\DocBlock\Tags\Author; use phpDocumentor\Reflection\DocBlock\Tags\Formatter; use phpDocumentor\Reflection\DocBlock\Tags\Formatter\PassthroughFormatter; @@ -225,6 +226,22 @@ public function testPassingYourOwnSetOfTagHandlersWithoutComment(): void $this->assertSame('author', $tag->getName()); } + public function testTagWithHandlerObject(): void + { + $fqsenResolver = new FqsenResolver(); + + $customFactory = new CustomTagFactory(); + $injectedClass = new CustomServiceClass(); + + $tagFactory = new StandardTagFactory($fqsenResolver); + $tagFactory->addService($injectedClass); + $tagFactory->registerTagHandler('param', $customFactory); + $tag = $tagFactory->create('@param foo'); + + self::assertSame('custom', $tag->getName()); + self::assertSame($injectedClass, $customFactory->class); + } + /** * @uses \phpDocumentor\Reflection\DocBlock\StandardTagFactory::addService * @uses \phpDocumentor\Reflection\DocBlock\Tags\Author diff --git a/tests/unit/DocBlock/Tags/Factory/MethodFactoryTest.php b/tests/unit/DocBlock/Tags/Factory/MethodFactoryTest.php new file mode 100644 index 00000000..2daad8b3 --- /dev/null +++ b/tests/unit/DocBlock/Tags/Factory/MethodFactoryTest.php @@ -0,0 +1,151 @@ +parseTag($tagLine); + $factory = new MethodFactory($this->giveTypeResolver(), $this->givenDescriptionFactory()); + $context = new Context('global'); + + self::assertTrue($factory->supports($ast, $context)); + self::assertEquals( + $tag, + $factory->create($ast, $context) + ); + } + + /** @return array> */ + public function tagProvider(): array + { + return [ + [ + '@method static string myMethod()', + new Method( + 'myMethod', + [], + new String_(), + true, + new Description(''), + false, + [] + ), + ], + [ + '@method string myMethod()', + new Method( + 'myMethod', + [], + new String_(), + false, + new Description(''), + false, + [] + ), + ], + [ + '@method myMethod()', + new Method( + 'myMethod', + [], + new Void_(), + false, + new Description(''), + false, + [] + ), + ], + [ + '@method myMethod($a)', + new Method( + 'myMethod', + [], + new Void_(), + false, + new Description(''), + false, + [new MethodParameter('a', new Mixed_())] + ), + ], + [ + '@method myMethod($a = 1)', + new Method( + 'myMethod', + [], + new Void_(), + false, + new Description(''), + false, + [new MethodParameter('a', new Mixed_(), false, false, '1')] + ), + ], + [ + '@method myMethod(int $a = 1)', + new Method( + 'myMethod', + [], + new Void_(), + false, + new Description(''), + false, + [new MethodParameter('a', new Integer(), false, false, '1')] + ), + ], + [ + '@method myMethod(int ...$a)', + new Method( + 'myMethod', + [], + new Void_(), + false, + new Description(''), + false, + [new MethodParameter('a', new Integer(), false, true)] + ), + ], + [ + '@method myMethod(int &$a, string $b)', + new Method( + 'myMethod', + [], + new Void_(), + false, + new Description(''), + false, + [ + new MethodParameter('a', new Integer(), true, false), + new MethodParameter('b', new String_(), false, false), + ] + ), + ], + ]; + } +} diff --git a/tests/unit/DocBlock/Tags/Factory/ParamFactoryTest.php b/tests/unit/DocBlock/Tags/Factory/ParamFactoryTest.php new file mode 100644 index 00000000..df2c23cd --- /dev/null +++ b/tests/unit/DocBlock/Tags/Factory/ParamFactoryTest.php @@ -0,0 +1,46 @@ +parseTag('@param string $var'); + $factory = new ParamFactory($this->giveTypeResolver(), $this->givenDescriptionFactory()); + $context = new Context('global'); + + self::assertTrue($factory->supports($ast, $context)); + self::assertEquals( + new Param( + 'var', + new String_(), + false, + new Description(''), + false + ), + $factory->create($ast, $context) + ); + } +} diff --git a/tests/unit/DocBlock/Tags/Factory/PropertyFactoryTest.php b/tests/unit/DocBlock/Tags/Factory/PropertyFactoryTest.php new file mode 100644 index 00000000..738275cb --- /dev/null +++ b/tests/unit/DocBlock/Tags/Factory/PropertyFactoryTest.php @@ -0,0 +1,44 @@ +parseTag('@property string $var'); + $factory = new PropertyFactory($this->giveTypeResolver(), $this->givenDescriptionFactory()); + $context = new Context('global'); + + self::assertTrue($factory->supports($ast, $context)); + self::assertEquals( + new Property( + 'var', + new String_(), + new Description('') + ), + $factory->create($ast, $context) + ); + } +} diff --git a/tests/unit/DocBlock/Tags/Factory/PropertyReadFactoryTest.php b/tests/unit/DocBlock/Tags/Factory/PropertyReadFactoryTest.php new file mode 100644 index 00000000..40fd1226 --- /dev/null +++ b/tests/unit/DocBlock/Tags/Factory/PropertyReadFactoryTest.php @@ -0,0 +1,44 @@ +parseTag('@property-read string $var'); + $factory = new PropertyReadFactory($this->giveTypeResolver(), $this->givenDescriptionFactory()); + $context = new Context('global'); + + self::assertTrue($factory->supports($ast, $context)); + self::assertEquals( + new PropertyRead( + 'var', + new String_(), + new Description('') + ), + $factory->create($ast, $context) + ); + } +} diff --git a/tests/unit/DocBlock/Tags/Factory/PropertyWriteFactoryTest.php b/tests/unit/DocBlock/Tags/Factory/PropertyWriteFactoryTest.php new file mode 100644 index 00000000..0f3e239d --- /dev/null +++ b/tests/unit/DocBlock/Tags/Factory/PropertyWriteFactoryTest.php @@ -0,0 +1,44 @@ +parseTag('@property-write string $var'); + $factory = new PropertyWriteFactory($this->giveTypeResolver(), $this->givenDescriptionFactory()); + $context = new Context('global'); + + self::assertTrue($factory->supports($ast, $context)); + self::assertEquals( + new PropertyWrite( + 'var', + new String_(), + new Description('') + ), + $factory->create($ast, $context) + ); + } +} diff --git a/tests/unit/DocBlock/Tags/Factory/ReturnFactoryTest.php b/tests/unit/DocBlock/Tags/Factory/ReturnFactoryTest.php new file mode 100644 index 00000000..5b664cc3 --- /dev/null +++ b/tests/unit/DocBlock/Tags/Factory/ReturnFactoryTest.php @@ -0,0 +1,43 @@ +parseTag('@return string'); + $factory = new ReturnFactory($this->giveTypeResolver(), $this->givenDescriptionFactory()); + $context = new Context('global'); + + self::assertTrue($factory->supports($ast, $context)); + self::assertEquals( + new Return_( + 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 new file mode 100644 index 00000000..8684ba51 --- /dev/null +++ b/tests/unit/DocBlock/Tags/Factory/TagFactoryTestCase.php @@ -0,0 +1,62 @@ +tokenize($tag); + $constParser = new ConstExprParser(); + + return (new PhpDocParser(new TypeParser($constParser), $constParser))->parseTag(new TokenIterator($tokens)); + } + + public function giveTypeResolver(): TypeResolver + { + return new TypeResolver(new FqsenResolver()); + } + + public function givenDescriptionFactory(): DescriptionFactory + { + $factory = m::mock(DescriptionFactory::class); + $factory->shouldReceive('create')->andReturn(new Description('')); + + return $factory; + } + + /** + * Call Mockery::close after each test. + * + * @after + */ + public function closeMockery(): void + { + m::close(); + } +} diff --git a/tests/unit/DocBlock/Tags/Factory/VarFactoryTest.php b/tests/unit/DocBlock/Tags/Factory/VarFactoryTest.php new file mode 100644 index 00000000..06befbb9 --- /dev/null +++ b/tests/unit/DocBlock/Tags/Factory/VarFactoryTest.php @@ -0,0 +1,44 @@ +parseTag('@var string $var'); + $factory = new VarFactory($this->giveTypeResolver(), $this->givenDescriptionFactory()); + $context = new Context('global'); + + self::assertTrue($factory->supports($ast, $context)); + self::assertEquals( + new Var_( + 'var', + new String_(), + new Description('') + ), + $factory->create($ast, $context) + ); + } +}