From b4ab3df2ec85fbe477d7721344a8cd3630b437a1 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 16 Aug 2026 13:59:28 +0900 Subject: feat(plugin): guard Rust-owned classes the worker has no proxy for The worker's autoloader fell through to the real Composer source for every Rust-owned FQCN without a proxy stub, so plugin code doing `new Filesystem()` or subclassing `LibraryInstaller` silently ran on a second instance the Rust side never sees. An unimplemented part of the plugin API has to fail with an explicit error naming it, not quietly work on a disconnected copy. The stub generator now emits a guard class for each of those FQCNs: the real declaration, hierarchy and constants, with every constructor and method raising an explicit error. References satisfied by the declaration alone (`instanceof`, `X::class`, `Link::TYPE_REQUIRE`) keep working. Two FQCNs stay resolvable to the real class, each listed with the worker-side mechanism that makes a natively constructed instance correct. The error had nowhere to go: `Installer::run` dropped the `Result` of both `dispatch_script` calls, so an exception from a listener ended in exit 0. Both propagate now, the way the exception does upstream. Three real-plugin E2E comparisons stop at a guard and are ignored, each naming the class it needs. Co-Authored-By: Claude Opus 5 (1M context) --- docs/dev/plugin-stub-generation.md | 52 +++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 7 deletions(-) (limited to 'docs/dev/plugin-stub-generation.md') diff --git a/docs/dev/plugin-stub-generation.md b/docs/dev/plugin-stub-generation.md index 68dde156..e2b6d848 100644 --- a/docs/dev/plugin-stub-generation.md +++ b/docs/dev/plugin-stub-generation.md @@ -1,4 +1,4 @@ -# Plugin proxy stub generation +# Plugin proxy stub and guard generation ## Purpose @@ -12,13 +12,21 @@ 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. +The same tool emits the **guard classes** under +`crates/shirabe-php-rpc/php/guards/`, one for every Rust-owned class no stub and +no `php/runtime/` class shadows. Without them the worker's autoloader falls +through to the real Composer implementation, and code running there works on a +second instance the Rust side never sees — silent breakage in place of the +explicit error an unimplemented plugin API is supposed to raise. `build.rs` +embeds the guards, building the list from the directory itself. + ## 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 +./generate-stubs # rewrites the stubs/ and guards/ directories +./generate-stubs --check # verifies committed files are fresh; exit 1 otherwise ``` Inputs: @@ -36,10 +44,19 @@ Inputs: 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. + report is git-ignored. Every stub target must be classified `rust-proxy` or + `contract`, and a report with classification violations is rejected. The guard + set is read from the report alone: every `rust-proxy`, `rust-snapshot` and + `unsupported` class that is not a stub target, a `php/runtime/` class or an + exemption. +* `guard-exemptions.list` — the Rust-owned FQCNs that stay resolvable to the real + Composer class, each with the worker-side mechanism that makes a native + instance correct (the materialized-value codec for `Composer\Package\Link`, + the dual-mode `Composer\EventDispatcher\Event` for + `Composer\Plugin\PreCommandRunEvent`). An entry that needs no guard anyway + fails the run. -The freshness check runs in `cargo test` as +The freshness check for both sets 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. @@ -93,6 +110,27 @@ the generator's vendor directory or the classifier report is unavailable. order, restricted to names the emitted stub references; signatures declared elsewhere (interface files) are re-spelled through that import table. +## What a guard looks like + +A guard reproduces the real class's declaration — `abstract`/`final`, `extends`, +`implements`, the constants and public static properties verbatim — so that +references satisfied by the declaration alone keep working: `instanceof`, +`catch`, `X::class`, `Link::TYPE_REQUIRE`. Everything executable raises +`ShirabeUnsupportedClass::fail()`, which names the class and the member: + +* the constructor (the class's own, or the one it would inherit; a class whose + hierarchy declares none gets a no-argument one, so the implicit constructor + cannot be reached either), +* every public and protected method the guard would otherwise inherit from real + code: its own, its traits', and those of ancestors up to the first one that is + itself guarded or shadowed by a stub. Private methods are reachable only from + the code the guard replaces, and a destructor that throws would fire during + unwinding, so neither is emitted. + +Abstract methods of an abstract guard stay abstract. A method an ancestor +declares `final` cannot be redeclared, so it keeps running the real +implementation; the guard's header names each one. + ## Coverage assertions Generation fails — instead of emitting something quietly wrong — on: @@ -107,7 +145,7 @@ Generation fails — instead of emitting something quietly wrong — on: * a target whose FQCN is also provided by `php/runtime/`. `generate-stubs` (in both modes) additionally fails when a `.php` file exists -under the stubs directory that no target produces, or when `STUB_FILES` / +under the stubs or guards directory that no target produces, or when `STUB_FILES` / `RUNTIME_FILES` in `crates/shirabe-php-rpc/src/lib.rs` does not embed every generated stub / runtime file. It also cross-checks the handoff property table for `Composer\Console\Application` (declared in `generate-stubs` itself) against -- cgit v1.3.1-4-g156e