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

voku / Simple-PHP-Code-Parser / 24277438854

11 Apr 2026 07:15AM UTC coverage: 82.757%. Remained the same
24277438854

push

github

web-flow
Merge pull request #85 from voku/renovate/migrate-config

chore(config): migrate Renovate config

1531 of 1850 relevant lines covered (82.76%)

90.77 hits per line

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

93.12
/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);
242✔
41

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

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

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

48
        // Keep the guard for cross-version php-parser compatibility when readonly
49
        // helpers are restored or backported differently in downstream installs.
50
        if (\method_exists($node, 'isReadonly')) {
242✔
51
            $this->is_readonly = $node->isReadonly();
242✔
52
        }
53

54
        $this->is_anonymous = $node->isAnonymous();
242✔
55

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

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

82
        $this->collectTags($node);
242✔
83

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

92
        $docComment = $node->getDocComment();
242✔
93
        if ($docComment) {
242✔
94
            $this->readPhpDocProperties($docComment->getText());
206✔
95
        }
96

97
        foreach ($node->getProperties() as $property) {
242✔
98
            $propertyNameTmp = $this->getConstantFQN($property, $property->props[0]->name->name);
154✔
99

100
            if (isset($this->properties[$propertyNameTmp])) {
154✔
101
                $this->properties[$propertyNameTmp] = $this->properties[$propertyNameTmp]->readObjectFromPhpNode($property, $this->name);
142✔
102
            } else {
103
                $this->properties[$propertyNameTmp] = (new PHPProperty($this->parserContainer))->readObjectFromPhpNode($property, $this->name);
16✔
104
            }
105

106
            if ($this->is_readonly) {
154✔
107
                $this->properties[$propertyNameTmp]->is_readonly = true;
14✔
108
            }
109
        }
110

111
        foreach ($node->getMethods() as $method) {
242✔
112
            $methodNameTmp = $method->name->name;
218✔
113

114
            if (isset($this->methods[$methodNameTmp])) {
218✔
115
                $this->methods[$methodNameTmp] = $this->methods[$methodNameTmp]->readObjectFromPhpNode($method, $this->name);
166✔
116
            } else {
117
                $this->methods[$methodNameTmp] = (new PHPMethod($this->parserContainer))->readObjectFromPhpNode($method, $this->name);
68✔
118
            }
119

120
            if (!$this->methods[$methodNameTmp]->file) {
218✔
121
                $this->methods[$methodNameTmp]->file = $this->file;
68✔
122
            }
123
        }
124

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

135
        return $this;
242✔
136
    }
137

138
    /**
139
     * @param ReflectionClass<object> $clazz
140
     *
141
     * @return $this
142
     */
143
    public function readObjectFromReflection($clazz): self
144
    {
145
        $this->name = $clazz->getName();
178✔
146

147
        if (!$this->line) {
178✔
148
            $lineTmp = $clazz->getStartLine();
80✔
149
            if ($lineTmp !== false) {
80✔
150
                $this->line = $lineTmp;
48✔
151
            }
152
        }
153

154
        $file = $clazz->getFileName();
178✔
155
        if ($file) {
178✔
156
            $this->file = $file;
178✔
157
        }
158

159
        $this->is_final = $clazz->isFinal();
178✔
160

161
        $this->is_abstract = $clazz->isAbstract();
178✔
162

163
        if (method_exists($clazz, 'isReadOnly')) {
178✔
164
            $this->is_readonly = $clazz->isReadOnly();
142✔
165
        }
166

167
        $this->is_anonymous = $clazz->isAnonymous();
178✔
168

169
        $this->is_cloneable = $clazz->isCloneable();
178✔
170

171
        $this->is_instantiable = $clazz->isInstantiable();
178✔
172

173
        $this->is_iterable = $clazz->isIterable();
178✔
174

175
        // Extract PHP 8.0+ attributes
176
        $this->attributes = Utils::extractAttributesFromReflection($clazz);
178✔
177

178
        $parent = $clazz->getParentClass();
178✔
179
        if ($parent) {
178✔
180
            $this->parentClass = $parent->getName();
80✔
181

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

201
        foreach ($clazz->getProperties() as $property) {
178✔
202
            $propertyPhp = (new PHPProperty($this->parserContainer))->readObjectFromReflection($property);
166✔
203
            $this->properties[$propertyPhp->name] = $propertyPhp;
166✔
204

205
            if ($this->is_readonly) {
166✔
206
                $this->properties[$propertyPhp->name]->is_readonly = true;
12✔
207
            }
208
        }
209

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

217
        foreach ($clazz->getMethods() as $method) {
178✔
218
            $methodNameTmp = $method->getName();
174✔
219

220
            $this->methods[$methodNameTmp] = (new PHPMethod($this->parserContainer))->readObjectFromReflection($method);
174✔
221

222
            if (!$this->methods[$methodNameTmp]->file) {
174✔
223
                $this->methods[$methodNameTmp]->file = $this->file;
×
224
            }
225
        }
226

227
        foreach ($clazz->getReflectionConstants() as $constant) {
178✔
228
            $constantNameTmp = $constant->getName();
108✔
229

230
            $this->constants[$constantNameTmp] = (new PHPConst($this->parserContainer))->readObjectFromReflection($constant);
108✔
231

232
            if (!$this->constants[$constantNameTmp]->file) {
108✔
233
                $this->constants[$constantNameTmp]->file = $this->file;
×
234
            }
235
        }
236

237
        return $this;
178✔
238
    }
239

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

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

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

271
            $types = [];
8✔
272
            $types['type'] = $property->type;
8✔
273
            $types['typeFromPhpDocMaybeWithComment'] = $property->typeFromPhpDocMaybeWithComment;
8✔
274
            $types['typeFromPhpDoc'] = $property->typeFromPhpDoc;
8✔
275
            $types['typeFromPhpDocSimple'] = $property->typeFromPhpDocSimple;
8✔
276
            $types['typeFromPhpDocExtended'] = $property->typeFromPhpDocExtended;
8✔
277
            $types['typeFromDefaultValue'] = $property->typeFromDefaultValue;
8✔
278

279
            $allInfo[$property->name] = $types;
8✔
280
        }
