• 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

92.64
/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);
611✔
41

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

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

46
        $this->is_abstract = $node->isAbstract();
611✔
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')) {
611✔
51
            $this->is_readonly = $node->isReadonly();
611✔
52
        }
53

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

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

61
        // Skip autoloading when the current runtime cannot safely compile newer syntax;
62
        // AST data is still read from the node below.
63
        $canAutoload = self::canAutoloadFromPhpNode($node);
611✔
64
        $classExists = false;
611✔
65
        if ($canAutoload) {
611✔
66
            try {
67
                if (\class_exists($this->name, true)) {
601✔
68
                    $classExists = true;
601✔
69
                }
70
            } catch (\Throwable $e) {
4✔
71
                // nothing
72
            }
73
        }
74
        if ($classExists) {
611✔
75
            $reflectionClass = Utils::createClassReflectionInstance($this->name);
271✔
76
            $this->readObjectFromReflection($reflectionClass);
271✔
77
        }
78

79
        $this->collectTags($node);
611✔
80

81
        $this->collectTraitUsesFromPhpNode($node);
611✔
82

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

91
        $docComment = $node->getDocComment();
611✔
92
        if ($docComment) {
611✔
93
            $this->readPhpDocProperties($docComment->getText());
305✔
94
        }
95

96
        foreach ($node->getProperties() as $property) {
611✔
97
            foreach ($property->props as $propertyItem) {
304✔
98
                $propertyNameTmp = $this->getConstantFQN($property, $propertyItem->name->name);
304✔
99

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

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

112
        foreach ($node->getMethods() as $method) {
611✔
113
            $methodNameTmp = $method->name->name;
539✔
114

115
            if (isset($this->methods[$methodNameTmp])) {
539✔
116
                $this->methods[$methodNameTmp] = $this->methods[$methodNameTmp]->readObjectFromPhpNode($method, $this->name);
251✔
117
            } else {
118
                $this->methods[$methodNameTmp] = (new PHPMethod($this->parserContainer))->readObjectFromPhpNode($method, $this->name);
318✔
119
            }
120

121
            if (!$this->methods[$methodNameTmp]->file) {
539✔
122
                $this->methods[$methodNameTmp]->file = $this->file;
318✔
123
            }
124
        }
125

126
        $this->addPromotedPropertiesFromConstructor($node);
611✔
127

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

138
        return $this;
611✔
139
    }
140

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

150
        if (!$this->line) {
271✔
151
            $lineTmp = $clazz->getStartLine();
120✔
152
            if ($lineTmp !== false) {
120✔
153
                $this->line = $lineTmp;
80✔
154
            }
155
        }
156

157
        if ($this->endLine === null) {
271✔
158
            $endLineTmp = $clazz->getEndLine();
120✔
159
            if ($endLineTmp !== false) {
120✔
160
                $this->endLine = $endLineTmp;
80✔
161
            }
162
        }
163

164
        $file = $clazz->getFileName();
271✔
165
        if ($file) {
271✔
166
            $this->file = $file;
271✔
167
        }
168

169
        $this->is_final = $clazz->isFinal();
271✔
170

171
        $this->is_abstract = $clazz->isAbstract();
271✔
172

173
        if (method_exists($clazz, 'isReadOnly')) {
271✔
174
            $this->is_readonly = $clazz->isReadOnly();
227✔
175
        }
176

177
        $this->is_anonymous = $clazz->isAnonymous();
271✔
178

179
        $this->is_cloneable = $clazz->isCloneable();
271✔
180

181
        $this->is_instantiable = $clazz->isInstantiable();
271✔
182

183
        $this->is_iterable = $clazz->isIterable();
271✔
184

185
        $this->collectTraitUsesFromReflection($clazz);
271✔
186

187
        // Extract PHP 8.0+ attributes
188
        $this->attributes = Utils::extractAttributesFromReflection($clazz);
271✔
189

190
        $parent = $clazz->getParentClass();
271✔
191
        if ($parent) {
271✔
192
            $this->parentClass = $parent->getName();
120✔
193

194
            $classExists = false;
120✔
195
            try {
196
                if (
197
                    !$this->parserContainer->getClass($this->parentClass)
120✔
198
                    &&
199
                    \class_exists($this->parentClass, true)
120✔
200
                ) {
201
                    $classExists = true;
120✔
202
                }
203
            } catch (\Throwable $e) {
×
204
                // nothing
205
            }
206
            if ($classExists) {
120✔
207
                $reflectionClass = Utils::createClassReflectionInstance($this->parentClass);
120✔
208
                $class = (new self($this->parserContainer))->readObjectFromReflection($reflectionClass);
120✔
209
                $this->parserContainer->addClass($class);
120✔
210
            }
211
        }
212

213
        foreach ($clazz->getProperties() as $property) {
271✔
214
            $propertyPhp = (new PHPProperty($this->parserContainer))->readObjectFromReflection($property);
255✔
215
            $this->properties[$propertyPhp->name] = $propertyPhp;
255✔
216

217
            if ($this->is_readonly) {
255✔
218
                $this->properties[$propertyPhp->name]->is_readonly = true;
24✔
219
            }
220
        }
221

222
        foreach ($clazz->getInterfaceNames() as $interfaceName) {
271✔
223
            /** @noinspection PhpSillyAssignmentInspection - hack for phpstan */
224
            /** @var class-string $interfaceName */
225
            $interfaceName = $interfaceName;
122✔
226
            $this->interfaces[$interfaceName] = $interfaceName;
122✔
227
        }
228

229
        foreach ($clazz->getMethods() as $method) {
271✔
230
            $methodNameTmp = $method->getName();
261✔
231

232
            $this->methods[$methodNameTmp] = (new PHPMethod($this->parserContainer))->readObjectFromReflection($method);
261✔
233

234
            if (!$this->methods[$methodNameTmp]->file) {
261✔
235
                $this->methods[$methodNameTmp]->file = $this->file;
×
236
            }
237
        }
238

239
        foreach ($clazz->getReflectionConstants() as $constant) {
271✔
240
            $constantNameTmp = $constant->getName();
146✔
241

242
            $this->constants[$constantNameTmp] = (new PHPConst($this->parserContainer))->readObjectFromReflection($constant);
146✔
243

244
            if (!$this->constants[$constantNameTmp]->file) {
146✔
245
                $this->constants[$constantNameTmp]->file = $this->file;
×
246
            }
247
        }
248

249
        return $this;
271✔
250
    }
