From 7192e67cfa536867249d390a555692efa6db4807 Mon Sep 17 00:00:00 2001 From: Jaapio Date: Fri, 14 Oct 2022 15:26:54 +0200 Subject: [PATCH] First POC steps --- composer.json | 5 +- composer.lock | 47 +++++- src/DocBlockFactoryInterface.php | 2 +- src/PhpStan/TagFactory.php | 33 ++++ src/PhpStanDocblockFactory.php | 152 ++++++++++++++++++ src/PseudoTypes/ArrayShape.php | 32 ++++ src/PseudoTypes/ArrayShapeItem.php | 50 ++++++ .../PhpStanDocblockFactoryTest.php | 61 +++++++ 8 files changed, 378 insertions(+), 4 deletions(-) create mode 100644 src/PhpStan/TagFactory.php create mode 100644 src/PhpStanDocblockFactory.php create mode 100644 src/PseudoTypes/ArrayShape.php create mode 100644 src/PseudoTypes/ArrayShapeItem.php create mode 100644 tests/integration/PhpStanDocblockFactoryTest.php diff --git a/composer.json b/composer.json index 7d76f16..8a91cb2 100644 --- a/composer.json +++ b/composer.json @@ -14,11 +14,12 @@ } ], "require": { - "php": "^7.2 || ^8.0", + "php": "^7.4 || ^8.0", "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", diff --git a/composer.lock b/composer.lock index c4be8e9..852e5c4 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": "5ca8bb643a11d17932ef43044dc2f12c", "packages": [ { "name": "phpdocumentor/reflection-common", @@ -109,6 +109,51 @@ }, "time": "2022-03-15T21:29:03+00:00" }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "367a8d9d5f7da2a0136422d27ce8840583926955" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/367a8d9d5f7da2a0136422d27ce8840583926955", + "reference": "367a8d9d5f7da2a0136422d27ce8840583926955", + "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.7.0" + }, + "time": "2022-08-09T12:23:23+00:00" + }, { "name": "webmozart/assert", "version": "1.11.0", diff --git a/src/DocBlockFactoryInterface.php b/src/DocBlockFactoryInterface.php index 9995c0c..cacc382 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/src/PhpStan/TagFactory.php b/src/PhpStan/TagFactory.php new file mode 100644 index 0000000..4215a2a --- /dev/null +++ b/src/PhpStan/TagFactory.php @@ -0,0 +1,33 @@ +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; + } + } +} diff --git a/src/PseudoTypes/ArrayShape.php b/src/PseudoTypes/ArrayShape.php new file mode 100644 index 0000000..e1cf57c --- /dev/null +++ b/src/PseudoTypes/ArrayShape.php @@ -0,0 +1,32 @@ +items = $items; + } + + public function underlyingType() : Type + { + return new Array_(new Mixed_(), new ArrayKey()); + } + + public function __toString(): string + { + return 'array{' . implode(', ', $this->items) . '}'; + } +} diff --git a/src/PseudoTypes/ArrayShapeItem.php b/src/PseudoTypes/ArrayShapeItem.php new file mode 100644 index 0000000..282b45a --- /dev/null +++ b/src/PseudoTypes/ArrayShapeItem.php @@ -0,0 +1,50 @@ +key = $key; + $this->value = $value; + $this->optional = $optional; + } + + public function getKey() : ?string + { + return $this->key; + } + + public function getValue() : Type + { + return $this->value; + } + + public function isOptional() : bool + { + return $this->optional; + } + + public function __toString() + { + if ($this->key !== null) { + return sprintf( + '%s%s: %s', + $this->key, + $this->optional ? '?' : '', + $this->value + ); + } + + return (string) $this->value; + } +} diff --git a/tests/integration/PhpStanDocblockFactoryTest.php b/tests/integration/PhpStanDocblockFactoryTest.php new file mode 100644 index 0000000..a6aa80a --- /dev/null +++ b/tests/integration/PhpStanDocblockFactoryTest.php @@ -0,0 +1,61 @@ +create( + $string, + new Context('/') + ); + + self::assertEquals($docblock, $actual); + } +}