Skip to content
Closed
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
6 changes: 3 additions & 3 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1692,19 +1692,19 @@ parameters:
-
rawMessage: 'Doing instanceof PHPStan\Type\ArrayType is error-prone and deprecated. Use Type::isArray() or Type::getArrays() instead.'
identifier: phpstanApi.instanceofType
count: 5
count: 7
path: src/Type/TypeCombinator.php

-
rawMessage: 'Doing instanceof PHPStan\Type\CallableType is error-prone and deprecated. Use Type::isCallable() and Type::getCallableParametersAcceptors() instead.'
identifier: phpstanApi.instanceofType
count: 1
count: 5
path: src/Type/TypeCombinator.php

-
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantArrayType is error-prone and deprecated. Use Type::getConstantArrays() instead.'
identifier: phpstanApi.instanceofType
count: 19
count: 21
path: src/Type/TypeCombinator.php

-
Expand Down
2 changes: 1 addition & 1 deletion src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco
return new IntersectionType([new ObjectWithoutClassType(), new CallableType()]);

case 'callable-array':
return new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]);
return TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), new CallableType());

case 'never':
case 'noreturn':
Expand Down
23 changes: 0 additions & 23 deletions src/Type/IntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -945,14 +945,6 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic

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

if (!$result->yes() && $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;
}

Expand All @@ -963,21 +955,6 @@ public function getOffsetValueType(Type $offsetType): Type
return TypeUtils::toBenevolentUnion($result);
}

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);
}
}

return $result;
}

Expand Down
112 changes: 112 additions & 0 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -1683,6 +1683,54 @@ public static function intersect(Type ...$types): Type
continue;
}

if (
$types[$i] instanceof ArrayType
&& get_class($types[$i]) === ArrayType::class
&& $types[$j] instanceof CallableType
) {
$narrowed = self::narrowArrayTypeWithCallable($types[$i]);
if ($narrowed instanceof NeverType) {
return new NeverType();
}
$types[$i] = $narrowed;
continue;
}

if (
$types[$j] instanceof ArrayType
&& get_class($types[$j]) === ArrayType::class
&& $types[$i] instanceof CallableType
) {
$narrowed = self::narrowArrayTypeWithCallable($types[$j]);
if ($narrowed instanceof NeverType) {
return new NeverType();
}
$types[$j] = $narrowed;
continue;
}

if (
$types[$i] instanceof ConstantArrayType
&& $types[$j] instanceof CallableType
) {
$types[$i] = self::narrowConstantArrayWithCallable($types[$i]);
if ($types[$i] instanceof NeverType) {
return new NeverType();
}
continue;
}

if (
$types[$j] instanceof ConstantArrayType
&& $types[$i] instanceof CallableType
) {
$types[$j] = self::narrowConstantArrayWithCallable($types[$j]);
if ($types[$j] instanceof NeverType) {
return new NeverType();
}
continue;
}

continue;
}

Expand Down Expand Up @@ -1779,4 +1827,68 @@ public static function removeTruthy(Type $type): Type
return self::remove($type, StaticTypeFactory::truthy());
}

private static function narrowArrayTypeWithCallable(ArrayType $arrayType): Type
{
if ($arrayType->getKeyType()->isSuperTypeOf(new IntegerType())->no()) {
return new NeverType();
}

$existingValueType = $arrayType->getItemType();
$offset0ValueType = self::intersect($existingValueType, new UnionType([new ClassStringType(), new ObjectWithoutClassType()]));
$offset1ValueType = self::intersect($existingValueType, new StringType());
if ($offset0ValueType instanceof NeverType || $offset1ValueType instanceof NeverType) {
return new NeverType();
}

return new ConstantArrayType(
[new ConstantIntegerType(0), new ConstantIntegerType(1)],
[$offset0ValueType, $offset1ValueType],
[2],
isList: TrinaryLogic::createYes(),
);
}

