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 phpstanpasses on the current code (no false positives on the existing detectors). - Temporarily add
$entity->calculateDependencies();(or$entity->set(...)) inside anyneeds*()method onCanvasConfigUpdaterand re-runcomposer phpstan; the rule reportscanvas.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.