diff --git a/src/Methods/BuilderHelper.php b/src/Methods/BuilderHelper.php index b5ba994de..a9fbefd15 100644 --- a/src/Methods/BuilderHelper.php +++ b/src/Methods/BuilderHelper.php @@ -22,6 +22,7 @@ use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; class BuilderHelper @@ -211,7 +212,7 @@ public function determineBuilderName(string $modelClassName): string return EloquentBuilder::class; } - if ($returnType instanceof ObjectType) { + if ($returnType instanceof TypeWithClassName) { return $returnType->getClassName(); } @@ -223,7 +224,7 @@ public function determineCollectionClassName(string $modelClassName): string try { $newCollectionMethod = $this->reflectionProvider->getClass($modelClassName)->getNativeMethod('newCollection'); $returnType = ParametersAcceptorSelector::selectSingle($newCollectionMethod->getVariants())->getReturnType(); - if ($returnType instanceof ObjectType) { + if ($returnType instanceof TypeWithClassName) { return $returnType->getClassName(); } diff --git a/src/Methods/HigherOrderTapProxyExtension.php b/src/Methods/HigherOrderTapProxyExtension.php index 8fcbbc0a5..4593585c4 100644 --- a/src/Methods/HigherOrderTapProxyExtension.php +++ b/src/Methods/HigherOrderTapProxyExtension.php @@ -10,6 +10,7 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; use PHPStan\Type\ObjectType; +use PHPStan\Type\TypeWithClassName; final class HigherOrderTapProxyExtension implements MethodsClassReflectionExtension { @@ -23,7 +24,7 @@ public function hasMethod(ClassReflection $classReflection, string $methodName): $templateType = $templateTypeMap->getType('TClass'); - if (! $templateType instanceof ObjectType) { + if (! $templateType instanceof TypeWithClassName) { return false; } diff --git a/src/ReturnTypes/BuilderModelFindExtension.php b/src/ReturnTypes/BuilderModelFindExtension.php index 23ff724b7..f1fa4d650 100644 --- a/src/ReturnTypes/BuilderModelFindExtension.php +++ b/src/ReturnTypes/BuilderModelFindExtension.php @@ -22,6 +22,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeWithClassName; /** * @internal @@ -61,7 +62,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool $model = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TModelClass'); - if (! $model instanceof ObjectType) { + if (! $model instanceof TypeWithClassName) { return false; } diff --git a/src/ReturnTypes/CollectionGenericStaticMethodDynamicMethodReturnTypeExtension.php b/src/ReturnTypes/CollectionGenericStaticMethodDynamicMethodReturnTypeExtension.php index 77be35a52..8c125e90a 100644 --- a/src/ReturnTypes/CollectionGenericStaticMethodDynamicMethodReturnTypeExtension.php +++ b/src/ReturnTypes/CollectionGenericStaticMethodDynamicMethodReturnTypeExtension.php @@ -17,6 +17,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; +use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; class CollectionGenericStaticMethodDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension @@ -62,7 +63,7 @@ public function getTypeFromMethodCall( $calledOnType = $scope->getType($methodCall->var); - if (! $calledOnType instanceof ObjectType) { + if (! $calledOnType instanceof TypeWithClassName) { return $returnType; } diff --git a/src/ReturnTypes/EloquentBuilderExtension.php b/src/ReturnTypes/EloquentBuilderExtension.php index 9a96206b7..4da89792d 100644 --- a/src/ReturnTypes/EloquentBuilderExtension.php +++ b/src/ReturnTypes/EloquentBuilderExtension.php @@ -19,6 +19,7 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeWithClassName; final class EloquentBuilderExtension implements DynamicMethodReturnTypeExtension { @@ -58,7 +59,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool $templateTypeMap = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap(); - if (! $templateTypeMap->getType('TModelClass') instanceof ObjectType) { + if (! $templateTypeMap->getType('TModelClass') instanceof TypeWithClassName) { return false; } diff --git a/src/ReturnTypes/HigherOrderTapProxyExtension.php b/src/ReturnTypes/HigherOrderTapProxyExtension.php index f39e13f9b..31ff625bb 100644 --- a/src/ReturnTypes/HigherOrderTapProxyExtension.php +++ b/src/ReturnTypes/HigherOrderTapProxyExtension.php @@ -11,8 +11,8 @@ use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\MixedType; -use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeWithClassName; /** * @internal @@ -46,7 +46,7 @@ public function getTypeFromMethodCall( $type = $scope->getType($methodCall->var); if ($type instanceof GenericObjectType) { $types = $type->getTypes(); - if (count($types) === 1 && $types[0] instanceof ObjectType) { + if (count($types) === 1 && $types[0] instanceof TypeWithClassName) { return $types[0]; } } diff --git a/src/ReturnTypes/RelationCollectionExtension.php b/src/ReturnTypes/RelationCollectionExtension.php index 7295a299c..6a05c8f16 100644 --- a/src/ReturnTypes/RelationCollectionExtension.php +++ b/src/ReturnTypes/RelationCollectionExtension.php @@ -17,6 +17,7 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeWithClassName; /** * @internal @@ -50,7 +51,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool $modelType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TRelatedModel'); - if (! $modelType instanceof ObjectType) { + if (! $modelType instanceof TypeWithClassName) { return false; } diff --git a/src/ReturnTypes/RelationFindExtension.php b/src/ReturnTypes/RelationFindExtension.php index 7bb1dd882..a62dc5897 100644 --- a/src/ReturnTypes/RelationFindExtension.php +++ b/src/ReturnTypes/RelationFindExtension.php @@ -22,6 +22,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeWithClassName; /** * @internal @@ -59,7 +60,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool $modelType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TRelatedModel'); - if (! $modelType instanceof ObjectType) { + if (! $modelType instanceof TypeWithClassName) { return false; } diff --git a/src/Rules/ModelProperties/ModelPropertiesRuleHelper.php b/src/Rules/ModelProperties/ModelPropertiesRuleHelper.php index fecfe7c89..42e8fc1b7 100644 --- a/src/Rules/ModelProperties/ModelPropertiesRuleHelper.php +++ b/src/Rules/ModelProperties/ModelPropertiesRuleHelper.php @@ -14,8 +14,6 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; @@ -62,17 +60,21 @@ public function check(MethodReflection $methodReflection, Scope $scope, array $a $argType = $scope->getType($argValue); - if ($argType instanceof ConstantArrayType) { + if ($argType->isConstantArray()->yes()) { $errors = []; - $keyType = $argType->getKeyType()->generalize(GeneralizePrecision::lessSpecific()); + $constantArrays = $argType->getConstantArrays(); - if ($keyType->isInteger()->yes()) { - $valueTypes = $argType->getValuesArray()->getValueTypes(); - } elseif ($keyType->isString()->yes()) { - $valueTypes = $argType->getKeysArray()->getValueTypes(); - } else { - $valueTypes = []; + $valueTypes = []; + + foreach ($constantArrays as $constantArray) { + $keyType = $constantArray->getKeyType()->generalize(GeneralizePrecision::lessSpecific()); + + if ($keyType->isInteger()->yes()) { + $valueTypes = array_merge($valueTypes, $constantArray->getValuesArray()->getValueTypes()); + } elseif ($keyType->isString()->yes()) { + $valueTypes = array_merge($valueTypes, $constantArray->getKeysArray()->getValueTypes()); + } } foreach ($valueTypes as $valueType) { @@ -159,9 +161,9 @@ public function hasModelPropertyParameter( return [$index, new ObjectType($modelReflection->getName())]; } } - } elseif ($type instanceof ArrayType) { - $keyType = $type->getKeyType(); - $itemType = $type->getItemType(); + } elseif ($type->isArray()->yes()) { + $keyType = $type->getIterableKeyType(); + $itemType = $type->getIterableValueType(); if ($keyType instanceof GenericModelPropertyType) { return [$index, $keyType->getGenericType()]; diff --git a/src/Rules/ModelProperties/ModelPropertyStaticCallRule.php b/src/Rules/ModelProperties/ModelPropertyStaticCallRule.php index e5ee18880..a8a738cb2 100644 --- a/src/Rules/ModelProperties/ModelPropertyStaticCallRule.php +++ b/src/Rules/ModelProperties/ModelPropertyStaticCallRule.php @@ -13,10 +13,10 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; -use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeUtils; +use PHPStan\Type\TypeWithClassName; /** * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\StaticCall> @@ -116,9 +116,11 @@ static function (Type $type) use ($methodName): bool { return []; } - if ($classType instanceof ConstantStringType) { - $modelClassName = $classType->getValue(); - } elseif ($classType instanceof ObjectType) { + $strings = TypeUtils::getConstantStrings($classType); + + if (count($strings) === 1) { + $modelClassName = $strings[0]->getValue(); + } elseif ($classType instanceof TypeWithClassName) { $modelClassName = $classType->getClassName(); } else { return []; diff --git a/src/Rules/ModelRuleHelper.php b/src/Rules/ModelRuleHelper.php index 10d12f38c..19e2384c4 100644 --- a/src/Rules/ModelRuleHelper.php +++ b/src/Rules/ModelRuleHelper.php @@ -13,6 +13,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeWithClassName; final class ModelRuleHelper { @@ -39,7 +40,7 @@ public function findModelReflectionFromType(Type $type): ?ClassReflection $modelType = TypeCombinator::removeNull($modelType); - if (! $modelType instanceof ObjectType) { + if (! $modelType instanceof TypeWithClassName) { return null; } diff --git a/src/Rules/OctaneCompatibilityRule.php b/src/Rules/OctaneCompatibilityRule.php index c75a0e001..0438eb150 100644 --- a/src/Rules/OctaneCompatibilityRule.php +++ b/src/Rules/OctaneCompatibilityRule.php @@ -13,6 +13,7 @@ use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ObjectType; +use PHPStan\Type\TypeWithClassName; /** * @implements Rule @@ -43,7 +44,7 @@ public function processNode(Node $node, Scope $scope): array $calledOnType = $scope->getType($node->var); - if (! $calledOnType instanceof ObjectType) { + if (! $calledOnType instanceof TypeWithClassName) { return []; } diff --git a/src/Rules/RelationExistenceRule.php b/src/Rules/RelationExistenceRule.php index 02db9221f..173c33548 100644 --- a/src/Rules/RelationExistenceRule.php +++ b/src/Rules/RelationExistenceRule.php @@ -72,15 +72,19 @@ public function processNode(Node $node, Scope $scope): array $relations = []; if ($valueType->isConstantArray()->yes()) { - $relations = array_merge( - $relations, - ...array_map(function (Type $type) { - return TypeUtils::getConstantStrings($type); - }, $valueType->getKeyTypes()), // @phpstan-ignore-line - ...array_map(function (Type $type) { - return TypeUtils::getConstantStrings($type); - }, $valueType->getValueTypes()), // @phpstan-ignore-line - ); + $arrays = $valueType->getConstantArrays(); + + foreach ($arrays as $array) { + $relations = array_merge( + $relations, + ...array_map(function (Type $type) { + return TypeUtils::getConstantStrings($type); + }, $array->getKeyTypes()), + ...array_map(function (Type $type) { + return TypeUtils::getConstantStrings($type); + }, $array->getValueTypes()), + ); + } } else { $constants = TypeUtils::getConstantStrings($valueType); @@ -153,7 +157,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $errors += $closure($calledOnType, $relationName, $node); + $errors = array_merge($errors, $closure($calledOnType, $relationName, $node)); } return $errors; diff --git a/src/Types/ModelProperty/GenericModelPropertyType.php b/src/Types/ModelProperty/GenericModelPropertyType.php index 4f561d18d..b797f3855 100644 --- a/src/Types/ModelProperty/GenericModelPropertyType.php +++ b/src/Types/ModelProperty/GenericModelPropertyType.php @@ -5,7 +5,6 @@ namespace NunoMaduro\Larastan\Types\ModelProperty; use PHPStan\TrinaryLogic; -use PHPStan\Type\ClassStringType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\TemplateType; @@ -82,7 +81,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap $typeToInfer = new ObjectType($receivedType->getValue()); } elseif ($receivedType instanceof self) { $typeToInfer = $receivedType->type; - } elseif ($receivedType instanceof ClassStringType) { + } elseif ($receivedType->isClassStringType()->yes()) { $typeToInfer = $this->getGenericType(); if ($typeToInfer instanceof TemplateType) { diff --git a/src/Types/ModelRelationsDynamicMethodReturnTypeExtension.php b/src/Types/ModelRelationsDynamicMethodReturnTypeExtension.php index e14e8c11d..ece93a0c6 100644 --- a/src/Types/ModelRelationsDynamicMethodReturnTypeExtension.php +++ b/src/Types/ModelRelationsDynamicMethodReturnTypeExtension.php @@ -17,6 +17,7 @@ use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeWithClassName; class ModelRelationsDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -41,7 +42,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool $returnType = $variants->getReturnType(); - if (! $returnType instanceof ObjectType) { + if (! $returnType instanceof TypeWithClassName) { return false; } diff --git a/src/Types/RelationDynamicMethodReturnTypeExtension.php b/src/Types/RelationDynamicMethodReturnTypeExtension.php index e0949483d..cab354eaa 100644 --- a/src/Types/RelationDynamicMethodReturnTypeExtension.php +++ b/src/Types/RelationDynamicMethodReturnTypeExtension.php @@ -6,7 +6,6 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Database\Eloquent\Relations\MorphTo; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionVariant; @@ -20,6 +19,7 @@ use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\TypeUtils; +use PHPStan\Type\TypeWithClassName; class RelationDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -57,7 +57,7 @@ public function getTypeFromMethodCall( $functionVariant = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants()); $returnType = $functionVariant->getReturnType(); - if (! $returnType instanceof ObjectType) { + if (! $returnType instanceof TypeWithClassName) { return $returnType; } diff --git a/src/Types/RelationParserHelper.php b/src/Types/RelationParserHelper.php index c487408c9..ff071a616 100644 --- a/src/Types/RelationParserHelper.php +++ b/src/Types/RelationParserHelper.php @@ -12,10 +12,10 @@ use PHPStan\Parser\Parser; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateTypeMap; -use PHPStan\Type\ObjectType; +use PHPStan\Type\TypeUtils; +use PHPStan\Type\TypeWithClassName; class RelationParserHelper { @@ -87,14 +87,16 @@ public function findRelatedModelInRelationMethod( $argType = $methodScope->getType($methodCall->getArgs()[0]->value); $returnClass = null; - if ($argType instanceof ConstantStringType) { - $returnClass = $argType->getValue(); + $constantStrings = TypeUtils::getConstantStrings($argType); + + if (count($constantStrings) === 1) { + $returnClass = $constantStrings[0]->getValue(); } if ($argType instanceof GenericClassStringType) { $modelType = $argType->getGenericType(); - if (! $modelType instanceof ObjectType) { + if (! $modelType instanceof TypeWithClassName) { return null; } diff --git a/tests/Rules/Data/relation-existence-rule.php b/tests/Rules/Data/relation-existence-rule.php index 48b3c39a7..c4303c1bb 100644 --- a/tests/Rules/Data/relation-existence-rule.php +++ b/tests/Rules/Data/relation-existence-rule.php @@ -50,3 +50,6 @@ \App\User::query()->with('foo:id,name'); \App\User::with(['foo:id', 'accounts']); \App\User::query()->with(['foo:id', 'accounts']); + +/** @var array{foo:string}|array{0: 'bar'} $a */ +\App\User::with($a); diff --git a/tests/Rules/RelationExistenceRuleTest.php b/tests/Rules/RelationExistenceRuleTest.php index 874156aeb..8d4ec5f15 100644 --- a/tests/Rules/RelationExistenceRuleTest.php +++ b/tests/Rules/RelationExistenceRuleTest.php @@ -184,11 +184,18 @@ public function testRule(): void 'Relation \'foo\' is not found in App\User model.', 51, ], - [ 'Relation \'foo\' is not found in App\User model.', 52, ], + [ + 'Relation \'foo\' is not found in App\User model.', + 55, + ], + [ + 'Relation \'bar\' is not found in App\User model.', + 55, + ], ]); }