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
241 changes: 231 additions & 10 deletions src/Analyser/ArgumentsHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\Php\DummyParameter;
use PHPStan\Reflection\Php\ExtendedDummyParameter;
use PHPStan\Reflection\ResolvedFunctionVariant;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
Expand All @@ -44,13 +46,16 @@
use PHPStan\Type\FunctionParameterClosureThisExtension;
use PHPStan\Type\FunctionParameterClosureTypeExtension;
use PHPStan\Type\FunctionParameterOutTypeExtension;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeHelper;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
use PHPStan\Type\MethodParameterClosureThisExtension;
use PHPStan\Type\MethodParameterClosureTypeExtension;
use PHPStan\Type\MethodParameterOutTypeExtension;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\ResourceType;
use PHPStan\Type\StaticMethodParameterClosureThisExtension;
Expand All @@ -59,7 +64,9 @@
use PHPStan\Type\ThisType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\TypeUtils;
use PHPStan\Type\UnionType;
use function array_keys;
use function array_last;
use function array_map;
Expand Down Expand Up @@ -234,7 +241,7 @@
// resolution (see gatherClosureArgType()).
$originalArgForGather = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg;
$gatheredArgTypeByIndex[$i] = $typeDrivenAcceptorSelection
? $this->gatherClosureArgType($parametersAcceptors, $i, $arg->value, $scope)
? $this->gatherClosureArgType($nodeScopeResolver, $args, $parametersAcceptors, $namedArgumentsVariants, $gatheredArgTypeByIndex, $i, $arg->value, $scope)
: $this->closureTypeResolver->getClosureType($scope, $arg->value, true, $storage);
$this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArgForGather, $i, $gatheredArgTypeByIndex[$i]);
}
Expand Down Expand Up @@ -951,15 +958,35 @@
/**
* Resolves the type of a closure/arrow function argument for the generic
* gather, mirroring ParametersAcceptorSelector::selectFromArgs(): the closure
* type is read with the RAW (un-generic-resolved) acceptor parameter pushed
* onto the in-function-call stack, so its body sees the template parameter
* (effectively mixed for an untyped param) rather than a parameter already
* resolved from sibling args. That keeps the inferred return type (the U in
* callable(T): U) faithful to the closure's own declaration.
* type is read with the acceptor parameter pushed onto the in-function-call
* stack before its templates are resolved, so the inferred return type (the
* U in callable(T): U) stays faithful to the closure's own declaration.
*
* The templates the closure only receives (the T in callable(T): U) are
* resolved from the other arguments gathered so far before the push: left
* as T, an offset or a property read off the closure's parameter is mixed,
* and U would be inferred from that mixed (see resolveReceivedTemplateTypes()).
*
* A template of the callee that the raw parameter leaks into the closure
* type (the T in callable(T): T, read back from a closure returning its
* parameter) would infer T from T itself, so it is resolved from the other
* arguments gathered so far instead (see resolveCalleeTemplateTypes()).
*
* @param Node\Arg[] $args
* @param ParametersAcceptor[] $parametersAcceptors
* @param ParametersAcceptor[]|null $namedArgumentsVariants
* @param array<int, Type> $gatheredArgTypeByIndex
*/
private function gatherClosureArgType(array $parametersAcceptors, int $i, Expr $closureExpr, MutatingScope $scope): Type
private function gatherClosureArgType(
NodeScopeResolver $nodeScopeResolver,
array $args,
array $parametersAcceptors,
?array $namedArgumentsVariants,
array $gatheredArgTypeByIndex,
int $i,
Expr $closureExpr,
MutatingScope $scope,
): Type
{
$rawParameter = null;
if (count($parametersAcceptors) === 1) {
Expand All @@ -971,11 +998,205 @@
}
}

