2
2
3
3
namespace ShipMonk \Composer ;
4
4
5
+ use function array_merge ;
5
6
use function count ;
7
+ use function explode ;
8
+ use function is_array ;
6
9
use function ltrim ;
10
+ use function strlen ;
11
+ use function substr ;
7
12
use function token_get_all ;
8
13
use const PHP_VERSION_ID ;
9
14
use const T_AS ;
10
15
use const T_COMMENT ;
11
16
use const T_DOC_COMMENT ;
12
17
use const T_NAME_FULLY_QUALIFIED ;
13
18
use const T_NAME_QUALIFIED ;
19
+ use const T_NAMESPACE ;
14
20
use const T_NS_SEPARATOR ;
15
21
use const T_STRING ;
16
22
use const T_USE ;
@@ -34,35 +40,92 @@ class UsedSymbolExtractor
34
40
*/
35
41
private $ pointer = 0 ;
36
42
37
- /**
38
- * @var int
39
- */
40
- private $ level = 0 ;
41
-
42
43
public function __construct (string $ code )
43
44
{
44
45
$ this ->tokens = token_get_all ($ code );
45
46
$ this ->numTokens = count ($ this ->tokens );
46
47
}
47
48
48
49
/**
50
+ * As we do not verify if the resulting name are classes, it can return even used functions or constants (due to FQNs).
51
+ * - elimination of those is solved in ComposerDependencyAnalyser::isConstOrFunction
52
+ *
53
+ * It does not produce any local names in current namespace
54
+ * - this results in very limited functionality in files without namespace
55
+ *
49
56
* @return list<string>
57
+ * @license Inspired by https://github.com/doctrine/annotations/blob/2.0.0/lib/Doctrine/Common/Annotations/TokenParser.php
50
58
*/
51
- public function parseUsedSymbols (): array
59
+ public function parseUsedClasses (): array
52
60
{
53
- $ statements = [];
61
+ $ usedSymbols = [];
62
+ $ useStatements = [];
54
63
55
64
while ($ token = $ this ->getNextEffectiveToken ()) {
56
- if ($ token [0 ] === T_USE && $ this -> level === 0 ) {
57
- $ usedClass = $ this ->parseSimpleUseStatement ();
65
+ if ($ token [0 ] === T_USE ) {
66
+ $ usedClass = $ this ->parseUseStatement ();
58
67
59
68
if ($ usedClass !== null ) {
60
- $ statements [] = $ usedClass ;
69
+ $ useStatements = array_merge ($ useStatements , $ usedClass );
70
+ }
71
+ }
72
+
73
+ if (PHP_VERSION_ID >= 80000 ) {
74
+ if ($ token [0 ] === T_NAMESPACE ) {
75
+ $ useStatements = []; // reset use statements on namespace change
76
+ }
77
+
78
+ if ($ token [0 ] === T_NAME_FULLY_QUALIFIED ) {
79
+ $ usedSymbols [] = $ this ->normalizeBackslash ($ token [1 ]);
80
+ }
81
+
82
+ if ($ token [0 ] === T_NAME_QUALIFIED ) {
83
+ [$ neededAlias ] = explode ('\\' , $ token [1 ], 2 );
84
+
85
+ if (isset ($ useStatements [$ neededAlias ])) {
86
+ $ usedSymbols [] = $ this ->normalizeBackslash ($ useStatements [$ neededAlias ] . substr ($ token [1 ], strlen ($ neededAlias )));
87
+ }
88
+ }
89
+
90
+ if ($ token [0 ] === T_STRING ) {
91
+ $ symbolName = $ token [1 ];
92
+
93
+ if (isset ($ useStatements [$ symbolName ])) {
94
+ $ usedSymbols [] = $ this ->normalizeBackslash ($ useStatements [$ symbolName ]);
95
+ }
96
+ }
97
+ } else {
98
+ if ($ token [0 ] === T_NAMESPACE ) {
99
+ $ this ->pointer ++;
100
+ $ nextName = $ this ->parseNameForOldPhp ();
101
+
102
+ if (substr ($ nextName , 0 , 1 ) !== '\\' ) { // not a namespace-relative name, but a new namespace declaration
103
+ $ useStatements = []; // reset use statements on namespace change
104
+ }
105
+ }
106
+
107
+ if ($ token [0 ] === T_NS_SEPARATOR ) { // fully qualified name
108
+ $ usedSymbols [] = $ this ->normalizeBackslash ($ this ->parseNameForOldPhp ());
109
+ }
110
+
111
+ if ($ token [0 ] === T_STRING ) {
112
+ $ symbolName = $ this ->parseNameForOldPhp ();
113
+
114
+ if (isset ($ useStatements [$ symbolName ])) { // unqualified name
115
+ $ usedSymbols [] = $ this ->normalizeBackslash ($ useStatements [$ symbolName ]);
116
+
117
+ } else {
118
+ [$ neededAlias ] = explode ('\\' , $ symbolName , 2 );
119
+
120
+ if (isset ($ useStatements [$ neededAlias ])) { // qualified name
121
+ $ usedSymbols [] = $ this ->normalizeBackslash ($ useStatements [$ neededAlias ] . substr ($ symbolName , strlen ($ neededAlias )));
122
+ }
123
+ }
61
124
}
62
125
}
63
126
}
64
127
65
- return $ statements ;
128
+ return $ usedSymbols ;
66
129
}
67
130
68
131
/**
@@ -74,59 +137,104 @@ private function getNextEffectiveToken()
74
137
$ this ->pointer ++;
75
138
$ token = $ this ->tokens [$ i ];
76
139
77
- if (
78
- $ token [0 ] === T_WHITESPACE ||
79
- $ token [0 ] === T_COMMENT ||
80
- $ token [0 ] === T_DOC_COMMENT
81
- ) {
140
+ if ($ this ->isNonEffectiveToken ($ token )) {
82
141
continue ;
83
142
}
84
143
85
- if ($ token === '{ ' ) {
86
- $ this ->level ++;
87
- } elseif ($ token === '} ' ) {
88
- $ this ->level --;
89
- }
90
-
91
144
return $ token ;
92
145
}
93
146
94
147
return null ;
95
148
}
96
149
97
150
/**
98
- * Parses simple use statement like:
99
- *
100
- * use Foo\Bar;
101
- * use Foo\Bar as Alias;
102
- *
103
- * Does not support bracket syntax nor comma-separated statements:
104
- *
105
- * use Foo\{ Bar, Baz };
106
- * use Foo\Bar, Foo\Baz;
151
+ * @param array{int, string, int}|string $token
152
+ */
153
+ private function isNonEffectiveToken ($ token ): bool
154
+ {
155
+ if (!is_array ($ token )) {
156
+ return false ;
157
+ }
158
+
159
+ return $ token [0 ] === T_WHITESPACE ||
160
+ $ token [0 ] === T_COMMENT ||
161
+ $ token [0 ] === T_DOC_COMMENT ;
162
+ }
163
+
164
+ /**
165
+ * See old behaviour: https://wiki.php.net/rfc/namespaced_names_as_token
107
166
*/
108
- private function parseSimpleUseStatement (): ? string
167
+ private function parseNameForOldPhp (): string
109
168
{
169
+ $ this ->pointer --; // we already detected start token above
170
+
171
+ $ name = '' ;
172
+
173
+ do {
174
+ $ token = $ this ->getNextEffectiveToken ();
175
+ $ isNamePart = is_array ($ token ) && ($ token [0 ] === T_STRING || $ token [0 ] === T_NS_SEPARATOR );
176
+
177
+ if (!$ isNamePart ) {
178
+ break ;
179
+ }
180
+
181
+ $ name .= $ token [1 ];
182
+
183
+ } while (true );
184
+
185
+ return $ name ;
186
+ }
187
+
188
+ /**
189
+ * @return array<string, string>|null
190
+ */
191
+ public function parseUseStatement (): ?array
192
+ {
193
+ $ groupRoot = '' ;
110
194
$ class = '' ;
195
+ $ alias = '' ;
196
+ $ statements = [];
197
+ $ explicitAlias = false ;
111
198
112
- while ($ token = $ this ->getNextEffectiveToken ()) {
113
- if ($ token [0 ] === T_STRING ) {
199
+ while (( $ token = $ this ->getNextEffectiveToken () )) {
200
+ if (! $ explicitAlias && $ token [0 ] === T_STRING ) {
114
201
$ class .= $ token [1 ];
202
+ $ alias = $ token [1 ];
203
+ } elseif ($ explicitAlias && $ token [0 ] === T_STRING ) {
204
+ $ alias = $ token [1 ];
115
205
} elseif (
116
- PHP_VERSION_ID >= 80000 &&
117
- ($ token [0 ] === T_NAME_QUALIFIED || $ token [0 ] === T_NAME_FULLY_QUALIFIED )
206
+ PHP_VERSION_ID >= 80000
207
+ && ($ token [0 ] === T_NAME_QUALIFIED || $ token [0 ] === T_NAME_FULLY_QUALIFIED )
118
208
) {
119
209
$ class .= $ token [1 ];
210
+
211
+ $ classSplit = explode ('\\' , $ token [1 ]);
212
+ $ alias = $ classSplit [count ($ classSplit ) - 1 ];
120
213
} elseif ($ token [0 ] === T_NS_SEPARATOR ) {
121
214
$ class .= '\\' ;
122
- } elseif ($ token [0 ] === T_AS || $ token === '; ' ) {
123
- return $ this ->normalizeBackslash ($ class );
215
+ $ alias = '' ;
216
+ } elseif ($ token [0 ] === T_AS ) {
217
+ $ explicitAlias = true ;
218
+ $ alias = '' ;
219
+ } elseif ($ token === ', ' ) {
220
+ $ statements [$ alias ] = $ groupRoot . $ class ;
221
+ $ class = '' ;
222
+ $ alias = '' ;
223
+ $ explicitAlias = false ;
224
+ } elseif ($ token === '; ' ) {
225
+ $ statements [$ alias ] = $ groupRoot . $ class ;
226
+ break ;
227
+ } elseif ($ token === '{ ' ) {
228
+ $ groupRoot = $ class ;
229
+ $ class = '' ;
230
+ } elseif ($ token === '} ' ) {
231
+ continue ;
124
232
} else {
125
233
break ;
126
234
}
127
235
}
128
236
129
- return null ;
237
+ return $ statements === [] ? null : $ statements ;
130
238
}
131
239
132
240
private function normalizeBackslash (string $ class ): string
0 commit comments