From 02da3194774a1d3b7a4d069845ffe7dbc8cbced6 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Fri, 9 Oct 2026 23:58:02 +0200 Subject: [PATCH 1/3] Keep the array part of a union after unset() of an offset UnionType::unsetOffset() mapped every member through unsetOffset(). A member without offsets (false, int, string, ...) returns ErrorType there, and the union with ErrorType made the whole variable *ERROR*, so every later check on it went silent. Leave those members out, as getOffsetValueType() already does, and return ErrorType only when no member is left. unset() of an offset leaves false as it is (a deprecation since PHP 8.1) and throws on true, so false now keeps itself, as null already does, and bool becomes false. BenevolentUnionType keeps its previous unionTypes() call, which already leaves out ErrorType results and keeps the result benevolent. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/Type/BenevolentUnionType.php | 5 + src/Type/BooleanType.php | 6 + src/Type/Constant/ConstantBooleanType.php | 9 + src/Type/UnionType.php | 29 ++- .../Analyser/nsrt/unset-offset-on-union.php | 109 +++++++++++ turbo-ext/src/BenevolentUnionType.cpp | 27 ++- turbo-ext/src/BooleanType.cpp | 20 +- turbo-ext/src/ConstantBooleanType.cpp | 16 ++ turbo-ext/src/UnionType.cpp | 30 ++- turbo-ext/src/generated/BenevolentUnionType.h | 76 ++++---- turbo-ext/src/generated/BooleanType.h | 181 +++++++++--------- turbo-ext/src/generated/ConstantBooleanType.h | 31 +-- turbo-ext/tests/type-family.php | 3 + 13 files changed, 395 insertions(+), 147 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/unset-offset-on-union.php diff --git a/src/Type/BenevolentUnionType.php b/src/Type/BenevolentUnionType.php index 5a44af5e202..4fe27d46760 100644 --- a/src/Type/BenevolentUnionType.php +++ b/src/Type/BenevolentUnionType.php @@ -96,6 +96,11 @@ protected function pickFromTypes( return $values; } + public function unsetOffset(Type $offsetType): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType)); + } + public function getOffsetValueType(Type $offsetType): Type { $types = []; diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index f7062440278..73a5084cd8c 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -49,6 +49,12 @@ public function getConstantStrings(): array return []; } + /** unset() of an offset leaves false as it is (deprecated since PHP 8.1) and throws on true. */ + public function unsetOffset(Type $offsetType): Type + { + return new ConstantBooleanType(false); + } + public function getConstantScalarTypes(): array { return [new ConstantBooleanType(true), new ConstantBooleanType(false)]; diff --git a/src/Type/Constant/ConstantBooleanType.php b/src/Type/Constant/ConstantBooleanType.php index 4c71f00b2d1..6479dbd6738 100644 --- a/src/Type/Constant/ConstantBooleanType.php +++ b/src/Type/Constant/ConstantBooleanType.php @@ -137,6 +137,15 @@ public function isFalse(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->value === false); } + public function unsetOffset(Type $offsetType): Type + { + if ($this->value) { + return new ErrorType(); + } + + return $this; + } + public function generalize(GeneralizePrecision $precision): Type { return new BooleanType(); diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 4edc065cd63..a658aae0aba 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -1004,9 +1004,36 @@ public function setExistingOffsetValueType(Type $offsetType, Type $valueType): T return $this->unionTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType)); } + /** + * Members that cannot have an offset unset (true, int, string, ...) yield ErrorType. + * They are left out instead of turning the whole union into ErrorType. + */ public function unsetOffset(Type $offsetType): Type { - return $this->unionTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType)); + $types = []; + $changed = false; + foreach ($this->types as $innerType) { + $newType = $innerType->unsetOffset($offsetType); + if ($newType instanceof ErrorType) { + $changed = true; + continue; + } + if ($newType !== $innerType) { + $changed = true; + } + + $types[] = $newType; + } + + if (count($types) === 0) { + return new ErrorType(); + } + + if (!$changed) { + return $this; + } + + return TypeCombinator::union(...$types); } public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type diff --git a/tests/PHPStan/Analyser/nsrt/unset-offset-on-union.php b/tests/PHPStan/Analyser/nsrt/unset-offset-on-union.php new file mode 100644 index 00000000000..336a327633e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/unset-offset-on-union.php @@ -0,0 +1,109 @@ += 8.0 + +namespace UnsetOffsetOnUnion; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @param array{port?: int, path?: string}|false $parsed + */ + public function arrayOrFalse($parsed): void + { + unset($parsed['port']); + assertType('array{path?: string}|false', $parsed); + } + + /** + * @param array{port?: int, path?: string}|string $value + */ + public function arrayOrString($value): void + { + unset($value['port']); + assertType('array{path?: string}', $value); + } + + /** + * @param array{port?: int, path?: string}|null $value + */ + public function arrayOrNull($value): void + { + unset($value['port']); + assertType('array{path?: string}|null', $value); + } + + /** + * @param array|int|false $value + */ + public function generalArray($value): void + { + unset($value['port']); + assertType('array|false', $value); + } + + /** + * @param int|string $value + */ + public function nothingToUnset($value): void + { + unset($value['port']); + assertType('*ERROR*', $value); + } + + public function parseUrl(string $url): void + { + $parsed = parse_url($url); + unset($parsed['port']); + assertType('array{scheme?: string, host?: string, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsed); + assertNativeType('array|false|null', $parsed); + } + + /** + * @param array|false $value + */ + public function nativeArrayOrFalse(array|false $value): void + { + unset($value['port']); + assertNativeType('array|false', $value); + } + + /** + * @param __benevolent $value + */ + public function benevolent($value): void + { + unset($value['port']); + assertType('(array{path?: string}|false)', $value); + } + + /** + * @param __benevolent $value + */ + public function benevolentStaysBenevolent($value): void + { + unset($value['port']); + assertType('(array{path?: string}|false|null)', $value); + } + + /** + * @param array{port?: int, path?: string}|bool $value + */ + public function arrayOrBool($value): void + { + unset($value['port']); + assertType('array{path?: string}|false', $value); + } + + /** + * @param array{port?: int, path?: string}|true $value + */ + public function arrayOrTrue($value): void + { + unset($value['port']); + assertType('array{path?: string}', $value); + } + +} diff --git a/turbo-ext/src/BenevolentUnionType.cpp b/turbo-ext/src/BenevolentUnionType.cpp index 3163402d720..03145415118 100644 --- a/turbo-ext/src/BenevolentUnionType.cpp +++ b/turbo-ext/src/BenevolentUnionType.cpp @@ -7,12 +7,12 @@ * extends it in turn), not final, declaring only what the twin declares — * the constructor forwarding to the parent's, and the overrides that make * the union benevolent: unionResults() answering yes as soon as one member - * does, unionTypes()/getOffsetValueType()/traverse()/traverseSimultaneously()/ - * tryRemove()/filterTypes() keeping the result benevolent, describe() - * parenthesizing the parent's, pickFromTypes() consulting the criteria, - * isAcceptedBy() or'ing the members, the template maps combined with - * benevolentUnion(). Everything else is inherited from the parent's - * class entry, as the PHP class inherits it. + * does, unionTypes()/unsetOffset()/getOffsetValueType()/traverse()/ + * traverseSimultaneously()/tryRemove()/filterTypes() keeping the result + * benevolent, describe() parenthesizing the parent's, pickFromTypes() + * consulting the criteria, isAcceptedBy() or'ing the members, the template + * maps combined with benevolentUnion(). Everything else is inherited from + * the parent's class entry, as the PHP class inherits it. * * The logic lives in the BenevolentUnionType handle class below, mirroring * src/Type/BenevolentUnionType.php method for method; the parent:: calls go @@ -183,6 +183,19 @@ class BenevolentUnionType return zv::Val(std::move(values)); } + /* $this->unionTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType)): + * the native body when unionTypes() is this class's own, a PHP + * subclass's override through a callable otherwise; UNDEF = pending + * exception */ + zv::Val unsetOffset(zval *offsetType) const + { + UnionMemberOp op = UnionMemberOp::call(PT_LC("unsetoffset"), 1, offsetType); + if (EXPECTED(pt_type_method_is(self, PT_LC("uniontypes"), buUnionTypes))) return unionTypes(op); + zv::Val callback = pt_union_op_callback(op); + if (UNEXPECTED(callback.isUndef())) return zv::Val(); + return callType(self, PT_LC("uniontypes"), 1, callback.raw()); + } + /* the benevolent union of the members' offset value types that are no * ErrorType, an ErrorType when none is; UNDEF = pending exception */ zv::Val getOffsetValueType(zval *offsetType) const @@ -520,6 +533,8 @@ PT_MINIT_REGISTRATION(pt_register_benevolent_union_type) cls.method(sigs::unionTypes, buUnionTypes); cls.method(sigs::pickFromTypes, buPickFromTypes); + cls.method<&BenevolentUnionType::unsetOffset, zp::Obj>(sigs::unsetOffset); + cls.method<&BenevolentUnionType::getOffsetValueType, zp::Obj>(sigs::getOffsetValueType); cls.op(); diff --git a/turbo-ext/src/BooleanType.cpp b/turbo-ext/src/BooleanType.cpp index 64c6f7c7550..7d728a70dad 100644 --- a/turbo-ext/src/BooleanType.cpp +++ b/turbo-ext/src/BooleanType.cpp @@ -28,6 +28,15 @@ class BooleanType static zv::Val getConstantStrings() { return zv::Val(zv::Arr::empty()); } + /* new ConstantBooleanType(false): unset() of an offset leaves false as + * it is and throws on true; UNDEF = pending exception */ + static zv::Val unsetOffset() + { + zval falseType; + if (UNEXPECTED(!pt_constant_boolean_type_new(&falseType, false))) return zv::Val(); + return zv::Val::adopt(falseType); + } + /* UNDEF = pending exception */ static zv::Val getConstantScalarTypes() { return bothConstants(); } @@ -244,6 +253,12 @@ PT_MINIT_REGISTRATION(pt_register_boolean_type) cls.method<&BooleanType::getConstantStrings>(sigs::getConstantStrings); + cls.method(sigs::unsetOffset, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *offsetType; + if (!zp::parse(execute_data, offsetType)) RETURN_THROWS(); + PT_RETURN_VAL(BooleanType::unsetOffset()); + }); + cls.method<&BooleanType::getConstantScalarTypes>(sigs::getConstantScalarTypes); cls.method<&BooleanType::getConstantScalarValues>(sigs::getConstantScalarValues); @@ -335,8 +350,9 @@ PT_MINIT_REGISTRATION(pt_register_boolean_type) cls.op(PT_OP_HAS_TEMPLATE_OR_LATE_RESOLVABLE_TYPE, PT_OP_LAMBDA { return zv::Val::boolean(BooleanType::hasTemplateOrLateResolvableType()); }); /* the traits, in the twin's `use` order; the class body above wins over - * every name it declares (getConstantStrings, getConstantScalarTypes, - * getConstantScalarValues, isNull, isTrue, isFalse, isBoolean) */ + * every name it declares (getConstantStrings, unsetOffset, + * getConstantScalarTypes, getConstantScalarValues, isNull, isTrue, + * isFalse, isBoolean) */ ptdecl::BooleanType::registerTraits(cls); cls.shadow(&pt_ce_boolean_type); diff --git a/turbo-ext/src/ConstantBooleanType.cpp b/turbo-ext/src/ConstantBooleanType.cpp index 81ed2a8bcca..625d05a67b9 100644 --- a/turbo-ext/src/ConstantBooleanType.cpp +++ b/turbo-ext/src/ConstantBooleanType.cpp @@ -193,6 +193,16 @@ class ConstantBooleanType return v ? PT_TRI_NO : PT_TRI_YES; } + /* an ErrorType for true, $this for false: unset() of an offset throws on + * true and leaves false as it is; UNDEF = pending exception */ + zv::Val unsetOffset() const + { + bool v = false; + if (UNEXPECTED(!value(v))) return zv::Val(); + if (v) return pt_type_new_error_type(); + return thisValue(); + } + /* new BooleanType() */ static zv::Val generalize() { @@ -324,6 +334,12 @@ PT_MINIT_REGISTRATION(pt_register_constant_boolean_type) PT_RETURN_TRINARY_OR_THROW(PT_THIS.isFalse()); }); + cls.method(sigs::unsetOffset, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *offsetType; + if (!zp::parse(execute_data, offsetType)) RETURN_THROWS(); + PT_RETURN_VAL(PT_THIS.unsetOffset()); + }); + cls.method(sigs::generalize, [](INTERNAL_FUNCTION_PARAMETERS) { zval *precision; if (!zp::parse(execute_data, precision)) RETURN_THROWS(); diff --git a/turbo-ext/src/UnionType.cpp b/turbo-ext/src/UnionType.cpp index ac355c53dfc..a69801c24c1 100644 --- a/turbo-ext/src/UnionType.cpp +++ b/turbo-ext/src/UnionType.cpp @@ -1435,7 +1435,35 @@ class UnionType return unionTypes(UnionMemberOp::call(PT_LC("setexistingoffsetvaluetype"), 2, args)); } - zv::Val unsetOffset(zval *offsetType) const { return unionTypes(UnionMemberOp::call(PT_LC("unsetoffset"), 1, offsetType)); } + /* the members' unsetOffset() results with the ErrorTypes left out: $this + * when nothing changed, an ErrorType when nothing is left, their union + * otherwise; UNDEF = pending exception */ + zv::Val unsetOffset(zval *offsetType) const + { + zval *types = this->types(); + if (UNEXPECTED(types == NULL)) return zv::Val(); + zv::Val typesCopy = zv::Val::copyOf(zv::Ref(types)); + zv::Arr newTypes = zv::Arr::create(zv::ArrRef(typesCopy.raw()).size()); + bool changed = false; + for (zv::ArrayEntry entry : zv::ArrRef(typesCopy.raw())) { + zend_object *innerType = entry.value().deref().asObject(); + zv::Val newType = callType(innerType, PT_LC("unsetoffset"), 1, offsetType); + if (UNEXPECTED(newType.isUndef())) return zv::Val(); + bool isError; + if (UNEXPECTED(!isInstance(newType.raw(), pt_ce_error_type, isError))) return zv::Val(); + if (isError) { + changed = true; + continue; + } + if (Z_OBJ_P(newType.raw()) != innerType) { + changed = true; + } + newTypes.push(std::move(newType)); + } + if (zend_hash_num_elements(newTypes.table()) == 0) return errorType(); + if (!changed) return thisValue(); + return combinatorUnion(newTypes.table()); + } zv::Val getKeysArrayFiltered(zval *filterValueType, zval *strict) const { diff --git a/turbo-ext/src/generated/BenevolentUnionType.h b/turbo-ext/src/generated/BenevolentUnionType.h index 196e687b237..719ed78d270 100644 --- a/turbo-ext/src/generated/BenevolentUnionType.h +++ b/turbo-ext/src/generated/BenevolentUnionType.h @@ -41,23 +41,24 @@ inline constexpr char strings[] = "criteria\0" /* 170 */ "pickFromTypes\0" /* 179 */ "offsetType\0" /* 193 */ - "getOffsetValueType\0" /* 204 */ - "getResult\0" /* 223 */ - "PHPStan\\TrinaryLogic\0" /* 233 */ - "unionResults\0" /* 254 */ - "acceptingType\0" /* 267 */ - "strictTypes\0" /* 281 */ - "PHPStan\\Type\\AcceptsResult\0" /* 293 */ - "isAcceptedBy\0" /* 320 */ - "receivedType\0" /* 333 */ - "PHPStan\\Type\\Generic\\TemplateTypeMap\0" /* 346 */ - "inferTemplateTypes\0" /* 383 */ - "templateType\0" /* 402 */ - "inferTemplateTypesOn\0" /* 415 */ - "cb\0" /* 436 */ - "traverse\0" /* 439 */ - "right\0" /* 448 */ - "traverseSimultaneously"; /* 454 */ + "unsetOffset\0" /* 204 */ + "getOffsetValueType\0" /* 216 */ + "getResult\0" /* 235 */ + "PHPStan\\TrinaryLogic\0" /* 245 */ + "unionResults\0" /* 266 */ + "acceptingType\0" /* 279 */ + "strictTypes\0" /* 293 */ + "PHPStan\\Type\\AcceptsResult\0" /* 305 */ + "isAcceptedBy\0" /* 332 */ + "receivedType\0" /* 345 */ + "PHPStan\\Type\\Generic\\TemplateTypeMap\0" /* 358 */ + "inferTemplateTypes\0" /* 395 */ + "templateType\0" /* 414 */ + "inferTemplateTypesOn\0" /* 427 */ + "cb\0" /* 448 */ + "traverse\0" /* 451 */ + "right\0" /* 460 */ + "traverseSimultaneously"; /* 466 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_ARRAY), /* __construct $types */ reg::packed(6, MAY_BE_BOOL, reg::NoString, false, false, 17), /* __construct $normalized */ @@ -72,21 +73,23 @@ inline constexpr reg::PackedArg args[] = { reg::packed(160, MAY_BE_CALLABLE), /* pickFromTypes $getValues */ reg::packed(170, MAY_BE_CALLABLE), /* pickFromTypes $criteria */ reg::packed(44, MAY_BE_ARRAY), /* pickFromTypes return */ + reg::packed(193, 0, 45), /* unsetOffset $offsetType */ + reg::packed(44, 0, 45), /* unsetOffset return */ reg::packed(193, 0, 45), /* getOffsetValueType $offsetType */ reg::packed(44, 0, 45), /* getOffsetValueType return */ - reg::packed(223, MAY_BE_CALLABLE), /* unionResults $getResult */ - reg::packed(44, 0, 233), /* unionResults return */ - reg::packed(267, 0, 45), /* isAcceptedBy $acceptingType */ - reg::packed(281, MAY_BE_BOOL), /* isAcceptedBy $strictTypes */ - reg::packed(44, 0, 293), /* isAcceptedBy return */ - reg::packed(333, 0, 45), /* inferTemplateTypes $receivedType */ - reg::packed(44, 0, 346), /* inferTemplateTypes return */ - reg::packed(402, 0, 45), /* inferTemplateTypesOn $templateType */ - reg::packed(44, 0, 346), /* inferTemplateTypesOn return */ - reg::packed(436, MAY_BE_CALLABLE), /* traverse $cb */ + reg::packed(235, MAY_BE_CALLABLE), /* unionResults $getResult */ + reg::packed(44, 0, 245), /* unionResults return */ + reg::packed(279, 0, 45), /* isAcceptedBy $acceptingType */ + reg::packed(293, MAY_BE_BOOL), /* isAcceptedBy $strictTypes */ + reg::packed(44, 0, 305), /* isAcceptedBy return */ + reg::packed(345, 0, 45), /* inferTemplateTypes $receivedType */ + reg::packed(44, 0, 358), /* inferTemplateTypes return */ + reg::packed(414, 0, 45), /* inferTemplateTypesOn $templateType */ + reg::packed(44, 0, 358), /* inferTemplateTypesOn return */ + reg::packed(448, MAY_BE_CALLABLE), /* traverse $cb */ reg::packed(44, 0, 45), /* traverse return */ - reg::packed(448, 0, 45), /* traverseSimultaneously $right */ - reg::packed(436, MAY_BE_CALLABLE), /* traverseSimultaneously $cb */ + reg::packed(460, 0, 45), /* traverseSimultaneously $right */ + reg::packed(448, MAY_BE_CALLABLE), /* traverseSimultaneously $cb */ reg::packed(44, 0, 45), /* traverseSimultaneously return */ }; using Sig = reg::Sig; @@ -100,13 +103,14 @@ inline constexpr sigtab::Sig tryRemove = { { 88 /* tryRemove */, 1, 4, 1, 5, ZEN inline constexpr sigtab::Sig describe = { { 132 /* describe */, 1, 6, 1, 7, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig unionTypes = { { 149 /* unionTypes */, 1, 8, 1, 9, ZEND_ACC_PROTECTED } }; inline constexpr sigtab::Sig pickFromTypes = { { 179 /* pickFromTypes */, 2, 10, 2, 12, ZEND_ACC_PROTECTED } }; -inline constexpr sigtab::Sig getOffsetValueType = { { 204 /* getOffsetValueType */, 1, 13, 1, 14, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig unionResults = { { 254 /* unionResults */, 1, 15, 1, 16, ZEND_ACC_PROTECTED } }; -inline constexpr sigtab::Sig isAcceptedBy = { { 320 /* isAcceptedBy */, 2, 17, 2, 19, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig inferTemplateTypes = { { 383 /* inferTemplateTypes */, 1, 20, 1, 21, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig inferTemplateTypesOn = { { 415 /* inferTemplateTypesOn */, 1, 22, 1, 23, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig traverse = { { 439 /* traverse */, 1, 24, 1, 25, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig traverseSimultaneously = { { 454 /* traverseSimultaneously */, 2, 26, 2, 28, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig unsetOffset = { { 204 /* unsetOffset */, 1, 13, 1, 14, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getOffsetValueType = { { 216 /* getOffsetValueType */, 1, 15, 1, 16, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig unionResults = { { 266 /* unionResults */, 1, 17, 1, 18, ZEND_ACC_PROTECTED } }; +inline constexpr sigtab::Sig isAcceptedBy = { { 332 /* isAcceptedBy */, 2, 19, 2, 21, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig inferTemplateTypes = { { 395 /* inferTemplateTypes */, 1, 22, 1, 23, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig inferTemplateTypesOn = { { 427 /* inferTemplateTypesOn */, 1, 24, 1, 25, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig traverse = { { 451 /* traverse */, 1, 26, 1, 27, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig traverseSimultaneously = { { 466 /* traverseSimultaneously */, 2, 28, 2, 30, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::BenevolentUnionType diff --git a/turbo-ext/src/generated/BooleanType.h b/turbo-ext/src/generated/BooleanType.h index 4122ecd2c9f..5acc55ea575 100644 --- a/turbo-ext/src/generated/BooleanType.h +++ b/turbo-ext/src/generated/BooleanType.h @@ -40,75 +40,79 @@ inline constexpr char strings[] = "__construct\0" /* 0 */ "\0" /* 12 */ "getConstantStrings\0" /* 13 */ - "getConstantScalarTypes\0" /* 32 */ - "getConstantScalarValues\0" /* 55 */ - "level\0" /* 79 */ - "PHPStan\\Type\\VerbosityLevel\0" /* 85 */ - "describe\0" /* 113 */ - "PHPStan\\Type\\Type\0" /* 122 */ - "toNumber\0" /* 140 */ - "toBitwiseNotType\0" /* 149 */ - "toAbsoluteNumber\0" /* 166 */ - "toString\0" /* 183 */ - "toInteger\0" /* 192 */ - "toFloat\0" /* 202 */ - "toArray\0" /* 210 */ - "toArrayKey\0" /* 218 */ - "strictTypes\0" /* 229 */ - "toCoercedArgumentType\0" /* 241 */ - "PHPStan\\TrinaryLogic\0" /* 263 */ - "isOffsetAccessLegal\0" /* 284 */ - "isNull\0" /* 304 */ - "isTrue\0" /* 311 */ - "isFalse\0" /* 318 */ - "isBoolean\0" /* 326 */ - "isScalar\0" /* 336 */ - "type\0" /* 345 */ - "phpVersion\0" /* 350 */ - "PHPStan\\Php\\PhpVersion\0" /* 361 */ - "PHPStan\\Type\\BooleanType\0" /* 384 */ - "looseCompare\0" /* 409 */ - "typeToRemove\0" /* 422 */ - "tryRemove\0" /* 435 */ - "getFiniteTypes\0" /* 445 */ - "exponent\0" /* 460 */ - "exponentiate\0" /* 469 */ - "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 482 */ - "toPhpDocNode\0" /* 521 */ - "toTrinaryLogic\0" /* 534 */ - "hasTemplateOrLateResolvableType"; /* 549 */ + "offsetType\0" /* 32 */ + "PHPStan\\Type\\Type\0" /* 43 */ + "unsetOffset\0" /* 61 */ + "getConstantScalarTypes\0" /* 73 */ + "getConstantScalarValues\0" /* 96 */ + "level\0" /* 120 */ + "PHPStan\\Type\\VerbosityLevel\0" /* 126 */ + "describe\0" /* 154 */ + "toNumber\0" /* 163 */ + "toBitwiseNotType\0" /* 172 */ + "toAbsoluteNumber\0" /* 189 */ + "toString\0" /* 206 */ + "toInteger\0" /* 215 */ + "toFloat\0" /* 225 */ + "toArray\0" /* 233 */ + "toArrayKey\0" /* 241 */ + "strictTypes\0" /* 252 */ + "toCoercedArgumentType\0" /* 264 */ + "PHPStan\\TrinaryLogic\0" /* 286 */ + "isOffsetAccessLegal\0" /* 307 */ + "isNull\0" /* 327 */ + "isTrue\0" /* 334 */ + "isFalse\0" /* 341 */ + "isBoolean\0" /* 349 */ + "isScalar\0" /* 359 */ + "type\0" /* 368 */ + "phpVersion\0" /* 373 */ + "PHPStan\\Php\\PhpVersion\0" /* 384 */ + "PHPStan\\Type\\BooleanType\0" /* 407 */ + "looseCompare\0" /* 432 */ + "typeToRemove\0" /* 445 */ + "tryRemove\0" /* 458 */ + "getFiniteTypes\0" /* 468 */ + "exponent\0" /* 483 */ + "exponentiate\0" /* 492 */ + "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 505 */ + "toPhpDocNode\0" /* 544 */ + "toTrinaryLogic\0" /* 557 */ + "hasTemplateOrLateResolvableType"; /* 572 */ inline constexpr reg::PackedArg args[] = { reg::packed(12, MAY_BE_ARRAY), /* getConstantStrings return */ + reg::packed(32, 0, 43), /* unsetOffset $offsetType */ + reg::packed(12, 0, 43), /* unsetOffset return */ reg::packed(12, MAY_BE_ARRAY), /* getConstantScalarTypes return */ reg::packed(12, MAY_BE_ARRAY), /* getConstantScalarValues return */ - reg::packed(79, 0, 85), /* describe $level */ + reg::packed(120, 0, 126), /* describe $level */ reg::packed(12, MAY_BE_STRING), /* describe return */ - reg::packed(12, 0, 122), /* toNumber return */ - reg::packed(12, 0, 122), /* toBitwiseNotType return */ - reg::packed(12, 0, 122), /* toAbsoluteNumber return */ - reg::packed(12, 0, 122), /* toString return */ - reg::packed(12, 0, 122), /* toInteger return */ - reg::packed(12, 0, 122), /* toFloat return */ - reg::packed(12, 0, 122), /* toArray return */ - reg::packed(12, 0, 122), /* toArrayKey return */ - reg::packed(229, MAY_BE_BOOL), /* toCoercedArgumentType $strictTypes */ - reg::packed(12, 0, 122), /* toCoercedArgumentType return */ - reg::packed(12, 0, 263), /* isOffsetAccessLegal return */ - reg::packed(12, 0, 263), /* isNull return */ - reg::packed(12, 0, 263), /* isTrue return */ - reg::packed(12, 0, 263), /* isFalse return */ - reg::packed(12, 0, 263), /* isBoolean return */ - reg::packed(12, 0, 263), /* isScalar return */ - reg::packed(345, 0, 122), /* looseCompare $type */ - reg::packed(350, 0, 361), /* looseCompare $phpVersion */ - reg::packed(12, 0, 384), /* looseCompare return */ - reg::packed(422, 0, 122), /* tryRemove $typeToRemove */ - reg::packed(12, MAY_BE_NULL, 122), /* tryRemove return */ + reg::packed(12, 0, 43), /* toNumber return */ + reg::packed(12, 0, 43), /* toBitwiseNotType return */ + reg::packed(12, 0, 43), /* toAbsoluteNumber return */ + reg::packed(12, 0, 43), /* toString return */ + reg::packed(12, 0, 43), /* toInteger return */ + reg::packed(12, 0, 43), /* toFloat return */ + reg::packed(12, 0, 43), /* toArray return */ + reg::packed(12, 0, 43), /* toArrayKey return */ + reg::packed(252, MAY_BE_BOOL), /* toCoercedArgumentType $strictTypes */ + reg::packed(12, 0, 43), /* toCoercedArgumentType return */ + reg::packed(12, 0, 286), /* isOffsetAccessLegal return */ + reg::packed(12, 0, 286), /* isNull return */ + reg::packed(12, 0, 286), /* isTrue return */ + reg::packed(12, 0, 286), /* isFalse return */ + reg::packed(12, 0, 286), /* isBoolean return */ + reg::packed(12, 0, 286), /* isScalar return */ + reg::packed(368, 0, 43), /* looseCompare $type */ + reg::packed(373, 0, 384), /* looseCompare $phpVersion */ + reg::packed(12, 0, 407), /* looseCompare return */ + reg::packed(445, 0, 43), /* tryRemove $typeToRemove */ + reg::packed(12, MAY_BE_NULL, 43), /* tryRemove return */ reg::packed(12, MAY_BE_ARRAY), /* getFiniteTypes return */ - reg::packed(460, 0, 122), /* exponentiate $exponent */ - reg::packed(12, 0, 122), /* exponentiate return */ - reg::packed(12, 0, 482), /* toPhpDocNode return */ - reg::packed(12, 0, 263), /* toTrinaryLogic return */ + reg::packed(483, 0, 43), /* exponentiate $exponent */ + reg::packed(12, 0, 43), /* exponentiate return */ + reg::packed(12, 0, 505), /* toPhpDocNode return */ + reg::packed(12, 0, 286), /* toTrinaryLogic return */ reg::packed(12, MAY_BE_BOOL), /* hasTemplateOrLateResolvableType return */ }; using Sig = reg::Sig; @@ -118,31 +122,32 @@ using Sig = reg::Sig; namespace sig { inline constexpr sigtab::Sig __construct = { { 0 /* __construct */, 0, 0, 0, reg::NoArg, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig getConstantStrings = { { 13 /* getConstantStrings */, 0, 0, 0, 0, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getConstantScalarTypes = { { 32 /* getConstantScalarTypes */, 0, 1, 0, 1, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getConstantScalarValues = { { 55 /* getConstantScalarValues */, 0, 2, 0, 2, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig describe = { { 113 /* describe */, 1, 3, 1, 4, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toNumber = { { 140 /* toNumber */, 0, 5, 0, 5, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toBitwiseNotType = { { 149 /* toBitwiseNotType */, 0, 6, 0, 6, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toAbsoluteNumber = { { 166 /* toAbsoluteNumber */, 0, 7, 0, 7, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toString = { { 183 /* toString */, 0, 8, 0, 8, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toInteger = { { 192 /* toInteger */, 0, 9, 0, 9, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toFloat = { { 202 /* toFloat */, 0, 10, 0, 10, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toArray = { { 210 /* toArray */, 0, 11, 0, 11, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toArrayKey = { { 218 /* toArrayKey */, 0, 12, 0, 12, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toCoercedArgumentType = { { 241 /* toCoercedArgumentType */, 1, 13, 1, 14, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isOffsetAccessLegal = { { 284 /* isOffsetAccessLegal */, 0, 15, 0, 15, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isNull = { { 304 /* isNull */, 0, 16, 0, 16, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isTrue = { { 311 /* isTrue */, 0, 17, 0, 17, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isFalse = { { 318 /* isFalse */, 0, 18, 0, 18, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isBoolean = { { 326 /* isBoolean */, 0, 19, 0, 19, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isScalar = { { 336 /* isScalar */, 0, 20, 0, 20, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig looseCompare = { { 409 /* looseCompare */, 2, 21, 2, 23, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig tryRemove = { { 435 /* tryRemove */, 1, 24, 1, 25, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getFiniteTypes = { { 445 /* getFiniteTypes */, 0, 26, 0, 26, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig exponentiate = { { 469 /* exponentiate */, 1, 27, 1, 28, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toPhpDocNode = { { 521 /* toPhpDocNode */, 0, 29, 0, 29, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toTrinaryLogic = { { 534 /* toTrinaryLogic */, 0, 30, 0, 30, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig hasTemplateOrLateResolvableType = { { 549 /* hasTemplateOrLateResolvableType */, 0, 31, 0, 31, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig unsetOffset = { { 61 /* unsetOffset */, 1, 1, 1, 2, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getConstantScalarTypes = { { 73 /* getConstantScalarTypes */, 0, 3, 0, 3, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getConstantScalarValues = { { 96 /* getConstantScalarValues */, 0, 4, 0, 4, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig describe = { { 154 /* describe */, 1, 5, 1, 6, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toNumber = { { 163 /* toNumber */, 0, 7, 0, 7, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toBitwiseNotType = { { 172 /* toBitwiseNotType */, 0, 8, 0, 8, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toAbsoluteNumber = { { 189 /* toAbsoluteNumber */, 0, 9, 0, 9, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toString = { { 206 /* toString */, 0, 10, 0, 10, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toInteger = { { 215 /* toInteger */, 0, 11, 0, 11, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toFloat = { { 225 /* toFloat */, 0, 12, 0, 12, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toArray = { { 233 /* toArray */, 0, 13, 0, 13, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toArrayKey = { { 241 /* toArrayKey */, 0, 14, 0, 14, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toCoercedArgumentType = { { 264 /* toCoercedArgumentType */, 1, 15, 1, 16, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isOffsetAccessLegal = { { 307 /* isOffsetAccessLegal */, 0, 17, 0, 17, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isNull = { { 327 /* isNull */, 0, 18, 0, 18, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isTrue = { { 334 /* isTrue */, 0, 19, 0, 19, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isFalse = { { 341 /* isFalse */, 0, 20, 0, 20, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isBoolean = { { 349 /* isBoolean */, 0, 21, 0, 21, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isScalar = { { 359 /* isScalar */, 0, 22, 0, 22, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig looseCompare = { { 432 /* looseCompare */, 2, 23, 2, 25, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryRemove = { { 458 /* tryRemove */, 1, 26, 1, 27, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getFiniteTypes = { { 468 /* getFiniteTypes */, 0, 28, 0, 28, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig exponentiate = { { 492 /* exponentiate */, 1, 29, 1, 30, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toPhpDocNode = { { 544 /* toPhpDocNode */, 0, 31, 0, 31, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toTrinaryLogic = { { 557 /* toTrinaryLogic */, 0, 32, 0, 32, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig hasTemplateOrLateResolvableType = { { 572 /* hasTemplateOrLateResolvableType */, 0, 33, 0, 33, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::BooleanType diff --git a/turbo-ext/src/generated/ConstantBooleanType.h b/turbo-ext/src/generated/ConstantBooleanType.h index a1c444b8ff6..3020d8d189e 100644 --- a/turbo-ext/src/generated/ConstantBooleanType.h +++ b/turbo-ext/src/generated/ConstantBooleanType.h @@ -62,13 +62,15 @@ inline constexpr char strings[] = "PHPStan\\TrinaryLogic\0" /* 347 */ "isTrue\0" /* 368 */ "isFalse\0" /* 375 */ - "precision\0" /* 383 */ - "PHPStan\\Type\\GeneralizePrecision\0" /* 393 */ - "generalize\0" /* 426 */ - "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 437 */ - "toPhpDocNode\0" /* 476 */ - "type\0" /* 489 */ - "looseCompare"; /* 494 */ + "offsetType\0" /* 383 */ + "unsetOffset\0" /* 394 */ + "precision\0" /* 406 */ + "PHPStan\\Type\\GeneralizePrecision\0" /* 416 */ + "generalize\0" /* 449 */ + "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 460 */ + "toPhpDocNode\0" /* 499 */ + "type\0" /* 512 */ + "looseCompare"; /* 517 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_BOOL), /* __construct $value */ reg::packed(18, MAY_BE_BOOL), /* getValue return */ @@ -94,10 +96,12 @@ inline constexpr reg::PackedArg args[] = { reg::packed(18, 0, 105), /* toCoercedArgumentType return */ reg::packed(18, 0, 347), /* isTrue return */ reg::packed(18, 0, 347), /* isFalse return */ - reg::packed(383, 0, 393), /* generalize $precision */ + reg::packed(383, 0, 105), /* unsetOffset $offsetType */ + reg::packed(18, 0, 105), /* unsetOffset return */ + reg::packed(406, 0, 416), /* generalize $precision */ reg::packed(18, 0, 105), /* generalize return */ - reg::packed(18, 0, 437), /* toPhpDocNode return */ - reg::packed(489, 0, 105), /* looseCompare $type */ + reg::packed(18, 0, 460), /* toPhpDocNode return */ + reg::packed(512, 0, 105), /* looseCompare $type */ reg::packed(71, 0, 82), /* looseCompare $phpVersion */ reg::packed(18, 0, 197), /* looseCompare return */ }; @@ -124,9 +128,10 @@ inline constexpr sigtab::Sig toArrayKey = { { 302 /* toArrayKey */, 0, 19, 0, 19 inline constexpr sigtab::Sig toCoercedArgumentType = { { 325 /* toCoercedArgumentType */, 1, 20, 1, 21, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig isTrue = { { 368 /* isTrue */, 0, 22, 0, 22, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig isFalse = { { 375 /* isFalse */, 0, 23, 0, 23, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig generalize = { { 426 /* generalize */, 1, 24, 1, 25, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toPhpDocNode = { { 476 /* toPhpDocNode */, 0, 26, 0, 26, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig looseCompare = { { 494 /* looseCompare */, 2, 27, 2, 29, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig unsetOffset = { { 394 /* unsetOffset */, 1, 24, 1, 25, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig generalize = { { 449 /* generalize */, 1, 26, 1, 27, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toPhpDocNode = { { 499 /* toPhpDocNode */, 0, 28, 0, 28, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig looseCompare = { { 517 /* looseCompare */, 2, 29, 2, 31, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::ConstantBooleanType diff --git a/turbo-ext/tests/type-family.php b/turbo-ext/tests/type-family.php index b3c2d16ab39..2ebe99882e0 100644 --- a/turbo-ext/tests/type-family.php +++ b/turbo-ext/tests/type-family.php @@ -3455,8 +3455,11 @@ public function getValuesArray(): \PHPStan\Type\Constant\ConstantArrayType 'unionBools' => new $union([new \PHPStan\Type\Constant\ConstantBooleanType(true), new \PHPStan\Type\Constant\ConstantBooleanType(false)]), 'unionArrays' => new $union([new \PHPStan\Type\ArrayType(new \PHPStan\Type\IntegerType(), new \PHPStan\Type\StringType()), new \PHPStan\Type\NullType()]), 'unionObjects' => new $union([new \PHPStan\Type\ObjectType(\stdClass::class), new \PHPStan\Type\ObjectType(\PHPStan\TrinaryLogic::class)]), + 'unionArrayOrFalse' => new $union([$compoundConstShape(['a' => new \PHPStan\Type\IntegerType(), 'b' => new \PHPStan\Type\StringType()], [0]), new \PHPStan\Type\Constant\ConstantBooleanType(false)]), + 'unionArrayOrString' => new $union([new \PHPStan\Type\ArrayType(new \PHPStan\Type\StringType(), new \PHPStan\Type\IntegerType()), new \PHPStan\Type\StringType()]), 'benevolent' => new $benevolent([new \PHPStan\Type\IntegerType(), new \PHPStan\Type\StringType()]), 'benevolentNullable' => new $benevolent([new \PHPStan\Type\IntegerType(), new \PHPStan\Type\StringType(), new \PHPStan\Type\NullType()]), + 'benevolentArrayOrFalse' => new $benevolent([new \PHPStan\Type\ArrayType(new \PHPStan\Type\StringType(), new \PHPStan\Type\IntegerType()), new \PHPStan\Type\Constant\ConstantBooleanType(false), new \PHPStan\Type\NullType()]), 'array' => new \PHPStan\Type\ArrayType(new \PHPStan\Type\MixedType(), new \PHPStan\Type\MixedType()), 'arrayIntString' => new \PHPStan\Type\ArrayType(new \PHPStan\Type\IntegerType(), new \PHPStan\Type\StringType()), 'arrayStringInt' => new \PHPStan\Type\ArrayType(new \PHPStan\Type\StringType(), new \PHPStan\Type\IntegerType()), From f1ba4a3136399143e9f27741d168b3329bc27e48 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Fri, 9 Oct 2026 23:59:31 +0200 Subject: [PATCH 2/3] Bump expected turbo version --- src/Turbo/TurboExtensionEnabler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 88c83cac563..0100a471b3b 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = '28c29b8'; + public const EXPECTED_EXTENSION_VERSION = '02da319'; private static bool $active = false; From c946474481ae0cd05f42152dd2d1ec98e3a00493 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Sat, 10 Oct 2026 14:22:06 +0200 Subject: [PATCH 3/3] Test unset() of an offset on ArrayAccess|false and similar unions Co-Authored-By: Claude Opus 5.5 (1M context) --- src/Type/Constant/ConstantBooleanType.php | 1 + .../Analyser/nsrt/unset-offset-on-union.php | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/Type/Constant/ConstantBooleanType.php b/src/Type/Constant/ConstantBooleanType.php index 6479dbd6738..8ee8698f3ec 100644 --- a/src/Type/Constant/ConstantBooleanType.php +++ b/src/Type/Constant/ConstantBooleanType.php @@ -139,6 +139,7 @@ public function isFalse(): TrinaryLogic public function unsetOffset(Type $offsetType): Type { + /** unset() of an offset leaves false as it is (deprecated since PHP 8.1) and throws on true. see https://3v4l.org/mHHkL#veol */ if ($this->value) { return new ErrorType(); } diff --git a/tests/PHPStan/Analyser/nsrt/unset-offset-on-union.php b/tests/PHPStan/Analyser/nsrt/unset-offset-on-union.php index 336a327633e..90db9cbca10 100644 --- a/tests/PHPStan/Analyser/nsrt/unset-offset-on-union.php +++ b/tests/PHPStan/Analyser/nsrt/unset-offset-on-union.php @@ -106,4 +106,46 @@ public function arrayOrTrue($value): void assertType('array{path?: string}', $value); } + /** + * @param \ArrayAccess|false $value + */ + public function arrayAccessOrFalse($value): void + { + unset($value['port']); + assertType('ArrayAccess|false', $value); + } + + public function nativeArrayAccessOrFalse(\ArrayAccess|false $value): void + { + unset($value['port']); + assertNativeType('ArrayAccess|false', $value); + } + + /** + * @param \ArrayAccess|array{port?: int, path?: string}|false $value + */ + public function arrayAccessOrArrayOrFalse($value): void + { + unset($value['port']); + assertType('array{path?: string}|ArrayAccess|false', $value); + } + + /** + * @param \ArrayAccess|string $value + */ + public function arrayAccessOrString($value): void + { + unset($value['port']); + assertType('ArrayAccess', $value); + } + + /** + * @param \ArrayAccess|bool $value + */ + public function arrayAccessOrBool($value): void + { + unset($value['port']); + assertType('ArrayAccess|false', $value); + } + }