Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add IntersectionType isAcceptedBy and isSubTypeOf tests for array&cal…
…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>
  • Loading branch information
phpstan-bot and claude committed May 6, 2026
commit 3b83616057107dee7bb36cad3aaf8ba06fc3b023
159 changes: 159 additions & 0 deletions tests/PHPStan/Type/IntersectionTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,118 @@ public function testAccepts(Type $type, Type $otherType, TrinaryLogic $expectedR
);
}

/**
* @return Iterator<int, array{Type, Type, TrinaryLogic}>
*/
public static function dataIsAcceptedBy(): Iterator
{
// array&callable isAcceptedBy array - success
yield [
new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]),
new ArrayType(new MixedType(), new MixedType()),
TrinaryLogic::createYes(),
];

// array&callable isAcceptedBy array - failure
Comment thread
VincentLanglet marked this conversation as resolved.
Resolved
yield [
new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]),
new ArrayType(new MixedType(), new IntegerType()),
TrinaryLogic::createNo(),
];

// array&callable isAcceptedBy constantArray - success
Comment thread
VincentLanglet marked this conversation as resolved.
Resolved
yield [
new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]),
new ConstantArrayType(
[new ConstantIntegerType(0), new ConstantIntegerType(1)],
[new UnionType([new ObjectType('stdClass'), new StringType()]), new StringType()],
),
TrinaryLogic::createMaybe(),
];

// array&callable isAcceptedBy constantArray - failure
Comment thread
VincentLanglet marked this conversation as resolved.
Resolved
yield [
new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]),
new ConstantArrayType(
[new ConstantIntegerType(0), new ConstantIntegerType(1)],
[new StringType(), new StringType()],
),
TrinaryLogic::createMaybe(),
];

// 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(),
];
Comment on lines +151 to +160

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.


// array&hasOffsetValue isAcceptedBy array - failure
yield [
new IntersectionType([
new ArrayType(new MixedType(), new MixedType()),
new NonEmptyArrayType(),
new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()),
]),
new ArrayType(new MixedType(), new StringType()),
TrinaryLogic::createNo(),
];

// array&hasOffsetValue isAcceptedBy array - success (matching value type)
Comment thread
VincentLanglet marked this conversation as resolved.
Resolved
yield [
new IntersectionType([
new ArrayType(new MixedType(), new MixedType()),
new NonEmptyArrayType(),
new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()),
]),
new ArrayType(new MixedType(), new IntegerType()),
TrinaryLogic::createYes(),
];

// array&hasOffsetValue isAcceptedBy constantArray - success
Comment thread
VincentLanglet marked this conversation as resolved.
Resolved
yield [
new IntersectionType([
new ArrayType(new MixedType(), new MixedType()),
new NonEmptyArrayType(),
new HasOffsetValueType(new ConstantIntegerType(0), new IntegerType()),
]),
new ConstantArrayType(
[new ConstantIntegerType(0), new ConstantIntegerType(1)],
[new IntegerType(), new IntegerType()],
),
TrinaryLogic::createMaybe(),
];

// array&hasOffsetValue isAcceptedBy constantArray - failure
Comment thread
VincentLanglet marked this conversation as resolved.
Resolved
yield [
new IntersectionType([
new ArrayType(new MixedType(), new MixedType()),
new NonEmptyArrayType(),
new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()),
Comment thread
VincentLanglet marked this conversation as resolved.
Resolved
]),
new ConstantArrayType(
[new ConstantIntegerType(0), new ConstantIntegerType(1)],
[new StringType(), new StringType()],
),
TrinaryLogic::createMaybe(),
];
}

#[DataProvider('dataIsAcceptedBy')]
public function testIsAcceptedBy(Type $type, Type $acceptingType, TrinaryLogic $expectedResult): void
{
$actualResult = $acceptingType->accepts($type, true)->result;
$this->assertSame(
$expectedResult->describe(),
$actualResult->describe(),
sprintf('%s -> isAcceptedBy(%s)', $type->describe(VerbosityLevel::precise()), $acceptingType->describe(VerbosityLevel::precise())),
);
}

public static function dataIsCallable(): array
{
return [
Expand Down Expand Up @@ -362,6 +474,53 @@ public static function dataIsSubTypeOf(): Iterator
]),
TrinaryLogic::createYes(),
];

// array&callable isSubTypeOf array - success
yield [
new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]),
new ArrayType(new MixedType(), new MixedType()),
TrinaryLogic::createYes(),
];

// array&callable isSubTypeOf array<int> - failure
yield [
new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]),
new ArrayType(new MixedType(), new IntegerType()),
TrinaryLogic::createNo(),
];

// array&hasOffsetValue isSubTypeOf 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(),
];

// array&hasOffsetValue isSubTypeOf array<int> - maybe
yield [
new IntersectionType([
new ArrayType(new MixedType(), new MixedType()),
new NonEmptyArrayType(),
new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()),
]),
new ArrayType(new MixedType(), new IntegerType()),
TrinaryLogic::createMaybe(),
];

// array&hasOffsetValue isSubTypeOf array<string> - failure
yield [
new IntersectionType([
new ArrayType(new MixedType(), new MixedType()),
new NonEmptyArrayType(),
new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()),
]),
new ArrayType(new MixedType(), new StringType()),
TrinaryLogic::createNo(),
];
}

#[DataProvider('dataIsSubTypeOf')]
Expand Down
Loading