Merge "Add .pipeline/ with dev image variant"
[lhc/web/wiklou.git] / includes / utils / ClassCollector.php
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 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 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 ( substr( $match, -1 ) === '{' ) {
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 switch ( $this->startToken[0] ) {
133 case T_DOUBLE_COLON:
134 // Skip over T_CLASS after T_DOUBLE_COLON because this is something like
135 // "self::static" which accesses the class name. It doens't define a new class.
136 $this->startToken = null;
137 break;
138 case T_NEW:
139 // Skip over T_CLASS after T_NEW because this is a PHP 7 anonymous class.
140 if ( !is_array( $token ) || $token[0] !== T_WHITESPACE ) {
141 $this->startToken = null;
142 }
143 break;
144 case T_NAMESPACE:
145 if ( $token === ';' || $token === '{' ) {
146 $this->namespace = $this->implodeTokens() . '\\';
147 } else {
148 $this->tokens[] = $token;
149 }
150 break;
151
152 case T_STRING:
153 if ( $this->alias !== null ) {
154 // Flow 1 - Two string literals:
155 // - T_STRING class_alias
156 // - '('
157 // - T_CONSTANT_ENCAPSED_STRING 'TargetClass'
158 // - ','
159 // - T_WHITESPACE
160 // - T_CONSTANT_ENCAPSED_STRING 'AliasName'
161 // - ')'
162 // Flow 2 - Use of ::class syntax for first parameter
163 // - T_STRING class_alias
164 // - '('
165 // - T_STRING TargetClass
166 // - T_DOUBLE_COLON ::
167 // - T_CLASS class
168 // - ','
169 // - T_WHITESPACE
170 // - T_CONSTANT_ENCAPSED_STRING 'AliasName'
171 // - ')'
172 if ( $token === '(' ) {
173 // Start of a function call to class_alias()
174 $this->alias = [ 'target' => false, 'name' => false ];
175 } elseif ( $token === ',' ) {
176 // Record that we're past the first parameter
177 if ( $this->alias['target'] === false ) {
178 $this->alias['target'] = true;
179 }
180 } elseif ( is_array( $token ) && $token[0] === T_CONSTANT_ENCAPSED_STRING ) {
181 if ( $this->alias['target'] === true ) {
182 // We already saw a first argument, this must be the second.
183 // Strip quotes from the string literal.
184 $this->alias['name'] = substr( $token[1], 1, -1 );
185 }
186 } elseif ( $token === ')' ) {
187 // End of function call
188 $this->classes[] = $this->alias['name'];
189 $this->alias = null;
190 $this->startToken = null;
191 } elseif ( !is_array( $token ) || (
192 $token[0] !== T_STRING &&
193 $token[0] !== T_DOUBLE_COLON &&
194 $token[0] !== T_CLASS &&
195 $token[0] !== T_WHITESPACE
196 ) ) {
197 // Ignore this call to class_alias() - compat/Timestamp.php
198 $this->alias = null;
199 $this->startToken = null;
200 }
201 }
202 break;
203
204 case T_CLASS:
205 case T_INTERFACE:
206 case T_TRAIT:
207 $this->tokens[] = $token;
208 if ( is_array( $token ) && $token[0] === T_STRING ) {
209 $this->classes[] = $this->namespace . $this->implodeTokens();
210 }
211 }
212 }
213
214 /**
215 * Returns the string representation of the tokens within the
216 * current expect sequence and resets the sequence.
217 *
218 * @return string
219 */
220 protected function implodeTokens() {
221 $content = [];
222 foreach ( $this->tokens as $token ) {
223 $content[] = is_string( $token ) ? $token : $token[1];
224 }
225
226 $this->tokens = [];
227 $this->startToken = null;
228
229 return trim( implode( '', $content ), " \n\t" );
230 }
231 }