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

voku / Simple-PHP-Code-Parser / 29085974904

10 Jul 2026 10:10AM UTC coverage: 85.103% (+0.3%) from 84.815%
29085974904

push

github

moellekenl
[+]: Add support for source-range information and enhance PHP model elements

- Introduced `endLine`, `startFilePos`, and `endFilePos` properties to model elements for precise source navigation.
- Added `PHPFileInfo` class for lightweight file summaries and improved AST access methods.
- Enhanced trait handling in class-like models to include trait uses and adaptations.
- Updated method and parameter models to include `is_returned_by_ref` and `is_promoted` properties.
- Improved PHPDoc context handling for better type resolution in models.

269 of 316 new or added lines in 16 files covered. (85.13%)

1 existing line in 1 file now uncovered.

1988 of 2336 relevant lines covered (85.1%)

175.85 hits per line

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

94.9
/src/voku/SimplePhpParser/Model/BasePHPClass.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace voku\SimplePhpParser\Model;
6

7
abstract class BasePHPClass extends BasePHPElement
8
{
9
    use PHPDocElement;
10

11
    private const PHP_VERSION_8_2_0 = 80200;
12

13
    private const PHP_VERSION_8_3_0 = 80300;
14

15
    private const PHP_VERSION_8_4_0 = 80400;
16

17
    /**
18
     * @var array<string, PHPMethod>
19
     */
20
    public array $methods = [];
21

22
    /**
23
     * @var array<string, PHPProperty>
24
     */
25
    public array $properties = [];
26

27
    /**
28
     * @var array<string, PHPConst>
29
     */
30
    public array $constants = [];
31

32
    /**
33
     * PHP 8.0+ attributes on this class/interface/trait/enum.
34
     *
35
     * @var PHPAttribute[]
36
     */
37
    public array $attributes = [];
38

39
    /**
40
     * Traits composed directly by this class-like declaration, keyed by their
41
     * fully-qualified names.
42
     *
43
     * @var array<string, string>
44
     *
45
     * @phpstan-var array<class-string, class-string>
46
     */
47
    public array $traitUses = [];
48

49
    /**
50
     * Trait conflict-resolution and alias rules in source order.
51
     *
52
     * An alias rule has type "alias" and may omit trait when PHP's shorthand
53
     * syntax is used. A precedence rule has type "precedence" and lists the
54
     * traits replaced by the selected method.
55
     *
56
     * @var list<array{
57
     *     type: 'alias'|'precedence',
58
     *     trait: class-string|null,
59
     *     method: string,
60
     *     alias?: string|null,
61
     *     visibility?: ''|'private'|'protected'|'public',
62
     *     insteadOf?: list<class-string>
63
     * }>
64
     */
65
    public array $traitAdaptations = [];
66

67
    public ?bool $is_final = null;
68

69
    public ?bool $is_abstract = null;
70

71
    public ?bool $is_readonly = null;
72

73
    public ?bool $is_anonymous = null;
74

75
    public ?bool $is_cloneable = null;
76

77
    public ?bool $is_instantiable = null;
78

79
    public ?bool $is_iterable = null;
80

81
    /**
82
     * Check if the parsed class-like node can be safely autoloaded on the
83
     * current runtime without triggering fatal syntax errors from newer PHP features.
84
     */
85
    protected static function canAutoloadFromPhpNode(\PhpParser\Node $node): bool
86
    {
87
        if (\PHP_VERSION_ID < self::PHP_VERSION_8_2_0 && self::containsPHP82PlusSyntax($node)) {
611✔
88
            return false;
10✔
89
        }
90

91
        if (\PHP_VERSION_ID < self::PHP_VERSION_8_3_0 && self::containsPHP83PlusSyntax($node)) {
607✔
92
            return false;
12✔
93
        }
94

95
        if (\PHP_VERSION_ID < self::PHP_VERSION_8_4_0 && self::containsPHP84PlusSyntax($node)) {
607✔
96
            return false;
18✔
97
        }
98

99
        return true;
601✔
100
    }
101

102
    protected function collectTraitUsesFromPhpNode(\PhpParser\Node $node): void
103
    {
104
        foreach ($node->stmts ?? [] as $statement) {
669✔
105
            if (!$statement instanceof \PhpParser\Node\Stmt\TraitUse) {
659✔
106
                continue;
659✔
107
            }
108

109
            foreach ($statement->traits as $trait) {
90✔
110
                $traitName = $trait->toString();
90✔
111
                /** @var class-string $traitName */
112
                $traitName = $traitName;
90✔
113
                $this->traitUses[$traitName] = $traitName;
90✔
114
            }
115

116
            foreach ($statement->adaptations as $adaptation) {
90✔
117
                $traitName = $adaptation->trait?->toString();
10✔
118
                if ($traitName !== null) {
10✔
119
                    /** @var class-string $traitName */
120
                    $traitName = $traitName;
10✔
121
                }
122

123
                if ($adaptation instanceof \PhpParser\Node\Stmt\TraitUseAdaptation\Alias) {
10✔
124
                    $this->traitAdaptations[] = [
10✔
125
                        'type'       => 'alias',
10✔
126
                        'trait'      => $traitName,
10✔
127
                        'method'     => $adaptation->method->toString(),
10✔
128
                        'alias'      => $adaptation->newName?->toString(),
10✔
129
                        'visibility' => self::getVisibilityFromModifierFlags($adaptation->newModifier ?? 0),
10✔
130
                    ];
10✔
131

132
                    continue;
10✔
133
                }
134

135
                if (!$adaptation instanceof \PhpParser\Node\Stmt\TraitUseAdaptation\Precedence) {
10✔
NEW
136
                    continue;
×
137
                }
138

139
                $insteadOf = [];
10✔
140
                foreach ($adaptation->insteadof as $replacedTrait) {
10✔
141
                    $replacedTraitName = $replacedTrait->toString();
10✔
142
                    /** @var class-string $replacedTraitName */
143
                    $replacedTraitName = $replacedTraitName;
10✔
144
                    $insteadOf[] = $replacedTraitName;
10✔
145
                }
146

147
                $this->traitAdaptations[] = [
10✔
148
                    'type'      => 'precedence',
10✔
149
                    'trait'     => $traitName,
10✔
150
                    'method'    => $adaptation->method->toString(),
10✔
151
                    'insteadOf' => $insteadOf,
10✔
152
                ];
10✔
153
            }
154
        }
155
    }
156

157
    /**
158
     * @param \ReflectionClass<object> $class
159
     */
160
    protected function collectTraitUsesFromReflection(\ReflectionClass $class): void
161
    {
162
        foreach ($class->getTraitNames() as $traitName) {
319✔
163
            /** @var class-string $traitName */
164
            $traitName = $traitName;
70✔
165
            $this->traitUses[$traitName] = $traitName;
70✔
166
        }
167
    }
168

169
    /**
170
     * Detect PHP 8.2-only syntax within a class-like AST such as readonly classes,
171
     * DNF types, and standalone null/true/false types.
172
     */
173
    private static function containsPHP82PlusSyntax(\PhpParser\Node $node): bool
174
    {
175
        if (
176
            $node instanceof \PhpParser\Node\Stmt\Class_
115✔
177
            &&
178
            $node->isReadonly()
115✔
179
        ) {
180
            return true;
2✔
181
        }
182

183
        if ($node instanceof \PhpParser\Node\UnionType) {
115✔
184
            foreach ($node->types as $innerType) {
14✔
185
                if ($innerType instanceof \PhpParser\Node\IntersectionType) {
14✔
186
                    return true;
6✔
187
                }
188
            }
189
        }
190

191
        if ($node instanceof \PhpParser\Node\Identifier) {
115✔
192
            $typeName = \strtolower($node->name);
115✔
193

194
            // Standalone null/true/false are represented as Identifier nodes too,
195
            // so they are only PHP 8.2+ when they are not part of a union type.
196
            if (
197
                ($typeName === 'null' || $typeName === 'true' || $typeName === 'false')
115✔
198
                &&
199
                !($node->getAttribute('parent') instanceof \PhpParser\Node\UnionType)
115✔
200
            ) {
201
                return true;
10✔
202
            }
203
        }
204

205
        foreach ($node->getSubNodeNames() as $subNodeName) {
115✔
206
            $subNode = $node->{$subNodeName};
115✔
207

208
            if ($subNode instanceof \PhpParser\Node && self::containsPHP82PlusSyntax($subNode)) {
115✔
209
                return true;
10✔
210
            }
211

212
            if (!\is_array($subNode)) {
115✔
213
                continue;
115✔
214
            }
215

216
            foreach ($subNode as $subNodeInner) {
115✔
217
                if ($subNodeInner instanceof \PhpParser\Node && self::containsPHP82PlusSyntax($subNodeInner)) {
113✔
218
                    return true;
10✔
219
                }
220
            }
221
        }
222

223
        return false;
115✔
224
    }
225

226
    /**
227
     * Detect PHP 8.3-only syntax within a class-like AST such as typed class constants.
228
     */
229
    private static function containsPHP83PlusSyntax(\PhpParser\Node $node): bool
230
    {
231
        if (
232
            $node instanceof \PhpParser\Node\Stmt\ClassConst
232✔
233
            &&
234
            $node->type !== null
232✔
235
        ) {
236
            return true;
12✔
237
        }
238

239
        foreach ($node->getSubNodeNames() as $subNodeName) {
232✔
240
            $subNode = $node->{$subNodeName};
232✔
241

242
            if ($subNode instanceof \PhpParser\Node && self::containsPHP83PlusSyntax($subNode)) {
232✔
243
                return true;
×
244
            }
245

246
            if (!\is_array($subNode)) {
232✔
247
                continue;
232✔
248
            }
249

250
            foreach ($subNode as $subNodeInner) {
232✔
251
                if ($subNodeInner instanceof \PhpParser\Node && self::containsPHP83PlusSyntax($subNodeInner)) {
228✔
252
                    return true;
12✔
253
                }
254
            }
255
        }
256

257
        return false;
232✔
258
    }
259

260
    /**
261
     * Detect PHP 8.4-only syntax within a class-like AST such as property hooks
262
     * and asymmetric visibility modifiers.
263
     */
264
    protected static function containsPHP84PlusSyntax(\PhpParser\Node $node): bool
265
    {
266
        // Property hooks (PHP 8.4+)
267
        if ($node instanceof \PhpParser\Node\Stmt\Property && !empty($node->hooks)) {
357✔
268
            return true;
15✔
269
        }
270

271
        // Asymmetric visibility on properties (PHP 8.4+)
272
        if (
273
            $node instanceof \PhpParser\Node\Stmt\Property
357✔
274
            && self::getAsymmetricSetVisibility($node) !== ''
357✔
275
        ) {
276
            return true;
×
277
        }
278

279
        // Property hooks on promoted constructor parameters (PHP 8.4+)
280
        if ($node instanceof \PhpParser\Node\Param && !empty($node->hooks)) {
357✔
281
            return true;
12✔
282
        }
283

284
        // Asymmetric visibility on promoted constructor parameters (PHP 8.4+)
285
        if (
286
            $node instanceof \PhpParser\Node\Param
357✔
287
            && self::getAsymmetricSetVisibility($node) !== ''
357✔
288
        ) {
289
            return true;
×
290
        }
291

292
        foreach ($node->getSubNodeNames() as $subNodeName) {
357✔
293
            $subNode = $node->{$subNodeName};
357✔
294

295
            if ($subNode instanceof \PhpParser\Node && self::containsPHP84PlusSyntax($subNode)) {
357✔
296
                return true;
×
297
            }
298

299
            if (!\is_array($subNode)) {
357✔
300
                continue;
357✔
301
            }
302

303
            foreach ($subNode as $subNodeInner) {
357✔
304
                if ($subNodeInner instanceof \PhpParser\Node && self::containsPHP84PlusSyntax($subNodeInner)) {
351✔
305
                    return true;
18✔
306
                }
307
            }
308
        }
309

310
        return false;
357✔
311
    }
312
}
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