251

252
    /**
253
     * @param string[] $access
254
     * @param bool     $skipMethodsWithLeadingUnderscore
255
     *
256
     * @return array
257
     *
258
     * @psalm-return array<string, array{
259
     *     type: null|string,
260
     *     typeFromPhpDocMaybeWithComment: null|string,
261
     *     typeFromPhpDoc: null|string,
262
     *     typeFromPhpDocSimple: null|string,
263
     *     typeFromPhpDocExtended: null|string,
264
     *     typeFromDefaultValue: null|string
265
     * }>
266
     */
267
    public function getPropertiesInfo(
268
        array $access = ['public', 'protected', 'private'],
269
        bool $skipMethodsWithLeadingUnderscore = false
270
    ): array {
271
        // init
272
        $allInfo = [];
20✔
273

274
        foreach ($this->properties as $property) {
20✔
275
            if (!\in_array($property->access, $access, true)) {
20✔
276
                continue;
×
277
            }
278

279
            if ($skipMethodsWithLeadingUnderscore && \strpos($property->name, '_') === 0) {
20✔
280
                continue;
×
281
            }
282

283
            $types = [];
20✔
284
            $types['type'] = $property->type;
20✔
285
            $types['typeFromPhpDocMaybeWithComment'] = $property->typeFromPhpDocMaybeWithComment;
20✔
286
            $types['typeFromPhpDoc'] = $property->typeFromPhpDoc;
20✔
287
            $types['typeFromPhpDocSimple'] = $property->typeFromPhpDocSimple;
20✔
288
            $types['typeFromPhpDocExtended'] = $property->typeFromPhpDocExtended;
20✔
289
            $types['typeFromDefaultValue'] = $property->typeFromDefaultValue;
20✔
290

291
            $allInfo[$property->name] = $types;
20✔
292
        }
293

294
        return $allInfo;
20✔
295
    }
