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

voku / Simple-PHP-Code-Parser / 24283750104

11 Apr 2026 01:44PM UTC coverage: 82.836% (-0.05%) from 82.886%
24283750104

Pull #83

github

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

161 of 206 new or added lines in 7 files covered. (78.16%)

44 existing lines in 5 files now uncovered.

1694 of 2045 relevant lines covered (82.84%)

59.66 hits per line

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

91.41
/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);
167✔
41

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

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

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

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

56
        // Extract PHP 8.0+ attributes
57
        if (!empty($node->attrGroups)) {
167✔
58
            $this->attributes = Utils::extractAttributesFromAstNode($node->attrGroups);
47✔
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
        // PHP < 8.4 raises an error for PHP 8.4+ syntax (property hooks, asymmetric visibility).
65
        // Skip autoloading in those cases; AST data is still read from the node below.
66
        $canAutoload = (\PHP_VERSION_ID >= 80200 || !self::nodeUsesPHP82PlusSyntax($node))
167✔
67
            && (\PHP_VERSION_ID >= 80300 || !self::nodeUsesPHP83PlusSyntax($node))
167✔
68
            && (\PHP_VERSION_ID >= 80400 || !self::containsPHP84PlusSyntax($node));
167✔
69
        $classExists = false;
167✔
70
        if ($canAutoload) {
167✔
71
            try {
72
                if (\class_exists($this->name, true)) {
157✔
73
                    $classExists = true;
157✔
74
                }
75
            } catch (\Throwable $e) {
×
76
                // nothing
77
            }
78
        }
79
        if ($classExists) {
167✔
80
            $reflectionClass = Utils::createClassReflectionInstance($this->name);
116✔
81
            $this->readObjectFromReflection($reflectionClass);
116✔
82
        }
83

84
        $this->collectTags($node);
167✔
85

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

94
        $docComment = $node->getDocComment();
167✔
95
        if ($docComment) {
167✔
96
            $this->readPhpDocProperties($docComment->getText());
136✔
97
        }
98

99
        foreach ($node->getProperties() as $property) {
167✔
100
            $propertyNameTmp = $this->getConstantFQN($property, $property->props[0]->name->name);
103✔
101

102
            if (isset($this->properties[$propertyNameTmp])) {
103✔
103
                $this->properties[$propertyNameTmp] = $this->properties[$propertyNameTmp]->readObjectFromPhpNode($property, $this->name);
90✔
104
            } else {
105
                $this->properties[$propertyNameTmp] = (new PHPProperty($this->parserContainer))->readObjectFromPhpNode($property, $this->name);
19✔
106
            }
107

108
            if ($this->is_readonly) {
103✔
109
                $this->properties[$propertyNameTmp]->is_readonly = true;
8✔
110
            }
111
        }
112

113
        foreach ($node->getMethods() as $method) {
167✔
114
            $methodNameTmp = $method->name->name;
151✔
115

116
            if (isset($this->methods[$methodNameTmp])) {
151✔
117
                $this->methods[$methodNameTmp] = $this->methods[$methodNameTmp]->readObjectFromPhpNode($method, $this->name);
110✔
118
            } else {
119
                $this->methods[$methodNameTmp] = (new PHPMethod($this->parserContainer))->readObjectFromPhpNode($method, $this->name);
51✔
120
            }
121

122
            if (!$this->methods[$methodNameTmp]->file) {
151✔
123
                $this->methods[$methodNameTmp]->file = $this->file;
51✔
124
            }
125
        }
126

127
        $this->addPromotedPropertiesFromConstructor($node);
167✔
128

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

139
        return $this;
167✔
140
    }
141

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

151
        if (!$this->line) {
116✔
152
            $lineTmp = $clazz->getStartLine();
55✔
153
            if ($lineTmp !== false) {
55✔
154
                $this->line = $lineTmp;
35✔
155
            }
156
        }
157

158
        $file = $clazz->getFileName();
116✔
159
        if ($file) {
116✔
160
            $this->file = $file;
116✔
161
        }
162

163
        $this->is_final = $clazz->isFinal();
116✔
164

165
        $this->is_abstract = $clazz->isAbstract();
116✔
166

167
        if (method_exists($clazz, 'isReadOnly')) {
116✔
168
            $this->is_readonly = $clazz->isReadOnly();
76✔
169
        }
170

171
        $this->is_anonymous = $clazz->isAnonymous();
116✔
172

173
        $this->is_cloneable = $clazz->isCloneable();
116✔
174

175
        $this->is_instantiable = $clazz->isInstantiable();
116✔
176

177
        $this->is_iterable = $clazz->isIterable();
116✔
178

179
        // Extract PHP 8.0+ attributes
180
        $this->attributes = Utils::extractAttributesFromReflection($clazz);
116✔
181

182
        $parent = $clazz->getParentClass();
116✔
183
        if ($parent) {
116✔
184
            $this->parentClass = $parent->getName();
55✔
185

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

205
        foreach ($clazz->getProperties() as $property) {
116✔
206
            $propertyPhp = (new PHPProperty($this->parserContainer))->readObjectFromReflection($property);
110✔
207
            $this->properties[$propertyPhp->name] = $propertyPhp;
110✔
208

209
            if ($this->is_readonly) {
110✔
210
                $this->properties[$propertyPhp->name]->is_readonly = true;
6✔
211
            }
212
        }
213

214
        foreach ($clazz->getInterfaceNames() as $interfaceName) {
116✔
215
            /** @noinspection PhpSillyAssignmentInspection - hack for phpstan */
216
            /** @var class-string $interfaceName */
217
            $interfaceName = $interfaceName;
52✔
218
            $this->interfaces[$interfaceName] = $interfaceName;
52✔
219
        }
220

221
        foreach ($clazz->getMethods() as $method) {
116✔
222
            $methodNameTmp = $method->getName();
115✔
223

224
            $this->methods[$methodNameTmp] = (new PHPMethod($this->parserContainer))->readObjectFromReflection($method);
115✔
225

226
            if (!$this->methods[$methodNameTmp]->file) {
115✔
227
                $this->methods[$methodNameTmp]->file = $this->file;
×
228
            }
229
        }
230

231
        foreach ($clazz->getReflectionConstants() as $constant) {
116✔
232
            $constantNameTmp = $constant->getName();
66✔
233

234
            $this->constants[$constantNameTmp] = (new PHPConst($this->parserContainer))->readObjectFromReflection($constant);
66✔
235

236
            if (!$this->constants[$constantNameTmp]->file) {
66✔
237
                $this->constants[$constantNameTmp]->file = $this->file;
×
238
            }
239
        }
240

241
        return $this;
116✔
242
    }
243

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

266
        foreach ($this->properties as $property) {
10✔
267
            if (!\in_array($property->access, $access, true)) {
10✔
268
                continue;
×
269
            }
270

271
            if ($skipMethodsWithLeadingUnderscore && \strpos($property->name, '_') === 0) {
10✔
272
                continue;
×
273
            }
274

275
            $types = [];
10✔
276
            $types['type'] = $property->type;
10✔
277
            $types['typeFromPhpDocMaybeWithComment'] = $property->typeFromPhpDocMaybeWithComment;
10✔
278
            $types['typeFromPhpDoc'] = $property->typeFromPhpDoc;
10✔
279
            $types['typeFromPhpDocSimple'] = $property->typeFromPhpDocSimple;
10✔
280
            $types['typeFromPhpDocExtended'] = $property->typeFromPhpDocExtended;
10✔
281
            $types['typeFromDefaultValue'] = $property->typeFromDefaultValue;
10✔
282

283
            $allInfo[$property->name] = $types;
10✔
284
        }
285

286
        return $allInfo;
10✔
287
    }
288

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

