diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 3922fd9fed..3e0c32fb5f 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1311,8 +1311,8 @@ private function resolveType(Expr $node): Type $rightType = $this->getType($right); if ($node instanceof Expr\AssignOp\Plus || $node instanceof Expr\BinaryOp\Plus) { - $leftConstantArrays = TypeUtils::getConstantArrays($leftType); - $rightConstantArrays = TypeUtils::getConstantArrays($rightType); + $leftConstantArrays = TypeUtils::getOldConstantArrays($leftType); + $rightConstantArrays = TypeUtils::getOldConstantArrays($rightType); $leftCount = count($leftConstantArrays); $rightCount = count($rightConstantArrays); @@ -1322,10 +1322,18 @@ private function resolveType(Expr $node): Type foreach ($rightConstantArrays as $rightConstantArray) { foreach ($leftConstantArrays as $leftConstantArray) { $newArrayBuilder = ConstantArrayTypeBuilder::createFromConstantArray($rightConstantArray); - foreach ($leftConstantArray->getKeyTypes() as $leftKeyType) { + foreach ($leftConstantArray->getKeyTypes() as $i => $leftKeyType) { + $optional = $leftConstantArray->isOptionalKey($i); + $valueType = $leftConstantArray->getOffsetValueType($leftKeyType); + if (!$optional) { + if ($rightConstantArray->hasOffsetValueType($leftKeyType)->maybe()) { + $valueType = TypeCombinator::union($valueType, $rightConstantArray->getOffsetValueType($leftKeyType)); + } + } $newArrayBuilder->setOffsetValueType( $leftKeyType, - $leftConstantArray->getOffsetValueType($leftKeyType), + $valueType, + $optional, ); } $resultTypes[] = $newArrayBuilder->getArray(); @@ -4306,7 +4314,7 @@ public function specifyExpressionType(Expr $expr, Type $type, ?Type $nativeType $this->parentScope, ); } elseif ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { - $constantArrays = TypeUtils::getConstantArrays($this->getType($expr->var)); + $constantArrays = TypeUtils::getOldConstantArrays($this->getType($expr->var)); if (count($constantArrays) > 0) { $setArrays = []; $dimType = $this->getType($expr->dim); diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 048af9b4a0..c9a893bb0c 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1863,7 +1863,7 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $arrayArg = $expr->getArgs()[0]->value; $originalArrayType = $scope->getType($arrayArg); - $constantArrays = TypeUtils::getConstantArrays($originalArrayType); + $constantArrays = TypeUtils::getOldConstantArrays($originalArrayType); if ( $functionReflection->getName() === 'array_push' || ($originalArrayType->isArray()->yes() && count($constantArrays) === 0) @@ -1881,24 +1881,36 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression } $defaultArrayType = $defaultArrayBuilder->getArray(); + if (!$defaultArrayType instanceof ConstantArrayType) { + $arrayType = $originalArrayType; + foreach ($argumentTypes as $argType) { + $arrayType = $arrayType->setOffsetValueType(null, $argType); + } - $arrayTypes = []; - foreach ($constantArrays as $constantArray) { - $arrayType = $defaultArrayType; - foreach ($constantArray->getKeyTypes() as $i => $keyType) { - $valueType = $constantArray->getValueTypes()[$i]; - if ($keyType instanceof ConstantIntegerType) { - $keyType = null; + $scope = $scope->invalidateExpression($arrayArg)->specifyExpressionType($arrayArg, TypeCombinator::intersect($arrayType, new NonEmptyArrayType())); + } else { + $arrayTypes = []; + foreach ($constantArrays as $constantArray) { + $arrayTypeBuilder = ConstantArrayTypeBuilder::createFromConstantArray($defaultArrayType); + foreach ($constantArray->getKeyTypes() as $i => $keyType) { + $valueType = $constantArray->getValueTypes()[$i]; + if ($keyType instanceof ConstantIntegerType) { + $keyType = null; + } + $arrayTypeBuilder->setOffsetValueType( + $keyType, + $valueType, + $constantArray->isOptionalKey($i), + ); } - $arrayType = $arrayType->setOffsetValueType($keyType, $valueType); + $arrayTypes[] = $arrayTypeBuilder->getArray(); } - $arrayTypes[] = $arrayType; - } - $scope = $scope->invalidateExpression($arrayArg)->specifyExpressionType( - $arrayArg, - TypeCombinator::union(...$arrayTypes), - ); + $scope = $scope->invalidateExpression($arrayArg)->specifyExpressionType( + $arrayArg, + TypeCombinator::union(...$arrayTypes), + ); + } } } diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 6e9b766275..715b5df326 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -770,10 +770,6 @@ public function specifyTypesInCondition( $vars = array_merge($vars, array_reverse($tmpVars)); } - if (count($vars) === 0) { - throw new ShouldNotHappenException(); - } - $types = null; foreach ($vars as $var) { if ($var instanceof Expr\Variable && is_string($var->name)) { diff --git a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php index 7e0d752e99..d76135fb86 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php @@ -34,7 +34,7 @@ public function processNode(Node $node, Scope $scope): array } $varType = $scope->getType($node->var); - if (count(TypeUtils::getArrays($varType)) === 0) { + if (count(TypeUtils::getAnyArrays($varType)) === 0) { return []; } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 04b4747561..1c8efef64e 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -94,7 +94,7 @@ public function findSpecifiedType( return null; } - $constantArrays = TypeUtils::getConstantArrays($haystackType); + $constantArrays = TypeUtils::getOldConstantArrays($haystackType); $needleType = $scope->getType($node->getArgs()[0]->value); $valueType = $haystackType->getIterableValueType(); $constantNeedleTypesCount = count(TypeUtils::getConstantScalars($needleType)); diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index f29b301fd3..135d212423 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -11,8 +11,11 @@ use PHPStan\Reflection\ResolvedFunctionVariant; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -94,11 +97,21 @@ public function check( $argumentName = $arg->name->toString(); } if ($arg->unpack) { - $arrays = TypeUtils::getConstantArrays($type); + $arrays = TypeUtils::getOldConstantArrays($type); if (count($arrays) > 0) { $minKeys = null; foreach ($arrays as $array) { - $keysCount = count($array->getKeyTypes()); + $countType = $array->count(); + if ($countType instanceof ConstantIntegerType) { + $keysCount = $countType->getValue(); + } elseif ($countType instanceof IntegerRangeType) { + $keysCount = $countType->getMin(); + if ($keysCount === null) { + throw new ShouldNotHappenException(); + } + } else { + throw new ShouldNotHappenException(); + } if ($minKeys !== null && $keysCount >= $minKeys) { continue; } diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index 1c8a9bc81d..3eaa82d757 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -158,7 +158,7 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], ); } diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 7e17a4f524..584c957095 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -154,7 +154,7 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], ); } diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 28438a2a37..2e720c9800 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -153,7 +153,7 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], ); } diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index 5521b2bacb..9162932f64 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -74,7 +74,7 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], ); } diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index de244f4613..ce67adef23 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -286,7 +286,7 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], ); } diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 761390ce05..c71d4c1326 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -42,9 +42,11 @@ use function count; use function implode; use function in_array; +use function is_int; use function is_string; use function max; use function pow; +use function sort; use function sprintf; use function strpos; @@ -59,21 +61,31 @@ class ConstantArrayType extends ArrayType implements ConstantType /** @var self[]|null */ private ?array $allArrays = null; + /** @var non-empty-list */ + private array $nextAutoIndexes; + /** * @api * @param array $keyTypes * @param array $valueTypes + * @param non-empty-list|int $nextAutoIndexes * @param int[] $optionalKeys */ public function __construct( private array $keyTypes, private array $valueTypes, - private int $nextAutoIndex = 0, + int|array $nextAutoIndexes = [0], private array $optionalKeys = [], ) { assert(count($keyTypes) === count($valueTypes)); + if (is_int($nextAutoIndexes)) { + $nextAutoIndexes = [$nextAutoIndexes]; + } + + $this->nextAutoIndexes = $nextAutoIndexes; + parent::__construct( count($keyTypes) > 0 ? TypeCombinator::union(...$keyTypes) : new NeverType(true), count($valueTypes) > 0 ? TypeCombinator::union(...$valueTypes) : new NeverType(true), @@ -85,9 +97,20 @@ public function isEmpty(): bool return count($this->keyTypes) === 0; } + /** + * @return non-empty-list + */ + public function getNextAutoIndexes(): array + { + return $this->nextAutoIndexes; + } + + /** + * @deprecated + */ public function getNextAutoIndex(): int { - return $this->nextAutoIndex; + return $this->nextAutoIndexes[count($this->nextAutoIndexes) - 1]; } /** @@ -488,17 +511,12 @@ public function unsetOffset(Type $offsetType): Type $k++; } - return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndex, $newOptionalKeys); + return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys); } } } - $arrays = []; - foreach ($this->getAllArrays() as $tmp) { - $arrays[] = new self($tmp->keyTypes, $tmp->valueTypes, $tmp->nextAutoIndex, array_keys($tmp->keyTypes)); - } - - return TypeCombinator::union(...$arrays)->generalize(GeneralizePrecision::moreSpecific()); + return new ArrayType($this->getKeyType(), $this->getItemType()); } public function isIterableAtLeastOnce(): TrinaryLogic @@ -533,7 +551,7 @@ public function removeLast(): self array_pop($valueTypes); $nextAutoindex = $removedKeyType instanceof ConstantIntegerType ? $removedKeyType->getValue() - : $this->nextAutoIndex; + : $this->getNextAutoIndex(); // @phpstan-ignore-line return new self( $keyTypes, @@ -650,7 +668,7 @@ public function generalizeValues(): ArrayType $valueTypes[] = $valueType->generalize(GeneralizePrecision::lessSpecific()); } - return new self($this->keyTypes, $valueTypes, $this->nextAutoIndex, $this->optionalKeys); + return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys); } /** @@ -825,7 +843,7 @@ public function traverse(callable $cb): Type return $this; } - return new self($this->keyTypes, $valueTypes, $this->nextAutoIndex, $this->optionalKeys); + return new self($this->keyTypes, $valueTypes, $this->nextAutoIndexes, $this->optionalKeys); } public function isKeysSupersetOf(self $otherArray): bool @@ -873,7 +891,10 @@ public function mergeWith(self $otherArray): self $optionalKeys = array_values(array_unique($optionalKeys)); - return new self($this->keyTypes, $valueTypes, $this->nextAutoIndex, $optionalKeys); + $nextAutoIndexes = array_unique(array_merge($this->nextAutoIndexes, $otherArray->nextAutoIndexes)); + sort($nextAutoIndexes); + + return new self($this->keyTypes, $valueTypes, $nextAutoIndexes, $optionalKeys); } /** @@ -902,7 +923,7 @@ public function makeOffsetRequired(Type $offsetType): self foreach ($optionalKeys as $j => $key) { if ($i === $key) { unset($optionalKeys[$j]); - return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndex, array_values($optionalKeys)); + return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, array_values($optionalKeys)); } } @@ -917,7 +938,7 @@ public function makeOffsetRequired(Type $offsetType): self */ public static function __set_state(array $properties): Type { - return new self($properties['keyTypes'], $properties['valueTypes'], $properties['nextAutoIndex'], $properties['optionalKeys'] ?? []); + return new self($properties['keyTypes'], $properties['valueTypes'], $properties['nextAutoIndexes'] ?? $properties['nextAutoIndex'], $properties['optionalKeys'] ?? []); } } diff --git a/src/Type/Constant/ConstantArrayTypeBuilder.php b/src/Type/Constant/ConstantArrayTypeBuilder.php index 6af191026b..6c263c2b63 100644 --- a/src/Type/Constant/ConstantArrayTypeBuilder.php +++ b/src/Type/Constant/ConstantArrayTypeBuilder.php @@ -9,8 +9,11 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use function array_filter; +use function array_map; +use function array_unique; use function array_values; use function count; +use function in_array; use function is_float; use function max; use function range; @@ -26,12 +29,13 @@ class ConstantArrayTypeBuilder /** * @param array $keyTypes * @param array $valueTypes + * @param non-empty-list $nextAutoIndexes * @param array $optionalKeys */ private function __construct( private array $keyTypes, private array $valueTypes, - private int $nextAutoIndex, + private array $nextAutoIndexes, private array $optionalKeys, ) { @@ -39,7 +43,7 @@ private function __construct( public static function createEmpty(): self { - return new self([], [], 0, []); + return new self([], [], [0], []); } public static function createFromConstantArray(ConstantArrayType $startArrayType): self @@ -47,7 +51,7 @@ public static function createFromConstantArray(ConstantArrayType $startArrayType $builder = new self( $startArrayType->getKeyTypes(), $startArrayType->getValueTypes(), - $startArrayType->getNextAutoIndex(), + $startArrayType->getNextAutoIndexes(), $startArrayType->getOptionalKeys(), ); @@ -60,36 +64,107 @@ public static function createFromConstantArray(ConstantArrayType $startArrayType public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $optional = false): void { - if ($offsetType === null) { - $offsetType = new ConstantIntegerType($this->nextAutoIndex); - } else { + if ($offsetType !== null) { $offsetType = ArrayType::castToArrayKeyType($offsetType); } if (!$this->degradeToGeneralArray) { + if ($offsetType === null) { + $newAutoIndexes = $optional ? $this->nextAutoIndexes : []; + $hasOptional = false; + foreach ($this->keyTypes as $i => $keyType) { + if (!$keyType instanceof ConstantIntegerType) { + continue; + } + + if (!in_array($keyType->getValue(), $this->nextAutoIndexes, true)) { + continue; + } + + $this->valueTypes[$i] = TypeCombinator::union($this->valueTypes[$i], $valueType); + + /** @var int|float $newAutoIndex */ + $newAutoIndex = $keyType->getValue() + 1; + if (is_float($newAutoIndex)) { + $newAutoIndex = $keyType->getValue(); + } + + $newAutoIndexes[] = $newAutoIndex; + $hasOptional = true; + } + + $max = max($this->nextAutoIndexes); + + $this->keyTypes[] = new ConstantIntegerType($max); + $this->valueTypes[] = $valueType; + + /** @var int|float $newAutoIndex */ + $newAutoIndex = $max + 1; + if (is_float($newAutoIndex)) { + $newAutoIndex = $max; + } + + $newAutoIndexes[] = $newAutoIndex; + $this->nextAutoIndexes = array_unique($newAutoIndexes); + + if ($optional || $hasOptional) { + $this->optionalKeys[] = count($this->keyTypes) - 1; + } + + if (count($this->keyTypes) > self::ARRAY_COUNT_LIMIT) { + $this->degradeToGeneralArray = true; + } + + return; + } + if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) { /** @var ConstantIntegerType|ConstantStringType $keyType */ foreach ($this->keyTypes as $i => $keyType) { - if ($keyType->getValue() === $offsetType->getValue()) { - $this->valueTypes[$i] = $valueType; + if ($keyType->getValue() !== $offsetType->getValue()) { + continue; + } + + if ($optional) { + $valueType = TypeCombinator::union($valueType, $this->valueTypes[$i]); + } + + $this->valueTypes[$i] = $valueType; + + if (!$optional) { $this->optionalKeys = array_values(array_filter($this->optionalKeys, static fn (int $index): bool => $index !== $i)); - return; + if ($keyType instanceof ConstantIntegerType) { + $nextAutoIndexes = array_values(array_filter($this->nextAutoIndexes, static fn (int $index) => $index > $keyType->getValue())); + if (count($nextAutoIndexes) === 0) { + throw new ShouldNotHappenException(); + } + $this->nextAutoIndexes = $nextAutoIndexes; + } } + return; } $this->keyTypes[] = $offsetType; $this->valueTypes[] = $valueType; - if ($optional) { - $this->optionalKeys[] = count($this->keyTypes) - 1; + if ($offsetType instanceof ConstantIntegerType) { + $max = max($this->nextAutoIndexes); + if ($offsetType->getValue() >= $max) { + /** @var int|float $newAutoIndex */ + $newAutoIndex = $offsetType->getValue() + 1; + if (is_float($newAutoIndex)) { + $newAutoIndex = $max; + } + if (!$optional) { + $this->nextAutoIndexes = [$newAutoIndex]; + } else { + $this->nextAutoIndexes[] = $newAutoIndex; + } + } } - /** @var int|float $newNextAutoIndex */ - $newNextAutoIndex = $offsetType instanceof ConstantIntegerType - ? max($this->nextAutoIndex, $offsetType->getValue() + 1) - : $this->nextAutoIndex; - if (!is_float($newNextAutoIndex)) { - $this->nextAutoIndex = $newNextAutoIndex; + if ($optional) { + $this->optionalKeys[] = count($this->keyTypes) - 1; } if (count($this->keyTypes) > self::ARRAY_COUNT_LIMIT) { @@ -157,6 +232,10 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt } } + if ($offsetType === null) { + $offsetType = TypeCombinator::union(...array_map(static fn (int $index) => new ConstantIntegerType($index), $this->nextAutoIndexes)); + } + $this->keyTypes[] = $offsetType; $this->valueTypes[] = $valueType; if ($optional) { @@ -180,7 +259,7 @@ public function getArray(): Type if (!$this->degradeToGeneralArray) { /** @var array $keyTypes */ $keyTypes = $this->keyTypes; - return new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndex, $this->optionalKeys); + return new ConstantArrayType($keyTypes, $this->valueTypes, $this->nextAutoIndexes, $this->optionalKeys); } $array = new ArrayType( diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index fb49da4d47..a81f1be379 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -108,7 +108,7 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], ); } diff --git a/src/Type/Generic/TemplateConstantArrayType.php b/src/Type/Generic/TemplateConstantArrayType.php index f4496e0b79..6ca0f99251 100644 --- a/src/Type/Generic/TemplateConstantArrayType.php +++ b/src/Type/Generic/TemplateConstantArrayType.php @@ -22,7 +22,7 @@ public function __construct( ConstantArrayType $bound, ) { - parent::__construct($bound->getKeyTypes(), $bound->getValueTypes(), $bound->getNextAutoIndex(), $bound->getOptionalKeys()); + parent::__construct($bound->getKeyTypes(), $bound->getValueTypes(), $bound->getNextAutoIndexes(), $bound->getOptionalKeys()); $this->scope = $scope; $this->strategy = $templateTypeStrategy; $this->variance = $templateTypeVariance; diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index 3ff1e48778..42bb93638a 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -74,7 +74,7 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], ); } diff --git a/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php b/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php index 2149ad66d2..f5381f9099 100644 --- a/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php @@ -6,10 +6,12 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; class ArgumentBasedFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -58,10 +60,15 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $argumentValueType = $argumentValueType->getIterableValueType()->generalize(GeneralizePrecision::moreSpecific()); } - return new ArrayType( + $array = new ArrayType( $argumentKeyType, $argumentValueType, ); + if ($functionReflection->getName() === 'array_unique' && $argumentType->isIterableAtLeastOnce()->yes()) { + $array = TypeCombinator::intersect($array, new NonEmptyArrayType()); + } + + return $array; } } diff --git a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php index cf614a5015..c127bf408b 100644 --- a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php @@ -64,6 +64,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new ConstantArrayType( $keyTypes, $valueTypes, + $keysParamType->getNextAutoIndexes(), ); } } diff --git a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php index 14b51256c7..c31c493465 100644 --- a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php @@ -62,7 +62,7 @@ public function getTypeFromFunctionCall( $limit = new NullType(); } - $constantArrays = TypeUtils::getConstantArrays($valueType); + $constantArrays = TypeUtils::getOldConstantArrays($valueType); if (count($constantArrays) === 0) { $arrays = TypeUtils::getArrays($valueType); if (count($arrays) !== 0) { diff --git a/src/Type/Php/CountFunctionReturnTypeExtension.php b/src/Type/Php/CountFunctionReturnTypeExtension.php index c9d13e34cd..b037a8422d 100644 --- a/src/Type/Php/CountFunctionReturnTypeExtension.php +++ b/src/Type/Php/CountFunctionReturnTypeExtension.php @@ -42,7 +42,7 @@ public function getTypeFromFunctionCall( } $argType = $scope->getType($functionCall->getArgs()[0]->value); - $constantArrays = TypeUtils::getConstantArrays($scope->getType($functionCall->getArgs()[0]->value)); + $constantArrays = TypeUtils::getOldConstantArrays($scope->getType($functionCall->getArgs()[0]->value)); if (count($constantArrays) === 0) { if ($argType->isIterableAtLeastOnce()->yes()) { return IntegerRangeType::fromInterval(1, null); diff --git a/src/Type/Php/HrtimeFunctionReturnTypeExtension.php b/src/Type/Php/HrtimeFunctionReturnTypeExtension.php index 05418cf926..2192fc8745 100644 --- a/src/Type/Php/HrtimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/HrtimeFunctionReturnTypeExtension.php @@ -26,7 +26,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $arrayType = new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new IntegerType(), new IntegerType()], 2); + $arrayType = new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new IntegerType(), new IntegerType()], [2]); $numberType = TypeUtils::toBenevolentUnion(TypeCombinator::union(new IntegerType(), new FloatType())); if (count($functionCall->getArgs()) < 1) { diff --git a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php index dce61f03fd..a8407cc52e 100644 --- a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php @@ -147,7 +147,7 @@ private static function createConstantArrayFrom(array $constantArray, Scope $sco $i++; } - return new ConstantArrayType($keyTypes, $valueTypes, $isList ? $i : 0); + return new ConstantArrayType($keyTypes, $valueTypes, $isList ? [$i] : [0]); } } diff --git a/src/Type/ResourceType.php b/src/Type/ResourceType.php index 1f1028b11a..c676787e24 100644 --- a/src/Type/ResourceType.php +++ b/src/Type/ResourceType.php @@ -64,7 +64,7 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], ); } diff --git a/src/Type/StringType.php b/src/Type/StringType.php index f151910b3d..7769dd4433 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -131,7 +131,7 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1, + [1], ); } diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 868ad502ab..454ca9717f 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -175,7 +175,13 @@ public static function union(Type ...$types): Type } $arrayTypes[] = $types[$i]; - $arrayAccessoryTypes[] = []; + + if ($types[$i]->isIterableAtLeastOnce()->yes()) { + $nonEmpty = new NonEmptyArrayType(); + $arrayAccessoryTypes[] = [$nonEmpty->describe(VerbosityLevel::cache()) => $nonEmpty]; + } else { + $arrayAccessoryTypes[] = []; + } unset($types[$i]); } @@ -588,7 +594,8 @@ private static function processArrayTypes(array $arrayTypes, array $accessoryTyp $builder->setOffsetValueType($data['keyType'], $data['valueType'], $data['optional']); } - $resultArrays[] = self::intersect($builder->getArray(), ...$accessoryTypes); + $arr = self::intersect($builder->getArray(), ...$accessoryTypes); + $resultArrays[] = $arr; } return self::reduceArrays($resultArrays); diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 5f11d19edc..291aa150fb 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -631,6 +631,16 @@ public function testBug6940(): void $this->assertNoErrors($errors); } + public function testBug4308(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-4308.php'); + $this->assertNoErrors($errors); + } + /** * @param string[]|null $allAnalysedFiles * @return Error[] diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 0d0b1f776e..8fb0f8341a 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -2596,7 +2596,7 @@ public function dataBinaryOperations(): array '$conditionalArray + $unshiftedConditionalArray', ], [ - 'array{0: \'lorem\', 1: stdClass, 2: 1, 3: 1, 4: 1, 5?: 2|3, 6?: 3}', + 'array{0: \'lorem\', 1: stdClass, 2: 1, 3: 1|2, 4: 1|3, 5?: 2|3, 6?: 3}', '$unshiftedConditionalArray + $conditionalArray', ], [ @@ -2752,7 +2752,7 @@ public function dataBinaryOperations(): array 'count($appendingToArrayInBranches)', ], [ - '3|4|5', + 'int<3, 5>', 'count($conditionalArray)', ], [ @@ -3040,7 +3040,7 @@ public function dataBinaryOperations(): array '$anotherConditionalString . $conditionalString', ], [ - '6|7|8', + 'int<6, 8>', 'count($conditionalArray) + count($array)', ], [ @@ -3132,11 +3132,11 @@ public function dataBinaryOperations(): array '$coalesceArray', ], [ - 'array', + 'array<0|1|2, 1|2|3>', '$arrayToBeUnset', ], [ - 'array', + 'array<0|1|2, 1|2|3>', '$arrayToBeUnset2', ], [ @@ -4530,6 +4530,10 @@ public function dataArrayFunctions(): array '123', '$filteredMixed[0]', ], + [ + 'non-empty-array<0|1|2, 1|2|3>', + '$uniquedIntegers', + ], [ '1|2|3', '$uniquedIntegers[1]', @@ -8047,11 +8051,11 @@ public function dataArrayKeysInBranches(): array '$arrayAppendedInIf', ], [ - 'array', + 'non-empty-array', '$arrayAppendedInForeach', ], [ - 'array, literal-string&non-empty-string>', // could be 'array, \'bar\'|\'baz\'|\'foo\'>' + 'non-empty-array, literal-string&non-empty-string>', // could be 'array, \'bar\'|\'baz\'|\'foo\'>' '$anotherArrayAppendedInForeach', ], [ @@ -8429,7 +8433,7 @@ public function dataIsset(): array '$anotherArrayCopy', ], [ - 'array', + "array<'a'|'b'|'c', 1|2|3|4|null>", '$yetAnotherArrayCopy', ], [ diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index ff72a3fd2c..4d9d532437 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -852,6 +852,7 @@ public function dataFileAsserts(): iterable } yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6927.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-array-optional-set.php'); } /** diff --git a/tests/PHPStan/Analyser/data/bug-4308.php b/tests/PHPStan/Analyser/data/bug-4308.php new file mode 100644 index 0000000000..9584ebaf12 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4308.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug4308; + +class Test +{ + /** + * @var (string|int|null)[] + * @phpstan-var array{ + * prop1?: string, prop2?: string, prop3?: string, + * prop4?: string, prop5?: string, prop6?: string, + * prop7?: string, prop8?: int, prop9?: int + * } + */ + protected array $updateData = []; + + /** + * @phpstan-param array{ + * prop1?: string, prop2?: string, prop3?: string, + * prop4?: string, prop5?: string, prop6?: string, + * prop7?: string, prop8?: int, prop9?: int + * } $data + */ + public function update(array $data): void + { + $this->updateData = $data + $this->updateData; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4700.php b/tests/PHPStan/Analyser/data/bug-4700.php index e50cce6f7c..45f09eaf9f 100644 --- a/tests/PHPStan/Analyser/data/bug-4700.php +++ b/tests/PHPStan/Analyser/data/bug-4700.php @@ -18,10 +18,10 @@ function(array $array, int $count): void { if (isset($array['d'])) $a[] = $array['d']; if (isset($array['e'])) $a[] = $array['e']; if (count($a) >= $count) { - assertType('1|2|3|4|5', count($a)); + assertType('int<1, 5>', count($a)); assertType('array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); } else { - assertType('0|1|2|3|4|5', count($a)); + assertType('int<0, 5>', count($a)); assertType('array{}|array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); } }; @@ -40,10 +40,10 @@ function(array $array, int $count): void { if (isset($array['d'])) $a[] = $array['d']; if (isset($array['e'])) $a[] = $array['e']; if (count($a) > $count) { - assertType('2|3|4|5', count($a)); + assertType('int<2, 5>', count($a)); assertType('array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); } else { - assertType('0|1|2|3|4|5', count($a)); + assertType('int<0, 5>', count($a)); assertType('array{}|array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); } }; diff --git a/tests/PHPStan/Analyser/data/bug-6936-limit.php b/tests/PHPStan/Analyser/data/bug-6936-limit.php index 63e9ab89f4..421d3d2ffe 100644 --- a/tests/PHPStan/Analyser/data/bug-6936-limit.php +++ b/tests/PHPStan/Analyser/data/bug-6936-limit.php @@ -36,17 +36,16 @@ public function testLimits():void $arr[] = 'g'; } - assertType("array{0: 1|'a'|'b'|'c'|'d'|'e'|'f'|'g', 1: 2|'b', 2: 3|'c', 3?: 'd', 4?: 'e', 5?: 'f', 6?: 'g'}", $arr + $arr2); + assertType("array{0: 1|'a'|'b'|'c'|'d'|'e'|'f'|'g', 1: 2|'b'|'c'|'d'|'e'|'f'|'g', 2: 3|'c'|'d'|'e'|'f'|'g', 3?: 'd'|'e'|'f'|'g', 4?: 'e'|'f'|'g', 5?: 'f'|'g', 6?: 'g'}", $arr + $arr2); if (rand(0,1)) { $arr[] = 'h'; } - assertType("array{0: 1|'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h', 1: 2|'b', 2: 3|'c', 3?: 'd', 4?: 'e', 5?: 'f', 6?: 'g', 7?: 'h'}", $arr + $arr2); + assertType("array{0: 1|'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h', 1: 2|'b'|'c'|'d'|'e'|'f'|'g'|'h', 2: 3|'c'|'d'|'e'|'f'|'g'|'h', 3?: 'd'|'e'|'f'|'g'|'h', 4?: 'e'|'f'|'g'|'h', 5?: 'f'|'g'|'h', 6?: 'g'|'h', 7?: 'h'}", $arr + $arr2); if (rand(0,1)) { $arr[] = 'i'; } - // fallback to a less precise form, which reduces the union-type size - assertType("non-empty-array<0|1|2|3|4|5|6|7|8, 1|2|3|'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i'>", $arr + $arr2); + assertType("array{0: 1|'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i', 1: 2|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i', 2: 3|'c'|'d'|'e'|'f'|'g'|'h'|'i', 3?: 'd'|'e'|'f'|'g'|'h'|'i', 4?: 'e'|'f'|'g'|'h'|'i', 5?: 'f'|'g'|'h'|'i', 6?: 'g'|'h'|'i', 7?: 'h'|'i', 8?: 'i'}", $arr + $arr2); } } diff --git a/tests/PHPStan/Analyser/data/constant-array-optional-set.php b/tests/PHPStan/Analyser/data/constant-array-optional-set.php new file mode 100644 index 0000000000..c61745e3e4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/constant-array-optional-set.php @@ -0,0 +1,114 @@ +|int $nextAutoIndexes + * @return void + */ + public function doFoo($nextAutoIndexes) + { + assertType('non-empty-array|int', $nextAutoIndexes); + if (is_int($nextAutoIndexes)) { + assertType('int', $nextAutoIndexes); + } else { + assertType('non-empty-array', $nextAutoIndexes); + } + assertType('non-empty-array|int', $nextAutoIndexes); + } + + /** + * @param non-empty-list|int $nextAutoIndexes + * @return void + */ + public function doBar($nextAutoIndexes) + { + assertType('non-empty-array|int', $nextAutoIndexes); + if (is_int($nextAutoIndexes)) { + $nextAutoIndexes = [$nextAutoIndexes]; + assertType('array{int}', $nextAutoIndexes); + } else { + assertType('non-empty-array', $nextAutoIndexes); + } + assertType('non-empty-array', $nextAutoIndexes); + } + +} + +class Baz +{ + + public function doFoo() + { + $conditionalArray = [1, 1, 1]; + if (doFoo()) { + $conditionalArray[] = 2; + $conditionalArray[] = 3; + } + + assertType('array{0: 1, 1: 1, 2: 1, 3?: 2, 4?: 3}', $conditionalArray); + + $unshiftedConditionalArray = $conditionalArray; + array_unshift($unshiftedConditionalArray, 'lorem', new \stdClass()); + assertType('array{0: \'lorem\', 1: stdClass, 2: 1, 3: 1, 4: 1, 5?: 2|3, 6?: 3}', $unshiftedConditionalArray); + + assertType('array{0: 1, 1: 1, 2: 1, 3: 1|2, 4: 1|3, 5?: 2|3, 6?: 3}', $conditionalArray + $unshiftedConditionalArray); + assertType('array{0: \'lorem\', 1: stdClass, 2: 1, 3: 1|2, 4: 1|3, 5?: 2|3, 6?: 3}', $unshiftedConditionalArray + $conditionalArray); + + $conditionalArray[] = 4; + assertType('array{0: 1, 1: 1, 2: 1, 3?: 2|4, 4?: 3, 5?: 4}', $conditionalArray); + } + +} diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php new file mode 100644 index 0000000000..968518bac9 --- /dev/null +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php @@ -0,0 +1,85 @@ +setOffsetValueType(null, new ConstantIntegerType(1)); + + $array1 = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array1); + $this->assertSame('array{1}', $array1->describe(VerbosityLevel::precise())); + $this->assertSame([1], $array1->getNextAutoIndexes()); + + $builder->setOffsetValueType(null, new ConstantIntegerType(2), true); + $array2 = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array2); + $this->assertSame('array{0: 1, 1?: 2}', $array2->describe(VerbosityLevel::precise())); + $this->assertSame([1, 2], $array2->getNextAutoIndexes()); + + $builder->setOffsetValueType(null, new ConstantIntegerType(3)); + $array3 = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array3); + $this->assertSame('array{0: 1, 1?: 2|3, 2?: 3}', $array3->describe(VerbosityLevel::precise())); + $this->assertSame([2, 3], $array3->getNextAutoIndexes()); + + $this->assertTrue($array3->isKeysSupersetOf($array2)); + $array2MergedWith3 = $array3->mergeWith($array2); + $this->assertSame('array{0: 1, 1?: 2|3, 2?: 3}', $array2MergedWith3->describe(VerbosityLevel::precise())); + $this->assertSame([1, 2, 3], $array2MergedWith3->getNextAutoIndexes()); + + $builder->setOffsetValueType(null, new ConstantIntegerType(4)); + $array4 = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array4); + $this->assertSame('array{0: 1, 1?: 2|3, 2?: 3|4, 3?: 4}', $array4->describe(VerbosityLevel::precise())); + $this->assertSame([3, 4], $array4->getNextAutoIndexes()); + + $builder->setOffsetValueType(new ConstantIntegerType(3), new ConstantIntegerType(5), true); + $array5 = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array5); + $this->assertSame('array{0: 1, 1?: 2|3, 2?: 3|4, 3?: 4|5}', $array5->describe(VerbosityLevel::precise())); + $this->assertSame([3, 4], $array5->getNextAutoIndexes()); + + $builder->setOffsetValueType(new ConstantIntegerType(3), new ConstantIntegerType(6)); + $array6 = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array6); + $this->assertSame('array{0: 1, 1?: 2|3, 2?: 3|4, 3: 6}', $array6->describe(VerbosityLevel::precise())); + $this->assertSame([4], $array6->getNextAutoIndexes()); + } + + public function testNextAutoIndex(): void + { + $builder = ConstantArrayTypeBuilder::createFromConstantArray(new ConstantArrayType( + [new ConstantIntegerType(0)], + [new ConstantStringType('foo')], + [1], + )); + $builder->setOffsetValueType(new ConstantIntegerType(0), new ConstantStringType('bar')); + $array = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array); + $this->assertSame('array{\'bar\'}', $array->describe(VerbosityLevel::precise())); + $this->assertSame([1], $array->getNextAutoIndexes()); + } + + public function testNextAutoIndexAnother(): void + { + $builder = ConstantArrayTypeBuilder::createFromConstantArray(new ConstantArrayType( + [new ConstantIntegerType(0)], + [new ConstantStringType('foo')], + [1], + )); + $builder->setOffsetValueType(new ConstantIntegerType(1), new ConstantStringType('bar')); + $array = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array); + $this->assertSame('array{\'foo\', \'bar\'}', $array->describe(VerbosityLevel::precise())); + $this->assertSame([2], $array->getNextAutoIndexes()); + } + +}