From a60156bc731089f4de60a92490d3dee999fe1ce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Hansl=C3=ADk?= Date: Fri, 22 Sep 2017 13:27:38 +0200 Subject: [PATCH] Added PhpStormStubsSourceStubber --- composer.json | 1 + phpstan.neon | 2 + .../Exception/DirectoryWithStubsNotFound.php | 15 ++ .../PhpStormStubsSourceStubber.php | 235 ++++++++++++++++++ .../DirectoryWithStubsNotFoundTest.php | 22 ++ 5 files changed, 275 insertions(+) create mode 100644 src/SourceLocator/SourceStubber/Exception/DirectoryWithStubsNotFound.php create mode 100644 src/SourceLocator/SourceStubber/PhpStormStubsSourceStubber.php create mode 100644 test/unit/SourceLocator/SourceStubber/Exception/DirectoryWithStubsNotFoundTest.php diff --git a/composer.json b/composer.json index 9e6a843aa..fba60db67 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "license": "MIT", "require": { "php": ">=7.1.0,<7.4.0", + "jetbrains/phpstorm-stubs": "2019.1", "nikic/php-parser": "^4.0.4", "phpdocumentor/reflection-docblock": "^4.1.1", "phpdocumentor/type-resolver": "^0.4.0", diff --git a/phpstan.neon b/phpstan.neon index 12fdb35cb..f1f74a070 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,3 +3,5 @@ parameters: # Some parent constructors are explicitly to be ignored - '#does not call parent constructor#' - '#Access to an undefined property PhpParser\\Node\\Param::\$isOptional#' + # Impossible to define type hint for anonymous class + - '#Call to an undefined method PhpParser\\NodeVisitorAbstract::getNode\(\)#' diff --git a/src/SourceLocator/SourceStubber/Exception/DirectoryWithStubsNotFound.php b/src/SourceLocator/SourceStubber/Exception/DirectoryWithStubsNotFound.php new file mode 100644 index 000000000..f956767a9 --- /dev/null +++ b/src/SourceLocator/SourceStubber/Exception/DirectoryWithStubsNotFound.php @@ -0,0 +1,15 @@ + true]; + private const SEARCH_DIRECTORIES = [ + __DIR__ . '/../../../../../jetbrains/phpstorm-stubs', + __DIR__ . '/../../../vendor/jetbrains/phpstorm-stubs', + ]; + + /** @var Parser */ + private $phpParser; + + /** @var Standard */ + private $prettyPrinter; + + /** @var NodeTraverser */ + private $nodeTraverser; + + /** @var string|null */ + private $stubsDirectory; + + /** @var string[][] */ + private $extensionStubsFiles = []; + + public function __construct(Parser $phpParser) + { + $this->phpParser = $phpParser; + $this->prettyPrinter = new Standard(self::BUILDER_OPTIONS); + + $this->nodeTraverser = new NodeTraverser(); + $this->nodeTraverser->addVisitor(new NameResolver()); + } + + /** + * {@inheritDoc} + */ + public function generateClassStub(CoreReflectionClass $classReflection) : ?string + { + if ($classReflection->isUserDefined()) { + return null; + } + + $stub = $this->getStub($classReflection->getExtensionName(), $this->getClassNodeVisitor($classReflection)); + + if ($classReflection->getName() === Traversable::class) { + // See https://github.com/JetBrains/phpstorm-stubs/commit/0778a26992c47d7dbee4d0b0bfb7fad4344371b1#diff-575bacb45377d474336c71cbf53c1729 + $stub = str_replace(' extends \iterable', '', $stub); + } + + return $stub; + } + + /** + * {@inheritDoc} + */ + public function generateFunctionStub(CoreReflectionFunction $functionReflection) : ?string + { + if ($functionReflection->isUserDefined()) { + return null; + } + + return $this->getStub($functionReflection->getExtensionName(), $this->getFunctionNodeVisitor($functionReflection)); + } + + private function getStub(string $extensionName, NodeVisitorAbstract $nodeVisitor) : ?string + { + $node = null; + + $this->nodeTraverser->addVisitor($nodeVisitor); + + foreach ($this->getExtensionStubsFiles($extensionName) as $filePath) { + FileChecker::assertReadableFile($filePath); + + $ast = $this->phpParser->parse(file_get_contents($filePath)); + + $this->nodeTraverser->traverse($ast); + + /** @psalm-suppress UndefinedMethod */ + $node = $nodeVisitor->getNode(); + if ($node !== null) { + break; + } + } + + $this->nodeTraverser->removeVisitor($nodeVisitor); + + if ($node === null) { + return null; + } + + return "prettyPrinter->prettyPrint([$node]) . "\n"; + } + + private function getClassNodeVisitor(CoreReflectionClass $classReflection) : NodeVisitorAbstract + { + return new class($classReflection->getName()) extends NodeVisitorAbstract + { + /** @var string */ + private $className; + + /** @var Node\Stmt\ClassLike|null */ + private $node; + + public function __construct(string $className) + { + $this->className = $className; + } + + public function enterNode(Node $node) : ?int + { + if ($node instanceof Node\Stmt\Namespace_) { + return null; + } + + if ($node instanceof Node\Stmt\ClassLike && $node->namespacedName->toString() === $this->className) { + $this->node = $node; + return NodeTraverser::STOP_TRAVERSAL; + } + + return NodeTraverser::DONT_TRAVERSE_CHILDREN; + } + + public function getNode() : ?Node\Stmt\ClassLike + { + return $this->node; + } + }; + } + + private function getFunctionNodeVisitor(CoreReflectionFunction $functionReflection) : NodeVisitorAbstract + { + return new class($functionReflection->getName()) extends NodeVisitorAbstract + { + /** @var string */ + private $functionName; + + /** @var Node\Stmt\Function_|null */ + private $node; + + public function __construct(string $className) + { + $this->functionName = $className; + } + + public function enterNode(Node $node) : ?int + { + if ($node instanceof Node\Stmt\Namespace_) { + return null; + } + + /** @psalm-suppress UndefinedPropertyFetch */ + if ($node instanceof Node\Stmt\Function_ && $node->namespacedName->toString() === $this->functionName) { + $this->node = $node; + return NodeTraverser::STOP_TRAVERSAL; + } + + return NodeTraverser::DONT_TRAVERSE_CHILDREN; + } + + public function getNode() : ?Node\Stmt\Function_ + { + return $this->node; + } + }; + } + + /** + * @return string[] + */ + private function getExtensionStubsFiles(string $extensionName) : array + { + if (array_key_exists($extensionName, $this->extensionStubsFiles)) { + return $this->extensionStubsFiles[$extensionName]; + } + + $this->extensionStubsFiles[$extensionName] = []; + + $extensionDirectory = sprintf('%s/%s', $this->getStubsDirectory(), $extensionName); + + if (! is_dir($extensionDirectory)) { + return []; + } + + foreach (new DirectoryIterator($extensionDirectory) as $fileInfo) { + if ($fileInfo->isDot()) { + continue; + } + + $this->extensionStubsFiles[$extensionName][] = $fileInfo->getPathname(); + } + + return $this->extensionStubsFiles[$extensionName]; + } + + private function getStubsDirectory() : string + { + if ($this->stubsDirectory !== null) { + return $this->stubsDirectory; + } + + foreach (self::SEARCH_DIRECTORIES as $directory) { + if (is_dir($directory)) { + return $this->stubsDirectory = $directory; + } + } + + throw DirectoryWithStubsNotFound::create(); + } +} diff --git a/test/unit/SourceLocator/SourceStubber/Exception/DirectoryWithStubsNotFoundTest.php b/test/unit/SourceLocator/SourceStubber/Exception/DirectoryWithStubsNotFoundTest.php new file mode 100644 index 000000000..52290e581 --- /dev/null +++ b/test/unit/SourceLocator/SourceStubber/Exception/DirectoryWithStubsNotFoundTest.php @@ -0,0 +1,22 @@ +getMessage()); + } +}