Skip to content

Commit 87fc501

Browse files
authored
Merge pull request #1485 from ondrejmirtes/trait-method-alias-test
Fixed modifiers of trait method when method does not exist in current class
2 parents 8d26665 + c3fa1cb commit 87fc501

File tree

3 files changed

+99
-13
lines changed

3 files changed

+99
-13
lines changed

psalm-baseline.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<files psalm-version="5.26.1@d747f6500b38ac4f7dfc5edbcae6e4b637d7add0">
2+
<files psalm-version="6.4.0@04f312ac6ea48ba1c3e5db4d815bf6d74641c0ee">
33
<file src="src/Reflection/Attribute/ReflectionAttributeHelper.php">
44
<ImpureMethodCall>
55
<code><![CDATA[toLowerString]]></code>
@@ -32,8 +32,8 @@
3232
[$valueParameter],
3333
new Node\NullableType(new Node\Identifier('static')),
3434
)]]></code>
35-
<code><![CDATA[$createMethod($method->getAliasName())]]></code>
36-
<code><![CDATA[$createMethod($traitAliasDefinition['alias'])]]></code>
35+
<code><![CDATA[$createMethod($method->getAliasName(), $modifiersUsedWithAlias ? $method->getModifiers() : $methodModifiers)]]></code>
36+
<code><![CDATA[$createMethod($traitAliasDefinition['alias'], $methodModifiers)]]></code>
3737
<code><![CDATA[$createMethod('cases', [], new Node\Identifier('array'))]]></code>
3838
<code><![CDATA[$createProperty('name', new Node\Identifier('string'))]]></code>
3939
<code><![CDATA[$createProperty('name', new Node\Identifier('string'))]]></code>

src/Reflection/ReflectionClass.php

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -360,8 +360,12 @@ public function getExtensionName(): string|null
360360
return $this->locatedSource->getExtensionName();
361361
}
362362

363-
/** @return list<ReflectionMethod> */
364-
private function createMethodsFromTrait(ReflectionMethod $method): array
363+
/**
364+
* @param array<lowercase-string, ReflectionMethod> $currentMethods
365+
*
366+
* @return list<ReflectionMethod>
367+
*/
368+
private function createMethodsFromTrait(ReflectionMethod $method, array $currentMethods): array
365369
{
366370
$methodModifiers = $method->getModifiers();
367371
$lowerCasedMethodHash = $this->lowerCasedMethodHash($method->getImplementingClass()->getName(), $method->getName());
@@ -377,17 +381,35 @@ private function createMethodsFromTrait(ReflectionMethod $method): array
377381
}
378382
}
379383

