Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 12 additions & 1 deletion src/Type/ArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,18 @@ private static function foldConstantStringKeyCase(ConstantStringType $type, ?int

public function isCallable(): TrinaryLogic
{
return TrinaryLogic::createMaybe()->and($this->itemType->isString());
if (!$this->itemType->isString()->no()) {
return TrinaryLogic::createMaybe();
}

// StrictMixedType denies isString() even though it is a supertype of
// string, so a value of that item type can still be the method name
// of a callable array.
if ((new StringType())->isSuperTypeOf($this->itemType)->maybe()) {
return TrinaryLogic::createMaybe();
}

return TrinaryLogic::createNo();
}

public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
Expand Down
63 changes: 63 additions & 0 deletions tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3029,4 +3029,67 @@ public function testBug11494(): void
]);
}

public static function dataBug13114(): array
{
return [
[false, false],
[true, false],
[true, true],
];
}

#[DataProvider('dataBug13114')]
public function testBug13114(bool $checkExplicitMixed, bool $checkImplicitMixed): void
{
$this->checkExplicitMixed = $checkExplicitMixed;
$this->checkImplicitMixed = $checkImplicitMixed;
$this->analyse([__DIR__ . '/data/bug-13114.php'], [
[
'Parameter #1 $arg of function Bug13114\\foo expects list{class-string|object, string}&callable(): mixed, array{Bug13114\\C, \'h\'} given.',
37,
],
[
'Parameter #1 $arg of function Bug13114\\bar expects non-empty-list<mixed>&callable(): mixed, array{Bug13114\\C, \'h\'} given.',
42,
],
[
'Parameter #1 $arg of function Bug13114\\baz expects non-empty-list&callable(): mixed, array{Bug13114\\C, \'h\'} given.',
47,
],
[
'Parameter #1 $arg of function Bug13114\\baz expects non-empty-list&callable(): mixed, array{1, 2} given.',
48,
],
[
'Parameter #1 $arg of function Bug13114\\baz expects non-empty-list&callable(): mixed, 42 given.',
49,
"\u{2022} 42 is not a list.\n\u{2022} 42 is empty.",
],
[
'Parameter #1 $arg of function Bug13114\\callableString expects callable-string, \'nonexistentFunction\' given.',
52,
],
[
'Parameter #1 $arg of function Bug13114\\callableString expects callable-string, 42 given.',
53,
],
[
'Parameter #1 $arg of function Bug13114\\callableObject expects callable-object, Bug13114\\C given.',
55,
],
[
'Parameter #1 $arg of function Bug13114\\callableObject expects callable-object, 42 given.',
57,
],
[
'Parameter #1 $i of function Bug13114\\takesInt expects int, callable&list<mixed> given.',
67,
],
[
'Parameter #1 $i of function Bug13114\\takesInt expects int, callable&list<mixed> given.',
77,
],
]);
}

}
12 changes: 12 additions & 0 deletions tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -481,4 +481,16 @@ public function testBug13190(): void
]);
}

public function testBug13114(): void
{
$this->checkNullables = true;
$this->checkExplicitMixed = true;
$this->analyse([__DIR__ . '/data/bug-13114-return.php'], [
[
'Function Bug13114Return\\returnsCallableArray() should return non-empty-list<mixed>&callable(): mixed but returns array{Bug13114Return\\C, \'h\'}.',
14,
],
]);
}

}
23 changes: 23 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-13114-return.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php declare(strict_types = 1);

namespace Bug13114Return;

class C {
static function f(): void {}
}

/**
* @return callable&array<mixed>
*/
function returnsCallableArray(): array
{
return [new C, 'h'];
}

/**
* @return callable&array<mixed>
*/
function returnsValidCallableArray(): array
{
return [C::class, 'f'];
}
79 changes: 79 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-13114.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php declare(strict_types = 1);

namespace Bug13114;

class C {
static function f(): void {}
function g(): void {}
}

/**
* @param callable&array{class-string|object, string} $arg
*/
function foo(array $arg): void {}

/**
* @param callable&array<mixed> $arg
*/
function bar(array $arg): void {}

/**
* @param callable-array $arg
*/
function baz($arg): void {}

/**
* @param callable&string $arg
*/
function callableString($arg): void {}

/**
* @param callable&object $arg
*/
function callableObject($arg): void {}

foo([new C, 'f']);
foo([new C, 'g']);
foo([new C, 'h']); // error
foo([C::class, 'f']);

bar([new C, 'f']);
bar([new C, 'g']);
bar([new C, 'h']); // error
bar([C::class, 'f']);

baz([new C, 'f']);
baz([new C, 'g']);
baz([new C, 'h']); // error
baz([1, 2]); // error
baz(42); // error

callableString('strtoupper');
callableString('nonexistentFunction'); // error
callableString(42); // error

callableObject(new C()); // error
callableObject(function (): void {});
callableObject(42); // error

function takesInt(int $i): void {}

/**
* @param array<mixed> $a
*/
function narrowedByIsCallable(array $a): void
{
if (is_callable($a)) {
takesInt($a); // error
}
}

/**
* @param mixed $m
*/
function narrowedFromMixed($m): void
{
if (is_callable($m) && is_array($m)) {
takesInt($m); // error
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1067,4 +1067,25 @@ public function testBug10749(): void
$this->analyse([__DIR__ . '/data/bug-10749.php'], []);
}

public function testBug13114(): void
{
$this->checkExplicitMixed = true;
$this->checkImplicitMixed = true;
$this->analyse([__DIR__ . '/data/bug-13114.php'], [
[
'Property Bug13114Property\\Holder::$implicit (non-empty-list&callable(): mixed) does not accept array{Bug13114Property\\C, \'h\'}.',
20,
],
[
'Property Bug13114Property\\Holder::$explicit (non-empty-list<mixed>&callable(): mixed) does not accept array{Bug13114Property\\C, \'h\'}.',
21,
],
[
'Property Bug13114Property\\Holder::$implicit (non-empty-list&callable(): mixed) does not accept 42.',
22,
"\u{2022} 42 is not a list.\n\u{2022} 42 is empty.",
],
]);
}

}
31 changes: 31 additions & 0 deletions tests/PHPStan/Rules/Properties/data/bug-13114.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);

namespace Bug13114Property;

class C {
static function f(): void {}
}

class Holder
{

/** @var callable-array */
private $implicit;

/** @var callable&array<mixed> */
private $explicit;

public function doFoo(): void
{
$this->implicit = [new C, 'h'];
$this->explicit = [new C, 'h'];
$this->implicit = 42;
}

public function doBar(): void
{
$this->implicit = [C::class, 'f'];
$this->explicit = [C::class, 'f'];
}

}
18 changes: 18 additions & 0 deletions tests/PHPStan/Type/TypeCombinatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5760,6 +5760,24 @@ public static function dataIntersect(): iterable
UnionType::class,
"1|'0'",
];

yield [
[
new ArrayType(new MixedType(), new StrictMixedType()),
new CallableType(),
],
IntersectionType::class,
'non-empty-list<mixed>&callable(): mixed',
];

yield [
[
new ArrayType(new MixedType(), new IntegerType()),
new CallableType(),
],
NeverType::class,
'*NEVER*=implicit',
];
}

/**
Expand Down
Loading