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

voku / Simple-PHP-Code-Parser / 30976630142

05 Aug 2026 04:57AM UTC coverage: 86.163% (+1.1%) from 85.098%
30976630142

Pull #99

github

web-flow
Merge 694b39dde into 27e504550
Pull Request #99: Harden parser autoload safety and port enum fixes

28 of 33 new or added lines in 6 files covered. (84.85%)

1 existing line in 1 file now uncovered.

2030 of 2356 relevant lines covered (86.16%)

146.44 hits per line

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

95.05
/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)) {
568✔
88
            return false;
12✔
89
        }
90

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

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

99
        return true;
556✔
100
    }
101

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

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

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

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

132
                    continue;
8✔
133
                }
134

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

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

147
                $this->traitAdaptations[] = [
8✔
148
                    'type'      => 'precedence',
8✔
149
                    'trait'     => $traitName,
8✔
150
                    'method'    => $adaptation->method->toString(),
8✔
151
                    'insteadOf' => $insteadOf,
8✔
152
                ];
8✔
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) {
277✔
163
            /** @var class-string $traitName */
164
            $traitName = $traitName;
56✔
165
            $this->traitUses[$traitName] = $traitName;
56✔
166
        }
167
    }
168

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

183
        if (
184
            $node instanceof \PhpParser\Node\Stmt\Class_
135✔
185
            &&
186
            $node->isReadonly()
135✔
187
        ) {
188
            return true;
2✔
189
        }
190

191
        if ($node instanceof \PhpParser\Node\UnionType) {
135✔
192
            foreach ($node->types as $innerType) {
16✔
193
                if ($innerType instanceof \PhpParser\Node\IntersectionType) {
16✔
194
                    return true;
8✔
195
                }
196
            }
197
        }
198

199
        if ($node instanceof \PhpParser\Node\Identifier) {
135✔
200
            $typeName = \strtolower($node->name);
135✔
201

202
            // Standalone null/true/false are represented as Identifier nodes too,
203
            // so they are only PHP 8.2+ when they are not part of a union type.
204
            if (
205
                ($typeName === 'null' || $typeName === 'true' || $typeName === 'false')
135✔
206
                &&
207
                !($node->getAttribute('parent') instanceof \PhpParser\Node\UnionType)
135✔
208
            ) {
209
                return true;
10✔
210
            }
211
        }
212

213
        foreach ($node->getSubNodeNames() as $subNodeName) {
135✔
214
            $subNode = $node->{$subNodeName};
135✔
215

216
            if ($subNode instanceof \PhpParser\Node && self::containsPHP82PlusSyntax($subNode)) {
135✔
217
                return true;
12✔
218
            }
219

220
            if (!\is_array($subNode)) {
135✔
221
                continue;
135✔
222
            }
223

224
            foreach ($subNode as $subNodeInner) {
135✔
225
                if ($subNodeInner instanceof \PhpParser\Node && self::containsPHP82PlusSyntax($subNodeInner)) {
133✔
226
                    return true;
12✔
227
                }
228
            }
229
        }
230

231
        return false;
135✔
232
    }
233

234
    /**
235
     * Detect PHP 8.3-only syntax within a class-like AST such as typed class constants.
236
     */
237
    private static function containsPHP83PlusSyntax(\PhpParser\Node $node): bool
238
    {
239
        if (
240
            $node instanceof \PhpParser\Node\Stmt\ClassConst
272✔
241
            &&
242
            $node->type !== null
272✔
243
        ) {
244
            return true;
14✔
245
        }
246

247
        foreach ($node->getSubNodeNames() as $subNodeName) {
272✔
248
            $subNode = $node->{$subNodeName};
272✔
249

250
            if ($subNode instanceof \PhpParser\Node && self::containsPHP83PlusSyntax($subNode)) {
272✔
251
                return true;
×
252
            }
253

254
            if (!\is_array($subNode)) {
272✔
255
                continue;
272✔
256
            }
257

258
            foreach ($subNode as $subNodeInner) {
272✔
259
                if ($subNodeInner instanceof \PhpParser\Node && self::containsPHP83PlusSyntax($subNodeInner)) {
268✔
260
                    return true;
14✔
261
                }
262
            }
263
        }
264

265
        return false;
272✔
266
    }
267

268
    /**
269
     * Detect PHP 8.4-only syntax within a class-like AST such as property hooks
270
     * and asymmetric visibility modifiers.
271
     */
272
    protected static function containsPHP84PlusSyntax(\PhpParser\Node $node): bool
273
    {
274
        // Property hooks (PHP 8.4+)
275
        if ($node instanceof \PhpParser\Node\Stmt\Property && !empty($node->hooks)) {
270✔
276
            return true;
10✔
277
        }
278

279
        // Asymmetric visibility on properties (PHP 8.4+)
280
        if (
281
            $node instanceof \PhpParser\Node\Stmt\Property
270✔
282
            && self::getAsymmetricSetVisibility($node) !== ''
270✔
283
        ) {
284
            return true;
×
285
        }
286

287
        // Property hooks on promoted constructor parameters (PHP 8.4+)
288
        if ($node instanceof \PhpParser\Node\Param && !empty($node->hooks)) {
270✔
289
            return true;
8✔
290
        }
291

292
        // Asymmetric visibility on promoted constructor parameters (PHP 8.4+)
293
        if (
294
            $node instanceof \PhpParser\Node\Param
270✔
295
            && self::getAsymmetricSetVisibility($node) !== ''
270✔
296
        ) {
297
            return true;
×
298
        }
299

300
        foreach ($node->getSubNodeNames() as $subNodeName) {
270✔
301
            $subNode = $node->{$subNodeName};
270✔
302

303
            if ($subNode instanceof \PhpParser\Node && self::containsPHP84PlusSyntax($subNode)) {
270✔
304
                return true;
×
305
            }
306

307
            if (!\is_array($subNode)) {
270✔
308
                continue;
270✔
309
            }
310

311
            foreach ($subNode as $subNodeInner) {
270✔
312
                if ($subNodeInner instanceof \PhpParser\Node && self::containsPHP84PlusSyntax($subNodeInner)) {
266✔
313
                    return true;
12✔
314
                }
315
            }
316
        }
317

318
        return false;
270✔
319
    }
320
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc