Skip to content

Report array&callable pass as wrong param#5573

Merged
VincentLanglet merged 11 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-k8eciuw
May 7, 2026
Merged

Report array&callable pass as wrong param#5573
VincentLanglet merged 11 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-k8eciuw

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When a parameter, return type, or property was declared with an array&callable intersection type (including the callable-array shorthand), MissingTypehintCheck did not report the missing iterable value type for the array part. This meant users were not warned about the imprecise array<mixed> in these intersections, and downstream argument type checks couldn't catch mismatches (e.g., passing array&callable to array<int>).

Changes

  • Modified src/Rules/MissingTypehintCheck.php: In the special-case block for array&callable intersections (lines 76–96), added a check on each array inner type's getIterableValueType(). If the value type is implicit mixed, it is now added to the missing-typehint list. The non-array parts (callable) are still traversed for their own inner type checks (e.g., callable parameter/return arrays).
  • Added test case doBaz() with @param callable&array in tests/PHPStan/Rules/Methods/data/bug-14549.php
  • Updated test expectations in tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php:
    • doFoo() (callable-array) now reports missingType.iterableValue for its array part
    • doIntersection() (array&callable(array): array) now reports a third missingType.iterableValue for the outer array
    • doBaz() (callable&array) reports both missingType.iterableValue and missingType.callable

Root cause

MissingTypehintCheck::getIterableTypesWithMissingValueTypehint() had a special-case block (lines 76–92) for intersection types that are both callable and array. This block extracted the non-array parts and traversed only those, completely skipping the array parts. The array parts were never checked for missing value types, so callable-array / callable&array silently passed the check even though their array part has implicit mixed values.

The fix adds the missing value type check for each array inner type within the existing special-case block, before the continue that skips it.

Analogous cases probed

Since the fix is in the shared MissingTypehintCheck class, all consumers are automatically fixed:

  • Method parameters (MissingMethodParameterTypehintRule) — tested and verified
  • Function parameters (MissingFunctionParameterTypehintRule) — shares the same check, tests pass
  • Method return types (MissingMethodReturnTypehintRule) — shares the same check, tests pass
  • Function return types (MissingFunctionReturnTypehintRule) — shares the same check, tests pass
  • Properties (MissingPropertyTypehintRule) — shares the same check, tests pass
  • Class constants (MissingClassConstantTypehintRule) — shares the same check, tests pass
  • @var tags, @phpstan-assert, self-out types — all use the same getIterableTypesWithMissingValueTypehint() method

Non-array callable intersections (e.g., Traversable&callable) don't hit this code path (the condition requires isArray()->yes()), and they are already handled correctly by the general iterable check below.

Test

  • testBug14549 in MissingMethodParameterTypehintRuleTest verifies:
    • callable-array reports missing iterable value type
    • array&callable(array): array reports missing value type for all three arrays (outer + callable param + callable return)
    • callable&array reports both missing iterable value type and missing callable signature
  • NSRT test for bug-14549 verifies type inference for callable array offsets remains correct

Fixes phpstan/phpstan#14549

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

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

No, this behavior is on purpose.

We don't want to report missingTypehint check for array&callable since we can infer it.

But we should still report passing array&callable when passed to a function expecting array.

For instance we already report passing array&callable to array{string, string} (because it could be array{object|string, string}).

@VincentLanglet VincentLanglet changed the title Report missing iterable value type for array part of array&callable intersections in MissingTypehintCheck Report array&callable pass as wrong param Apr 30, 2026
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

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

Use another file bug-14549-bis.php for your tests

And add test with callable&array passed to a method expecting constant array

  • array{string, string} => Error
  • array{object|string, string} => No error
  • array{object|string, string, string} => Error

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Here's what I changed:

  1. Created tests/PHPStan/Rules/Methods/data/bug-14549-bis.php — a separate test file for CallMethodsRuleTest with methods accepting array<int>, array{string, string}, array{object|string, string}, and array{object|string, string, string}, called from both callable-array and callable&array typed parameters.

  2. Removed call-related methods from bug-14549.php — the call(), doCallWithCallableArray(), and doCallWithCallableAndArray() methods that were only used by CallMethodsRuleTest are now in the separate file.

  3. Updated CallMethodsRuleTest — now uses bug-14549-bis.php and verifies the constant array test cases:

    • array{string, string}Error (callable-array has object at index 0)
    • array{object|string, string}No error (matches callable-array shape)
    • array{object|string, string, string}Error (callable-array has 2 elements, not 3)
  4. Updated MissingMethodParameterTypehintRuleTest — removed expectations for methods that moved to the new file.

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

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

