• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

brick / reflection / 18876859539

28 Oct 2025 01:43PM UTC coverage: 80.296% (+0.4%) from 79.856%
18876859539

push

github

BenMorel
Apply ECS & fix Psalm issues in TokenParser

9 of 14 new or added lines in 1 file covered. (64.29%)

163 of 203 relevant lines covered (80.3%)

7.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.25
/src/Internal/TokenParser.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Brick\Reflection\Internal;
6

7
use function array_merge;
8
use function count;
9
use function explode;
10
use function strtolower;
11
use function token_get_all;
12

13
use const PHP_VERSION_ID;
14
use const T_AS;
15
use const T_COMMENT;
16
use const T_DOC_COMMENT;
17
use const T_NAME_FULLY_QUALIFIED;
18
use const T_NAME_QUALIFIED;
19
use const T_NAMESPACE;
20
use const T_NS_SEPARATOR;
21
use const T_STRING;
22
use const T_USE;
23
use const T_WHITESPACE;
24

25
/**
26
 * Parses a file for namespaces/use/class declarations.
27
 *
28
 * Imported from the now abandoned Doctrine Annotations library:
29
 * https://github.com/doctrine/annotations/blob/2.0.2/lib/Doctrine/Common/Annotations/TokenParser.php
30
 *
31
 * @psalm-type Token = array{0: int, 1: string, 2: int}|string
32
 */
33
final class TokenParser
34
{
35
    /**
36
     * The token list.
37
     *
38
     * @var list<Token>
39
     */
40
    private array $tokens;
41

42
    /**
43
     * The number of tokens.
44
     */
45
    private int $numTokens;
46

47
    /**
48
     * The current array pointer.
49
     */
50
    private int $pointer = 0;
51

52
    public function __construct(string $contents)
53
    {
54
        $this->tokens = token_get_all($contents);
4✔
55

56
        // The PHP parser sets internal compiler globals for certain things. Annoyingly, the last docblock comment it
57
        // saw gets stored in doc_comment. When it comes to compile the next thing to be include()d this stored
58
        // doc_comment becomes owned by the first thing the compiler sees in the file that it considers might have a
59
        // docblock. If the first thing in the file is a class without a doc block this would cause calls to
60
        // getDocBlock() on said class to return our long lost doc_comment. Argh.
61
        // To workaround, cause the parser to parse an empty docblock. Sure getDocBlock() will return this, but at least
62
        // it's harmless to us.
63
        token_get_all("<?php\n/**\n *\n */");
4✔
64

65
        $this->numTokens = count($this->tokens);
4✔
66
    }
67

68
    /**
69
     * Gets the next non whitespace and non comment token.
70
     *
71
     * @param bool $docCommentIsComment If TRUE then a doc comment is considered a comment and skipped.
72
     *                                  If FALSE then only whitespace and normal comments are skipped.
73
     *
74
     * @return Token|null The token if exists, null otherwise.
75
     */
76
    public function next(bool $docCommentIsComment = true): null|array|string
77
    {
78
        for ($i = $this->pointer; $i < $this->numTokens; $i++) {
4✔
79
            $this->pointer++;
4✔
80
            if (
81
                $this->tokens[$i][0] === T_WHITESPACE ||
4✔
82
                $this->tokens[$i][0] === T_COMMENT ||
4✔
83
                ($docCommentIsComment && $this->tokens[$i][0] === T_DOC_COMMENT)
4✔
84
            ) {
85
                continue;
4✔
86
            }
87

88
            return $this->tokens[$i];
4✔
89
        }
90

91
        return null;
4✔
92
    }
93

94
    /**
95
     * Parses a single use statement.
96
     *
97
     * @return array<string, string> A list with all found class names for a use statement.
98
     */
99
    public function parseUseStatement(): array
100
    {
101
        $groupRoot = '';
4✔
102
        $class = '';
4✔
103
        $alias = '';
4✔
104
        $statements = [];
4✔
105
        $explicitAlias = false;
4✔
106
        while (($token = $this->next())) {
4✔
107
            if (! $explicitAlias && $token[0] === T_STRING) {
4✔
108
                $class .= $token[1];
4✔
109
                $alias = $token[1];
4✔
110
            } elseif ($explicitAlias && $token[0] === T_STRING) {
4✔
111
                $alias = $token[1];
4✔
112
            } elseif (
113
                PHP_VERSION_ID >= 80000 &&
4✔
114
                ($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED)
4✔
115
            ) {
116
                $class .= $token[1];
4✔
117

118
                $classSplit = explode('\\', $token[1]);
4✔
119
                $alias = $classSplit[count($classSplit) - 1];
4✔
120
            } elseif ($token[0] === T_NS_SEPARATOR) {
4✔
121
                $class .= '\\';
×
NEW
122
                $alias = '';
×
123
            } elseif ($token[0] === T_AS) {
4✔
124
                $explicitAlias = true;
4✔
125
                $alias = '';
4✔
126
            } elseif ($token === ',') {
4✔
127
                $statements[strtolower($alias)] = $groupRoot . $class;
×
NEW
128
                $class = '';
×
NEW
129
                $alias = '';
×
NEW
130
                $explicitAlias = false;
×
131
            } elseif ($token === ';') {
4✔
132
                $statements[strtolower($alias)] = $groupRoot . $class;
4✔
133

134
                break;
4✔
135
            } elseif ($token === '{') {
×
136
                $groupRoot = $class;
×
NEW
137
                $class = '';
×
138
            } elseif ($token === '}') {
×
139
                continue;
×
140
            } else {
141
                break;
×
142
            }
143
        }
144

145
        return $statements;
4✔
146
    }
147

148
    /**
149
     * Gets all use statements.
150
     *
151
     * @param string $namespaceName The namespace name of the reflected class.
152
     *
153
     * @return array<string, string> A list with all found use statements.
154
     */
155
    public function parseUseStatements(string $namespaceName): array
156
    {
157
        $statements = [];
4✔
158
        while (($token = $this->next())) {
4✔
159
            if ($token[0] === T_USE) {
4✔
160
                $statements = array_merge($statements, $this->parseUseStatement());
4✔
161

162
                continue;
4✔
163
            }
164

165
            if ($token[0] !== T_NAMESPACE || $this->parseNamespace() !== $namespaceName) {
4✔
166
                continue;
4✔
167
            }
168

169
            // Get fresh array for new namespace. This is to prevent the parser to collect the use statements
170
            // for a previous namespace with the same name. This is the case if a namespace is defined twice
171
            // or if a namespace with the same name is commented out.
172
            $statements = [];
4✔
173
        }
174

175
        return $statements;
4✔
176
    }
177

178
    /**
179
     * Gets the namespace.
180
     *
181
     * @return string The found namespace.
182
     */
183
    public function parseNamespace(): string
184
    {
185
        $name = '';
4✔
186
        while (
187
            ($token = $this->next()) && ($token[0] === T_STRING || $token[0] === T_NS_SEPARATOR || (
4✔
188
                PHP_VERSION_ID >= 80000 &&
4✔
189
                    ($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED)
4✔
190
            ))
4✔
191
        ) {
192
            $name .= $token[1];
4✔
193
        }
194

195
        return $name;
4✔
196
    }
197
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc