From 0bbca8f5e2a405d643fffaa113dcea714136ec7a Mon Sep 17 00:00:00 2001 From: Nereo Berardozzi Date: Tue, 31 Dec 2024 17:18:28 +0100 Subject: [PATCH] Fixed generating PHPDoc for methods with class templates (#1647) * Fixed generating PHPDoc for methods with class templates * Fix style * Use getter for alias template names * Fixed missing class in static analysis --- src/Alias.php | 53 ++++++++++++++++++++++++++++++++++---------- src/Method.php | 11 ++++++--- tests/AliasTest.php | 16 +++++++++++++ tests/MethodTest.php | 23 +++++++++++++++++++ 4 files changed, 88 insertions(+), 15 deletions(-) diff --git a/src/Alias.php b/src/Alias.php index 9103a5875..7ca59c583 100644 --- a/src/Alias.php +++ b/src/Alias.php @@ -16,6 +16,7 @@ use Barryvdh\Reflection\DocBlock\ContextFactory; use Barryvdh\Reflection\DocBlock\Serializer as DocBlockSerializer; use Barryvdh\Reflection\DocBlock\Tag\MethodTag; +use Barryvdh\Reflection\DocBlock\Tag\TemplateTag; use Closure; use Illuminate\Config\Repository as ConfigRepository; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; @@ -48,6 +49,9 @@ class Alias /** @var ConfigRepository */ protected $config; + /** @var string[] */ + protected $templateNames; + /** * @param ConfigRepository $config * @param string $alias @@ -347,7 +351,8 @@ protected function addMagicMethods() $magic, $this->interfaces, $this->classAliases, - $this->getReturnTypeNormalizers($class) + $this->getReturnTypeNormalizers($class), + $this->getTemplateNames() ); } $this->usedMethods[] = $magic; @@ -379,7 +384,8 @@ protected function detectMethods() $method->name, $this->interfaces, $this->classAliases, - $this->getReturnTypeNormalizers($reflection) + $this->getReturnTypeNormalizers($reflection), + $this->getTemplateNames(), ); } $this->usedMethods[] = $method->name; @@ -477,39 +483,62 @@ public function getDocComment($prefix = "\t\t") /** * @param $prefix * @return string - * @throws \ReflectionException */ public function getPhpDocTemplates($prefix = "\t\t") { $templateDoc = new DocBlock(''); $serializer = new DocBlockSerializer(1, $prefix); + foreach ($this->getTemplateNames() as $templateName) { + $template = new TemplateTag('template', $templateName); + $template->setBound('static'); + $template->setDocBlock($templateDoc); + $templateDoc->appendTag($template); + } + + return $serializer->getDocComment($templateDoc); + } + + /** + * @return string[] + */ + public function getTemplateNames() + { + if (!isset($this->templateNames)) { + $this->detectTemplateNames(); + } + return $this->templateNames; + } + + /** + * @return void + * @throws \ReflectionException + */ + protected function detectTemplateNames() + { + $templateNames = []; foreach ($this->classes as $class) { $reflection = new ReflectionClass($class); $traits = collect($reflection->getTraitNames()); $phpdoc = new DocBlock($reflection); $templates = $phpdoc->getTagsByName('template'); - /** @var DocBlock\Tag\TemplateTag $template */ + /** @var TemplateTag $template */ foreach ($templates as $template) { - $template->setBound('static'); - $template->setDocBlock($templateDoc); - $templateDoc->appendTag($template); + $templateNames[] = $template->getTemplateName(); } foreach ($traits as $trait) { $phpdoc = new DocBlock(new ReflectionClass($trait)); $templates = $phpdoc->getTagsByName('template'); - /** @var DocBlock\Tag\TemplateTag $template */ + /** @var TemplateTag $template */ foreach ($templates as $template) { - $template->setBound('static'); - $template->setDocBlock($templateDoc); - $templateDoc->appendTag($template); + $templateNames[] = $template->getTemplateName(); } } } - return $serializer->getDocComment($templateDoc); + $this->templateNames = $templateNames; } /** diff --git a/src/Method.php b/src/Method.php index b4e0f7003..9469ed2d3 100644 --- a/src/Method.php +++ b/src/Method.php @@ -39,6 +39,9 @@ class Method protected $classAliases; protected $returnTypeNormalizers; + /** @var string[] */ + protected $templateNames = []; + /** * @param \ReflectionMethod|\ReflectionFunctionAbstract $method * @param string $alias @@ -47,8 +50,9 @@ class Method * @param array $interfaces * @param array $classAliases * @param array $returnTypeNormalizers + * @param string[] $templateNames */ - public function __construct($method, $alias, $class, $methodName = null, $interfaces = [], array $classAliases = [], array $returnTypeNormalizers = []) + public function __construct($method, $alias, $class, $methodName = null, $interfaces = [], array $classAliases = [], array $returnTypeNormalizers = [], array $templateNames = []) { $this->method = $method; $this->interfaces = $interfaces; @@ -56,6 +60,7 @@ public function __construct($method, $alias, $class, $methodName = null, $interf $this->returnTypeNormalizers = $returnTypeNormalizers; $this->name = $methodName ?: $method->name; $this->real_name = $method->isClosure() ? $this->name : $method->name; + $this->templateNames = $templateNames; $this->initClassDefinedProperties($method, $class); //Reference the 'real' function in the declaring class @@ -84,7 +89,7 @@ public function __construct($method, $alias, $class, $methodName = null, $interf */ protected function initPhpDoc($method) { - $this->phpdoc = new DocBlock($method, new Context($this->namespace, $this->classAliases)); + $this->phpdoc = new DocBlock($method, new Context($this->namespace, $this->classAliases, generics: $this->templateNames)); } /** @@ -386,7 +391,7 @@ protected function getInheritDoc($reflectionMethod) } if ($method) { $namespace = $method->getDeclaringClass()->getNamespaceName(); - $phpdoc = new DocBlock($method, new Context($namespace, $this->classAliases)); + $phpdoc = new DocBlock($method, new Context($namespace, $this->classAliases, generics: $this->templateNames)); if (strpos($phpdoc->getText(), '{@inheritdoc}') !== false) { //Not at the end yet, try another parent/interface.. diff --git a/tests/AliasTest.php b/tests/AliasTest.php index 39d221c7e..e0aa43373 100644 --- a/tests/AliasTest.php +++ b/tests/AliasTest.php @@ -66,6 +66,21 @@ function () { $this->assertNotNull($this->getAliasMacro($alias, EloquentBuilder::class, $macro)); } + /** + * @covers ::detectTemplateNames + */ + public function testTemplateNamesAreDetected(): void + { + // Mock + $alias = new AliasMock(); + + // Prepare + $alias->setClasses([EloquentBuilder::class]); + + // Test + $this->assertSame(['TModel', 'TValue'], $alias->getTemplateNames()); + } + protected function getAliasMacro(Alias $alias, string $class, string $method): ?Macro { return Arr::first( @@ -82,6 +97,7 @@ function ($macro) use ($class, $method) { /** * @internal * @noinspection PhpMultipleClassesDeclarationsInOneFile + * @template TValue */ class AliasMock extends Alias { diff --git a/tests/MethodTest.php b/tests/MethodTest.php index c1681da62..e48a9af86 100644 --- a/tests/MethodTest.php +++ b/tests/MethodTest.php @@ -193,6 +193,29 @@ public function testClassAliases() $this->assertSame([], $method->getParamsWithDefault(false)); $this->assertTrue($method->shouldReturn()); } + + public function testEloquentBuilderWithTemplates() + { + $reflectionClass = new \ReflectionClass(EloquentBuilder::class); + $reflectionMethod = $reflectionClass->getMethod('firstOr'); + + $method = new Method($reflectionMethod, 'Builder', $reflectionClass, null, [], [], [], ['TModel']); + + $output = <<<'DOC' +/** + * Execute the query and get the first result or call a callback. + * + * @template TValue + * @param (\Closure(): TValue)|list $columns + * @param (\Closure(): TValue)|null $callback + * @return TModel|TValue + * @static + */ +DOC; + $this->assertSame($output, $method->getDocComment('')); + $this->assertSame('firstOr', $method->getName()); + $this->assertSame('\\' . EloquentBuilder::class, $method->getDeclaringClass()); + } } class ExampleClass