MediaWiki master
ClassCollector.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Autoload;
8
16
20 protected $namespace = '';
21
25 protected $classes;
26
30 protected $startToken;
31
35 protected $tokens;
36
40 protected $alias;
41
46 public function getClasses( $code ) {
47 $this->namespace = '';
48 $this->classes = [];
49 $this->startToken = null;
50 $this->alias = null;
51 $this->tokens = [];
52
53 // HACK: The PHP tokenizer is slow (T225730).
54 // Speed it up by reducing the input to the three kinds of statement we care about:
55 // - namespace X;
56 // - [final] [abstract] class X … {}
57 // - class_alias( … );
58 $lines = [];
59 $matches = null;
60 preg_match_all(
61 // phpcs:ignore Generic.Files.LineLength.TooLong
62 '#^\t*(?:namespace |(final )?(abstract )?(class|interface|trait|enum) |class_alias\‍()[^;{]+[;{]\s*\}?#m',
63 $code,
65 );
66 if ( isset( $matches[0][0] ) ) {
67 foreach ( $matches[0] as $match ) {
68 $match = trim( $match );
69 if ( str_ends_with( $match, '{' ) ) {
70 // Keep it balanced
71 $match .= '}';
72 }
73 $lines[] = $match;
74 }
75 }
76 $code = '<?php ' . implode( "\n", $lines ) . "\n";
77
78 foreach ( token_get_all( $code ) as $token ) {
79 if ( $this->startToken === null ) {
80 $this->tryBeginExpect( $token );
81 } else {
82 $this->tryEndExpect( $token );
83 }
84 }
85
86 return $this->classes;
87 }
88
94 protected function tryBeginExpect( $token ) {
95 if ( is_string( $token ) ) {
96 return;
97 }
98 // Note: When changing class name discovery logic,
99 // AutoLoaderStructureTest.php may also need to be updated.
100 switch ( $token[0] ) {
101 case T_NAMESPACE:
102 case T_CLASS:
103 case T_ENUM:
104 case T_INTERFACE:
105 case T_TRAIT:
106 case T_DOUBLE_COLON:
107 case T_NEW:
108 $this->startToken = $token;
109 break;
110 case T_STRING:
111 if ( $token[1] === 'class_alias' ) {
112 $this->startToken = $token;
113 $this->alias = [];
114 }
115 }
116 }
117
123 protected function tryEndExpect( $token ) {
124 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
125 switch ( $this->startToken[0] ) {
126 case T_DOUBLE_COLON:
127 // Skip over T_CLASS after T_DOUBLE_COLON because this is something like
128 // "ClassName::class" that evaluates to a fully qualified class name. It
129 // doesn't define a new class.
130 $this->startToken = null;
131 break;
132 case T_NEW:
133 // Skip over T_CLASS after T_NEW because this is an anonymous class.
134 if ( !is_array( $token ) || $token[0] !== T_WHITESPACE ) {
135 $this->startToken = null;
136 }
137 break;
138 case T_NAMESPACE:
139 if ( $token === ';' || $token === '{' ) {
140 $this->namespace = $this->implodeTokens() . '\\';
141 } else {
142 $this->tokens[] = $token;
143 }
144 break;
145
146 case T_STRING:
147 if ( $this->alias !== null ) {
148 // Flow 1 - Two string literals:
149 // - T_STRING class_alias
150 // - '('
151 // - T_CONSTANT_ENCAPSED_STRING 'TargetClass'
152 // - ','
153 // - T_WHITESPACE
154 // - T_CONSTANT_ENCAPSED_STRING 'AliasName'
155 // - ')'
156 // Flow 2 - Use of ::class syntax for first parameter
157 // - T_STRING class_alias
158 // - '('
159 // - T_STRING TargetClass
160 // - T_DOUBLE_COLON ::
161 // - T_CLASS class
162 // - ','
163 // - T_WHITESPACE
164 // - T_CONSTANT_ENCAPSED_STRING 'AliasName'
165 // - ')'
166 if ( $token === '(' ) {
167 // Start of a function call to class_alias()
168 $this->alias = [ 'target' => false, 'name' => false ];
169 } elseif ( $token === ',' ) {
170 // Record that we're past the first parameter
171 if ( $this->alias['target'] === false ) {
172 $this->alias['target'] = true;
173 }
174 } elseif ( is_array( $token ) && $token[0] === T_CONSTANT_ENCAPSED_STRING ) {
175 if ( $this->alias['target'] === true ) {
176 // We already saw a first argument, this must be the second.
177 // Strip quotes from the string literal.
178 $this->alias['name'] = self::stripQuotes( $token[1] );
179 }
180 } elseif ( $token === ')' ) {
181 // End of function call
182 $this->classes[] = $this->alias['name'];
183 $this->alias = null;
184 $this->startToken = null;
185 } elseif ( !is_array( $token ) || (
186 $token[0] !== T_STRING &&
187 $token[0] !== T_DOUBLE_COLON &&
188 $token[0] !== T_CLASS &&
189 $token[0] !== T_WHITESPACE
190 ) ) {
191 // Ignore this call to class_alias() - compat/Timestamp.php
192 $this->alias = null;
193 $this->startToken = null;
194 }
195 }
196 break;
197
198 case T_CLASS:
199 case T_ENUM:
200 case T_INTERFACE:
201 case T_TRAIT:
202 $this->tokens[] = $token;
203 if ( is_array( $token ) && $token[0] === T_STRING ) {
204 $this->classes[] = $this->namespace . $this->implodeTokens();
205 }
206 }
207 }
208
218 private static function stripQuotes( $str ) {
219 return str_replace( '\\\\', '\\', substr( $str, 1, -1 ) );
220 }
221
228 protected function implodeTokens() {
229 $content = [];
230 foreach ( $this->tokens as $token ) {
231 $content[] = is_string( $token ) ? $token : $token[1];
232 }
233
234 $this->tokens = [];
235 $this->startToken = null;
236
237 return trim( implode( '', $content ), " \n\t" );
238 }
239}
240
242class_alias( ClassCollector::class, 'ClassCollector' );
Read a PHP file and return the FQCN of every class defined within it.
string $namespace
Current namespace.
tryBeginExpect( $token)
Determine if $token begins the next expect sequence.
tryEndExpect( $token)
Accepts the next token in an expect sequence.
array $classes
List of FQCN detected in this pass.
array[] string[] $tokens
List of tokens that are members of the current expect sequence.
array null $alias
Class alias with target/name fields.
array null $startToken
Token from token_get_all() that started an expect sequence.
implodeTokens()
Returns the string representation of the tokens within the current expect sequence and resets the seq...