Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
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
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);

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.

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
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