if ($rawParameter !== null) {
$scope = $scope->pushInFunctionCall(null, $rawParameter, false);
if ($rawParameter === null) {
return $this->closureParameterResolver->resolveCallableTypeForScope($closureExpr, $scope);
}

$siblingTemplateTypeMap = null;
$pushedParameter = $rawParameter;
$receivedTemplateNames = self::getReceivedTemplateNames($rawParameter->getType(), $scope);
if ($receivedTemplateNames !== []) {
$siblingTemplateTypeMap = $this->selectSiblingTemplateTypeMap($nodeScopeResolver, $args, $parametersAcceptors, $namedArgumentsVariants, $gatheredArgTypeByIndex, $i, $scope);
$pushedParameter = self::resolveReceivedTemplateTypes($rawParameter, $receivedTemplateNames, $siblingTemplateTypeMap);
}

$closureType = $this->closureParameterResolver->resolveCallableTypeForScope(
$closureExpr,
$scope->pushInFunctionCall(null, $pushedParameter, false),
);

$calleeTemplateTypeMap = $parametersAcceptors[0]->getTemplateTypeMap();
if ($calleeTemplateTypeMap->isEmpty() || !self::referencesCalleeTemplateTypes($closureType, $calleeTemplateTypeMap)) {
return $closureType;
}

return self::resolveCalleeTemplateTypes(
$closureType,
$calleeTemplateTypeMap,
$siblingTemplateTypeMap ?? $this->selectSiblingTemplateTypeMap($nodeScopeResolver, $args, $parametersAcceptors, $namedArgumentsVariants, $gatheredArgTypeByIndex, $i, $scope),
);
}

/**
* The template types resolved from every argument gathered so far except
* the i-th one, which counts as mixed.
*
* @param Node\Arg[] $args
* @param ParametersAcceptor[] $parametersAcceptors
* @param ParametersAcceptor[]|null $namedArgumentsVariants
* @param array<int, Type> $gatheredArgTypeByIndex
*/
private function selectSiblingTemplateTypeMap(
NodeScopeResolver $nodeScopeResolver,
array $args,
array $parametersAcceptors,
?array $namedArgumentsVariants,
array $gatheredArgTypeByIndex,
int $i,
MutatingScope $scope,
): TemplateTypeMap
{
$siblingTypes = [];
$siblingUnpack = false;
$siblingHasName = false;
foreach ($args as $j => $siblingArg) {
$siblingOriginalArg = $siblingArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $siblingArg;
$this->addGatheredArgType($siblingTypes, $siblingUnpack, $siblingHasName, $siblingOriginalArg, $j, $j !== $i ? ($gatheredArgTypeByIndex[$j] ?? new MixedType()) : new MixedType());
}

return $this->selectArgsMetadataAcceptor($nodeScopeResolver, $args, $siblingTypes, $parametersAcceptors, $namedArgumentsVariants, $siblingHasName, $siblingUnpack, $scope)->getResolvedTemplateTypeMap();
}

