Skip to content

Commit bc90093

Browse files
SanderMullerclaude
andcommitted
Report inheritance cycles with a rule instead of a reflection error
A class that extends itself, interfaces that extend each other and traits that use each other cannot be declared in PHP. A class on a parent class cycle now has no parent instead of throwing CircularReference, and a trait inside a trait on a cycle processes only the traits reflection says it uses. CircularInheritanceRule reports the cycle on each of its members, at level 0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent c539adc commit bc90093

21 files changed

Lines changed: 644 additions & 39 deletions

File tree

‎.github/workflows/e2e-tests.yml‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1403,6 +1403,26 @@ jobs:
14031403
echo "$OUTPUT"
14041404
../bashunit assert contains 'Result cache was not saved because of non-ignorable exception: Class ResultCacheE2EInternalError\Missing was not found' "$OUTPUT"
14051405
test ! -f tmp/resultCache.php
1406+
- script: |
1407+
cd e2e/result-cache-circular-inheritance
1408+
# src/First.php, src/Second.php and src/Third.php extend each other in a cycle. src/TraitA.php
1409+
# uses TraitB, which uses TraitC, without a cycle.
1410+
OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
1411+
echo "$OUTPUT"
1412+
../bashunit assert contains 'First.php:5:Class ResultCacheE2ECircularInheritance\First extends ResultCacheE2ECircularInheritance\Second, which extends ResultCacheE2ECircularInheritance\Third, which extends ResultCacheE2ECircularInheritance\First.' "$OUTPUT"
1413+
../bashunit assert not_contains 'TraitA' "$OUTPUT"
1414+
# The cycle is reported by a rule and is not a reflection error, so the result cache is saved.
1415+
../bashunit assert contains 'Result cache is saved.' "$OUTPUT"
1416+
# Breaking the cycle in src/Third.php has to re-analyse src/First.php, although a class on a
1417+
# cycle has no parents and the file depends only on its direct parent otherwise.
1418+
cp patched/Third.php src/Third.php
1419+
# src/TraitC.php now uses TraitA, which closes a cycle. A trait declaration is not analysed on
1420+
# its own, so src/TraitA.php depends on the traits it uses only through the rule.
1421+
cp patched/TraitC.php src/TraitC.php
1422+
OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
1423+
echo "$OUTPUT"
1424+
../bashunit assert not_contains 'First.php' "$OUTPUT"
1425+
../bashunit assert contains 'TraitA.php:5:Trait ResultCacheE2ECircularInheritance\TraitA uses ResultCacheE2ECircularInheritance\TraitB, which uses ResultCacheE2ECircularInheritance\TraitC, which uses ResultCacheE2ECircularInheritance\TraitA.' "$OUTPUT"
14061426
- script: |
14071427
cd e2e/result-cache-string-class
14081428
../../bin/phpstan analyse
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2ECircularInheritance;
4+
5+
class Third
6+
{
7+
8+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2ECircularInheritance;
4+
5+
trait TraitC
6+
{
7+
8+
use TraitA;
9+
10+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
parameters:
2+
level: 0
3+
tmpDir: tmp
4+
paths:
5+
- src
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2ECircularInheritance;
4+
5+
class First extends Second
6+
{
7+
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2ECircularInheritance;
4+
5+
class Second extends Third
6+
{
7+
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2ECircularInheritance;
4+
5+
class Third extends First
6+
{
7+
8+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2ECircularInheritance;
4+
5+
trait TraitA
6+
{
7+
8+
use TraitB;
9+
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2ECircularInheritance;
4+
5+
trait TraitB
6+
{
7+
8+
use TraitC;
9+
10+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2ECircularInheritance;
4+
5+
trait TraitC
6+
{
7+
8+
}

0 commit comments

Comments
 (0)