Skip to content

Commit

Permalink
Merge pull request #972 from kukulich/phpstorm
Browse files Browse the repository at this point in the history
Improved `PhpStormStubsSourceStubber` for PHP < 8.0
  • Loading branch information
Ocramius committed Jan 18, 2022
2 parents 1d22d8f + 8f6d59d commit 23ee59d
Show file tree
Hide file tree
Showing 2 changed files with 229 additions and 30 deletions.
129 changes: 105 additions & 24 deletions src/SourceLocator/SourceStubber/PhpStormStubsSourceStubber.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@

namespace Roave\BetterReflection\SourceLocator\SourceStubber;

use CompileError;
use DatePeriod;
use Error;
use Generator;
use Iterator;
use IteratorAggregate;
use JetBrains\PHPStormStub\PhpStormStubsMap;
use JsonSerializable;
use ParseError;
use PDOStatement;
use PhpParser\BuilderFactory;
use PhpParser\BuilderHelpers;
use PhpParser\Comment\Doc;
Expand All @@ -14,10 +22,13 @@
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\Parser;
use PhpParser\PrettyPrinter\Standard;
use RecursiveIterator;
use Roave\BetterReflection\Reflection\Annotation\AnnotationHelper;
use Roave\BetterReflection\SourceLocator\FileChecker;
use Roave\BetterReflection\SourceLocator\SourceStubber\Exception\CouldNotFindPhpStormStubs;
use Roave\BetterReflection\SourceLocator\SourceStubber\PhpStormStubs\CachingVisitor;
use SimpleXMLElement;
use SplFixedArray;
use Traversable;

