Improve replacement of return type for methods from Query\Builder (#1575)

* Add return type `|static` to Query\Builder methods (#1574)

* Replace return type Query\Builder with Eloquent\Builder (#1574)

* Replace return type Query\Builder only for facade Eloquent (#1574)

* Add special return type replacement for Macros of \Eloquent

* Restrict special return type to methods from Eloquent\Builder and Query\Builder

* Do not overwrite return type in normalizeReturn() with conditional call of setType()

* Use generic return type for builder methods in \Eloquent

* composer fix-style

---------

Co-authored-by: laravel-ide-helper <[email protected]>
This commit is contained in:
Carl-Philipp Jung
2024-10-31 15:47:27 +01:00
committed by GitHub
co-authored by laravel-ide-helper
parent 64588afe8b
commit e3ec773374
4 changed files with 135 additions and 23 deletions
+29 -3
View File
@@ -18,6 +18,7 @@ use Barryvdh\Reflection\DocBlock\Tag\MethodTag;
use Closure; use Closure;
use Illuminate\Config\Repository as ConfigRepository; use Illuminate\Config\Repository as ConfigRepository;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\Facade;
use ReflectionClass; use ReflectionClass;
use Throwable; use Throwable;
@@ -333,7 +334,15 @@ class Alias
if (!in_array($magic, $this->usedMethods)) { if (!in_array($magic, $this->usedMethods)) {
if ($class !== $this->root) { if ($class !== $this->root) {
$this->methods[] = new Method($method, $this->alias, $class, $magic, $this->interfaces, $this->classAliases); $this->methods[] = new Method(
$method,
$this->alias,
$class,
$magic,
$this->interfaces,
$this->classAliases,
$this->getReturnTypeNormalizers($class)
);
} }
$this->usedMethods[] = $magic; $this->usedMethods[] = $magic;
} }
@@ -363,7 +372,8 @@ class Alias
$reflection, $reflection,
$method->name, $method->name,
$this->interfaces, $this->interfaces,
$this->classAliases $this->classAliases,
$this->getReturnTypeNormalizers($reflection)
); );
} }
$this->usedMethods[] = $method->name; $this->usedMethods[] = $method->name;
@@ -386,7 +396,8 @@ class Alias
$reflection, $reflection,
$macro_name, $macro_name,
$this->interfaces, $this->interfaces,
$this->classAliases $this->classAliases,
$this->getReturnTypeNormalizers($reflection)
); );
$this->usedMethods[] = $macro_name; $this->usedMethods[] = $macro_name;
} }
@@ -395,6 +406,21 @@ class Alias
} }
} }
/**
* @param ReflectionClass $class
* @return array<string, string>
*/
protected function getReturnTypeNormalizers($class)
{
if ($this->alias === 'Eloquent' && in_array($class->getName(), [EloquentBuilder::class, QueryBuilder::class])) {
return [
'$this' => '\\' . EloquentBuilder::class . ($this->config->get('ide-helper.use_generics_annotations') ? '<static>' : '|static'),
];
}
return [];
}
/** /**
* @param $macro_func * @param $macro_func
* *
+4 -2
View File
@@ -18,6 +18,7 @@ class Macro extends Method
* @param null $methodName * @param null $methodName
* @param array $interfaces * @param array $interfaces
* @param array $classAliases * @param array $classAliases
* @param array $returnTypeNormalizers
*/ */
public function __construct( public function __construct(
$method, $method,
@@ -25,9 +26,10 @@ class Macro extends Method
$class, $class,
$methodName = null, $methodName = null,
$interfaces = [], $interfaces = [],
$classAliases = [] $classAliases = [],
$returnTypeNormalizers = []
) { ) {
parent::__construct($method, $alias, $class, $methodName, $interfaces, $classAliases); parent::__construct($method, $alias, $class, $methodName, $interfaces, $classAliases, $returnTypeNormalizers);
} }
/** /**
+34 -14
View File
@@ -17,8 +17,6 @@ use Barryvdh\Reflection\DocBlock\Serializer as DocBlockSerializer;
use Barryvdh\Reflection\DocBlock\Tag; use Barryvdh\Reflection\DocBlock\Tag;
use Barryvdh\Reflection\DocBlock\Tag\ParamTag; use Barryvdh\Reflection\DocBlock\Tag\ParamTag;
use Barryvdh\Reflection\DocBlock\Tag\ReturnTag; use Barryvdh\Reflection\DocBlock\Tag\ReturnTag;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
class Method class Method
{ {
@@ -39,6 +37,7 @@ class Method
protected $return = null; protected $return = null;
protected $root; protected $root;
protected $classAliases; protected $classAliases;
protected $returnTypeNormalizers;
/** /**
* @param \ReflectionMethod|\ReflectionFunctionAbstract $method * @param \ReflectionMethod|\ReflectionFunctionAbstract $method
@@ -47,12 +46,14 @@ class Method
* @param string|null $methodName * @param string|null $methodName
* @param array $interfaces * @param array $interfaces
* @param array $classAliases * @param array $classAliases
* @param array $returnTypeNormalizers
*/ */
public function __construct($method, $alias, $class, $methodName = null, $interfaces = [], array $classAliases = []) public function __construct($method, $alias, $class, $methodName = null, $interfaces = [], array $classAliases = [], array $returnTypeNormalizers = [])
{ {
$this->method = $method; $this->method = $method;
$this->interfaces = $interfaces; $this->interfaces = $interfaces;
$this->classAliases = $classAliases; $this->classAliases = $classAliases;
$this->returnTypeNormalizers = $returnTypeNormalizers;
$this->name = $methodName ?: $method->name; $this->name = $methodName ?: $method->name;
$this->real_name = $method->isClosure() ? $this->name : $method->name; $this->real_name = $method->isClosure() ? $this->name : $method->name;
$this->initClassDefinedProperties($method, $class); $this->initClassDefinedProperties($method, $class);
@@ -180,6 +181,25 @@ class Method
return $implode ? implode(', ', $this->params) : $this->params; return $implode ? implode(', ', $this->params) : $this->params;
} }
/**
* @param DocBlock|null $phpdoc
* @return ReturnTag|null
*/
public function getReturnTag($phpdoc = null)
{
if ($phpdoc === null) {
$phpdoc = $this->phpdoc;
}
$returnTags = $phpdoc->getTagsByName('return');
if (count($returnTags) === 0) {
return null;
}
return reset($returnTags);
}
/** /**
* Get the parameters for this method including default values * Get the parameters for this method including default values
* *
@@ -248,25 +268,31 @@ class Method
} }
/** /**
* Normalize the return tag (make full namespace, replace interfaces) * Normalize the return tag (make full namespace, replace interfaces, resolve $this)
* *
* @param DocBlock $phpdoc * @param DocBlock $phpdoc
*/ */
protected function normalizeReturn(DocBlock $phpdoc) protected function normalizeReturn(DocBlock $phpdoc)
{ {
//Get the return type and adjust them for better autocomplete //Get the return type and adjust them for better autocomplete
$returnTags = $phpdoc->getTagsByName('return'); $tag = $this->getReturnTag($phpdoc);
if (count($returnTags) === 0) { if ($tag === null) {
$this->return = null; $this->return = null;
return; return;
} }
/** @var ReturnTag $tag */
$tag = reset($returnTags);
// Get the expanded type // Get the expanded type
$returnValue = $tag->getType(); $returnValue = $tag->getType();
if (array_key_exists($returnValue, $this->returnTypeNormalizers)) {
$returnValue = $this->returnTypeNormalizers[$returnValue];
}
if ($returnValue === '$this') {
$returnValue = $this->root;
}
// Replace the interfaces // Replace the interfaces
foreach ($this->interfaces as $interface => $real) { foreach ($this->interfaces as $interface => $real) {
$returnValue = str_replace($interface, $real, $returnValue); $returnValue = str_replace($interface, $real, $returnValue);
@@ -275,12 +301,6 @@ class Method
// Set the changed content // Set the changed content
$tag->setContent($returnValue . ' ' . $tag->getDescription()); $tag->setContent($returnValue . ' ' . $tag->getDescription());
$this->return = $returnValue; $this->return = $returnValue;
if ($tag->getType() === '$this') {
Str::contains($this->root, Builder::class)
? $tag->setType($this->root . '|static')
: $tag->setType($this->root);
}
} }
/** /**
+68 -4
View File
@@ -5,7 +5,8 @@ declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests; namespace Barryvdh\LaravelIdeHelper\Tests;
use Barryvdh\LaravelIdeHelper\Method; use Barryvdh\LaravelIdeHelper\Method;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
class MethodTest extends TestCase class MethodTest extends TestCase
@@ -54,11 +55,11 @@ DOC;
} }
/** /**
* Test the output of a class * Test the output of Illuminate\Database\Eloquent\Builder
*/ */
public function testEloquentBuilderOutput() public function testEloquentBuilderOutput()
{ {
$reflectionClass = new \ReflectionClass(Builder::class); $reflectionClass = new \ReflectionClass(EloquentBuilder::class);
$reflectionMethod = $reflectionClass->getMethod('upsert'); $reflectionMethod = $reflectionClass->getMethod('upsert');
$method = new Method($reflectionMethod, 'Builder', $reflectionClass); $method = new Method($reflectionMethod, 'Builder', $reflectionClass);
@@ -76,12 +77,75 @@ DOC;
DOC; DOC;
$this->assertSame($output, $method->getDocComment('')); $this->assertSame($output, $method->getDocComment(''));
$this->assertSame('upsert', $method->getName()); $this->assertSame('upsert', $method->getName());
$this->assertSame('\\' . Builder::class, $method->getDeclaringClass()); $this->assertSame('\\' . EloquentBuilder::class, $method->getDeclaringClass());
$this->assertSame('$values, $uniqueBy, $update', $method->getParams(true)); $this->assertSame('$values, $uniqueBy, $update', $method->getParams(true));
$this->assertSame(['$values', '$uniqueBy', '$update'], $method->getParams(false)); $this->assertSame(['$values', '$uniqueBy', '$update'], $method->getParams(false));
$this->assertSame('$values, $uniqueBy, $update = null', $method->getParamsWithDefault(true)); $this->assertSame('$values, $uniqueBy, $update = null', $method->getParamsWithDefault(true));
$this->assertSame(['$values', '$uniqueBy', '$update = null'], $method->getParamsWithDefault(false)); $this->assertSame(['$values', '$uniqueBy', '$update = null'], $method->getParamsWithDefault(false));
$this->assertTrue($method->shouldReturn()); $this->assertTrue($method->shouldReturn());
$this->assertSame('int', rtrim($method->getReturnTag()->getType()));
}
/**
* Test normalized return type of Illuminate\Database\Eloquent\Builder
*/
public function testEloquentBuilderNormalizedReturnType()
{
$reflectionClass = new \ReflectionClass(EloquentBuilder::class);
$reflectionMethod = $reflectionClass->getMethod('where');
$method = new Method($reflectionMethod, 'Builder', $reflectionClass, null, [], [], ['$this' => '\\' . EloquentBuilder::class . '<static>']);
$output = <<<'DOC'
/**
* Add a basic where clause to the query.
*
* @param (\Closure(static): mixed)|string|array|\Illuminate\Contracts\Database\Query\Expression $column
* @param mixed $operator
* @param mixed $value
* @param string $boolean
* @return \Illuminate\Database\Eloquent\Builder<static>
* @static
*/
DOC;
$this->assertSame($output, $method->getDocComment(''));
$this->assertSame('where', $method->getName());
$this->assertSame('\\' . EloquentBuilder::class, $method->getDeclaringClass());
$this->assertSame(['$column', '$operator', '$value', '$boolean'], $method->getParams(false));
$this->assertSame(['$column', '$operator = null', '$value = null', "\$boolean = 'and'"], $method->getParamsWithDefault(false));
$this->assertTrue($method->shouldReturn());
$this->assertSame('\Illuminate\Database\Eloquent\Builder<static>', rtrim($method->getReturnTag()->getType()));
}
/**
* Test normalized return type of Illuminate\Database\Query\Builder
*/
public function testQueryBuilderNormalizedReturnType()
{
$reflectionClass = new \ReflectionClass(QueryBuilder::class);
$reflectionMethod = $reflectionClass->getMethod('whereNull');
$method = new Method($reflectionMethod, 'Builder', $reflectionClass, null, [], [], ['$this' => '\\' . EloquentBuilder::class . '<static>']);
$output = <<<'DOC'
/**
* Add a "where null" clause to the query.
*
* @param string|array|\Illuminate\Contracts\Database\Query\Expression $columns
* @param string $boolean
* @param bool $not
* @return \Illuminate\Database\Eloquent\Builder<static>
* @static
*/
DOC;
$this->assertSame($output, $method->getDocComment(''));
$this->assertSame('whereNull', $method->getName());
$this->assertSame('\\' . QueryBuilder::class, $method->getDeclaringClass());
$this->assertSame(['$columns', '$boolean', '$not'], $method->getParams(false));
$this->assertSame(['$columns', "\$boolean = 'and'", '$not = false'], $method->getParamsWithDefault(false));
$this->assertTrue($method->shouldReturn());
$this->assertSame('\Illuminate\Database\Eloquent\Builder<static>', rtrim($method->getReturnTag()->getType()));
} }
/** /**