Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
95.89% |
70 / 73 |
|
80.00% |
4 / 5 |
CRAP | |
0.00% |
0 / 1 |
ClassCollector | |
95.89% |
70 / 73 |
|
80.00% |
4 / 5 |
47 | |
0.00% |
0 / 1 |
getClasses | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
6 | |||
tryBeginExpect | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
10 | |||
tryEndExpect | |
91.18% |
31 / 34 |
|
0.00% |
0 / 1 |
27.50 | |||
stripQuotes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
implodeTokens | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | /** |
22 | * Reads PHP code and returns the FQCN of every class defined within it. |
23 | */ |
24 | class ClassCollector { |
25 | |
26 | /** |
27 | * @var string Current namespace |
28 | */ |
29 | protected $namespace = ''; |
30 | |
31 | /** |
32 | * @var array List of FQCN detected in this pass |
33 | */ |
34 | protected $classes; |
35 | |
36 | /** |
37 | * @var array|null Token from token_get_all() that started an expect sequence |
38 | */ |
39 | protected $startToken; |
40 | |
41 | /** |
42 | * @var array[]|string[] List of tokens that are members of the current expect sequence |
43 | */ |
44 | protected $tokens; |
45 | |
46 | /** |
47 | * @var array|null Class alias with target/name fields |
48 | */ |
49 | protected $alias; |
50 | |
51 | /** |
52 | * @param string $code PHP code (including <?php) to detect class names from |
53 | * @return array List of FQCN detected within the tokens |
54 | */ |
55 | public function getClasses( $code ) { |
56 | $this->namespace = ''; |
57 | $this->classes = []; |
58 | $this->startToken = null; |
59 | $this->alias = null; |
60 | $this->tokens = []; |
61 | |
62 | // HACK: The PHP tokenizer is slow (T225730). |
63 | // Speed it up by reducing the input to the three kinds of statement we care about: |
64 | // - namespace X; |
65 | // - [final] [abstract] class X … {} |
66 | // - class_alias( … ); |
67 | $lines = []; |
68 | $matches = null; |
69 | preg_match_all( |
70 | // phpcs:ignore Generic.Files.LineLength.TooLong |
71 | '#^\t*(?:namespace |(final )?(abstract )?(class|interface|trait) |class_alias\()[^;{]+[;{]\s*\}?#m', |
72 | $code, |
73 | $matches |
74 | ); |
75 | if ( isset( $matches[0][0] ) ) { |
76 | foreach ( $matches[0] as $match ) { |
77 | $match = trim( $match ); |
78 | if ( str_ends_with( $match, '{' ) ) { |
79 | // Keep it balanced |
80 | $match .= '}'; |
81 | } |
82 | $lines[] = $match; |
83 | } |
84 | } |
85 | $code = '<?php ' . implode( "\n", $lines ) . "\n"; |
86 | |
87 | foreach ( token_get_all( $code ) as $token ) { |
88 | if ( $this->startToken === null ) { |
89 | $this->tryBeginExpect( $token ); |
90 | } else { |
91 | $this->tryEndExpect( $token ); |
92 | } |
93 | } |
94 | |
95 | return $this->classes; |
96 | } |
97 | |
98 | /** |
99 | * Determine if $token begins the next expect sequence. |
100 | * |
101 | * @param array $token |
102 | */ |
103 | protected function tryBeginExpect( $token ) { |
104 | if ( is_string( $token ) ) { |
105 | return; |
106 | } |
107 | // Note: When changing class name discovery logic, |
108 | // AutoLoaderStructureTest.php may also need to be updated. |
109 | switch ( $token[0] ) { |
110 | case T_NAMESPACE: |
111 | case T_CLASS: |
112 | case T_INTERFACE: |
113 | case T_TRAIT: |
114 | case T_DOUBLE_COLON: |
115 | case T_NEW: |
116 | $this->startToken = $token; |
117 | break; |
118 | case T_STRING: |
119 | if ( $token[1] === 'class_alias' ) { |
120 | $this->startToken = $token; |
121 | $this->alias = []; |
122 | } |
123 | } |
124 | } |
125 | |
126 | /** |
127 | * Accepts the next token in an expect sequence |
128 | * |
129 | * @param array|string $token |
130 | */ |
131 | protected function tryEndExpect( $token ) { |
132 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
133 | switch ( $this->startToken[0] ) { |
134 | case T_DOUBLE_COLON: |
135 | // Skip over T_CLASS after T_DOUBLE_COLON because this is something like |
136 | // "ClassName::class" that evaluates to a fully qualified class name. It |
137 | // doesn't define a new class. |
138 | $this->startToken = null; |
139 | break; |
140 | case T_NEW: |
141 | // Skip over T_CLASS after T_NEW because this is an anonymous class. |
142 | if ( !is_array( $token ) || $token[0] !== T_WHITESPACE ) { |
143 | $this->startToken = null; |
144 | } |
145 | break; |
146 | case T_NAMESPACE: |
147 | if ( $token === ';' || $token === '{' ) { |
148 | $this->namespace = $this->implodeTokens() . '\\'; |
149 | } else { |
150 | $this->tokens[] = $token; |
151 | } |
152 | break; |
153 | |
154 | case T_STRING: |
155 | if ( $this->alias !== null ) { |
156 | // Flow 1 - Two string literals: |
157 | // - T_STRING class_alias |
158 | // - '(' |
159 | // - T_CONSTANT_ENCAPSED_STRING 'TargetClass' |
160 | // - ',' |
161 | // - T_WHITESPACE |
162 | // - T_CONSTANT_ENCAPSED_STRING 'AliasName' |
163 | // - ')' |
164 | // Flow 2 - Use of ::class syntax for first parameter |
165 | // - T_STRING class_alias |
166 | // - '(' |
167 | // - T_STRING TargetClass |
168 | // - T_DOUBLE_COLON :: |
169 | // - T_CLASS class |
170 | // - ',' |
171 | // - T_WHITESPACE |
172 | // - T_CONSTANT_ENCAPSED_STRING 'AliasName' |
173 | // - ')' |
174 | if ( $token === '(' ) { |
175 | // Start of a function call to class_alias() |
176 | $this->alias = [ 'target' => false, 'name' => false ]; |
177 | } elseif ( $token === ',' ) { |
178 | // Record that we're past the first parameter |
179 | if ( $this->alias['target'] === false ) { |
180 | $this->alias['target'] = true; |
181 | } |
182 | } elseif ( is_array( $token ) && $token[0] === T_CONSTANT_ENCAPSED_STRING ) { |
183 | if ( $this->alias['target'] === true ) { |
184 | // We already saw a first argument, this must be the second. |
185 | // Strip quotes from the string literal. |
186 | $this->alias['name'] = self::stripQuotes( $token[1] ); |
187 | } |
188 | } elseif ( $token === ')' ) { |
189 | // End of function call |
190 | $this->classes[] = $this->alias['name']; |
191 | $this->alias = null; |
192 | $this->startToken = null; |
193 | } elseif ( !is_array( $token ) || ( |
194 | $token[0] !== T_STRING && |
195 | $token[0] !== T_DOUBLE_COLON && |
196 | $token[0] !== T_CLASS && |
197 | $token[0] !== T_WHITESPACE |
198 | ) ) { |
199 | // Ignore this call to class_alias() - compat/Timestamp.php |
200 | $this->alias = null; |
201 | $this->startToken = null; |
202 | } |
203 | } |
204 | break; |
205 | |
206 | case T_CLASS: |
207 | case T_INTERFACE: |
208 | case T_TRAIT: |
209 | $this->tokens[] = $token; |
210 | if ( is_array( $token ) && $token[0] === T_STRING ) { |
211 | $this->classes[] = $this->namespace . $this->implodeTokens(); |
212 | } |
213 | } |
214 | } |
215 | |
216 | /** |
217 | * Decode a quoted PHP string, interpreting escape sequences, like eval($str). |
218 | * The implementation is half-baked, but the character set allowed in class |
219 | * names is pretty small. This could be replaced by a call to a fully-baked |
220 | * utility function. |
221 | * |
222 | * @param string $str |
223 | * @return string |
224 | */ |
225 | private static function stripQuotes( $str ) { |
226 | return str_replace( '\\\\', '\\', substr( $str, 1, -1 ) ); |
227 | } |
228 | |
229 | /** |
230 | * Returns the string representation of the tokens within the |
231 | * current expect sequence and resets the sequence. |
232 | * |
233 | * @return string |
234 | */ |
235 | protected function implodeTokens() { |
236 | $content = []; |
237 | foreach ( $this->tokens as $token ) { |
238 | $content[] = is_string( $token ) ? $token : $token[1]; |
239 | } |
240 | |
241 | $this->tokens = []; |
242 | $this->startToken = null; |
243 | |
244 | return trim( implode( '', $content ), " \n\t" ); |
245 | } |
246 | } |