use function array_change_key_case;
Expand Down Expand Up @@ -205,30 +216,27 @@ public function __construct(private Parser $phpParser, private int $phpVersion =
*/
public function generateClassStub(string $className): ?StubData
{
$lowercaseClassName = strtolower($className);
$classNodeData = $this->getClassNodeData($className);

if (! array_key_exists($lowercaseClassName, self::$classMap)) {
if ($classNodeData === null) {
return null;
}

$filePath = self::$classMap[$lowercaseClassName];
$classNode = $classNodeData[0];

if (! array_key_exists($lowercaseClassName, $this->classNodes)) {
$this->parseFile($filePath);

/** @psalm-suppress RedundantCondition */
if (! array_key_exists($lowercaseClassName, $this->classNodes)) {
// Save `null` so we don't parse the file again for the same $lowercaseClassName
$this->classNodes[$lowercaseClassName] = null;
if ($classNode instanceof Node\Stmt\Class_) {
if ($classNode->extends !== null) {
$modifiedExtends = $this->replaceExtendsOrImplementsByPhpVersion($className, [$classNode->extends]);
$classNode->extends = $modifiedExtends !== [] ? $modifiedExtends[0] : null;
}
}

if ($this->classNodes[$lowercaseClassName] === null) {
return null;
$classNode->implements = $this->replaceExtendsOrImplementsByPhpVersion($className, $classNode->implements);
} elseif ($classNode instanceof Node\Stmt\Interface_) {
$classNode->extends = $this->replaceExtendsOrImplementsByPhpVersion($className, $classNode->extends);
}

$extension = $this->getExtensionFromFilePath($filePath);
$stub = $this->createStub($this->classNodes[$lowercaseClassName]);
$extension = $this->getExtensionFromFilePath(self::$classMap[strtolower($className)]);
$stub = $this->createStub($classNode, $classNodeData[1]);

if ($className === Traversable::class) {
// See https://github.com/JetBrains/phpstorm-stubs/commit/0778a26992c47d7dbee4d0b0bfb7fad4344371b1#diff-575bacb45377d474336c71cbf53c1729
Expand All @@ -240,6 +248,32 @@ public function generateClassStub(string $className): ?StubData
return new StubData($stub, $extension);
}

/**
* @return array{0: Node\Stmt\ClassLike, 1: Node\Stmt\Namespace_|null}|null
*/
private function getClassNodeData(string $className): ?array
{
$lowercaseClassName = strtolower($className);

if (! array_key_exists($lowercaseClassName, self::$classMap)) {
return null;
}

$filePath = self::$classMap[$lowercaseClassName];

if (! array_key_exists($lowercaseClassName, $this->classNodes)) {
$this->parseFile($filePath);

/** @psalm-suppress RedundantCondition */
if (! array_key_exists($lowercaseClassName, $this->classNodes)) {
// Save `null` so we don't parse the file again for the same $lowercaseClassName
$this->classNodes[$lowercaseClassName] = null;
}
}

return $this->classNodes[$lowercaseClassName];
}

public function generateFunctionStub(string $functionName): ?StubData
{
$lowercaseFunctionName = strtolower($functionName);
Expand All @@ -264,9 +298,10 @@ public function generateFunctionStub(string $functionName): ?StubData
return null;
}

$extension = $this->getExtensionFromFilePath($filePath);
$functionNodeData = $this->functionNodes[$lowercaseFunctionName];
$extension = $this->getExtensionFromFilePath($filePath);

return new StubData($this->createStub($this->functionNodes[$lowercaseFunctionName]), $extension);
return new StubData($this->createStub($functionNodeData[0], $functionNodeData[1]), $extension);
}

public function generateConstantStub(string $constantName): ?StubData
Expand Down Expand Up @@ -302,7 +337,7 @@ public function generateConstantStub(string $constantName): ?StubData

$extension = $this->getExtensionFromFilePath($filePath);

return new StubData($this->createStub($constantNodeData), $extension);
return new StubData($this->createStub($constantNodeData[0], $constantNodeData[1]), $extension);
}

private function parseFile(string $filePath): void
Expand Down Expand Up @@ -369,13 +404,8 @@ private function parseFile(string $filePath): void
}
}

/**
* @param array{0: Node\Stmt\ClassLike|Node\Stmt\Function_|Node\Stmt\Const_|Node\Expr\FuncCall, 1: Node\Stmt\Namespace_|null} $nodeData
*/
private function createStub(array $nodeData): string
private function createStub(Node\Stmt\ClassLike|Node\Stmt\Function_|Node\Stmt\Const_|Node\Expr\FuncCall $node, ?Node\Stmt\Namespace_ $namespaceNode): string
{
[$node, $namespaceNode] = $nodeData;

if (! ($node instanceof Node\Expr\FuncCall)) {
$this->addDeprecatedDocComment($node);

Expand Down Expand Up @@ -415,6 +445,57 @@ private function getAbsoluteFilePath(string $filePath): string
return sprintf('%s/%s', $this->getStubsDirectory(), $filePath);
}

/**
* Some stubs extend/implement classes from newer PHP versions. We need to filter those names in regard to set PHP version so that those stubs remain valid.
*
* @param list<Node\Name> $nameNodes
*
* @return list<Node\Name>
*/
private function replaceExtendsOrImplementsByPhpVersion(string $className, array $nameNodes): array
{
$modifiedNames = [];
foreach ($nameNodes as $nameNode) {
$name = $nameNode->toString();

if ($className === ParseError::class) {
if ($name === CompileError::class && $this->phpVersion < 70300) {
$modifiedNames[] = new Node\Name\FullyQualified(Error::class);
continue;
}
} elseif ($className === SplFixedArray::class) {
if ($name === JsonSerializable::class && $this->phpVersion < 80100) {
continue;
}

if ($name === IteratorAggregate::class && $this->phpVersion < 80000) {
continue;
}

if ($name === Iterator::class && $this->phpVersion >= 80000) {
continue;
}
} elseif ($className === SimpleXMLElement::class) {
if ($name === RecursiveIterator::class && $this->phpVersion < 80000) {
continue;
}
} elseif ($className === DatePeriod::class || $className === PDOStatement::class) {
if ($name === IteratorAggregate::class && $this->phpVersion < 80000) {
$modifiedNames[] = new Node\Name\FullyQualified(Traversable::class);
continue;
}
}

if ($this->getClassNodeData($name) === null) {
continue;
}

$modifiedNames[] = $nameNode;
}

return $modifiedNames;
}

/**
* @param list<Node\Stmt> $stmts
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
use DateTime;
use DateTimeInterface;
use DOMNode;
use Error;
use Generator;
use ParseError;
use PDO;
use PDOException;
use PhpParser\Parser;
Expand Down Expand Up @@ -46,6 +48,7 @@

use function array_filter;
use function array_key_exists;
use function array_keys;
use function array_map;
use function array_merge;
use function get_declared_classes;
Expand Down Expand Up @@ -167,12 +170,6 @@ private function assertSameClassAttributes(CoreReflectionClass $original, Reflec
self::assertSame($original->getName(), $stubbed->getName());

$this->assertSameParentClass($original, $stubbed);

// Needs fix in JetBrains/phpstorm-stubs
if ($original->getName() === 'SplFixedArray') {
return;
}

$this->assertSameInterfaces($original, $stubbed);

foreach ($original->getMethods() as $method) {
Expand Down Expand Up @@ -1165,4 +1162,125 @@ public function testModifiedStubForGeneratorClass(): void
self::assertInstanceOf(ReflectionClass::class, $classReflection);
self::assertTrue($classReflection->hasMethod('throw'));
}

public function dataImmediateInterfaces(): array
{
return [
[
'PDOStatement',
['Traversable'],
70400,
],
[
'PDOStatement',
['IteratorAggregate'],
80000,
],
[
'DatePeriod',
['Traversable'],
70400,
],
[
'DatePeriod',
['IteratorAggregate'],
80000,
],
[
'SplFixedArray',
['Iterator', 'ArrayAccess', 'Countable'],
70400,
],
[
'SplFixedArray',
['ArrayAccess', 'Countable', 'IteratorAggregate'],
80000,
],
[
'SplFixedArray',
['ArrayAccess', 'Countable', 'IteratorAggregate', 'JsonSerializable'],
80100,
],
[
'SplFixedArray',
['Iterator', 'ArrayAccess', 'Countable'],
70400,
],
[
'SimpleXMLElement',
['Traversable', 'ArrayAccess', 'Countable', 'Iterator'],
70400,
],
[
'SimpleXMLElement',
['Traversable', 'ArrayAccess', 'Countable', 'Iterator', 'Stringable', 'RecursiveIterator'],
80000,
],
[
'DOMDocument',
[],
70400,
],
[
'DOMDocument',
['DOMParentNode'],
80000,
],
];
}

/**
* @param string[] $interfaceNames
*
* @dataProvider dataImmediateInterfaces
*/
public function testImmediateInterfaces(
string $className,
array $interfaceNames,
int $phpVersion,
): void {
$sourceStubber = new PhpStormStubsSourceStubber($this->phpParser, $phpVersion);
$phpInternalSourceLocator = new PhpInternalSourceLocator($this->astLocator, $sourceStubber);
$reflector = new DefaultReflector($phpInternalSourceLocator);
$class = $reflector->reflectClass($className);

self::assertSame($interfaceNames, array_keys($class->getImmediateInterfaces()));
}

public function dataSubclass(): array
{
return [
[
ParseError::class,
CompileError::class,
70300,
],
[
ParseError::class,
CompileError::class,
70400,
],
[
ParseError::class,
Error::class,
70200,
],
];
}

/**
* @dataProvider dataSubclass
*/
public function testSubclass(
string $className,
string $subclassName,
int $phpVersion,
): void {
$sourceStubber = new PhpStormStubsSourceStubber($this->phpParser, $phpVersion);
$phpInternalSourceLocator = new PhpInternalSourceLocator($this->astLocator, $sourceStubber);
$reflector = new DefaultReflector($phpInternalSourceLocator);
$class = $reflector->reflectClass($className);

self::assertTrue($class->isSubclassOf($subclassName));
}
}

0 comments on commit 23ee59d

Please sign in to comment.