diff options
Diffstat (limited to 'scripts/plugin-stub-generator/src/SourceFile.php')
| -rw-r--r-- | scripts/plugin-stub-generator/src/SourceFile.php | 86 |
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)); + } +} |
