MediaWiki master
ClassCollector.php
Go to the documentation of this file.
1<?php
25
29 protected $namespace = '';
30
34 protected $classes;
35
39 protected $startToken;
40
44 protected $tokens;
45
49 protected $alias;
50
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|enum) |class_alias\‍()[^;{]+[;{]\s*\}?#m',
72 $code,
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
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_ENUM:
113 case T_INTERFACE:
114 case T_TRAIT:
115 case T_DOUBLE_COLON:
116 case T_NEW:
117 $this->startToken = $token;
118 break;
119 case T_STRING:
120 if ( $token[1] === 'class_alias' ) {
121 $this->startToken = $token;
122 $this->alias = [];
123 }
124 }
125 }
126
132 protected function tryEndExpect( $token ) {
133 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
134 switch ( $this->startToken[0] ) {
135 case T_DOUBLE_COLON:
136 // Skip over T_CLASS after T_DOUBLE_COLON because this is something like
137 // "ClassName::class" that evaluates to a fully qualified class name. It
138 // doesn't define a new class.
139 $this->startToken = null;
140 break;
141 case T_NEW:
142 // Skip over T_CLASS after T_NEW because this is an anonymous class.
143 if ( !is_array( $token ) || $token[0] !== T_WHITESPACE ) {
144 $this->startToken = null;
145 }
146 break;
147 case T_NAMESPACE:
148 if ( $token === ';' || $token === '{' ) {
149 $this->namespace = $this->implodeTokens() . '\\';
150 } else {
151 $this->tokens[] = $token;
152 }
153 break;
154
155 case T_STRING:
156 if ( $this->alias !== null ) {
157 // Flow 1 - Two string literals:
158 // - T_STRING class_alias
159 // - '('
160 // - T_CONSTANT_ENCAPSED_STRING 'TargetClass'
161 // - ','
162 // - T_WHITESPACE
163 // - T_CONSTANT_ENCAPSED_STRING 'AliasName'
164 // - ')'
165 // Flow 2 - Use of ::class syntax for first parameter
166 // - T_STRING class_alias
167 // - '('
168 // - T_STRING TargetClass
169 // - T_DOUBLE_COLON ::
170 // - T_CLASS class
171 // - ','
172 // - T_WHITESPACE
173 // - T_CONSTANT_ENCAPSED_STRING 'AliasName'
174 // - ')'
175 if ( $token === '(' ) {
176 // Start of a function call to class_alias()
177 $this->alias = [ 'target' => false, 'name' => false ];
178 } elseif ( $token === ',' ) {
179 // Record that we're past the first parameter
180 if ( $this->alias['target'] === false ) {
181 $this->alias['target'] = true;
182 }
183 } elseif ( is_array( $token ) && $token[0] === T_CONSTANT_ENCAPSED_STRING ) {
184 if ( $this->alias['target'] === true ) {
185 // We already saw a first argument, this must be the second.
186 // Strip quotes from the string literal.
187 $this->alias['name'] = self::stripQuotes( $token[1] );
188 }
189 } elseif ( $token === ')' ) {
190 // End of function call
191 $this->classes[] = $this->alias['name'];
192 $this->alias = null;
193 $this->startToken = null;
194 } elseif ( !is_array( $token ) || (
195 $token[0] !== T_STRING &&
196 $token[0] !== T_DOUBLE_COLON &&
197 $token[0] !== T_CLASS &&
198 $token[0] !== T_WHITESPACE
199 ) ) {
200 // Ignore this call to class_alias() - compat/Timestamp.php
201 $this->alias = null;
202 $this->startToken = null;
203 }
204 }
205 break;
206
207 case T_CLASS:
208 case T_ENUM:
209 case T_INTERFACE:
210 case T_TRAIT:
211 $this->tokens[] = $token;
212 if ( is_array( $token ) && $token[0] === T_STRING ) {
213 $this->classes[] = $this->namespace . $this->implodeTokens();
214 }
215 }
216 }
217
227 private static function stripQuotes( $str ) {
228 return str_replace( '\\\\', '\\', substr( $str, 1, -1 ) );
229 }
230
237 protected function implodeTokens() {
238 $content = [];
239 foreach ( $this->tokens as $token ) {
240 $content[] = is_string( $token ) ? $token : $token[1];
241 }
242
243 $this->tokens = [];
244 $this->startToken = null;
245
246 return trim( implode( '', $content ), " \n\t" );
247 }
248}
implodeTokens()
Returns the string representation of the tokens within the current expect sequence and resets the seq...
Reads PHP code and returns the FQCN of every class defined within it.
string $namespace
Current namespace.
array $classes
List of FQCN detected in this pass.
array null $alias
Class alias with target/name fields.
array null $startToken
Token from token_get_all() that started an expect sequence.
tryEndExpect( $token)
Accepts the next token in an expect sequence.
array[] string[] $tokens
List of tokens that are members of the current expect sequence.
tryBeginExpect( $token)
Determine if $token begins the next expect sequence.