Skip to content

Report inheritance cycles with a rule instead of a reflection error - #6712

Open
SanderMuller wants to merge 2 commits into
phpstan:2.3.xfrom
SanderMuller:report-circular-inheritance
Open

SanderMuller wants to merge 2 commits into
phpstan:2.3.xfrom
SanderMuller:report-circular-inheritance

Conversation

@SanderMuller

Copy link
Copy Markdown
Contributor

BetterReflection 6.73.0.6 (ondrejmirtes/BetterReflection#47) no longer throws CircularReference for an inheritance cycle. A class-like on a cycle has no supertypes of its kind instead. Since 613b1b1, 2.3.x therefore reports nothing for such a cycle. This is the phpstan-src part of the change, with a rule that reports the cycle.

The changes:

  • CircularInheritanceRule, at level 0, reports the cycle on each of its members. The identifiers are class.circularExtends, interface.circularExtends and traitUse.circular, and the error cannot be ignored. Examples: Class Foo extends itself. and Class Foo extends Bar, which extends Foo.
  • ClassReflection::getParentClass() returns null for a class that extends itself, instead of throwing CircularReference. A class that extends a class on a cycle keeps that parent.
  • Inside a trait, TraitUseHandler processes only the traits that reflection says the trait uses. A trait on a trait cycle uses none.
  • ClassReflection.cpp and TraitUseHandler.cpp get the same changes.
  • When the rule finds a cycle, it tracks a dependency on the other members of the cycle. For a trait declaration, it also tracks the traits it uses. A trait declaration is not analysed on its own, so its file does not depend on the traits it uses otherwise. Without these dependencies, the result cache keeps a stale error after a cycle is broken or closed elsewhere.

The TraitUseHandler change also fixes a crash on 2.3.x. Take a class that uses a trait on a cycle, where the members of one trait use the members of the other one. Analysing it stops with an internal error, MissingPropertyFromReflectionException. The new AnalyserIntegrationTest::testTraitCycleMembers() covers this case. A trait on a cycle uses no traits, so the class gets the members of the first trait only. The test therefore also expects Access to an undefined property.

The last part of the result-cache-parse-error e2e step fails on 2.3.x since 613b1b1. It expected a reflection error for a class that extends itself, and that error is gone. The step now checks that the rule reports the class, and that the result cache is saved.

Verification:

  • CircularInheritanceRuleTest covers:
    • classes on cycles of one, two and three members, and interfaces and traits on cycles of one and two,
    • a class that extends itself with a name in different case,
    • diamonds of interfaces and traits without a cycle,
    • class-likes that extend, implement or use a member of a cycle, which are not reported.
  • testTraitCycleMembers() fails on 2.3.x with the internal error.
  • The new e2e test result-cache-circular-inheritance breaks a class cycle and closes a trait cycle between two runs, and checks that the cached errors follow.
  • make tests passes with and without the Turbo extension (22,512 tests, 74 skipped). make phpstan, phpcs and make lint report no errors.
  • Turbo: the strict build passes, smoke.php reports ALL OK, and side-by-side.php, signature-parity.php and walk-trace.php pass. On the rule's fixtures and the cycle fixtures of AnalyserIntegrationTest, the raw output is identical with the extension loaded and not loaded.

Performance on wordpress-develop at level 9, cold runs from source without Turbo, on an M4 Pro. Base is cb3b7e994. Each value is the median of 3 alternating rounds, with the range in brackets. The output is identical, because wordpress-develop has no inheritance cycles.

Base This PR
Wall time 34.04 s (33.40 to 34.11) 34.54 s (33.84 to 34.60)
User CPU 309.1 s (308.1 to 309.4) 308.5 s (307.9 to 311.2)

🤖 Generated with Claude Code

SanderMuller and others added 2 commits October 8, 2026 22:47
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant