Skip to content

Commit 31470ab

Browse files
ondrejmirtesclaude
andcommitted
Do not let placeholder constants of intersection members override declared constants
`IntersectionType::getConstant()` returned the constant of the first member answering `hasConstant()` with yes. `T of mixed` does so for every name with a `DummyClassConstantReflection` declared on `stdClass`, so on `T&Foo` the result depended on the order of the members. Placeholder constants are now used only when no member declares the constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBzALCg92yf3LXFxxNBzoY
1 parent a068a23 commit 31470ab

2 files changed

Lines changed: 27 additions & 2 deletions

File tree

‎src/Type/IntersectionType.php‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHPStan\Reflection\Callables\CallableParametersAcceptor;
1313
use PHPStan\Reflection\ClassConstantReflection;
1414
use PHPStan\Reflection\ClassMemberAccessAnswerer;
15+
use PHPStan\Reflection\Dummy\DummyClassConstantReflection;
1516
use PHPStan\Reflection\Dummy\DummyMethodReflection;
1617
use PHPStan\Reflection\Dummy\DummyPropertyReflection;
1718
use PHPStan\Reflection\ExtendedMethodReflection;
@@ -804,10 +805,26 @@ public function hasConstant(string $constantName): TrinaryLogic
804805

805806
public function getConstant(string $constantName): ClassConstantReflection
806807
{
808+
$dummyConstant = null;
807809
foreach ($this->types as $type) {
808-
if ($type->hasConstant($constantName)->yes()) {
809-
return $type->getConstant($constantName);
810+
if (!$type->hasConstant($constantName)->yes()) {
811+
continue;
812+
}
813+
814+
$constant = $type->getConstant($constantName);
815+
816+
// a member like T of mixed has every constant only as a placeholder,
817+
// it must not override the constant declared by another member
818+
if ($constant instanceof DummyClassConstantReflection) {
819+
$dummyConstant ??= $constant;
820+
continue;
810821
}
822+
823+
return $constant;
824+
}
825+
826+
if ($dummyConstant !== null) {
827+
return $dummyConstant;
811828
}
812829

813830
throw new MissingConstantFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $constantName);

‎tests/PHPStan/Type/IntersectionTypeTest.php‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Type;
44

55
use DateTimeImmutable;
6+
use DateTimeInterface;
67
use DoctrineIntersectionTypeIsSupertypeOf\Collection;
78
use Iterator;
89
use ObjectTypeEnums\FooEnum;
@@ -1008,4 +1009,11 @@ public function testMethodOfIntersectionWithTemplateMixed(IntersectionType $type
10081009
$this->assertSame('modifier', $variant->getParameters()[0]->getName());
10091010
}
10101011

1012+
#[DataProvider('dataIntersectionWithTemplateMixed')]
1013+
public function testConstantOfIntersectionWithTemplateMixed(IntersectionType $type): void
1014+
{
1015+
$constant = $type->getConstant('ATOM');
1016+
$this->assertSame(DateTimeInterface::class, $constant->getDeclaringClass()->getName());
1017+
}
1018+
10111019
}

0 commit comments

Comments
 (0)