/**
* The names of the template types a callable parameter type passes to the
* callable without expecting them back: those its parameters reference and
* its return type does not. A template in the return type is left alone, as
* the closure's own return takes part in its inference.
*
* @return list<string>
*/
private static function getReceivedTemplateNames(Type $parameterType, MutatingScope $scope): array
{
if (!$parameterType->hasTemplateOrLateResolvableType() || $parameterType->isCallable()->no()) {
return [];
}

$callableType = $parameterType;
if ($callableType instanceof UnionType) {
$callableType = $callableType->filterTypes(static fn (Type $innerType): bool => $innerType->isCallable()->yes());

Check warning on line 1076 in src/Analyser/ArgumentsHandler.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $callableType = $parameterType; if ($callableType instanceof UnionType) { - $callableType = $callableType->filterTypes(static fn (Type $innerType): bool => $innerType->isCallable()->yes()); + $callableType = $callableType->filterTypes(static fn (Type $innerType): bool => !$innerType->isCallable()->no()); } if (!$callableType->isCallable()->yes()) { return [];
}
if (!$callableType->isCallable()->yes()) {

Check warning on line 1078 in src/Analyser/ArgumentsHandler.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($callableType instanceof UnionType) { $callableType = $callableType->filterTypes(static fn (Type $innerType): bool => $innerType->isCallable()->yes()); } - if (!$callableType->isCallable()->yes()) { + if ($callableType->isCallable()->no()) { return []; }
return [];
}

$returnedTemplateNames = [];
foreach ($callableType->getCallableParametersAcceptors($scope) as $acceptor) {
foreach ($acceptor->getReturnType()->getReferencedTemplateTypes(TemplateTypeVariance::createCovariant()) as $reference) {
$returnedTemplateNames[$reference->getType()->getName()] = true;
}
}

return $this->closureParameterResolver->resolveCallableTypeForScope($closureExpr, $scope);
$receivedTemplateNames = [];
foreach ($callableType->getReferencedTemplateTypes(TemplateTypeVariance::createInvariant()) as $reference) {
$name = $reference->getType()->getName();
if (isset($returnedTemplateNames[$name])) {
continue;
}
$receivedTemplateNames[$name] = true;
}

return array_keys($receivedTemplateNames);
}

/**
* The parameter with the received template types replaced by what the other
* arguments resolve them to - left alone when they say nothing about them,
* or pass no value of them (never, from an empty array).
*
* @param list<string> $receivedTemplateNames
*/
private static function resolveReceivedTemplateTypes(ParameterReflection $parameter, array $receivedTemplateNames, TemplateTypeMap $siblingTemplateTypeMap): ParameterReflection
{
$receivedTypes = [];
foreach ($receivedTemplateNames as $name) {
$resolvedType = $siblingTemplateTypeMap->getType($name);
if ($resolvedType === null || $resolvedType instanceof ErrorType || $resolvedType instanceof NeverType) {
continue;
}
$receivedTypes[$name] = $resolvedType;
}
if ($receivedTypes === []) {
return $parameter;
}

$type = TemplateTypeHelper::resolveTemplateTypes(
$parameter->getType(),
new TemplateTypeMap($receivedTypes),
TemplateTypeVarianceMap::createEmpty(),
TemplateTypeVariance::createInvariant(),
);

if (!$parameter instanceof ExtendedParameterReflection) {
return new DummyParameter(
$parameter->getName(),
$type,
$parameter->isOptional(),
$parameter->passedByReference(),
$parameter->isVariadic(),
$parameter->getDefaultValue(),
);
}

return new ExtendedDummyParameter(
$parameter->getName(),
$type,
$parameter->isOptional(),
$parameter->passedByReference(),
$parameter->isVariadic(),
$parameter->getDefaultValue(),
$parameter->getNativeType(),
$type,
$parameter->getOutType(),
$parameter->isImmediatelyInvokedCallable(),
$parameter->getClosureThisType(),
$parameter->getAttributes(),
$parameter->getAllowedConstants(),
$parameter->isPureUnlessCallableIsImpureParameter(),
$parameter->isPureUnlessParameterPassedParameter(),
);
}

private static function referencesCalleeTemplateTypes(Type $type, TemplateTypeMap $calleeTemplateTypeMap): bool
{
if (!$type->hasTemplateOrLateResolvableType()) {
return false;
}

foreach ($type->getReferencedTemplateTypes(TemplateTypeVariance::createInvariant()) as $reference) {
if (self::isCalleeTemplateType($reference->getType(), $calleeTemplateTypeMap)) {
return true;
}
}

return false;
}

/**
* Replaces the callee's own template types with what the other arguments
* resolve them to - their default or bound when they say nothing about them,
* or pass no value of them at all (never, from an empty array).
*/
private static function resolveCalleeTemplateTypes(Type $type, TemplateTypeMap $calleeTemplateTypeMap, TemplateTypeMap $resolvedTemplateTypeMap): Type
{
return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($calleeTemplateTypeMap, $resolvedTemplateTypeMap): Type {
if (!$type instanceof TemplateType || !self::isCalleeTemplateType($type, $calleeTemplateTypeMap)) {
return $traverse($type);
}

$resolvedType = $resolvedTemplateTypeMap->getType($type->getName());
if ($resolvedType === null || $resolvedType instanceof ErrorType || $resolvedType instanceof NeverType) {
return $traverse($type->getDefault() ?? $type->getBound());
}

return $resolvedType;
});
}

private static function isCalleeTemplateType(TemplateType $type, TemplateTypeMap $calleeTemplateTypeMap): bool
{
$calleeTemplateType = $calleeTemplateTypeMap->getType($type->getName());

return $calleeTemplateType instanceof TemplateType && $calleeTemplateType->getScope()->equals($type->getScope());
}

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-11861.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,5 @@ function filterArrayNotNull(array $source): array
filterArrayNotNull(...)
);

assertType('array<int<0, max>, array<int|string, mixed>>', $mappedA);
assertType('array<int<0, max>, array<string, int>>', $mappedA);
assertType('array<int<0, max>, array<int|string, mixed>>', $mappedAWithFirstClassSyntax);
Loading
Loading