335
        foreach ($this->methods as $method) {
20✔
336
            if (!\in_array($method->access, $access, true)) {
20✔
337
                continue;
×
338
            }
339

340
            if ($skipDeprecatedMethods && $method->hasDeprecatedTag) {
20✔
341
                continue;
×
342
            }
343

344
            if ($skipMethodsWithLeadingUnderscore && \strpos($method->name, '_') === 0) {
20✔
345
                continue;
×
346
            }
347

348
            $paramsTypes = [];
20✔
349
            foreach ($method->parameters as $tagParam) {
20✔
350
                $paramsTypes[$tagParam->name]['type'] = $tagParam->type;
20✔
351
                $paramsTypes[$tagParam->name]['typeFromPhpDocMaybeWithComment'] = $tagParam->typeFromPhpDocMaybeWithComment;
20✔
352
                $paramsTypes[$tagParam->name]['typeFromPhpDoc'] = $tagParam->typeFromPhpDoc;
20✔
353
                $paramsTypes[$tagParam->name]['typeFromPhpDocSimple'] = $tagParam->typeFromPhpDocSimple;
20✔
354
                $paramsTypes[$tagParam->name]['typeFromPhpDocExtended'] = $tagParam->typeFromPhpDocExtended;
20✔
355
                $paramsTypes[$tagParam->name]['typeFromDefaultValue'] = $tagParam->typeFromDefaultValue;
20✔
356
            }
357

358
            $returnTypes = [];
20✔
359
            $returnTypes['type'] = $method->returnType;
20✔
360
            $returnTypes['typeFromPhpDocMaybeWithComment'] = $method->returnTypeFromPhpDocMaybeWithComment;
20✔
361
            $returnTypes['typeFromPhpDoc'] = $method->returnTypeFromPhpDoc;
20✔
362
            $returnTypes['typeFromPhpDocSimple'] = $method->returnTypeFromPhpDocSimple;
20✔
363
            $returnTypes['typeFromPhpDocExtended'] = $method->returnTypeFromPhpDocExtended;
20✔
364

365
            $paramsPhpDocRaw = [];
20✔
366
            foreach ($method->parameters as $tagParam) {
20✔
367
                $paramsPhpDocRaw[$tagParam->name] = $tagParam->phpDocRaw;
20✔
368
            }
369

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

388
            $allInfo[$method->name] = $infoTmp;
20✔
389
        }
390

391
        \asort($allInfo);
20✔
392

393
        return $allInfo;
20✔
394
    }
395

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

