Skip to content

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

Open
SanderMuller wants to merge 1 commit into
phpstan:2.3.xfrom
SanderMuller:report-circular-inheritance
Open

SanderMuller wants to merge 1 commit into
phpstan:2.3.xfrom
SanderMuller:report-circular-inheritance

Conversation

@SanderMuller

@SanderMuller SanderMuller commented Oct 8, 2026 •

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.

#6729 removed the part of the result-cache-parse-error e2e step that expected a reflection error for a class that extends itself. The new result-cache-circular-inheritance step checks instead that the result cache is saved when the rule reports a cycle.

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. It also checks that the first run saves the result cache.
  • make tests passes with and without the Turbo extension (22,636 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

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>
@SanderMuller
SanderMuller force-pushed the report-circular-inheritance branch from 20c7a3c to 05e9f43 Compare October 23, 2026 20:32
@staabm

staabm commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

when did the phpstan-laravel and the tempest-framework CI check broke? I saw them failling also in others PRs but did not yet saw the root-cause PR (unrelated to this PR - just asking)

@staabm

staabm commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

did you verify the results of newly added tests in this PR against the state we had before 613b1b1 ?

@SanderMuller

Copy link
Copy Markdown
Contributor Author

when did the phpstan-laravel and the tempest-framework CI check broke? I saw them failling also in others PRs but did not yet saw the root-cause PR (unrelated to this PR - just asking)

calebdw/phpstan-laravel#50 is the reason for phpstan-laravel I think

@SanderMuller

Copy link
Copy Markdown
Contributor Author

did you verify the results of newly added tests in this PR against the state we had before 613b1b1 ?

Yes. I analysed the PR's cycle fixtures one file at a time, at level 8, on three trees:

  • 28f54de, the commit before 613b1b1, with BetterReflection 6.73.0.5. Each file reported Reflection error: Circular reference to class "..." without a line, for the first cycle only. For example, circular-inheritance.php reported only ExtendsSelf.
  • 2.3.x at bb2b515. The class and trait cycles report nothing, and trait-cycle-members.php crashes with MissingPropertyFromReflectionException.
  • This PR. Every member of every cycle gets the rule error on its line. That is 13 errors in circular-inheritance.php, and one for each class of the e2e fixture. trait-cycle-members.php does not crash.

So each cycle that was reported before 613b1b1 is reported again, now as a rule error with a line.

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.

2 participants