Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PhpStan based tag parsing #343

Merged
merged 16 commits into from
Nov 11, 2022
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
}
],
"require": {
"php": "^7.2 || ^8.0",
"php": "^7.4 || ^8.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the version drop required ? could we drop less of them ?

Copy link
Member Author

@jaapio jaapio Oct 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would you like to support end-of-life versions of PHP? I think for people using old versions of PHP the older library versions will work.

Tools using this library can easily run on newer versions while the applications being processed can be on old PHP versions.

But I'm open to see if we can support PHP 7.2 as well. however phpstan requires > 7.4

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because that would mean that I also need to drop PHP versions. I am not saying this is bad but if it can be avoided because x.y is supported by the lib you add then let's not drop them ?

"phpdocumentor/type-resolver": "^1.3",
"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",
Expand Down
49 changes: 47 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 20 additions & 10 deletions src/DocBlock/StandardTagFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
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;
Expand Down Expand Up @@ -162,18 +163,23 @@ public function addService(object $service, ?string $alias = null): void
$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;
}

Expand Down Expand Up @@ -210,6 +216,8 @@ private function createTag(string $body, string $name, TypeContext $context): Ta
$this->getServiceLocatorWithDynamicParameters($context, $name, $body)
);

$arguments['tagLine'] = sprintf('@%s %s', $name, $body);

try {
$callable = [$handlerClassName, 'create'];
Assert::isCallable($callable);
Expand All @@ -225,9 +233,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<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])) {
Expand Down Expand Up @@ -275,11 +283,11 @@ private function getArgumentsForParametersFromWiring(array $parameters, array $l

$parameterName = $parameter->getName();
if (isset($locator[$parameterName])) {
$arguments[] = $locator[$parameterName];
$arguments[$parameterName] = $locator[$parameterName];
continue;
}

$arguments[] = null;
$arguments[$parameterName] = null;
}

return $arguments;
Expand All @@ -289,12 +297,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|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();
Expand Down
79 changes: 79 additions & 0 deletions src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php
Original file line number Diff line number Diff line change
@@ -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.
}
}
16 changes: 16 additions & 0 deletions src/DocBlock/Tags/Factory/PHPStanFactory.php
Original file line number Diff line number Diff line change
@@ -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;
}
49 changes: 49 additions & 0 deletions src/DocBlock/Tags/Factory/ParamFactory.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading