This commit is contained in:
Jaapio
2022-10-28 14:05:33 +02:00
parent 7192e67cfa
commit d6c050a533
16 changed files with 734 additions and 272 deletions
Generated
+2 -2
View File
@@ -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": "5ca8bb643a11d17932ef43044dc2f12c",
"content-hash": "99af646bb3e1fa0d77d855f0f5995c9f",
"packages": [
{
"name": "phpdocumentor/reflection-common",
@@ -4053,7 +4053,7 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^7.2 || ^8.0",
"php": "^7.4 || ^8.0",
"ext-filter": "*"
},
"platform-dev": [],
+20 -10
View File
@@ -45,6 +45,7 @@ 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 strpos;
use function trim;
@@ -162,18 +163,23 @@ final class StandardTagFactory implements TagFactory
$this->serviceLocator[$alias ?: get_class($service)] = $service;
}
public function registerTagHandler(string $tagName, string $handler): void
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::implementsInterface($handler, TagFactory::class);
$this->tagHandlerMappings[$tagName] = $handler;
return;
}
Assert::classExists($handler);
Assert::implementsInterface($handler, Tag::class);
$this->tagHandlerMappings[$tagName] = $handler;
}
@@ -210,6 +216,8 @@ final class StandardTagFactory implements TagFactory
$this->getServiceLocatorWithDynamicParameters($context, $name, $body)
);
$arguments['tagLine'] = sprintf('@%s %s', $name, $body);
try {
$callable = [$handlerClassName, 'create'];
Assert::isCallable($callable);
@@ -225,9 +233,9 @@ final class StandardTagFactory implements TagFactory
/**
* Determines the Fully Qualified Class Name of the Factory or Tag (containing a Factory Method `create`).
*
* @return class-string<Tag>
* @return class-string<Tag>|TagFactory
*/
private function findHandlerClassName(string $tagName, TypeContext $context): string
private function findHandlerClassName(string $tagName, TypeContext $context)
{
$handlerClassName = Generic::class;
if (isset($this->tagHandlerMappings[$tagName])) {
@@ -275,11 +283,11 @@ final class StandardTagFactory implements TagFactory
$parameterName = $parameter->getName();
if (isset($locator[$parameterName])) {
$arguments[] = $locator[$parameterName];
$arguments[$parameterName] = $locator[$parameterName];
continue;
}
$arguments[] = null;
$arguments[$parameterName] = null;
}
return $arguments;
@@ -289,12 +297,14 @@ final class StandardTagFactory implements TagFactory
* 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|TagFactory $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();
@@ -0,0 +1,79 @@
<?php
/*
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*
*/
declare(strict_types=1);
namespace phpDocumentor\Reflection\DocBlock\Tags\Factory;
use phpDocumentor\Reflection\DocBlock\Tag;
use phpDocumentor\Reflection\DocBlock\TagFactory;
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\PhpDocParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
/**
* Factory class creating tags using phpstan's parser
*
* This class uses {@see PHPStanFactory} implementations to create tags
* from the ast of the phpstan docblock parser.
*
* @internal This class is not part of the BC promise of this library.
*/
class AbstractPHPStanFactory implements TagFactory
{
private PhpDocParser $parser;
private Lexer $lexer;
private array $factories;
public function __construct(PHPStanFactory ...$factories)
{
$this->lexer = new Lexer();
$constParser = new ConstExprParser();
$this->parser = new PhpDocParser(new TypeParser($constParser), $constParser);
$this->factories = $factories;
}
public function addParameter(string $name, $value): void
{
// TODO: Implement addParameter() method.
}
public function create(string $tagLine, ?TypeContext $context = null): Tag
{
$tokens = $this->lexer->tokenize($tagLine);
$ast = $this->parser->parseTag(new TokenIterator($tokens));
foreach ($this->factories as $factory) {
if ($factory->supports($ast, $context)) {
return $factory->create($ast, $context);
}
}
return InvalidTag::create(
$ast->name,
(string) $ast->value
);
}
public function addService(object $service): void
{
// TODO: Implement addService() method.
}
public function registerTagHandler(string $tagName, string $handler): void
{
// TODO: Implement registerTagHandler() method.
}
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace phpDocumentor\Reflection\DocBlock\Tags\Factory;
use phpDocumentor\Reflection\DocBlock\Tag;
use phpDocumentor\Reflection\Types\Context;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
interface PHPStanFactory
{
public function create(PhpDocTagNode $node, Context $context): Tag;
public function supports(PhpDocTagNode $node, ?Context $context): bool;
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace phpDocumentor\Reflection\DocBlock\Tags\Factory;
use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
use phpDocumentor\Reflection\DocBlock\Tag;
use phpDocumentor\Reflection\DocBlock\Tags\Param;
use phpDocumentor\Reflection\Types\Context;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use Webmozart\Assert\Assert;
use function trim;
/**
* @internal This class is not part of the BC promise of this library.
*/
final class ParamFactory implements PHPStanFactory
{
private TypeFactory $typeFactory;
private DescriptionFactory $descriptionFactory;
public function __construct(TypeFactory $typeFactory, DescriptionFactory $descriptionFactory)
{
$this->typeFactory = $typeFactory;
$this->descriptionFactory = $descriptionFactory;
}
public function create(PhpDocTagNode $node, Context $context): Tag
{
$tagValue = $node->value;
Assert::isInstanceOf($tagValue, ParamTagValueNode::class);
return new Param(
trim($tagValue->parameterName, '$'),
$this->typeFactory->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;
}
}
+169
View File
@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace phpDocumentor\Reflection\DocBlock\Tags\Factory;
use phpDocumentor\Reflection\PseudoTypes\ArrayShape;
use phpDocumentor\Reflection\PseudoTypes\ArrayShapeItem;
use phpDocumentor\Reflection\PseudoTypes\IntegerRange;
use phpDocumentor\Reflection\PseudoTypes\List_;
use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\TypeResolver;
use phpDocumentor\Reflection\Types\Array_;
use phpDocumentor\Reflection\Types\Callable_;
use phpDocumentor\Reflection\Types\ClassString;
use phpDocumentor\Reflection\Types\Collection;
use phpDocumentor\Reflection\Types\Compound;
use phpDocumentor\Reflection\Types\Context;
use phpDocumentor\Reflection\Types\InterfaceString;
use phpDocumentor\Reflection\Types\Intersection;
use phpDocumentor\Reflection\Types\Nullable;
use phpDocumentor\Reflection\Types\This;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use function array_map;
use function array_reverse;
use function get_class;
use function strtolower;
/**
* @internal This class is not part of the BC promise of this library.
*/
class TypeFactory
{
private TypeResolver $resolver;
public function __construct(TypeResolver $resolver)
{
$this->resolver = $resolver;
}
public function createType(TypeNode $type, Context $context): ?Type
{
switch (get_class($type)) {
case ArrayTypeNode::class:
return new Array_(
$this->createType($type->type, $context)
);
case ArrayShapeNode::class:
return new ArrayShape(
...array_map(
fn (ArrayShapeItemNode $item) => new ArrayShapeItem(
(string) $item->keyName,
$this->createType($item->valueType, $context),
$item->optional
),
$type->items
)
);
case CallableTypeNode::class:
return $this->createFromCallable($type, $context);
case ConstTypeNode::class:
case GenericTypeNode::class:
return $this->createFromGeneric($type, $context);
case IdentifierTypeNode::class:
return $this->resolver->resolve($type->name, $context);
case IntersectionTypeNode::class:
return new Intersection(
array_map(
fn (TypeNode $nestedType) => $this->createType($nestedType, $context),
$type->types
)
);
case NullableTypeNode::class:
return new Nullable(
$this->createType($type->type, $context)
);
case UnionTypeNode::class:
return new Compound(
array_map(
fn (TypeNode $nestedType) => $this->createType($nestedType, $context),
$type->types
)
);
case ThisTypeNode::class:
return new This();
case ConditionalTypeNode::class:
case ConditionalTypeForParameterNode::class:
case OffsetAccessTypeNode::class:
default:
return null;
}
}
private function createFromGeneric(GenericTypeNode $type, Context $context): Type
{
switch (strtolower($type->type->name)) {
case 'array':
return new Array_(
...array_reverse(
array_map(
fn (TypeNode $genericType) => $this->createType($genericType, $context),
$type->genericTypes
)
)
);
case 'class-string':
return new ClassString(
$this->createType($type->genericTypes[0], $context)->getFqsen()
);
case 'interface-string':
return new InterfaceString(
$this->createType($type->genericTypes[0], $context)->getFqsen()
);
case 'list':
return new List_(
$this->createType($type->genericTypes[0], $context)
);
case 'int':
return new IntegerRange(
(string) $type->genericTypes[0],
(string) ($type->genericTypes[1] ?? ''),
);
default:
return new Collection(
$this->createType($type->type, $context)->getFqsen(),
...array_reverse(
array_map(
fn (TypeNode $genericType) => $this->createType($genericType, $context),
$type->genericTypes
)
)
);
}
}
private function createFromCallable(CallableTypeNode $type, Context $context): Callable_
{
return new Callable_();
}
}
+19 -8
View File
@@ -19,6 +19,9 @@ use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
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\ParamFactory;
use phpDocumentor\Reflection\DocBlock\Tags\Factory\TypeFactory;
use Webmozart\Assert\Assert;
use function array_shift;
@@ -47,22 +50,29 @@ 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<string, class-string<Tag>> $additionalTags
* @param array<string, class-string<Tag>|TagFactory> $additionalTags
*/
public static function createInstance(array $additionalTags = []): self
{
$fqsenResolver = new FqsenResolver();
$tagFactory = new StandardTagFactory($fqsenResolver);
$fqsenResolver = new FqsenResolver();
$tagFactory = new StandardTagFactory($fqsenResolver);
$descriptionFactory = new DescriptionFactory($tagFactory);
$typeResolver = new TypeResolver($fqsenResolver);
$typeFactory = new TypeFactory($typeResolver);
$phpstanTagFactory = new AbstractPHPStanFactory(
new ParamFactory($typeFactory, $descriptionFactory)
);
$tagFactory->addService($descriptionFactory);
$tagFactory->addService(new TypeResolver($fqsenResolver));
$tagFactory->addService($typeResolver);
$tagFactory->registerTagHandler('param', $phpstanTagFactory);
$docBlockFactory = new self($descriptionFactory, $tagFactory);
foreach ($additionalTags as $tagName => $tagHandler) {
@@ -138,6 +148,7 @@ final class DocBlockFactory implements DocBlockFactoryInterface
}
// phpcs:disable
/**
* Splits the DocBlock into a template marker, summary, description and block of tags.
*
@@ -149,7 +160,7 @@ final class DocBlockFactory implements DocBlockFactoryInterface
*
* @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 +238,7 @@ final class DocBlockFactory implements DocBlockFactoryInterface
/**
* 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 +251,7 @@ final class DocBlockFactory implements DocBlockFactoryInterface
}
$result = [];
$lines = $this->splitTagBlockIntoTagLines($tags);
$lines = $this->splitTagBlockIntoTagLines($tags);
foreach ($lines as $key => $tagLine) {
$result[$key] = $this->tagFactory->create(trim($tagLine), $context);
}
-33
View File
@@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace phpDocumentor\Reflection\PhpStan;
use phpDocumentor\Reflection\DocBlock\Tag;
use phpDocumentor\Reflection\DocBlock\TagFactory as TagFactoryInterface;
use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag;
use phpDocumentor\Reflection\Types\Context as TypeContext;
class TagFactory implements TagFactoryInterface
{
public function addParameter(string $name, $value) : void
{
// TODO: Implement addParameter() method.
}
public function create(string $tagLine, ?TypeContext $context = null) : Tag
{
return InvalidTag::create($tagLine);
}
public function addService(object $service) : void
{
// TODO: Implement addService() method.
}
public function registerTagHandler(string $tagName, string $handler) : void
{
// TODO: Implement registerTagHandler() method.
}
}
-152
View File
@@ -1,152 +0,0 @@
<?php
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*
*/
declare(strict_types=1);
namespace phpDocumentor\Reflection;
use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
use phpDocumentor\Reflection\DocBlock\StandardTagFactory;
use phpDocumentor\Reflection\DocBlock\Tags\Param;
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
use phpDocumentor\Reflection\PhpStan\TagFactory;
use phpDocumentor\Reflection\PseudoTypes\ArrayShape;
use phpDocumentor\Reflection\PseudoTypes\ArrayShapeItem;
use phpDocumentor\Reflection\Types\Context;
use PHPStan\PhpDoc\Tag\ParamTag;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
use Webmozart\Assert\Assert;
use InvalidArgumentException;
use LogicException;
final class PhpStanDocblockFactory implements DocBlockFactoryInterface
{
private PhpDocParser $parser;
private Lexer $lexer;
private DescriptionFactory $descriptionFactory;
private TypeResolver $typeResolver;
private function __construct()
{
}
public static function createInstance(array $additionalTags = []): DocBlockFactoryInterface
{
$fqsenResolver = new FqsenResolver();
$constExprParser = new ConstExprParser();
$self = new self();
$self->lexer = new Lexer();
$self->parser = new PhpDocParser(
new TypeParser($constExprParser),
$constExprParser
);
$self->descriptionFactory = new DescriptionFactory(new TagFactory());
$self->typeResolver = new TypeResolver($fqsenResolver);
return $self;
}
public function create($docblock, ?Types\Context $context = null, ?Location $location = null): DocBlock
{
if (is_object($docblock)) {
if (!method_exists($docblock, 'getDocComment')) {
$exceptionMessage = 'Invalid object passed; the given object must support the getDocComment method';
throw new InvalidArgumentException($exceptionMessage);
}
$docblock = $docblock->getDocComment();
Assert::string($docblock);
}
Assert::stringNotEmpty($docblock);
$tokens = $this->lexer->tokenize($docblock);
$ast = $this->parser->parse(new TokenIterator($tokens));
$textNodes = [];
foreach ($ast->children as $child) {
if ($child instanceof PhpDocTextNode) {
$textNodes[] = $child->text;
continue;
}
//If node is not a text node this is the end of description;
break;
}
$tags = [];
foreach ($ast->getTags() as $node) {
switch ($node->name) {
case '@param':
$tag = $node->value;
$tags[] = new Param(
ltrim($tag->parameterName, '$'),
$this->createType($tag->type, $context),
$tag->isVariadic,
$this->descriptionFactory->create($tag->description),
$tag->isReference
);
break;
case '@return':
$tag = $node->value;
$tags[] = new Return_(
$this->createType($tag->type, $context),
$this->descriptionFactory->create($tag->description)
);
}
}
return new DocBlock(
'',
$this->descriptionFactory->create(
implode("\n", $textNodes)
),
$tags,
null,
$location
);
}
private function createType(TypeNode $type, Context $context)
{
switch (get_class($type)) {
case IdentifierTypeNode::class:
return $this->typeResolver->resolve($type->name, $context);
case ArrayShapeNode::class:
return new ArrayShape(
... array_map(
fn(ArrayShapeItemNode $item) => new ArrayShapeItem(
(string) $item->keyName,
$this->createType($item->valueType, $context),
$item->optional
),
$type->items
)
);
default:
return null;
}
}
}
+3 -1
View File
@@ -10,6 +10,8 @@ use phpDocumentor\Reflection\Types\Array_;
use phpDocumentor\Reflection\Types\ArrayKey;
use phpDocumentor\Reflection\Types\Mixed_;
use function implode;
class ArrayShape implements PseudoType
{
/** @var ArrayShapeItem[] */
@@ -20,7 +22,7 @@ class ArrayShape implements PseudoType
$this->items = $items;
}
public function underlyingType() : Type
public function underlyingType(): Type
{
return new Array_(new Mixed_(), new ArrayKey());
}
+5 -3
View File
@@ -6,6 +6,8 @@ namespace phpDocumentor\Reflection\PseudoTypes;
use phpDocumentor\Reflection\Type;
use function sprintf;
final class ArrayShapeItem
{
private ?string $key;
@@ -19,17 +21,17 @@ final class ArrayShapeItem
$this->optional = $optional;
}
public function getKey() : ?string
public function getKey(): ?string
{
return $this->key;
}
public function getValue() : Type
public function getValue(): Type
{
return $this->value;
}
public function isOptional() : bool
public function isOptional(): bool
{
return $this->optional;
}
@@ -17,7 +17,11 @@ use Mockery as m;
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 @@ DESCRIPTION;
str_replace(
PHP_EOL,
"\n",
$descriptionText
$descriptionText
),
$description->render()
);
@@ -134,7 +138,7 @@ DESCRIPTION;
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 @@ DESCRIPTION
$foundDescription
);
}
public function testMultilineTags(): void
{
$docCommment = <<<DOC
/**
* This is an example of a summary.
*
* @param array<
* int,
* string
* > \$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()
);
}
}
@@ -1,61 +0,0 @@
<?php
/*
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*
*/
declare(strict_types=1);
namespace phpDocumentor\Reflection;
use phpDocumentor\Reflection\DocBlock\Description;
use phpDocumentor\Reflection\DocBlock\Tags\Param;
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
use phpDocumentor\Reflection\PseudoTypes\ArrayShape;
use phpDocumentor\Reflection\PseudoTypes\ArrayShapeItem;
use phpDocumentor\Reflection\Types\Context;
use phpDocumentor\Reflection\Types\String_;
use PHPUnit\Framework\TestCase;
class PhpStanDocblockFactoryTest extends TestCase
{
public function testDocblockIsParsed()
{
$docblock = new DocBlock(
'test summary',
new Description(
"This description contains tags \n And is multiline"
),
[
new Param('firstParam', new String_(), false, new Description('Some description'), false),
new Return_(new ArrayShape(new ArrayShapeItem('foo', new String_(), false)))
]
);
$string = <<<DOC
/**
* test summary
*
* THis description contains tags {@see https://phpdoc.org with a description}
* And is multi line
*
* @param string \$firstParam Some description
* @return array{foo: string}
*/
DOC;
$factory = PhpStanDocblockFactory::createInstance();
$actual = $factory->create(
$string,
new Context('/')
);
self::assertEquals($docblock, $actual);
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
/*
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*
*/
declare(strict_types=1);
namespace phpDocumentor\Reflection\Assets;
use phpDocumentor\Reflection\DocBlock\Tag;
use phpDocumentor\Reflection\DocBlock\TagFactory;
use phpDocumentor\Reflection\DocBlock\Tags\Generic;
use phpDocumentor\Reflection\Types\Context;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
class CustomTagFactory implements TagFactory
{
public $class;
public function addParameter(string $name, $value): void
{
// TODO: Implement addParameter() method.
}
public function create(string $tagLine, ?Context $context = null, CustomServiceClass $class = null): Tag
{
$this->class = $class;
return new Generic('custom');
}
public function addService(object $service): void
{
// TODO: Implement addService() method.
}
public function registerTagHandler(string $tagName, string $handler): void
{
// TODO: Implement registerTagHandler() method.
}
}
@@ -18,6 +18,7 @@ use Mockery as m;
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 @@ class StandardTagFactoryTest extends TestCase
$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
@@ -0,0 +1,270 @@
<?php
declare(strict_types=1);
namespace phpDocumentor\Reflection\DocBlock\Tags\Factory;
use phpDocumentor\Reflection\Fqsen;
use phpDocumentor\Reflection\FqsenResolver;
use phpDocumentor\Reflection\PseudoTypes\IntegerRange;
use phpDocumentor\Reflection\PseudoTypes\List_;
use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\TypeResolver;
use phpDocumentor\Reflection\Types\Array_;
use phpDocumentor\Reflection\Types\ArrayKey;
use phpDocumentor\Reflection\Types\Callable_;
use phpDocumentor\Reflection\Types\ClassString;
use phpDocumentor\Reflection\Types\Collection;
use phpDocumentor\Reflection\Types\Compound;
use phpDocumentor\Reflection\Types\Context;
use phpDocumentor\Reflection\Types\Float_;
use phpDocumentor\Reflection\Types\Integer;
use phpDocumentor\Reflection\Types\InterfaceString;
use phpDocumentor\Reflection\Types\Intersection;
use phpDocumentor\Reflection\Types\Nullable;
use phpDocumentor\Reflection\Types\Object_;
use phpDocumentor\Reflection\Types\Self_;
use phpDocumentor\Reflection\Types\String_;
use phpDocumentor\Reflection\Types\This;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
use PHPUnit\Framework\TestCase;
final class TypeFactoryTest extends TestCase
{
/**
* @covers \phpDocumentor\Reflection\DocBlock\Tags\Factory\TypeFactory::createType
* @covers \phpDocumentor\Reflection\DocBlock\Tags\Factory\TypeFactory::createFromGeneric
* @covers \phpDocumentor\Reflection\DocBlock\Tags\Factory\TypeFactory::createFromCallable
* @dataProvider typeProvider
* @dataProvider genericsProvider
* @dataProvider callableProvider
*/
public function testTypeBuilding(string $type, Type $expected): void
{
$lexer = new Lexer();
$tokens = $lexer->tokenize($type);
$constParser = new ConstExprParser();
$parser = new TypeParser($constParser);
$ast = $parser->parse(new TokenIterator($tokens));
$factory = new TypeFactory(new TypeResolver(new FqsenResolver()));
$actual = $factory->createType($ast, new Context('phpDocumentor'));
self::assertEquals($expected, $actual);
}
public function typeProvider(): array
{
return [
[
'string',
new String_(),
],
[
'( string )',
new String_(),
],
[
'\\Foo\Bar\\Baz',
new Object_(new Fqsen('\\Foo\Bar\\Baz')),
],
[
'string|int',
new Compound(
[
new String_(),
new Integer(),
]
),
],
[
'string&int',
new Intersection(
[
new String_(),
new Integer(),
]
),
],
[
'string & (int | float)',
new Intersection(
[
new String_(),
new Compound(
[
new Integer(),
new Float_(),
]
),
]
),
],
[
'string[]',
new Array_(
new String_()
),
],
[
'$this',
new This(),
],
[
'?int',
new Nullable(
new Integer()
),
],
[
'self',
new Self_(),
],
];
}
public function genericsProvider(): array
{
return [
[
'array<int, Foo\\Bar>',
new Array_(
new Object_(new Fqsen('\\phpDocumentor\\Foo\\Bar')),
new Integer()
),
],
[
'Collection<array-key, int>[]',
new Array_(
new Collection(
new Fqsen('\\phpDocumentor\\Collection'),
new Integer(),
new ArrayKey()
)
),
],
[
'class-string',
new ClassString(null),
],
[
'class-string<Foo>',
new ClassString(new Fqsen('\\phpDocumentor\\Foo')),
],
[
'interface-string<Foo>',
new InterfaceString(new Fqsen('\\phpDocumentor\\Foo')),
],
[
'List<Foo>',
new List_(new Object_(new Fqsen('\\phpDocumentor\\Foo'))),
],
[
'int<1, 100>',
new IntegerRange('1', '100'),
],
];
}
public function callableProvider(): array
{
return [
[
'callable',
new Callable_(),
],
[
'callable()',
new Callable_(),
],
[
'callable(): Foo',
new Callable_(),
],
[
'callable(): (Foo&Bar)',
new Callable_(),
],
[
'callable(A&...$a=, B&...=, C): Foo',
new Callable_(),
],
];
}
public function constExpressions(): array
{
return [
['Foo::FOO_CONSTANT'],
[
'123',
//new ConstTypeNode(new ConstExprIntegerNode('123')),
],
[
'123.2',
//new ConstTypeNode(new ConstExprFloatNode('123.2')),
],
[
'"bar"',
//new ConstTypeNode(new ConstExprStringNode('bar')),
],
[
'Foo::FOO_*',
//new ConstTypeNode(new ConstFetchNode('Foo', 'FOO_*')),
],
[
'Foo::FOO_*BAR',
//new ConstTypeNode(new ConstFetchNode('Foo', 'FOO_*BAR')),
],
[
'Foo::*FOO*',
//new ConstTypeNode(new ConstFetchNode('Foo', '*FOO*')),
],
[
'Foo::A*B*C',
//new ConstTypeNode(new ConstFetchNode('Foo', 'A*B*C')),
],
[
'self::*BAR',
//new ConstTypeNode(new ConstFetchNode('self', '*BAR')),
],
[
'Foo::*',
//new ConstTypeNode(new ConstFetchNode('Foo', '*')),
],
[
'Foo::**',
//new ConstTypeNode(new ConstFetchNode('Foo', '*')), // fails later in PhpDocParser
//Lexer::TOKEN_WILDCARD,
],
[
'Foo::*a',
//new ConstTypeNode(new ConstFetchNode('Foo', '*a')),
],
[
'( "foo" | Foo::FOO_* )',
// new UnionTypeNode([
// new ConstTypeNode(new ConstExprStringNode('foo')),
// new ConstTypeNode(new ConstFetchNode('Foo', 'FOO_*')),
// ]),
],
[
'DateTimeImmutable::*|DateTime::*',
// new UnionTypeNode([
// new ConstTypeNode(new ConstFetchNode('DateTimeImmutable', '*')),
// new ConstTypeNode(new ConstFetchNode('DateTime', '*')),
// ]),
],
[
'ParameterTier::*|null',
// new UnionTypeNode([
// new ConstTypeNode(new ConstFetchNode('ParameterTier', '*')),
// new IdentifierTypeNode('null'),
// ]),
],
];
}
}