296

297
    /**
298
     * @param string[] $access
299
     * @param bool     $skipDeprecatedMethods
300
     * @param bool     $skipMethodsWithLeadingUnderscore
301
     *
302
     * @return array<mixed>
303
     *
304
     * @psalm-return array<string, array{
305
     *     fullDescription: string,
306
     *     line: null|int,
307
     *     file: null|string,
308
     *     error: string,
309
     *     is_deprecated: bool,
310
     *     is_static: null|bool,
311
     *     is_meta: bool,
312
     *     is_internal: bool,
313
     *     is_removed: bool,
314
     *     paramsTypes: array<string,
315
     *         array{
316
     *              type?: null|string,
317
     *              typeFromPhpDoc?: null|string,
318
     *              typeFromPhpDocExtended?: null|string,
319
     *              typeFromPhpDocSimple?: null|string,
320
     *              typeFromPhpDocMaybeWithComment?: null|string,
321
     *              typeFromDefaultValue?: null|string
322
     *         }
323
     *     >,
324
     *     returnTypes: array{
325
     *         type: null|string,
326
     *         typeFromPhpDoc: null|string,
327
     *         typeFromPhpDocExtended: null|string,
328
     *         typeFromPhpDocSimple: null|string,
329
     *         typeFromPhpDocMaybeWithComment: null|string
330
     *     },
331
     *     paramsPhpDocRaw: array<string, null|string>,
332
     *     returnPhpDocRaw: null|string
333
     * }>
334
     */
335
    public function getMethodsInfo(
336
        array $access = ['public', 'protected', 'private'],
337
        bool $skipDeprecatedMethods = false,
338
        bool $skipMethodsWithLeadingUnderscore = false
339
    ): array {
340
        // init
341
        $allInfo = [];
40✔
342

343
        foreach ($this->methods as $method) {
40✔
344
            if (!\in_array($method->access, $access, true)) {
40✔
345
                continue;
×
346
            }
347

348
            if ($skipDeprecatedMethods && $method->hasDeprecatedTag) {
40✔
349
                continue;
×
350
            }
351

352
            if ($skipMethodsWithLeadingUnderscore && \strpos($method->name, '_') === 0) {
40✔
353
                continue;
×
354
            }
355

356
            $paramsTypes = [];
40✔
357
            foreach ($method->parameters as $tagParam) {
40✔
358
                $paramsTypes[$tagParam->name]['type'] = $tagParam->type;
40✔
359
                $paramsTypes[$tagParam->name]['typeFromPhpDocMaybeWithComment'] = $tagParam->typeFromPhpDocMaybeWithComment;
40✔
360
                $paramsTypes[$tagParam->name]['typeFromPhpDoc'] = $tagParam->typeFromPhpDoc;
40✔
361
                $paramsTypes[$tagParam->name]['typeFromPhpDocSimple'] = $tagParam->typeFromPhpDocSimple;
40✔
362
                $paramsTypes[$tagParam->name]['typeFromPhpDocExtended'] = $tagParam->typeFromPhpDocExtended;
40✔
363
                $paramsTypes[$tagParam->name]['typeFromDefaultValue'] = $tagParam->typeFromDefaultValue;
40✔
364
            }
365

366
            $returnTypes = [];
40✔
367
            $returnTypes['type'] = $method->returnType;
40✔
368
            $returnTypes['typeFromPhpDocMaybeWithComment'] = $method->returnTypeFromPhpDocMaybeWithComment;
40✔
369
            $returnTypes['typeFromPhpDoc'] = $method->returnTypeFromPhpDoc;
40✔
370
            $returnTypes['typeFromPhpDocSimple'] = $method->returnTypeFromPhpDocSimple;
40✔
371
            $returnTypes['typeFromPhpDocExtended'] = $method->returnTypeFromPhpDocExtended;
40✔
372

373
            $paramsPhpDocRaw = [];
40✔
374
            foreach ($method->parameters as $tagParam) {
40✔
375
                $paramsPhpDocRaw[$tagParam->name] = $tagParam->phpDocRaw;
40✔
376
            }
377

378
            $infoTmp = [];
40✔
379
            $infoTmp['fullDescription'] = \trim($method->summary . "\n\n" . $method->description);
40✔
380
            $infoTmp['paramsTypes'] = $paramsTypes;
40✔
381
            $infoTmp['returnTypes'] = $returnTypes;
40✔
382
            $infoTmp['paramsPhpDocRaw'] = $paramsPhpDocRaw;
40✔
383
            $infoTmp['returnPhpDocRaw'] = $method->returnPhpDocRaw;
40✔
384
            $infoTmp['line'] = $method->line ?? $this->line;
40✔
385
            $infoTmp['file'] = $method->file ?? $this->file;
40✔
386
            $infoTmp['error'] = \implode("\n", $method->parseError);
40✔
387
            foreach ($method->parameters as $parameter) {
40✔
388
                $infoTmp['error'] .= ($infoTmp['error'] ? "\n" : '') . \implode("\n", $parameter->parseError);
40✔
389
            }
390
            $infoTmp['is_deprecated'] = $method->hasDeprecatedTag;
40✔
391
            $infoTmp['is_static'] = $method->is_static;
40✔
392
            $infoTmp['is_meta'] = $method->hasMetaTag;
40✔
393
            $infoTmp['is_internal'] = $method->hasInternalTag;
40✔
394
            $infoTmp['is_removed'] = $method->hasRemovedTag;
40✔
395

396
            $allInfo[$method->name] = $infoTmp;
40✔
397
        }
398

399
        \asort($allInfo);
40✔
400

401
        return $allInfo;
40✔
402
    }