private static function narrowConstantArrayWithCallable(ConstantArrayType $constantArray): Type
{
$keyTypes = $constantArray->getKeyTypes();
$valueTypes = $constantArray->getValueTypes();

if (count($keyTypes) < 2) {
return new NeverType();
}

$offset0Index = null;
$offset1Index = null;

foreach ($keyTypes as $k => $keyType) {
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.

we might want to immediately return NeverType when we have less than 2 key-types, so we don't create new ConstantArrayType for "too small/empty" constant arrays

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.

Done. The change adds an early NeverType return in narrowConstantArrayWithCallable when the constant array has fewer than 2 keys or doesn't contain both offsets 0 and 1 — avoiding unnecessary ConstantArrayType construction for arrays that can't be callable. All tests pass and phpstan reports no errors.

if ((new ConstantIntegerType(0))->isSuperTypeOf($keyType)->yes()) {
$offset0Index = $k;
} elseif ((new ConstantIntegerType(1))->isSuperTypeOf($keyType)->yes()) {
$offset1Index = $k;
}
}

if ($offset0Index === null || $offset1Index === null) {
return new NeverType();
}

$newValueTypes = $valueTypes;
$newValueTypes[$offset0Index] = self::intersect($valueTypes[$offset0Index], new UnionType([new ClassStringType(), new ObjectWithoutClassType()]));
if ($newValueTypes[$offset0Index] instanceof NeverType) {
return new NeverType();
}
$newValueTypes[$offset1Index] = self::intersect($valueTypes[$offset1Index], new StringType());
if ($newValueTypes[$offset1Index] instanceof NeverType) {
return new NeverType();
}

return new ConstantArrayType(
$keyTypes,
$newValueTypes,
$constantArray->getNextAutoIndexes(),
$constantArray->getOptionalKeys(),
$constantArray->isList(),
);
}

}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-12393.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ class CallableArray {

public function doFoo(callable $foo): void {
$this->foo = $foo;
assertType('array', $this->foo); // could be non-empty-array
assertType('array<mixed>', $this->foo); // could be non-empty-array
}
}

Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-12393b.php
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ class CallableArray {

public function doFoo(callable $foo): void {
$this->foo = $foo;
assertType('array', $this->foo); // could be non-empty-array
assertType('array<mixed>', $this->foo); // could be non-empty-array
}
}

Expand Down
70 changes: 70 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14549.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php declare(strict_types = 1);

namespace Bug14549;

use function PHPStan\Testing\assertType;

class Foo
{
public function foo(array $task): void
{
if (\is_callable($task)) {
assertType('list{class-string|object, string}&callable(): mixed', $task);
assertType('class-string|object', $task[0]);
assertType('string', $task[1]);

foreach ($task as $key => $value) {
assertType('object|string', $value);
assertType('0|1', $key);
}
}
}

public function testCallableArrayIterableTypes(callable $value): void
{
if (is_array($value)) {
assertType('list{class-string|object, string}&callable(): mixed', $value);

foreach ($value as $key => $val) {
assertType('0|1', $key);
assertType('object|string', $val);
}
}
}

/** @param array{string, string} $task */
public function testConstantArrayNarrowing(array $task): void
{
if (\is_callable($task)) {
assertType('list{class-string, string}&callable(): mixed', $task);
assertType('class-string', $task[0]);
assertType('string', $task[1]);
}
}

/** @param array<string> $task */
public function testTypedArrayNarrowing(array $task): void
{
if (\is_callable($task)) {
// When value type is string, intersect with class-string|object gives class-string
// and intersect with string gives string
assertType('list{class-string, string}&callable(): mixed', $task);
}
}

/** @param array<string, mixed> $task */
public function testStringKeyedArrayNarrowing(array $task): void
{
if (\is_callable($task)) {
assertType('*NEVER*', $task);
}
}

/** @param callable-array $value */
public function testCallableArrayPhpDoc(array $value): void
{
assertType('list{class-string|object, string}&callable(): mixed', $value);
assertType('class-string|object', $value[0]);
assertType('string', $value[1]);
}
}
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/nsrt/bug-3842.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ public function callback(): void

function testIsArrayOnCallable(callable $value): void {
if (is_array($value)) {
assertType('array<mixed, mixed>&callable(): mixed', $value);
assertType('list{class-string|object, string}&callable(): mixed', $value);
assertType('class-string|object', $value[0]);
assertType('string', $value[1]);
}
}

/** @param callable-array $value */
function testCallableArrayPhpDoc(array $value): void {
assertType('array&callable(): mixed', $value);
assertType('list{class-string|object, string}&callable(): mixed', $value);
assertType('class-string|object', $value[0]);
assertType('string', $value[1]);
}
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/more-types.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function doFoo(
): void
{
assertType('pure-callable(): mixed', $pureCallable);
assertType('array&callable(): mixed', $callableArray);
assertType('list{class-string|object, string}&callable(): mixed', $callableArray);
assertType('resource', $closedResource);
assertType('resource', $openResource);
assertType('class-string<UnitEnum>', $enumString);
Expand Down
13 changes: 13 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4017,4 +4017,17 @@ 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.php'], [
[
'Parameter #1 $task of method Bug14549Rule\Foo::call() expects array<int>, callable&list<object|string> given.',
10,
],
]);
}

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

namespace Bug14549Rule;

class Foo
{
public function foo(array $task): void
{
if (\is_callable($task)) {
$this->call($task);
}
}

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