From 261516d5ce6f8b0d69cf9e3da7dd2f0ef1cdc36a Mon Sep 17 00:00:00 2001 From: nsfisis Date: Tue, 4 Aug 2026 03:36:10 +0900 Subject: feat(plugin): generate the worker proxy stubs from the Composer sources Replace the hand-written proxy stubs under crates/shirabe-php-rpc/php/stubs with output of scripts/plugin-stub-generator, a deterministic emitter that derives every stub from the Composer checkout and the classifier report. Anything it cannot faithfully proxy (by-ref/variadic parameters, magic methods, public properties, diverging omitted overrides, stale stub files, a STUB_FILES entry missing in lib.rs) fails generation instead of degrading silently, so future Composer releases surface new members as explicit errors rather than silent gaps. Regenerating the stubs also normalizes the hand-written inconsistencies (uniform guarded constructors, import-based type spellings) and fixes real gaps the review of the generated diff uncovered: the BaseIO authentication methods now carry the real class's untyped signatures, and the previously missing ConsoleIO::sanitize is materialized together with its private static helper. A cargo test runs generate-stubs --check to keep the committed stubs, the generator and the embedded list from drifting apart. Co-Authored-By: Claude Fable 5 --- docs/dev/php-rpc.md | 7 ++-- docs/dev/plugin-stub-generation.md | 86 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 docs/dev/plugin-stub-generation.md (limited to 'docs') diff --git a/docs/dev/php-rpc.md b/docs/dev/php-rpc.md index f8f42e90..a1093ec5 100644 --- a/docs/dev/php-rpc.md +++ b/docs/dev/php-rpc.md @@ -131,10 +131,11 @@ function; an unknown name is an explicit error. Notable internal helpers: ## Proxy stubs -`php/stubs/` holds hand-written proxy stub classes (`Composer\EventDispatcher\Event`, +`php/stubs/` holds the proxy stub classes (`Composer\EventDispatcher\Event`, `Composer\Script\Event`, `Composer\PartialComposer`, `Composer\Composer`, and the -`Composer\IO\{BaseIO,ConsoleIO,BufferIO,NullIO}` hierarchy), written in the shape the future -stub generator will output. They are autoloaded with highest priority so a proxied FQCN can +`Composer\IO\{BaseIO,ConsoleIO,BufferIO,NullIO}` hierarchy). They are generated by +`scripts/plugin-stub-generator/generate-stubs` and must not be edited by hand; see +`docs/dev/plugin-stub-generation.md`. They are autoloaded with highest priority so a proxied FQCN can never be shadowed by the real implementation; `__shirabe_require` restores that priority after loading code that prepends its own autoloader. Stubs are interned per rhandle (`WeakReference`-based registry) so identity (`===`) holds, and their destructors send diff --git a/docs/dev/plugin-stub-generation.md b/docs/dev/plugin-stub-generation.md new file mode 100644 index 00000000..1fb77ff4 --- /dev/null +++ b/docs/dev/plugin-stub-generation.md @@ -0,0 +1,86 @@ +# Plugin proxy stub generation + +## Purpose + +The plugin PHP worker resolves proxied Composer FQCNs to thin stub classes whose +public methods forward to the Rust-side entity over RPC (see +`docs/dev/php-rpc.md`, "Proxy stubs"). These stub files, committed under +`crates/shirabe-php-rpc/php/stubs/` and embedded into the binary by the +`STUB_FILES` list in `crates/shirabe-php-rpc/src/lib.rs`, are *generated* by +`scripts/plugin-stub-generator` — never edited by hand. Generating them from the +Composer sources keeps every stub signature mechanically faithful to the real +class and turns "the stub is missing something the real class has" into a +generation failure instead of silent breakage. + +## Running + +``` +cd scripts/plugin-stub-generator +composer install # once; vendor/ is git-ignored +./generate-stubs # rewrites crates/shirabe-php-rpc/php/stubs/ +./generate-stubs --check # verifies committed stubs are fresh; exit 1 otherwise +``` + +Inputs: + +* `targets.list` — the FQCNs to emit, stub base classes before their subclasses. + Growing the stub set means adding a line here and regenerating. +* the Composer checkout (`composer/`, override with `--composer-root=`); the + generator locates sources through the checkout's own PSR-4 autoload map, so + interfaces from vendor packages (e.g. `Psr\Log\LoggerInterface`) resolve too. +* the classifier report (`scripts/plugin-class-classifier/report.json`, override + with `--report=`). Run `scripts/plugin-class-classifier/classify` first; the + report is git-ignored. Every target must be classified `rust-proxy` or + `contract`, and a report with classification violations is rejected. + +The freshness check runs in `cargo test` as +`crates/shirabe-php-rpc/tests/generated_stubs.rs`; it returns early when PHP, +the generator's vendor directory or the classifier report is unavailable. + +## What the generator emits + +* **Root stubs** (targets whose parent class is not itself a target) carry the + proxy boilerplate: `__rhandle`/`__epoch` properties, a constructor that + accepts `(rhandle, epoch)` from proxy instantiation and throws a diagnosable + `RuntimeException` when plugin code tries to `new` the class directly, a + destructor releasing the Rust handle, and the wire descriptor helper. The + real `extends`/`implements` hierarchy is preserved and `\ShirabeRustStub` is + appended to the interface list. +* **Instance methods** forward via `\ShirabeRpcRuntime::callRust`. For a root + stub the emitted surface is the interface closure (each interface before the + ones it extends, methods in declaration order; a concrete redeclaration in + the class wins over the interface signature) followed by the class's own + remaining public methods. A subclass stub declares only the methods whose + *name* is new relative to the inherited stub surface; an omitted override + must match the inherited parameter list (names, arity, defaults, passing + modes — type declarations may differ), otherwise generation fails. +* Methods returning `self`/`static` perform the RPC and then `return $this;` + to preserve identity instead of round-tripping the handle. +* **Class constants and static methods** are materialized verbatim from the + real source (they read no instance state and run locally in the worker), + together with any non-public static helpers they call. +* **Imports**: the original file's `use` statements are kept in their original + order, restricted to names the emitted stub references; signatures declared + elsewhere (interface files) are re-spelled through that import table. + +## Coverage assertions + +Generation fails — instead of emitting something quietly wrong — on: + +* a target missing from the classifier report, classified other than + `rust-proxy`/`contract`, or a report carrying violations, +* public properties (a stub cannot forward property access), +* by-ref or variadic parameters, magic methods, static interface methods, +* an omitted override diverging from the inherited stub signature, +* a subclass target listed before its base class, or extending a class that is + not a target, +* non-public class constants (materializing them is unsupported so far). + +`generate-stubs` (in both modes) additionally fails when a `.php` file exists +under the stubs directory that no target produces, or when `STUB_FILES` in +`crates/shirabe-php-rpc/src/lib.rs` does not embed every generated file. + +When a future Composer release adds a public member the emitter cannot handle, +these assertions surface it at generation time; extending the emitter (or +deciding the porting policy) is then an explicit step, mirroring the +completeness stance of `docs/dev/plugin-class-classification.md`. -- cgit v1.3.1