Move ClassCollector to its own file
[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 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 foreach ( token_get_all( $code ) as $token ) {
63 if ( $this->startToken === null ) {
64 $this->tryBeginExpect( $token );
65 } else {
66 $this->tryEndExpect( $token );
67 }
68 }
69
70 return $this->classes;
71 }
72
73 /**
74 * Determine if $token begins the next expect sequence.
75 *
76 * @param array $token
77 */
78 protected function tryBeginExpect( $token ) {
79 if ( is_string( $token ) ) {
80 return;
81 }
82 // Note: When changing class name discovery logic,
83 // AutoLoaderStructureTest.php may also need to be updated.
84 switch ( $token[0] ) {
85 case T_NAMESPACE:
86 case T_CLASS:
87 case T_INTERFACE:
88 case T_TRAIT:
89 case T_DOUBLE_COLON:
90 case T_NEW:
91 $this->startToken = $token;
92 break;
93 case T_STRING:
94 if ( $token[1] === 'class_alias' ) {
95 $this->startToken = $token;
96 $this->alias = [];
97 }
98 }
99 }
100
101 /**
102 * Accepts the next token in an expect sequence
103 *
104 * @param array $token
105 */
106 protected function tryEndExpect( $token ) {
107 switch ( $this->startToken[0] ) {
108 case T_DOUBLE_COLON:
109 // Skip over T_CLASS after T_DOUBLE_COLON because this is something like
110 // "self::static" which accesses the class name. It doens't define a new class.
111 $this->startToken = null;
112 break;
113 case T_NEW:
114 // Skip over T_CLASS after T_NEW because this is a PHP 7 anonymous class.
115 if ( !is_array( $token ) || $token[0] !== T_WHITESPACE ) {
116 $this->startToken = null;
117 }
118 break;
119 case T_NAMESPACE:
120 if ( $token === ';' || $token === '{' ) {
121 $this->namespace = $this->implodeTokens() . '\\';
122 } else {
123 $this->tokens[] = $token;
124 }
125 break;
126
127 case T_STRING:
128 if ( $this->alias !== null ) {
129 // Flow 1 - Two string literals:
130 // - T_STRING class_alias
131 // - '('
132 // - T_CONSTANT_ENCAPSED_STRING 'TargetClass'
133 // - ','
134 // - T_WHITESPACE
135 // - T_CONSTANT_ENCAPSED_STRING 'AliasName'
136 // - ')'
137 // Flow 2 - Use of ::class syntax for first parameter
138 // - T_STRING class_alias
139 // - '('
140 // - T_STRING TargetClass
141 // - T_DOUBLE_COLON ::
142 // - T_CLASS class
143 // - ','
144 // - T_WHITESPACE
145 // - T_CONSTANT_ENCAPSED_STRING 'AliasName'
146 // - ')'
147 if ( $token === '(' ) {
148 // Start of a function call to class_alias()
149 $this->alias = [ 'target' => false, 'name' => false ];
150 } elseif ( $token === ',' ) {
151 // Record that we're past the first parameter
152 if ( $this->alias['target'] === false ) {
153 $this->alias['target'] = true;
154 }
155 } elseif ( is_array( $token ) && $token[0] === T_CONSTANT_ENCAPSED_STRING ) {
156 if ( $this->alias['target'] === true ) {
157 // We already saw a first argument, this must be the second.
158 // Strip quotes from the string literal.
159 $this->alias['name'] = substr( $token[1], 1, -1 );
160 }
161 } elseif ( $token === ')' ) {
162 // End of function call
163 $this->classes[] = $this->alias['name'];
164 $this->alias = null;
165 $this->startToken = null;
166 } elseif ( !is_array( $token ) || (
167 $token[0] !== T_STRING &&
168 $token[0] !== T_DOUBLE_COLON &&
169 $token[0] !== T_CLASS &&
170 $token[0] !== T_WHITESPACE
171 ) ) {
172 // Ignore this call to class_alias() - compat/Timestamp.php
173 $this->alias = null;
174 $this->startToken = null;
175 }
176 }
177 break;
178
179 case T_CLASS:
180 case T_INTERFACE:
181 case T_TRAIT:
182 $this->tokens[] = $token;
183 if ( is_array( $token ) && $token[0] === T_STRING ) {
184 $this->classes[] = $this->namespace . $this->implodeTokens();
185 }
186 }
187 }
188
189 /**
190 * Returns the string representation of the tokens within the
191 * current expect sequence and resets the sequence.
192 *
193 * @return string
194 */
195 protected function implodeTokens() {
196 $content = [];
197 foreach ( $this->tokens as $token ) {
198 $content[] = is_string( $token ) ? $token : $token[1];
199 }
200
201 $this->tokens = [];
202 $this->startToken = null;
203
204 return trim( implode( '', $content ), " \n\t" );
205 }
206 }