aboutsummaryrefslogtreecommitdiffhomepage
path: root/scripts/plugin-stub-generator/src
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/plugin-stub-generator/src')
-rw-r--r--scripts/plugin-stub-generator/src/GenerationError.php14
-rw-r--r--scripts/plugin-stub-generator/src/Generator.php383
-rw-r--r--scripts/plugin-stub-generator/src/NamePrinter.php92
-rw-r--r--scripts/plugin-stub-generator/src/Project.php59
-rw-r--r--scripts/plugin-stub-generator/src/Report.php37
-rw-r--r--scripts/plugin-stub-generator/src/SourceFile.php86
6 files changed, 671 insertions, 0 deletions
diff --git a/scripts/plugin-stub-generator/src/GenerationError.php b/scripts/plugin-stub-generator/src/GenerationError.php
new file mode 100644
index 00000000..c85bede4
--- /dev/null
+++ b/scripts/plugin-stub-generator/src/GenerationError.php
@@ -0,0 +1,14 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shirabe\PluginStubGenerator;
+
+final class GenerationError extends \RuntimeException
+{
+ /** @param list<string> $errors */
+ public function __construct(public readonly array $errors)
+ {
+ parent::__construct(implode("\n", $errors));
+ }
+}
diff --git a/scripts/plugin-stub-generator/src/Generator.php b/scripts/plugin-stub-generator/src/Generator.php
new file mode 100644
index 00000000..9ab16ed8
--- /dev/null
+++ b/scripts/plugin-stub-generator/src/Generator.php
@@ -0,0 +1,383 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shirabe\PluginStubGenerator;
+
+use PhpParser\Node\Name;
+use PhpParser\Node\Stmt\Class_;
+use PhpParser\Node\Stmt\ClassMethod;
+use PhpParser\Node\Stmt\Interface_;
+
+/**
+ * Emits the proxy stub files deterministically from the Composer sources and the classifier
+ * report. Anything the emitter cannot faithfully proxy (by-ref or variadic parameters,
+ * magic methods, public properties, non-public constants) fails generation instead of
+ * degrading silently.
+ */
+final class Generator
+{
+ private const BOILERPLATE = <<<'PHP'
+ /** @var int */
+ protected $__rhandle;
+ /** @var int */
+ protected $__epoch;
+
+ public function __construct(int $rhandle = 0, int $epoch = 0)
+ {
+ if (func_num_args() < 2) {
+ // Constructing the class from plugin code (a common idiom for e.g. `new BufferIO()`)
+ // is an open question of the plugin design; only proxy instantiation passes a
+ // Rust handle. Fail with a diagnosable message instead of an ArgumentCountError.
+ throw new \RuntimeException(
+ 'Shirabe does not support constructing ' . static::class . ' inside the plugin process yet'
+ );
+ }
+ $this->__rhandle = $rhandle;
+ $this->__epoch = $epoch;
+ }
+
+ public function __destruct()
+ {
+ \ShirabeRustObjectRegistry::release($this->__rhandle);
+ }
+
+ public function __shirabeRustHandleDescriptor(): array
+ {
+ return [
+ '__rhandle' => $this->__rhandle,
+ '__class' => static::class,
+ '__epoch' => $this->__epoch,
+ ];
+ }
+ PHP;
+
+ private Project $project;
+
+ private NamePrinter $printer;
+
+ /** @var list<string> */
+ private array $errors = [];
+
+ /** @var array<string, true> */
+ private array $targetSet = [];
+
+ /**
+ * Emitted method records per stub class: fqcn => name => fingerprint. A subclass stub
+ * omits overrides whose parameter list matches the record; the record therefore doubles
+ * as the divergence detector for omitted overrides.
+ *
+ * @var array<string, array<string, list<array{string, bool, string, bool, bool}>>>
+ */
+ private array $surfaces = [];
+
+ /** @param list<string> $targets */
+ public function __construct(
+ string $composerRoot,
+ private readonly Report $report,
+ private readonly array $targets,
+ ) {
+ $this->project = new Project($composerRoot);
+ $this->printer = new NamePrinter();
+ foreach ($targets as $fqcn) {
+ $this->targetSet[$fqcn] = true;
+ }
+ }
+
+ /** @return array<string, string> relative stub 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 '';
+ }
+ $category = $this->report->category($fqcn);
+ if (!in_array($category, ['rust-proxy', 'contract'], true)) {
+ $this->errors[] = "$fqcn is classified as " . ($category ?? 'nothing')
+ . '; only rust-proxy and contract classes can become proxy stubs';
+ }
+
+ $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 (!$isRoot && !isset($this->surfaces[$parentFqcn])) {
+ $this->errors[] = "$fqcn must come after its base class $parentFqcn in targets.list";
+ $isRoot = true;
+ }
+
+ foreach ($class->getProperties() as $property) {
+ if ($property->isPublic()) {
+ $this->errors[] = "$fqcn declares a public property; a proxy stub cannot forward property access";
+ }
+ }
+
+ $constants = [];
+ foreach ($class->getConstants() as $constant) {
+ if (!$constant->isPublic()) {
+ $this->errors[] = "$fqcn declares a non-public constant; materializing it is not supported";
+ continue;
+ }
+ $constants[] = $file->verbatim($constant->getStartLine(), $constant->getEndLine());
+ }
+
+ $publicStatics = [];
+ $nonPublicStatics = [];
+ $ownInstanceMethods = [];
+ foreach ($class->getMethods() as $method) {
+ $name = $method->name->toString();
+ if ($name === '__construct') {
+ continue;
+ }
+ if (str_starts_with($name, '__')) {
+ if ($method->isPublic()) {
+ $this->errors[] = "$fqcn::$name: magic methods cannot be proxied";
+ }
+ continue;
+ }
+ if ($method->isStatic()) {
+ if ($method->isPublic()) {
+ $publicStatics[$name] = $method;
+ } else {
+ $nonPublicStatics[$name] = $method;
+ }
+ } elseif ($method->isPublic()) {
+ $ownInstanceMethods[$name] = $method;
+ }
+ }
+ $staticMethods = $this->materializeStatics($file, $publicStatics, $nonPublicStatics);
+
+ $surface = [];
+ $emitted = [];
+ if ($isRoot) {
+ 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;
+ }
+ $emitted[$name] ??= $method;
+ }
+ }
+ // A concrete redeclaration wins over the interface signature (it may widen
+ // defaults); it keeps the interface's position in the emission order.
+ foreach ($ownInstanceMethods as $name => $method) {
+ $emitted[$name] = $method;
+ }
+ } else {
+ $surface = $this->surfaces[$parentFqcn];
+ if ($class->implements !== []) {
+ $this->errors[] = "$fqcn adds interfaces to a stub base class; this is not supported yet";
+ }
+ foreach ($ownInstanceMethods as $name => $method) {
+ if (isset($surface[$name])) {
+ $this->checkOmittedOverride($fqcn, $name, $method, $file, $surface[$name]);
+ } else {
+ $emitted[$name] = $method;
+ }
+ }
+ }
+
+ $methodTexts = [];
+ foreach ($emitted as $name => $method) {
+ $methodTexts[] = $this->renderProxyMethod($fqcn, $name, $method, $file);
+ $surface[$name] = $this->fingerprint($method, $file);
+ }
+ $this->surfaces[$fqcn] = $surface;
+
+ $decl = ($class->isAbstract() ? 'abstract ' : '') . 'class ' . $class->name?->toString();
+ if ($class->extends !== null) {
+ $decl .= ' extends ' . $this->printer->renderName($class->extends, $file);
+ }
+ if ($isRoot) {
+ $interfaces = array_map(fn (Name $n): string => $this->printer->renderName($n, $file), $class->implements);
+ $interfaces[] = '\ShirabeRustStub';
+ $decl .= ' implements ' . implode(', ', $interfaces);
+ }
+
+ $members = [];
+ if ($isRoot) {
+ $members[] = self::BOILERPLATE;
+ }
+ if ($constants !== []) {
+ $members[] = implode("\n", $constants);
+ }
+ $members = array_merge($members, $staticMethods, $methodTexts);
+ $body = implode("\n\n", $members);
+
+ $header = "// Generated by scripts/plugin-stub-generator; do not edit by hand.\n"
+ . "// Proxy stub for $fqcn: the public surface forwards to the Rust-side entity over RPC.";
+ $text = "<?php\n\n$header\n\nnamespace {$file->namespace};\n\n";
+ $uses = $this->usedImports($file, $decl . "\n" . $body);
+ if ($uses !== '') {
+ $text .= $uses . "\n\n";
+ }
+ return $text . $decl . "\n{\n" . ($body === '' ? '' : $body . "\n") . "}\n";
+ }
+
+ /**
+ * Static methods read no instance state; their real implementation is materialized
+ * verbatim so they run locally in the worker. Non-public static helpers they call
+ * (through `self::`, `static::` or the class name) are materialized along with them.
+ *
+ * @param array<string, ClassMethod> $publicStatics
+ * @param array<string, ClassMethod> $nonPublicStatics
+ * @return list<string>
+ */
+ private function materializeStatics(SourceFile $file, array $publicStatics, array $nonPublicStatics): array
+ {
+ $texts = [];
+ foreach ($publicStatics as $name => $method) {
+ $texts[$name] = $file->verbatim($method->getStartLine(), $method->getEndLine());
+ }
+ $scan = array_values($texts);
+ while ($scan !== []) {
+ $text = array_shift($scan);
+ foreach ($nonPublicStatics as $name => $method) {
+ if (isset($texts[$name])) {
+ continue;
+ }
+ $receiver = '(?:self|static|' . preg_quote($file->classLike->name?->toString() ?? '', '/') . ')';
+ if (preg_match('/(?<![\w$])' . $receiver . '::' . preg_quote($name, '/') . '\s*\(/', $text) === 1) {
+ $scan[] = $texts[$name] = $file->verbatim($method->getStartLine(), $method->getEndLine());
+ }
+ }
+ }
+ return array_values($texts);
+ }
+
+ /** Interfaces implemented by the class, each interface preceding the ones it extends. */
+ private function interfaceClosure(Class_ $class): array
+ {
+ $out = [];
+ $seen = [];
+ $visit = function (string $fqcn) use (&$visit, &$out, &$seen): void {
+ if (isset($seen[$fqcn])) {
+ return;
+ }
+ $seen[$fqcn] = true;
+ $file = $this->project->sourceFor($fqcn);
+ if (!$file->classLike instanceof Interface_) {
+ $this->errors[] = "$fqcn is implemented as an interface but is not one";
+ return;
+ }
+ $out[] = $file;
+ foreach ($file->classLike->extends as $parent) {
+ $visit($this->resolvedName($parent));
+ }
+ };
+ foreach ($class->implements as $interface) {
+ $visit($this->resolvedName($interface));
+ }
+ return $out;
+ }
+
+ private function renderProxyMethod(string $fqcn, string $name, ClassMethod $method, SourceFile $target): string
+ {
+ $params = [];
+ $args = [];
+ foreach ($method->params as $param) {
+ $paramName = $param->var->name;
+ if ($param->byRef) {
+ $this->errors[] = "$fqcn::$name: by-ref parameter \$$paramName cannot be proxied yet";
+ }
+ if ($param->variadic) {
+ $this->errors[] = "$fqcn::$name: variadic parameter \$$paramName cannot be proxied yet";
+ }
+ $rendered = '';
+ if ($param->type !== null) {
+ $rendered = $this->printer->renderType($param->type, $target) . ' ';
+ }
+ $rendered .= '$' . $paramName;
+ if ($param->default !== null) {
+ $rendered .= ' = ' . $this->printer->renderExpr($param->default, $target);
+ }
+ $params[] = $rendered;
+ $args[] = '$' . $paramName;
+ }
+
+ $returnType = $this->printer->renderType($method->returnType, $target);
+ $signature = "public function $name(" . implode(', ', $params) . ')'
+ . ($returnType === '' ? '' : ": $returnType");
+ $call = "\\ShirabeRpcRuntime::callRust(\$this->__rhandle, '$name', [" . implode(', ', $args) . '])';
+ // `self`/`static` returns are fluent interfaces; the local stub itself is returned to
+ // preserve identity instead of round-tripping the handle.
+ $body = match ($returnType) {
+ 'void' => " $call;",
+ 'self', 'static' => " $call;\n return \$this;",
+ default => " return $call;",
+ };
+ return " $signature\n {\n$body\n }";
+ }
+
+ /**
+ * An override that is omitted from a subclass stub must take exactly the parameters the
+ * inherited stub method declares: same names, arity, defaults and passing modes. Type
+ * declarations are allowed to differ (overrides may widen them; the forwarding body is
+ * type-agnostic either way).
+ */
+ private function checkOmittedOverride(
+ string $fqcn,
+ string $name,
+ ClassMethod $method,
+ SourceFile $file,
+ array $inherited,
+ ): void {
+ if ($this->fingerprint($method, $file) !== $inherited) {
+ $this->errors[] = "$fqcn::$name: override diverges from the inherited stub signature"
+ . ' (names, arity, defaults or passing modes); it can no longer be omitted';
+ }
+ }
+
+ /** @return list<array{string, bool, string, bool, bool}> */
+ private function fingerprint(ClassMethod $method, SourceFile $file): array
+ {
+ $fingerprint = [];
+ foreach ($method->params as $param) {
+ $fingerprint[] = [
+ $param->var->name,
+ $param->default !== null,
+ $param->default === null ? '' : $this->printer->renderExpr($param->default, $file),
+ $param->byRef,
+ $param->variadic,
+ ];
+ }
+ return $fingerprint;
+ }
+
+ /** The original file's imports, restricted to names the emitted stub actually uses. */
+ private function usedImports(SourceFile $file, string $emittedText): string
+ {
+ $kept = [];
+ foreach ($file->aliases as $alias => $fqcn) {
+ if (preg_match('/(?<![\\\\$\w])' . preg_quote($alias, '/') . '\b/', $emittedText) === 1) {
+ $kept[] = 'use ' . $fqcn
+ . (str_ends_with($fqcn, '\\' . $alias) || $fqcn === $alias ? '' : " as $alias") . ';';
+ }
+ }
+ return implode("\n", $kept);
+ }
+
+ private function resolvedName(Name $name): string
+ {
+ $resolved = $name->getAttribute('resolvedName');
+ return $resolved instanceof Name ? $resolved->toString() : $name->toString();
+ }
+}
diff --git a/scripts/plugin-stub-generator/src/NamePrinter.php b/scripts/plugin-stub-generator/src/NamePrinter.php
new file mode 100644
index 00000000..1675fee1
--- /dev/null
+++ b/scripts/plugin-stub-generator/src/NamePrinter.php
@@ -0,0 +1,92 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shirabe\PluginStubGenerator;
+
+use PhpParser\Node;
+use PhpParser\Node\Identifier;
+use PhpParser\Node\IntersectionType;
+use PhpParser\Node\Name;
+use PhpParser\Node\NullableType;
+use PhpParser\Node\UnionType;
+use PhpParser\PrettyPrinter\Standard;
+
+/**
+ * Renders type and default-value nodes into a target file context: class names resolve to
+ * their FQCN and are re-spelled through the target's import table (alias if imported, bare
+ * name if in the stub's namespace, `\FQCN` otherwise). This lets signatures declared in one
+ * file (an interface) be emitted into a stub that carries another file's `use` block.
+ */
+final class NamePrinter extends Standard
+{
+ private SourceFile $context;
+
+ public function __construct()
+ {
+ parent::__construct(['shortArraySyntax' => true]);
+ }
+
+ public function renderType(?Node $type, SourceFile $context): string
+ {
+ if ($type === null) {
+ return '';
+ }
+ if ($type instanceof Identifier) {
+ return $type->toString();
+ }
+ if ($type instanceof NullableType) {
+ return '?' . $this->renderType($type->type, $context);
+ }
+ if ($type instanceof UnionType) {
+ return implode('|', array_map(fn (Node $t): string => $this->renderType($t, $context), $type->types));
+ }
+ if ($type instanceof IntersectionType) {
+ return implode('&', array_map(fn (Node $t): string => $this->renderType($t, $context), $type->types));
+ }
+ if ($type instanceof Name) {
+ return $this->renderName($type, $context);
+ }
+ throw new GenerationError(['unsupported type node ' . get_class($type)]);
+ }
+
+ public function renderName(Name $name, SourceFile $context): string
+ {
+ if ($name->isSpecialClassName()) {
+ return $name->toString();
+ }
+ $resolved = $name->getAttribute('resolvedName');
+ $fqcn = $resolved instanceof Name ? $resolved->toString() : $name->toString();
+ if ($name->isUnqualified() && !str_contains($fqcn, '\\')) {
+ // Names that resolved into the global namespace only occur in constant space
+ // (true, false, null, ...); they keep their source spelling.
+ return $name->toString();
+ }
+ return $this->renderFqcn($fqcn, $context);
+ }
+
+ public function renderFqcn(string $fqcn, SourceFile $context): string
+ {
+ foreach ($context->aliases as $alias => $target) {
+ if ($target === $fqcn) {
+ return $alias;
+ }
+ }
+ $prefix = $context->namespace === null ? '' : $context->namespace . '\\';
+ if ($prefix !== '' && str_starts_with($fqcn, $prefix) && !str_contains(substr($fqcn, strlen($prefix)), '\\')) {
+ return substr($fqcn, strlen($prefix));
+ }
+ return '\\' . $fqcn;
+ }
+
+ public function renderExpr(Node\Expr $expr, SourceFile $context): string
+ {
+ $this->context = $context;
+ return $this->prettyPrintExpr($expr);
+ }
+
+ protected function pName(Name $node): string
+ {
+ return $this->renderName($node, $this->context);
+ }
+}
diff --git a/scripts/plugin-stub-generator/src/Project.php b/scripts/plugin-stub-generator/src/Project.php
new file mode 100644
index 00000000..13f1067d
--- /dev/null
+++ b/scripts/plugin-stub-generator/src/Project.php
@@ -0,0 +1,59 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shirabe\PluginStubGenerator;
+
+use PhpParser\Parser;
+use PhpParser\ParserFactory;
+
+/**
+ * Locates and parses classes of the Composer checkout (composer/src and composer/vendor),
+ * using the checkout's own PSR-4 autoload map.
+ */
+final class Project
+{
+ private Parser $parser;
+
+ /** @var array<string, list<string>> namespace prefix => base dirs, longest prefix first */
+ private array $psr4;
+
+ /** @var array<string, SourceFile> */
+ private array $cache = [];
+
+ public function __construct(string $composerRoot)
+ {
+ $this->parser = (new ParserFactory())->createForNewestSupportedVersion();
+
+ $mapFile = $composerRoot . '/vendor/composer/autoload_psr4.php';
+ if (!is_file($mapFile)) {
+ throw new GenerationError(["missing $mapFile (run composer install in the checkout)"]);
+ }
+ $map = require $mapFile;
+ $map['Composer\\'] ??= [$composerRoot . '/src/Composer'];
+ uksort($map, static fn (string $a, string $b): int => strlen($b) <=> strlen($a));
+ $this->psr4 = $map;
+ }
+
+ public function sourceFor(string $fqcn): SourceFile
+ {
+ return $this->cache[$fqcn] ??= new SourceFile($this->fileFor($fqcn), $fqcn, $this->parser);
+ }
+
+ private function fileFor(string $fqcn): string
+ {
+ foreach ($this->psr4 as $prefix => $dirs) {
+ if (!str_starts_with($fqcn, $prefix)) {
+ continue;
+ }
+ $relative = str_replace('\\', '/', substr($fqcn, strlen($prefix))) . '.php';
+ foreach ($dirs as $dir) {
+ $path = $dir . '/' . $relative;
+ if (is_file($path)) {
+ return $path;
+ }
+ }
+ }
+ throw new GenerationError(["cannot locate a source file for $fqcn"]);
+ }
+}
diff --git a/scripts/plugin-stub-generator/src/Report.php b/scripts/plugin-stub-generator/src/Report.php
new file mode 100644
index 00000000..92e0565e
--- /dev/null
+++ b/scripts/plugin-stub-generator/src/Report.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shirabe\PluginStubGenerator;
+
+/** The classifier report (scripts/plugin-class-classifier/report.json). */
+final class Report
+{
+ /** @param array<string, string> $categories fqcn => category */
+ private function __construct(private readonly array $categories)
+ {
+ }
+
+ public static function load(string $path): self
+ {
+ if (!is_file($path)) {
+ throw new GenerationError([
+ "missing classifier report $path (run scripts/plugin-class-classifier/classify first)",
+ ]);
+ }
+ $data = json_decode((string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR);
+ if (($data['violations'] ?? null) !== []) {
+ throw new GenerationError(["$path records classification violations; fix the classifier lists first"]);
+ }
+ $categories = [];
+ foreach ($data['classes'] as $class) {
+ $categories[$class['fqcn']] = $class['category'];
+ }
+ return new self($categories);
+ }
+
+ public function category(string $fqcn): ?string
+ {
+ return $this->categories[$fqcn] ?? null;
+ }
+}
diff --git a/scripts/plugin-stub-generator/src/SourceFile.php b/scripts/plugin-stub-generator/src/SourceFile.php
new file mode 100644
index 00000000..771e83a8
--- /dev/null
+++ b/scripts/plugin-stub-generator/src/SourceFile.php
@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shirabe\PluginStubGenerator;
+
+use PhpParser\Node\Stmt\ClassLike;
+use PhpParser\Node\Stmt\GroupUse;
+use PhpParser\Node\Stmt\Namespace_;
+use PhpParser\Node\Stmt\Use_;
+use PhpParser\NodeTraverser;
+use PhpParser\NodeVisitor\NameResolver;
+use PhpParser\Parser;
+
+/**
+ * A parsed source file: the class-like it declares, its namespace and its import table.
+ * All Name nodes in the AST carry a `resolvedName` attribute (NameResolver with
+ * replaceNodes disabled), so signatures can be re-rendered under another file's context.
+ */
+final class SourceFile
+{
+ public ?string $namespace = null;
+
+ /** @var array<string, string> alias as written => FQCN, in declaration order */
+ public array $aliases = [];
+
+ public ClassLike $classLike;
+
+ /** @var list<string> */
+ public array $lines;
+
+ public function __construct(
+ public readonly string $path,
+ public readonly string $expectedFqcn,
+ Parser $parser,
+ ) {
+ $code = file_get_contents($path);
+ if ($code === false) {
+ throw new GenerationError(["cannot read $path"]);
+ }
+ $this->lines = explode("\n", $code);
+
+ $ast = $parser->parse($code);
+ if ($ast === null) {
+ throw new GenerationError(["cannot parse $path"]);
+ }
+ $traverser = new NodeTraverser();
+ $traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false]));
+ $ast = $traverser->traverse($ast);
+
+ $classLike = null;
+ foreach ($ast as $stmt) {
+ if (!$stmt instanceof Namespace_) {
+ continue;
+ }
+ $this->namespace = $stmt->name?->toString();
+ foreach ($stmt->stmts as $inner) {
+ if ($inner instanceof Use_) {
+ if ($inner->type !== Use_::TYPE_NORMAL) {
+ throw new GenerationError(["$path: function/const use statements are not supported"]);
+ }
+ foreach ($inner->uses as $use) {
+ $this->aliases[$use->getAlias()->toString()] = $use->name->toString();
+ }
+ } elseif ($inner instanceof GroupUse) {
+ throw new GenerationError(["$path: group use statements are not supported"]);
+ } elseif ($inner instanceof ClassLike) {
+ $fqcn = ($this->namespace === null ? '' : $this->namespace . '\\') . $inner->name?->toString();
+ if ($fqcn === $expectedFqcn) {
+ $classLike = $inner;
+ }
+ }
+ }
+ }
+ if ($classLike === null) {
+ throw new GenerationError(["$path does not declare $expectedFqcn"]);
+ }
+ $this->classLike = $classLike;
+ }
+
+ /** The declaration's verbatim source text, without any leading doc comment. */
+ public function verbatim(int $startLine, int $endLine): string
+ {
+ return implode("\n", array_slice($this->lines, $startLine - 1, $endLine - $startLine + 1));
+ }
+}