403

404
    /**
405
     * @param Doc|string $doc
406
     */
407
    private function readPhpDocProperties($doc): void
408
    {
409
        if ($doc instanceof Doc) {
305✔
410
            $docComment = $doc->getText();
×
411
        } else {
412
            $docComment = $doc;
305✔
413
        }
414
        if ($docComment === '') {
305✔
415
            return;
×
416
        }
417

418
        try {
419
            $phpDoc = DocFactoryProvider::getDocFactory()->create($docComment);
305✔
420

421
            $parsedPropertyTags = $phpDoc->getTagsByName('property')
305✔
422
                               + $phpDoc->getTagsByName('property-read')
305✔
423
                               + $phpDoc->getTagsByName('property-write');
305✔
424

425
            if (!empty($parsedPropertyTags)) {
305✔
426
                foreach ($parsedPropertyTags as $parsedPropertyTag) {
305✔
427
                    if (
428
                        $parsedPropertyTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\PropertyRead
90✔
429
                        ||
430
                        $parsedPropertyTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\PropertyWrite
90✔
431
                        ||
432
                        $parsedPropertyTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Property
90✔
433
                    ) {
434
                        $propertyPhp = new PHPProperty($this->parserContainer);
90✔
435

436
                        $nameTmp = $parsedPropertyTag->getVariableName();
90✔
437
                        if (!$nameTmp) {
90✔
438
                            continue;
×
439
                        }
440

441
                        $propertyPhp->name = $nameTmp;
90✔
442

443
                        $propertyPhp->access = 'public';
90✔
444

445
                        $type = $parsedPropertyTag->getType();
90✔
446

447
                        $propertyPhp->typeFromPhpDoc = Utils::normalizePhpType($type . '');
90✔
448

449
                        $typeFromPhpDocMaybeWithCommentTmp = \trim((string) $parsedPropertyTag);
90✔
450
                        if (
451
                            $typeFromPhpDocMaybeWithCommentTmp
90✔
452
                            &&
453
                            \strpos($typeFromPhpDocMaybeWithCommentTmp, '$') !== 0
90✔
454
                        ) {
455
                            $propertyPhp->typeFromPhpDocMaybeWithComment = $typeFromPhpDocMaybeWithCommentTmp;
90✔
456
                        }
457

458
                        $typeTmp = Utils::parseDocTypeObject($type);
90✔
459
                        if ($typeTmp !== '') {
90✔
460
                            $propertyPhp->typeFromPhpDocSimple = $typeTmp;
90✔
461
                        }
462

463
                        if ($propertyPhp->typeFromPhpDoc) {
90✔
464
                            $propertyPhp->typeFromPhpDocExtended = Utils::modernPhpdoc($propertyPhp->typeFromPhpDoc);
90✔
465
                        }
466

467
                        $this->properties[$propertyPhp->name] = $propertyPhp;
90✔
468
                    }
469
                }
470
            }
471
        } catch (\Exception $e) {
×
472
            $tmpErrorMessage = ($this->name ?: '?') . ':' . ($this->line ?? '?') . ' | ' . \print_r($e->getMessage(), true);
×
473
            $this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage;
×
474
        }
475
    }
