From 18a37a098157a98edbc8473e9c0d8dff3e8a88fa Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 7 Aug 2026 20:51:59 +0900 Subject: fix(plugin): make proxy stub property access an explicit error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A proxy stub declares none of the real class's instance properties, and the `__get`/`__set` forwarders were emitted only for classes that declare a public one. Every other property access therefore got PHP's own answer for an undeclared property — null on a read, a dynamic property on a write — so plugin code reading state the entity holds ran on with null and failed somewhere else entirely, or not at all. Emit the forwarders on every root stub, add `__isset`/`__unset` alongside them so `isset()` cannot answer false silently either, and serve all four from one dispatcher that answers the state the Rust-side entity exposes and rejects every other name. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/plugin-stub-generator/src/Generator.php | 29 ++++++++++++------------- 1 file changed, 14 insertions(+), 15 deletions(-) (limited to 'scripts') diff --git a/scripts/plugin-stub-generator/src/Generator.php b/scripts/plugin-stub-generator/src/Generator.php index 395155dd..12967787 100644 --- a/scripts/plugin-stub-generator/src/Generator.php +++ b/scripts/plugin-stub-generator/src/Generator.php @@ -12,8 +12,7 @@ use PhpParser\Node\Stmt\Interface_; /** * Emits the proxy stub files deterministically from the Composer sources and the classifier * report. Anything the emitter cannot faithfully proxy (by-ref or variadic parameters, - * magic methods, public properties, non-public constants) fails generation instead of - * degrading silently. + * magic methods, non-public constants) fails generation instead of degrading silently. */ final class Generator { @@ -59,7 +58,6 @@ final class Generator PHP; private const PROPERTY_FORWARDERS = <<<'PHP' - /** The real class declares public properties; every access forwards to the entity. */ public function __get($name) { return \ShirabeRpcRuntime::callRust($this->__rhandle, '__get', [$name]); @@ -69,6 +67,16 @@ final class Generator { \ShirabeRpcRuntime::callRust($this->__rhandle, '__set', [$name, $value]); } + + public function __isset($name): bool + { + return \ShirabeRpcRuntime::callRust($this->__rhandle, '__isset', [$name]); + } + + public function __unset($name): void + { + \ShirabeRpcRuntime::callRust($this->__rhandle, '__unset', [$name]); + } PHP; private Project $project; @@ -202,20 +210,13 @@ final class Generator $isRoot = true; } - $hasPublicInstanceProperties = false; + // Instance properties are entity state, so the stub declares none of them. $staticProperties = []; foreach ($class->getProperties() as $property) { - if (!$property->isPublic()) { - continue; - } - if ($property->isStatic()) { + if ($property->isPublic() && $property->isStatic()) { // A public static property reads no instance state; its real declaration is // materialized so it lives locally in the worker, like static methods. $staticProperties[] = $file->verbatim($property->getStartLine(), $property->getEndLine()); - } else { - // Instance properties are entity state: the stub declares none and lets the - // __get/__set forwarders below carry every access to the Rust side. - $hasPublicInstanceProperties = true; } } @@ -331,6 +332,7 @@ final class Generator $members = []; if ($isRoot) { $members[] = self::BOILERPLATE; + $members[] = self::PROPERTY_FORWARDERS; } if ($isRoot || $constructor !== null) { $members[] = $this->renderConstructor($fqcn, $constructor, $file); @@ -341,9 +343,6 @@ final class Generator if ($staticProperties !== []) { $members[] = implode("\n", $staticProperties); } - if ($hasPublicInstanceProperties) { - $members[] = self::PROPERTY_FORWARDERS; - } $members = array_merge($members, $staticMethods, $methodTexts); $body = implode("\n\n", $members); -- cgit v1.3.1