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) |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 
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 
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  // "self::static" which accesses the class name. It doens't define a new class.
137  $this->startToken = null;
138  break;
139  case T_NEW:
140  // Skip over T_CLASS after T_NEW because this is a PHP 7 anonymous class.
141  if ( !is_array( $token ) || $token[0] !== T_WHITESPACE ) {
142  $this->startToken = null;
143  }
144  break;
145  case T_NAMESPACE:
146  if ( $token === ';' || $token === '{' ) {
147  $this->namespace = $this->implodeTokens() . '\\';
148  } else {
149  $this->tokens[] = $token;
150  }
151  break;
152 
153  case T_STRING:
154  if ( $this->alias !== null ) {
155  // Flow 1 - Two string literals:
156  // - T_STRING class_alias
157  // - '('
158  // - T_CONSTANT_ENCAPSED_STRING 'TargetClass'
159  // - ','
160  // - T_WHITESPACE
161  // - T_CONSTANT_ENCAPSED_STRING 'AliasName'
162  // - ')'
163  // Flow 2 - Use of ::class syntax for first parameter
164  // - T_STRING class_alias
165  // - '('
166  // - T_STRING TargetClass
167  // - T_DOUBLE_COLON ::
168  // - T_CLASS class
169  // - ','
170  // - T_WHITESPACE
171  // - T_CONSTANT_ENCAPSED_STRING 'AliasName'
172  // - ')'
173  if ( $token === '(' ) {
174  // Start of a function call to class_alias()
175  $this->alias = [ 'target' => false, 'name' => false ];
176  } elseif ( $token === ',' ) {
177  // Record that we're past the first parameter
178  if ( $this->alias['target'] === false ) {
179  $this->alias['target'] = true;
180  }
181  } elseif ( is_array( $token ) && $token[0] === T_CONSTANT_ENCAPSED_STRING ) {
182  if ( $this->alias['target'] === true ) {
183  // We already saw a first argument, this must be the second.
184  // Strip quotes from the string literal.
185  $this->alias['name'] = self::stripQuotes( $token[1] );
186  }
187  } elseif ( $token === ')' ) {
188  // End of function call
189  $this->classes[] = $this->alias['name'];
190  $this->alias = null;
191  $this->startToken = null;
192  } elseif ( !is_array( $token ) || (
193  $token[0] !== T_STRING &&
194  $token[0] !== T_DOUBLE_COLON &&
195  $token[0] !== T_CLASS &&
196  $token[0] !== T_WHITESPACE
197  ) ) {
198  // Ignore this call to class_alias() - compat/Timestamp.php
199  $this->alias = null;
200  $this->startToken = null;
201  }
202  }
203  break;
204 
205  case T_CLASS:
206  case T_INTERFACE:
207  case T_TRAIT:
208  $this->tokens[] = $token;
209  if ( is_array( $token ) && $token[0] === T_STRING ) {
210  $this->classes[] = $this->namespace . $this->implodeTokens();
211  }
212  }
213  }
214 
224  private static function stripQuotes( $str ) {
225  return str_replace( '\\\\', '\\', substr( $str, 1, -1 ) );
226  }
227 
234  protected function implodeTokens() {
235  $content = [];
236  foreach ( $this->tokens as $token ) {
237  $content[] = is_string( $token ) ? $token : $token[1];
238  }
239 
240  $this->tokens = [];
241  $this->startToken = null;
242 
243  return trim( implode( '', $content ), " \n\t" );
244  }
245 }
implodeTokens
implodeTokens()
Returns the string representation of the tokens within the current expect sequence and resets the seq...
Definition: ClassCollector.php:234
ClassCollector\tryBeginExpect
tryBeginExpect( $token)
Determine if $token begins the next expect sequence.
Definition: ClassCollector.php:103
ClassCollector\$tokens
array[] string[] $tokens
List of tokens that are members of the current expect sequence.
Definition: ClassCollector.php:44
ClassCollector
Reads PHP code and returns the FQCN of every class defined within it.
Definition: ClassCollector.php:24
ClassCollector\$classes
array $classes
List of FQCN detected in this pass.
Definition: ClassCollector.php:34
$matches
$matches
Definition: NoLocalSettings.php:24
ClassCollector\getClasses
getClasses( $code)
Definition: ClassCollector.php:55
ClassCollector\$namespace
string $namespace
Current namespace.
Definition: ClassCollector.php:29
$content
$content
Definition: router.php:76
stripQuotes
static stripQuotes( $str)
Decode a quoted PHP string, interpreting escape sequences, like eval($str).
Definition: ClassCollector.php:224
$lines
if(!file_exists( $CREDITS)) $lines
Definition: updateCredits.php:49
ClassCollector\$alias
array $alias
Class alias with target/name fields.
Definition: ClassCollector.php:49
ClassCollector\tryEndExpect
tryEndExpect( $token)
Accepts the next token in an expect sequence.
Definition: ClassCollector.php:131
ClassCollector\$startToken
array $startToken
Token from token_get_all() that started an expect sequence.
Definition: ClassCollector.php:39