Skip to content
7 changes: 7 additions & 0 deletions src/Type/IntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,13 @@
static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes),
);

if ($result->yes()) {

Check warning on line 307 in src/Type/IntersectionType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes), ); - if ($result->yes()) { + if (!$result->no()) { $isSuperType = $acceptingType->isSuperTypeOf($this); if ($isSuperType->no()) { return $isSuperType->toAcceptsResult();

Check warning on line 307 in src/Type/IntersectionType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes), ); - if ($result->yes()) { + if (!$result->no()) { $isSuperType = $acceptingType->isSuperTypeOf($this); if ($isSuperType->no()) { return $isSuperType->toAcceptsResult();
$isSuperType = $acceptingType->isSuperTypeOf($this);
if ($isSuperType->no()) {
return $isSuperType->toAcceptsResult();
}
}

if ($this->isOversizedArray()->yes()) {
if (!$result->no()) {
return AcceptsResult::createYes();
Expand Down
37 changes: 37 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4025,4 +4025,41 @@ public function testBug13272(): void
$this->analyse([__DIR__ . '/data/bug-13272.php'], []);
}

public function testBug14549(): void
{
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/data/bug-14549-bis.php'], [
[
'Parameter #1 $param of method Bug14549Bis\Foo::callArrayInt() expects array<int>, array&callable given.',
33,
],
[
'Parameter #1 $param of method Bug14549Bis\Foo::callConstantArrayStringString() expects array{string, string}, array&callable(): mixed given.',
34,
],
[
'Parameter #1 $param of method Bug14549Bis\Foo::callConstantArrayObjectOrStringStringString() expects array{object|string, string, string}, array&callable(): mixed given.',
36,
],
[
'Parameter #1 $param of method Bug14549Bis\Foo::callArrayInt() expects array<int>, array&callable given.',
44,
],
[
'Parameter #1 $param of method Bug14549Bis\Foo::callConstantArrayStringString() expects array{string, string}, array&callable(): mixed given.',
45,
],
[
'Parameter #1 $param of method Bug14549Bis\Foo::callConstantArrayObjectOrStringStringString() expects array{object|string, string, string}, array&callable(): mixed given.',
47,
],
[
'Parameter #1 $param of method Bug14549Bis\Foo::callArrayString() expects array<string>, array given.',
58,
],
]);
}

}
63 changes: 63 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-14549-bis.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace Bug14549Bis;

class Foo
{

/** @param array<int> $param */
public function callArrayInt(array $param): void
{
}

/** @param array{string, string} $param */
public function callConstantArrayStringString(array $param): void
{
}

/** @param array{object|string, string} $param */
public function callConstantArrayObjectOrStringString(array $param): void
{
}

/** @param array{object|string, string, string} $param */
public function callConstantArrayObjectOrStringStringString(array $param): void
{
}

/**
* @param callable-array $task
*/
public function doCallWithCallableArray(array $task): void
{
$this->callArrayInt($task);
$this->callConstantArrayStringString($task);
$this->callConstantArrayObjectOrStringString($task);
$this->callConstantArrayObjectOrStringStringString($task);
}

/**
* @param callable&array $task
*/
public function doCallWithCallableAndArray(array $task): void
{
$this->callArrayInt($task);
$this->callConstantArrayStringString($task);
$this->callConstantArrayObjectOrStringString($task);
$this->callConstantArrayObjectOrStringStringString($task);
}

/** @param array<string> $param */
public function callArrayString(array $param): void
{
}

public function doCallWithHasOffsetValue(array $arr): void
{
if (isset($arr[1]) && $arr[1] === 1) {
$this->callArrayString($arr);
$this->callArrayInt($arr);
}
}

}
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
yield [
new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]),
new ArrayType(new MixedType(), new IntegerType()),
TrinaryLogic::createNo(),
];

// array&callable isAcceptedBy constantArray - success
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
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(),
];

// 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)
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
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
yield [
new IntersectionType([
new ArrayType(new MixedType(), new MixedType()),
new NonEmptyArrayType(),
new HasOffsetValueType(new ConstantIntegerType(3), new IntegerType()),
]),
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