From 1cb0b2dc82c12fb8a4f4dabef0f978aa44779252 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Mon, 24 Nov 2025 03:50:52 +0000 Subject: [PATCH 1/4] Fixes PHPStan error in ApplicationAnalyzer --- src/NodeAnalyzer/ApplicationAnalyzer.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/NodeAnalyzer/ApplicationAnalyzer.php b/src/NodeAnalyzer/ApplicationAnalyzer.php index 8ca69b9b..0288136c 100644 --- a/src/NodeAnalyzer/ApplicationAnalyzer.php +++ b/src/NodeAnalyzer/ApplicationAnalyzer.php @@ -9,6 +9,9 @@ class ApplicationAnalyzer { private ?string $version = null; + /** + * @param class-string $applicationClass + */ public function __construct( private string $applicationClass = 'Illuminate\Foundation\Application', ) {} @@ -20,6 +23,10 @@ public function setVersion(?string $version): static return $this; } + /** + * @param class-string $applicationClass + * @return $this + */ public function setApplicationClass(string $applicationClass): static { $this->applicationClass = $applicationClass; @@ -27,6 +34,9 @@ public function setApplicationClass(string $applicationClass): static return $this; } + /** + * @return class-string + */ public function getApplicationClass(): string { return $this->applicationClass; From 16c9b5e1ff380db6dc47cc012e215cc6bdeeb082 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Mon, 24 Nov 2025 04:32:08 +0000 Subject: [PATCH 2/4] Improve first rule --- ...hereRelationTypeHintingParameterRector.php | 35 +++++++++++++------ .../Database/Eloquent/Relations/Relation.php | 4 ++- .../Fixture/fixture3.php.inc | 2 +- .../Fixture/fixture4.php.inc | 23 ++++++++++++ .../Source/FooModel.php | 9 ++++- 5 files changed, 60 insertions(+), 13 deletions(-) create mode 100644 tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture4.php.inc diff --git a/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php b/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php index b52393b6..0e79c24c 100644 --- a/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php +++ b/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php @@ -11,6 +11,8 @@ use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified; +use PHPStan\Type\ObjectType; +use PHPStan\Type\Type; use RectorLaravel\AbstractRector; use RectorLaravel\NodeAnalyzer\QueryBuilderAnalyzer; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; @@ -72,15 +74,19 @@ public function getNodeTypes(): array return [MethodCall::class, StaticCall::class]; } - public function refactor(Node $node): ?Node + /** + * @param MethodCall|StaticCall $node + */ + public function refactor(Node $node): MethodCall|StaticCall|null { - if (! $node instanceof MethodCall && ! $node instanceof StaticCall) { - return null; - } + $type = new ObjectType('Illuminate\Database\Eloquent\Builder'); - if ($this->isWhereRelationMethodWithClosureOrArrowFunction($node)) { - $this->changeClosureParamType($node); + if ($node instanceof MethodCall) { + $type = $this->getType($node->var); + } + /** @phpstan-ignore argument.type */ + if ($this->isWhereRelationMethodWithClosureOrArrowFunction($node) && $this->changeClosureParamType($node, $type)) { return $node; } @@ -103,7 +109,10 @@ private function isWhereRelationMethodWithClosureOrArrowFunction(MethodCall|Stat ! ($node->getArgs()[$position]->value ?? null) instanceof ArrowFunction); } - private function changeClosureParamType(MethodCall|StaticCall $node): void + /** + * @param ObjectType $type + */ + private function changeClosureParamType(MethodCall|StaticCall $node, Type $type): bool { // Morph methods have the closure in the 3rd position, others use the 2nd. $position = $this->isNames( @@ -115,16 +124,22 @@ private function changeClosureParamType(MethodCall|StaticCall $node): void $closure = $node->getArgs()[$position]->value; if (! isset($closure->getParams()[0])) { - return; + return false; } $param = $closure->getParams()[0]; if ($param->type instanceof Name) { - return; + return false; + } + + if ($type->isObject()->no()) { + return false; } - $param->type = new FullyQualified('Illuminate\Contracts\Database\Query\Builder'); + $param->type = new FullyQualified($type->getClassName()); + + return true; } private function expectedObjectTypeAndMethodCall(MethodCall|StaticCall $node): bool diff --git a/stubs/Illuminate/Database/Eloquent/Relations/Relation.php b/stubs/Illuminate/Database/Eloquent/Relations/Relation.php index b5ded38c..2d35fbce 100644 --- a/stubs/Illuminate/Database/Eloquent/Relations/Relation.php +++ b/stubs/Illuminate/Database/Eloquent/Relations/Relation.php @@ -2,8 +2,10 @@ namespace Illuminate\Database\Eloquent\Relations; +use Illuminate\Contracts\Database\Eloquent\Builder; + if (class_exists('Illuminate\Database\Eloquent\Relations\Relation')) { return; } -abstract class Relation {} +abstract class Relation implements Builder {} diff --git a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc index 8b5024f8..4f8319ab 100644 --- a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc +++ b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc @@ -16,7 +16,7 @@ namespace RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHinting use RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector\Source\FooModel; -FooModel::whereHas('posts', function (\Illuminate\Contracts\Database\Query\Builder $query) { +FooModel::whereHas('posts', function (\Illuminate\Database\Eloquent\Builder $query) { $query->where('is_published', true); }); diff --git a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture4.php.inc b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture4.php.inc new file mode 100644 index 00000000..1b9edda1 --- /dev/null +++ b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture4.php.inc @@ -0,0 +1,23 @@ +relation()->whereHas('posts', function ($query) { + $query->where('is_published', true); +}); + +?> +----- +relation()->whereHas('posts', function (\Illuminate\Database\Eloquent\Relations\HasMany $query) { + $query->where('is_published', true); +}); + +?> diff --git a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Source/FooModel.php b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Source/FooModel.php index c07f28b8..b21c743f 100644 --- a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Source/FooModel.php +++ b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Source/FooModel.php @@ -3,5 +3,12 @@ namespace RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector\Source; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; -class FooModel extends Model {} +class FooModel extends Model +{ + public function relation(): HasMany + { + return new HasMany; + } +} From 2c057d144768910b99708b599f1af9a873c7adcf Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Mon, 24 Nov 2025 04:40:56 +0000 Subject: [PATCH 3/4] second rule + improvements --- ...hereRelationTypeHintingParameterRector.php | 2 +- ...entWhereTypeHintClosureParameterRector.php | 36 +++++++++++-------- .../Fixture/fixture3.php.inc | 2 +- .../Fixture/fixture4.php.inc | 25 +++++++++++++ .../Source/FooModel.php | 8 ++++- 5 files changed, 55 insertions(+), 18 deletions(-) create mode 100644 tests/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector/Fixture/fixture4.php.inc diff --git a/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php b/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php index 0e79c24c..3aaadd64 100644 --- a/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php +++ b/src/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector.php @@ -79,7 +79,7 @@ public function getNodeTypes(): array */ public function refactor(Node $node): MethodCall|StaticCall|null { - $type = new ObjectType('Illuminate\Database\Eloquent\Builder'); + $type = new ObjectType('Illuminate\Contracts\Database\Eloquent\Builder'); if ($node instanceof MethodCall) { $type = $this->getType($node->var); diff --git a/src/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector.php b/src/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector.php index 413ce7fa..d2ce22b8 100644 --- a/src/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector.php +++ b/src/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector.php @@ -16,6 +16,7 @@ use RectorLaravel\NodeAnalyzer\QueryBuilderAnalyzer; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; +use PHPStan\Type\Type; /** * @see \RectorLaravel\Tests\Rector\MethodCall\EloquentWhereTypeHintClosureParameterRector\EloquentWhereTypeHintClosureParameterRectorTest @@ -70,15 +71,19 @@ public function getNodeTypes(): array return [MethodCall::class, StaticCall::class]; } + /** + * @param StaticCall|MethodCall $node + * @return Node|null + */ public function refactor(Node $node): ?Node { - if (! $node instanceof MethodCall && ! $node instanceof StaticCall) { - return null; - } + $type = new ObjectType('Illuminate\Contracts\Database\Eloquent\Builder'); - if ($this->isWhereMethodWithClosureOrArrowFunction($node)) { - $this->changeClosureParamType($node); + if ($node instanceof MethodCall) { + $type = $this->getType($node->var); + } + if ($this->isWhereMethodWithClosureOrArrowFunction($node) && $this->changeClosureParamType($node, $type)) { return $node; } @@ -95,31 +100,32 @@ private function isWhereMethodWithClosureOrArrowFunction(MethodCall|StaticCall $ ! ($node->getArgs()[0]->value ?? null) instanceof ArrowFunction); } - private function changeClosureParamType(MethodCall|StaticCall $node): void + /** + * @param ObjectType $type + */ + private function changeClosureParamType(MethodCall|StaticCall $node, Type $type): bool { /** @var ArrowFunction|Closure $closure */ $closure = $node->getArgs()[0] ->value; if (! isset($closure->getParams()[0])) { - return; + return false; } $param = $closure->getParams()[0]; if ($param->type instanceof Name) { - return; + return false; } - $classOrVar = $node instanceof MethodCall - ? $node->var - : $node->class; + if ($type->isObject()->no()) { + return false; + } - $type = $this->isObjectType($classOrVar, new ObjectType('Illuminate\Database\Eloquent\Model')) - ? 'Illuminate\Contracts\Database\Eloquent\Builder' - : 'Illuminate\Contracts\Database\Query\Builder'; + $param->type = new FullyQualified($type->getClassName()); - $param->type = new FullyQualified($type); + return true; } private function expectedObjectTypeAndMethodCall(MethodCall|StaticCall $node): bool diff --git a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc index 4f8319ab..195c1c9c 100644 --- a/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc +++ b/tests/Rector/MethodCall/EloquentWhereRelationTypeHintingParameterRector/Fixture/fixture3.php.inc @@ -16,7 +16,7 @@ namespace RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHinting use RectorLaravel\Tests\Rector\MethodCall\EloquentWhereRelationTypeHintingParameterRector\Source\FooModel; -FooModel::whereHas('posts', function (\Illuminate\Database\Eloquent\Builder $query) { +FooModel::whereHas('posts', function (\Illuminate\Contracts\Database\Eloquent\Builder $query) { $query->where('is_published', true); }); diff --git a/tests/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector/Fixture/fixture4.php.inc b/tests/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector/Fixture/fixture4.php.inc new file mode 100644 index 00000000..079aca65 --- /dev/null +++ b/tests/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector/Fixture/fixture4.php.inc @@ -0,0 +1,25 @@ +relation()->where(fn ($query) => + $query->where('id', 1) + ->orWhere('id', 2) +); + +?> +----- +relation()->where(fn (\Illuminate\Database\Eloquent\Relations\HasMany $query) => + $query->where('id', 1) + ->orWhere('id', 2) +); + +?> diff --git a/tests/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector/Source/FooModel.php b/tests/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector/Source/FooModel.php index b2f96c32..5ec0a8fb 100644 --- a/tests/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector/Source/FooModel.php +++ b/tests/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector/Source/FooModel.php @@ -3,5 +3,11 @@ namespace RectorLaravel\Tests\Rector\MethodCall\EloquentWhereTypeHintClosureParameterRector\Source; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; -class FooModel extends Model {} +class FooModel extends Model { + public function relation(): HasMany + { + return new HasMany; + } +} From f3deb29ea34cf86528b2dad193fbd4e9863d64f0 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Mon, 24 Nov 2025 04:42:03 +0000 Subject: [PATCH 4/4] cs fixes --- .../EloquentWhereTypeHintClosureParameterRector.php | 8 ++++---- .../Source/FooModel.php | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector.php b/src/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector.php index d2ce22b8..561512b2 100644 --- a/src/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector.php +++ b/src/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector.php @@ -12,11 +12,11 @@ use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified; use PHPStan\Type\ObjectType; +use PHPStan\Type\Type; use RectorLaravel\AbstractRector; use RectorLaravel\NodeAnalyzer\QueryBuilderAnalyzer; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; -use PHPStan\Type\Type; /** * @see \RectorLaravel\Tests\Rector\MethodCall\EloquentWhereTypeHintClosureParameterRector\EloquentWhereTypeHintClosureParameterRectorTest @@ -72,8 +72,7 @@ public function getNodeTypes(): array } /** - * @param StaticCall|MethodCall $node - * @return Node|null + * @param StaticCall|MethodCall $node */ public function refactor(Node $node): ?Node { @@ -83,6 +82,7 @@ public function refactor(Node $node): ?Node $type = $this->getType($node->var); } + /** @phpstan-ignore argument.type */ if ($this->isWhereMethodWithClosureOrArrowFunction($node) && $this->changeClosureParamType($node, $type)) { return $node; } @@ -101,7 +101,7 @@ private function isWhereMethodWithClosureOrArrowFunction(MethodCall|StaticCall $ } /** - * @param ObjectType $type + * @param ObjectType $type */ private function changeClosureParamType(MethodCall|StaticCall $node, Type $type): bool { diff --git a/tests/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector/Source/FooModel.php b/tests/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector/Source/FooModel.php index 5ec0a8fb..e650440a 100644 --- a/tests/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector/Source/FooModel.php +++ b/tests/Rector/MethodCall/EloquentWhereTypeHintClosureParameterRector/Source/FooModel.php @@ -5,7 +5,8 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; -class FooModel extends Model { +class FooModel extends Model +{ public function relation(): HasMany { return new HasMany;