281

282
        return $allInfo;
8✔
283
    }
284

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

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

336
            if ($skipDeprecatedMethods && $method->hasDeprecatedTag) {
24✔
337
                continue;
×
338
            }
339

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

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

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

361
            $paramsPhpDocRaw = [];
24✔
362
            foreach ($method->parameters as $tagParam) {
24✔
363
                $paramsPhpDocRaw[$tagParam->name] = $tagParam->phpDocRaw;
24✔
364
            }
365

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

384
            $allInfo[$method->name] = $infoTmp;
24✔
385
        }
386

387
        \asort($allInfo);
24✔
388

389
        return $allInfo;
24✔
390
    }
391

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

406
        try {
407
            $phpDoc = DocFactoryProvider::getDocFactory()->create($docComment);
206✔
408

409
            $parsedPropertyTags = $phpDoc->getTagsByName('property')
206✔
410
                               + $phpDoc->getTagsByName('property-read')
206✔
411
                               + $phpDoc->getTagsByName('property-write');
206✔
412

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

424
                        $nameTmp = $parsedPropertyTag->getVariableName();
56✔
425
                        if (!$nameTmp) {
56✔
426
                            continue;
×
427
                        }
428

429
                        $propertyPhp->name = $nameTmp;
56✔
430

431
                        $propertyPhp->access = 'public';
56✔
432

433
                        $type = $parsedPropertyTag->getType();
56✔
434

435
                        $propertyPhp->typeFromPhpDoc = Utils::normalizePhpType($type . '');
56✔
436

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

446
                        $typeTmp = Utils::parseDocTypeObject($type);
56✔
447
                        if ($typeTmp !== '') {
56✔
448
                            $propertyPhp->typeFromPhpDocSimple = $typeTmp;
56✔
449
                        }
450

451
                        if ($propertyPhp->typeFromPhpDoc) {
56✔
452
                            $propertyPhp->typeFromPhpDocExtended = Utils::modernPhpdoc($propertyPhp->typeFromPhpDoc);
56✔
453
                        }
454

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

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

480
        foreach ($node->stmts as $stmt) {
54✔
481
            if ($stmt instanceof \PhpParser\Node\Stmt\ClassMethod) {
52✔
482
                if (self::containsPHP82PlusType($stmt->returnType)) {
50✔
483
                    return true;
8✔
484
                }
485
                foreach ($stmt->params as $param) {
50✔
486
                    if (self::containsPHP82PlusType($param->type)) {
46✔
487
                        return true;
2✔
488
                    }
489
                }
490
            } elseif ($stmt instanceof \PhpParser\Node\Stmt\Property) {
36✔
491
                if (self::containsPHP82PlusType($stmt->type)) {
34✔
492
                    return true;
4✔
493
                }
494
            }
495
        }
496

497
        return false;
48✔
498
    }
499

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

519
        return false;
108✔
520
    }
521

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

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

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

555
        // Recurse into nullable type
556
        if ($typeNode instanceof \PhpParser\Node\NullableType) {
28✔
557
            return self::containsPHP82PlusType($typeNode->type);
10✔
558
        }
559

560
        return false;
28✔
561
    }
562
}
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