Fix callable array intersection type offset narrowing#5548
Fix callable array intersection type offset narrowing#5548phpstan-bot wants to merge 5 commits intophpstan:2.1.xfrom
Conversation
…rsections
When is_array() narrows a callable parameter, the resulting
array&callable intersection type now correctly reports that offset 0
is class-string|object and offset 1 is string. This fixes false
positives when passing the narrowed value to functions expecting
array{string|object, string} or similar callable-array shapes.
| if (!$result->yes() && $this->isCallable()->yes() && $this->isArray()->yes()) { | ||
| $arrayKeyOffsetType = $offsetType->toArrayKey(); | ||
| if ( | ||
| (new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes() |
There was a problem hiding this comment.
(new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes() || (new ConstantIntegerType(1))->isSuperTypeOf($arrayKeyOffsetType)->yes()
Should be rework into an UnionType->isSuperTypeOf check.
Cause we could look for the offset 0|1, which always exists.
There was a problem hiding this comment.
Pushed. Here's a summary of the changes addressing the review:
-
hasOffsetValueType: Replaced the two separateConstantIntegerType(0)->isSuperTypeOf(...) || ConstantIntegerType(1)->isSuperTypeOf(...)checks with a singleUnionType([0, 1])->isSuperTypeOf(...)check. This correctly handles the case where the offset itself is0|1. -
getOffsetValueType: Instead of anif/elseifthat only matched exact offsets, the code now:- Guards with the same
UnionType([0, 1])->isSuperTypeOf(...)check - Checks which concrete offsets are possible within the offset type (using reversed
isSuperTypeOf) - Unions the applicable narrowed types — so offset
0|1correctly narrows toobject|string
- Guards with the same
-
Test: Added a test case for
0|1offset access asserting the result isobject|string.
All 11927 tests pass and PHPStan self-analysis reports no errors.
| if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) { | ||
| $result = TypeCombinator::intersect($result, TypeCombinator::union(new ClassStringType(), new ObjectWithoutClassType())); | ||
| } elseif ((new ConstantIntegerType(1))->isSuperTypeOf($arrayKeyOffsetType)->yes()) { |
There was a problem hiding this comment.
What if I access to the offset 0|1 ?
This could be narrowed to object|string.
Address review feedback: use a single UnionType(0|1)->isSuperTypeOf check instead of separate ConstantIntegerType checks, and handle the case where the offset is itself a union (e.g. 0|1) by narrowing to the union of both offset types (object|string). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| if ($callableArrayOffsetType->isSuperTypeOf($arrayKeyOffsetType)->yes()) { | ||
| $narrowedTypes = []; | ||
| if (!$arrayKeyOffsetType->isSuperTypeOf(new ConstantIntegerType(0))->no()) { | ||
| $narrowedTypes[] = TypeCombinator::union(new ClassStringType(), new ObjectWithoutClassType()); |
There was a problem hiding this comment.
new UnionType can be used here
There was a problem hiding this comment.
Done. Replaced TypeCombinator::union(new ClassStringType(), new ObjectWithoutClassType()) with new UnionType([new ClassStringType(), new ObjectWithoutClassType()]) as the reviewer suggested — since these types are known and won't simplify, using UnionType directly avoids unnecessary normalization overhead. All tests pass and PHPStan self-analysis is clean.
| $narrowedTypes[] = new StringType(); | ||
| } | ||
| if ($narrowedTypes !== []) { | ||
| $result = TypeCombinator::intersect($result, TypeCombinator::union(...$narrowedTypes)); |
There was a problem hiding this comment.
We could save the TypeCombinator::union
if (0 is superType) we narrow new Union (new ClassStringType(), new ObjectWithoutClassType())
elseif (1 is superType) we narrow String
else we narrow new Union (new StringType(), new ObjectWithoutClassType())
There was a problem hiding this comment.
Pushed. The change simplifies getOffsetValueType() by replacing the array-building + TypeCombinator::union pattern with a direct if/elseif/else that handles each case:
- Offset
0→class-string|object - Offset
1→string - Offset
0|1→object|string
All 11927 tests pass, PHPStan self-analysis and coding standards are clean.
…ngType|ObjectWithoutClassType Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use direct UnionType construction instead of building an array and calling TypeCombinator::union, as suggested in review. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| @@ -0,0 +1,29 @@ | |||
| <?php declare(strict_types = 1); | |||
|
|
|||
| namespace Bug3842; | |||
There was a problem hiding this comment.
Merge the nsrt and data file in a single one which is loaded for both nsrt and rule tests.
There was a problem hiding this comment.
Done. The change merges the two test files into a single shared one:
- Deleted
tests/PHPStan/Rules/Functions/data/bug-3842.php(duplicate data file) - Updated
CallToFunctionParametersRuleTest::testBug3842()to referencetests/PHPStan/Analyser/nsrt/bug-3842.phpinstead - The nsrt file already contained all the test scenarios (including
check()andcheckClassString()calls), so no content changes were needed
All 11927 tests pass and PHPStan self-analysis is clean.
The rule test now references the nsrt test file directly instead of maintaining a separate duplicate data file. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Fixes phpstan/phpstan#3842
When
is_array()narrows acallableparameter, the resulting type isarray<mixed, mixed>&callable(): mixed. Previously, accessing offsets on this intersection type returnedmixedfor all offsets, causing false positives when passing the value to functions expectingarray{string|object, string}orarray{class-string|object, string}.This PR teaches
IntersectionTypethat when an intersection is both callable and an array (i.e., a callable array), offsets 0 and 1 are guaranteed to exist with specific types:class-string|object(the class or object the method belongs to)string(the method name)Changes
src/Type/IntersectionType.php: EnhancedhasOffsetValueType()to returnYesfor offsets 0 and 1 on callable array intersections. EnhancedgetOffsetValueType()to narrow the result type for those offsets.Test plan
$value[0]isclass-string|objectand$value[1]isstringafteris_array()narrowing on acallablecallable-arrayPHPDoc typearray{string|object, string}parameter