Skip to content

[detector-report] mutable-static-state #584

Description

@jessegall

Detector: mutable-static-state

Report (why the flagged code is CORRECT and the detector is wrong):
ReferenceTracker::$composing is a dynamic scope, not a global: Compose pushes the tracker for exactly the duration of one compose() call and pops it in finally. Compose bodies are static by the project's architecture and build children with new and ::of(), so the writes that pair with the parent's recorded reads (535 StateProxy writes, Primitive passThrough) land on fresh components that hold no collaborator and never pass through the parent's Reactive recorder. Pairing matches reads and writes in the order they happen, which a walk after compose cannot recover.

Cleanest design the reporter can conceive:
A try/finally-scoped stack pushed and popped by the one pipe that runs compose, read only by the write path of components built inside it, which is the flagged code.

⚖️ Maintainer litmus: a valid detector-report needs the flagged code to ALREADY BE the
cleanest design. If the design above differs from the flagged code at all, THAT design is
the owed fix — close this report; the fix is still owed.

Where: src/View/Core/ReferenceTracker.php:60-104

Code (src/View/Core/ReferenceTracker.php:60-104):

   57          private readonly Component | null $component = null,
   58      ) {}
   5960      /**
→  61       * @template TReturn
→  62       *
→  63       * @param  Closure(): TReturn  $compose
→  64       * @return TReturn
→  65       */66      public static function composing(self $passed, Closure $compose): mixed
→  67      {
→  68          self::$composing[] = $passed;
→  6970          try71          {
→  72              return $compose();
→  73          }
→  74          finally75          {
→  76              array_pop(self::$composing);
→  7778              $passed->drain();
→  79          }
→  80      }
→  8182      /**
→  83       * @param  array<array-key, mixed>  $values
→  84       */85      public static function set(Component $component, array $values): void86      {
→  87          $composing = end(self::$composing);
→  8889          if ($composing === false)
→  90          {
→  91              return;
→  92          }
→  9394          $composing->pair($component, $values);
→  95      }
→  9697      public static function authored(Component $component, string $property, mixed $value): void98      {
→  99          if (! Referable::is($value))
→ 100          {
→ 101              return;
→ 102          }
→ 103104          $write = new AuthoredWrite($component, $property, $value);
  105          $composing = end(self::$composing);
  106  
  107          $component->wrote($write);

Where: src/View/StateProxy.php:25-32

Code (src/View/StateProxy.php:25-32):

  22       */
  23      public function __construct(private readonly Component $component) {}
  2425      public function __set(string $name, mixed $value): void26      {
→ 27          $value = self::cased($this->component::stateClass(), $name, $value);
→ 2829          $this->component->setState(static fn (State $state) => $state->{$name} = $value);
→ 3031          ReferenceTracker::authored($this->component, $name, $value);
→ 32      }
  33  
  34      /**
  35       * @param  class-string  $state

Where: src/View/Compiling/Pipes/Compose.php:39-45

Code (src/View/Compiling/Pipes/Compose.php:39-45):

  36              self::exposeReferenced($component, $opened->state);
  37          }
  3839          if ($opened->composes && $component instanceof Composition)
→ 40          {
→ 41              $component->replaceChildren(ReferenceTracker::composing(
→ 42                  $opened->passed,
→ 43                  fn () => $this->composed($component, $opened->node, $opened->state, $opened->passed),
→ 44              ));
→ 45          }
  46  
  47          return new ComposedChildren(
  48              component: $component,

Filed via commandments report from a consumer project.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions