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

voku / Simple-PHP-Code-Parser / 24274355871

11 Apr 2026 04:10AM UTC coverage: 82.099%. Remained the same
24274355871

push

github

web-flow
Merge pull request #81 from voku/renovate/actions-upload-artifact-7.x

Update actions/upload-artifact action to v7.0.1

1541 of 1877 relevant lines covered (82.1%)

44.81 hits per line

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

92.66
/src/voku/SimplePhpParser/Model/PHPClass.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace voku\SimplePhpParser\Model;
6

7
use PhpParser\Comment\Doc;
8
use PhpParser\Node\Stmt\Class_;
9
use ReflectionClass;
10
use voku\SimplePhpParser\Parsers\Helper\DocFactoryProvider;
11
use voku\SimplePhpParser\Parsers\Helper\Utils;
12

13
class PHPClass extends BasePHPClass
14
{
15
    /**
16
     * @phpstan-var class-string
17
     */
18
    public string $name;
19

20
    /**
21
     * @phpstan-var class-string|null
22
     */
23
    public ?string $parentClass = null;
24

25
    /**
26
     * @var string[]
27
     *
28
     * @phpstan-var class-string[]
29
     */
30
    public array $interfaces = [];
31

32
    /**
33
     * @param Class_ $node
34
     * @param null   $dummy
35
     *
36
     * @return $this
37
     */
38
    public function readObjectFromPhpNode($node, $dummy = null): self
39
    {
40
        $this->prepareNode($node);
115✔
41

42
        $this->name = static::getFQN($node);
115✔
43

44
        $this->is_final = $node->isFinal();
115✔
45

46
        $this->is_abstract = $node->isAbstract();
115✔
47

48
        if (method_exists($node, 'isReadOnly')) {
115✔
49
            $this->is_readonly = $node->isReadOnly();
115✔
50
        }
51

52
        $this->is_anonymous = $node->isAnonymous();
115✔
53

54
        // Extract PHP 8.0+ attributes
55
        if (!empty($node->attrGroups)) {
115✔
56
            $this->attributes = Utils::extractAttributesFromAstNode($node->attrGroups);
28✔
57
        }
58

59
        // PHP < 8.2 raises an uncatchable E_COMPILE_ERROR for certain PHP 8.2+ syntax
60
        // (standalone true/false/null types, DNF types, readonly class). Similarly,
61
        // PHP < 8.3 raises an error for PHP 8.3+ syntax (typed class constants).
62
        // Skip autoloading in those cases; AST data is still read from the node below.
63
        $canAutoload = (\PHP_VERSION_ID >= 80200 || !self::nodeUsesPHP82PlusSyntax($node))
115✔
64
            && (\PHP_VERSION_ID >= 80300 || !self::nodeUsesPHP83PlusSyntax($node));
115✔
65
        $classExists = false;
115✔
66
        if ($canAutoload) {
115✔
67
            try {
68
                if (\class_exists($this->name, true)) {
113✔
69
                    $classExists = true;
113✔
70
                }
71
            } catch (\Throwable $e) {
×
72
                // nothing
73
            }
74
        }
75
        if ($classExists) {
115✔
76
            $reflectionClass = Utils::createClassReflectionInstance($this->name);
89✔
77
            $this->readObjectFromReflection($reflectionClass);
89✔
78
        }
79

80
        $this->collectTags($node);
115✔
81

82
        if (!empty($node->extends)) {
115✔
83
            $classExtended = $node->extends->toString();
48✔
84
            /** @noinspection PhpSillyAssignmentInspection - hack for phpstan */
85
            /** @var class-string $classExtended */
86
            $classExtended = $classExtended;
48✔
87
            $this->parentClass = $classExtended;
48✔
88
        }
89

90
        $docComment = $node->getDocComment();
115✔
91
        if ($docComment) {
115✔
92
            $this->readPhpDocProperties($docComment->getText());
103✔
93
        }
94

95
        foreach ($node->getProperties() as $property) {
115✔
96
            $propertyNameTmp = $this->getConstantFQN($property, $property->props[0]->name->name);
77✔
97

98
            if (isset($this->properties[$propertyNameTmp])) {
77✔
99
                $this->properties[$propertyNameTmp] = $this->properties[$propertyNameTmp]->readObjectFromPhpNode($property, $this->name);
71✔
100
            } else {
101
                $this->properties[$propertyNameTmp] = (new PHPProperty($this->parserContainer))->readObjectFromPhpNode($property, $this->name);
8✔
102
            }
103

104
            if ($this->is_readonly) {
77✔
105
                $this->properties[$propertyNameTmp]->is_readonly = true;
7✔
106
            }
107
        }
108

109
        foreach ($node->getMethods() as $method) {
115✔
110
            $methodNameTmp = $method->name->name;
105✔
111

112
            if (isset($this->methods[$methodNameTmp])) {
105✔
113
                $this->methods[$methodNameTmp] = $this->methods[$methodNameTmp]->readObjectFromPhpNode($method, $this->name);
83✔
114
            } else {
115
                $this->methods[$methodNameTmp] = (new PHPMethod($this->parserContainer))->readObjectFromPhpNode($method, $this->name);
30✔
116
            }
117

118
            if (!$this->methods[$methodNameTmp]->file) {
105✔
119
                $this->methods[$methodNameTmp]->file = $this->file;
30✔
120
            }
121
        }
122

123
        if (!empty($node->implements)) {
115✔
124
            foreach ($node->implements as $interfaceObject) {
30✔
125
                $interfaceFQN = $interfaceObject->toString();
30✔
126
                /** @noinspection PhpSillyAssignmentInspection - hack for phpstan */
127
                /** @var class-string $interfaceFQN */
128
                $interfaceFQN = $interfaceFQN;
30✔
129
                $this->interfaces[$interfaceFQN] = $interfaceFQN;
30✔
130
            }
131
        }
132

133
        return $this;
115✔
134
    }
135

136
    /**
137
     * @param ReflectionClass $clazz
138
     *
139
     * @return $this
140
     */
141
    public function readObjectFromReflection($clazz): self
142
    {
143
        $this->name = $clazz->getName();
89✔
144

145
        if (!$this->line) {
89✔
146
            $lineTmp = $clazz->getStartLine();
40✔
147
            if ($lineTmp !== false) {
40✔
148
                $this->line = $lineTmp;
24✔
149
            }
150
        }
151

152
        $file = $clazz->getFileName();
89✔
153
        if ($file) {
89✔
154
            $this->file = $file;
89✔
155
        }
156

157
        $this->is_final = $clazz->isFinal();
89✔
158

159
        $this->is_abstract = $clazz->isAbstract();
89✔
160

161
        if (method_exists($clazz, 'isReadOnly')) {
89✔
162
            $this->is_readonly = $clazz->isReadOnly();
71✔
163
        }
164

165
        $this->is_anonymous = $clazz->isAnonymous();
89✔
166

167
        $this->is_cloneable = $clazz->isCloneable();
89✔
168

169
        $this->is_instantiable = $clazz->isInstantiable();
89✔
170

171
        $this->is_iterable = $clazz->isIterable();
89✔
172

173
        // Extract PHP 8.0+ attributes
174
        $this->attributes = Utils::extractAttributesFromReflection($clazz);
89✔
175

176
        $parent = $clazz->getParentClass();
89✔
177
        if ($parent) {
89✔
178
            $this->parentClass = $parent->getName();
40✔
179

180
            $classExists = false;
40✔
181
            try {
182
                if (
183
                    !$this->parserContainer->getClass($this->parentClass)
40✔
184
                    &&
185
                    \class_exists($this->parentClass, true)
40✔
186
                ) {
187
                    $classExists = true;
40✔
188
                }
189
            } catch (\Throwable $e) {
×
190
                // nothing
191
            }
192
            if ($classExists) {
40✔
193
                $reflectionClass = Utils::createClassReflectionInstance($this->parentClass);
40✔
194
                $class = (new self($this->parserContainer))->readObjectFromReflection($reflectionClass);
40✔
195
                $this->parserContainer->addClass($class);
40✔
196
            }
197
        }
198

199
        foreach ($clazz->getProperties() as $property) {
89✔
200
            $propertyPhp = (new PHPProperty($this->parserContainer))->readObjectFromReflection($property);
83✔
201
            $this->properties[$propertyPhp->name] = $propertyPhp;
83✔
202

203
            if ($this->is_readonly) {
83✔
204
                $this->properties[$propertyPhp->name]->is_readonly = true;
6✔
205
            }
206
        }
207

208
        foreach ($clazz->getInterfaceNames() as $interfaceName) {
89✔
209
            /** @noinspection PhpSillyAssignmentInspection - hack for phpstan */
210
            /** @var class-string $interfaceName */
211
            $interfaceName = $interfaceName;
40✔
212
            $this->interfaces[$interfaceName] = $interfaceName;
40✔
213
        }
214

215
        foreach ($clazz->getMethods() as $method) {
89✔
216
            $methodNameTmp = $method->getName();
87✔
217

218
            $this->methods[$methodNameTmp] = (new PHPMethod($this->parserContainer))->readObjectFromReflection($method);
87✔
219

220
            if (!$this->methods[$methodNameTmp]->file) {
87✔
221
                $this->methods[$methodNameTmp]->file = $this->file;
×
222
            }
223
        }
224

225
        foreach ($clazz->getReflectionConstants() as $constant) {
89✔
226
            $constantNameTmp = $constant->getName();
54✔
227

228
            $this->constants[$constantNameTmp] = (new PHPConst($this->parserContainer))->readObjectFromReflection($constant);
54✔
229

230
            if (!$this->constants[$constantNameTmp]->file) {
54✔
231
                $this->constants[$constantNameTmp]->file = $this->file;
×
232
            }
233
        }
234

235
        return $this;
89✔
236
    }
237

238
    /**
239
     * @param string[] $access
240
     * @param bool     $skipMethodsWithLeadingUnderscore
241
     *
242
     * @return array
243
     *
244
     * @psalm-return array<string, array{
245
     *     type: null|string,
246
     *     typeFromPhpDocMaybeWithComment: null|string,
247
     *     typeFromPhpDoc: null|string,
248
     *     typeFromPhpDocSimple: null|string,
249
     *     typeFromPhpDocExtended: null|string,
250
     *     typeFromDefaultValue: null|string
251
     * }>
252
     */
253
    public function getPropertiesInfo(
254
        array $access = ['public', 'protected', 'private'],
255
        bool $skipMethodsWithLeadingUnderscore = false
256
    ): array {
257
        // init
258
        $allInfo = [];
4✔
259

260
        foreach ($this->properties as $property) {
4✔
261
            if (!\in_array($property->access, $access, true)) {
4✔
262
                continue;
×
263
            }
264

265
            if ($skipMethodsWithLeadingUnderscore && \strpos($property->name, '_') === 0) {
4✔
266
                continue;
×
267
            }
268

269
            $types = [];
4✔
270
            $types['type'] = $property->type;
4✔
271
            $types['typeFromPhpDocMaybeWithComment'] = $property->typeFromPhpDocMaybeWithComment;
4✔
272
            $types['typeFromPhpDoc'] = $property->typeFromPhpDoc;
4✔
273
            $types['typeFromPhpDocSimple'] = $property->typeFromPhpDocSimple;
4✔
274
            $types['typeFromPhpDocExtended'] = $property->typeFromPhpDocExtended;
4✔
275
            $types['typeFromDefaultValue'] = $property->typeFromDefaultValue;
4✔
276

277
            $allInfo[$property->name] = $types;
4✔
278
        }
279

280
        return $allInfo;
4✔
281
    }
282

283
    /**
284
     * @param string[] $access
285
     * @param bool     $skipDeprecatedMethods
286
     * @param bool     $skipMethodsWithLeadingUnderscore
287
     *
288
     * @return array<mixed>
289
     *
290
     * @psalm-return array<string, array{
291
     *     fullDescription: string,
292
     *     line: null|int,
293
     *     file: null|string,
294
     *     error: string,
295
     *     is_deprecated: bool,
296
     *     is_static: null|bool,
297
     *     is_meta: bool,
298
     *     is_internal: bool,
299
     *     is_removed: bool,
300
     *     paramsTypes: array<string,
301
     *         array{
302
     *              type?: null|string,
303
     *              typeFromPhpDoc?: null|string,
304
     *              typeFromPhpDocExtended?: null|string,
305
     *              typeFromPhpDocSimple?: null|string,
306
     *              typeFromPhpDocMaybeWithComment?: null|string,
307
     *              typeFromDefaultValue?: null|string
308
     *         }
309
     *     >,
310
     *     returnTypes: array{
311
     *         type: null|string,
312
     *         typeFromPhpDoc: null|string,
313
     *         typeFromPhpDocExtended: null|string,
314
     *         typeFromPhpDocSimple: null|string,
315
     *         typeFromPhpDocMaybeWithComment: null|string
316
     *     },
317
     *     paramsPhpDocRaw: array<string, null|string>,
318
     *     returnPhpDocRaw: null|string
319
     * }>
320
     */
321
    public function getMethodsInfo(
322
        array $access = ['public', 'protected', 'private'],
323
        bool $skipDeprecatedMethods = false,
324
        bool $skipMethodsWithLeadingUnderscore = false
325
    ): array {
326
        // init
327
        $allInfo = [];
12✔
328

329
        foreach ($this->methods as $method) {
12✔
330
            if (!\in_array($method->access, $access, true)) {
12✔
331
                continue;
×
332
            }
333

334
            if ($skipDeprecatedMethods && $method->hasDeprecatedTag) {
12✔
335
                continue;
×
336
            }
337

338
            if ($skipMethodsWithLeadingUnderscore && \strpos($method->name, '_') === 0) {
12✔
339
                continue;
×
340
            }
341

342
            $paramsTypes = [];
12✔
343
            foreach ($method->parameters as $tagParam) {
12✔
344
                $paramsTypes[$tagParam->name]['type'] = $tagParam->type;
12✔
345
                $paramsTypes[$tagParam->name]['typeFromPhpDocMaybeWithComment'] = $tagParam->typeFromPhpDocMaybeWithComment;
12✔
346
                $paramsTypes[$tagParam->name]['typeFromPhpDoc'] = $tagParam->typeFromPhpDoc;
12✔
347
                $paramsTypes[$tagParam->name]['typeFromPhpDocSimple'] = $tagParam->typeFromPhpDocSimple;
12✔
348
                $paramsTypes[$tagParam->name]['typeFromPhpDocExtended'] = $tagParam->typeFromPhpDocExtended;
12✔
349
                $paramsTypes[$tagParam->name]['typeFromDefaultValue'] = $tagParam->typeFromDefaultValue;
12✔
350
            }
351

352
            $returnTypes = [];
12✔
353
            $returnTypes['type'] = $method->returnType;
12✔
354
            $returnTypes['typeFromPhpDocMaybeWithComment'] = $method->returnTypeFromPhpDocMaybeWithComment;
12✔
355
            $returnTypes['typeFromPhpDoc'] = $method->returnTypeFromPhpDoc;
12✔
356
            $returnTypes['typeFromPhpDocSimple'] = $method->returnTypeFromPhpDocSimple;
12✔
357
            $returnTypes['typeFromPhpDocExtended'] = $method->returnTypeFromPhpDocExtended;
12✔
358

359
            $paramsPhpDocRaw = [];
12✔
360
            foreach ($method->parameters as $tagParam) {
12✔
361
                $paramsPhpDocRaw[$tagParam->name] = $tagParam->phpDocRaw;
12✔
362
            }
363

364
            $infoTmp = [];
12✔
365
            $infoTmp['fullDescription'] = \trim($method->summary . "\n\n" . $method->description);
12✔
366
            $infoTmp['paramsTypes'] = $paramsTypes;
12✔
367
            $infoTmp['returnTypes'] = $returnTypes;
12✔
368
            $infoTmp['paramsPhpDocRaw'] = $paramsPhpDocRaw;
12✔
369
            $infoTmp['returnPhpDocRaw'] = $method->returnPhpDocRaw;
12✔
370
            $infoTmp['line'] = $method->line ?? $this->line;
12✔
371
            $infoTmp['file'] = $method->file ?? $this->file;
12✔
372
            $infoTmp['error'] = \implode("\n", $method->parseError);
12✔
373
            foreach ($method->parameters as $parameter) {
12✔
374
                $infoTmp['error'] .= ($infoTmp['error'] ? "\n" : '') . \implode("\n", $parameter->parseError);
12✔
375
            }
376
            $infoTmp['is_deprecated'] = $method->hasDeprecatedTag;
12✔
377
            $infoTmp['is_static'] = $method->is_static;
12✔
378
            $infoTmp['is_meta'] = $method->hasMetaTag;
12✔
379
            $infoTmp['is_internal'] = $method->hasInternalTag;
12✔
380
            $infoTmp['is_removed'] = $method->hasRemovedTag;
12✔
381

382
            $allInfo[$method->name] = $infoTmp;
12✔
383
        }
384

385
        \asort($allInfo);
12✔
386

387
        return $allInfo;
12✔
388
    }
389

390
    /**
391
     * @param Doc|string $doc
392
     */
393
    private function readPhpDocProperties($doc): void
394
    {
395
        if ($doc instanceof Doc) {
103✔
396
            $docComment = $doc->getText();
×
397
        } else {
398
            $docComment = $doc;
103✔
399
        }
400
        if ($docComment === '') {
103✔
401
            return;
×
402
        }
403

404
        try {
405
            $phpDoc = DocFactoryProvider::getDocFactory()->create($docComment);
103✔
406

407
            $parsedPropertyTags = $phpDoc->getTagsByName('property')
103✔
408
                               + $phpDoc->getTagsByName('property-read')
103✔
409
                               + $phpDoc->getTagsByName('property-write');
103✔
410

411
            if (!empty($parsedPropertyTags)) {
103✔
412
                foreach ($parsedPropertyTags as $parsedPropertyTag) {
103✔
413
                    if (
414
                        $parsedPropertyTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\PropertyRead
28✔
415
                        ||
416
                        $parsedPropertyTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\PropertyWrite
28✔
417
                        ||
418
                        $parsedPropertyTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Property
28✔
419
                    ) {
420
                        $propertyPhp = new PHPProperty($this->parserContainer);
28✔
421

422
                        $nameTmp = $parsedPropertyTag->getVariableName();
28✔
423
                        if (!$nameTmp) {
28✔
424
                            continue;
×
425
                        }
426

427
                        $propertyPhp->name = $nameTmp;
28✔
428

429
                        $propertyPhp->access = 'public';
28✔
430

431
                        $type = $parsedPropertyTag->getType();
28✔
432

433
                        $propertyPhp->typeFromPhpDoc = Utils::normalizePhpType($type . '');
28✔
434

435
                        $typeFromPhpDocMaybeWithCommentTmp = \trim((string) $parsedPropertyTag);
28✔
436
                        if (
437
                            $typeFromPhpDocMaybeWithCommentTmp
28✔
438
                            &&
439
                            \strpos($typeFromPhpDocMaybeWithCommentTmp, '$') !== 0
28✔
440
                        ) {
441
                            $propertyPhp->typeFromPhpDocMaybeWithComment = $typeFromPhpDocMaybeWithCommentTmp;
28✔
442
                        }
443

444
                        $typeTmp = Utils::parseDocTypeObject($type);
28✔
445
                        if ($typeTmp !== '') {
28✔
446
                            $propertyPhp->typeFromPhpDocSimple = $typeTmp;
28✔
447
                        }
448

449
                        if ($propertyPhp->typeFromPhpDoc) {
28✔
450
                            $propertyPhp->typeFromPhpDocExtended = Utils::modernPhpdoc($propertyPhp->typeFromPhpDoc);
28✔
451
                        }
452

453
                        $this->properties[$propertyPhp->name] = $propertyPhp;
28✔
454
                    }
455
                }
456
            }
457
        } catch (\Exception $e) {
×
458
            $tmpErrorMessage = ($this->name ?: '?') . ':' . ($this->line ?? '?') . ' | ' . \print_r($e->getMessage(), true);
×
459
            $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage;
×
460
        }
461
    }
462

463
    /**
464
     * Returns true if the class node uses syntax that requires PHP 8.2+ and would
465
     * cause an uncatchable E_COMPILE_ERROR when autoloaded on PHP < 8.2.
466
     *
467
     * @param Class_ $node
468
     *
469
     * @return bool
470
     */
471
    private static function nodeUsesPHP82PlusSyntax(Class_ $node): bool
472
    {
473
        // readonly class is PHP 8.2+
474
        if (\method_exists($node, 'isReadOnly') && $node->isReadOnly()) {
26✔
475
            return true;
1✔
476
        }
477

478
        foreach ($node->stmts as $stmt) {
26✔
479
            if ($stmt instanceof \PhpParser\Node\Stmt\ClassMethod) {
25✔
480
                if (self::containsPHP82PlusType($stmt->returnType)) {
24✔
481
                    return true;
4✔
482
                }
483
                foreach ($stmt->params as $param) {
24✔
484
                    if (self::containsPHP82PlusType($param->type)) {
22✔
485
                        return true;
×
486
                    }
487
                }
488
            } elseif ($stmt instanceof \PhpParser\Node\Stmt\Property) {
18✔
489
                if (self::containsPHP82PlusType($stmt->type)) {
17✔
490
                    return true;
2✔
491
                }
492
            }
493
        }
494

495
        return false;
24✔
496
    }
497

498
    /**
499
     * Returns true if the class node uses syntax that requires PHP 8.3+ and would
500
     * cause an uncatchable E_COMPILE_ERROR when autoloaded on PHP < 8.3.
501
     *
502
     * Covers: typed class constants (Stmt\ClassConst with a non-null type).
503
     *
504
     * @param Class_ $node
505
     *
506
     * @return bool
507
     */
508
    private static function nodeUsesPHP83PlusSyntax(Class_ $node): bool
509
    {
510
        foreach ($node->stmts as $stmt) {
53✔
511
            // Typed class constants are PHP 8.3+
512
            if ($stmt instanceof \PhpParser\Node\Stmt\ClassConst && $stmt->type !== null) {
51✔
513
                return true;
4✔
514
            }
515
        }
516

517
        return false;
53✔
518
    }
519

520
    /**
521
     * Returns true if the given type node is a PHP 8.2+ type that causes an
522
     * uncatchable E_COMPILE_ERROR when loaded on PHP < 8.2.
523
     *
524
     * Covers: standalone true/false/null types and DNF types (union of intersections).
525
     *
526
     * @param \PhpParser\Node|null $typeNode
527
     *
528
     * @return bool
529
     */
530
    private static function containsPHP82PlusType($typeNode): bool
531
    {
532
        if ($typeNode === null) {
25✔
533
            return false;
21✔
534
        }
535

536
        // Standalone true, false, null as the *sole* type (not in a nullable like ?string)
537
        // are PHP 8.2+ only. PHP-Parser represents these as Identifier nodes (not Name).
538
        // Nullable null (?null) is syntactically invalid; NullableType wraps the inner type.
539
        if ($typeNode instanceof \PhpParser\Node\Identifier) {
22✔
540
            $name = \strtolower($typeNode->name);
22✔
541
            return $name === 'true' || $name === 'false' || $name === 'null';
22✔
542
        }
543

544
        // DNF types: union type containing an intersection type (PHP 8.2+)
545
        if ($typeNode instanceof \PhpParser\Node\UnionType) {
13✔
546
            foreach ($typeNode->types as $t) {
4✔
547
                if ($t instanceof \PhpParser\Node\IntersectionType || self::containsPHP82PlusType($t)) {
4✔
548
                    return true;
2✔
549
                }
550
            }
551
        }
552

553
        // Recurse into nullable type
554
        if ($typeNode instanceof \PhpParser\Node\NullableType) {
13✔
555
            return self::containsPHP82PlusType($typeNode->type);
5✔
556
        }
557

558
        return false;
13✔
559
    }
560
}
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