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

voku / Simple-PHP-Code-Parser / 24285518739

11 Apr 2026 03:24PM UTC coverage: 83.966% (+1.1%) from 82.886%
24285518739

Pull #83

github

web-flow
Merge 8b03fb1da into 90e1e60d3
Pull Request #83: Fix CI pipeline: phpunit.xml validation warning, php-parser v4 test skip, and comprehensive type-analysis regression coverage

197 of 222 new or added lines in 7 files covered. (88.74%)

37 existing lines in 4 files now uncovered.

1702 of 2027 relevant lines covered (83.97%)

49.53 hits per line

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

90.67
/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);
139✔
41

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

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

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

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

56
        // Extract PHP 8.0+ attributes
57
        if (!empty($node->attrGroups)) {
139✔
58
            $this->attributes = Utils::extractAttributesFromAstNode($node->attrGroups);
38✔
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);
139✔
64
        $classExists = false;
139✔
65
        if ($canAutoload) {
139✔
66
            try {
67
                if (\class_exists($this->name, true)) {
135✔
68
                    $classExists = true;
135✔
69
                }
70
            } catch (\Throwable $e) {
3✔
71
                // nothing
72
            }
73
        }
74
        if ($classExists) {
139✔
75
            $reflectionClass = Utils::createClassReflectionInstance($this->name);
99✔
76
            $this->readObjectFromReflection($reflectionClass);
99✔
77
        }
78

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

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

89
        $docComment = $node->getDocComment();
139✔
90
        if ($docComment) {
139✔
91
            $this->readPhpDocProperties($docComment->getText());
113✔
92
        }
93

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

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

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

108
        foreach ($node->getMethods() as $method) {
139✔
109
            $methodNameTmp = $method->name->name;
123✔
110

111
            if (isset($this->methods[$methodNameTmp])) {
123✔
112
                $this->methods[$methodNameTmp] = $this->methods[$methodNameTmp]->readObjectFromPhpNode($method, $this->name);
91✔
113
            } else {
114
                $this->methods[$methodNameTmp] = (new PHPMethod($this->parserContainer))->readObjectFromPhpNode($method, $this->name);
40✔
115
            }
116

117
            if (!$this->methods[$methodNameTmp]->file) {
123✔
118
                $this->methods[$methodNameTmp]->file = $this->file;
40✔
119
            }
120
        }
121

122
        $this->addPromotedPropertiesFromConstructor($node);
139✔
123

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

134
        return $this;
139✔
135
    }
136

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

146
        if (!$this->line) {
99✔
147
            $lineTmp = $clazz->getStartLine();
44✔
148
            if ($lineTmp !== false) {
44✔
149
                $this->line = $lineTmp;
28✔
150
            }
151
        }
152

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

158
        $this->is_final = $clazz->isFinal();
99✔
159

160
        $this->is_abstract = $clazz->isAbstract();
99✔
161

162
        if (method_exists($clazz, 'isReadOnly')) {
99✔
163
            $this->is_readonly = $clazz->isReadOnly();
79✔
164
        }
165

166
        $this->is_anonymous = $clazz->isAnonymous();
99✔
167

168
        $this->is_cloneable = $clazz->isCloneable();
99✔
169

170
        $this->is_instantiable = $clazz->isInstantiable();
99✔
171

172
        $this->is_iterable = $clazz->isIterable();
99✔
173

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

177
        $parent = $clazz->getParentClass();
99✔
178
        if ($parent) {
99✔
179
            $this->parentClass = $parent->getName();
44✔
180

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

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

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

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

216
        foreach ($clazz->getMethods() as $method) {
99✔
217
            $methodNameTmp = $method->getName();
95✔
218

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

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

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

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

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

236
        return $this;
99✔
237
    }
238

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

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

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

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

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

281
        return $allInfo;
8✔
282
    }
283

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

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

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

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

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

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

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

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

383
            $allInfo[$method->name] = $infoTmp;
16✔
384
        }
385

386
        \asort($allInfo);
16✔
387

388
        return $allInfo;
16✔
389
    }
390

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

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

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

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

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

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

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

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

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

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

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

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

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

464
    private function addPromotedPropertiesFromConstructor(Class_ $node): void
465
    {
466
        foreach ($node->getMethods() as $method) {
139✔
467
            if ($method->name->name !== '__construct') {
123✔
468
                continue;
106✔
469
            }
470

471
            foreach ($method->params as $parameter) {
49✔
472
                if (!self::isPromotedParameter($parameter)) {
49✔
473
                    continue;
15✔
474
                }
475

476
                $parameterVar = $parameter->var;
42✔
477
                if (
478
                    !($parameterVar instanceof \PhpParser\Node\Expr\Variable)
42✔
479
                    || !\is_string($parameterVar->name)
42✔
480
                ) {
NEW
481
                    continue;
×
482
                }
483

484
                $promotedProperty = (new PHPProperty($this->parserContainer))
42✔
485
                    ->readObjectFromPromotedParam($parameter, $this->name);
42✔
486

487
                $propertyName = $parameterVar->name;
42✔
488
                $existingProperty = $this->properties[$propertyName] ?? null;
42✔
489
                if ($existingProperty !== null) {
42✔
490
                    $this->mergePromotedPropertyData($existingProperty, $promotedProperty, $parameter);
32✔
491

492
                    continue;
32✔
493
                }
494

495
                $this->properties[$propertyName] = $promotedProperty;
16✔
496
            }
497

498
            break;
49✔
499
        }
500
    }
501

502
    private function mergePromotedPropertyData(
503
        PHPProperty $existingProperty,
504
        PHPProperty $promotedProperty,
505
        \PhpParser\Node\Param $parameter
506
    ): void {
507
        if ($existingProperty->access === '' && $promotedProperty->access !== '') {
32✔
NEW
508
            $existingProperty->access = $promotedProperty->access;
×
509
        }
510

511
        if ($existingProperty->type === null && $promotedProperty->type !== null) {
32✔
NEW
512
            $existingProperty->type = $promotedProperty->type;
×
513
        }
514

515
        if ($existingProperty->is_readonly === null && $promotedProperty->is_readonly !== null) {
32✔
NEW
516
            $existingProperty->is_readonly = $promotedProperty->is_readonly;
×
517
        }
518

519
        if ($existingProperty->is_final === null && $promotedProperty->is_final !== null) {
32✔
520
            $existingProperty->is_final = $promotedProperty->is_final;
16✔
521
        }
522

523
        if ($existingProperty->access_set === '' && $promotedProperty->access_set !== '') {
32✔
NEW
524
            $existingProperty->access_set = $promotedProperty->access_set;
×
525
        }
526

527
        if ($existingProperty->hooks === [] && $promotedProperty->hooks !== []) {
32✔
NEW
528
            $existingProperty->hooks = $promotedProperty->hooks;
×
529
        }
530

531
        if ($existingProperty->attributes === [] && $promotedProperty->attributes !== []) {
32✔
NEW
532
            $existingProperty->attributes = $promotedProperty->attributes;
×
533
        }
534

535
        if ($parameter->default !== null && $promotedProperty->typeFromDefaultValue !== null) {
32✔
536
            $existingProperty->defaultValue = $promotedProperty->defaultValue;
28✔
537
            $existingProperty->typeFromDefaultValue = $promotedProperty->typeFromDefaultValue;
28✔
538
        }
539
    }
540
}
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