*/ private array $errors = []; /** @var array */ private array $guardSet = []; /** @var array */ private array $providedSet = []; /** @var list `Ancestor::member` entries the class being emitted cannot redeclare */ private array $inheritedFinals = []; /** * @param list $targets FQCNs to emit a guard for * @param list $provided FQCNs the worker resolves to a proxy stub or a hand-written * runtime class; a guard never reproduces their members, since * those declarations already shadow the real class */ public function __construct( private readonly Project $project, private readonly NamePrinter $printer, private readonly array $targets, array $provided, ) { foreach ($targets as $fqcn) { $this->guardSet[$fqcn] = true; } foreach ($provided as $fqcn) { $this->providedSet[$fqcn] = true; } } /** @return array relative guard path => file content */ public function generate(): array { $files = []; foreach ($this->targets as $fqcn) { $files[str_replace('\\', '/', $fqcn) . '.php'] = $this->emitClass($fqcn); } if ($this->errors !== []) { throw new GenerationError($this->errors); } return $files; } private function emitClass(string $fqcn): string { $file = $this->project->sourceFor($fqcn); $class = $file->classLike; if (!$class instanceof Class_) { $this->errors[] = "$fqcn is not a class"; return ''; } // Constants and public static properties are compile-time data with no instance behind // them; they are copied verbatim, visibility included, so that the references a guard is // meant to keep working (`Link::TYPE_REQUIRE` and friends) resolve to the real values. $data = []; foreach ($class->getConstants() as $constant) { $data[] = $file->verbatim($constant->getStartLine(), $constant->getEndLine()); } foreach ($class->getProperties() as $property) { if ($property->isPublic() && $property->isStatic()) { $data[] = $file->verbatim($property->getStartLine(), $property->getEndLine()); } } $methods = []; $this->inheritedFinals = []; $closure = $this->methodClosure($fqcn); if (!isset($closure['__construct'])) { // Nothing in the hierarchy declares one, so PHP would hand out the implicit // constructor. The guard declares its own to close that door. $methods[] = " public function __construct()\n {\n" . " \\ShirabeUnsupportedClass::fail(self::class, '__construct');\n }"; } foreach ($closure as $lowerName => [$method, $declaringFile]) { if ($method->isPrivate() || $lowerName === '__destruct') { // A private method is reachable only from the code the guard replaces, and a // destructor that throws would fire while an exception is already unwinding. continue; } $methods[] = $this->renderMethod($class, $method, $declaringFile); } $decl = ($class->isAbstract() ? 'abstract ' : '') . ($class->isFinal() ? 'final ' : '') . 'class ' . $class->name?->toString(); 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 ($interfaces !== []) { $decl .= ' implements ' . implode(', ', $interfaces); } $body = implode("\n\n", array_merge($data === [] ? [] : [implode("\n", $data)], $methods)); $header = "// Generated by scripts/plugin-stub-generator; do not edit by hand.\n" . "// Guard for $fqcn.\n" . "// The Rust side owns this class and the worker has no proxy for it, so this\n" . "// declaration shadows the real one: the constants and the hierarchy stay, while\n" . "// constructing it or calling anything on it raises an explicit error."; foreach ($this->inheritedFinals as $member) { $header .= "\n// $member() is final, so it keeps running the real implementation here."; } $text = "namespace};\n\n"; $uses = $file->importsUsedBy($decl . "\n" . $body); if ($uses !== '') { $text .= $uses . "\n\n"; } return $text . $decl . "\n{\n" . ($body === '' ? '' : $body . "\n") . "}\n"; } /** * Every method a guard must redeclare: its own (traits included), plus the ones it would * otherwise inherit from real ancestors. The walk stops at an ancestor that is guarded or * shadowed by a stub, since that declaration carries the members from there on up. * * @return array lowercased name => method and the * file its signature is written in */ private function methodClosure(string $fqcn): array { $file = $this->project->sourceFor($fqcn); $class = $file->classLike; $methods = []; foreach ($class->getMethods() as $method) { $methods[strtolower($method->name->toString())] ??= [$method, $file]; } foreach ($class->getTraitUses() as $traitUse) { foreach ($traitUse->traits as $trait) { foreach ($this->methodClosure(SourceFile::resolvedName($trait)) as $name => $entry) { $methods[$name] ??= $entry; } } } $parent = $class instanceof Class_ && $class->extends !== null ? SourceFile::resolvedName($class->extends) : null; // A PHP builtin ancestor (FilterIterator, ...) has no source file to read; its methods // work on internal state that only its own constructor sets up, which the guard blocks. $isInternal = $parent !== null && class_exists($parent, false) && (new \ReflectionClass($parent))->isInternal(); if ($parent !== null && !$isInternal && !isset($this->guardSet[$parent]) && !isset($this->providedSet[$parent])) { foreach ($this->methodClosure($parent) as $name => $entry) { if (isset($methods[$name])) { continue; } if ($entry[0]->isFinal()) { // PHP refuses the redeclaration, so this member keeps running the real // implementation; emitClass records it in the guard's header. $this->inheritedFinals[] = $parent . '::' . $entry[0]->name->toString(); continue; } $methods[$name] = $entry; } } return $methods; } private function renderMethod(Class_ $class, ClassMethod $method, SourceFile $target): string { $name = $method->name->toString(); $params = []; foreach ($method->params as $param) { $rendered = ''; if ($param->type !== null) { $rendered = $this->printer->renderType($param->type, $target) . ' '; } // A promoted constructor property declares state the guard has no use for; the // parameter itself is kept so the signature a caller sees does not change. $rendered .= ($param->byRef ? '&' : '') . ($param->variadic ? '...' : '') . '$' . $param->var->name; if ($param->default !== null) { $rendered .= ' = ' . $this->printer->renderExpr($param->default, $target); } $params[] = $rendered; } $abstract = $method->isAbstract() && $class->isAbstract(); $returnType = $this->printer->renderType($method->returnType, $target); $signature = ($abstract ? 'abstract ' : '') . ($method->isFinal() ? 'final ' : '') . ($method->isPublic() ? 'public' : 'protected') . ($method->isStatic() ? ' static' : '') . ' function ' . ($method->byRef ? '&' : '') . $name . '(' . implode(', ', $params) . ')' . ($returnType === '' ? '' : ": $returnType"); if ($abstract) { return " $signature;"; } return " $signature\n {\n" . " \\ShirabeUnsupportedClass::fail(self::class, '$name');\n }"; } }