380-
$createMethod = function (string|null $aliasMethodName) use ($method, $methodModifiers): ReflectionMethod {
384+
$createMethod = function (string|null $aliasMethodName, int $methodModifiers) use ($method): ReflectionMethod {
381385
assert($aliasMethodName === null || $aliasMethodName !== '');
382386

383-
/** @phpstan-ignore argument.type */
387+
/** @var int-mask-of<ReflectionMethodAdapter::IS_*> $methodModifiers */
388+
$methodModifiers = $methodModifiers;
389+
384390
return $method->withImplementingClass($this, $aliasMethodName, $methodModifiers);
385391
};
386392

387393
$methods = [];
388394

389-
if (! array_key_exists($lowerCasedMethodHash, $this->traitsData['precedences'])) {
390-
$methods[] = $createMethod($method->getAliasName());
395+
if (
396+
! array_key_exists($lowerCasedMethodHash, $this->traitsData['precedences'])
397+
&& ! array_key_exists($lowerCasedMethodHash, $currentMethods)
398+
) {
399+
$modifiersUsedWithAlias = false;
400+
401+
foreach ($this->traitsData['aliases'] as $traitAliasDefinitions) {
402+
foreach ($traitAliasDefinitions as $traitAliasDefinition) {
403+
if ($lowerCasedMethodHash === $traitAliasDefinition['hash']) {
404+
$modifiersUsedWithAlias = true;
405+
break;
406+
}
407+
}
408+
}
409+
410+
// Modifiers used with alias -> copy method with original modifiers (will be added later with the alias name and new modifiers)
411+
// Modifiers not used with alias -> add method with new modifiers
412+
$methods[] = $createMethod($method->getAliasName(), $modifiersUsedWithAlias ? $method->getModifiers() : $methodModifiers);
391413
}
392414

393415
if ($this->traitsData['aliases'] !== []) {
@@ -403,7 +425,7 @@ private function createMethodsFromTrait(ReflectionMethod $method): array
403425
continue;
404426
}
405427

406-
$methods[] = $createMethod($traitAliasDefinition['alias']);
428+
$methods[] = $createMethod($traitAliasDefinition['alias'], $methodModifiers);
407429
}
408430
}
409431
}
@@ -455,7 +477,7 @@ private function getMethodsIndexedByLowercasedName(AlreadyVisitedClasses $alread
455477
foreach ($this->getTraits() as $trait) {
456478
$alreadyVisitedClassesCopy = clone $alreadyVisitedClasses;
457479
foreach ($trait->getMethodsIndexedByLowercasedName($alreadyVisitedClassesCopy) as $method) {
458-
foreach ($this->createMethodsFromTrait($method) as $traitMethod) {
480+
foreach ($this->createMethodsFromTrait($method, $methods) as $traitMethod) {
459481
$lowercasedMethodName = strtolower($traitMethod->getName());
460482

461483
if (! array_key_exists($lowercasedMethodName, $methods)) {

test/unit/Reflection/ReflectionClassTest.php

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2646,8 +2646,8 @@ class Foo
26462646
self::assertTrue($protectedMethodFromTrait->isProtected());
26472647

26482648
$privateMethodFromClass = $classReflection->getMethod('privateMethod');
2649-
self::assertTrue($privateMethodFromClass->isProtected());
2650-
self::assertFalse($privateMethodFromClass->isPrivate());
2649+
self::assertFalse($privateMethodFromClass->isProtected());
2650+
self::assertTrue($privateMethodFromClass->isPrivate());
26512651

26522652
$privateMethodFromTrait = $traitReflection->getMethod('privateMethod');
26532653
self::assertFalse($privateMethodFromTrait->isProtected());
@@ -3105,4 +3105,68 @@ interface EntityInterface extends AccessibleInterface, CacheableDependencyInterf
31053105

31063106
self::assertSame($expectedInterfaceNames, $classReflection->getInterfaceNames());
31073107
}
3108+
3109+
public function testTraitAliasCopiesMethod(): void
3110+
{
3111+
// runtime proof https://3v4l.org/7R74v
3112+
3113+
$php = <<<'PHP'
3114+
<?php
3115+
trait Foo {
3116+
public function hello(): string
3117+
{
3118+
return "Hello from Foo!";
3119+
}
3120+
}
3121+
3122+
class Bar {
3123+
use Foo {
3124+
hello as private somethingElse;
3125+
}
3126+
}
3127+
PHP;
3128+
3129+
$classInfo = (new DefaultReflector(new StringSourceLocator($php, $this->astLocator)))->reflectClass('Bar');
3130+
3131+
self::assertTrue($classInfo->hasMethod('hello'));
3132+
$hello = $classInfo->getMethod('hello');
3133+
self::assertTrue($hello->isPublic());
3134+
3135+
self::assertTrue($classInfo->hasMethod('somethingElse'));
3136+
$somethingElse = $classInfo->getMethod('somethingElse');
3137+
self::assertTrue($somethingElse->isPrivate());
3138+
}
3139+
3140+
public function testTraitAliasCopiesMethodUnlessMethodWithSameNameAlreadyInClass(): void
3141+
{
3142+
// runtime proof https://3v4l.org/WbpSi
3143+
$php = <<<'PHP'
3144+
<?php
3145+
trait Foo {
3146+
public function hello(): string
3147+
{
3148+
return "Hello from Foo!";
3149+
}
3150+
}
3151+
3152+
class Bar {
3153+
use Foo {
3154+
hello as private somethingElse;
3155+
}
3156+
3157+
protected function hello(int $i): void
3158+
{
3159+
}
3160+
}
3161+
PHP;
3162+
3163+
$classInfo = (new DefaultReflector(new StringSourceLocator($php, $this->astLocator)))->reflectClass('Bar');
3164+
3165+
self::assertTrue($classInfo->hasMethod('hello'));
3166+
self::assertTrue($classInfo->hasMethod('somethingElse'));
3167+
3168+
$hello = $classInfo->getMethod('hello');
3169+
self::assertTrue($hello->isProtected());
3170+
self::assertCount(1, $hello->getParameters());
3171+
}
31083172
}

0 commit comments

Comments
 (0)