Skip to content

Commit 0c95f6f

Browse files
authored
Merge pull request #618 from kukulich/phpstorm-stubber
PhpStormStubsSourceStubber fixes for case-insensitive classes/functions/constants
2 parents 259bb1e + a66fc3f commit 0c95f6f

File tree

2 files changed

+151
-19
lines changed

2 files changed

+151
-19
lines changed

src/SourceLocator/SourceStubber/PhpStormStubsSourceStubber.php

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@
1818
use Roave\BetterReflection\SourceLocator\SourceStubber\Exception\CouldNotFindPhpStormStubs;
1919
use Roave\BetterReflection\Util\ConstantNodeChecker;
2020
use Traversable;
21+
use function array_change_key_case;
2122
use function array_key_exists;
2223
use function assert;
2324
use function constant;
2425
use function count;
2526
use function defined;
2627
use function explode;
2728
use function file_get_contents;
28-
use function in_array;
2929
use function is_dir;
3030
use function is_string;
3131
use function sprintf;
@@ -67,9 +67,22 @@ final class PhpStormStubsSourceStubber implements SourceStubber
6767
/** @var array<string, Node\Stmt\Function_> */
6868
private $functionNodes = [];
6969

70-
/** @var array<string, Node\Stmt\Const_|Node\Expr\FuncCall> */
70+
/**
71+
* `null` means "failed lookup" for constant that is not case insensitive
72+
*
73+
* @var array<string, Node\Stmt\Const_|Node\Expr\FuncCall|null>
74+
*/
7175
private $constantNodes = [];
7276

77+
/** @var array<lowercase-string, string> */
78+
private $classMap;
79+
80+
/** @var array<lowercase-string, string> */
81+
private $functionMap;
82+
83+
/** @var array<lowercase-string, string> */
84+
private $constantMap;
85+
7386
public function __construct(Parser $phpParser)
7487
{
7588
$this->phpParser = $phpParser;
@@ -81,21 +94,27 @@ public function __construct(Parser $phpParser)
8194
$this->nodeTraverser = new NodeTraverser();
8295
$this->nodeTraverser->addVisitor(new NameResolver());
8396
$this->nodeTraverser->addVisitor($this->cachingVisitor);
97+
98+
$this->classMap = array_change_key_case(PhpStormStubsMap::CLASSES);
99+
$this->functionMap = array_change_key_case(PhpStormStubsMap::FUNCTIONS);
100+
$this->constantMap = array_change_key_case(PhpStormStubsMap::CONSTANTS);
84101
}
85102

