diff --git a/src/Handlers/Eloquent/RelationsMethodHandler.php b/src/Handlers/Eloquent/RelationsMethodHandler.php index 32c7cbd6..ae058b6b 100644 --- a/src/Handlers/Eloquent/RelationsMethodHandler.php +++ b/src/Handlers/Eloquent/RelationsMethodHandler.php @@ -14,6 +14,7 @@ use Illuminate\Database\Query\Builder as QueryBuilder; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\Variable; +use PhpParser\Node\Identifier; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\MethodIdentifier; use Psalm\LaravelPlugin\Util\ProxyMethodReturnTypeProvider; @@ -21,6 +22,7 @@ use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface; use Psalm\Type; use Psalm\Type\Union; +use function strtolower; final class RelationsMethodHandler implements MethodReturnTypeProviderInterface { @@ -57,14 +59,55 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) // If this method name is on the builder object, proxy it over there - if ($source->getCodebase()->methods->methodExists(new MethodIdentifier(Builder::class, $method_name_lowercase)) || - $source->getCodebase()->methods->methodExists(new MethodIdentifier(QueryBuilder::class, $method_name_lowercase)) - ) { + if ($method_name_lowercase === '__call') { + $stmt = $event->getStmt(); + + if (!($stmt instanceof MethodCall)) { + return null; + } + + $name = $stmt->name; + + if (!($name instanceof Identifier)) { + return null; + } + + $codebase = $source->getCodebase(); + /** @psalm-var lowercase-string $called_method_name_lowercase */ + $called_method_name_lowercase = $name->toLowerString(); + $type = null; + + foreach ([Builder::class, QueryBuilder::class] as $class) { + $method_id = new MethodIdentifier($class, $called_method_name_lowercase); + if ($codebase->methods->methodExists($method_id)) { + $self_class = null; + $type = $codebase->methods->getMethodReturnType( + $method_id, + $self_class, + null, + $event->getCallArgs() + ); + } + } + $template_type_parameters = $event->getTemplateTypeParameters(); if (!$template_type_parameters) { return null; } + if ($type instanceof Union) { + if ($type->hasType('static')) { + return new Union([ + new Type\Atomic\TGenericObject( + $event->getFqClasslikeName(), + $template_type_parameters + ), + ]); + } + + return $type; + } + $fake_method_call = new MethodCall( new Variable('builder'), $method_name_lowercase, diff --git a/tests/acceptance/EloquentRelationTypes.feature b/tests/acceptance/EloquentRelationTypes.feature index e36764f0..65448a0f 100644 --- a/tests/acceptance/EloquentRelationTypes.feature +++ b/tests/acceptance/EloquentRelationTypes.feature @@ -310,7 +310,6 @@ Feature: Eloquent Relation types When I run Psalm Then I see no errors - @skip Scenario: Relationships return themselves when the underlying method returns a builder Given I have the following code """ @@ -332,7 +331,6 @@ Feature: Eloquent Relation types When I run Psalm Then I see no errors - @skip Scenario: Relationships return themselves when the proxied method is a query builder method Given I have the following code """ @@ -340,9 +338,17 @@ Feature: Eloquent Relation types * @param HasOne $relationship * @psalm-return HasOne */ - function test(HasOne $relationship): HasOne { + function test_hasOne(HasOne $relationship): HasOne { return $relationship->orderBy('id', 'ASC'); } + + /** + * @param HasMany $relationship + * @psalm-return HasMany + */ + function test_hasMany(HasMany $relationship): HasMany { + return $relationship->orderBy('id', 'DESC'); + } """ When I run Psalm Then I see no errors