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

voku / Simple-PHP-Code-Parser / 5703591739

pending completion
5703591739

push

github

voku
Merge branch 'master' of ssh://github.com/voku/Simple-PHP-Code-Parser

* 'master' of ssh://github.com/voku/Simple-PHP-Code-Parser:
  Update actions/cache action to v3
  Apply fixes from StyleCI
  Update codecov/codecov-action action to v3
  Update shivammathur/setup-php action to v2.24.0
  Update actions/cache action to v2.1.8

3 of 3 new or added lines in 1 file covered. (100.0%)

1293 of 1572 relevant lines covered (82.25%)

20.24 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(
15✔
44
            $code,
15✔
45
            $autoloaderProjectPaths
15✔
46
        );
15✔
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);
3✔
62

63
        return self::getPhpFiles(
3✔
64
            (string) $reflectionClass->getFileName(),
3✔
65
            $autoloaderProjectPaths
3✔
66
        );
3✔
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) {
53✔
84
            if (\file_exists($projectPath) && \is_file($projectPath)) {
×
85
                /** @noinspection PhpIncludeInspection */
86
                require_once $projectPath;
×
87
            } elseif (\file_exists($projectPath . '/vendor/autoload.php')) {
×
88
                /** @noinspection PhpIncludeInspection */
89
                require_once $projectPath . '/vendor/autoload.php';
×
90
            } elseif (\file_exists($projectPath . '/../vendor/autoload.php')) {
×
91
                /** @noinspection PhpIncludeInspection */
92
                require_once $projectPath . '/../vendor/autoload.php';
×
93
            }
94
        }
95
        \restore_error_handler();
53✔
96

97
        $phpCodes = self::getCode(
53✔
98
            $pathOrCode,
53✔
99
            $pathExcludeRegex,
53✔
100
            $fileExtensions
53✔
101
        );
53✔
102

103
        $parserContainer = new ParserContainer();
53✔
104
        $visitor = new ASTVisitor($parserContainer);
53✔
105

106
        $processResults = [];
53✔
107
        $phpCodesChunks = \array_chunk($phpCodes, Utils::getCpuCores(), true);
53✔
108

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

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

132
        $interfaces = $parserContainer->getInterfaces();
53✔
133
        foreach ($interfaces as &$interface) {
53✔
134
            $interface->parentInterfaces = $visitor->combineParentInterfaces($interface);
3✔
135
        }
136
        unset($interface);
53✔
137

138
        $pathTmp = null;
53✔
139
        if (\is_file($pathOrCode)) {
53✔
140
            $pathTmp = \realpath(\pathinfo($pathOrCode, \PATHINFO_DIRNAME));
35✔
141
        } elseif (\is_dir($pathOrCode)) {
18✔
142
            $pathTmp = \realpath($pathOrCode);
3✔
143
        }
144

145
        $classesTmp = &$parserContainer->getClassesByReference();
53✔
146
        foreach ($classesTmp as &$classTmp) {
53✔
147
            $classTmp->interfaces = Utils::flattenArray(
41✔
148
                $visitor->combineImplementedInterfaces($classTmp),
41✔
149
                false
41✔
150
            );
41✔
151

152
            self::mergeInheritdocData(
41✔
153
                $classTmp,
41✔
154
                $classesTmp,
41✔
155
                $interfaces,
41✔
156
                $parserContainer
41✔
157
            );
41✔
158
        }
159
        unset($classTmp);
53✔
160

161
        // remove properties / methods / classes from outside of the current file-path-scope
162
        if ($pathTmp) {
53✔
163
            $classesTmp2 = &$parserContainer->getClassesByReference();
38✔
164
            foreach ($classesTmp2 as $classKey => $classTmp2) {
38✔
165
                foreach ($classTmp2->constants as $constantKey => $constant) {
35✔
166
                    if ($constant->file && \strpos($constant->file, $pathTmp) === false) {
23✔
167
                        unset($classTmp2->constants[$constantKey]);
3✔
168
                    }
169
                }
170

171
                foreach ($classTmp2->properties as $propertyKey => $property) {
35✔
172
                    if ($property->file && \strpos($property->file, $pathTmp) === false) {
32✔
173
                        unset($classTmp2->properties[$propertyKey]);
3✔
174
                    }
175
                }
176

177
                foreach ($classTmp2->methods as $methodKey => $method) {
35✔
178
                    if ($method->file && \strpos($method->file, $pathTmp) === false) {
35✔
179
                        unset($classTmp2->methods[$methodKey]);
3✔
180
                    }
181
                }
182

183
                if ($classTmp2->file && \strpos($classTmp2->file, $pathTmp) === false) {
35✔
184
                    unset($classesTmp2[$classKey]);
3✔
185
                }
186
            }
187
        }
188

189
        return $parserContainer;
53✔
190
    }
191

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

221
        $errorHandler = new ParserErrorHandler();
53✔
222

223
        $nameResolver = new NameResolver(
53✔
224
            $errorHandler,
53✔
225
            [
53✔
226
                'preserveOriginalNames' => true,
53✔
227
            ]
53✔
228
        );
