diff --git a/src/Analyser/Generics/TemplateArgumentObserver.php b/src/Analyser/Generics/TemplateArgumentObserver.php index 43d9cfa68e4..407c97b31a2 100644 --- a/src/Analyser/Generics/TemplateArgumentObserver.php +++ b/src/Analyser/Generics/TemplateArgumentObserver.php @@ -630,6 +630,15 @@ private function observeLowerBound(TemplateArgumentConstraints $constraints, Typ } continue; } + // a callable member says nothing flows into its markers, but + // it still takes the callable it accepts: (callable(T): bool)|T + // receiving a closure does not bind T to the closure + if ($member->isCallable()->yes()) { + if ($member->isSuperTypeOf($argumentMember)->yes()) { + $taken = true; + } + continue; + } $before = $constraints; $constraints = $this->observeLowerBound($constraints, $member, $argumentMember); if ($constraints === $before) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-15414.php b/tests/PHPStan/Analyser/nsrt/bug-15414.php new file mode 100644 index 00000000000..3020503e2a6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15414.php @@ -0,0 +1,112 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug15414; + +use Closure; +use function PHPStan\Testing\assertType; + +/** + * @template TValue + * @implements \IteratorAggregate + */ +final class Coll implements \IteratorAggregate +{ + /** @param list $items */ + public function __construct(private array $items = []) + { + } + + /** @return \ArrayIterator */ + #[\Override] + public function getIterator(): \ArrayIterator + { + return new \ArrayIterator($this->items); + } + + /** + * @template TMergeValue + * @param iterable $items + * @return Coll + */ + public function merge(iterable $items): Coll + { + return new Coll(); + } + + /** + * @param (callable(TValue): bool)|TValue $callback + * @return Coll + */ + public function reject($callback): Coll + { + return $this; + } + + /** + * @param (Closure(TValue): bool)|TValue $callback + * @return Coll + */ + public function rejectClosure($callback): Coll + { + return $this; + } +} + +final class Field +{ + public bool $hidden = false; + + public function isHidden(Field $field): bool + { + return $field->hidden; + } + + public static function isHiddenStatic(Field $field): bool + { + return $field->hidden; + } +} + +final class FieldFilter +{ + public function __invoke(Field $field): bool + { + return $field->hidden; + } +} + +function isExcluded(Field $field): bool +{ + return $field->hidden; +} + +/** + * @param Coll $a + * @param Coll $b + */ +function test(Coll $a, Coll $b, Field $f, FieldFilter $filter): void +{ + assertType('Bug15414\Coll', $a->merge($b)->reject(isExcluded(...))); + assertType('Bug15414\Coll', $a->merge($b)->reject(static fn (Field $f): bool => isExcluded($f))); + assertType('Bug15414\Coll', $a->reject(isExcluded(...))); + + assertType('Bug15414\Coll', $a->merge($b)->reject($f->isHidden(...))); + assertType('Bug15414\Coll', $a->merge($b)->reject(Field::isHiddenStatic(...))); + assertType('Bug15414\Coll', $a->merge($b)->reject(static function (Field $f): bool { + return isExcluded($f); + })); + + assertType('Bug15414\Coll', $a->merge($b)->rejectClosure(isExcluded(...))); + assertType('Bug15414\Coll', $a->merge($b)->rejectClosure(static fn (Field $f): bool => isExcluded($f))); + + assertType('Bug15414\Coll', $a->merge($b)->reject($filter)); +} + +function testEmpty(Field $f): void +{ + $c = new Coll(); + $c = $c->merge([$f])->reject(isExcluded(...)); + assertType('Bug15414\Coll', $c); +} diff --git a/turbo-ext/src/TemplateArgumentObserver.cpp b/turbo-ext/src/TemplateArgumentObserver.cpp index 265a244c6f9..43337091132 100644 --- a/turbo-ext/src/TemplateArgumentObserver.cpp +++ b/turbo-ext/src/TemplateArgumentObserver.cpp @@ -989,6 +989,16 @@ class TemplateArgumentObserver if (verdict == PT_TRI_YES) taken = true; continue; } + zend_long memberCallable = pt_type_op_trinary(Z_OBJ_P(member), PT_OP_IS_CALLABLE, 0, NULL); + if (UNEXPECTED(memberCallable < 0)) return zv::Val(); + if (memberCallable == PT_TRI_YES) { + zv::Val superType = pt_type_op(Z_OBJ_P(member), PT_OP_IS_SUPER_TYPE_OF, 1, argumentMember); + if (UNEXPECTED(superType.isUndef())) return zv::Val(); + zend_long verdict = pt_type_result_trinary(superType.raw()); + if (UNEXPECTED(verdict < 0)) return zv::Val(); + if (verdict == PT_TRI_YES) taken = true; + continue; + } zv::Val before = zv::Val::copyOf(zv::Ref(constraints.raw())); constraints = observeLowerBound(std::move(constraints), member, argumentMember); if (UNEXPECTED(constraints.isUndef())) return zv::Val();