aboutsummaryrefslogtreecommitdiffhomepage
path: root/scripts/plugin-stub-generator/src/SourceFile.php
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/plugin-stub-generator/src/SourceFile.php')
-rw-r--r--scripts/plugin-stub-generator/src/SourceFile.php86
1 files changed, 86 insertions, 0 deletions
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));
+ }
+}