Skip to content
Open
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
9 changes: 9 additions & 0 deletions src/Analyser/Generics/TemplateArgumentObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,15 @@
}
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()) {

Check warning on line 636 in src/Analyser/Generics/TemplateArgumentObserver.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ // 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->isCallable()->no()) { if ($member->isSuperTypeOf($argumentMember)->yes()) { $taken = true; }

Check warning on line 636 in src/Analyser/Generics/TemplateArgumentObserver.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ // 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->isCallable()->no()) { if ($member->isSuperTypeOf($argumentMember)->yes()) { $taken = true; }
if ($member->isSuperTypeOf($argumentMember)->yes()) {

Check warning on line 637 in src/Analyser/Generics/TemplateArgumentObserver.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ // 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()) { + if (!$member->isSuperTypeOf($argumentMember)->no()) { $taken = true; } continue;

Check warning on line 637 in src/Analyser/Generics/TemplateArgumentObserver.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ // 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()) { + if (!$member->isSuperTypeOf($argumentMember)->no()) { $taken = true; } continue;
$taken = true;
}
continue;
}
$before = $constraints;
$constraints = $this->observeLowerBound($constraints, $member, $argumentMember);
if ($constraints === $before) {
Expand Down
112 changes: 112 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15414.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace Bug15414;

use Closure;
use function PHPStan\Testing\assertType;

/**
* @template TValue
* @implements \IteratorAggregate<int, TValue>
*/
final class Coll implements \IteratorAggregate
{
/** @param list<TValue> $items */
public function __construct(private array $items = [])
{
}

/** @return \ArrayIterator<int, TValue> */
#[\Override]
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->items);
}

/**
* @template TMergeValue
* @param iterable<TMergeValue> $items
* @return Coll<TValue|TMergeValue>
*/
public function merge(iterable $items): Coll
{
return new Coll();
}

/**
* @param (callable(TValue): bool)|TValue $callback
* @return Coll<TValue>
*/
public function reject($callback): Coll
{
return $this;
}

/**
* @param (Closure(TValue): bool)|TValue $callback
* @return Coll<TValue>
*/
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<Field> $a
* @param Coll<Field> $b
*/
function test(Coll $a, Coll $b, Field $f, FieldFilter $filter): void
{
assertType('Bug15414\Coll<Bug15414\Field>', $a->merge($b)->reject(isExcluded(...)));
assertType('Bug15414\Coll<Bug15414\Field>', $a->merge($b)->reject(static fn (Field $f): bool => isExcluded($f)));
assertType('Bug15414\Coll<Bug15414\Field>', $a->reject(isExcluded(...)));

assertType('Bug15414\Coll<Bug15414\Field>', $a->merge($b)->reject($f->isHidden(...)));
assertType('Bug15414\Coll<Bug15414\Field>', $a->merge($b)->reject(Field::isHiddenStatic(...)));
assertType('Bug15414\Coll<Bug15414\Field>', $a->merge($b)->reject(static function (Field $f): bool {
return isExcluded($f);
}));

assertType('Bug15414\Coll<Bug15414\Field>', $a->merge($b)->rejectClosure(isExcluded(...)));
assertType('Bug15414\Coll<Bug15414\Field>', $a->merge($b)->rejectClosure(static fn (Field $f): bool => isExcluded($f)));

assertType('Bug15414\Coll<Bug15414\Field>', $a->merge($b)->reject($filter));
}

function testEmpty(Field $f): void
{
$c = new Coll();
$c = $c->merge([$f])->reject(isExcluded(...));
assertType('Bug15414\Coll<Bug15414\Field>', $c);
}
10 changes: 10 additions & 0 deletions turbo-ext/src/TemplateArgumentObserver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading