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
106 changes: 67 additions & 39 deletions src/Reflection/ResolvedFunctionVariantWithOriginal.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeHelper;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\TemplateTypeReference;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
use PHPStan\Type\NarrowedSubjectType;
Expand Down Expand Up @@ -230,19 +231,19 @@ private function resolveResolvableTemplateTypes(Type $type, TemplateTypeVariance
{
$references = $type->getReferencedTemplateTypes($positionVariance);

$objectCb = function (Type $type, callable $traverse) use ($references): Type {
if (
$type instanceof TemplateType
&& !$type instanceof NarrowedSubjectType
&& !$type->isArgument()
&& $type->getScope()->getFunctionName() !== null
) {
$objectCb = fn (Type $type, callable $traverse): Type => $this->resolveTemplateTypeInGenericType($type, $traverse, $references, null);

return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($references, $objectCb): Type {
if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) {
return TypeTraverser::map($type, $objectCb);
}

if ($type instanceof TemplateType && !$type instanceof NarrowedSubjectType && !$type->isArgument()) {
$newType = $this->resolvedTemplateTypeMap->getType($type->getName());
if ($newType === null || $newType instanceof ErrorType) {
return $traverse($type);
}

$newType = TemplateTypeHelper::generalizeInferredTemplateType($type, $newType);
$variance = TemplateTypeVariance::createInvariant();
foreach ($references as $reference) {
// this uses identity to distinguish between different occurrences of the same template type
Expand Down Expand Up @@ -270,47 +271,74 @@ private function resolveResolvableTemplateTypes(Type $type, TemplateTypeVariance
}

return $traverse($type);
};
});
}

return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($references, $objectCb): Type {
if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) {
return TypeTraverser::map($type, $objectCb);
}
/**
* @param callable(Type): Type $traverse
* @param list<TemplateTypeReference> $references
* @param string|null $keepInferredName a template type whose inferred type is not generalized here
*/
private function resolveTemplateTypeInGenericType(Type $type, callable $traverse, array $references, ?string $keepInferredName): Type
{
if (
$type instanceof NarrowedSubjectType
&& $keepInferredName === null
&& !$type->isArgument()
&& $type->getScope()->getFunctionName() !== null
) {
// generalize what the branch knows about the subject, not the subject: in the
// else branch of `(T is \UnitEnum ? array-key : T)` with T of array-key|\UnitEnum,
// T is a scalar and an inferred 'foo' stays 'foo'. Only the subject is taken as
// inferred - a template type in the target is generalized as the condition sees it.
$subjectName = $type->getName();
$resolved = TypeTraverser::map($type, fn (Type $type, callable $traverse): Type => $this->resolveTemplateTypeInGenericType($type, $traverse, $references, $subjectName));

return TemplateTypeHelper::generalizeInferredTemplateType($type, $resolved);
}

if ($type instanceof TemplateType && !$type instanceof NarrowedSubjectType && !$type->isArgument()) {
$newType = $this->resolvedTemplateTypeMap->getType($type->getName());
if ($newType === null || $newType instanceof ErrorType) {
return $traverse($type);
}
if (
$type instanceof TemplateType
&& !$type instanceof NarrowedSubjectType
&& !$type->isArgument()
&& $type->getScope()->getFunctionName() !== null
) {
$newType = $this->resolvedTemplateTypeMap->getType($type->getName());
if ($newType === null || $newType instanceof ErrorType) {
return $traverse($type);
}

$variance = TemplateTypeVariance::createInvariant();
foreach ($references as $reference) {
// this uses identity to distinguish between different occurrences of the same template type
// see https://github.com/phpstan/phpstan-src/pull/2485#discussion_r1328555397 for details
if ($reference->getType() === $type) {
$variance = $reference->getPositionVariance();
break;
}
}
if ($type->getName() !== $keepInferredName) {
$newType = TemplateTypeHelper::generalizeInferredTemplateType($type, $newType);
}

$callSiteVariance = $this->callSiteVarianceMap->getVariance($type->getName());
if ($callSiteVariance === null || $callSiteVariance->invariant()) {
return $newType;
$variance = TemplateTypeVariance::createInvariant();
foreach ($references as $reference) {
// this uses identity to distinguish between different occurrences of the same template type
// see https://github.com/phpstan/phpstan-src/pull/2485#discussion_r1328555397 for details
if ($reference->getType() === $type) {
$variance = $reference->getPositionVariance();
break;
}
}

if (!$callSiteVariance->covariant() && $variance->covariant()) {
return $traverse($type->getBound());
}
$callSiteVariance = $this->callSiteVarianceMap->getVariance($type->getName());
if ($callSiteVariance === null || $callSiteVariance->invariant()) {
return $newType;
}

if (!$callSiteVariance->contravariant() && $variance->contravariant()) {
return new NonAcceptingNeverType();
}
if (!$callSiteVariance->covariant() && $variance->covariant()) {
return $traverse($type->getBound());
}

return $newType;
if (!$callSiteVariance->contravariant() && $variance->contravariant()) {
return new NonAcceptingNeverType();
}

return $traverse($type);
});
return $newType;
}

return $traverse($type);
}

/**
Expand Down
90 changes: 90 additions & 0 deletions tests/PHPStan/Analyser/nsrt/narrowed-subject-inferred-literal.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace NarrowedSubjectInferredLiteral;

use function PHPStan\Testing\assertType;

class User
{

}

enum Digit: int
{

case One = 1;

}

/**
* @template TKey of array-key
* @template TValue
*/
class Collection
{

/**
* @template TNewKey of array-key|\UnitEnum
*
* @param (callable(TValue, TKey): TNewKey)|array<mixed>|string $keyBy
* @return static<($keyBy is (array|string) ? array-key : (TNewKey is \UnitEnum ? array-key : TNewKey)), TValue>
*/
public function keyBy($keyBy)
{
throw new \Exception();
}

/**
* @template TNewKey of array-key|\UnitEnum
*
* @param callable(TValue, TKey): TNewKey $keyBy
* @return static<(TNewKey is \UnitEnum ? array-key : TNewKey), TValue>
*/
public function keyByCallback($keyBy)
{
throw new \Exception();
}

}

/**
* @template T of array-key
* @template U of array-key
* @param T $a
* @param U $b
* @return Collection<(T is U ? T : int), int>
*/
function templateTarget($a, $b)
{
throw new \Exception();
}

/**
* @template T of array-key
* @template U of array-key
* @param T $a
* @param U $b
* @return Collection<(T is not U ? int : T), int>
*/
function negatedTemplateTarget($a, $b)
{
throw new \Exception();
}

/** @param Collection<int, User> $collection */
function test(Collection $collection): void
{
assertType("NarrowedSubjectInferredLiteral\\Collection<'foo', NarrowedSubjectInferredLiteral\\User>", $collection->keyBy(fn ($user) => 'foo'));
assertType('NarrowedSubjectInferredLiteral\\Collection<0, NarrowedSubjectInferredLiteral\\User>', $collection->keyBy(static fn ($user): int => 0));
assertType('NarrowedSubjectInferredLiteral\\Collection<(int|string), NarrowedSubjectInferredLiteral\\User>', $collection->keyBy('name'));
assertType('NarrowedSubjectInferredLiteral\\Collection<(int|string), NarrowedSubjectInferredLiteral\\User>', $collection->keyBy(static fn ($user) => Digit::One));

assertType("NarrowedSubjectInferredLiteral\\Collection<'foo', NarrowedSubjectInferredLiteral\\User>", $collection->keyByCallback(fn ($user) => 'foo'));
assertType('NarrowedSubjectInferredLiteral\\Collection<(int|string), NarrowedSubjectInferredLiteral\\User>', $collection->keyByCallback(static fn ($user) => Digit::One));

// the condition compares the generalized T and U, the branch keeps the inferred T
assertType("NarrowedSubjectInferredLiteral\\Collection<'foo', int>", templateTarget('foo', 'bar'));
assertType("NarrowedSubjectInferredLiteral\\Collection<'foo', int>", negatedTemplateTarget('foo', 'bar'));
}
Loading