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.
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.
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
22 * Reads PHP code and returns the FQCN of every class defined within it.
24 class ClassCollector
{
27 * @var string Current namespace
29 protected $namespace = '';
32 * @var array List of FQCN detected in this pass
37 * @var array Token from token_get_all() that started an expect sequence
39 protected $startToken;
42 * @var array[]|string[] List of tokens that are members of the current expect sequence
47 * @var array Class alias with target/name fields
52 * @param string $code PHP code (including <?php) to detect class names from
53 * @return array List of FQCN detected within the tokens
55 public function getClasses( $code ) {
56 $this->namespace = '';
58 $this->startToken
= null;
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:
65 // - [final] [abstract] class X … {}
66 // - class_alias( … );
70 // phpcs:ignore Generic.Files.LineLength.TooLong
71 '#^\t*(?:namespace |(final )?(abstract )?(class|interface|trait) |class_alias\()[^;{]+[;{]\s*\}?#m',
75 if ( isset( $matches[0][0] ) ) {
76 foreach ( $matches[0] as $match ) {
77 $match = trim( $match );
78 if ( substr( $match, -1 ) === '{' ) {
85 $code = '<?php ' . implode( "\n", $lines ) . "\n";
87 foreach ( token_get_all( $code ) as $token ) {
88 if ( $this->startToken
=== null ) {
89 $this->tryBeginExpect( $token );
91 $this->tryEndExpect( $token );
95 return $this->classes
;
99 * Determine if $token begins the next expect sequence.
101 * @param array $token
103 protected function tryBeginExpect( $token ) {
104 if ( is_string( $token ) ) {
107 // Note: When changing class name discovery logic,
108 // AutoLoaderStructureTest.php may also need to be updated.
109 switch ( $token[0] ) {
116 $this->startToken
= $token;
119 if ( $token[1] === 'class_alias' ) {
120 $this->startToken
= $token;
127 * Accepts the next token in an expect sequence
129 * @param array|string $token
131 protected function tryEndExpect( $token ) {
132 switch ( $this->startToken
[0] ) {
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;
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;
145 if ( $token === ';' ||
$token === '{' ) {
146 $this->namespace = $this->implodeTokens() . '\\';
148 $this->tokens
[] = $token;
153 if ( $this->alias
!== null ) {
154 // Flow 1 - Two string literals:
155 // - T_STRING class_alias
157 // - T_CONSTANT_ENCAPSED_STRING 'TargetClass'
160 // - T_CONSTANT_ENCAPSED_STRING 'AliasName'
162 // Flow 2 - Use of ::class syntax for first parameter
163 // - T_STRING class_alias
165 // - T_STRING TargetClass
166 // - T_DOUBLE_COLON ::
170 // - T_CONSTANT_ENCAPSED_STRING 'AliasName'
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;
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 );
186 } elseif ( $token === ')' ) {
187 // End of function call
188 $this->classes
[] = $this->alias
['name'];
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
197 // Ignore this call to class_alias() - compat/Timestamp.php
199 $this->startToken
= null;
207 $this->tokens
[] = $token;
208 if ( is_array( $token ) && $token[0] === T_STRING
) {
209 $this->classes
[] = $this->namespace . $this->implodeTokens();
215 * Returns the string representation of the tokens within the
216 * current expect sequence and resets the sequence.
220 protected function implodeTokens() {
222 foreach ( $this->tokens
as $token ) {
223 $content[] = is_string( $token ) ?
$token : $token[1];
227 $this->startToken
= null;
229 return trim( implode( '', $content ), " \n\t" );