Repository navigation
Keep the array part of a union after unset() of an offset - #6722
SanderMuller wants to merge 3 commits into
Conversation
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) <noreply@anthropic.com>
|
The The test does The other red checks fail on other open 2.3.x PRs as well (for example #6721 and #6723). |
|
|
||
| public function unsetOffset(Type $offsetType): Type | ||
| { | ||
| if ($this->value) { |
There was a problem hiding this comment.
| 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) { |
There was a problem hiding this comment.
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.
| } | ||
|
|
||
| /** | ||
| * @param array<string, int>|int|false $value |
There was a problem hiding this comment.
please also test with ArrayAccess|false variants
There was a problem hiding this comment.
Added in c946474: ArrayAccess|false (PHPDoc and native), ArrayAccess|array|false, ArrayAccess|string and ArrayAccess|bool. All five gave *ERROR* without this change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
unset($x['key'])on a union that has a member without offsets turned the whole variable into*ERROR*. That is the case forarray|false,array|string,array|intand similar unions. PHPStan reported nothing on theunset, and every later check on the variable went silent.UnionType::unsetOffset()mapped every member throughunsetOffset().false,int,stringand the other scalars returnErrorTypethere, and the union of an array withErrorTypeisErrorType. Now the members that returnErrorTypeare left out, the same wayUnionType::getOffsetValueType()already does it. The result isErrorTypeonly when no member is left.At runtime,
unset($x['k'])leavesfalseas it is, with a deprecation since PHP 8.1. It throws anErrorfortrue,int,float,stringand objects withoutArrayAccess. SoConstantBooleanType(false)now returns itself fromunsetOffset(), asNullTypealready does, andBooleanTypereturnsfalse. Without that,$x === falseafter theunset()was reported as always false.BenevolentUnionType::unionTypes()already dropsErrorTyperesults, so benevolent unions did not have this bug.BenevolentUnionTypenow overridesunsetOffset()with the oldunionTypes()call, so its result stays benevolent.I found this while comparing PHPStan and Mago findings on WordPress. One WordPress bug fix was for a missing
'path'key on aparse_url()result inredirect_canonical(). PHPStan reported nothing on that line before the fix, because anunset( $redirect['port'] )a few lines earlier had turned$redirectinto*ERROR*. With this change, PHPStan reports the line. On WordPress trunk from 2026-10-07, the change adds one error. Insanitize_trackback_urls(),preg_split()can returnfalse, and that value reachesarray_map()after a loop that unsets offsets.UnsetRulestill reports nothing forunset()onarray|false, because it reports only when the offset can never be accessed. Reporting the "maybe" case would add new errors, so I left it for a separate change.UnionType,BenevolentUnionType,BooleanTypeandConstantBooleanTypeare shadowed by the Turbo extension, so the change is ported to their.cppfiles, and the declarations are regenerated. The existingbool,false,scalarsandnullableIntsubjects intype-family.phpdiverge between the two implementations if only one side has the change. I addedarray|false,array|stringand a benevolentarray|false|nullsubject as well. Analysis output on WordPress is identical with and without the extension.🤖 Generated with Claude Code