Skip to content

Commit e35ca44

Browse files
phpstan-botclaude
andcommitted
Narrow TemplateMixedType in TypeCombinator::intersect() for scalar types
Revert IntersectionType changes and instead narrow TemplateMixedType to a concrete template type (e.g. TemplateIntegerType) when intersected with a scalar type in TypeCombinator::intersect(). For `int & T` where T is TemplateMixedType, the result is now `T of int` (TemplateIntegerType) instead of an IntersectionType. Since TemplateIntegerType delegates hasMethod() to IntegerType (which returns No for __toString), the false positive purity error is avoided. The narrowing is restricted to scalar types only. Narrowing object or array types would cause information loss during template resolution (e.g. `T&MockObject` becoming just the resolved type without MockObject). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 992a666 commit e35ca44

5 files changed

Lines changed: 53 additions & 26 deletions

File tree

src/Type/IntersectionType.php

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ public function canAccessProperties(): TrinaryLogic
552552

553553
public function hasProperty(string $propertyName): TrinaryLogic
554554
{
555-
return $this->intersectResultsWithNonObjectGuard(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName));
555+
return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName));
556556
}
557557

558558
public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
@@ -585,7 +585,7 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember
585585

586586
public function hasInstanceProperty(string $propertyName): TrinaryLogic
587587
{
588-
return $this->intersectResultsWithNonObjectGuard(static fn (Type $type): TrinaryLogic => $type->hasInstanceProperty($propertyName));
588+
return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasInstanceProperty($propertyName));
589589
}
590590

591591
public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
@@ -618,7 +618,7 @@ public function getUnresolvedInstancePropertyPrototype(string $propertyName, Cla
618618

619619
public function hasStaticProperty(string $propertyName): TrinaryLogic
620620
{
621-
return $this->intersectResultsWithNonObjectGuard(static fn (Type $type): TrinaryLogic => $type->hasStaticProperty($propertyName));
621+
return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasStaticProperty($propertyName));
622622
}
623623

624624
public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
@@ -656,7 +656,7 @@ public function canCallMethods(): TrinaryLogic
656656

657657
public function hasMethod(string $methodName): TrinaryLogic
658658
{
659-
return $this->intersectResultsWithNonObjectGuard(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName));
659+
return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName));
660660
}
661661

662662
public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection
@@ -694,7 +694,7 @@ public function canAccessConstants(): TrinaryLogic
694694

695695
public function hasConstant(string $constantName): TrinaryLogic
696696
{
697-
return $this->intersectResultsWithNonObjectGuard(static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName));
697+
return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName));
698698
}
699699

700700
public function getConstant(string $constantName): ClassConstantReflection
@@ -1483,23 +1483,6 @@ private function intersectResults(
14831483
return TrinaryLogic::lazyMaxMin($types, $getResult);
14841484
}
14851485

1486-
/**
1487-
* @param callable(Type $type): TrinaryLogic $getResult
1488-
*/
1489-
private function intersectResultsWithNonObjectGuard(callable $getResult): TrinaryLogic
1490-
{
1491-
$results = [];
1492-
foreach ($this->types as $type) {
1493-
$result = $getResult($type);
1494-
if ($type->isObject()->no() && $result->no()) {
1495-
return TrinaryLogic::createNo();
1496-
}
1497-
$results[] = $result;
1498-
}
1499-
1500-
return TrinaryLogic::maxMin(...$results);
1501-
}
1502-
15031486
/**
15041487
* @param callable(Type $type): Type $getType
15051488
*/

src/Type/TypeCombinator.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,6 +1399,50 @@ public static function intersect(Type ...$types): Type
13991399
continue;
14001400
}
14011401

