From 73178d7f90072f5a86f88ebebb089dc910c276ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Hansl=C3=ADk?= Date: Thu, 5 May 2022 18:22:35 +0200 Subject: [PATCH 1/3] FileNodesFetcher can use one instance of NodeTraverser --- .../BetterReflection/SourceLocator/FileNodesFetcher.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php b/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php index c43c0866fb..b2958b37a5 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php +++ b/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php @@ -11,18 +11,19 @@ class FileNodesFetcher { + private NodeTraverser $nodeTraverser; + public function __construct( private CachingVisitor $cachingVisitor, private Parser $parser, ) { + $this->nodeTraverser = new NodeTraverser(); + $this->nodeTraverser->addVisitor($this->cachingVisitor); } public function fetchNodes(string $fileName): FetchedNodesResult { - $nodeTraverser = new NodeTraverser(); - $nodeTraverser->addVisitor($this->cachingVisitor); - $contents = FileReader::read($fileName); try { @@ -32,7 +33,7 @@ public function fetchNodes(string $fileName): FetchedNodesResult return new FetchedNodesResult([], [], []); } $this->cachingVisitor->reset($fileName, $contents); - $nodeTraverser->traverse($ast); + $this->nodeTraverser->traverse($ast); $result = new FetchedNodesResult( $this->cachingVisitor->getClassNodes(), From 48e129f8935063e89b5170fd61cb0ffedfb3b879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Hansl=C3=ADk?= Date: Thu, 5 May 2022 18:25:40 +0200 Subject: [PATCH 2/3] CachingVisitor cleanup --- build/baseline-7.4.neon | 14 -------------- .../SourceLocator/CachingVisitor.php | 16 ++++++++++------ .../SourceLocator/FileNodesFetcher.php | 5 +++-- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/build/baseline-7.4.neon b/build/baseline-7.4.neon index f1efeccdf6..c5554ae3df 100644 --- a/build/baseline-7.4.neon +++ b/build/baseline-7.4.neon @@ -65,20 +65,6 @@ parameters: count: 1 path: ../src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php - - - message: "#^Class PHPStan\\\\Reflection\\\\BetterReflection\\\\SourceLocator\\\\CachingVisitor has an uninitialized property \\$classNodes\\. Give it default value or assign it in the constructor\\.$#" - count: 1 - path: ../src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php - - - - message: "#^Class PHPStan\\\\Reflection\\\\BetterReflection\\\\SourceLocator\\\\CachingVisitor has an uninitialized property \\$functionNodes\\. Give it default value or assign it in the constructor\\.$#" - count: 1 - path: ../src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php - - - - message: "#^Class PHPStan\\\\Reflection\\\\BetterReflection\\\\SourceLocator\\\\CachingVisitor has an uninitialized property \\$constantNodes\\. Give it default value or assign it in the constructor\\.$#" - count: 1 - path: ../src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php - message: "#^Class PHPStan\\\\Reflection\\\\ReflectionProvider\\\\SetterReflectionProviderProvider has an uninitialized property \\$reflectionProvider\\. Give it default value or assign it in the constructor\\.$#" count: 1 diff --git a/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php b/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php index 6d12e5d09e..0c9bc957ef 100644 --- a/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php +++ b/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php @@ -20,13 +20,13 @@ class CachingVisitor extends NodeVisitorAbstract private string $contents; /** @var array>> */ - private array $classNodes; + private array $classNodes = []; /** @var array>> */ - private array $functionNodes; + private array $functionNodes = []; /** @var array>> */ - private array $constantNodes; + private array $constantNodes = []; private ?Node\Stmt\Namespace_ $currentNamespaceNode = null; @@ -146,13 +146,17 @@ public function getConstantNodes(): array return $this->constantNodes; } - public function reset(string $fileName, string $contents): void + public function prepare(string $fileName, string $contents): void + { + $this->fileName = $fileName; + $this->contents = $contents; + } + + public function reset(): void { $this->classNodes = []; $this->functionNodes = []; $this->constantNodes = []; - $this->fileName = $fileName; - $this->contents = $contents; } } diff --git a/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php b/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php index b2958b37a5..1f4447a513 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php +++ b/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php @@ -32,7 +32,8 @@ public function fetchNodes(string $fileName): FetchedNodesResult } catch (ParserErrorsException) { return new FetchedNodesResult([], [], []); } - $this->cachingVisitor->reset($fileName, $contents); + + $this->cachingVisitor->prepare($fileName, $contents); $this->nodeTraverser->traverse($ast); $result = new FetchedNodesResult( @@ -41,7 +42,7 @@ public function fetchNodes(string $fileName): FetchedNodesResult $this->cachingVisitor->getConstantNodes(), ); - $this->cachingVisitor->reset($fileName, $contents); + $this->cachingVisitor->reset(); return $result; } From 90ae3619abfdad161caf0fd979416a0314773c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Hansl=C3=ADk?= Date: Thu, 5 May 2022 15:19:35 +0200 Subject: [PATCH 3/3] OptimizedDirectorySourceLocator based only on PhpParser --- conf/config.neon | 1 + .../OptimizedDirectorySourceLocator.php | 357 +++++++----------- ...OptimizedDirectorySourceLocatorFactory.php | 13 +- .../SourceLocator/PhpFileCleaner.php | 293 -------------- 4 files changed, 152 insertions(+), 512 deletions(-) delete mode 100644 src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php diff --git a/conf/config.neon b/conf/config.neon index 56b7965763..306ea33786 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -822,6 +822,7 @@ services: class: PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorFactory arguments: fileFinder: @fileFinderScan + phpParser: @currentPhpVersionPhpParser - class: PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorRepository diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php index 6c0c211e67..a0b4144048 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php @@ -3,6 +3,9 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; use PhpParser\Node; +use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor\NameResolver; +use PhpParser\Parser; use PHPStan\BetterReflection\Identifier\Identifier; use PHPStan\BetterReflection\Identifier\IdentifierType; use PHPStan\BetterReflection\Reflection\Reflection; @@ -12,32 +15,28 @@ use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ConstantNameHelper; use PHPStan\ShouldNotHappenException; +use Throwable; +use function array_filter; use function array_key_exists; +use function array_keys; use function array_values; -use function count; use function current; -use function ltrim; use function php_strip_whitespace; -use function preg_match_all; -use function preg_replace; -use function sprintf; use function strtolower; class OptimizedDirectorySourceLocator implements SourceLocator { - private PhpFileCleaner $cleaner; + /** @var array */ + private array $classToFile = []; - private string $extraTypes; + /** @var array */ + private array $constantToFile = []; - /** @var array|null */ - private ?array $classToFile = null; + /** @var array */ + private array $functionToFiles = []; - /** @var array|null */ - private ?array $constantToFile = null; - - /** @var array>|null */ - private ?array $functionToFiles = null; + private NodeTraverser $nodeTraverser; /** * @param string[] $files @@ -46,71 +45,93 @@ public function __construct( private FileNodesFetcher $fileNodesFetcher, private PhpVersion $phpVersion, private array $files, + private Parser $phpParser, + private CachingVisitor $cachingVisitor, ) { - $this->extraTypes = $this->phpVersion->supportsEnums() ? '|enum' : ''; - - $this->cleaner = new PhpFileCleaner(); + $this->nodeTraverser = new NodeTraverser(); + $this->nodeTraverser->addVisitor(new NameResolver()); + $this->nodeTraverser->addVisitor($cachingVisitor); } public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { if ($identifier->isClass()) { $className = strtolower($identifier->getName()); - $file = $this->findFileByClass($className); - if ($file === null) { - return null; + + if (!array_key_exists($className, $this->classToFile)) { + $found = $this->parse(fn () => array_key_exists($className, $this->classToFile)); + + if (!$found) { + // Don't try to search twice + $this->classToFile[$className] = null; + } } - $fetchedClassNodes = $this->fileNodesFetcher->fetchNodes($file)->getClassNodes(); + $file = $this->classToFile[$className]; - if (!array_key_exists($className, $fetchedClassNodes)) { + if ($file === null) { return null; } + $fetchedClassNodes = $this->fileNodesFetcher->fetchNodes($file)->getClassNodes(); + /** @var FetchedNode $fetchedClassNode */ $fetchedClassNode = current($fetchedClassNodes[$className]); + if ($fetchedClassNode->getNode() instanceof Node\Stmt\Enum_ && !$this->phpVersion->supportsEnums()) { + return null; + } + return $this->nodeToReflection($reflector, $fetchedClassNode); } if ($identifier->isFunction()) { $functionName = strtolower($identifier->getName()); - $files = $this->findFilesByFunction($functionName); - $fetchedFunctionNode = null; - foreach ($files as $file) { - $fetchedFunctionNodes = $this->fileNodesFetcher->fetchNodes($file)->getFunctionNodes(); + if (!array_key_exists($functionName, $this->functionToFiles)) { + $found = $this->parse(fn () => array_key_exists($functionName, $this->functionToFiles)); - if (!array_key_exists($functionName, $fetchedFunctionNodes)) { - continue; + if (!$found) { + // Don't try to search twice + $this->functionToFiles[$functionName] = null; } - - /** @var FetchedNode $fetchedFunctionNode */ - $fetchedFunctionNode = current($fetchedFunctionNodes[$functionName]); } - if ($fetchedFunctionNode === null) { + $file = $this->functionToFiles[$functionName]; + + if ($file === null) { return null; } + $fetchedFunctionNodes = $this->fileNodesFetcher->fetchNodes($file)->getFunctionNodes(); + + /** @var FetchedNode $fetchedFunctionNode */ + $fetchedFunctionNode = current($fetchedFunctionNodes[$functionName]); + return $this->nodeToReflection($reflector, $fetchedFunctionNode); } if ($identifier->isConstant()) { $constantName = ConstantNameHelper::normalize($identifier->getName()); - $file = $this->findFileByConstant($constantName); - if ($file === null) { - return null; + if (!array_key_exists($constantName, $this->constantToFile)) { + $found = $this->parse(fn () => array_key_exists($constantName, $this->constantToFile)); + + if (!$found) { + // Don't try to search twice + $this->constantToFile[$constantName] = null; + } } - $fetchedConstantNodes = $this->fileNodesFetcher->fetchNodes($file)->getConstantNodes(); + $file = $this->constantToFile[$constantName]; - if (!array_key_exists($constantName, $fetchedConstantNodes)) { + if ($file === null) { return null; } + $fetchedConstantNodes = $this->fileNodesFetcher->fetchNodes($file)->getConstantNodes(); + /** @var FetchedNode $fetchedConstantNode */ $fetchedConstantNode = current($fetchedConstantNodes[$constantName]); @@ -125,221 +146,121 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): } /** - * @param FetchedNode|FetchedNode|FetchedNode $fetchedNode + * @return list */ - private function nodeToReflection(Reflector $reflector, FetchedNode $fetchedNode, ?int $positionInNode = null): Reflection - { - $nodeToReflection = new NodeToReflection(); - return $nodeToReflection->__invoke( - $reflector, - $fetchedNode->getNode(), - $fetchedNode->getLocatedSource(), - $fetchedNode->getNamespace(), - $positionInNode, - ); - } - - private function findFileByClass(string $className): ?string + public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array { - if ($this->classToFile === null) { - $this->init(); - if ($this->classToFile === null) { - throw new ShouldNotHappenException(); - } - } - - if (!array_key_exists($className, $this->classToFile)) { - return null; - } + $this->parse(); - return $this->classToFile[$className]; - } + $reflections = []; + if ($identifierType->isClass()) { + foreach (array_filter($this->classToFile) as $file) { + $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); + foreach ($fetchedNodesResult->getClassNodes() as $identifierName => $fetchedClassNodes) { + foreach ($fetchedClassNodes as $fetchedClassNode) { + if (!$this->phpVersion->supportsEnums() && $fetchedClassNode->getNode() instanceof Node\Stmt\Enum_) { + continue; + } - private function findFileByConstant(string $constantName): ?string - { - if ($this->constantToFile === null) { - $this->init(); - if ($this->constantToFile === null) { - throw new ShouldNotHappenException(); + $reflections[$identifierName] = $this->nodeToReflection($reflector, $fetchedClassNode); + } + } + } + } elseif ($identifierType->isFunction()) { + foreach (array_filter($this->functionToFiles) as $file) { + $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); + foreach ($fetchedNodesResult->getFunctionNodes() as $identifierName => $fetchedFunctionNodes) { + foreach ($fetchedFunctionNodes as $fetchedFunctionNode) { + $reflections[$identifierName] = $this->nodeToReflection($reflector, $fetchedFunctionNode); + } + } + } + } elseif ($identifierType->isConstant()) { + foreach (array_filter($this->constantToFile) as $file) { + $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); + foreach ($fetchedNodesResult->getConstantNodes() as $identifierName => $fetchedConstantNodes) { + foreach ($fetchedConstantNodes as $fetchedConstantNode) { + $reflections[$identifierName] = $this->nodeToReflection( + $reflector, + $fetchedConstantNode, + $this->findConstantPositionInConstNode($fetchedConstantNode->getNode(), $identifierName), + ); + } + } } } - if (!array_key_exists($constantName, $this->constantToFile)) { - return null; - } - - return $this->constantToFile[$constantName]; + return array_values($reflections); } /** - * @return string[] + * @param (callable(): bool) $isTypeFound */ - private function findFilesByFunction(string $functionName): array + private function parse(?callable $isTypeFound = null): bool { - if ($this->functionToFiles === null) { - $this->init(); - if ($this->functionToFiles === null) { - throw new ShouldNotHappenException(); - } - } - - if (!array_key_exists($functionName, $this->functionToFiles)) { - return []; - } + foreach ($this->files as $filesNo => $file) { + $fetchNodesResult = $this->parseFile($file); - return $this->functionToFiles[$functionName]; - } + foreach (array_keys($fetchNodesResult->getClassNodes()) as $className) { + $this->classToFile[$className] = $file; + } - private function init(): void - { - $classToFile = []; - $constantToFile = []; - $functionToFiles = []; - foreach ($this->files as $file) { - $symbols = $this->findSymbols($file); - foreach ($symbols['classes'] as $classInFile) { - $classToFile[$classInFile] = $file; + foreach (array_keys($fetchNodesResult->getFunctionNodes()) as $functionName) { + $this->functionToFiles[$functionName] = $file; } - foreach ($symbols['constants'] as $constantInFile) { - $constantToFile[$constantInFile] = $file; + + foreach (array_keys($fetchNodesResult->getConstantNodes()) as $constantName) { + $this->constantToFile[$constantName] = $file; } - foreach ($symbols['functions'] as $functionInFile) { - if (!array_key_exists($functionInFile, $functionToFiles)) { - $functionToFiles[$functionInFile] = []; - } - $functionToFiles[$functionInFile][] = $file; + + unset($this->files[$filesNo]); + + if ($isTypeFound !== null && $isTypeFound()) { + return true; } } - $this->classToFile = $classToFile; - $this->functionToFiles = $functionToFiles; - $this->constantToFile = $constantToFile; + return false; } - /** - * Inspired by Composer\Autoload\ClassMapGenerator::findClasses() - * @link https://github.com/composer/composer/blob/45d3e133a4691eccb12e9cd6f9dfd76eddc1906d/src/Composer/Autoload/ClassMapGenerator.php#L216 - * - * @return array{classes: string[], functions: string[], constants: string[]} - */ - private function findSymbols(string $file): array + private function parseFile(string $file): FetchedNodesResult { $contents = @php_strip_whitespace($file); - if ($contents === '') { - return ['classes' => [], 'functions' => [], 'constants' => []]; - } - $matchResults = (bool) preg_match_all(sprintf('{\b(?:(?:class|interface|trait|const|function%s)\s)|(?:define\s*\()}i', $this->extraTypes), $contents, $matches); - if (!$matchResults) { - return ['classes' => [], 'functions' => [], 'constants' => []]; + try { + /** @var Node[] $ast */ + $ast = $this->phpParser->parse($contents); + } catch (Throwable) { + return new FetchedNodesResult([], [], []); } - $contents = $this->cleaner->clean($contents, count($matches[0])); - - preg_match_all(sprintf('{ - (?: - \b(?])(?: - (?: (?Pclass|interface|trait%s) \s++ (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\-]*+) ) - | (?: (?Pfunction) \s++ (?:&\s*)? (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\-]*+) \s*+ [&\(] ) - | (?: (?Pconst) \s++ (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\-]*+) \s*+ [^;] ) - | (?: (?:\\\)? (?Pdefine) \s*+ \( \s*+ [\'"] (?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:[\\\\]{1,2}[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+) ) - | (?: (?Pnamespace) (?P\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;] ) - ) - ) - }ix', $this->extraTypes), $contents, $matches); - - $classes = []; - $functions = []; - $constants = []; - $namespace = ''; - - for ($i = 0, $len = count($matches['type']); $i < $len; $i++) { - if (isset($matches['ns'][$i]) && $matches['ns'][$i] !== '') { - $namespace = preg_replace('~\s+~', '', strtolower($matches['nsname'][$i])) . '\\'; - continue; - } - - if ($matches['function'][$i] !== '') { - $functions[] = strtolower(ltrim($namespace . $matches['fname'][$i], '\\')); - continue; - } - - if ($matches['constant'][$i] !== '') { - $constants[] = ConstantNameHelper::normalize(ltrim($namespace . $matches['cname'][$i], '\\')); - } - - if ($matches['define'][$i] !== '') { - $constants[] = ConstantNameHelper::normalize($matches['dname'][$i]); - continue; - } + $this->cachingVisitor->prepare($file, $contents); + $this->nodeTraverser->traverse($ast); - $name = $matches['name'][$i]; - - // skip anon classes extending/implementing - if ($name === 'extends' || $name === 'implements') { - continue; - } + $result = new FetchedNodesResult( + $this->cachingVisitor->getClassNodes(), + $this->cachingVisitor->getFunctionNodes(), + $this->cachingVisitor->getConstantNodes(), + ); - $classes[] = strtolower(ltrim($namespace . $name, '\\')); - } + $this->cachingVisitor->reset(); - return [ - 'classes' => $classes, - 'functions' => $functions, - 'constants' => $constants, - ]; + return $result; } /** - * @return list + * @param FetchedNode|FetchedNode|FetchedNode $fetchedNode */ - public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array + private function nodeToReflection(Reflector $reflector, FetchedNode $fetchedNode, ?int $positionInNode = null): Reflection { - if ($this->classToFile === null || $this->functionToFiles === null || $this->constantToFile === null) { - $this->init(); - if ($this->classToFile === null || $this->functionToFiles === null || $this->constantToFile === null) { - throw new ShouldNotHappenException(); - } - } - - $reflections = []; - if ($identifierType->isClass()) { - foreach ($this->classToFile as $file) { - $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); - foreach ($fetchedNodesResult->getClassNodes() as $identifierName => $fetchedClassNodes) { - foreach ($fetchedClassNodes as $fetchedClassNode) { - $reflections[$identifierName] = $this->nodeToReflection($reflector, $fetchedClassNode); - } - } - } - } elseif ($identifierType->isFunction()) { - foreach ($this->functionToFiles as $files) { - foreach ($files as $file) { - $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); - foreach ($fetchedNodesResult->getFunctionNodes() as $identifierName => $fetchedFunctionNodes) { - foreach ($fetchedFunctionNodes as $fetchedFunctionNode) { - $reflections[$identifierName] = $this->nodeToReflection($reflector, $fetchedFunctionNode); - continue 2; - } - } - } - } - } elseif ($identifierType->isConstant()) { - foreach ($this->constantToFile as $file) { - $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); - foreach ($fetchedNodesResult->getConstantNodes() as $identifierName => $fetchedConstantNodes) { - foreach ($fetchedConstantNodes as $fetchedConstantNode) { - $reflections[$identifierName] = $this->nodeToReflection( - $reflector, - $fetchedConstantNode, - $this->findConstantPositionInConstNode($fetchedConstantNode->getNode(), $identifierName), - ); - } - } - } - } - - return array_values($reflections); + $nodeToReflection = new NodeToReflection(); + return $nodeToReflection->__invoke( + $reflector, + $fetchedNode->getNode(), + $fetchedNode->getLocatedSource(), + $fetchedNode->getNamespace(), + $positionInNode, + ); } private function findConstantPositionInConstNode(Node\Stmt\Const_|Node\Expr\FuncCall $constantNode, string $constantName): ?int diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php index ec6d1e60db..7dc5b47f34 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php @@ -2,13 +2,20 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use PhpParser\Parser; use PHPStan\File\FileFinder; use PHPStan\Php\PhpVersion; class OptimizedDirectorySourceLocatorFactory { - public function __construct(private FileNodesFetcher $fileNodesFetcher, private FileFinder $fileFinder, private PhpVersion $phpVersion) + public function __construct( + private FileNodesFetcher $fileNodesFetcher, + private FileFinder $fileFinder, + private PhpVersion $phpVersion, + private Parser $phpParser, + private CachingVisitor $cachingVisitor, + ) { } @@ -18,6 +25,8 @@ public function createByDirectory(string $directory): OptimizedDirectorySourceLo $this->fileNodesFetcher, $this->phpVersion, $this->fileFinder->findFiles([$directory])->getFiles(), + $this->phpParser, + $this->cachingVisitor, ); } @@ -30,6 +39,8 @@ public function createByFiles(array $files): OptimizedDirectorySourceLocator $this->fileNodesFetcher, $this->phpVersion, $files, + $this->phpParser, + $this->cachingVisitor, ); } diff --git a/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php b/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php deleted file mode 100644 index d8c2d8a789..0000000000 --- a/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php +++ /dev/null @@ -1,293 +0,0 @@ - - * @see https://github.com/composer/composer/pull/10107 - */ -class PhpFileCleaner -{ - - /** @var array */ - private array $typeConfig = []; - - private string $restPattern; - - private string $contents = ''; - - private int $len = 0; - - private int $index = 0; - - public function __construct() - { - foreach (['class', 'interface', 'trait', 'enum'] as $type) { - $this->typeConfig[$type[0]] = [ - 'name' => $type, - 'length' => strlen($type), - 'pattern' => '{.\b(?])' . $type . '\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\-]*+}Ais', - ]; - } - - $this->restPattern = '{[^{}?"\'typeConfig)) . ']+}A'; - } - - public function clean(string $contents, int $maxMatches): string - { - $this->contents = $contents; - $this->len = strlen($contents); - $this->index = 0; - - $inType = false; - $typeLevel = 0; - - $inDefine = false; - - $clean = ''; - while ($this->index < $this->len) { - $this->skipToPhp(); - $clean .= 'index < $this->len) { - $char = $this->contents[$this->index]; - if ($char === '?' && $this->peek('>')) { - $clean .= '?>'; - $this->index += 2; - continue 2; - } - - if ($char === '"' || $char === "'") { - if ($inDefine) { - $clean .= $char . $this->consumeString($char); - $inDefine = false; - } else { - $this->skipString($char); - $clean .= 'null'; - } - - continue; - } - - if ($char === '{') { - if ($inType) { - $typeLevel++; - } - - $clean .= $char; - $this->index++; - continue; - } - - if ($char === '}') { - if ($inType) { - $typeLevel--; - - if ($typeLevel === 0) { - $inType = false; - } - } - - $clean .= $char; - $this->index++; - continue; - } - - if ($char === '<' && $this->peek('<') && $this->match('{<<<[ \t]*+([\'"]?)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*+)\\1(?:\r\n|\n|\r)}A', $match)) { - $this->index += strlen($match[0]); - $this->skipHeredoc($match[2]); - $clean .= 'null'; - continue; - } - - if ($char === '/') { - if ($this->peek('/')) { - $this->skipToNewline(); - continue; - } - if ($this->peek('*')) { - $this->skipComment(); - continue; - } - } - - if ( - $inType - && $char === 'c' - && $this->match('~.\b(?])const(\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\-]*+)~Ais', $match, $this->index - 1) - ) { - // It's invalid PHP but it does not matter - $clean .= 'class_const' . $match[1]; - $this->index += strlen($match[0]) - 1; - continue; - } - - if ($char === 'd' && $this->match('~.\b(?])define\s*+\(~Ais', $match, $this->index - 1)) { - $inDefine = true; - $clean .= $match[0]; - $this->index += strlen($match[0]) - 1; - continue; - } - - if (isset($this->typeConfig[$char])) { - $type = $this->typeConfig[$char]; - - if (substr($this->contents, $this->index, $type['length']) === $type['name']) { - if ($maxMatches === 1 && $this->match($type['pattern'], $match, $this->index - 1)) { - return $clean . $match[0]; - } - - $inType = true; - } - } - - $this->index += 1; - if ($this->match($this->restPattern, $match)) { - $clean .= $char . $match[0]; - $this->index += strlen($match[0]); - } else { - $clean .= $char; - } - } - } - - return $clean; - } - - private function skipToPhp(): void - { - while ($this->index < $this->len) { - if ($this->contents[$this->index] === '<' && $this->peek('?')) { - $this->index += 2; - break; - } - - $this->index += 1; - } - } - - private function consumeString(string $delimiter): string - { - $string = ''; - - $this->index += 1; - while ($this->index < $this->len) { - if ($this->contents[$this->index] === '\\' && ($this->peek('\\') || $this->peek($delimiter))) { - $string .= $this->contents[$this->index]; - $string .= $this->contents[$this->index + 1]; - - $this->index += 2; - continue; - } - - if ($this->contents[$this->index] === $delimiter) { - $string .= $delimiter; - $this->index += 1; - break; - } - - $string .= $this->contents[$this->index]; - $this->index += 1; - } - - return $string; - } - - private function skipString(string $delimiter): void - { - $this->index += 1; - while ($this->index < $this->len) { - if ($this->contents[$this->index] === '\\' && ($this->peek('\\') || $this->peek($delimiter))) { - $this->index += 2; - continue; - } - if ($this->contents[$this->index] === $delimiter) { - $this->index += 1; - break; - } - $this->index += 1; - } - } - - private function skipComment(): void - { - $this->index += 2; - while ($this->index < $this->len) { - if ($this->contents[$this->index] === '*' && $this->peek('/')) { - $this->index += 2; - break; - } - - $this->index += 1; - } - } - - private function skipToNewline(): void - { - while ($this->index < $this->len) { - if ($this->contents[$this->index] === "\r" || $this->contents[$this->index] === "\n") { - return; - } - $this->index += 1; - } - } - - private function skipHeredoc(string $delimiter): void - { - $firstDelimiterChar = $delimiter[0]; - $delimiterLength = strlen($delimiter); - $delimiterPattern = '{' . preg_quote($delimiter) . '(?![a-zA-Z0-9_\x80-\xff])}A'; - - while ($this->index < $this->len) { - // check if we find the delimiter after some spaces/tabs - switch ($this->contents[$this->index]) { - case "\t": - case ' ': - $this->index += 1; - continue 2; - case $firstDelimiterChar: - if ( - substr($this->contents, $this->index, $delimiterLength) === $delimiter - && $this->match($delimiterPattern) - ) { - $this->index += $delimiterLength; - return; - } - break; - } - - // skip the rest of the line - while ($this->index < $this->len) { - $this->skipToNewline(); - - // skip newlines - while ($this->index < $this->len && ($this->contents[$this->index] === "\r" || $this->contents[$this->index] === "\n")) { - $this->index += 1; - } - - break; - } - } - } - - private function peek(string $char): bool - { - return $this->index + 1 < $this->len && $this->contents[$this->index + 1] === $char; - } - - /** - * @param string[]|null $match - */ - private function match(string $regex, ?array &$match = null, ?int $offset = null): bool - { - return preg_match($regex, $this->contents, $match, 0, $offset ?? $this->index) === 1; - } - -}