diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 605c71c9da..280b94658a 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -507,6 +507,14 @@ private static function isAlreadyNormalized(array $alreadyNormalized, Type $a, T */ private static function compareTypesInUnion(Type $a, Type $b): ?array { + if ( + self::isClosureOrNonCommonCallableType($a) + && self::isClosureOrNonCommonCallableType($b) + && !$a->equals($b) + ) { + return null; + } + if ($a instanceof IntegerRangeType) { $type = $a->tryUnion($b); if ($type !== null) { @@ -655,6 +663,16 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array return null; } + private static function isClosureOrNonCommonCallableType(Type $type): bool + { + if ($type instanceof ClosureType) { + return true; + } + + return get_class($type) === CallableType::class + && !$type->equals(new CallableType()); + } + /** * @return list */ diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 810af0c7dd..23dce5c33f 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -280,6 +280,7 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Variables/data/bug-14124.php'; yield __DIR__ . '/../Rules/Variables/data/bug-14124b.php'; yield __DIR__ . '/../Rules/Arrays/data/bug-14308.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-4192.php'; } /** diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index f226cb3658..a1cc63a8bf 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -198,6 +198,11 @@ public function testNamedArguments(): void ]); } + public function testBug4192(): void + { + $this->analyse([__DIR__ . '/../Methods/data/bug-4192.php'], []); + } + public static function dataBug3566(): array { return [ diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index fcf9d6d449..e79c902ddc 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -1992,6 +1992,15 @@ public function testBug4188(): void $this->analyse([__DIR__ . '/data/bug-4188.php'], []); } + public function testBug4192(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + $this->analyse([__DIR__ . '/data/bug-4192.php'], []); + } + public function testOnlyRelevantUnableToResolveTemplateType(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/data/bug-4192.php b/tests/PHPStan/Rules/Methods/data/bug-4192.php new file mode 100644 index 0000000000..6d7979f30b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4192.php @@ -0,0 +1,78 @@ + */ + private array $array; + + /** + * @param array $array + */ + public function __construct(array $array) + { + $this->array = $array; + } + + /** + * @param \Closure|null $closure + * @phpstan-param null|(\Closure(T,TKey): bool)|(\Closure(T): bool)|(\Closure(TKey): bool) $closure + */ + public function filter($closure = null, int $flag = \ARRAY_FILTER_USE_BOTH): void + { + if (!$closure) { + return; + } + + if ($flag === \ARRAY_FILTER_USE_KEY) { + /** @phpstan-var \Closure(TKey): bool $closure */ + $closure = $closure; + $generator = function () use ($closure): void { + foreach ($this->array as $key => $value) { + assertType('bool', $closure($key)); + } + }; + $generator(); + } elseif ($flag === \ARRAY_FILTER_USE_BOTH) { + /** @phpstan-var \Closure(T,TKey): bool $closure */ + $closure = $closure; + $generator = function () use ($closure): void { + foreach ($this->array as $key => $value) { + assertType('bool', $closure($value, $key)); + } + }; + $generator(); + } else { + /** @phpstan-var \Closure(T): bool $closure */ + $closure = $closure; + $generator = function () use ($closure): void { + foreach ($this->array as $key => $value) { + assertType('bool', $closure($value)); + } + }; + $generator(); + } + } + +} + +(new Arrayy([0 => 1, 1 => 2, 2 => 3, 3 => 4, 7 => 7]))->filter( + static function ($value): bool { + return $value % 2 !== 0; + }, +); + +(new Arrayy([0 => 1, 1 => 2, 2 => 3, 3 => 4, 7 => 7]))->filter( + static function ($key, $value): bool { + return ($value % 2 !== 0) && (($key & 2) !== 0); + }, + \ARRAY_FILTER_USE_BOTH, +);