Skip to content

Commit fbbc741

Browse files
authored
Merge pull request #1460 from kukulich/broken-class-methods-with-trait-aliases
Fixed missing class method when multiple trait aliases are used
2 parents 00acbfc + 6d33d26 commit fbbc741

File tree

3 files changed

+126
-41
lines changed

3 files changed

+126
-41
lines changed

psalm-baseline.xml

Lines changed: 2 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.25.0@01a8eb06b9e9cc6cfb6a320bf9fb14331919d505">
2+
<files psalm-version="5.26.1@d747f6500b38ac4f7dfc5edbcae6e4b637d7add0">
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($aliasMethodName)]]></code>
3635
<code><![CDATA[$createMethod($method->getAliasName())]]></code>
36+
<code><![CDATA[$createMethod($traitAliasDefinition['alias'])]]></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>
@@ -46,7 +46,6 @@
4646
<code><![CDATA[array_map]]></code>
4747
<code><![CDATA[array_map]]></code>
4848
<code><![CDATA[array_map]]></code>
49-
<code><![CDATA[array_map]]></code>
5049
</ImpureFunctionCall>
5150
<ImpureMethodCall>
5251
<code><![CDATA[createEmpty]]></code>

src/Reflection/ReflectionClass.php

Lines changed: 84 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
use function array_slice;
5454
use function array_values;
5555
use function assert;
56-
use function end;
5756
use function in_array;
5857
use function is_int;
5958
use function is_string;
@@ -120,7 +119,7 @@ class ReflectionClass implements Reflection
120119
private array $immediateMethods;
121120

122121
/** @var array{
123-
* aliases: array<non-empty-string, non-empty-string>,
122+
* aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
124123
* modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
125124
* precedences: array<non-empty-string, non-empty-string>,
126125
* hashes: array<non-empty-string, non-empty-string>,
@@ -391,12 +390,22 @@ private function createMethodsFromTrait(ReflectionMethod $method): array
391390
$methods[] = $createMethod($method->getAliasName());
392391
}
393392

394-
foreach ($this->traitsData['aliases'] as $aliasMethodName => $traitAliasDefinition) {
395-
if ($lowerCasedMethodHash !== $traitAliasDefinition) {
396-
continue;
397-
}
393+
if ($this->traitsData['aliases'] !== []) {
394+
$traits = array_combine($this->traitClassNames, $this->getTraits());
395+
396+
foreach ($this->traitsData['aliases'] as $traitClassName => $traitAliasDefinitions) {
397+
foreach ($traitAliasDefinitions as $traitAliasDefinition) {
398+
if ($lowerCasedMethodHash !== $traitAliasDefinition['hash']) {
399+
continue;
400+
}
401+
402+
if (! $traits[$traitClassName]->hasMethod($traitAliasDefinition['method'])) {
403+
continue;
404+
}
398405

399-
$methods[] = $createMethod($aliasMethodName);
406+
$methods[] = $createMethod($traitAliasDefinition['alias']);
407+
}
408+
}
400409
}
401410

402411
return $methods;
@@ -1366,18 +1375,32 @@ static function (ReflectionClass $trait): string {
13661375
*/
13671376
public function getTraitAliases(): array
13681377
{
1369-
return array_map(
1370-
fn (string $lowerCasedMethodHash): string => $this->traitsData['hashes'][$lowerCasedMethodHash],
1371-
$this->traitsData['aliases'],
1372-
);
1378+
if ($this->traitsData['aliases'] === []) {
1379+
return [];
1380+
}
1381+
1382+
$traits = array_combine($this->traitClassNames, $this->getTraits());
1383+
$traitAliases = [];
1384+
1385+
foreach ($this->traitsData['aliases'] as $traitClassName => $traitAliasDefinitions) {
1386+
foreach ($traitAliasDefinitions as $traitAliasDefinition) {
1387+
if (! $traits[$traitClassName]->hasMethod($traitAliasDefinition['method'])) {
1388+
continue;
1389+
}
1390+
1391+
$traitAliases[$traitAliasDefinition['alias']] = $this->traitsData['hashes'][$traitAliasDefinition['hash']];
1392+
}
1393+
}
1394+
1395+
return $traitAliases;
13731396
}
13741397

