diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-05 03:58:03 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-05 03:58:03 +0900 |
| commit | 9b444a9a879b75a6af3d3c7ba8b9a4294574c3ec (patch) | |
| tree | b1041b55bc3ed36a391370cc2f5ececc8cb386b2 /scripts/plugin-stub-generator/src | |
| parent | be3458128ef09b3d1074b134f2822162e077e165 (diff) | |
| download | php-shirabe-9b444a9a879b75a6af3d3c7ba8b9a4294574c3ec.tar.gz php-shirabe-9b444a9a879b75a6af3d3c7ba8b9a4294574c3ec.tar.zst php-shirabe-9b444a9a879b75a6af3d3c7ba8b9a4294574c3ec.zip | |
feat(plugin): run plugin-provided commands in a worker-side application
A same-FQCN Composer\Console\Application, hand-written under the new
php/runtime/ tree, hosts CommandProvider commands inside the PHP worker:
PhpCommandProxy overrides run() and forwards the stringified input, so the
real Symfony machinery binds, validates and executes against the live
command object, while help/list render Rust-side from a definition read
back at construction. Reverse \Shirabe\RustCommandStub rows let a plugin
command invoke built-in commands back in the Rust process, keeping every
command on the side whose helper set it was written for.
Composer\EventDispatcher\Event moves from a generated stub to a dual-mode
runtime class: the real BaseCommand::initialize constructs a
PreCommandRunEvent natively in the worker, which a proxy-only constructor
guard rejected. Its PRE_COMMAND_RUN dispatch reaches a new EventDispatcher
stub whose dispatch supports the observably-no-op no-listener case and
fails explicitly otherwise. The stub generator now accepts runtime-provided
classes as stub bases (never as targets) and cross-checks the Application
handoff property table against the real class.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat (limited to 'scripts/plugin-stub-generator/src')
| -rw-r--r-- | scripts/plugin-stub-generator/src/Generator.php | 67 |
1 files changed, 64 insertions, 3 deletions
diff --git a/scripts/plugin-stub-generator/src/Generator.php b/scripts/plugin-stub-generator/src/Generator.php index b7cfc859..1e41eed8 100644 --- a/scripts/plugin-stub-generator/src/Generator.php +++ b/scripts/plugin-stub-generator/src/Generator.php @@ -93,22 +93,40 @@ final class Generator */ private array $surfaces = []; - /** @param list<string> $targets */ + /** @var array<string, true> */ + private array $runtimeSet = []; + + /** + * @param list<string> $targets + * @param list<string> $runtimeProvided FQCNs of the hand-written dual-mode classes under + * php/runtime/; they may serve as stub base classes + * but must never be generation targets themselves + */ public function __construct( string $composerRoot, private readonly Report $report, private readonly array $targets, + private readonly array $runtimeProvided = [], ) { $this->project = new Project($composerRoot); $this->printer = new NamePrinter(); foreach ($targets as $fqcn) { $this->targetSet[$fqcn] = true; } + foreach ($runtimeProvided as $fqcn) { + $this->runtimeSet[$fqcn] = true; + } } /** @return array<string, string> relative stub path => file content */ public function generate(): array { + foreach ($this->runtimeProvided as $fqcn) { + if (isset($this->targetSet[$fqcn])) { + $this->errors[] = "$fqcn is both a stub target and provided by php/runtime/;" + . ' the runtime definition would be shadowed by the generated stub'; + } + } $files = []; foreach ($this->targets as $fqcn) { $files[str_replace('\\', '/', $fqcn) . '.php'] = $this->emitClass($fqcn); @@ -119,6 +137,45 @@ final class Generator return $files; } + /** + * The stub surface a runtime-provided (hand-written, dual-mode) base class exposes, + * computed from the real Composer class the runtime file mirrors, so a generated subclass + * stub can omit the methods it inherits — the same records emitClass builds for generated + * parents. + * + * @return array<string, list<array{string, bool, string, bool, bool}>> + */ + private function surfaceFromRealClass(string $fqcn): array + { + $file = $this->project->sourceFor($fqcn); + $class = $file->classLike; + if (!$class instanceof Class_) { + $this->errors[] = "$fqcn is not a class"; + return []; + } + $surface = []; + $parentFqcn = $class->extends === null ? null : $this->resolvedName($class->extends); + if ($parentFqcn !== null) { + $surface = $this->surfaces[$parentFqcn] ?? $this->surfaceFromRealClass($parentFqcn); + } + foreach ($this->interfaceClosure($class) as $interface) { + foreach ($interface->classLike->getMethods() as $method) { + if ($method->isStatic()) { + continue; + } + $surface[$method->name->toString()] ??= $this->fingerprint($method, $file); + } + } + foreach ($class->getMethods() as $method) { + $name = $method->name->toString(); + if ($method->isStatic() || !$method->isPublic() || str_starts_with($name, '__')) { + continue; + } + $surface[$name] = $this->fingerprint($method, $file); + } + return $surface; + } + private function emitClass(string $fqcn): string { $file = $this->project->sourceFor($fqcn); @@ -136,8 +193,12 @@ final class Generator $parentFqcn = $class->extends === null ? null : $this->resolvedName($class->extends); $isRoot = $parentFqcn === null; if ($parentFqcn !== null && !isset($this->targetSet[$parentFqcn])) { - $this->errors[] = "$fqcn extends $parentFqcn, which is not a stub target"; - $isRoot = true; + if (isset($this->runtimeSet[$parentFqcn])) { + $this->surfaces[$parentFqcn] ??= $this->surfaceFromRealClass($parentFqcn); + } else { + $this->errors[] = "$fqcn extends $parentFqcn, which is not a stub target"; + $isRoot = true; + } } if (!$isRoot && !isset($this->surfaces[$parentFqcn])) { $this->errors[] = "$fqcn must come after its base class $parentFqcn in targets.list"; |