1402+
if (
1403+
$types[$j] instanceof TemplateMixedType
1404+
&& !$types[$i] instanceof TemplateType
1405+
&& $types[$i]->isScalar()->yes()
1406+
&& $types[$j]->getBound()->isSuperTypeOf($types[$i])->yes()
1407+
) {
1408+
$narrowed = TemplateTypeFactory::create(
1409+
$types[$j]->getScope(),
1410+
$types[$j]->getName(),
1411+
$types[$i],
1412+
$types[$j]->getVariance(),
1413+
$types[$j]->getStrategy(),
1414+
$types[$j]->getDefault(),
1415+
);
1416+
if (!$narrowed instanceof TemplateMixedType) {
1417+
$types[$j] = $narrowed;
1418+
array_splice($types, $i--, 1);
1419+
$typesCount--;
1420+
continue 2;
1421+
}
1422+
}
1423+
1424+
if (
1425+
$types[$i] instanceof TemplateMixedType
1426+
&& !$types[$j] instanceof TemplateType
1427+
&& $types[$j]->isScalar()->yes()
1428+
&& $types[$i]->getBound()->isSuperTypeOf($types[$j])->yes()
1429+
) {
1430+
$narrowed = TemplateTypeFactory::create(
1431+
$types[$i]->getScope(),
1432+
$types[$i]->getName(),
1433+
$types[$j],
1434+
$types[$i]->getVariance(),
1435+
$types[$i]->getStrategy(),
1436+
$types[$i]->getDefault(),
1437+
);
1438+
if (!$narrowed instanceof TemplateMixedType) {
1439+
$types[$i] = $narrowed;
1440+
array_splice($types, $j--, 1);
1441+
$typesCount--;
1442+
continue;
1443+
}
1444+
}
1445+
14021446
if ($types[$i] instanceof IterableType) {
14031447
$isSuperTypeB = $types[$i]->isSuperTypeOfMixed($types[$j]);
14041448
} else {

tests/PHPStan/Analyser/nsrt/bug-4117.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public function broken(int $key)
3434
if ($item) {
3535
assertType("T of mixed~(0|0.0|''|'0'|array{}|false|null) (class Bug4117Types\GenericList, argument)", $item);
3636
} else {
37-
assertType("(T of mixed~null (class Bug4117Types\GenericList, argument)&false)|(0.0&T of mixed~null (class Bug4117Types\GenericList, argument))|(0&T of mixed~null (class Bug4117Types\GenericList, argument))|(list{}&T of mixed~null (class Bug4117Types\GenericList, argument))|(''&T of mixed~null (class Bug4117Types\GenericList, argument))|('0'&T of mixed~null (class Bug4117Types\GenericList, argument))|null", $item);
37+
assertType("T of 0 (class Bug4117Types\GenericList, argument)|T of '' (class Bug4117Types\GenericList, argument)|T of '0' (class Bug4117Types\GenericList, argument)|(T of mixed~null (class Bug4117Types\GenericList, argument)&false)|(0.0&T of mixed~null (class Bug4117Types\GenericList, argument))|(list{}&T of mixed~null (class Bug4117Types\GenericList, argument))|null", $item);
3838
}
3939

4040
assertType('T of mixed~null (class Bug4117Types\GenericList, argument)|null', $item);

tests/PHPStan/Analyser/nsrt/bug-4498.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public function fcn(iterable $iterable): iterable
3838
public function bar(iterable $iterable): iterable
3939
{
4040
if (is_array($iterable)) {
41-
assertType('array<((int&TKey (method Bug4498\Foo::bar(), argument))|(string&TKey (method Bug4498\Foo::bar(), argument))), TValue (method Bug4498\Foo::bar(), argument)>', $iterable);
41+
assertType('array<(TKey of int (method Bug4498\Foo::bar(), argument)|TKey of string (method Bug4498\Foo::bar(), argument)), TValue (method Bug4498\Foo::bar(), argument)>', $iterable);
4242
return $iterable;
4343
}
4444

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -725,11 +725,11 @@ public function testArrayUdiffCallback(): void
725725
14,
726726
],
727727
[
728-
'Parameter #1 $arr1 of function array_udiff expects array<(int&TK)|(string&TK), string>, null given.',
728+
'Parameter #1 $arr1 of function array_udiff expects array<(TK of int)|TK of string, string>, null given.',
729729
20,
730730
],
731731
[
732-
'Parameter #2 $arr2 of function array_udiff expects array<(int&TK)|(string&TK), string>, null given.',
732+
'Parameter #2 $arr2 of function array_udiff expects array<(TK of int)|TK of string, string>, null given.',
733733
21,
734734
],
735735
[

0 commit comments

Comments
 (0)