410
        try {
411
            $phpDoc = DocFactoryProvider::getDocFactory()->create($docComment);
136✔
412

413
            $parsedPropertyTags = $phpDoc->getTagsByName('property')
136✔
414
                               + $phpDoc->getTagsByName('property-read')
136✔
415
                               + $phpDoc->getTagsByName('property-write');
136✔
416

417
            if (!empty($parsedPropertyTags)) {
136✔
418
                foreach ($parsedPropertyTags as $parsedPropertyTag) {
136✔
419
                    if (
420
                        $parsedPropertyTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\PropertyRead
35✔
421
                        ||
422
                        $parsedPropertyTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\PropertyWrite
35✔
423
                        ||
424
                        $parsedPropertyTag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Property
35✔
425
                    ) {
426
                        $propertyPhp = new PHPProperty($this->parserContainer);
35✔
427

428
                        $nameTmp = $parsedPropertyTag->getVariableName();
35✔
429
                        if (!$nameTmp) {
35✔
430
                            continue;
×
431
                        }
432

433
                        $propertyPhp->name = $nameTmp;
35✔
434

435
                        $propertyPhp->access = 'public';
35✔
436

437
                        $type = $parsedPropertyTag->getType();
35✔
438

439
                        $propertyPhp->typeFromPhpDoc = Utils::normalizePhpType($type . '');
35✔
440

441
                        $typeFromPhpDocMaybeWithCommentTmp = \trim((string) $parsedPropertyTag);
35✔
442
                        if (
443
                            $typeFromPhpDocMaybeWithCommentTmp
35✔
444
                            &&
445
                            \strpos($typeFromPhpDocMaybeWithCommentTmp, '$') !== 0
35✔
446
                        ) {
447
                            $propertyPhp->typeFromPhpDocMaybeWithComment = $typeFromPhpDocMaybeWithCommentTmp;
35✔
448
                        }
449

450
                        $typeTmp = Utils::parseDocTypeObject($type);
35✔
451
                        if ($typeTmp !== '') {
35✔
452
                            $propertyPhp->typeFromPhpDocSimple = $typeTmp;
35✔
453
                        }
454

455
                        if ($propertyPhp->typeFromPhpDoc) {
35✔
456
                            $propertyPhp->typeFromPhpDocExtended = Utils::modernPhpdoc($propertyPhp->typeFromPhpDoc);
35✔
457
                        }
458

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

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

484
        foreach ($node->stmts as $stmt) {
63✔
485
            if ($stmt instanceof \PhpParser\Node\Stmt\ClassMethod) {
61✔
486
                if (self::containsPHP82PlusType($stmt->returnType)) {
57✔
487
                    return true;
8✔
488
                }
489
                foreach ($stmt->params as $param) {
57✔
490
                    if (self::containsPHP82PlusType($param->type)) {
53✔
491
                        return true;
2✔
492
                    }
493
                }
494
            } elseif ($stmt instanceof \PhpParser\Node\Stmt\Property) {
42✔
495
                if (self::containsPHP82PlusType($stmt->type)) {
38✔
496
                    return true;
4✔
497
                }
498
            }
499
        }
500

501
        return false;
57✔
502
    }
503

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

523
        return false;
126✔
524
    }
525

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

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

550
        // DNF types: union type containing an intersection type (PHP 8.2+)
551
        if ($typeNode instanceof \PhpParser\Node\UnionType) {
33✔
552
            foreach ($typeNode->types as $t) {
12✔
553
                if ($t instanceof \PhpParser\Node\IntersectionType || self::containsPHP82PlusType($t)) {
12✔
554
                    return true;
6✔
555
                }
556
            }
557
        }
558

559
        // Recurse into nullable type
560
        if ($typeNode instanceof \PhpParser\Node\NullableType) {
33✔
561
            return self::containsPHP82PlusType($typeNode->type);
13✔
562
        }
563

564
        return false;
30✔
565
    }
566

567
    private function addPromotedPropertiesFromConstructor(Class_ $node): void
568
    {
569
        foreach ($node->getMethods() as $method) {
167✔
570
            if ($method->name->name !== '__construct') {
151✔
571
                continue;
131✔
572
            }
573

574
            foreach ($method->params as $parameter) {
60✔
575
                if (!self::isPromotedParameter($parameter)) {
60✔
576
                    continue;
18✔
577
                }
578

579
                $parameterVar = $parameter->var;
52✔
580
                if (
581
                    !($parameterVar instanceof \PhpParser\Node\Expr\Variable)
52✔
582
                    || !\is_string($parameterVar->name)
52✔
583
                ) {
NEW
584
                    continue;
×
585
                }
586

587
                $promotedProperty = (new PHPProperty($this->parserContainer))
52✔
588
                    ->readObjectFromPromotedParam($parameter, $this->name);
52✔
589

590
                $propertyName = $parameterVar->name;
52✔
591
                $existingProperty = $this->properties[$propertyName] ?? null;
52✔
592
                if ($existingProperty !== null) {
52✔
593
                    $this->mergePromotedPropertyData($existingProperty, $promotedProperty, $parameter);
40✔
594

595
                    continue;
40✔
596
                }
597

598
                $this->properties[$propertyName] = $promotedProperty;
19✔
599
            }
600

601
            break;
60✔
602
        }
603
    }
604

605
    private function mergePromotedPropertyData(
606
        PHPProperty $existingProperty,
607
        PHPProperty $promotedProperty,
608
        \PhpParser\Node\Param $parameter
609
    ): void {
610
        if ($existingProperty->access === '' && $promotedProperty->access !== '') {
40✔
NEW
611
            $existingProperty->access = $promotedProperty->access;
×
612
        }
613

614
        if ($existingProperty->type === null && $promotedProperty->type !== null) {
40✔
NEW
615
            $existingProperty->type = $promotedProperty->type;
×
616
        }
617

618
        if ($existingProperty->is_readonly === null && $promotedProperty->is_readonly !== null) {
40✔
NEW
619
            $existingProperty->is_readonly = $promotedProperty->is_readonly;
×
620
        }
621

622
        if ($existingProperty->is_final === null && $promotedProperty->is_final !== null) {
40✔
623
            $existingProperty->is_final = $promotedProperty->is_final;
32✔
624
        }
625

626
        if ($existingProperty->access_set === '' && $promotedProperty->access_set !== '') {
40✔
NEW
627
            $existingProperty->access_set = $promotedProperty->access_set;
×
628
        }
629

630
        if ($existingProperty->hooks === [] && $promotedProperty->hooks !== []) {
40✔
NEW
631
            $existingProperty->hooks = $promotedProperty->hooks;
×
632
        }
633

634
        if ($existingProperty->attributes === [] && $promotedProperty->attributes !== []) {
40✔
NEW
635
            $existingProperty->attributes = $promotedProperty->attributes;
×
636
        }
637

638
        if ($parameter->default !== null && $promotedProperty->typeFromDefaultValue !== null) {
40✔
639
            $existingProperty->defaultValue = $promotedProperty->defaultValue;
35✔
640
            $existingProperty->typeFromDefaultValue = $promotedProperty->typeFromDefaultValue;
35✔
641
        }
642
    }
643
}
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