aboutsummaryrefslogtreecommitdiffhomepage
path: root/scripts/plugin-stub-generator
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-04 05:09:04 +0900
committernsfisis <nsfisis@gmail.com>2026-08-04 05:43:32 +0900
commit80631930a6ca7b64d96a89de61c8b63f302161a8 (patch)
treedc5acf4a8dbe74b576ec7826206defa0e3f8fba1 /scripts/plugin-stub-generator
parent850b67c048bbf80d9739520d950f4dd1ed4190a5 (diff)
downloadphp-shirabe-80631930a6ca7b64d96a89de61c8b63f302161a8.tar.gz
php-shirabe-80631930a6ca7b64d96a89de61c8b63f302161a8.tar.zst
php-shirabe-80631930a6ca7b64d96a89de61c8b63f302161a8.zip
feat(plugin): generate proxy stubs for the package and repository graph
Emit the eleven stubs a subscriber plugin's object-graph calls reach (BasePackage through RootPackage, the installed-repository hierarchy, RepositoryManager, InstallationManager). The generator learns the member kinds these classes need: builtin interfaces contribute no closure entries, public static properties are materialized, public instance properties forward through __get/__set, __toString forwards like a plain method, __clone throws (proxy clone semantics are still an open design question), and a subclass stub may add interfaces to its inherited surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat (limited to 'scripts/plugin-stub-generator')
-rw-r--r--scripts/plugin-stub-generator/src/Generator.php83
-rw-r--r--scripts/plugin-stub-generator/targets.list11
2 files changed, 88 insertions, 6 deletions
diff --git a/scripts/plugin-stub-generator/src/Generator.php b/scripts/plugin-stub-generator/src/Generator.php
index 9ab16ed8..b7cfc859 100644
--- a/scripts/plugin-stub-generator/src/Generator.php
+++ b/scripts/plugin-stub-generator/src/Generator.php
@@ -52,6 +52,28 @@ 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]);
+ }
+
+ public function __set($name, $value): void
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, '__set', [$name, $value]);
+ }
+ PHP;
+
+ private const CLONE_THROW = <<<'PHP'
+ public function __clone()
+ {
+ // Cloning a proxy is an open design question; fail instead of silently sharing
+ // the Rust-side entity between two stub instances.
+ throw new \RuntimeException('Shirabe does not support cloning ' . static::class . ' inside the plugin process yet');
+ }
+ PHP;
+
private Project $project;
private NamePrinter $printer;
@@ -122,9 +144,20 @@ final class Generator
$isRoot = true;
}
+ $hasPublicInstanceProperties = false;
+ $staticProperties = [];
foreach ($class->getProperties() as $property) {
- if ($property->isPublic()) {
- $this->errors[] = "$fqcn declares a public property; a proxy stub cannot forward property access";
+ if (!$property->isPublic()) {
+ continue;
+ }
+ if ($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;
}
}
@@ -140,15 +173,25 @@ final class Generator
$publicStatics = [];
$nonPublicStatics = [];
$ownInstanceMethods = [];
+ $cloneThrows = false;
foreach ($class->getMethods() as $method) {
$name = $method->name->toString();
if ($name === '__construct') {
continue;
}
if (str_starts_with($name, '__')) {
- if ($method->isPublic()) {
+ if ($method->isPublic() && !in_array($name, ['__toString', '__clone'], true)) {
$this->errors[] = "$fqcn::$name: magic methods cannot be proxied";
}
+ // __toString is an ordinary zero-argument call under a magic name and is
+ // forwarded below; __clone semantics are an open design question and the
+ // emitted body throws instead of silently sharing the Rust handle.
+ if ($method->isPublic() && $name === '__toString') {
+ $ownInstanceMethods[$name] = $method;
+ }
+ if ($method->isPublic() && $name === '__clone') {
+ $cloneThrows = true;
+ }
continue;
}
if ($method->isStatic()) {
@@ -183,12 +226,24 @@ final class Generator
}
} else {
$surface = $this->surfaces[$parentFqcn];
- if ($class->implements !== []) {
- $this->errors[] = "$fqcn adds interfaces to a stub base class; this is not supported yet";
+ // Interfaces the subclass adds contribute the methods whose names are new
+ // relative to the inherited stub surface.
+ foreach ($this->interfaceClosure($class) as $interface) {
+ foreach ($interface->classLike->getMethods() as $method) {
+ $name = $method->name->toString();
+ if ($method->isStatic()) {
+ $this->errors[] = "$fqcn: static interface method $name is not supported";
+ continue;
+ }
+ if (!isset($surface[$name])) {
+ $emitted[$name] ??= $method;
+ }
+ }
}
foreach ($ownInstanceMethods as $name => $method) {
if (isset($surface[$name])) {
$this->checkOmittedOverride($fqcn, $name, $method, $file, $surface[$name]);
+ unset($emitted[$name]);
} else {
$emitted[$name] = $method;
}
@@ -206,9 +261,11 @@ final class Generator
if ($class->extends !== null) {
$decl .= ' extends ' . $this->printer->renderName($class->extends, $file);
}
+ $interfaces = array_map(fn (Name $n): string => $this->printer->renderName($n, $file), $class->implements);
if ($isRoot) {
- $interfaces = array_map(fn (Name $n): string => $this->printer->renderName($n, $file), $class->implements);
$interfaces[] = '\ShirabeRustStub';
+ }
+ if ($interfaces !== []) {
$decl .= ' implements ' . implode(', ', $interfaces);
}
@@ -219,6 +276,15 @@ final class Generator
if ($constants !== []) {
$members[] = implode("\n", $constants);
}
+ if ($staticProperties !== []) {
+ $members[] = implode("\n", $staticProperties);
+ }
+ if ($hasPublicInstanceProperties) {
+ $members[] = self::PROPERTY_FORWARDERS;
+ }
+ if ($cloneThrows) {
+ $members[] = self::CLONE_THROW;
+ }
$members = array_merge($members, $staticMethods, $methodTexts);
$body = implode("\n\n", $members);
@@ -273,6 +339,11 @@ final class Generator
return;
}
$seen[$fqcn] = true;
+ if (interface_exists($fqcn, false) && (new \ReflectionClass($fqcn))->isInternal()) {
+ // A PHP builtin interface (Countable, Stringable, ...) has no source file to
+ // read; the class's own public methods already cover its surface.
+ return;
+ }
$file = $this->project->sourceFor($fqcn);
if (!$file->classLike instanceof Interface_) {
$this->errors[] = "$fqcn is implemented as an interface but is not one";
diff --git a/scripts/plugin-stub-generator/targets.list b/scripts/plugin-stub-generator/targets.list
index 42794e13..59656c6a 100644
--- a/scripts/plugin-stub-generator/targets.list
+++ b/scripts/plugin-stub-generator/targets.list
@@ -10,3 +10,14 @@ Composer\IO\BaseIO
Composer\IO\ConsoleIO
Composer\IO\BufferIO
Composer\IO\NullIO
+Composer\Package\BasePackage
+Composer\Package\Package
+Composer\Package\CompletePackage
+Composer\Package\RootPackage
+Composer\Repository\ArrayRepository
+Composer\Repository\WritableArrayRepository
+Composer\Repository\InstalledArrayRepository
+Composer\Repository\FilesystemRepository
+Composer\Repository\InstalledFilesystemRepository
+Composer\Repository\RepositoryManager
+Composer\Installer\InstallationManager