22
33namespace ShipMonk \Composer ;
44
5+ use function array_merge ;
56use function count ;
7+ use function explode ;
8+ use function is_array ;
69use function ltrim ;
10+ use function strlen ;
11+ use function substr ;
712use function token_get_all ;
813use const PHP_VERSION_ID ;
914use const T_AS ;
1015use const T_COMMENT ;
1116use const T_DOC_COMMENT ;
1217use const T_NAME_FULLY_QUALIFIED ;
1318use const T_NAME_QUALIFIED ;
19+ use const T_NAMESPACE ;
1420use const T_NS_SEPARATOR ;
1521use const T_STRING ;
1622use const T_USE ;
@@ -34,35 +40,92 @@ class UsedSymbolExtractor
3440 */
3541 private $ pointer = 0 ;
3642
37- /**
38- * @var int
39- */
40- private $ level = 0 ;
41-
4243 public function __construct (string $ code )
4344 {
4445 $ this ->tokens = token_get_all ($ code );
4546 $ this ->numTokens = count ($ this ->tokens );
4647 }
4748
4849 /**
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+ *
4956 * @return list<string>
57+ * @license Inspired by https://github.com/doctrine/annotations/blob/2.0.0/lib/Doctrine/Common/Annotations/TokenParser.php
5058 */
51- public function parseUsedSymbols (): array
59+ public function parseUsedClasses (): array
5260 {
53- $ statements = [];
61+ $ usedSymbols = [];
62+ $ useStatements = [];
5463
5564 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 ();
5867
5968 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+ }
61124 }
62125 }
63126 }
64127
65- return $ statements ;
128+ return $ usedSymbols ;
66129 }
67130
68131 /**
@@ -74,59 +137,104 @@ private function getNextEffectiveToken()
74137 $ this ->pointer ++;
75138 $ token = $ this ->tokens [$ i ];
76139
77- if (
78- $ token [0 ] === T_WHITESPACE ||
79- $ token [0 ] === T_COMMENT ||
80- $ token [0 ] === T_DOC_COMMENT
81- ) {
140+ if ($ this ->isNonEffectiveToken ($ token )) {
82141 continue ;
83142 }
84143
85- if ($ token === '{ ' ) {
86- $ this ->level ++;
87- } elseif ($ token === '} ' ) {
88- $ this ->level --;
89- }
90-
91144 return $ token ;
92145 }
93146
94147 return null ;
95148 }
96149
97150 /**
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
107166 */
108- private function parseSimpleUseStatement (): ? string
167+ private function parseNameForOldPhp (): string
109168 {
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 = '' ;
110194 $ class = '' ;
195+ $ alias = '' ;
196+ $ statements = [];
197+ $ explicitAlias = false ;
111198
112- while ($ token = $ this ->getNextEffectiveToken ()) {
113- if ($ token [0 ] === T_STRING ) {
199+ while (( $ token = $ this ->getNextEffectiveToken () )) {
200+ if (! $ explicitAlias && $ token [0 ] === T_STRING ) {
114201 $ class .= $ token [1 ];
202+ $ alias = $ token [1 ];
203+ } elseif ($ explicitAlias && $ token [0 ] === T_STRING ) {
204+ $ alias = $ token [1 ];
115205 } 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 )
118208 ) {
119209 $ class .= $ token [1 ];
210+
211+ $ classSplit = explode ('\\' , $ token [1 ]);
212+ $ alias = $ classSplit [count ($ classSplit ) - 1 ];
120213 } elseif ($ token [0 ] === T_NS_SEPARATOR ) {
121214 $ 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 ;
124232 } else {
125233 break ;
126234 }
127235 }
128236
129- return null ;
237+ return $ statements === [] ? null : $ statements ;
130238 }
131239
132240 private function normalizeBackslash (string $ class ): string
0 commit comments