53✔
229

230
        /** @var \PhpParser\Node[]|null $parsedCode */
231
        $parsedCode = $parser->parse($phpCode, $errorHandler);
53✔
232

233
        if ($parsedCode === null) {
53✔
234
            return $errorHandler;
×
235
        }
236

237
        $visitor->fileName = $fileName;
53✔
238

239
        $traverser = new NodeTraverser();
53✔
240
        $traverser->addVisitor(new ParentConnector());
53✔
241
        $traverser->addVisitor($nameResolver);
53✔
242
        $traverser->addVisitor($visitor);
53✔
243
        $traverser->traverse($parsedCode);
53✔
244

245
        return $parserContainer;
53✔
246
    }
247

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

269
        // fallback
270
        if (\count($fileExtensions) === 0) {
53✔
271
            $fileExtensions = ['.php'];
53✔
272
        }
273

274
        if (\is_file($pathOrCode)) {
53✔
275
            $phpFileIterators = [new SplFileInfo($pathOrCode)];
35✔
276
        } elseif (\is_dir($pathOrCode)) {
18✔
277
            $phpFileIterators = new RecursiveIteratorIterator(
3✔
278
                new RecursiveDirectoryIterator($pathOrCode, FilesystemIterator::SKIP_DOTS)
3✔
279
            );
3✔
280
        } else {
281
            $cacheKey = self::CACHE_KEY_HELPER . \md5($pathOrCode);
15✔
282

283
            $phpCodes[$cacheKey]['content'] = $pathOrCode;
15✔
284
            $phpCodes[$cacheKey]['fileName'] = null;
15✔
285
        }
286

287
        $cache = new Cache(null, null, false);
53✔
288

289
        $phpFileArray = [];
53✔
290
        foreach ($phpFileIterators as $fileOrCode) {
53✔
291
            $path = $fileOrCode->getRealPath();
38✔
292
            if (!$path) {
38✔
293
                continue;
×
294
            }
295

296
            $fileExtensionFound = false;
38✔
297
            foreach ($fileExtensions as $fileExtension) {
38✔
298
                if (\substr($path, -\strlen($fileExtension)) === $fileExtension) {
38✔
299
                    $fileExtensionFound = true;
38✔
300

301
                    break;
38✔
302
                }
303
            }
304
            if ($fileExtensionFound === false) {
38✔
305
                continue;
×
306
            }
307

308
            foreach ($pathExcludeRegex as $regex) {
38✔
309
                if (\preg_match($regex, $path)) {
3✔
310
                    continue 2;
3✔
311
                }
312
            }
313

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

321
                $phpCodes[$response['cacheKey']]['content'] = $response['content'];
15✔
322
                $phpCodes[$response['cacheKey']]['fileName'] = $response['fileName'];
15✔
323

324
                continue;
15✔
325
            }
326

327
            $phpFileArray[$cacheKey] = $path;
26✔
328
        }
329

330
        $phpFileArrayChunks = \array_chunk($phpFileArray, Utils::getCpuCores(), true);
53✔
331
        foreach ($phpFileArrayChunks as $phpFileArrayChunk) {
53✔
332
            $filesystem = \React\Filesystem\Factory::create();
26✔
333

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

351
            $phpFilePromiseResponses = await(all($phpFilePromises));
26✔
352
            foreach ($phpFilePromiseResponses as $response) {
26✔
353
                $response['content'] = await($response['content']);
26✔
354

355
                assert(is_string($response['content']));
356
                assert(is_string($response['cacheKey']));
357
                assert($response['fileName'] === null || is_string($response['fileName']));
358

359
                $cache->setItem($response['cacheKey'], $response);
26✔
360

361
                $phpCodes[$response['cacheKey']]['content'] = $response['content'];
26✔
362
                $phpCodes[$response['cacheKey']]['fileName'] = $response['fileName'];
26✔
363
            }
364
        }
365

366
        return $phpCodes;
53✔
367
    }
368

369
    /**
370
     * @param \voku\SimplePhpParser\Model\PHPClass   $class
371
     * @param \voku\SimplePhpParser\Model\PHPClass[] $classes
372
     * @param PHPInterface[]                         $interfaces
373
     * @param ParserContainer                        $parserContainer
374
     *
375
     * @return void
376
     */
