diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index cb009f52e5..3da6c5cbfb 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -128,6 +128,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function toNumber(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 2dada3492f..50aad6085a 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -133,6 +133,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function toNumber(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php index f4a4f7662b..a57bf262bc 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -134,6 +134,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function toNumber(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 29585d5c36..67fb6472f1 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -133,6 +133,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function toNumber(): Type { return new UnionType([ diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 8e8e3273ae..8760f1ffd7 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -144,6 +144,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index 23e8523b20..bf0dfbe054 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -163,6 +163,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index 2c90b28108..430d14c231 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -141,6 +141,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php new file mode 100644 index 0000000000..ee820e3c5f --- /dev/null +++ b/src/Type/Accessory/OversizedArrayType.php @@ -0,0 +1,208 @@ +isAcceptedBy($this, $strictTypes); + } + + return $type->isArray() + ->and($type->isIterableAtLeastOnce()); + } + + public function isSuperTypeOf(Type $type): TrinaryLogic + { + if ($this->equals($type)) { + return TrinaryLogic::createYes(); + } + + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + return $type->isArray() + ->and($type->isOversizedArray()); + } + + public function isSubTypeOf(Type $otherType): TrinaryLogic + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + return $otherType->isArray() + ->and($otherType->isOversizedArray()) + ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + { + return $this->isSubTypeOf($acceptingType); + } + + public function equals(Type $type): bool + { + return $type instanceof self; + } + + public function describe(VerbosityLevel $level): string + { + return 'oversized-array'; + } + + public function isOffsetAccessible(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getOffsetValueType(Type $offsetType): Type + { + return new MixedType(); + } + + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + + public function isIterable(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isIterableAtLeastOnce(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function getIterableKeyType(): Type + { + return new MixedType(); + } + + public function getIterableValueType(): Type + { + return new MixedType(); + } + + public function isArray(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function toNumber(): Type + { + return new ErrorType(); + } + + public function toInteger(): Type + { + return new ConstantIntegerType(1); + } + + public function toFloat(): Type + { + return new ConstantFloatType(1.0); + } + + public function toString(): Type + { + return new ErrorType(); + } + + public function toArray(): Type + { + return new MixedType(); + } + + public function traverse(callable $cb): Type + { + return $this; + } + + public static function __set_state(array $properties): Type + { + return new self(); + } + +} diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 49cc1712b4..17ff1f3dd6 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -207,6 +207,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index 2f30531584..62ea5e002f 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -315,6 +315,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index a5067bf538..3b98321c22 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -395,6 +395,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index bc8a6cfd27..2ace3329d1 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -117,6 +117,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 9249808218..074990846d 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -19,7 +19,6 @@ use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\AccessoryType; -use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; @@ -27,7 +26,6 @@ use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonRemoveableTypeTrait; use function array_map; -use function array_values; use function count; use function implode; use function in_array; @@ -42,43 +40,21 @@ class IntersectionType implements CompoundType use NonRemoveableTypeTrait; use NonGeneralizableTypeTrait; - /** @var Type[] */ - private array $types; - private bool $sortedTypes = false; /** * @api * @param Type[] $types */ - public function __construct(array $types) + public function __construct(private array $types) { - $hasOffsetValueTypeCount = 0; - $newTypes = []; - foreach ($types as $type) { - if (!$type instanceof HasOffsetValueType) { - $newTypes[] = $type; - continue; - } - - $hasOffsetValueTypeCount++; - if ($hasOffsetValueTypeCount > 32) { - continue; - } - - $newTypes[] = $type; - } - - $newTypes = array_values($newTypes); - if (count($newTypes) < 2) { + if (count($types) < 2) { throw new ShouldNotHappenException(sprintf( 'Cannot create %s with: %s', self::class, - implode(', ', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::value()), $newTypes)), + implode(', ', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::value()), $types)), )); } - - $this->types = $newTypes; } /** @@ -420,6 +396,11 @@ public function isArray(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isArray()); } + public function isOversizedArray(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray()); + } + public function isString(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isString()); diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index 6428889dcd..9b381ef3da 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -234,6 +234,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/JustNullableTypeTrait.php b/src/Type/JustNullableTypeTrait.php index 7dc5d7e1bc..a2895ff246 100644 --- a/src/Type/JustNullableTypeTrait.php +++ b/src/Type/JustNullableTypeTrait.php @@ -57,6 +57,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 8b950f5d32..b0ecf936da 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -21,6 +21,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Generic\TemplateMixedType; @@ -438,6 +439,22 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isOversizedArray(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $oversizedArray = TypeCombinator::intersect( + new ArrayType(new MixedType(), new MixedType()), + new OversizedArrayType(), + ); + + if ($this->subtractedType->isSuperTypeOf($oversizedArray)->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + public function isString(): TrinaryLogic { if ($this->subtractedType !== null) { diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index d48c4a7f9f..bb9e34b915 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -233,6 +233,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/NullType.php b/src/Type/NullType.php index 432008312d..9e0be8341f 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -175,6 +175,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index b4aaafefb8..0bfeac32ac 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -797,6 +797,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index eaa60c4003..7cd9706e68 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -332,6 +332,11 @@ public function isArray(): TrinaryLogic return $this->getStaticObjectType()->isArray(); } + public function isOversizedArray(): TrinaryLogic + { + return $this->getStaticObjectType()->isOversizedArray(); + } + public function isString(): TrinaryLogic { return $this->getStaticObjectType()->isString(); diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index 9df8237e7d..5276ef19f7 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -153,6 +153,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index f550a0831b..858533a9b1 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -130,6 +130,11 @@ public function isArray(): TrinaryLogic return $this->resolve()->isArray(); } + public function isOversizedArray(): TrinaryLogic + { + return $this->resolve()->isOversizedArray(); + } + public function isOffsetAccessible(): TrinaryLogic { return $this->resolve()->isOffsetAccessible(); diff --git a/src/Type/Traits/ObjectTypeTrait.php b/src/Type/Traits/ObjectTypeTrait.php index 92a7b1f48f..cf7562dc9b 100644 --- a/src/Type/Traits/ObjectTypeTrait.php +++ b/src/Type/Traits/ObjectTypeTrait.php @@ -105,6 +105,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Type.php b/src/Type/Type.php index 4fd05a67b9..c9d7657bd8 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -63,6 +63,8 @@ public function getIterableValueType(): Type; public function isArray(): TrinaryLogic; + public function isOversizedArray(): TrinaryLogic; + public function isOffsetAccessible(): TrinaryLogic; public function hasOffsetValueType(Type $offsetType): TrinaryLogic; diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 727e7e5a05..a2c5c9403f 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -7,6 +7,7 @@ use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\Accessory\OversizedArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; @@ -805,6 +806,7 @@ public static function intersect(Type ...$types): Type // transform A & (B & C) to A & B & C for ($i = 0; $i < $typesCount; $i++) { $type = $types[$i]; + if (!($type instanceof IntersectionType)) { continue; } @@ -813,6 +815,23 @@ public static function intersect(Type ...$types): Type $typesCount = count($types); } + $hasOffsetValueTypeCount = 0; + $newTypes = []; + foreach ($types as $type) { + if (!$type instanceof HasOffsetValueType) { + $newTypes[] = $type; + continue; + } + + $hasOffsetValueTypeCount++; + } + + if ($hasOffsetValueTypeCount > 32) { + $newTypes[] = new OversizedArrayType(); + $types = array_values($newTypes); + $typesCount = count($types); + } + usort($types, static function (Type $a, Type $b): int { // move subtractables with subtracts before those without to avoid loosing them in the union logic if ($a instanceof SubtractableType && $a->getSubtractedType() !== null) { @@ -944,6 +963,18 @@ public static function intersect(Type ...$types): Type continue 2; } + if ($types[$i] instanceof OversizedArrayType && $types[$j] instanceof HasOffsetValueType) { + array_splice($types, $j--, 1); + $typesCount--; + continue; + } + + if ($types[$j] instanceof OversizedArrayType && $types[$i] instanceof HasOffsetValueType) { + array_splice($types, $i--, 1); + $typesCount--; + continue 2; + } + if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof ArrayType && !$types[$j] instanceof ConstantArrayType) { $newArray = ConstantArrayTypeBuilder::createEmpty(); $valueTypes = $types[$i]->getValueTypes(); diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 9ae63c6cde..41f90bb02f 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -424,6 +424,11 @@ public function isArray(): TrinaryLogic return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isArray()); } + public function isOversizedArray(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray()); + } + public function isString(): TrinaryLogic { return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isString()); diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index 5b5b05563c..5f3e8f4cea 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -102,6 +102,11 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isOversizedArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/tests/PHPStan/Analyser/data/bug-5081.php b/tests/PHPStan/Analyser/data/bug-5081.php index f089863bc2..4d6c09089c 100644 --- a/tests/PHPStan/Analyser/data/bug-5081.php +++ b/tests/PHPStan/Analyser/data/bug-5081.php @@ -2,6 +2,8 @@ namespace Bug5081; +use function PHPStan\Testing\assertType; + global $_LANGADM; $_LANGADM = []; @@ -501,3 +503,4 @@ $_LANGADM['AdminCustomerPreferencesf2c822352f0e0a62e2de6d716475911b'] = 'Standard (account creation and address creation)'; $_LANGADM['AdminCustomerPreferences0db377921f4ce762c62526131097968f'] = 'General'; $_LANGADM['AdminCustomerPreferencesbcb9adf1d2347258b5c65483e34cf86f'] = 'Registration process type'; +assertType('non-empty-array&oversized-array', $_LANGADM); diff --git a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php index f88ce86077..372b594683 100644 --- a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php @@ -42,7 +42,7 @@ public function testRuleOutOfPhpStan(): void ], [ 'Implementing PHPStan\Reflection\ReflectionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 312, + 317, $tip, ], ]); diff --git a/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php b/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php index f81bed22ab..e705828b93 100644 --- a/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php +++ b/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php @@ -156,6 +156,11 @@ public function isArray(): \PHPStan\TrinaryLogic // TODO: Implement isArray() method. } + public function isOversizedArray(): \PHPStan\TrinaryLogic + { + // TODO: Implement isOversizedArray() method. + } + public function isOffsetAccessible(): \PHPStan\TrinaryLogic { // TODO: Implement isOffsetAccessible() method.