Skip to content
3 changes: 3 additions & 0 deletions src/Rules/MissingTypehintCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@
if ($type instanceof AccessoryType) {
return $type;
}
if ($type->isCallable()->yes() && $type->isArray()->yes()) {

Check warning on line 76 in src/Rules/MissingTypehintCheck.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($type instanceof AccessoryType) { return $type; } - if ($type->isCallable()->yes() && $type->isArray()->yes()) { + if ($type->isCallable()->yes() && !$type->isArray()->no()) { return $type; } if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) {

Check warning on line 76 in src/Rules/MissingTypehintCheck.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($type instanceof AccessoryType) { return $type; } - if ($type->isCallable()->yes() && $type->isArray()->yes()) { + if ($type->isCallable()->yes() && !$type->isArray()->no()) { return $type; } if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) {
return $type;
}
if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) {
$iterablesWithMissingValueTypehint = array_merge(
$iterablesWithMissingValueTypehint,
Expand Down
38 changes: 23 additions & 15 deletions src/Type/IntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,9 @@ public function getArraySize(): Type

public function getIterableKeyType(): Type
{
if ($this->isCallable()->yes() && $this->isArray()->yes()) {
return new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]);
}
return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType());
}

Expand All @@ -797,7 +800,17 @@ public function getLastIterableKeyType(): Type

public function getIterableValueType(): Type
{
return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
$result = $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
if ($this->isCallable()->yes() && $this->isArray()->yes()) {
return TypeCombinator::intersect(
$result,
new UnionType([
new ObjectWithoutClassType(),
new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]),
]),
);
}
return $result;
}

public function getFirstIterableValueType(): Type
Expand Down Expand Up @@ -960,17 +973,15 @@ private function doHasOffsetValueType(Type $offsetType): TrinaryLogic
}
}

$result = $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));

if (!$result->yes() && $this->isCallable()->yes() && $this->isArray()->yes()) {
if ($this->isCallable()->yes() && $this->isArray()->yes()) {
$arrayKeyOffsetType = $offsetType->toArrayKey();
$callableArrayOffsetType = new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]);
if ($callableArrayOffsetType->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
return TrinaryLogic::createYes();
}
}

return $result;
return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
}

public function getOffsetValueType(Type $offsetType): Type
Expand All @@ -991,17 +1002,14 @@ private function doGetOffsetValueType(Type $offsetType): Type

if ($this->isCallable()->yes() && $this->isArray()->yes()) {
$arrayKeyOffsetType = $offsetType->toArrayKey();
$callableArrayOffsetType = new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]);
if ($callableArrayOffsetType->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
$narrowedType = new UnionType([new ClassStringType(), new ObjectWithoutClassType()]);
} elseif ((new ConstantIntegerType(1))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
$narrowedType = new StringType();
} else {
$narrowedType = new UnionType([new StringType(), new ObjectWithoutClassType()]);
}
$result = TypeCombinator::intersect($result, $narrowedType);
if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
$narrowedType = new UnionType([new ClassStringType(), new ObjectWithoutClassType()]);
} elseif ((new ConstantIntegerType(1))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
$narrowedType = new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]);
} else {
$narrowedType = new UnionType([new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), new ObjectWithoutClassType()]);
}
$result = TypeCombinator::intersect($result, $narrowedType);
}

return $result;
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ private static function findTestFiles(): iterable
yield __DIR__ . '/../Rules/Variables/data/bug-7417.php';
yield __DIR__ . '/../Rules/Arrays/data/bug-7469.php';
yield __DIR__ . '/../Rules/Variables/data/bug-3391.php';
yield __DIR__ . '/../Rules/Methods/data/bug-14549.php';

yield __DIR__ . '/../Rules/Functions/data/bug-anonymous-function-method-constant.php';

Expand Down
6 changes: 3 additions & 3 deletions tests/PHPStan/Analyser/nsrt/bug-3842.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ function testIsArrayOnCallable(callable $value): void {
if (is_array($value)) {
assertType('array<mixed, mixed>&callable(): mixed', $value);
assertType('class-string|object', $value[0]);
assertType('string', $value[1]);
assertType('non-falsy-string', $value[1]);
}
}

/** @param callable-array $value */
function testCallableArrayPhpDoc(array $value): void {
assertType('array&callable(): mixed', $value);
assertType('class-string|object', $value[0]);
assertType('string', $value[1]);
assertType('non-falsy-string', $value[1]);
}

function testIsStringOnCallable(callable $value): void {
Expand All @@ -50,7 +50,7 @@ function checkClassString(array $values): void {
/** @param 0|1 $offset */
function testCallableArrayUnionOffset(callable $value, int $offset): void {
if (is_array($value)) {
assertType('object|string', $value[$offset]);
assertType('object|non-falsy-string', $value[$offset]);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,14 @@ public function testBug7662(): void
]);
}

public function testBug14549(): void
{
$this->analyse([__DIR__ . '/data/bug-14549.php'], [
[
'Method Bug14549\Foo::doFoo() has parameter $task with no signature specified for callable.',
12,
],
]);
}

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

namespace Bug14549;

use function PHPStan\Testing\assertType;

class Foo
{
/**
* @param callable-array $task
*/
public function doFoo(array $task): void
{
foreach($task as $k => $v) {
assertType('0|1', $k);
assertType('object|non-falsy-string', $v);
}
assertType('class-string|object', $task[0]);
assertType('non-falsy-string', $task[1]);
}

/**
* @param non-empty-list<string> $list
*/
public function doBar(array $list): void
{
if ($list[0] !== '') {
assertType('non-empty-list<string>&hasOffsetValue(0, non-empty-string)', $list);

if (is_callable($list)) {
assertType('non-empty-list<string>&callable(): mixed&hasOffsetValue(0, non-empty-string)', $list);
assertType('non-empty-string', $list[0]);
assertType('non-falsy-string', $list[1]);

foreach($list as $k => $v) {
assertType('0|1', $k);
assertType('non-falsy-string', $v);
}
}
}
}
}


18 changes: 18 additions & 0 deletions tests/PHPStan/Type/Constant/ConstantStringTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,22 @@ public function testSetInvalidValue(): void
$this->assertInstanceOf(ErrorType::class, $result);
}

#[DataProvider('dataIsCallable')]
public function testIsCallable(TrinaryLogic $trinaryLogic, string $constantValue): void
{
$this->assertSame(
$trinaryLogic,
(new ConstantStringType($constantValue))->isCallable(),
);
}

public static function dataIsCallable(): iterable
{
yield [TrinaryLogic::createNo(), ''];
yield [TrinaryLogic::createNo(), '0'];
yield [TrinaryLogic::createYes(), 'substr'];
yield [TrinaryLogic::createYes(), self::class . '::dataIsCallable'];
yield [TrinaryLogic::createMaybe(), self::class . '::methodDoesNotExist'];
}

}
Loading