Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Turbo/TurboExtensionEnabler.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
final class TurboExtensionEnabler
{

public const EXPECTED_EXTENSION_VERSION = '28c29b8';
public const EXPECTED_EXTENSION_VERSION = '02da319';

private static bool $active = false;

Expand Down
5 changes: 5 additions & 0 deletions src/Type/BenevolentUnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down
6 changes: 6 additions & 0 deletions src/Type/BooleanType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)];
Expand Down
10 changes: 10 additions & 0 deletions src/Type/Constant/ConstantBooleanType.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ public function isFalse(): TrinaryLogic
return TrinaryLogic::createFromBoolean($this->value === false);
}

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) {

@staabm staabm Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ($this->value) {
/** 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) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in c946474. The snippet also shows that before PHP 8.1, unset() on true does nothing. unsetOffset() does not know the PHP version, so it still returns ErrorType for true, the same as before this PR.

return new ErrorType();
}

return $this;
}

public function generalize(GeneralizePrecision $precision): Type
{
return new BooleanType();
Expand Down
29 changes: 28 additions & 1 deletion src/Type/UnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
151 changes: 151 additions & 0 deletions tests/PHPStan/Analyser/nsrt/unset-offset-on-union.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php // lint >= 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<string, int>|int|false $value

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please also test with ArrayAccess|false variants

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in c946474: ArrayAccess|false (PHPDoc and native), ArrayAccess|array|false, ArrayAccess|string and ArrayAccess|bool. All five gave *ERROR* without this change.

*/
public function generalArray($value): void
{
unset($value['port']);
assertType('array<string, int>|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<mixed~\'port\', mixed>|false|null', $parsed);
}

/**
* @param array<string, int>|false $value
*/
public function nativeArrayOrFalse(array|false $value): void
{
unset($value['port']);
assertNativeType('array<mixed~\'port\', mixed>|false', $value);
}

/**
* @param __benevolent<array{port?: int, path?: string}|false> $value
*/
public function benevolent($value): void
{
unset($value['port']);
assertType('(array{path?: string}|false)', $value);
}

/**
* @param __benevolent<array{port?: int, path?: string}|false|null> $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);
}

/**
* @param \ArrayAccess<string, int>|false $value
*/
public function arrayAccessOrFalse($value): void
{
unset($value['port']);
assertType('ArrayAccess<string, int>|false', $value);
}

public function nativeArrayAccessOrFalse(\ArrayAccess|false $value): void
{
unset($value['port']);
assertNativeType('ArrayAccess|false', $value);
}

/**
* @param \ArrayAccess<string, int>|array{port?: int, path?: string}|false $value
*/
public function arrayAccessOrArrayOrFalse($value): void
{
unset($value['port']);
assertType('array{path?: string}|ArrayAccess<string, int>|false', $value);
}

/**
* @param \ArrayAccess<string, int>|string $value
*/
public function arrayAccessOrString($value): void
{
unset($value['port']);
assertType('ArrayAccess<string, int>', $value);
}

/**
* @param \ArrayAccess<string, int>|bool $value
*/
public function arrayAccessOrBool($value): void
{
unset($value['port']);
assertType('ArrayAccess<string, int>|false', $value);
}

}
27 changes: 21 additions & 6 deletions turbo-ext/src/BenevolentUnionType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<PT_OP_GET_OFFSET_VALUE_TYPE, &BenevolentUnionType::getOffsetValueType>();

Expand Down
20 changes: 18 additions & 2 deletions turbo-ext/src/BooleanType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(); }

Expand Down Expand Up @@ -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<zp::Obj>(execute_data, offsetType)) RETURN_THROWS();
PT_RETURN_VAL(BooleanType::unsetOffset());
});

cls.method<&BooleanType::getConstantScalarTypes>(sigs::getConstantScalarTypes);

cls.method<&BooleanType::getConstantScalarValues>(sigs::getConstantScalarValues);
Expand Down Expand Up @@ -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);
Expand Down
16 changes: 16 additions & 0 deletions turbo-ext/src/ConstantBooleanType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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<zp::Obj>(execute_data, offsetType)) RETURN_THROWS();
PT_RETURN_VAL(PT_THIS.unsetOffset());
});

cls.method(sigs::generalize, [](INTERNAL_FUNCTION_PARAMETERS) {
zval *precision;
if (!zp::parse<zp::Obj>(execute_data, precision)) RETURN_THROWS();
Expand Down
30 changes: 29 additions & 1 deletion turbo-ext/src/UnionType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Loading
Loading