Skip to content

Commit 491d3e9

Browse files
committed
Extension symbols: track separatelly
1 parent 7ca4789 commit 491d3e9

File tree

6 files changed

+216
-52
lines changed

6 files changed

+216
-52
lines changed

src/Analyser.php

+1
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,7 @@ private function initExistingSymbols(): void
535535

536536
/** @var array<string, array<string, mixed>> $definedConstants */
537537
$definedConstants = get_defined_constants(true);
538+
538539
foreach ($definedConstants as $constantExtension => $constants) {
539540
foreach ($constants as $constantName => $_) {
540541
if ($constantExtension === 'user') {

src/UsedSymbolExtractor.php

+116-46
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22

33
namespace ShipMonk\ComposerDependencyAnalyser;
44

5-
use function array_combine;
65
use function array_fill_keys;
6+
use function array_map;
77
use function array_merge;
88
use function count;
99
use function explode;
1010
use function is_array;
1111
use function ltrim;
1212
use function strlen;
1313
use function strpos;
14+
use function strtolower;
1415
use function substr;
1516
use function token_get_all;
1617
use const PHP_VERSION_ID;
@@ -22,14 +23,18 @@
2223
use const T_CURLY_OPEN;
2324
use const T_DOC_COMMENT;
2425
use const T_DOLLAR_OPEN_CURLY_BRACES;
26+
use const T_DOUBLE_COLON;
2527
use const T_ENUM;
2628
use const T_FUNCTION;
29+
use const T_INSTEADOF;
2730
use const T_INTERFACE;
2831
use const T_NAME_FULLY_QUALIFIED;
2932
use const T_NAME_QUALIFIED;
3033
use const T_NAMESPACE;
3134
use const T_NEW;
3235
use const T_NS_SEPARATOR;
36+
use const T_NULLSAFE_OBJECT_OPERATOR;
37+
use const T_OBJECT_OPERATOR;
3338
use const T_STRING;
3439
use const T_TRAIT;
3540
use const T_USE;
@@ -65,10 +70,9 @@ public function __construct(string $code)
6570
* It does not produce any local names in current namespace
6671
* - this results in very limited functionality in files without namespace
6772
*
68-
* @param array<string> $extClasses
69-
* @param array<string> $extFunctions
70-
* @param array<string> $extConstants
71-
*
73+
* @param list<string> $extClasses
74+
* @param list<string> $extFunctions
75+
* @param list<string> $extConstants
7276
* @return array<SymbolKind::*, array<string, list<int>>>
7377
* @license Inspired by https://github.com/doctrine/annotations/blob/2.0.0/lib/Doctrine/Common/Annotations/TokenParser.php
7478
*/
@@ -79,16 +83,13 @@ public function parseUsedSymbols(
7983
): array
8084
{
8185
$usedSymbols = [];
82-
$useStatements = $initialUseStatements = array_merge(
83-
array_combine($extClasses, $extClasses),
84-
array_combine($extFunctions, $extFunctions),
85-
array_combine($extConstants, $extConstants)
86-
);
87-
$useStatementKinds = $initialUseStatementKinds = array_merge(
88-
array_fill_keys($extClasses, SymbolKind::CLASSLIKE),
89-
array_fill_keys($extFunctions, SymbolKind::FUNCTION),
90-
array_fill_keys($extConstants, SymbolKind::CONSTANT)
86+
$extensionSymbols = array_merge(
87+
array_fill_keys(array_map('strtolower', $extClasses), SymbolKind::CLASSLIKE),
88+
array_fill_keys(array_map('strtolower', $extFunctions), SymbolKind::FUNCTION),
89+
array_fill_keys(array_map('strtolower', $extConstants), SymbolKind::CONSTANT)
9190
);
91+
$useStatements = [];
92+
$useStatementKinds = [];
9293

9394
$level = 0; // {, }, {$, ${
9495
$squareLevel = 0; // [, ], #[
@@ -128,13 +129,14 @@ public function parseUsedSymbols(
128129
case PHP_VERSION_ID >= 80000 ? T_NAMESPACE : -1:
129130
// namespace change
130131
$inGlobalScope = false;
131-
$useStatements = $initialUseStatements;
132-
$useStatementKinds = $initialUseStatementKinds;
132+
$useStatements = [];
133+
$useStatementKinds = [];
133134
break;
134135

135136
case PHP_VERSION_ID >= 80000 ? T_NAME_FULLY_QUALIFIED : -1:
136137
$symbolName = $this->normalizeBackslash($token[1]);
137-
$kind = $this->getFqnSymbolKind($this->pointer - 2, $this->pointer, $inAttributeSquareLevel !== null);
138+
$lowerSymbolName = strtolower($symbolName);
139+
$kind = $extensionSymbols[$lowerSymbolName] ?? $this->getFqnSymbolKind($this->pointer - 2, $this->pointer, $inAttributeSquareLevel !== null);
138140
$usedSymbols[$kind][$symbolName][] = $token[2];
139141
break;
140142

@@ -143,21 +145,34 @@ public function parseUsedSymbols(
143145

144146
if (isset($useStatements[$neededAlias])) {
145147
$symbolName = $useStatements[$neededAlias] . substr($token[1], strlen($neededAlias));
146-
$kind = $this->getFqnSymbolKind($this->pointer - 2, $this->pointer, $inAttributeSquareLevel !== null);
147-
$usedSymbols[$kind][$symbolName][] = $token[2];
148-
149148
} elseif ($inGlobalScope) {
150149
$symbolName = $token[1];
151-
$kind = $this->getFqnSymbolKind($this->pointer - 2, $this->pointer, $inAttributeSquareLevel !== null);
152-
$usedSymbols[$kind][$symbolName][] = $token[2];
150+
} else {
151+
break;
153152
}
154153

154+
$lowerSymbolName = strtolower($symbolName);
155+
$kind = $extensionSymbols[$lowerSymbolName] ?? $this->getFqnSymbolKind($this->pointer - 2, $this->pointer, $inAttributeSquareLevel !== null);
156+
$usedSymbols[$kind][$symbolName][] = $token[2];
157+
155158
break;
156159

157160
case PHP_VERSION_ID >= 80000 ? T_STRING : -1:
158161
$name = $token[1];
162+
$lowerName = strtolower($name);
163+
$pointerBeforeName = $this->pointer - 2;
164+
$pointerAfterName = $this->pointer;
165+
166+
if (!$this->canBeSymbolName($pointerBeforeName, $pointerAfterName)) {
167+
break;
168+
}
159169

160-
if (isset($useStatements[$name])) {
170+
if (isset($extensionSymbols[$lowerName])) {
171+
$symbolName = $name;
172+
$kind = $extensionSymbols[$lowerName];
173+
$usedSymbols[$kind][$symbolName][] = $token[2];
174+
175+
} elseif (isset($useStatements[$name])) {
161176
$symbolName = $useStatements[$name];
162177
$kind = $useStatementKinds[$name];
163178
$usedSymbols[$kind][$symbolName][] = $token[2];
@@ -172,18 +187,19 @@ public function parseUsedSymbols(
172187
if (substr($nextName, 0, 1) !== '\\') { // not a namespace-relative name, but a new namespace declaration
173188
// namespace change
174189
$inGlobalScope = false;
175-
$useStatements = $initialUseStatements;
176-
$useStatementKinds = $initialUseStatementKinds;
190+
$useStatements = [];
191+
$useStatementKinds = [];
177192
}
178193

179194
break;
180195

181196
case PHP_VERSION_ID < 80000 ? T_NS_SEPARATOR : -1:
182197
$pointerBeforeName = $this->pointer - 2;
183198
$symbolName = $this->normalizeBackslash($this->parseNameForOldPhp());
199+
$lowerSymbolName = strtolower($symbolName);
184200

185201
if ($symbolName !== '') { // e.g. \array (NS separator followed by not-a-name)
186-
$kind = $this->getFqnSymbolKind($pointerBeforeName, $this->pointer - 1, false);
202+
$kind = $extensionSymbols[$lowerSymbolName] ?? $this->getFqnSymbolKind($pointerBeforeName, $this->pointer - 1, false);
187203
$usedSymbols[$kind][$symbolName][] = $token[2];
188204
}
189205

@@ -192,23 +208,34 @@ public function parseUsedSymbols(
192208
case PHP_VERSION_ID < 80000 ? T_STRING : -1:
193209
$pointerBeforeName = $this->pointer - 2;
194210
$name = $this->parseNameForOldPhp();
211+
$lowerName = strtolower($name);
212+
$pointerAfterName = $this->pointer - 1;
213+
214+
if (!$this->canBeSymbolName($pointerBeforeName, $pointerAfterName)) {
215+
break;
216+
}
195217

196218
if (isset($useStatements[$name])) { // unqualified name
197219
$symbolName = $useStatements[$name];
198220
$kind = $useStatementKinds[$name];
199221
$usedSymbols[$kind][$symbolName][] = $token[2];
200222

223+
} elseif (isset($extensionSymbols[$lowerName])) {
224+
$symbolName = $name;
225+
$kind = $extensionSymbols[$lowerName];
226+
$usedSymbols[$kind][$symbolName][] = $token[2];
227+
201228
} else {
202229
[$neededAlias] = explode('\\', $name, 2);
203230

204231
if (isset($useStatements[$neededAlias])) { // qualified name
205232
$symbolName = $useStatements[$neededAlias] . substr($name, strlen($neededAlias));
206-
$kind = $this->getFqnSymbolKind($pointerBeforeName, $this->pointer - 1, false);
233+
$kind = $this->getFqnSymbolKind($pointerBeforeName, $pointerAfterName, false);
207234
$usedSymbols[$kind][$symbolName][] = $token[2];
208235

209236
} elseif ($inGlobalScope && strpos($name, '\\') !== false) {
210237
$symbolName = $name;
211-
$kind = $this->getFqnSymbolKind($pointerBeforeName, $this->pointer - 1, false);
238+
$kind = $this->getFqnSymbolKind($pointerBeforeName, $pointerAfterName, false);
212239
$usedSymbols[$kind][$symbolName][] = $token[2];
213240
}
214241
}
@@ -369,44 +396,87 @@ private function getFqnSymbolKind(
369396
return SymbolKind::CLASSLIKE;
370397
}
371398

399+
$tokenBeforeName = $this->getTokenBefore($pointerBeforeName);
400+
$tokenAfterName = $this->getTokenAfter($pointerAfterName);
401+
402+
if (
403+
$tokenAfterName === '('
404+
&& $tokenBeforeName[0] !== T_NEW // eliminate new \ClassName(
405+
) {
406+
return SymbolKind::FUNCTION;
407+
}
408+
409+
return SymbolKind::CLASSLIKE; // constant may fall here, this is eliminated later
410+
}
411+
412+
private function canBeSymbolName(
413+
int $pointerBeforeName,
414+
int $pointerAfterName
415+
): bool
416+
{
417+
$tokenBeforeName = $this->getTokenBefore($pointerBeforeName);
418+
$tokenAfterName = $this->getTokenAfter($pointerAfterName);
419+
420+
if (
421+
$tokenBeforeName[0] === T_DOUBLE_COLON
422+
|| $tokenBeforeName[0] === T_INSTEADOF
423+
|| $tokenBeforeName[0] === T_AS
424+
|| $tokenBeforeName[0] === T_FUNCTION
425+
|| $tokenBeforeName[0] === T_OBJECT_OPERATOR
426+
|| $tokenBeforeName[0] === (PHP_VERSION_ID > 80000 ? T_NULLSAFE_OBJECT_OPERATOR : -1)
427+
|| $tokenAfterName[0] === T_INSTEADOF
428+
|| $tokenAfterName[0] === T_AS
429+
) {
430+
return false;
431+
}
432+
433+
return true;
434+
}
435+
436+
/**
437+
* @return array{int, string}|string
438+
*/
439+
private function getTokenBefore(int $pointer)
440+
{
372441
do {
373-
$tokenBeforeName = $this->tokens[$pointerBeforeName];
442+
$token = $this->tokens[$pointer];
374443

375-
if (!is_array($tokenBeforeName)) {
444+
if (!is_array($token)) {
376445
break;
377446
}
378447

379-
if ($tokenBeforeName[0] === T_WHITESPACE || $tokenBeforeName[0] === T_COMMENT || $tokenBeforeName[0] === T_DOC_COMMENT) {
380-
$pointerBeforeName--;
448+
if ($token[0] === T_WHITESPACE || $token[0] === T_COMMENT || $token[0] === T_DOC_COMMENT) {
449+
$pointer--;
381450
continue;
382451
}
383452

384453
break;
385-
} while ($pointerBeforeName >= 0);
454+
} while ($pointer >= 0);
386455

456+
return $token;
457+
}
458+
459+
/**
460+
* @return array{int, string}|string
461+
*/
462+
private function getTokenAfter(int $pointer)
463+
{
387464
do {
388-
$tokenAfterName = $this->tokens[$pointerAfterName];
465+
$token = $this->tokens[$pointer];
389466

390-
if (!is_array($tokenAfterName)) {
467+
if (!is_array($token)) {
391468
break;
392469
}
393470

394-
if ($tokenAfterName[0] === T_WHITESPACE || $tokenAfterName[0] === T_COMMENT || $tokenAfterName[0] === T_DOC_COMMENT) {
395-
$pointerAfterName++;
471+
if ($token[0] === T_WHITESPACE || $token[0] === T_COMMENT || $token[0] === T_DOC_COMMENT) {
472+
$pointer++;
396473
continue;
397474
}
398475

399476
break;
400-
} while ($pointerAfterName < $this->numTokens);
477+
} while ($pointer < $this->numTokens);
401478

402-
if (
403-
$tokenAfterName === '('
404-
&& $tokenBeforeName[0] !== T_NEW // eliminate new \ClassName(
405-
) {
406-
return SymbolKind::FUNCTION;
407-
}
408-
409-
return SymbolKind::CLASSLIKE; // constant may fall here, this is eliminated later
479+
return $token;
410480
}
411481

412482
}

tests/UsedSymbolExtractorTest.php

+44-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace ShipMonk\ComposerDependencyAnalyser;
44

55
use PHPUnit\Framework\TestCase;
6+
use function array_map;
67
use function file_get_contents;
78
use const PHP_VERSION_ID;
89

@@ -20,7 +21,14 @@ public function test(string $path, array $expectedUsages): void
2021

2122
$extractor = new UsedSymbolExtractor($code);
2223

23-
self::assertSame($expectedUsages, $extractor->parseUsedSymbols(['PDO'], ['json_encode'], ['LIBXML_ERR_FATAL']));
24+
self::assertSame(
25+
$expectedUsages,
26+
$extractor->parseUsedSymbols(
27+
['PDO'],
28+
array_map('strtolower', ['json_encode', 'DDTrace\active_span', 'DDTrace\root_span']),
29+
['LIBXML_ERR_FATAL', 'LIBXML_ERR_ERROR', 'DDTrace\DBM_PROPAGATION_FULL']
30+
)
31+
);
2432
}
2533

2634
/**
@@ -49,6 +57,11 @@ public function provideVariants(): iterable
4957
],
5058
];
5159

60+
yield 'T_STRING issues' => [
61+
__DIR__ . '/data/not-autoloaded/used-symbols/t-string-issues.php',
62+
[],
63+
];
64+
5265
yield 'various usages' => [
5366
__DIR__ . '/data/not-autoloaded/used-symbols/various-usages.php',
5467
[
@@ -122,15 +135,40 @@ public function provideVariants(): iterable
122135
__DIR__ . '/data/not-autoloaded/used-symbols/extensions.php',
123136
[
124137
SymbolKind::FUNCTION => [
125-
'json_encode' => [5],
126-
'json_decode' => [12],
138+
'json_encode' => [8],
139+
'DDTrace\active_span' => [12],
140+
'DDTrace\root_span' => [13],
141+
'json_decode' => [21],
142+
],
143+
SymbolKind::CONSTANT => [
144+
'LIBXML_ERR_FATAL' => [9],
145+
'LIBXML_ERR_ERROR' => [10],
146+
'DDTrace\DBM_PROPAGATION_FULL' => [14],
147+
],
148+
SymbolKind::CLASSLIKE => [
149+
'PDO' => [11],
150+
'CURLOPT_SSL_VERIFYHOST' => [19],
151+
],
152+
],
153+
];
154+
155+
yield 'extensions global' => [
156+
__DIR__ . '/data/not-autoloaded/used-symbols/extensions-global.php',
157+
[
158+
SymbolKind::FUNCTION => [
159+
'json_encode' => [8],
160+
'DDTrace\active_span' => [12],
161+
'DDTrace\root_span' => [13],
162+
'json_decode' => [21],
127163
],
128164
SymbolKind::CONSTANT => [
129-
'LIBXML_ERR_FATAL' => [6],
165+
'LIBXML_ERR_FATAL' => [9],
166+
'LIBXML_ERR_ERROR' => [10],
167+
'DDTrace\DBM_PROPAGATION_FULL' => [14],
130168
],
131169
SymbolKind::CLASSLIKE => [
132-
'PDO' => [7],
133-
'CURLOPT_SSL_VERIFYHOST' => [10],
170+
'PDO' => [11],
171+
'CURLOPT_SSL_VERIFYHOST' => [19],
134172
],
135173
],
136174
];

0 commit comments

Comments
 (0)