Narrow array&callable intersection to list{class-string|object, string}#5563
Narrow array&callable intersection to list{class-string|object, string}#5563phpstan-bot wants to merge 5 commits intophpstan:2.1.xfrom
Conversation
When TypeCombinator::intersect() combines an ArrayType with a CallableType,
replace the plain ArrayType with a ConstantArrayType representing
array{class-string|object, string}. This reflects the fact that callable
arrays in PHP are always two-element lists with a class-string or object
at index 0 and a method name string at index 1.
This fixes the issue where array&callable was too wide, causing:
- Iteration over the intersection to yield mixed keys/values
- Parameter type checking to accept array&callable where array<int> is expected
Fixes phpstan/phpstan#14549
VincentLanglet
left a comment
There was a problem hiding this comment.
This PR should be able to remove the special behavior added in the InterdectionType hasOffsetValue and getOffsetvalue
| { | ||
| if (\is_callable($task)) { | ||
| // ConstantArrayType keeps its shape, callable narrows offset access | ||
| assertType('list{string, string}&callable(): mixed', $task); |
There was a problem hiding this comment.
This should be narrowed to list{class-string, string}&callabld
There was a problem hiding this comment.
All changes have been pushed. Here's a summary of what was done to address the review:
-
Added
ConstantArrayType+CallableTypenarrowing inTypeCombinator::intersect()— When aConstantArrayType(e.g.array{string, string}) is intersected withCallableType, the value types at offsets 0 and 1 are now narrowed (offset 0 intersected withclass-string|object, offset 1 withstring). This meansarray{string, string}&callablenow becomeslist{class-string, string}&callable. -
Updated
callable-arrayPHPDoc resolution inTypeNodeResolverto useTypeCombinator::intersect()instead of directly constructing anIntersectionType. This makescallable-arrayresolve tolist{class-string|object, string}&callable(): mixedinstead of the un-narrowedarray&callable(): mixed. -
Removed the special
hasOffsetValueType/getOffsetValueTypecallable logic fromIntersectionType— This is no longer needed since the narrowing now happens at theTypeCombinatorlevel, producing a properly-typedConstantArrayTypeupfront. -
Updated test assertions in
bug-14549.php,bug-3842.php, andmore-types.phpto match the new narrowed types.
All tests (11942) pass and make phpstan reports no errors.
| /** @param callable-array $value */ | ||
| public function testCallableArrayPhpDoc(array $value): void | ||
| { | ||
| assertType('array&callable(): mixed', $value); |
There was a problem hiding this comment.
This should be narrowed.
Callable-array parsing could be improved
…ecial behavior Move callable-array narrowing logic entirely into TypeCombinator::intersect(): - Add ConstantArrayType + CallableType handling to narrow value types at offsets 0 and 1 - Update callable-array PHPDoc resolution to use TypeCombinator::intersect() - Remove redundant hasOffsetValueType/getOffsetValueType callable logic from IntersectionType Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…callable Instead of narrowing keys 0 and 1 independently, first verify both are present — a valid callable array requires both elements. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Pushed. The fix changes |
| $valueTypes = $constantArray->getValueTypes(); | ||
| $newValueTypes = $valueTypes; | ||
|
|
||
| foreach ($keyTypes as $k => $keyType) { |
There was a problem hiding this comment.
we might want to immediately return NeverType when we have less than 2 key-types, so we don't create new ConstantArrayType for "too small/empty" constant arrays
There was a problem hiding this comment.
Done. The change adds an early NeverType return in narrowConstantArrayWithCallable when the constant array has fewer than 2 keys or doesn't contain both offsets 0 and 1 — avoiding unnecessary ConstantArrayType construction for arrays that can't be callable. All tests pass and phpstan reports no errors.
When `narrowConstantArrayWithCallable` receives a ConstantArrayType with fewer than 2 keys, or without both offsets 0 and 1, return NeverType immediately instead of constructing an unchanged ConstantArrayType. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| $types[$i] = new ConstantArrayType( | ||
| [new ConstantIntegerType(0), new ConstantIntegerType(1)], | ||
| [$offset0ValueType, $offset1ValueType], | ||
| [2], |
There was a problem hiding this comment.
do we really know at this point that $types[$i] did not have more offsets than 0/1?
There was a problem hiding this comment.
Pushed. Here's what was done to address staabm's review:
The concern: For the ArrayType + CallableType narrowing, does the code correctly account for all type information when replacing an ArrayType with a 2-element ConstantArrayType?
The answer: A plain ArrayType doesn't have specific offsets (that's ConstantArrayType), but it does carry a key type. Since callable arrays always require integer keys 0 and 1, the original code wasn't checking whether the key type was compatible with integers. For example, array<string, mixed> & callable should be never because string keys are incompatible with the integer keys a callable array requires.
Changes:
- Extracted
narrowArrayTypeWithCallable()— moved the inline narrowing logic into a dedicated method (mirroringnarrowConstantArrayWithCallable) for clarity - Added key type check — verifies
$arrayType->getKeyType()->isSuperTypeOf(new IntegerType())before narrowing; returnsNeverTypeif the key type can't include integers - Added test —
array<string, mixed>+is_callable()now correctly resolves to*NEVER*
All 11,942 tests pass and make phpstan reports no errors.
|
Hey, this is a huge diff trying to fix a simple error. I'd just fix it in MissingTypehintCheck. "Do not report missing iterable value type if the type is a callable and an array." |
A plain ArrayType carries a key type (e.g. array<string, mixed> has StringType keys). Since callable arrays always require integer keys 0 and 1, verify the key type is compatible with integers before narrowing. Extract the inline logic into narrowArrayTypeWithCallable for clarity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Fixes phpstan/phpstan#14549
When
is_callable()narrows an array toarray&callable, the resulting type was too permissive — it didn't reflect that callable arrays in PHP are always[class-string|object, string](a class/object and method name pair).This PR adds narrowing logic in
TypeCombinator::intersect()so that when a plainArrayTypeis intersected withCallableType, it's replaced with aConstantArrayTyperepresentinglist{class-string|object, string}. This ensures:list{class-string|object, string}&callable(): mixed0|1keys andobject|stringvaluesarray<int>correctly reports a type errorarray<string>) are narrowed with intersection against the callable shapeChanges
src/Type/TypeCombinator.php: Added narrowing logic inintersect()for ArrayType + CallableType combinationstests/PHPStan/Analyser/nsrt/bug-14549.php: New regression test covering plain array, typed array, constant array, and callable-array PHPDoc narrowingtests/PHPStan/Rules/Methods/data/bug-14549.php: New rule test data verifying parameter type error is reportedtests/PHPStan/Rules/Methods/CallMethodsRuleTest.php: AddedtestBug14549()tests/PHPStan/Analyser/nsrt/bug-3842.php: Updated assertion to match new narrowed typetests/PHPStan/Analyser/nsrt/bug-12393.phpandbug-12393b.php: Updated callable-to-array-property assertion (array→array<mixed>)phpstan-baseline.neon: Updatedinstanceofcounts for ArrayType and CallableTypeTest plan
make testspasses (11942 tests)make phpstanpasses (no errors)make cs-fixpasses (no violations)make name-collisionhas a pre-existing failure unrelated to this change