13751398
/**
13761399
* Returns data when importing traits for this class:
13771400
*
13781401
* 'aliases': List of the aliases used when importing traits. In format:
13791402
*
1380-
* 'aliasedMethodName' => 'ActualClass::actualMethod'
1403+
* 'traitClassName' => ['alias' => 'aliasedMethodName', 'method' => 'actualMethodName', 'hash' => 'traitClassName::actualMethodName'],
13811404
*
13821405
* Example:
13831406
* // When reflecting a code such as:
@@ -1387,7 +1410,7 @@ public function getTraitAliases(): array
13871410
* }
13881411
*
13891412
* // This method would return
1390-
* // ['myAliasedMethod' => 'MyTrait::myTraitMethod']
1413+
* // ['MyTrait' => ['alias' => 'myAliasedMethod', 'method' => 'myTraitMethod', 'hash' => 'mytrait::mytraitmethod']]
13911414
*
13921415
* 'modifiers': Used modifiers when importing traits. In format:
13931416
*
@@ -1418,7 +1441,7 @@ public function getTraitAliases(): array
14181441
* // ['MyTrait1::foo' => 'MyTrait2::foo']
14191442
*
14201443
* @return array{
1421-
* aliases: array<non-empty-string, non-empty-string>,
1444+
* aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
14221445
* modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
14231446
* precedences: array<non-empty-string, non-empty-string>,
14241447
* hashes: array<non-empty-string, non-empty-string>,
@@ -1434,37 +1457,60 @@ private function computeTraitsData(ClassNode|InterfaceNode|TraitNode|EnumNode $n
14341457
];
14351458

14361459
foreach ($node->getTraitUses() as $traitUsage) {
1437-
$traitNames = $traitUsage->traits;
1438-
$adaptations = $traitUsage->adaptations;
1439-
1440-
foreach ($adaptations as $adaptation) {
1441-
$usedTrait = $adaptation->trait;
1442-
if ($usedTrait === null) {
1443-
$usedTrait = end($traitNames);
1444-
}
1460+
foreach ($traitUsage->adaptations as $adaptation) {
1461+
$usedTraits = $adaptation->trait !== null ? [$adaptation->trait] : $traitUsage->traits;
1462+
$traitsData = $this->processTraitAdaptation($adaptation, $usedTraits, $traitsData);
1463+
}
1464+
}
14451465

1446-
$methodHash = $this->methodHash($usedTrait->toString(), $adaptation->method->toString());
1447-
$lowerCasedMethodHash = $this->lowerCasedMethodHash($usedTrait->toString(), $adaptation->method->toString());
1466+
return $traitsData;
1467+
}
14481468

1449-
$traitsData['hashes'][$lowerCasedMethodHash] = $methodHash;
1469+
/**
1470+
* @phpcs:disable Squiz.Commenting.FunctionComment.MissingParamName
1471+
*
1472+
* @param array<array-key, Node\Name> $usedTraits
1473+
* @param array{
1474+
* aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
1475+
* modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
1476+
* precedences: array<non-empty-string, non-empty-string>,
1477+
* hashes: array<non-empty-string, non-empty-string>,
1478+
* } $traitsData
1479+
*
1480+
* @return array{
1481+
* aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
1482+
* modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
1483+
* precedences: array<non-empty-string, non-empty-string>,
1484+
* hashes: array<non-empty-string, non-empty-string>,
1485+
* }
1486+
*/
1487+
private function processTraitAdaptation(Node\Stmt\TraitUseAdaptation $adaptation, array $usedTraits, array $traitsData): array
1488+
{
1489+
foreach ($usedTraits as $usedTrait) {
1490+
$methodHash = $this->methodHash($usedTrait->toString(), $adaptation->method->toString());
1491+
$lowerCasedMethodHash = $this->lowerCasedMethodHash($usedTrait->toString(), $adaptation->method->toString());
14501492

1451-
if ($adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
1452-
if ($adaptation->newModifier !== null) {
1453-
/** @var int-mask-of<ReflectionMethodAdapter::IS_*> $modifier */
1454-
$modifier = $adaptation->newModifier;
1455-
$traitsData['modifiers'][$lowerCasedMethodHash] = $modifier;
1456-
}
1493+
$traitsData['hashes'][$lowerCasedMethodHash] = $methodHash;
14571494

1458-
if ($adaptation->newName) {
1459-
$traitsData['aliases'][$adaptation->newName->name] = $lowerCasedMethodHash;
1460-
continue;
1461-
}
1495+
if ($adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
1496+
if ($adaptation->newModifier !== null) {
1497+
/** @var int-mask-of<ReflectionMethodAdapter::IS_*> $modifier */
1498+
$modifier = $adaptation->newModifier;
1499+
$traitsData['modifiers'][$lowerCasedMethodHash] = $modifier;
14621500
}
14631501

1464-
if (! $adaptation instanceof Node\Stmt\TraitUseAdaptation\Precedence || ! $adaptation->insteadof) {
1465-
continue;
1502+
if ($adaptation->newName !== null) {
1503+
// We need to save all possible combinations of trait and method names
1504+
// The real aliases will be filtered in getters
1505+
/** @var trait-string $usedTraitClassName */
1506+
$usedTraitClassName = $usedTrait->toString();
1507+
$traitsData['aliases'][$usedTraitClassName][] = [
1508+
'alias' => $adaptation->newName->name,
1509+
'method' => $adaptation->method->toString(),
1510+
'hash' => $lowerCasedMethodHash,
1511+
];
14661512
}
1467-
1513+
} elseif ($adaptation instanceof Node\Stmt\TraitUseAdaptation\Precedence) {
14681514
foreach ($adaptation->insteadof as $insteadof) {
14691515
$adaptationNameHash = $this->lowerCasedMethodHash($insteadof->toString(), $adaptation->method->toString());
14701516

test/unit/Reflection/ReflectionClassTest.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1505,6 +1505,46 @@ public function testGetTraitAliases(): void
15051505
], $classInfo->getTraitAliases());
15061506
}
15071507

1508+
public function testGetMethodsWithTraitAliases(): void
1509+
{
1510+
$phpCode = <<<'PHP'
1511+
<?php
1512+
trait Trait1Fixture {
1513+
public function method1() {}
1514+
public function method2() {}
1515+
}
1516+
1517+
trait Trait2Fixture {
1518+
public function method3() {}
1519+
public function method4() {}
1520+
}
1521+
1522+
class ClassFixture {
1523+
use Trait1Fixture, Trait2Fixture {
1524+
method1 as alias1;
1525+
method3 as alias3;
1526+
}
1527+
}
1528+
PHP;
1529+
1530+
$reflector = new DefaultReflector(new StringSourceLocator($phpCode, $this->astLocator));
1531+
$classInfo = $reflector->reflectClass('ClassFixture');
1532+
1533+
self::assertSame([
1534+
'alias1' => 'Trait1Fixture::method1',
1535+
'alias3' => 'Trait2Fixture::method3',
1536+
], $classInfo->getTraitAliases());
1537+
1538+
self::assertSame([
1539+
'method1',
1540+
'alias1',
1541+
'method2',
1542+
'method3',
1543+
'alias3',
1544+
'method4',
1545+
], array_keys($classInfo->getMethods()));
1546+
}
1547+
15081548
public function testGetTraitNamesWithMissingTraitDefinitions(): void
15091549
{
15101550
$reflector = new DefaultReflector(new SingleFileSourceLocator(

0 commit comments

Comments
 (0)