86103
public function generateClassStub(string $className) : ?StubData
87104
{
88-
if (! array_key_exists($className, PhpStormStubsMap::CLASSES)) {
105+
$lowercaseClassName = strtolower($className);
106+
107+
if (! array_key_exists($lowercaseClassName, $this->classMap)) {
89108
return null;
90109
}
91110

92-
$filePath = PhpStormStubsMap::CLASSES[$className];
111+
$filePath = $this->classMap[$lowercaseClassName];
93112

94-
if (! array_key_exists($className, $this->classNodes)) {
113+
if (! array_key_exists($lowercaseClassName, $this->classNodes)) {
95114
$this->parseFile($filePath);
96115
}
97116

98-
$stub = $this->createStub($this->classNodes[$className]);
117+
$stub = $this->createStub($this->classNodes[$lowercaseClassName]);
99118

100119
if ($className === Traversable::class) {
101120
// See https://github.com/JetBrains/phpstorm-stubs/commit/0778a26992c47d7dbee4d0b0bfb7fad4344371b1#diff-575bacb45377d474336c71cbf53c1729
@@ -107,37 +126,52 @@ public function generateClassStub(string $className) : ?StubData
107126

108127
public function generateFunctionStub(string $functionName) : ?StubData
109128
{
110-
if (! array_key_exists($functionName, PhpStormStubsMap::FUNCTIONS)) {
129+
$lowercaseFunctionName = strtolower($functionName);
130+
131+
if (! array_key_exists($lowercaseFunctionName, $this->functionMap)) {
111132
return null;
112133
}
113134

114-
$filePath = PhpStormStubsMap::FUNCTIONS[$functionName];
135+
$filePath = $this->functionMap[$lowercaseFunctionName];
115136

116-
if (! array_key_exists($functionName, $this->functionNodes)) {
137+
if (! array_key_exists($lowercaseFunctionName, $this->functionNodes)) {
117138
$this->parseFile($filePath);
118139
}
119140

120-
return new StubData($this->createStub($this->functionNodes[$functionName]), $this->getExtensionFromFilePath($filePath));
141+
return new StubData($this->createStub($this->functionNodes[$lowercaseFunctionName]), $this->getExtensionFromFilePath($filePath));
121142
}
122143

123144
public function generateConstantStub(string $constantName) : ?StubData
124145
{
125-
// https://github.com/JetBrains/phpstorm-stubs/pull/591
126-
if (in_array($constantName, ['TRUE', 'FALSE', 'NULL'], true)) {
127-
$constantName = strtolower($constantName);
146+
$lowercaseConstantName = strtolower($constantName);
147+
148+
if (! array_key_exists($lowercaseConstantName, $this->constantMap)) {
149+
return null;
128150
}
129151

130-
if (! array_key_exists($constantName, PhpStormStubsMap::CONSTANTS)) {
152+
if (array_key_exists($lowercaseConstantName, $this->constantNodes)
153+
&& $this->constantNodes[$lowercaseConstantName] === null
154+
) {
131155
return null;
132156
}
133157

134-
$filePath = PhpStormStubsMap::CONSTANTS[$constantName];
158+
$filePath = $this->constantMap[$lowercaseConstantName];
159+
$constantNode = $this->constantNodes[$constantName] ?? $this->constantNodes[$lowercaseConstantName] ?? null;
135160

136-
if (! array_key_exists($constantName, $this->constantNodes)) {
161+
if ($constantNode === null) {
137162
$this->parseFile($filePath);
163+
164+
$constantNode = $this->constantNodes[$constantName] ?? $this->constantNodes[$lowercaseConstantName] ?? null;
165+
166+
if ($constantNode === null) {
167+
// Still `null` - the constant is not case-insensitive. Save `null` so we don't parse the file again for the same $constantName
168+
$this->constantNodes[$lowercaseConstantName] = null;
169+
170+
return null;
171+
}
138172
}
139173

140-
return new StubData($this->createStub($this->constantNodes[$constantName]), $this->getExtensionFromFilePath($filePath));
174+
return new StubData($this->createStub($constantNode), $this->getExtensionFromFilePath($filePath));
141175
}
142176

143177
private function parseFile(string $filePath) : void
@@ -158,7 +192,8 @@ private function parseFile(string $filePath) : void
158192
foreach ($this->cachingVisitor->getClassNodes() as $className => $classNode) {
159193
assert(is_string($className));
160194
assert($classNode instanceof Node\Stmt\ClassLike);
161-
$this->classNodes[$className] = $classNode;
195+
196+
$this->classNodes[strtolower($className)] = $classNode;
162197
}
163198

164199
/**
@@ -167,7 +202,8 @@ private function parseFile(string $filePath) : void
167202
foreach ($this->cachingVisitor->getFunctionNodes() as $functionName => $functionNode) {
168203
assert(is_string($functionName));
169204
assert($functionNode instanceof Node\Stmt\Function_);
170-
$this->functionNodes[$functionName] = $functionNode;
205+
206+
$this->functionNodes[strtolower($functionName)] = $functionNode;
171207
}
172208

173209
/**

test/unit/SourceLocator/SourceStubber/PhpStormStubsSourceStubberTest.php

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Roave\BetterReflection\Reflection\ReflectionParameter;
1616
use Roave\BetterReflection\Reflector\ClassReflector;
1717
use Roave\BetterReflection\Reflector\ConstantReflector;
18+
use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound;
1819
use Roave\BetterReflection\Reflector\FunctionReflector;
1920
use Roave\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber;
2021
use Roave\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator;
@@ -562,4 +563,99 @@ public function testNoStubForUnknownConstant() : void
562563
{
563564
self::assertNull($this->sourceStubber->generateConstantStub('SOME_CONSTANT'));
564565
}
566+
567+
public function dataCaseInsensitiveClass() : array
568+
{
569+
return [
570+
[
571+
'SoapFault',
572+
'SoapFault',
573+
],
574+
[
575+
'SOAPFault',
576+
'SoapFault',
577+
],
578+
];
579+
}
580+
581+
/**
582+
* @dataProvider dataCaseInsensitiveClass
583+
*/
584+
public function testCaseInsensitiveClass(string $className, string $expectedClassName) : void
585+
{
586+
$classReflection = $this->classReflector->reflect($className);
587+
588+
$this->assertSame($expectedClassName, $classReflection->getName());
589+
}
590+
591+
public function dataCaseInsensitiveFunction() : array
592+
{
593+
return [
594+
[
595+
'htmlspecialchars',
596+
'htmlspecialchars',
597+
],
598+
[
599+
'htmlSpecialChars',
600+
'htmlspecialchars',
601+
],
602+
];
603+
}
604+
605+
/**
606+
* @dataProvider dataCaseInsensitiveFunction
607+
*/
608+
public function testCaseInsensitiveFunction(string $functionName, string $expectedFunctionName) : void
609+
{
610+
$functionReflection = $this->functionReflector->reflect($functionName);
611+
612+
$this->assertSame($expectedFunctionName, $functionReflection->getName());
613+
}
614+
615+
public function dataCaseInsensitiveConstant() : array
616+
{
617+
return [
618+
[
619+
'TRUE',
620+
'true',
621+
],
622+
[
623+
'__file__',
624+
'__FILE__',
625+
],
626+
[
627+
'YaF_VeRsIoN',
628+
'YAF_VERSION',
629+
],
630+
];
631+
}
632+
633+
/**
634+
* @dataProvider dataCaseInsensitiveConstant
635+
*/
636+
public function testCaseInsensitiveConstant(string $constantName, string $expectedConstantName) : void
637+
{
638+
$constantReflector = $this->constantReflector->reflect($constantName);
639+
640+
$this->assertSame($expectedConstantName, $constantReflector->getName());
641+
}
642+
643+
public function dataCaseSensitiveConstant() : array
644+
{
645+
return [
646+
['date_atom'],
647+
['PHP_version_ID'],
648+
['FiLeInFo_NoNe'],
649+
];
650+
}
651+
652+
/**
653+
* @dataProvider dataCaseSensitiveConstant
654+
*/
655+
public function testCaseSensitiveConstant(string $constantName) : void
656+
{
657+
self::expectException(IdentifierNotFound::class);
658+
659+
$this->constantReflector->reflect($constantName);
660+
}
565661
}

0 commit comments

Comments
 (0)