476

477
    private function addPromotedPropertiesFromConstructor(Class_ $node): void
478
    {
479
        $method = $node->getMethod('__construct');
611✔
480
        if ($method === null) {
611✔
481
            return;
558✔
482
        }
483

484
        foreach ($method->params as $parameter) {
153✔
485
            if (!self::isPromotedParameter($parameter)) {
153✔
486
                continue;
48✔
487
            }
488

489
            $parameterVar = $parameter->var;
135✔
490
            if (
491
                !($parameterVar instanceof \PhpParser\Node\Expr\Variable)
135✔
492
                || !\is_string($parameterVar->name)
135✔
493
            ) {
494
                continue;
×
495
            }
496

497
            $promotedProperty = (new PHPProperty($this->parserContainer))
135✔
498
                ->readObjectFromPromotedParam($parameter, $this->name);
135✔
499

500
            $propertyName = $parameterVar->name;
135✔
501
            $existingProperty = $this->properties[$propertyName] ?? null;
135✔
502
            if ($existingProperty !== null) {
135✔
503
                $this->mergePromotedPropertyData($existingProperty, $promotedProperty, $parameter);
111✔
504

505
                continue;
111✔
506
            }
507

508
            $this->properties[$propertyName] = $promotedProperty;
42✔
509
        }
510
    }
511

512
    private function mergePromotedPropertyData(
513
        PHPProperty $existingProperty,
514
        PHPProperty $promotedProperty,
515
        \PhpParser\Node\Param $parameter
516
    ): void {
517
        if ($promotedProperty->access !== '') {
111✔
518
            $existingProperty->access = $promotedProperty->access;
111✔
519
        }
520

521
        if ($existingProperty->type === null && $promotedProperty->type !== null) {
111✔
522
            $existingProperty->type = $promotedProperty->type;
10✔
523
        }
524

525
        if ($existingProperty->is_readonly === null && $promotedProperty->is_readonly !== null) {
111✔
526
            $existingProperty->is_readonly = $promotedProperty->is_readonly;
10✔
527
        }
528

529
        if ($existingProperty->is_final === null && $promotedProperty->is_final !== null) {
111✔
530
            $existingProperty->is_final = $promotedProperty->is_final;
70✔
531
        }
532

533
        if ($existingProperty->access_set === '' && $promotedProperty->access_set !== '') {
111✔
534
            $existingProperty->access_set = $promotedProperty->access_set;
×
535
        }
536

537
        // AST is the ground truth for promoted-property hooks; always prefer it
538
        // when the param node carries hook data. Only fall back to whatever
539
        // reflection already populated when the AST node has nothing.
540
        if ($promotedProperty->hooks !== []) {
111✔
UNCOV
541
            $existingProperty->hooks = $promotedProperty->hooks;
4✔
542
        }
543

544
        if ($existingProperty->attributes === [] && $promotedProperty->attributes !== []) {
111✔
545
            $existingProperty->attributes = $promotedProperty->attributes;
×
546
        }
547

548
        if ($parameter->default !== null && $promotedProperty->typeFromDefaultValue !== null) {
111✔
549
            $existingProperty->defaultValue = $promotedProperty->defaultValue;
91✔
550
            $existingProperty->typeFromDefaultValue = $promotedProperty->typeFromDefaultValue;
91✔
551
        }
552
    }
553
}
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