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

voku / Simple-PHP-Code-Parser / 24284942833

11 Apr 2026 02:52PM UTC coverage: 81.002% (-1.9%) from 82.886%
24284942833

Pull #83

github

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

162 of 207 new or added lines in 7 files covered. (78.26%)

33 existing lines in 4 files now uncovered.

1633 of 2016 relevant lines covered (81.0%)

23.77 hits per line

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

88.68
/src/voku/SimplePhpParser/Parsers/Helper/Utils.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace voku\SimplePhpParser\Parsers\Helper;
6

7
use PhpParser\Node\Expr\UnaryMinus;
8
use RecursiveArrayIterator;
9
use RecursiveIteratorIterator;
10
use ReflectionClass;
11
use ReflectionFunction;
12
use voku\SimplePhpParser\Model\PHPAttribute;
13

14
final class Utils
15
{
16
    public const GET_PHP_PARSER_VALUE_FROM_NODE_HELPER = '!!!_SIMPLE_PHP_CODE_PARSER_HELPER_!!!';
17

18
    /**
19
     * @param array<mixed> $arr
20
     * @param bool  $group
21
     *
22
     * @return array<int|string, mixed>
23
     */
24
    public static function flattenArray(array $arr, bool $group): array
25
    {
26
        return \iterator_to_array(new RecursiveIteratorIterator(new RecursiveArrayIterator($arr)), $group);
96✔
27
    }
28

29
    /**
30
     * @param \phpDocumentor\Reflection\DocBlock\Tag $parsedParamTag
31
     *
32
     * @return array{parsedParamTagStr: string, variableName: null|string}
33
     */
34
    public static function splitTypeAndVariable(\phpDocumentor\Reflection\DocBlock\Tag $parsedParamTag): array
35
    {
36
        $parsedParamTagStr = $parsedParamTag . '';
46✔
37
        $variableName = null;
46✔
38

39
        if (\strpos($parsedParamTagStr, '$') !== false) {
46✔
40
            \preg_match('#\$(?<variableName>[^ ]*)#u', $parsedParamTagStr, $variableNameHelper);
46✔
41
            if (isset($variableNameHelper['variableName'])) {
46✔
42
                $variableName = $variableNameHelper['variableName'];
46✔
43
            }
44
            $parsedParamTagStr = \str_replace(
46✔
45
                (string) $variableName,
46✔
46
                '',
46✔
47
                $parsedParamTagStr
46✔
48
            );
46✔
49
        }
50

51
        // clean-up
52
        if ($variableName) {
46✔
53
            $variableName = \str_replace('$', '', $variableName);
46✔
54
        }
55

56
        $parsedParamTagStr = \trim($parsedParamTagStr);
46✔
57

58
        return [
46✔
59
            'parsedParamTagStr' => $parsedParamTagStr,
46✔
60
            'variableName'      => $variableName,
46✔
61
        ];
46✔
62
    }
63

64
    /**
65
     * @param \PhpParser\Node\Arg|\PhpParser\Node\Const_|\PhpParser\Node\Expr $node
66
     * @param string|null                                                     $classStr
67
     * @param \voku\SimplePhpParser\Parsers\Helper\ParserContainer|null       $parserContainer
68
     *
69
     * @phpstan-param class-string|null                                         $classStr
70
     *
71
     * @return mixed
72
     *               Will return "Utils::GET_PHP_PARSER_VALUE_FROM_NODE_HELPER" if we can't get the default value
73
     */
74
    public static function getPhpParserValueFromNode(
75
        $node,
76
        ?string $classStr = null,
77
        ?ParserContainer $parserContainer = null
78
    ) {
79
        if (\property_exists($node, 'value')) {
74✔
80
            /** @psalm-suppress UndefinedPropertyFetch - false-positive ? */
81
            if (\is_object($node->value)) {
64✔
82
                \assert($node->value instanceof \PhpParser\Node);
83

84
                if (
85
                    \in_array('value', $node->value->getSubNodeNames(), true)
50✔
86
                    &&
87
                    \property_exists($node->value, 'value')
50✔
88
                ) {
89
                    return $node->value->value;
50✔
90
                }
91

92
                if (
93
                    \in_array('expr', $node->value->getSubNodeNames(), true)
43✔
94
                    &&
95
                    \property_exists($node->value, 'expr')
43✔
96
                ) {
97
                    $exprTmp = $node->value->expr;
6✔
98
                    if (\property_exists($exprTmp, 'value')) {
6✔
99
                        if ($node->value instanceof UnaryMinus) {
6✔
100
                            return -$exprTmp->value;
6✔
101
                        }
102

103
                        return $exprTmp->value;
×
104
                    }
105
                }
106

107
                if (
108
                    \in_array('name', $node->value->getSubNodeNames(), true)
40✔
109
                    &&
110
                    \property_exists($node->value, 'name')
40✔
111
                    &&
112
                    $node->value->name
40✔
113
                ) {
114
                    if ($node->value->name instanceof \PhpParser\Node\Name) {
21✔
115
                        $value = $node->value->name->toString();
9✔
116
                    } else {
117
                        $value = \is_string($node->value->name) ? $node->value->name : (string) $node->value->name;
18✔
118
                    }
119
                    return $value === 'null' ? null : $value;
21✔
120
                }
121
            }
122

123
            /**
124
             * @psalm-suppress UndefinedPropertyFetch - false-positive from psalm
125
             */
126
            return $node->value;
57✔
127
        }
128

129
        if ($node instanceof \PhpParser\Node\Expr\Array_) {
53✔
130
            $defaultValue = [];
18✔
131
            foreach ($node->items as $item) {
18✔
132
                if ($item === null) {
18✔
133
                    continue;
×
134
                }
135

136
                $defaultValue[] = self::getPhpParserValueFromNode($item->value);
18✔
137
            }
138

139
            return $defaultValue;
18✔
140
        }
141

142
        if ($node instanceof \PhpParser\Node\Expr\ClassConstFetch) {
53✔
143
            \assert($node->class instanceof \PhpParser\Node\Name);
144
            \assert($node->name instanceof \PhpParser\Node\Identifier);
145
            $className = $node->class->toString();
27✔
146
            $constantName = $node->name->name;
27✔
147

148
            if ($className === 'self' || $className === 'static') {
27✔
149
                if ($classStr === null || $parserContainer === null) {
21✔
150
                    return self::GET_PHP_PARSER_VALUE_FROM_NODE_HELPER;
×
151
                }
152

153
                $className = self::findParentClassDeclaringConstant($classStr, $constantName, $parserContainer);
21✔
154
            }
155

156
            $className = '\\' . \ltrim($className, '\\');
27✔
157

158
            if ($node->name->name === 'class') {
27✔
159
                return $className;
9✔
160
            }
161

162
            if (\class_exists($className, true)) {
21✔
163
                return \constant($className . '::' . $node->name->name);
21✔
164
            }
165
        }
166

167
        if ($node instanceof \PhpParser\Node\Expr\ConstFetch) {
47✔
168
            $nameStr = $node->name->toString();
47✔
169
            $parts = explode('\\', $nameStr);
47✔
170

171
            $returnTmp = \strtolower($parts[0]);
47✔
172
            if ($returnTmp === 'true') {
47✔
173
                return true;
18✔
174
            }
175
            if ($returnTmp === 'false') {
35✔
176
                return false;
18✔
177
            }
178
            if ($returnTmp === 'null') {
23✔
179
                return null;
23✔
180
            }
181

182
            $constantNameTmp = '\\' . $nameStr;
9✔
183
            if (\defined($constantNameTmp)) {
9✔
184
                return \constant($constantNameTmp);
9✔
185
            }
186
        }
187

188
        return self::GET_PHP_PARSER_VALUE_FROM_NODE_HELPER;
9✔
189
    }
190

191
    public static function normalizePhpType(string $type_string, bool $sort = false): ?string
192
    {
193
        $type_string_lower = \strtolower($type_string);
118✔
194

195
        /** @noinspection PhpSwitchCaseWithoutDefaultBranchInspection */
196
        switch ($type_string_lower) {
197
            case 'int':
118✔
198
            case 'void':
118✔
199
            case 'float':
118✔
200
            case 'string':
118✔
201
            case 'bool':
109✔
202
            case 'callable':
109✔
203
            case 'iterable':
109✔
204
            case 'array':
109✔
205
            case 'object':
109✔
206
            case 'true':
106✔
207
            case 'false':
106✔
208
            case 'null':
106✔
209
            case 'mixed':
103✔
210
            case 'never':
103✔
211
                return $type_string_lower;
103✔
212
        }
213

214
        /** @noinspection PhpSwitchCaseWithoutDefaultBranchInspection */
215
        switch ($type_string_lower) {
216
            case 'boolean':
99✔
217
                return 'bool';
33✔
218

219
            case 'integer':
99✔
220
                return 'int';
58✔
221

222
            case 'double':
71✔
223
            case 'real':
71✔
224
                return 'float';
18✔
225
        }
226

227
        if ($type_string === '') {
71✔
228
            return null;
×
229
        }
230

231
        if ($sort && \strpos($type_string, '|') !== false) {
71✔
232
            $type_string_exploded = \explode('|', $type_string);
22✔
233
            sort($type_string_exploded);
22✔
234
            $type_string = \implode('|', $type_string_exploded);
22✔
235
        }
236

237
        return $type_string;
71✔
238
    }
239

240
    /**
241
     * @param \PhpParser\Node|null $typeNode
242
     *
243
     * @return string|null
244
     */
245
    public static function typeNodeToString($typeNode): ?string
246
    {
247
        if ($typeNode === null) {
61✔
248
            return null;
×
249
        }
250

251
        if ($typeNode instanceof \PhpParser\Node\NullableType) {
61✔
252
            $innerType = self::typeNodeToString($typeNode->type);
16✔
253

254
            return $innerType !== null ? 'null|' . $innerType : 'null|mixed';
16✔
255
        }
256

257
        if ($typeNode instanceof \PhpParser\Node\UnionType) {
61✔
258
            $parts = [];
7✔
259

260
            foreach ($typeNode->types as $innerType) {
7✔
261
                if ($innerType instanceof \PhpParser\Node\IntersectionType) {
7✔
262
                    $innerIntersection = self::typeNodeToString($innerType);
4✔
263
                    $parts[] = $innerIntersection !== null ? '(' . $innerIntersection . ')' : 'mixed';
4✔
264

265
                    continue;
4✔
266
                }
267

268
                $parts[] = self::typeNodeToString($innerType) ?? 'mixed';
7✔
269
            }
270

271
            return \implode('|', $parts);
7✔
272
        }
273

274
        if ($typeNode instanceof \PhpParser\Node\IntersectionType) {
61✔
275
            $parts = [];
8✔
276

277
            foreach ($typeNode->types as $innerType) {
8✔
278
                $parts[] = self::typeNodeToString($innerType) ?? 'mixed';
8✔
279
            }
280

281
            return \implode('&', $parts);
8✔
282
        }
283

284
        if ($typeNode instanceof \PhpParser\Node\Name) {
61✔
285
            $typeString = $typeNode->toString();
19✔
286
            if (
287
                $typeString === 'self'
19✔
288
                ||
289
                $typeString === 'static'
19✔
290
                ||
291
                $typeString === 'parent'
19✔
292
            ) {
293
                return $typeString;
×
294
            }
295

296
            return '\\' . \ltrim($typeString, '\\');
19✔
297
        }
298

299
        if ($typeNode instanceof \PhpParser\Node\Identifier) {
61✔
300
            return self::normalizePhpType($typeNode->name) ?? $typeNode->name;
61✔
301
        }
302

303
        if (\method_exists($typeNode, 'toString')) {
×
304
            $typeString = $typeNode->toString();
×
305

306
            return self::normalizePhpType($typeString) ?? $typeString;
×
307
        }
308

309
        if (\property_exists($typeNode, 'name') && $typeNode->name) {
×
310
            $typeString = (string) $typeNode->name;
×
311

312
            return self::normalizePhpType($typeString) ?? $typeString;
×
313
        }
314

315
        return null;
×
316
    }
317

318
    /**
319
     * @param \phpDocumentor\Reflection\Type|\phpDocumentor\Reflection\Type[]|null $type
320
     *
321
     * @return string
322
     *
323
     * @psalm-suppress InvalidReturnType - false-positive from psalm
324
     */
325
    public static function parseDocTypeObject($type): string
326
    {
327
        if ($type === null) {
58✔
328
            return '';
×
329
        }
330

331
        if ($type instanceof \phpDocumentor\Reflection\Types\Object_) {
58✔
332
            return $type->__toString();
40✔
333
        }
334

335
        if (\is_array($type) || $type instanceof \phpDocumentor\Reflection\Types\Compound) {
58✔
336
            $types = [];
48✔
337
            foreach ($type as $subType) {
48✔
338
                $types[] = self::parseDocTypeObject($subType);
48✔
339
            }
340

341
            /**
342
             * @psalm-suppress InvalidReturnStatement - false-positive from psalm
343
             */
344
            return \implode('|', $types);
48✔
345
        }
346

347
        if ($type instanceof \phpDocumentor\Reflection\Types\Array_) {
58✔
348
            $valueTypeTmp = $type->getValueType() . '';
49✔
349
            if ($valueTypeTmp !== 'mixed') {
49✔
350
                if (\strpos($valueTypeTmp, '|') !== false) {
42✔
351
                    $valueTypeTmpExploded = \explode('|', $valueTypeTmp);
21✔
352
                    $valueTypeTmp = '';
21✔
353
                    foreach ($valueTypeTmpExploded as $valueTypeTmpExplodedInner) {
21✔
354
                        $valueTypeTmp .= $valueTypeTmpExplodedInner . '[]|';
21✔
355
                    }
356

357
                    return \rtrim($valueTypeTmp, '|');
21✔
358
                }
359

360
                return $valueTypeTmp . '[]';
36✔
361
            }
362

363
            return 'array';
43✔
364
        }
365

366
        if ($type instanceof \phpDocumentor\Reflection\Types\Null_) {
52✔
367
            return 'null';
45✔
368
        }
369

370
        if ($type instanceof \phpDocumentor\Reflection\Types\Mixed_) {
52✔
371
            return 'mixed';
27✔
372
        }
373

374
        if ($type instanceof \phpDocumentor\Reflection\PseudoTypes\Scalar) {
49✔
375
            return 'string|int|float|bool';
×
376
        }
377

378
        if ($type instanceof \phpDocumentor\Reflection\PseudoTypes\True_) {
49✔
379
            return 'true';
×
380
        }
381

382
        if ($type instanceof \phpDocumentor\Reflection\PseudoTypes\False_) {
49✔
383
            return 'false';
27✔
384
        }
385

386
        if ($type instanceof \phpDocumentor\Reflection\Types\Boolean) {
46✔
387
            return 'bool';
12✔
388
        }
389

390
        if ($type instanceof \phpDocumentor\Reflection\Types\Callable_) {
46✔
391
            return 'callable';
10✔
392
        }
393

394
        if ($type instanceof \phpDocumentor\Reflection\Types\Float_) {
45✔
395
            return 'float';
36✔
396
        }
397

398
        if ($type instanceof \phpDocumentor\Reflection\Types\String_) {
45✔
399
            return 'string';
36✔
400
        }
401

402
        if ($type instanceof \phpDocumentor\Reflection\Types\Integer) {
45✔
403
            return 'int';
45✔
404
        }
405

406
        if ($type instanceof \phpDocumentor\Reflection\Types\Void_) {
12✔
407
            return 'void';
9✔
408
        }
409

410
        if ($type instanceof \phpDocumentor\Reflection\Types\Resource_) {
12✔
411
            return 'resource';
3✔
412
        }
413

414
        return $type . '';
9✔
415
    }
416

417
    public static function createFunctionReflectionInstance(string $functionName): ReflectionFunction
418
    {
419
        static $FUNCTION_REFLECTION_INSTANCE = [];
18✔
420

421
        if (isset($FUNCTION_REFLECTION_INSTANCE[$functionName])) {
18✔
422
            return $FUNCTION_REFLECTION_INSTANCE[$functionName];
12✔
423
        }
424

425
        $reflection = new \ReflectionFunction($functionName);
6✔
426
        $FUNCTION_REFLECTION_INSTANCE[$functionName] = $reflection;
6✔
427

428
        return $reflection;
6✔
429
    }
430

431
    /**
432
     * @phpstan-param class-string $className
433
     *
434
     * @phpstan-return ReflectionClass<object>
435
     */
436
    public static function createClassReflectionInstance(string $className): ReflectionClass
437
    {
438
        static $CLASS_REFLECTION_INSTANCE = [];
78✔
439

440
        if (isset($CLASS_REFLECTION_INSTANCE[$className])) {
78✔
441
            return $CLASS_REFLECTION_INSTANCE[$className];
63✔
442
        }
443

444
        $reflection = new ReflectionClass($className);
34✔
445
        $CLASS_REFLECTION_INSTANCE[$className] = $reflection;
34✔
446

447
        return $reflection;
34✔
448
    }
449

450
    /**
451
     * Factory method for easy instantiation.
452
     *
453
     * @param string[] $additionalTags
454
     *
455
     * @phpstan-param array<string, class-string<\phpDocumentor\Reflection\DocBlock\Tag>> $additionalTags
456
     */
457
    public static function createDocBlockInstance(array $additionalTags = []): \phpDocumentor\Reflection\DocBlockFactoryInterface
458
    {
459
        static $DOC_BLOCK_FACTORY_INSTANCE = null;
×
460

461
        if ($DOC_BLOCK_FACTORY_INSTANCE !== null) {
×
462
            return $DOC_BLOCK_FACTORY_INSTANCE;
×
463
        }
464

465
        $DOC_BLOCK_FACTORY_INSTANCE = \phpDocumentor\Reflection\DocBlockFactory::createInstance($additionalTags);
×
466

467
        return $DOC_BLOCK_FACTORY_INSTANCE;
×
468
    }
469

470
    public static function modernPhpdocTokens(string $input): \PHPStan\PhpDocParser\Parser\TokenIterator
471
    {
472
        static $LAXER = null;
58✔
473

474
        if ($LAXER === null) {
58✔
475
            $config = new \PHPStan\PhpDocParser\ParserConfig([]);
3✔
476
            $LAXER = new \PHPStan\PhpDocParser\Lexer\Lexer($config);
3✔
477
        }
478

479
        return new \PHPStan\PhpDocParser\Parser\TokenIterator($LAXER->tokenize($input));
58✔
480
    }
481

482
    /**
483
     * @throws \PHPStan\PhpDocParser\Parser\ParserException
484
     */
485
    public static function modernPhpdoc(string $input): string
486
    {
487
        static $TYPE_PARSER = null;
58✔
488

489
        if ($TYPE_PARSER === null) {
58✔
490
            $config = new \PHPStan\PhpDocParser\ParserConfig([]);
3✔
491
            $TYPE_PARSER = new \PHPStan\PhpDocParser\Parser\TypeParser($config, new \PHPStan\PhpDocParser\Parser\ConstExprParser($config));
3✔
492
        }
493

494
        $tokens = self::modernPhpdocTokens($input);
58✔
495
        $typeNode = $TYPE_PARSER->parse($tokens);
58✔
496

497
        return \str_replace(
58✔
498
            [
58✔
499
                ' | ',
58✔
500
            ],
58✔
501
            [
58✔
502
                '|',
58✔
503
            ],
58✔
504
            \trim((string) $typeNode, ')(')
58✔
505
        );
58✔
506
    }
507

508
    public static function recoverBrokenPhpdocType(string $input): ?string
509
    {
510
        $parts = [];
12✔
511
        foreach (self::modernPhpdocTokens($input)->getTokens() as $token) {
12✔
512
            if (!empty($token[0])) {
12✔
513
                $parts[] = $token[0];
12✔
514
            }
515
        }
516

517
        for ($i = \count($parts); $i > 0; --$i) {
12✔
518
            $candidate = \trim(\implode('', \array_slice($parts, 0, $i)));
12✔
519
            if ($candidate === '') {
12✔
NEW
520
                return null;
×
521
            }
522

523
            try {
524
                return self::modernPhpdoc($candidate);
12✔
525
            } catch (\PHPStan\PhpDocParser\Parser\ParserException $e) {
12✔
526
                continue;
12✔
527
            }
528
        }
529

NEW
530
        return null;
×
531
    }
532

533
    /**
534
     * Returns number of cpu cores available for parallelisation.
535
     *
536
     * @return int<1, max>
537
     */
538
    public static function getCpuCores(): int
539
    {
540
        static $cores = null;
124✔
541
        if ($cores === null) {
124✔
542
            $cores = (new \Fidry\CpuCoreCounter\CpuCoreCounter())->getAvailableForParallelisation()->availableCpus;
3✔
543
        }
544

545
        return $cores;
124✔
546
    }
547

548
    /**
549
     * Extract PHPAttribute instances from AST node attribute groups.
550
     *
551
     * @param \PhpParser\Node\AttributeGroup[] $attrGroups
552
     *
553
     * @return PHPAttribute[]
554
     */
555
    public static function extractAttributesFromAstNode(array $attrGroups): array
556
    {
557
        $result = [];
31✔
558
        foreach ($attrGroups as $group) {
31✔
559
            foreach ($group->attrs as $attr) {
31✔
560
                // If NameResolver has already resolved the name to FullyQualified,
561
                // use that; otherwise check resolvedName attribute, then fall back
562
                if ($attr->name instanceof \PhpParser\Node\Name\FullyQualified) {
31✔
563
                    $name = $attr->name->toString();
31✔
564
                } else {
565
                    $resolvedName = $attr->name->getAttribute('resolvedName');
×
566
                    if ($resolvedName instanceof \PhpParser\Node\Name) {
×
567
                        $name = $resolvedName->toString();
×
568
                    } else {
569
                        $name = $attr->name->toString();
×
570
                    }
571
                }
572

573
                $arguments = [];
31✔
574
                foreach ($attr->args as $arg) {
31✔
575
                    $argValue = self::getPhpParserValueFromNode($arg);
28✔
576
                    if ($argValue === self::GET_PHP_PARSER_VALUE_FROM_NODE_HELPER) {
28✔
577
                        $argValue = null;
×
578
                    }
579

580
                    if ($arg->name !== null) {
28✔
581
                        $arguments[$arg->name->name] = $argValue;
28✔
582
                    } else {
583
                        $arguments[] = $argValue;
25✔
584
                    }
585
                }
586

587
                $result[] = new PHPAttribute($name, $arguments);
31✔
588
            }
589
        }
590

591
        return $result;
31✔
592
    }
593

594
    /**
595
     * Extract PHPAttribute instances from a Reflection object that supports getAttributes().
596
     *
597
     * @param \ReflectionClass<object>|\ReflectionMethod|\ReflectionProperty|\ReflectionClassConstant|\ReflectionParameter|\ReflectionFunction $reflection
598
     *
599
     * @return PHPAttribute[]
600
     */
601
    public static function extractAttributesFromReflection($reflection): array
602
    {
603
        $result = [];
81✔
604
        foreach ($reflection->getAttributes() as $attr) {
81✔
605
            $result[] = new PHPAttribute($attr->getName(), $attr->getArguments());
26✔
606
        }
607

608
        return $result;
81✔
609
    }
610

611
    private static function findParentClassDeclaringConstant(
612
        string $classStr,
613
        string $constantName,
614
        ParserContainer $parserContainer
615
    ): string {
616
        do {
617
            $class = $parserContainer->getClass($classStr);
21✔
618
            if ($class && $class->name && isset($class->constants[$constantName])) {
21✔
619
                return $class->name;
×
620
            }
621

622
            if ($class && $class->parentClass) {
21✔
623
                $class = $parserContainer->getClass($class->parentClass);
×
624
            }
625
        } while ($class);
21✔
626

627
        return $classStr;
21✔
628
    }
629
}
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