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

voku / Simple-PHP-Code-Parser / 5844259483

pending completion
5844259483

push

github

voku
[+]: use "phpstan/phpdoc-parser" for more phpdocs + more tests

40 of 40 new or added lines in 4 files covered. (100.0%)

1340 of 1629 relevant lines covered (82.26%)

28.27 hits per line

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

87.83
/src/voku/SimplePhpParser/Parsers/PhpCodeParser.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace voku\SimplePhpParser\Parsers;
6

7
use FilesystemIterator;
8
use PhpParser\Lexer\Emulative;
9
use PhpParser\NodeTraverser;
10
use PhpParser\NodeVisitor\NameResolver;
11
use PhpParser\ParserFactory;
12
use React\Filesystem\Node\FileInterface;
13
use RecursiveDirectoryIterator;
14
use RecursiveIteratorIterator;
15
use SplFileInfo;
16
use voku\cache\Cache;
17
use voku\SimplePhpParser\Model\PHPInterface;
18
use voku\SimplePhpParser\Parsers\Helper\ParserContainer;
19
use voku\SimplePhpParser\Parsers\Helper\ParserErrorHandler;
20
use voku\SimplePhpParser\Parsers\Helper\Utils;
21
use voku\SimplePhpParser\Parsers\Visitors\ASTVisitor;
22
use voku\SimplePhpParser\Parsers\Visitors\ParentConnector;
23
use function React\Async\await;
24
use function React\Promise\all;
25

