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
21 changes: 18 additions & 3 deletions src/Analyser/Generics/TemplateArgumentObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ResolvedFunctionVariant;
use PHPStan\Turbo\ShadowedByTurboExtension;
use PHPStan\Type\CallableType;
use PHPStan\Type\ClosureType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeHelper;
Expand Down Expand Up @@ -606,9 +607,7 @@
// never is its own iterable key and value type
return $constraints;
}
if ($parameterType instanceof TemplateType || $parameterType->isCallable()->yes()) {
// callable parameters put the template in a contravariant position:
// what they say about it is an upper bound, not something flowing in
if ($parameterType instanceof TemplateType) {
return $constraints;
}
if ($parameterType instanceof UnionType) {
Expand Down Expand Up @@ -659,6 +658,22 @@
return $constraints;
}

if (($parameterType instanceof CallableType || $parameterType instanceof ClosureType) && $argumentType->isCallable()->yes()) { // @phpstan-ignore phpstanApi.instanceofType (Traverse signatures only; nominal invocable objects can return themselves.)

Check warning on line 661 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": @@ @@ return $constraints; } - if (($parameterType instanceof CallableType || $parameterType instanceof ClosureType) && $argumentType->isCallable()->yes()) { // @PHPStan-Ignore phpstanApi.instanceofType (Traverse signatures only; nominal invocable objects can return themselves.) + if (($parameterType instanceof CallableType || $parameterType instanceof ClosureType) && !$argumentType->isCallable()->no()) { // @PHPStan-Ignore phpstanApi.instanceofType (Traverse signatures only; nominal invocable objects can return themselves.) $scope = new OutOfClassScope(); foreach ($parameterType->getCallableParametersAcceptors($scope) as $parameterAcceptor) { foreach ($argumentType->getCallableParametersAcceptors($scope) as $argumentAcceptor) {

Check warning on line 661 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": @@ @@ return $constraints; } - if (($parameterType instanceof CallableType || $parameterType instanceof ClosureType) && $argumentType->isCallable()->yes()) { // @PHPStan-Ignore phpstanApi.instanceofType (Traverse signatures only; nominal invocable objects can return themselves.) + if (($parameterType instanceof CallableType || $parameterType instanceof ClosureType) && !$argumentType->isCallable()->no()) { // @PHPStan-Ignore phpstanApi.instanceofType (Traverse signatures only; nominal invocable objects can return themselves.) $scope = new OutOfClassScope(); foreach ($parameterType->getCallableParametersAcceptors($scope) as $parameterAcceptor) { foreach ($argumentType->getCallableParametersAcceptors($scope) as $argumentAcceptor) {
$scope = new OutOfClassScope();
foreach ($parameterType->getCallableParametersAcceptors($scope) as $parameterAcceptor) {
foreach ($argumentType->getCallableParametersAcceptors($scope) as $argumentAcceptor) {
// Returns flow into the callable; its parameters are contravariant.
$constraints = $this->observeLowerBound(
$constraints,
$parameterAcceptor->getReturnType(),
$argumentAcceptor->getReturnType(),
);
}
}

return $constraints;
}

$parameterReflections = $parameterType->getObjectClassReflections();
if (count($parameterReflections) === 1) {
$parameterReflection = $parameterReflections[0];
Expand Down
2 changes: 1 addition & 1 deletion src/Turbo/TurboExtensionEnabler.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
final class TurboExtensionEnabler
{

public const EXPECTED_EXTENSION_VERSION = '28c29b8';
public const EXPECTED_EXTENSION_VERSION = 'f545859';

private static bool $active = false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PHPStan\Testing\TypeInferenceTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use function array_merge;
use const PHP_VERSION_ID;

/**
* With unresolved template arguments on but closure signature inference off,
Expand All @@ -19,6 +20,10 @@ public static function dataFileAsserts(): iterable
{
yield from self::gatherAssertTypes(__DIR__ . '/data/closure-signature-from-usages-off.php');
yield from self::gatherAssertTypes(__DIR__ . '/data/closure-byref-uses-at-invocation-off.php');
if (PHP_VERSION_ID < 80000) {
return;
}
yield from self::gatherAssertTypes(__DIR__ . '/data/callable-return-template-inference.php');
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public static function dataConstraintsSurviveControlFlow(): iterable
if (PHP_VERSION_ID < 80000) {
return;
}
yield from self::gatherAssertTypes(__DIR__ . '/data/callable-return-template-inference.php');
yield from self::gatherAssertTypes(__DIR__ . '/../../Rules/Functions/data/joint-inference-named.php');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Variable;
use PHPStan\Reflection\ExtendedFunctionVariant;
use PHPStan\Reflection\Native\NativeParameterReflection;
use PHPStan\Reflection\PassedByReference;
use PHPStan\Reflection\ResolvedFunctionVariantWithOriginal;
use PHPStan\Testing\PHPStanTestCase;
use PHPStan\Type\ArrayType;
use PHPStan\Type\CallableType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\FloatType;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeFactory;
Expand Down Expand Up @@ -125,10 +128,12 @@ public function testLowerBoundsUnionWithTheInitialUnlessASendWins(): void
$constraints = $constraints->merge((new TemplateArgumentObserver())->collectArgument($marker, new ConstantIntegerType(2)));
$constraints = $constraints->merge((new TemplateArgumentObserver())->collectArgument($marker, new ConstantStringType('a')));
$constraints = $constraints->merge((new TemplateArgumentObserver())->collectArgument(new ArrayType(new IntegerType(), $marker), new ArrayType(new IntegerType(), new NullType())));
$constraints = $constraints->merge((new TemplateArgumentObserver())->collectArgument(new CallableType([], $marker, false), new CallableType([], new StringType(), false)));
$parameter = new NativeParameterReflection('value', false, $marker, PassedByReference::createNo(), false, null);
$argument = new NativeParameterReflection('value', false, new FloatType(), PassedByReference::createNo(), false, null);
$constraints = $constraints->merge((new TemplateArgumentObserver())->collectArgument(new CallableType([$parameter], $marker, false), new CallableType([$argument], new StringType(), false)));
$frame = (new TemplateArgumentResolver())->resolve($constraints, null, []);

$this->assertSame("1|2|'a'|null", self::describe($frame->resolve($site, 'T')), 'callable parameters are contravariant and contribute nothing');
$this->assertSame('1|2|string|null', self::describe($frame->resolve($site, 'T')), 'callable returns contribute lower bounds, but contravariant parameters do not');

[$constraints, $site, $ofMarker] = self::constraintsWithA(new ConstantIntegerType(1));
$constraints = $constraints->merge((new TemplateArgumentObserver())->collectArgument($ofMarker->getTypes()[0], new ConstantStringType('a')));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php // lint >= 8.0
declare(strict_types = 1);

namespace CallableReturnTemplateInference;

use function PHPStan\Testing\assertType;

/**
* @template K
* @template V
*/
class Pair
{

/**
* @param K $key
* @param V $value
*/
public function __construct(public mixed $key, public mixed $value)
{
}

}

/**
* @template K
* @template V
*/
class Map
{
}

/**
* @template K
* @template V
* @template KReturn
* @template VReturn
* @param iterable<K, V> $items
* @param callable(K, V): Pair<KReturn, VReturn> $mapper
* @return Map<KReturn, VReturn>
*/
function mapItems(iterable $items, callable $mapper): Map
{
return new Map();
}

/** @param array<string, int> $items */
function callbacks(array $items): void
{
assertType('CallableReturnTemplateInference\Map<string, int>', mapItems(
$items,
static fn (string $key, int $value) => new Pair($key, $value),
));

assertType('CallableReturnTemplateInference\Map<string, int>', mapItems(
$items,
static function (string $key, int $value): Pair {
return new Pair($key, $value);
},
));

assertType('CallableReturnTemplateInference\Map<string, CallableReturnTemplateInference\Pair<int, int>>', mapItems(
$items,
static fn (string $key, int $value) => new Pair($key, new Pair($value, $value)),
));
}

/** @template T */
class Invoker
{

/** @param T $value */
public function __construct(public mixed $value)
{
}

/** @return self<T> */
public function __invoke(): self
{
return $this;
}

}

/**
* @template T
* @param Invoker<T> $callback
* @return Invoker<T>
*/
function passInvoker(Invoker $callback): Invoker
{
return $callback;
}

function recursiveCallable(int $value): void
{
assertType('CallableReturnTemplateInference\Invoker<int>', passInvoker(new Invoker($value)));
}
28 changes: 25 additions & 3 deletions turbo-ext/src/TemplateArgumentObserver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -957,9 +957,6 @@ class TemplateArgumentObserver
bool parameterTemplate;
if (UNEXPECTED(!isTemplateType(parameterType, parameterTemplate))) return zv::Val();
if (parameterTemplate) return constraints;
zend_long callable = pt_type_op_trinary(Z_OBJ_P(parameterType), PT_OP_IS_CALLABLE, 0, NULL);
if (UNEXPECTED(callable < 0)) return zv::Val();
if (callable == PT_TRI_YES) return constraints;

if (isInstance(parameterType, pt_ce_union_type)) {
zv::Val argumentMembers;
Expand Down Expand Up @@ -1017,6 +1014,31 @@ class TemplateArgumentObserver
return constraints;
}

if (isInstance(parameterType, pt_ce_callable_type) || isInstance(parameterType, pt_ce_closure_type)) {
zend_long argumentCallable = pt_type_op_trinary(Z_OBJ_P(argumentType), PT_OP_IS_CALLABLE, 0, NULL);
if (UNEXPECTED(argumentCallable < 0)) return zv::Val();
if (argumentCallable == PT_TRI_YES) {
zv::Val scope = pt_type_new(PT_CLASS_OUT_OF_CLASS_SCOPE, 0, NULL);
if (UNEXPECTED(scope.isUndef())) return zv::Val();
zv::Val parameterAcceptors = pt_type_call(Z_OBJ_P(parameterType), PT_LC("getcallableparametersacceptors"), 1, scope.raw());
if (UNEXPECTED(parameterAcceptors.isUndef())) return zv::Val();
for (auto parameterEntry : zv::ArrRef(parameterAcceptors.raw())) {
zv::Val argumentAcceptors = pt_type_call(Z_OBJ_P(argumentType), PT_LC("getcallableparametersacceptors"), 1, scope.raw());
if (UNEXPECTED(argumentAcceptors.isUndef())) return zv::Val();
for (auto argumentEntry : zv::ArrRef(argumentAcceptors.raw())) {
// Returns flow into the callable; its parameters are contravariant.
zv::Val parameterReturn = pt_parameters_acceptor_call(parameterEntry.value().deref().raw(), PT_PA_GET_RETURN_TYPE);
if (UNEXPECTED(parameterReturn.isUndef())) return zv::Val();
zv::Val argumentReturn = pt_parameters_acceptor_call(argumentEntry.value().deref().raw(), PT_PA_GET_RETURN_TYPE);
if (UNEXPECTED(argumentReturn.isUndef())) return zv::Val();
constraints = observeLowerBound(std::move(constraints), parameterReturn.raw(), argumentReturn.raw());
if (UNEXPECTED(constraints.isUndef())) return zv::Val();
}
}
return constraints;
}
}

zv::Val parameterReflections = objectClassReflections(parameterType);
if (UNEXPECTED(parameterReflections.isUndef())) return zv::Val();
uint32_t parameterReflectionCount = countOf(parameterReflections.raw());
Expand Down
44 changes: 44 additions & 0 deletions turbo-ext/tests/type-family.php
Original file line number Diff line number Diff line change
Expand Up @@ -8172,6 +8172,50 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu
$r["observer pure argument $declaredName <- $actualName"] = $tacCatching(static fn () => $viewConstraints($observer->collectArgument($declared, $actual, true)));
}
}
// Callable returns contribute bounds, but contravariant parameters do not.
$callableParameterMarker = new \PHPStan\Reflection\Php\DummyParameter(
'value', $markers['m1U'], false, null, false, null,
);
$callableArgumentParameter = new \PHPStan\Reflection\Php\DummyParameter(
'value', $int, false, null, false, null,
);
$callableReturns = [
'bare return marker' => [
new \PHPStan\Type\CallableType([], $markers['m1T']),
new \PHPStan\Type\ClosureType([], $string),
],
'nested generic return marker' => [
new \PHPStan\Type\ClosureType([], $ao($int, $markers['m1T'])),
new \PHPStan\Type\CallableType([], $ao($int, $string)),
],
'contravariant parameter marker' => [
new \PHPStan\Type\CallableType([$callableParameterMarker], new \PHPStan\Type\VoidType()),
new \PHPStan\Type\ClosureType([$callableArgumentParameter], new \PHPStan\Type\VoidType()),
],
'return and contravariant parameter markers' => [
new \PHPStan\Type\CallableType([$callableParameterMarker], $markers['m1T']),
new \PHPStan\Type\ClosureType([$callableArgumentParameter], $string),
],
'union of callable arguments' => [
new \PHPStan\Type\CallableType([], $markers['m1T']),
new \PHPStan\Type\UnionType([
new \PHPStan\Type\ClosureType([], $string),
new \PHPStan\Type\CallableType([], $int),
]),
],
'union of callable parameters' => [
new \PHPStan\Type\UnionType([
new \PHPStan\Type\CallableType([], $markers['m1T']),
new \PHPStan\Type\NullType(),
]),
new \PHPStan\Type\ClosureType([], $string),
],
];
foreach ($callableReturns as $name => [$parameterType, $argumentType]) {
$r["observer callable argument $name"] = $tacCatching(
static fn () => $viewConstraints($observer->collectArgument($parameterType, $argumentType)),
);
}
$templateMap = new \PHPStan\Type\Generic\TemplateTypeMap(['T' => $tT, 'U' => $tU]);
$parameters = [
new \PHPStan\Reflection\Php\DummyParameter('a', $ao($int, $tT), false, null, false, null),
Expand Down
Loading