I can reproduce a similar issue with array<mixed>&hasOffsetValue(1,1) which seems acceptedBy array<string>.

Maybe IntersectionType::isAccepted implementation is wrong.
I wonder if lazyMaxMin is the right check

When looking for A&B isAcceptedBy C
If C does not accept A, A&B cannot be accepted.

What about a strategy

  • If a NO => Return No
  • Else returns the Max (Maybe & Yes)

What would be the impact of such change ?

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

I processed this review but have nothing to report.

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Here's a summary of what I changed:

The problem: The reviewer pointed out that the PR's approach of special-casing callable&array in IntersectionType::isAcceptedBy() was too narrow. The same issue exists for other intersection types like array<mixed>&hasOffsetValue(1,1) — they were incorrectly accepted by array<string> because MixedType::isAcceptedBy() always returns Yes, and lazyMaxMin short-circuits on that.

Root cause: MixedType implements CompoundType and its isAcceptedBy() always returns Yes (since mixed is a supertype of everything). When lazyMaxMin iterates intersection components individually, it short-circuits on the first Yes from array<mixed>, ignoring that other components (like hasOffsetValue(1,1) or callable) constrain the intersection in ways that make it incompatible.

The fix: Replace the callable&array-specific narrowing with a general guard: after lazyMaxMin produces a Yes result, verify it with $acceptingType->isSuperTypeOf($this). The isSuperTypeOf chain (which goes through IntersectionType::isSubTypeOf) correctly considers all components together and returns No when the intersection as a whole is not a subtype. If isSuperTypeOf says No, we override the false Yes.

This approach:

  • Fixes the original callable&array vs array<int> case
  • Also fixes the reviewer's array<mixed>&hasOffsetValue(1,1) vs array<string> case
  • Preserves $strictTypes coercion behavior (since accepts is still the primary check)
  • Preserves error reasons from accepts (the isSuperTypeOf guard only overrides on definitive No)
  • All 11,971 tests pass and make phpstan reports no errors

Comment on lines +50 to +55
/**
* @param callable&array $task
*/
public function doBaz(array $task): void
{
}
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.

Remove this

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. Removed the doBaz() method from bug-14549.php and its corresponding test expectation in MissingMethodParameterTypehintRuleTest.php. All 11,971 tests pass and make phpstan reports no errors.

@VincentLanglet
Copy link
Copy Markdown
Contributor

This seems to work but I wonder if I and the bot are not making a mistake by confusing accepts and isSuperTypeOf...

And there is multiple integration test failure I'll need to check

I'll need to take more time on it, just wanted to know if it's inspire you @staabm ?
Maybe you see a better direction for this PR.

VincentLanglet and others added 6 commits May 5, 2026 23:27
… intersections in `MissingTypehintCheck`

- In `MissingTypehintCheck::getIterableTypesWithMissingValueTypehint()`, the
  special-case block for `array&callable` intersection types was skipping the
  array part entirely, never checking it for missing value types
- Added a check for the array inner type's iterable value type before skipping
  it, so `callable-array`, `callable&array`, and `array&callable(...)` now
  correctly report `missingType.iterableValue` when the array has no value type
- Updated test expectations for `doIntersection()` (gains a third error for the
  outer array) and `doFoo()` (now reports missing iterable value type)
- Added new test case `doBaz()` with explicit `callable&array` parameter
- All consumers of `MissingTypehintCheck` (method/function parameters, return
  types, properties, constants, @var tags, @phpstan-assert) are automatically
  fixed since they share the same check
…ngTypehintCheck

The missing typehint suppression for array&callable is intentional because
PHPStan can infer the value type (object|non-falsy-string). Reverts the
MissingTypehintCheck changes and their test expectations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rray&callable

When an array&callable intersection is checked for acceptance, the raw
ArrayType(mixed, mixed) component was used, causing array<int> to
incorrectly accept array&callable. Now the array component uses the
narrowed key/value types (int<0,1>, object|non-falsy-string) so the
acceptance check correctly rejects incompatible array types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d constant array test cases

Test callable&array passed to methods expecting constant array types:
- array{string, string} => Error (object at index 0 is not string)
- array{object|string, string} => No error (matches callable-array shape)
- array{object|string, string, string} => Error (callable-array has 2 elements, not 3)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d of special-casing callable&array

Replace the callable&array-specific narrowing logic with a general
approach: after the standard lazyMaxMin check, if it returns Yes,
verify with isSuperTypeOf that the accepting type is actually a
supertype of the full intersection. This catches cases where
MixedType's accepts-everything behavior causes false acceptances
(e.g. array<int> falsely accepting array<mixed>&hasOffsetValue
or array<mixed>&callable intersections).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-k8eciuw branch from e3e1b99 to 3afa0ac Compare May 5, 2026 21:27
@VincentLanglet VincentLanglet self-assigned this May 5, 2026
@staabm
Copy link
Copy Markdown
Contributor

staabm commented May 6, 2026

to fix this one, I would investigate why https://phpstan.org/r/d89df38c-c43c-49fe-bcdc-03a29f285690 reports

Parameter #1 $task of method MondayMorning::call() expects array, array given.

and than would try to check how the relevant logic can be applied to
https://phpstan.org/r/d6ef05eb-6a35-4621-a996-d6558786e70f


saying that and looking at

if (
!$parameter->passedByReference()->createsNewVariable()
|| (!$isBuiltin && !$argumentValueType instanceof ErrorType)
) {
// @see https://github.com/php/php-src/issues/21568#issuecomment-4148832540
$isStrictTypes = $scope->isDeclareStrictTypes()
&& (!$isBuiltin || !$parameterType->isCallable()->yes());
$accepts = $this->ruleLevelHelper->accepts($parameterType, $argumentValueType, $isStrictTypes);
if (!$accepts->result) {
$verbosityLevel = VerbosityLevel::getRecommendedLevelByType($parameterType, $argumentValueType);
$errors[] = RuleErrorBuilder::message(sprintf(
$wrongArgumentTypeMessage,
$this->describeParameter($parameter, $argumentName ?? $i + 1),
$parameterType->describe($verbosityLevel),
$argumentValueType->describe($verbosityLevel),
))
->identifier('argument.type')
->line($argumentLine)
->acceptsReasonsTip($accepts->reasons)
->build();
}
}
I think the fix need to be in one of the Type->accepts() or Type->isAcceptedBy() implementations.

looking deeper my feeling is the problem needs a fix in IntersectionType->isAcceptedBy(), likely using something like if ($this->isCallable()->yes() && $this->isArray()->yes()) {

@staabm
Copy link
Copy Markdown
Contributor

staabm commented May 6, 2026

another thing which could work is

diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php
index 777343626..124405a14 100644
--- a/src/Type/ArrayType.php
+++ b/src/Type/ArrayType.php
@@ -112,11 +112,9 @@ class ArrayType implements Type
                        }
 
                        return $result;
-               }
-
-               if ($type instanceof ArrayType) {
-                       return $this->getItemType()->accepts($type->getItemType(), $strictTypes)
-                               ->and($this->keyType->accepts($type->keyType, $strictTypes));
+               } else if ($type->isArray()->yes()) {
+                       return $this->getIterableValueType()->accepts($type->getIterableValueType(), $strictTypes)
+                               ->and($this->keyType->accepts($type->getIterableKeyType(), $strictTypes));
                }
 
                return AcceptsResult::createNo();

@VincentLanglet
Copy link
Copy Markdown
Contributor

VincentLanglet commented May 6, 2026

It's not 100% related to callable because the issue exist with HasOffsetType and HasOffsetValueType too

Array of string
shouldnt accept array&hasOffsetValue<3, int>

For instance

@staabm
Copy link
Copy Markdown
Contributor

staabm commented May 6, 2026

It's not 100% related to callable because the issue exist with HasOffsetType and HasOffsetValueType too

Array of string shouldnt accept array&hasOffsetValue<3, int>

ok, than its definitly in IntersectionType (which is the type all your examples have in common)

@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-k8eciuw branch from f8560a4 to 3afa0ac Compare May 6, 2026 07:53
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

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

Add IntersectionTypeTest with

  • array&callable isAcceptedBy array
  • array&callable isAcceptedBy constantArray
  • array&hasOffsetValue isAcceptedBy array
  • array&hasOffsetValue isAcceptedBy constantArray

