diff --git a/src/ReturnTypes/ModelFactoryDynamicStaticMethodReturnTypeExtension.php b/src/ReturnTypes/ModelFactoryDynamicStaticMethodReturnTypeExtension.php index b9e0d7617..996e56ddd 100644 --- a/src/ReturnTypes/ModelFactoryDynamicStaticMethodReturnTypeExtension.php +++ b/src/ReturnTypes/ModelFactoryDynamicStaticMethodReturnTypeExtension.php @@ -10,6 +10,7 @@ use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; +use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\TrinaryLogic; @@ -19,13 +20,15 @@ use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use function array_map; use function class_exists; use function count; -use function ltrim; final class ModelFactoryDynamicStaticMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { @@ -51,9 +54,9 @@ public function getTypeFromStaticMethodCall( ): Type { $class = $methodCall->class; - if (! $class instanceof Name) { - return new ErrorType(); - } + $calledOnType = $class instanceof Name + ? new ObjectType($scope->resolveName($class)) + : $scope->getType($class); if (count($methodCall->getArgs()) === 0) { $isSingleModel = TrinaryLogic::createYes(); @@ -72,61 +75,66 @@ public function getTypeFromStaticMethodCall( $isSingleModel = (new UnionType($numericTypes))->isSuperTypeOf($argType)->negate(); } - $factoryClass = $this->getFactoryClass($class, $scope); + return TypeCombinator::union(...array_map( + function (ClassReflection $classReflection) use ($scope, $isSingleModel) { + $factoryReflection = $this->getFactoryReflection($classReflection, $scope); - if ($factoryClass === null) { - return new ErrorType(); - } + if ($factoryReflection === null) { + return new ErrorType(); + } - return new ModelFactoryType($factoryClass, null, null, $isSingleModel); + return new ModelFactoryType($factoryReflection->getName(), null, $factoryReflection, $isSingleModel); + }, + $calledOnType->getObjectClassReflections(), + )); } - private function getFactoryClass(Name $model, Scope $scope): string|null - { - $factoryClass = $this->getFactoryClassFromNewFactoryMethod($model, $scope); + private function getFactoryReflection( + ClassReflection $modelReflection, + Scope $scope, + ): ClassReflection|null { + $factoryReflection = $this->getFactoryFromNewFactoryMethod($modelReflection, $scope); - if ($factoryClass !== null) { - return $factoryClass; + if ($factoryReflection !== null) { + return $factoryReflection; } - /** @var class-string $className */ - $className = ltrim($model->toCodeString(), '\\'); - $factoryClass = Factory::resolveFactoryName($className); + /** @phpstan-ignore argument.type (guaranteed to be model class-string) */ + $factoryClass = Factory::resolveFactoryName($modelReflection->getName()); if (class_exists($factoryClass)) { - return $factoryClass; + return $this->reflectionProvider->getClass($factoryClass); } return null; } - private function getFactoryClassFromNewFactoryMethod(Name $model, Scope $scope): string|null - { - $modelReflection = $this->reflectionProvider->getClass($model->toString()); - + private function getFactoryFromNewFactoryMethod( + ClassReflection $modelReflection, + Scope $scope, + ): ClassReflection|null { if (! $modelReflection->hasMethod('newFactory')) { return null; } - $returnType = $modelReflection->getMethod('newFactory', $scope) + $factoryReflections = $modelReflection->getMethod('newFactory', $scope) ->getVariants()[0] - ->getReturnType(); - - $factoryClasses = $returnType->getObjectClassNames(); + ->getReturnType() + ->getObjectClassReflections(); - if (count($factoryClasses) !== 1) { + if (count($factoryReflections) !== 1) { return null; } - $factoryReflection = $this->reflectionProvider->getClass($factoryClasses[0]); - - if ( - ! $factoryReflection->isSubclassOf(Factory::class) - || $factoryReflection->isAbstract() - ) { - return null; + foreach ($factoryReflections as $factoryReflection) { + if ( + $factoryReflection->isSubclassOf(Factory::class) + && ! $factoryReflection->isAbstract() + ) { + return $factoryReflection; + } } - return $factoryReflection->getName(); + return null; } } diff --git a/tests/Type/data/model-factories.php b/tests/Type/data/model-factories.php index cecb54664..ac3c86b34 100644 --- a/tests/Type/data/model-factories.php +++ b/tests/Type/data/model-factories.php @@ -5,6 +5,9 @@ use App\Post; use App\User; +use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; use function PHPStan\Testing\assertType; function test(?int $foo): void { @@ -75,3 +78,28 @@ function test(?int $foo): void { assertType('Database\Factories\UserFactory', User::factory()->trashed()); assertType('*ERROR*', Post::factory()->trashed()); } + +class Comment extends Model +{ + use HasFactory; + + /** @return CommentFactory */ + protected static function newFactory(): Factory + { + return CommentFactory::new(); + } + + private function test(): void + { + assertType('ModelFactories\CommentFactory', static::factory()); + } +} + +/** @extends Factory */ +class CommentFactory extends Factory +{ + public function definition(): array + { + return []; + } +} diff --git a/tests/Unit/ModelFactoryDynamicStaticMethodReturnTypeExtensionTest.php b/tests/Unit/ModelFactoryDynamicStaticMethodReturnTypeExtensionTest.php index 07caedb28..5b5dc7349 100644 --- a/tests/Unit/ModelFactoryDynamicStaticMethodReturnTypeExtensionTest.php +++ b/tests/Unit/ModelFactoryDynamicStaticMethodReturnTypeExtensionTest.php @@ -4,6 +4,7 @@ namespace Unit; +use App\User; use Generator; use Larastan\Larastan\ReturnTypes\ModelFactoryDynamicStaticMethodReturnTypeExtension; use Larastan\Larastan\Types\Factory\ModelFactoryType; @@ -12,7 +13,7 @@ use PhpParser\Node\Name; use PhpParser\Node\Scalar\LNumber; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\Dummy\DummyMethodReflection; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNumericStringType; @@ -31,18 +32,30 @@ class ModelFactoryDynamicStaticMethodReturnTypeExtensionTest extends PHPStanTestCase { + private ReflectionProvider $reflectionProvider; + + public function setUp(): void + { + parent::setUp(); + + $this->reflectionProvider = $this->createReflectionProvider(); + } + /** @test */ public function it_sets_the_is_single_model_flag_to_true_if_no_args_given(): void { - $scope = $this->createMock(Scope::class); + $class = new Name(User::class); + + $scope = $this->createMock(Scope::class); + $scope->method('resolveName')->with($class)->willReturn(User::class); + $extension = new ModelFactoryDynamicStaticMethodReturnTypeExtension( $this->createReflectionProvider(), ); $type = $extension->getTypeFromStaticMethodCall( - /** @phpstan-ignore phpstanApi.constructor (not covered by BC promise) */ - new DummyMethodReflection('factory'), - new StaticCall(new Name('App\\User'), 'factory', []), + $this->reflectionProvider->getClass(User::class)->getNativeMethod('factory'), + new StaticCall($class, 'factory', []), $scope, ); @@ -56,17 +69,19 @@ public function it_sets_the_is_single_model_flag_to_true_if_no_args_given(): voi */ public function it_sets_the_is_single_model_flag_correctly(Type $phpstanType, TrinaryLogic $expected): void { - $scope = $this->createMock(Scope::class); + $class = new Name(User::class); + + $scope = $this->createMock(Scope::class); + $scope->method('resolveName')->with($class)->willReturn(User::class); + $scope->method('getType')->willReturn($phpstanType); + $extension = new ModelFactoryDynamicStaticMethodReturnTypeExtension( $this->createReflectionProvider(), ); - $scope->method('getType')->willReturn($phpstanType); - $type = $extension->getTypeFromStaticMethodCall( - /** @phpstan-ignore phpstanApi.constructor (not covered by BC promise) */ - new DummyMethodReflection('factory'), - new StaticCall(new Name('App\\User'), 'factory', [new Arg(new LNumber(1))]), // args doesn't matter + $this->reflectionProvider->getClass(User::class)->getNativeMethod('factory'), + new StaticCall($class, 'factory', [new Arg(new LNumber(1))]), // args doesn't matter $scope, );