26
final class PhpCodeParser
27
{
28
    /**
29
     * @internal
30
     */
31
    private const CACHE_KEY_HELPER = 'simple-php-code-parser-v4-';
32

33
    /**
34
     * @param string   $code
35
     * @param string[] $autoloaderProjectPaths
36
     *
37
     * @return \voku\SimplePhpParser\Parsers\Helper\ParserContainer
38
     */
39
    public static function getFromString(
40
        string $code,
41
        array $autoloaderProjectPaths = []
42
    ): ParserContainer {
43
        return self::getPhpFiles(
20✔
44
            $code,
20✔
45
            $autoloaderProjectPaths
20✔
46
        );
20✔
47
    }
48

49
    /**
50
     * @param string   $className
51
     * @param string[] $autoloaderProjectPaths
52
     *
53
     * @phpstan-param class-string $className
54
     *
55
     * @return \voku\SimplePhpParser\Parsers\Helper\ParserContainer
56
     */
57
    public static function getFromClassName(
58
        string $className,
59
        array $autoloaderProjectPaths = []
60
    ): ParserContainer {
61
        $reflectionClass = Utils::createClassReflectionInstance($className);
4✔
62

63
        return self::getPhpFiles(
4✔
64
            (string) $reflectionClass->getFileName(),
4✔
65
            $autoloaderProjectPaths
4✔
66
        );
4✔
67
    }
68

69
    /**
70
     * @param string   $pathOrCode
71
     * @param string[] $autoloaderProjectPaths
72
     * @param string[] $pathExcludeRegex
73
     * @param string[] $fileExtensions
74
     *
75
     * @return \voku\SimplePhpParser\Parsers\Helper\ParserContainer
76
     */
77
    public static function getPhpFiles(
78
        string $pathOrCode,
79
        array $autoloaderProjectPaths = [],
80
        array $pathExcludeRegex = [],
81
        array $fileExtensions = []
82
    ): ParserContainer {
83
        foreach ($autoloaderProjectPaths as $projectPath) {
73✔
84
            if (\file_exists($projectPath) && \is_file($projectPath)) {
×
85
                require_once $projectPath;
×
86
            } elseif (\file_exists($projectPath . '/vendor/autoload.php')) {
×
87
                require_once $projectPath . '/vendor/autoload.php';
×
88
            } elseif (\file_exists($projectPath . '/../vendor/autoload.php')) {
×
89
                require_once $projectPath . '/../vendor/autoload.php';
×
90
            }
91
        }
92
        \restore_error_handler();
73✔
93

94
        $phpCodes = self::getCode(
73✔
95
            $pathOrCode,
73✔
96
            $pathExcludeRegex,
73✔
97
            $fileExtensions
73✔
98
        );
73✔
99

100
        $parserContainer = new ParserContainer();
73✔
101
        $visitor = new ASTVisitor($parserContainer);
73✔
102

103
        $processResults = [];
73✔
104
        $phpCodesChunks = \array_chunk($phpCodes, Utils::getCpuCores(), true);
73✔
105

106
        foreach ($phpCodesChunks as $phpCodesChunk) {
73✔
107
            foreach ($phpCodesChunk as $codeAndFileName) {
73✔
108
                $processResults[] = self::process(
73✔
109
                    $codeAndFileName['content'],
73✔
110
                    $codeAndFileName['fileName'],
73✔
111
                    $parserContainer,
73✔
112
                    $visitor
73✔
113
                );
73✔
114
            }
115
        }
116

117
        foreach ($processResults as $response) {
73✔
118
            if ($response instanceof ParserContainer) {
73✔
119
                $parserContainer->setTraits($response->getTraits());
73✔
120
                $parserContainer->setClasses($response->getClasses());
73✔
121
                $parserContainer->setInterfaces($response->getInterfaces());
73✔
122
                $parserContainer->setConstants($response->getConstants());
73✔
123
                $parserContainer->setFunctions($response->getFunctions());
73✔
124
            } elseif ($response instanceof ParserErrorHandler) {
×
125
                $parserContainer->setParseError($response);
×
126
            }
127
        }
128

129
        $interfaces = $parserContainer->getInterfaces();
73✔
130
        foreach ($interfaces as &$interface) {
73✔
131
            $interface->parentInterfaces = $visitor->combineParentInterfaces($interface);
4✔
132
        }
133
        unset($interface);
73✔
134

135
        $pathTmp = null;
73✔
136
        if (\is_file($pathOrCode)) {
73✔
137
            $pathTmp = \realpath(\pathinfo($pathOrCode, \PATHINFO_DIRNAME));
49✔
138
        } elseif (\is_dir($pathOrCode)) {
24✔
139
            $pathTmp = \realpath($pathOrCode);
4✔
140
        }
141

142
        $classesTmp = &$parserContainer->getClassesByReference();
73✔
143
        foreach ($classesTmp as &$classTmp) {
73✔
144
            $classTmp->interfaces = Utils::flattenArray(
57✔
145
                $visitor->combineImplementedInterfaces($classTmp),
57✔
146
                false
57✔
147
            );
57✔
148

149
            self::mergeInheritdocData(
57✔
150
                $classTmp,
57✔
151
                $classesTmp,
57✔
152
                $interfaces,
57✔
153
                $parserContainer
57✔
154
            );
57✔
155
        }
156
        unset($classTmp);
73✔
157

158
        // remove properties / methods / classes from outside of the current file-path-scope
159
        if ($pathTmp) {
73✔
160
            $classesTmp2 = &$parserContainer->getClassesByReference();
53✔
161
            foreach ($classesTmp2 as $classKey => $classTmp2) {
53✔
162
                foreach ($classTmp2->constants as $constantKey => $constant) {
49✔
163
                    if ($constant->file && \strpos($constant->file, $pathTmp) === false) {
31✔
164
                        unset($classTmp2->constants[$constantKey]);
4✔
165
                    }
166
                }
167

168
                foreach ($classTmp2->properties as $propertyKey => $property) {
49✔
169
                    if ($property->file && \strpos($property->file, $pathTmp) === false) {
45✔
170
                        unset($classTmp2->properties[$propertyKey]);
4✔
171
                    }
172
                }
173

174
                foreach ($classTmp2->methods as $methodKey => $method) {
49✔
175
                    if ($method->file && \strpos($method->file, $pathTmp) === false) {
49✔
176
                        unset($classTmp2->methods[$methodKey]);
4✔
177
                    }
178
                }
179

180
                if ($classTmp2->file && \strpos($classTmp2->file, $pathTmp) === false) {
49✔
181
                    unset($classesTmp2[$classKey]);
4✔
182
                }
183
            }
184
        }
185

186
        return $parserContainer;
73✔
187
    }
188

189
    /**
190
     * @param string                                               $phpCode
191
     * @param string|null                                          $fileName
192
     * @param \voku\SimplePhpParser\Parsers\Helper\ParserContainer $parserContainer
193
     * @param \voku\SimplePhpParser\Parsers\Visitors\ASTVisitor    $visitor
194
     *
195
     * @return \voku\SimplePhpParser\Parsers\Helper\ParserContainer|\voku\SimplePhpParser\Parsers\Helper\ParserErrorHandler
196
     */
197
    public static function process(
198
        string $phpCode,
199
        ?string $fileName,
200
        ParserContainer $parserContainer,
201
        ASTVisitor $visitor
202
    ) {
203
        $parser = (new ParserFactory())->create(
73✔
204
            ParserFactory::PREFER_PHP7,
73✔
205
            new Emulative(
73✔
206
                [
73✔
207
                    'usedAttributes' => [
73✔
208
                        'comments',
73✔
209
                        'startLine',
73✔
210
                        'endLine',
73✔
211
                        'startTokenPos',
73✔
212
                        'endTokenPos',
73✔
213
                    ],
73✔
214
                ]
73✔
215
            )
73✔
216
        );
73✔
217

218
        $errorHandler = new ParserErrorHandler();
73✔
219

220
        $nameResolver = new NameResolver(
73✔
221
            $errorHandler,
73✔
222
            [
73✔
223
                'preserveOriginalNames' => true,
73✔
224
            ]
73✔
225
        );
73✔
226

227
        /** @var \PhpParser\Node[]|null $parsedCode */
228
        $parsedCode = $parser->parse($phpCode, $errorHandler);
73✔
229

230
        if ($parsedCode === null) {
73✔
231
            return $errorHandler;
×
232
        }
233

234
        $visitor->fileName = $fileName;
73✔
235

236
        $traverser = new NodeTraverser();
73✔
237
        $traverser->addVisitor(new ParentConnector());
73✔
238
        $traverser->addVisitor($nameResolver);
73✔
239
        $traverser->addVisitor($visitor);
73✔
240
        $traverser->traverse($parsedCode);
73✔
241

242
        return $parserContainer;
73✔
243
    }
244

245
    /**
246
     * @param string   $pathOrCode
247
     * @param string[] $pathExcludeRegex
248
     * @param string[] $fileExtensions
249
     *
250
     * @return array
251
     *
252
     * @psalm-return array<string, array{content: string, fileName: null|string}>
253
     */
254
    private static function getCode(
255
        string $pathOrCode,
256
        array $pathExcludeRegex = [],
257
        array $fileExtensions = []
258
    ): array {
259
        // init
260
        $phpCodes = [];
73✔
261
        /** @var SplFileInfo[] $phpFileIterators */
262
        $phpFileIterators = [];
73✔
263
        /** @var \React\Promise\PromiseInterface[] $phpFilePromises */
264
        $phpFilePromises = [];
73✔
265

266
        // fallback
267
        if (\count($fileExtensions) === 0) {
73✔
268
            $fileExtensions = ['.php'];
73✔
269
        }
270

271
        if (\is_file($pathOrCode)) {
73✔
272
            $phpFileIterators = [new SplFileInfo($pathOrCode)];
49✔
273
        } elseif (\is_dir($pathOrCode)) {
24✔
274
            $phpFileIterators = new RecursiveIteratorIterator(
4✔
275
                new RecursiveDirectoryIterator($pathOrCode, FilesystemIterator::SKIP_DOTS)
4✔
276
            );
4✔
277
        } else {
278
            $cacheKey = self::CACHE_KEY_HELPER . \md5($pathOrCode);
20✔
279

280
            $phpCodes[$cacheKey]['content'] = $pathOrCode;
20✔
281
            $phpCodes[$cacheKey]['fileName'] = null;
20✔
282
        }
283

284
        $cache = new Cache(null, null, false);
73✔
285

286
        $phpFileArray = [];
73✔
287
        foreach ($phpFileIterators as $fileOrCode) {
73✔
288
            $path = $fileOrCode->getRealPath();
53✔
289
            if (!$path) {
53✔
290
                continue;
×
291
            }
292

293
            $fileExtensionFound = false;
53✔
294
            foreach ($fileExtensions as $fileExtension) {
53✔
295
                if (\substr($path, -\strlen($fileExtension)) === $fileExtension) {
53✔
296
                    $fileExtensionFound = true;
53✔
297

298
                    break;
53✔
299
                }
300
            }
301
            if ($fileExtensionFound === false) {
53✔
302
                continue;
×
303
            }
304

305
            foreach ($pathExcludeRegex as $regex) {
53✔
306
                if (\preg_match($regex, $path)) {
4✔
307
                    continue 2;
4✔
308
                }
309
            }
310

311
            $cacheKey = self::CACHE_KEY_HELPER . \md5($path) . '--' . \filemtime($path);
53✔
312
            if ($cache->getCacheIsReady() === true && $cache->existsItem($cacheKey)) {
53✔
313
                $response = $cache->getItem($cacheKey);
20✔
314
                /** @noinspection PhpSillyAssignmentInspection - helper for phpstan */
315
                /** @phpstan-var array{content: string, fileName: string, cacheKey: string} $response */
316
                $response = $response;
20✔
317

318
                $phpCodes[$response['cacheKey']]['content'] = $response['content'];
20✔
319
                $phpCodes[$response['cacheKey']]['fileName'] = $response['fileName'];
20✔
320

321
                continue;
20✔
322
            }
323

324
            $phpFileArray[$cacheKey] = $path;
37✔
325
        }
326

327
        $phpFileArrayChunks = \array_chunk($phpFileArray, Utils::getCpuCores(), true);
73✔
328
        foreach ($phpFileArrayChunks as $phpFileArrayChunk) {
73✔
329
            $filesystem = \React\Filesystem\Factory::create();
37✔
330

331
            foreach ($phpFileArrayChunk as $cacheKey => $path) {
37✔
332
                $phpFilePromises[] = $filesystem->detect($path)->then(
37✔
333
                    function (FileInterface $file) use ($path, $cacheKey) {
37✔
334
                        return [
37✔
335
                            'content'  => $file->getContents()->then(static function (string $contents) {
37✔
336
                                return $contents;
37✔
337
                            }),
37✔
338
                            'fileName' => $path,
37✔
339
                            'cacheKey' => $cacheKey,
37✔
340
                        ];
37✔
341
                    },
37✔
342
                    function ($e) {
37✔
343
                        throw $e;
×
344
                    }
37✔
345
                );
37✔
346
            }
347

348
            $phpFilePromiseResponses = await(all($phpFilePromises));
37✔
349
            foreach ($phpFilePromiseResponses as $response) {
37✔
350
                $response['content'] = await($response['content']);
37✔
351

352
                assert(is_string($response['content']));
353
                assert(is_string($response['cacheKey']));
354
                assert($response['fileName'] === null || is_string($response['fileName']));
355

356
                $cache->setItem($response['cacheKey'], $response);
37✔
357

358
                $phpCodes[$response['cacheKey']]['content'] = $response['content'];
37✔
359
                $phpCodes[$response['cacheKey']]['fileName'] = $response['fileName'];
37✔
360
            }
361
        }
362

363
        return $phpCodes;
73✔
364
    }
365

366
    /**
367
     * @param \voku\SimplePhpParser\Model\PHPClass   $class
368
     * @param \voku\SimplePhpParser\Model\PHPClass[] $classes
369
     * @param PHPInterface[]                         $interfaces
370
     * @param ParserContainer                        $parserContainer
371
     */
372
    private static function mergeInheritdocData(
373
        \voku\SimplePhpParser\Model\PHPClass $class,
374
        array $classes,
375
        array $interfaces,
376
        ParserContainer $parserContainer
377
    ): void {
378
        foreach ($class->properties as &$property) {
57✔
379
            if (!$class->parentClass) {
49✔
380
                break;
29✔
381
            }
382

383
            if (!$property->is_inheritdoc) {
35✔
384
                continue;
35✔
385
            }
386

387
            if (
388
                !isset($classes[$class->parentClass])
×
389
                &&
390
                \class_exists($class->parentClass, true)
×
391
            ) {
392
                $reflectionClassTmp = Utils::createClassReflectionInstance($class->parentClass);
×
393
                $classTmp = (new \voku\SimplePhpParser\Model\PHPClass($parserContainer))->readObjectFromReflection($reflectionClassTmp);
×
394
                if ($classTmp->name) {
×
395
                    $classes[$classTmp->name] = $classTmp;
×
396
                }
397
            }
398

399
            if (!isset($classes[$class->parentClass])) {
×
400
                continue;
×
401
            }
402

403
            if (!isset($classes[$class->parentClass]->properties[$property->name])) {
×
404
                continue;
×
405
            }
406

407
            $parentMethod = $classes[$class->parentClass]->properties[$property->name];
×
408

409
            foreach ($property as $key => &$value) {
×
410
                if (
411
                    $value === null
×
412
                    &&
413
                    $parentMethod->{$key} !== null
×
414
                    &&
415
                    \stripos($key, 'type') !== false
×
416
                ) {
417
                    $value = $parentMethod->{$key};
×
418
                }
419
            }
420
        }
421
        unset($property, $value); /* @phpstan-ignore-line ? */
57✔
422

423
        foreach ($class->methods as &$method) {
57✔
424
            if (!$method->is_inheritdoc) {
53✔
425
                continue;
49✔
426
            }
427

428
            foreach ($class->interfaces as $interfaceStr) {
24✔
429
                if (
430
                    !isset($interfaces[$interfaceStr])
24✔
431
                    &&
432
                    \interface_exists($interfaceStr, true)
24✔
433
                ) {
434
                    $reflectionInterfaceTmp = Utils::createClassReflectionInstance($interfaceStr);
20✔
435
                    $interfaceTmp = (new PHPInterface($parserContainer))->readObjectFromReflection($reflectionInterfaceTmp);
20✔
436
                    if ($interfaceTmp->name) {
20✔
437
                        $interfaces[$interfaceTmp->name] = $interfaceTmp;
20✔
438
                    }
439
                }
440

441
                if (!isset($interfaces[$interfaceStr])) {
24✔
442
                    continue;
×
443
                }
444

445
                if (!isset($interfaces[$interfaceStr]->methods[$method->name])) {
24✔
446
                    continue;
19✔
447
                }
448

449
                $interfaceMethod = $interfaces[$interfaceStr]->methods[$method->name];
13✔
450

451
                foreach ($method as $key => &$value) {
13✔
452
                    if (
453
                        $value === null
13✔
454
                        &&
455
                        $interfaceMethod->{$key} !== null
13✔
456
                        &&
457
                        \stripos($key, 'type') !== false
13✔
458
                    ) {
459
                        $value = $interfaceMethod->{$key};
13✔
460
                    }
461

462
                    if ($key === 'parameters') {
13✔
463
                        $parameterCounter = 0;
13✔
464
                        foreach ($value as &$parameter) {
13✔
465
                            ++$parameterCounter;
13✔
466

467
                            \assert($parameter instanceof \voku\SimplePhpParser\Model\PHPParameter);
468

469
                            $interfaceMethodParameter = null;
13✔
470
                            $parameterCounterInterface = 0;
13✔
471
                            foreach ($interfaceMethod->parameters as $parameterInterface) {
13✔
472
                                ++$parameterCounterInterface;
13✔
473

474
                                if ($parameterCounterInterface === $parameterCounter) {
13✔
475
                                    $interfaceMethodParameter = $parameterInterface;
13✔
476
                                }
477
                            }
478

479
                            if (!$interfaceMethodParameter) {
13✔
480
                                continue;
×
481
                            }
482

483
                            foreach ($parameter as $keyInner => &$valueInner) {
13✔
484
                                if (
485
                                    $valueInner === null
13✔
486
                                    &&
487
                                    $interfaceMethodParameter->{$keyInner} !== null
13✔
488
                                    &&
489
                                    \stripos($keyInner, 'type') !== false
13✔
490
                                ) {
491
                                    $valueInner = $interfaceMethodParameter->{$keyInner};
13✔
492
                                }
493
                            }
494
                            unset($valueInner); /* @phpstan-ignore-line ? */
13✔
495
                        }
496
                        unset($parameter);
13✔
497
                    }
498
                }
499
                unset($value); /* @phpstan-ignore-line ? */
13✔
500
            }
501

502
            if (!isset($classes[$class->parentClass])) {
24✔
503
                continue;
13✔
504
            }
505

506
            if (!isset($classes[$class->parentClass]->methods[$method->name])) {
15✔
507
                continue;
×
508
            }
509

510
            $parentMethod = $classes[$class->parentClass]->methods[$method->name];
15✔
511

512
            foreach ($method as $key => &$value) {
15✔
513
                if (
514
                    $value === null
15✔
515
                    &&
516
                    $parentMethod->{$key} !== null
15✔
517
                    &&
518
                    \stripos($key, 'type') !== false
15✔
519
                ) {
520
                    $value = $parentMethod->{$key};
15✔
521
                }
522

523
                if ($key === 'parameters') {
15✔
524
                    $parameterCounter = 0;
15✔
525
                    foreach ($value as &$parameter) {
15✔
526
                        ++$parameterCounter;
15✔
527

528
                        \assert($parameter instanceof \voku\SimplePhpParser\Model\PHPParameter);
529

530
                        $parentMethodParameter = null;
15✔
531
                        $parameterCounterParent = 0;
15✔
532
                        foreach ($parentMethod->parameters as $parameterParent) {
15✔
533
                            ++$parameterCounterParent;
15✔
534

535
                            if ($parameterCounterParent === $parameterCounter) {
15✔
536
                                $parentMethodParameter = $parameterParent;
15✔
537
                            }
538
                        }
539

540
                        if (!$parentMethodParameter) {
15✔
541
                            continue;
×
542
                        }
543

544
                        foreach ($parameter as $keyInner => &$valueInner) {
15✔
545
                            if (
546
                                $valueInner === null
15✔
547
                                &&
548
                                $parentMethodParameter->{$keyInner} !== null
15✔
549
                                &&
550
                                \stripos($keyInner, 'type') !== false
15✔
551
                            ) {
552
                                $valueInner = $parentMethodParameter->{$keyInner};
15✔
553
                            }
554
                        }
555
                    }
556
                }
557
            }
558
        }
559
    }
560
}
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