377
    private static function mergeInheritdocData(
378
        \voku\SimplePhpParser\Model\PHPClass $class,
379
        array $classes,
380
        array $interfaces,
381
        ParserContainer $parserContainer
382
    ): void {
383
        foreach ($class->properties as &$property) {
41✔
384
            if (!$class->parentClass) {
35✔
385
                break;
20✔
386
            }
387

388
            if (!$property->is_inheritdoc) {
26✔
389
                continue;
26✔
390
            }
391

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

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

408
            if (!isset($classes[$class->parentClass]->properties[$property->name])) {
×
409
                continue;
×
410
            }
411

412
            $parentMethod = $classes[$class->parentClass]->properties[$property->name];
×
413

414
            foreach ($property as $key => &$value) {
×
415
                if (
416
                    $value === null
×
417
                    &&
418
                    $parentMethod->{$key} !== null
×
419
                    &&
420
                    \stripos($key, 'type') !== false
×
421
                ) {
422
                    $value = $parentMethod->{$key};
×
423
                }
424
            }
425
        }
426
        unset($property, $value); /* @phpstan-ignore-line ? */
41✔
427

428
        foreach ($class->methods as &$method) {
41✔
429
            if (!$method->is_inheritdoc) {
38✔
430
                continue;
35✔
431
            }
432

433
            foreach ($class->interfaces as $interfaceStr) {
17✔
434
                if (
435
                    !isset($interfaces[$interfaceStr])
17✔
436
                    &&
437
                    \interface_exists($interfaceStr, true)
17✔
438
                ) {
439
                    $reflectionInterfaceTmp = Utils::createClassReflectionInstance($interfaceStr);
14✔
440
                    $interfaceTmp = (new PHPInterface($parserContainer))->readObjectFromReflection($reflectionInterfaceTmp);
14✔
441
                    if ($interfaceTmp->name) {
14✔
442
                        $interfaces[$interfaceTmp->name] = $interfaceTmp;
14✔
443
                    }
444
                }
445

446
                if (!isset($interfaces[$interfaceStr])) {
17✔
447
                    continue;
×
448
                }
449

450
                if (!isset($interfaces[$interfaceStr]->methods[$method->name])) {
17✔
451
                    continue;
14✔
452
                }
453

454
                $interfaceMethod = $interfaces[$interfaceStr]->methods[$method->name];
9✔
455

456
                foreach ($method as $key => &$value) {
9✔
457
                    if (
458
                        $value === null
9✔
459
                        &&
460
                        $interfaceMethod->{$key} !== null
9✔
461
                        &&
462
                        \stripos($key, 'type') !== false
9✔
463
                    ) {
464
                        $value = $interfaceMethod->{$key};
9✔
465
                    }
466

467
                    if ($key === 'parameters') {
9✔
468
                        $parameterCounter = 0;
9✔
469
                        foreach ($value as &$parameter) {
9✔
470
                            ++$parameterCounter;
9✔
471

472
                            \assert($parameter instanceof \voku\SimplePhpParser\Model\PHPParameter);
473

474
                            $interfaceMethodParameter = null;
9✔
475
                            $parameterCounterInterface = 0;
9✔
476
                            foreach ($interfaceMethod->parameters as $parameterInterface) {
9✔
477
                                ++$parameterCounterInterface;
9✔
478

479
                                if ($parameterCounterInterface === $parameterCounter) {
9✔
480
                                    $interfaceMethodParameter = $parameterInterface;
9✔
481
                                }
482
                            }
483

484
                            if (!$interfaceMethodParameter) {
9✔
485
                                continue;
×
486
                            }
487

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

507
            if (!isset($classes[$class->parentClass])) {
17✔
508
                continue;
9✔
509
            }
510

511
            if (!isset($classes[$class->parentClass]->methods[$method->name])) {
11✔
512
                continue;
×
513
            }
514

515
            $parentMethod = $classes[$class->parentClass]->methods[$method->name];
11✔
516

517
            foreach ($method as $key => &$value) {
11✔
518
                if (
519
                    $value === null
11✔
520
                    &&
521
                    $parentMethod->{$key} !== null
11✔
522
                    &&
523
                    \stripos($key, 'type') !== false
11✔
524
                ) {
525
                    $value = $parentMethod->{$key};
11✔
526
                }
527

528
                if ($key === 'parameters') {
11✔
529
                    $parameterCounter = 0;
11✔
530
                    foreach ($value as &$parameter) {
11✔
531
                        ++$parameterCounter;
11✔
532

533
                        \assert($parameter instanceof \voku\SimplePhpParser\Model\PHPParameter);
534

535
                        $parentMethodParameter = null;
11✔
536
                        $parameterCounterParent = 0;
11✔
537
                        foreach ($parentMethod->parameters as $parameterParent) {
11✔
538
                            ++$parameterCounterParent;
11✔
539

540
                            if ($parameterCounterParent === $parameterCounter) {
11✔
541
                                $parentMethodParameter = $parameterParent;
11✔
542
                            }
543
                        }
544

545
                        if (!$parentMethodParameter) {
11✔
546
                            continue;
×
547
                        }
548

549
                        foreach ($parameter as $keyInner => &$valueInner) {
11✔
550
                            if (
551
                                $valueInner === null
11✔
552
                                &&
553
                                $parentMethodParameter->{$keyInner} !== null
11✔
554
                                &&
555
                                \stripos($keyInner, 'type') !== false
11✔
556
                            ) {
557
                                $valueInner = $parentMethodParameter->{$keyInner};
11✔
558
                            }
559
                        }
560
                    }
561
                }
562
            }
563
        }
564
    }
565
}
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