diff --git a/conf/config.neon b/conf/config.neon index 07c9488783..d59c4a539b 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1126,6 +1126,9 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\ArrayColumnHelper + - class: PHPStan\Type\Php\ArrayColumnFunctionReturnTypeExtension tags: diff --git a/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php index 51d8999c2f..70f5892c2b 100644 --- a/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php @@ -4,27 +4,17 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\ShouldNotHappenException; -use PHPStan\TrinaryLogic; -use PHPStan\Type\Accessory\AccessoryArrayListType; -use PHPStan\Type\Accessory\NonEmptyArrayType; -use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntegerType; -use PHPStan\Type\MixedType; -use PHPStan\Type\NeverType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use function count; final class ArrayColumnFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) + public function __construct( + private ArrayColumnHelper $arrayColumnHelper, + ) { } @@ -46,167 +36,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $constantArrayTypes = $arrayType->getConstantArrays(); if (count($constantArrayTypes) === 1) { - $type = $this->handleConstantArray($constantArrayTypes[0], $columnType, $indexType, $scope); + $type = $this->arrayColumnHelper->handleConstantArray($constantArrayTypes[0], $columnType, $indexType, $scope); if ($type !== null) { return $type; } } - return $this->handleAnyArray($arrayType, $columnType, $indexType, $scope); - } - - private function handleAnyArray(Type $arrayType, Type $columnType, ?Type $indexType, Scope $scope): Type - { - $iterableAtLeastOnce = $arrayType->isIterableAtLeastOnce(); - if ($iterableAtLeastOnce->no()) { - return new ConstantArrayType([], []); - } - - $iterableValueType = $arrayType->getIterableValueType(); - $returnValueType = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, false); - - if ($returnValueType === null) { - $returnValueType = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, true); - $iterableAtLeastOnce = TrinaryLogic::createMaybe(); - if ($returnValueType === null) { - throw new ShouldNotHappenException(); - } - } - - if ($returnValueType instanceof NeverType) { - return new ConstantArrayType([], []); - } - - if ($indexType !== null) { - $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, false); - if ($type !== null) { - $returnKeyType = $type; - } else { - $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, true); - if ($type !== null) { - $returnKeyType = TypeCombinator::union($type, new IntegerType()); - } else { - $returnKeyType = new IntegerType(); - } - } - } else { - $returnKeyType = new IntegerType(); - } - - $returnType = new ArrayType($this->castToArrayKeyType($returnKeyType), $returnValueType); - - if ($iterableAtLeastOnce->yes()) { - $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); - } - if ($indexType === null) { - $returnType = TypeCombinator::intersect($returnType, new AccessoryArrayListType()); - } - - return $returnType; - } - - private function handleConstantArray(ConstantArrayType $arrayType, Type $columnType, ?Type $indexType, Scope $scope): ?Type - { - $builder = ConstantArrayTypeBuilder::createEmpty(); - - foreach ($arrayType->getValueTypes() as $i => $iterableValueType) { - $valueType = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, false); - if ($valueType === null) { - return null; - } - if ($valueType instanceof NeverType) { - continue; - } - - if ($indexType !== null) { - $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, false); - if ($type !== null) { - $keyType = $type; - } else { - $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, true); - if ($type !== null) { - $keyType = TypeCombinator::union($type, new IntegerType()); - } else { - $keyType = null; - } - } - } else { - $keyType = null; - } - - if ($keyType !== null) { - $keyType = $this->castToArrayKeyType($keyType); - } - $builder->setOffsetValueType($keyType, $valueType, $arrayType->isOptionalKey($i)); - } - - return $builder->getArray(); - } - - private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $scope, bool $allowMaybe): ?Type - { - $offsetIsNull = $offsetOrProperty->isNull(); - if ($offsetIsNull->yes()) { - return $type; - } - - $returnTypes = []; - - if ($offsetIsNull->maybe()) { - $returnTypes[] = $type; - } - - if (!$type->canAccessProperties()->no()) { - $propertyTypes = $offsetOrProperty->getConstantStrings(); - if ($propertyTypes === []) { - return new MixedType(); - } - foreach ($propertyTypes as $propertyType) { - $propertyName = $propertyType->getValue(); - $hasProperty = $type->hasProperty($propertyName); - if ($hasProperty->maybe()) { - return $allowMaybe ? new MixedType() : null; - } - if (!$hasProperty->yes()) { - continue; - } - - $returnTypes[] = $type->getProperty($propertyName, $scope)->getReadableType(); - } - } - - if ($type->isOffsetAccessible()->yes()) { - $hasOffset = $type->hasOffsetValueType($offsetOrProperty); - if (!$allowMaybe && $hasOffset->maybe()) { - return null; - } - if (!$hasOffset->no()) { - $returnTypes[] = $type->getOffsetValueType($offsetOrProperty); - } - } - - if ($returnTypes === []) { - return new NeverType(); - } - - return TypeCombinator::union(...$returnTypes); - } - - private function castToArrayKeyType(Type $type): Type - { - $isArray = $type->isArray(); - if ($isArray->yes()) { - return $this->phpVersion->throwsTypeErrorForInternalFunctions() ? new NeverType() : new IntegerType(); - } - if ($isArray->no()) { - return $type->toArrayKey(); - } - $withoutArrayType = TypeCombinator::remove($type, new ArrayType(new MixedType(), new MixedType())); - $keyType = $withoutArrayType->toArrayKey(); - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { - return $keyType; - } - return TypeCombinator::union($keyType, new IntegerType()); + return $this->arrayColumnHelper->handleAnyArray($arrayType, $columnType, $indexType, $scope); } } diff --git a/src/Type/Php/ArrayColumnHelper.php b/src/Type/Php/ArrayColumnHelper.php new file mode 100644 index 0000000000..dd0dd898ad --- /dev/null +++ b/src/Type/Php/ArrayColumnHelper.php @@ -0,0 +1,183 @@ +isIterableAtLeastOnce(); + if ($iterableAtLeastOnce->no()) { + return new ConstantArrayType([], []); + } + + $iterableValueType = $arrayType->getIterableValueType(); + $returnValueType = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, false); + + if ($returnValueType === null) { + $returnValueType = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, true); + $iterableAtLeastOnce = TrinaryLogic::createMaybe(); + if ($returnValueType === null) { + throw new ShouldNotHappenException(); + } + } + + if ($returnValueType instanceof NeverType) { + return new ConstantArrayType([], []); + } + + if ($indexType !== null) { + $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, false); + if ($type !== null) { + $returnKeyType = $type; + } else { + $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, true); + if ($type !== null) { + $returnKeyType = TypeCombinator::union($type, new IntegerType()); + } else { + $returnKeyType = new IntegerType(); + } + } + } else { + $returnKeyType = new IntegerType(); + } + + $returnType = new ArrayType($this->castToArrayKeyType($returnKeyType), $returnValueType); + + if ($iterableAtLeastOnce->yes()) { + $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); + } + if ($indexType === null) { + $returnType = TypeCombinator::intersect($returnType, new AccessoryArrayListType()); + } + + return $returnType; + } + + public function handleConstantArray(ConstantArrayType $arrayType, Type $columnType, ?Type $indexType, Scope $scope): ?Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + foreach ($arrayType->getValueTypes() as $i => $iterableValueType) { + $valueType = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, false); + if ($valueType === null) { + return null; + } + if ($valueType instanceof NeverType) { + continue; + } + + if ($indexType !== null) { + $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, false); + if ($type !== null) { + $keyType = $type; + } else { + $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, true); + if ($type !== null) { + $keyType = TypeCombinator::union($type, new IntegerType()); + } else { + $keyType = null; + } + } + } else { + $keyType = null; + } + + if ($keyType !== null) { + $keyType = $this->castToArrayKeyType($keyType); + } + $builder->setOffsetValueType($keyType, $valueType, $arrayType->isOptionalKey($i)); + } + + return $builder->getArray(); + } + + private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $scope, bool $allowMaybe): ?Type + { + $offsetIsNull = $offsetOrProperty->isNull(); + if ($offsetIsNull->yes()) { + return $type; + } + + $returnTypes = []; + + if ($offsetIsNull->maybe()) { + $returnTypes[] = $type; + } + + if (!$type->canAccessProperties()->no()) { + $propertyTypes = $offsetOrProperty->getConstantStrings(); + if ($propertyTypes === []) { + return new MixedType(); + } + foreach ($propertyTypes as $propertyType) { + $propertyName = $propertyType->getValue(); + $hasProperty = $type->hasProperty($propertyName); + if ($hasProperty->maybe()) { + return $allowMaybe ? new MixedType() : null; + } + if (!$hasProperty->yes()) { + continue; + } + + $returnTypes[] = $type->getProperty($propertyName, $scope)->getReadableType(); + } + } + + if ($type->isOffsetAccessible()->yes()) { + $hasOffset = $type->hasOffsetValueType($offsetOrProperty); + if (!$allowMaybe && $hasOffset->maybe()) { + return null; + } + if (!$hasOffset->no()) { + $returnTypes[] = $type->getOffsetValueType($offsetOrProperty); + } + } + + if ($returnTypes === []) { + return new NeverType(); + } + + return TypeCombinator::union(...$returnTypes); + } + + private function castToArrayKeyType(Type $type): Type + { + $isArray = $type->isArray(); + if ($isArray->yes()) { + return $this->phpVersion->throwsTypeErrorForInternalFunctions() ? new NeverType() : new IntegerType(); + } + if ($isArray->no()) { + return $type->toArrayKey(); + } + $withoutArrayType = TypeCombinator::remove($type, new ArrayType(new MixedType(), new MixedType())); + $keyType = $withoutArrayType->toArrayKey(); + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return $keyType; + } + return TypeCombinator::union($keyType, new IntegerType()); + } + +}