1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
|
<?php
declare(strict_types=1);
namespace Shirabe\PluginStubGenerator;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
/**
* Emits the guard classes: same FQCN, same hierarchy and constants as the real Composer class,
* but every constructor and method raises an explicit error. They cover the classes whose entity
* the Rust side owns and that have no proxy stub, so the worker's autoloader can no longer fall
* through to the real implementation and run a second instance the Rust side never sees.
*
* A declaration-only reference (`instanceof`, `X::class`, a constant) keeps working; only running
* the code fails.
*/
final class GuardGenerator
{
/** @var list<string> */
private array $errors = [];
/** @var array<string, true> */
private array $guardSet = [];
/** @var array<string, true> */
private array $providedSet = [];
/** @var list<string> `Ancestor::member` entries the class being emitted cannot redeclare */
private array $inheritedFinals = [];
/**
* @param list<string> $targets FQCNs to emit a guard for
* @param list<string> $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<string, string> 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 = "<?php\n\n$header\n\nnamespace {$file->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<string, array{ClassMethod, SourceFile}> 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 }";
}
}
|