Add PHPStan rule: needs*() config detectors must not mutate the entity they inspect

Closes #3591855 (closed)

What this MR does

The escaped-config health check (Doctor::runUpdatesEscapedConfigCheck()) reflects every public needs*() detector on CanvasConfigUpdater and runs them all against one shared, loaded config entity. A detector that mutates the entity it is passed leaks that change into every detector that runs after it, corrupting the audit's result. This has happened twice: one detector mutated via setComponentTree()/set(), another had to clone before calculateDependencies().

This adds a PHPStan rule (NeedsDetectorMustNotMutateEntityRule) that enforces the read-only contract statically. For each public needs*() method on CanvasConfigUpdater, it flags:

  • a blocklisted state-changing method (set, setComponentTree, save, delete, calculateDependencies, setInput(s), createVersion, …) called directly on the first (entity) parameter, and
  • a property or array-offset write to that parameter.

Working on a clone is allowed (the receiver is not the parameter). loadVersion() is not blocklisted: detectors switch a Component's version and restore it before returning.

Known limitation: only direct mutation of the parameter is detected. Indirect mutation (through a helper, a by-reference alias, or the entity's field object graph) is out of scope, matching the other Canvas rules.

Testing steps

  • composer phpstan passes on the current code (no false positives on the existing detectors).
  • Temporarily add $entity->calculateDependencies(); (or $entity->set(...)) inside any needs*() method on CanvasConfigUpdater and re-run composer phpstan; the rule reports canvas.needsDetectorMustNotMutate.
  • Revert the temporary change.

AI usage

Written with Claude Code (Opus 4.8), reviewed by a human before pushing, per Drupal.org's AI contribution policy.

Merge request reports

Loading