Skip to content

Commit

Permalink
Added PhpStormStubsSourceStubber
Browse files Browse the repository at this point in the history
  • Loading branch information
kukulich committed Apr 19, 2019
1 parent a6dc9ce commit a60156b
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 0 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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\(\)#'
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Roave\BetterReflection\SourceLocator\SourceStubber\Exception;

use RuntimeException;

class DirectoryWithStubsNotFound extends RuntimeException
{
public static function create() : self
{
return new self('No directory with stubs found');
}
}
235 changes: 235 additions & 0 deletions src/SourceLocator/SourceStubber/PhpStormStubsSourceStubber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
<?php

declare(strict_types=1);

namespace Roave\BetterReflection\SourceLocator\SourceStubber;

use DirectoryIterator;
use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\NodeVisitorAbstract;
use PhpParser\Parser;
use PhpParser\PrettyPrinter\Standard;
use ReflectionClass as CoreReflectionClass;
use ReflectionFunction as CoreReflectionFunction;
use Roave\BetterReflection\SourceLocator\FileChecker;
use Roave\BetterReflection\SourceLocator\SourceStubber\Exception\DirectoryWithStubsNotFound;
use Traversable;
use function array_key_exists;
use function file_get_contents;
use function is_dir;
use function sprintf;
use function str_replace;

/**
* @internal
*/
final class PhpStormStubsSourceStubber implements SourceStubber
{
private const BUILDER_OPTIONS = ['shortArraySyntax' => 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 "<?php\n\n" . $this->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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Roave\BetterReflectionTest\SourceLocator\SourceStubber\Exception;

use PHPUnit\Framework\TestCase;
use Roave\BetterReflection\SourceLocator\SourceStubber\Exception\DirectoryWithStubsNotFound;

/**
* @covers \Roave\BetterReflection\SourceLocator\SourceStubber\Exception\DirectoryWithStubsNotFound
*/
class DirectoryWithStubsNotFoundTest extends TestCase
{
public function testCreate() : void
{
$exception = DirectoryWithStubsNotFound::create();

self::assertInstanceOf(DirectoryWithStubsNotFound::class, $exception);
self::assertSame('No directory with stubs found', $exception->getMessage());
}
}

0 comments on commit a60156b

Please sign in to comment.