Skip to content

Align legacy rules and reflections with current PHPStan API conventions - #1027

Merged
mglaman merged 4 commits into
mainfrom
audit/2-api-conventions
Sep 9, 2026
Merged

Align legacy rules and reflections with current PHPStan API conventions#1027
mglaman merged 4 commits into
mainfrom
audit/2-api-conventions

Conversation

@mglaman

@mglaman mglaman commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Part 6 of 9 in the legacy-code audit stack (on top of #1036). Brings the oldest rules and reflection classes in line with current PHPStan 2.x API conventions and the patterns the newer code in this repo already uses.

The getStorage() return type extension change that was in this PR moved to #1045. Review found that the union it introduced is not handled by the storage method extensions and collapses when storage classes overlap, so it needs its own tests and a design decision before it lands.

What changed

  • PluginManagerInspectionRule migrates from Node\Stmt\Class_ to InClassNode, which provides the ClassReflection directly. The ReflectionProvider constructor dependency, anonymous-class handling, and manual FQN round-trip all disappear. Behavior is unchanged.

  • GlobalDrupalDependencyInjectionRule resolves the called class via $scope->resolveName() and checks against the MethodReflection interface instead of ExtendedMethodReflection, which PHPStan marks @api-do-not-implement. One observable change: parent::service() inside a subclass of the non-final \Drupal class is now reported. No such subclass exists in core or this repo.

  • EntityFieldReflection guards ReflectionProvider::getClass() with hasClass(); FieldItemListPropertyReflection builds nullable types with TypeCombinator::addNull().

  • EntityFieldsViaMagicReflectionExtension uses ClassReflection::is() for the FieldItemListInterface check instead of rebuilding an ObjectType from the class name on every property lookup. An earlier revision used implementsInterface(), which returns false for the interface itself and broke $node->uid->entity. Two public static helpers that only served the old check are removed; no callers exist outside vendored copies of this repo.

  • Redundant ->line($node->getStartLine()) calls removed — that is already the default.

  • PluginManagerInspectionRule no longer crashes when a plugin manager without a constructor contains an anonymous class that declares one. The constructor lookup searched the class body recursively and picked up the nested one, then getConstructor() threw on the hierarchy. It now uses the class node's own method lookup and guards with hasConstructor(). Pre-existing bug, surfaced by review.

Known but out of scope

  • The "must override __construct" branch in PluginManagerInspectionRule is unreachable because the rule returns early unless the class body declares its own constructor. Tracked as a follow-up.

Testing

EntityFieldsViaMagicReflectionExtensionTest gains cases for values typed as FieldItemListInterface itself, which is what $node->uid is. They fail on the implementsInterface() revision and pass now. A new plugin-manager-nested-constructor.php fixture reproduces the crash. Full suite, self-analysis, and phpcs are green.

🤖 Generated with Claude Code

@mglaman
mglaman force-pushed the audit/2-api-conventions branch from 11c49d2 to e8cd1dd Compare August 5, 2026 16:20
@mglaman
mglaman changed the base branch from audit/1-correctness-fixes to audit/1d-autoloader-cleanup August 5, 2026 16:44
@mglaman
mglaman force-pushed the audit/2-api-conventions branch from e8cd1dd to 99903cc Compare August 5, 2026 16:44
@mglaman
mglaman force-pushed the audit/2-api-conventions branch from 99903cc to 40887b7 Compare August 5, 2026 17:15
@mglaman
mglaman force-pushed the audit/2-api-conventions branch from 40887b7 to 6ae8385 Compare August 5, 2026 19:13
@mglaman
mglaman force-pushed the audit/2-api-conventions branch from 6ae8385 to 1c6b5bd Compare August 5, 2026 19:33
@mglaman
mglaman force-pushed the audit/2-api-conventions branch from 1c6b5bd to a15c05c Compare September 8, 2026 17:42
@mglaman mglaman changed the title audit/2 api conventions Align legacy rules and reflections with current PHPStan API conventions Sep 8, 2026
@mglaman
mglaman force-pushed the audit/2-api-conventions branch from a15c05c to 42f177e Compare September 8, 2026 18:14
@mglaman
mglaman force-pushed the audit/2-api-conventions branch 2 times, most recently from ee92408 to 198eec8 Compare September 8, 2026 18:27
@mglaman
mglaman force-pushed the audit/2-api-conventions branch from 198eec8 to f4e503b Compare September 8, 2026 18:59
Base automatically changed from audit/1d-autoloader-cleanup to main September 8, 2026 19:11
@mglaman
mglaman force-pushed the audit/2-api-conventions branch from f4e503b to 168c3a8 Compare September 8, 2026 19:11
mglaman and others added 3 commits September 8, 2026 14:44
InClassNode hands the rule its ClassReflection directly, which drops
the ReflectionProvider constructor dependency, the anonymous-class
namespacedName juggling, and the manual FQN round-trip through
ObjectType. Behavior is unchanged; the rule now simply never fires for
classes PHPStan cannot reflect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- GlobalDrupalDependencyInjectionRule: resolve the called class through
  $scope->resolveName() instead of requiring a FullyQualified node, and
  type-check against the MethodReflection interface instead of
  ExtendedMethodReflection, which is marked api-do-not-implement.
- EntityFieldReflection: guard ReflectionProvider::getClass() with
  hasClass() so a missing Drupal interface degrades instead of throwing.
- EntityFieldsViaMagicReflectionExtension: check interfaces via
  ClassReflection::implementsInterface() instead of rebuilding an
  ObjectType from the class name, which discarded generics and
  allocated on every property lookup; drop the now-unused static
  helpers.
- FieldItemListPropertyReflection: build nullable types with
  TypeCombinator::addNull() instead of new UnionType.
- Drop ->line($node->getStartLine()) calls that restate the default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
implementsInterface() returns false when asked about the interface
itself, so values typed as FieldItemListInterface, which is what
$node->uid is, lost their magic entity and target_id properties. is()
covers the interface and every implementation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…constructor

The constructor lookup searched the whole class body recursively, so a
constructor declared by an anonymous class inside a method was taken as
the plugin manager's own. With YAML discovery that reached
ClassReflection::getConstructor() on a hierarchy with no constructor,
which throws. Look up the class's own method and guard with
hasConstructor().

Co-Authored-By: Claude Fable 5.1 <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