In both success and failure cases (which wasn't fixed before).

Does the issue exists for another method ? (Maybe isSubtypeOf ?)

…lable and array&hasOffsetValue

Tests cover both success and failure cases:
- array&callable isAcceptedBy array/constantArray
- array&hasOffsetValue isAcceptedBy array/constantArray
- array&callable isSubTypeOf array
- array&hasOffsetValue isSubTypeOf array

The isSubTypeOf tests confirm that the issue does NOT exist there —
isSubTypeOf correctly returns No for incompatible types (e.g.
array&callable is not a subtype of array<int>, and
array&hasOffsetValue(3,int) is not a subtype of array<string>).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Here's a summary of what I did:

Added IntersectionTypeTest cases for isAcceptedBy and isSubTypeOf as requested by the reviewer:

  1. testIsAcceptedBy (new data provider + test method) with 9 test cases:

    • array&callable isAcceptedBy array → Yes (success)
    • array&callable isAcceptedBy array<int> → No (failure — the isSuperTypeOf guard catches this)
    • array&callable isAcceptedBy array{object|string, string} → Maybe (success)
    • array&callable isAcceptedBy array{string, string} → Maybe (failure)
    • array&hasOffsetValue(3,int) isAcceptedBy array → Yes (success)
    • array&hasOffsetValue(3,int) isAcceptedBy array<string> → No (failure — the guard catches this)
    • array&hasOffsetValue(3,int) isAcceptedBy array<int> → Yes (success, matching value type)
    • array&hasOffsetValue(0,int) isAcceptedBy array{int, int} → Maybe (success)
    • array&hasOffsetValue(3,int) isAcceptedBy array{string, string} → Maybe (failure)
  2. testIsSubTypeOf additions (5 cases) answering the reviewer's question about whether isSubTypeOf has the same issue:

    • array&callable isSubTypeOf array → Yes
    • array&callable isSubTypeOf array<int> → No (correct!)
    • array&hasOffsetValue(3,int) isSubTypeOf array → Yes
    • array&hasOffsetValue(3,int) isSubTypeOf array<int> → Maybe
    • array&hasOffsetValue(3,int) isSubTypeOf array<string> → No (correct!)

Answer to the reviewer's question: The issue does NOT exist in isSubTypeOf. Unlike isAcceptedBy (which uses lazyMaxMin and can short-circuit on Yes from MixedType::accepts), isSubTypeOf uses lazyMaxMin on $otherType->isSuperTypeOf($innerType) which correctly produces No results when types are incompatible (e.g., array<int>->isSuperTypeOf(CallableType) returns Maybe, not Yes, so it never short-circuits incorrectly).

Comment thread tests/PHPStan/Type/IntersectionTypeTest.php Outdated
Comment thread tests/PHPStan/Type/IntersectionTypeTest.php Outdated
Comment thread tests/PHPStan/Type/IntersectionTypeTest.php Outdated
Comment thread tests/PHPStan/Type/IntersectionTypeTest.php Outdated
Comment thread tests/PHPStan/Type/IntersectionTypeTest.php Outdated
Comment thread tests/PHPStan/Type/IntersectionTypeTest.php Outdated
Comment thread tests/PHPStan/Type/IntersectionTypeTest.php Outdated
- Make test comments more descriptive by including the accepting type details
- Fix last hasOffsetValue failure test to use offset 0 (matching the constant
  array's offsets) so it properly tests type incompatibility, and update
  expected result to No since int at offset 0 conflicts with string

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

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

This PR produce the new following errors in E2E tests

  • "Method PHPStan\Type\BeberleiAssert\AssertHelper::getExpressionResolvers() should return array<string, callable(PHPStan\Analyser\Scope, PhpParser\Node\Arg ...): (array{PhpParser\Node\Expr|null, PhpParser\Node\Expr|null}|PhpParser\Node\Expr|null)> but returns non-empty-array."

  • "Parameter #1 $generator of method Nette\DI\ContainerLoader::load() expects callable(Nette\DI\Compiler): (string|null), Closure(Nette\DI\Compiler): void given."

  • "Method Shopware\Core\Kernel::getKernelParameters() should return array<string, array<string, mixed>|bool|float|int|string|UnitEnum|null> but returns array<string, array|bool|float|int|string|UnitEnum|null>."

  • "Parameter #1 $type of method Nette\DI\Definitions\ServiceDefinition::setType() expects class-string|null, string given."

WDYT of them ? Which ones are regression ?

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Pushed. Here's my analysis of the E2E errors and what I changed:

Analysis of E2E errors

Error 1 (BeberleiAssert): non-empty-array<Closure> vs array<string, callable(...)>REGRESSION from the PR. The isSuperTypeOf guard was too strict: string->isSuperTypeOf(mixed) returns no (mixed is broader than string), but string->accepts(mixed) correctly returns yes (mixed could be string). The new approach fixes this.

Errors 2-4 (Nette ContainerLoader, Shopware, Nette ServiceDefinition) — Likely NOT caused by this PR. None of these types appear to involve IntersectionType in a way that would trigger the guard:

  • Error 2: Closure(Compiler): void vs callable(Compiler): (string|null) — plain ClosureType, not an intersection
  • Error 3: array vs array<string, mixed> — plain ArrayType, not an intersection
  • Error 4: string vs class-string|null — plain StringType, not an intersection

What changed

Replaced the blanket isSuperTypeOf guard with a targeted re-check: when lazyMaxMin short-circuits on yes (from ArrayType(mixed,mixed) being accepted by any array type due to MixedType::isAcceptedBy), re-check the non-array intersection components that were skipped. If any returns no, override the result. This correctly catches callable, HasOffsetValueType, and other accessory type constraints without the false positives from mixing accepts/isSuperTypeOf semantics.

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

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

Remove all the IntersectionTypeTest and remove the change in IntersectionType for now.

Instead look for a fix in ArrayType::accepts.

Seems like ConstantArrayType::accepts already have a special behavior

if ($type instanceof CompoundType && !$type instanceof IntersectionType) {

So maybe we should handle IntersectionType differently in ArrayType ?

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Pushed. Here's what I changed:

Removed from IntersectionType::isAcceptedBy() (lines 307-317): The guard block that re-checked non-array components when the result was Yes and the accepting type was an array.

Removed from IntersectionTypeTest: All dataIsAcceptedBy/testIsAcceptedBy test cases (9 cases) and the isSubTypeOf additions (5 cases) — 159 lines total.

Added to ArrayType::accepts(): After the CompoundType delegation to isAcceptedBy(), a guard that catches false Yes results from lazyMaxMin short-circuiting on array<mixed>. When the result is Yes and the type is an IntersectionType that is an array, each non-array component is re-checked individually. If any returns No (e.g., array<int>::accepts(callable) → No), the No is returned instead.

Updated baseline: instanceof IntersectionType count in ArrayType.php from 1 to 2.

This approach preserves the existing isAcceptedBy delegation (avoiding regressions with normal array intersections like non-empty-array<int, list<int>>) while catching the specific bug where array<mixed> in an intersection causes lazyMaxMin to short-circuit incorrectly. All 11,994 tests pass and make phpstan reports no errors.

@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-k8eciuw branch from fd77c6b to 9759075 Compare May 6, 2026 14:03
@VincentLanglet
Copy link
Copy Markdown
Contributor

Integration failure are valid to me ; for instance

Method Shopware\Core\Kernel::getKernelParameters() should return array<string, array<string, mixed>|bool|float|int|string|UnitEnum|null> but returns array<string, array|bool|float|int|string|UnitEnum|null>.  

'kernel.supported_api_versions' => [2, 3, 4], doest not respect array<string, mixed>

@VincentLanglet
Copy link
Copy Markdown
Contributor

From ondrej
image

);

if ($result->yes()) {
$isSuperType = $acceptingType->isSuperTypeOf($this);
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.

Add a quick comment to explain which situations is this about

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added a 4-line comment above the isSuperTypeOf guard in IntersectionType::isAcceptedBy() explaining that it catches false positives from lazyMaxMin short-circuiting when MixedType::isAcceptedBy() returns Yes for intersection types like array<mixed>&callable or array<mixed>&hasOffsetValue against specific array types. All 12,013 tests pass and make phpstan reports no errors.

…eptedBy()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@VincentLanglet VincentLanglet requested a review from staabm May 6, 2026 18:51
Comment on lines +151 to +160
// array&hasOffsetValue isAcceptedBy array - success
yield [
new IntersectionType([
new ArrayType(new MixedType(), new MixedType()),
new NonEmptyArrayType(),
new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()),
]),
new ArrayType(new MixedType(), new MixedType()),
TrinaryLogic::createYes(),
];
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.

I think we could have another test like "array&hasOffsetValue" is accepted by a union of e.g. "array|array"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added the test case staabm requested: array<mixed>&non-empty-array&hasOffsetValue(3, int) isAcceptedBy array<int>|array<string> → Yes. This verifies that when the accepting type is a union containing a compatible array type, the intersection is correctly accepted. All tests pass and make phpstan reports no errors.

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.

since ondrej seem to already "approved it" it seems to be good enough :)

phpstan-bot and others added 2 commits May 7, 2026 05:35
…ay<int>|array<string>

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests that array<mixed>&hasOffsetValue(3, int) is accepted by
array<int>|array<string> — the isSuperTypeOf guard correctly allows
this since array<int> individually accepts the intersection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. The reviewer's suggestion was already partially addressed by a prior commit on the remote (14d9af03f) which added the union test case. My commit improves the comment to explain why the result is Yes — because array<int> individually accepts the intersection (the hasOffsetValue(3, int) is compatible with array<int>, so the isSuperTypeOf guard correctly doesn't block it).

@VincentLanglet VincentLanglet merged commit 338b1b6 into phpstan:2.1.x May 7, 2026
649 of 659 checks passed
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-k8eciuw branch May 7, 2026 07:07
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