Skip to content

Fix callable array intersection type offset narrowing#5548

Merged
VincentLanglet merged 5 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-ty6mco0
Apr 28, 2026
Merged

Fix callable array intersection type offset narrowing#5548
VincentLanglet merged 5 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-ty6mco0

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

Fixes phpstan/phpstan#3842

When is_array() narrows a callable parameter, the resulting type is array<mixed, mixed>&callable(): mixed. Previously, accessing offsets on this intersection type returned mixed for all offsets, causing false positives when passing the value to functions expecting array{string|object, string} or array{class-string|object, string}.

This PR teaches IntersectionType that 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:

  • Offset 0: class-string|object (the class or object the method belongs to)
  • Offset 1: string (the method name)

Changes

  • src/Type/IntersectionType.php: Enhanced hasOffsetValueType() to return Yes for offsets 0 and 1 on callable array intersections. Enhanced getOffsetValueType() to narrow the result type for those offsets.
  • Tests: Added regression tests for both type inference (NSRT) and rule-level acceptance checking.

Test plan

  • New NSRT test verifies $value[0] is class-string|object and $value[1] is string after is_array() narrowing on a callable
  • New NSRT test verifies the same for callable-array PHPDoc type
  • New rule test verifies no false positive when passing callable array to array{string|object, string} parameter
  • Full test suite passes (11927 tests)
  • PHPStan self-analysis passes
  • Coding standards check passes

Comment thread src/Type/IntersectionType.php Outdated
Comment thread src/Type/IntersectionType.php Outdated
Comment on lines +943 to +945
if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
$result = TypeCombinator::intersect($result, TypeCombinator::union(new ClassStringType(), new ObjectWithoutClassType()));
} elseif ((new ConstantIntegerType(1))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
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.

What if I access to the offset 0|1 ?
This could be narrowed to object|string.

Comment thread src/Type/IntersectionType.php Outdated
Comment thread src/Type/IntersectionType.php Outdated
Comment thread tests/PHPStan/Rules/Functions/data/bug-3842.php Outdated
VincentLanglet and others added 5 commits April 28, 2026 07:13
…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.
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>
…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>
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>
@staabm staabm force-pushed the create-pull-request/patch-ty6mco0 branch from f7f94c6 to 35a6959 Compare April 28, 2026 05:13
Comment thread src/Type/IntersectionType.php
Copy link
Copy Markdown
Contributor

@staabm staabm left a comment

Choose a reason for hiding this comment

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

lgtm

btw: this PR fixes error on line 19, but not 27 of https://phpstan.org/r/2137cc31-c330-43b4-93f3-574dd2507b56

@VincentLanglet
Copy link
Copy Markdown
Contributor

btw: this PR fixes error on line 19, but not 27 of phpstan.org/r/2137cc31-c330-43b4-93f3-574dd2507b56

Which is expected to me, cause the line 27 produce Non-static method ClassB::callback() cannot be called statically

@VincentLanglet VincentLanglet merged commit 95e0784 into phpstan:2.1.x Apr 28, 2026
658 of 663 checks passed
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-ty6mco0 branch April 28, 2026 07:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants