aboutsummaryrefslogtreecommitdiffhomepage
path: root/scripts/plugin-stub-generator/src/SourceFile.php
blob: 771e83a8136690d99f01dbcbec1abe71654b2b40 (plain)
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
<?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));
    }
}