PluginProbe
Code Snippets / 4.0.0-beta.2
Code Snippets v4.0.0-beta.2
4.0.0-beta.2 3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 All 65 releases
← All changes | php/Utils/Validator.php +74 -10 3.10.24.0.0-beta.2 View file →
@@ -57,15 +57,22 @@
57 57 *
58 58 * A snippet is validated against everything PHP has declared so far, which
59 59 * does not include a snippet that is about to be activated in the same
60 60 * batch. Two snippets declaring the same function therefore both passed and
61 - * both activated, and the site fataled on the next request.
61 + * both activated, and the site crashed on the next request.
62 62 *
63 63 * @var array<string, string[]>
64 64 */
65 - private array $claimed_identifiers = [];
65 + private array $claimed_identifiers;
66 66
67 67 /**
68 + * Namespace the code being read currently declares, lower-cased, or empty for the global namespace.
69 + *
70 + * @var string
71 + */
72 + private string $namespace = '';
73 +
74 + /**
68 75 * Class constructor.
69 76 *
70 77 * @param string $code Snippet code for parsing.
71 78 * @param array<string, string[]> $claimed_identifiers Identifiers already claimed by
@@ -129,17 +136,20 @@
129 136 * @return bool true if the identifier is not unique.
130 137 */
131 138 private function check_duplicate_identifier( string $type, string $identifier ): bool {
132 139 $identifier = strtolower( ltrim( $identifier, '\\' ) );
140 +
141 + // PHP keeps declared names fully qualified, so that is the form compared
142 + // and claimed: the same short name in two namespaces is two names.
143 + $qualified = '' === $this->namespace ? $identifier : $this->namespace . '\\' . $identifier;
133 144 $namespaced_identifier = 'code_snippets\\' . $identifier;
134 145
135 146 if ( ! isset( $this->defined_identifiers[ $type ] ) ) {
136 147 switch ( $type ) {
137 148 case T_FUNCTION:
138 - $defined_functions = get_defined_functions();
139 149 $this->defined_identifiers[ T_FUNCTION ] = array_map(
140 150 'strtolower',
141 - array_merge( $defined_functions['internal'], $defined_functions['user'] )
151 + array_merge( get_defined_functions()['internal'], get_defined_functions()['user'] )
142 152 );
143 153 break;
144 154
145 155 case T_CLASS:
@@ -159,21 +169,70 @@
159 169 $this->defined_identifiers[ $type ],
160 170 $this->claimed_identifiers[ $type ] ?? []
161 171 );
162 172
163 - $duplicate_identifier = in_array( $identifier, $known, true );
164 - $duplicate_namespaced = in_array( $namespaced_identifier, $known, true );
173 + $duplicate_identifier = in_array( $qualified, $known, true );
174 + $duplicate_namespaced = '' === $this->namespace && in_array( $namespaced_identifier, $known, true );
165 175 $exceptions = $this->exceptions[ $type ] ?? [];
166 - $exception_identifier = in_array( $identifier, $exceptions, true );
167 - $exception_namespaced = in_array( $namespaced_identifier, $exceptions, true );
176 + $exception_identifier = in_array( $identifier, $exceptions, true ) || in_array( $qualified, $exceptions, true );
177 + $exception_namespaced = in_array( $identifier, $exceptions, true ) || in_array( $namespaced_identifier, $exceptions, true );
168 178
169 - array_unshift( $this->defined_identifiers[ $type ], $identifier );
170 - $this->claimed_identifiers[ $type ][] = $identifier;
179 + array_unshift( $this->defined_identifiers[ $type ], $qualified );
180 + $this->claimed_identifiers[ $type ][] = $qualified;
171 181
172 182 return ( $duplicate_identifier && ! $exception_identifier ) || ( $duplicate_namespaced && ! $exception_namespaced );
173 183 }
174 184
175 185 /**
186 + * Read the name a namespace declaration introduces, leaving the cursor after it.
187 + *
188 + * A bare "namespace {" opens the global namespace; "namespace\\foo()" is a
189 + * relative name rather than a declaration and is left alone.
190 + *
191 + * @return string Lower-cased namespace, or empty for the global namespace.
192 + */
193 + private function read_namespace_declaration(): string {
194 + $name = '';
195 +
196 + while ( ! $this->end() ) {
197 + $token = $this->peek();
198 +
199 + if ( is_array( $token ) ) {
200 + if ( T_WHITESPACE === $token[0] || T_COMMENT === $token[0] || T_DOC_COMMENT === $token[0] ) {
201 + $this->next();
202 + continue;
203 + }
204 +
205 + if ( T_NS_SEPARATOR === $token[0] && '' === $name ) {
206 + return $this->namespace;
207 + }
208 +
209 + if ( defined( 'T_NAME_RELATIVE' ) && T_NAME_RELATIVE === $token[0] ) {
210 + return $this->namespace;
211 + }
212 +
213 + if ( T_STRING === $token[0] || T_NS_SEPARATOR === $token[0]
214 + || ( defined( 'T_NAME_QUALIFIED' ) && T_NAME_QUALIFIED === $token[0] ) ) {
215 + $name .= $token[1];
216 + $this->next();
217 + continue;
218 + }
219 +
220 + return $this->namespace;
221 + }
222 +
223 + if ( ';' === $token || '{' === $token ) {
224 + $this->next();
225 + return strtolower( trim( $name, '\\' ) );
226 + }
227 +
228 + return $this->namespace;
229 + }
230 +
231 + return strtolower( trim( $name, '\\' ) );
232 + }
233 +
234 + /**
176 235 * Validate the given PHP code and return the result.
177 236 *
178 237 * @return array<string, mixed>|false Array containing message if an error was encountered, false if validation was successful.
179 238 */
@@ -183,8 +242,13 @@
183 242 $token = $this->peek();
184 243 $this->next();
185 244
186 245 if ( ! is_array( $token ) ) {
246 + continue;
247 + }
248 +
249 + if ( T_NAMESPACE === $token[0] ) {
250 + $this->namespace = $this->read_namespace_declaration();
187 251 continue;
188 252 }
189 253
190 254 // If this is a function or class exists check, then allow this function or class to be defined.