Skip to content

Do not report abstract interface methods provided by a non-abstract built-in parent class in MissingMethodImplementationRule#6059

Open
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-xdakn3q
Open

Do not report abstract interface methods provided by a non-abstract built-in parent class in MissingMethodImplementationRule#6059
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-xdakn3q

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

MissingMethodImplementationRule intermittently reported false positives such as:

Non-abstract class Bug3958\XmlHelper contains abstract method getChildren() from interface RecursiveIterator.
Non-abstract class Bug3958\XmlHelper contains abstract method hasChildren() from interface RecursiveIterator.

for a class extending \SimpleXMLElement (mostly seen on PHP 7.4 CI, MissingMethodImplementationRuleTest::testBug3958).

The rule walks the native reflection methods of a non-abstract class and reports every method that reflection marks as abstract. When a class extends a built-in class, the built-in parent implements its interface methods natively at runtime — but PHP-version-gated stubs can present a built-in class as implementing an interface while the interface's methods are filtered out for the target PHP version. That inconsistent (interface present, method filtered) reflection state makes an inherited interface method look unimplemented, producing a false "abstract method" error.

The fix teaches the rule to ignore an abstract method when a non-abstract built-in ancestor class implements the declaring interface, because such a parent always provides the method at runtime.

Changes

  • src/Rules/Methods/MissingMethodImplementationRule.php
    • Before reporting an abstract method whose declaring class is an interface, skip it when a non-abstract built-in ancestor class implements that interface (isProvidedByBuiltinAncestor() walks ClassReflection::getParents() and checks isBuiltin(), !isAbstract(), implementsInterface()).
  • tests/PHPStan/Rules/Methods/MissingMethodImplementationRulePhp74Test.php + data/bug-14964.php + data/missing-method-impl-php74.neon
    • New regression test running at phpVersion: 70400 covering both the reported \SimpleXMLElement case and the deterministic sibling \IntlBreakIterator case.

Root cause

The bug is a family: a non-abstract class extending a non-abstract built-in class whose interface method is version-gated in the stubs. The stub keeps the implements X on the built-in class but filters the version-gated method body, so reflection yields an inconsistent state where the inherited interface method appears abstract.

Instances found and covered by the single fix:

  • \SimpleXMLElement implements RecursiveIterator; getChildren()/hasChildren() are @since 8.0 — the originally reported (intermittent) case.
  • \IntlBreakIterator (and its subclasses \IntlRuleBasedBreakIterator, \IntlCodePointBreakIterator) implement IteratorAggregate; getIterator() is @since 8.0 — a deterministic sibling reproducible at phpVersion < 8.0 (no BetterReflection special-case masks it, unlike SimpleXMLElement/SplObjectStorage).

The fix is general: instead of relying on per-class stub special-cases, it trusts that any non-abstract built-in ancestor implementing the interface provides its methods. Verified that genuine errors are preserved — abstract methods from built-in abstract classes (\FilterIterator::accept()) and from interfaces the user class implements directly are still reported.

Test

  • New MissingMethodImplementationRulePhp74Test::testBug14964 analyses a class extending \IntlBreakIterator and a class extending \SimpleXMLElement under phpVersion: 70400 and expects no errors. It fails without the fix (Non-abstract class Bug14964\MyBreakIterator contains abstract method getIterator() from interface IteratorAggregate.) and passes with it.
  • The existing MissingMethodImplementationRuleTest (including the genuine positive cases and enum case) continues to pass.
  • make phpstan and the full test suite pass.

Fixes phpstan/phpstan#14964

…uilt-in parent class in `MissingMethodImplementationRule`

- Skip an abstract method whose declaring class is an interface when a
  non-abstract built-in (internal) ancestor class implements that interface.
  Built-in classes always provide their interface methods at runtime, so any
  "abstract" appearance is a stub artifact of PHP-version gating.
- Fixes the intermittent false positive on `SimpleXMLElement` subclasses
  (`RecursiveIterator::getChildren()`/`hasChildren()` gated at PHP 8.0) and the
  deterministic sibling on the `Intl*BreakIterator` family
  (`IteratorAggregate::getIterator()` gated at PHP 8.0) targeting PHP < 8.0.
- Genuine cases are unaffected: methods declared in interfaces the user class
  implements itself, and abstract methods declared in built-in abstract classes
  (e.g. `FilterIterator::accept()`), are still reported.
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.

CI intermittent

2 participants