@@ -251,6 +251,13 @@ class PHP extends Tokenizer
251
251
T_SWITCH => T_SWITCH ,
252
252
],
253
253
],
254
+ T_MATCH => [
255
+ 'start ' => [T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET ],
256
+ 'end ' => [T_CLOSE_CURLY_BRACKET => T_CLOSE_CURLY_BRACKET ],
257
+ 'strict ' => true ,
258
+ 'shared ' => false ,
259
+ 'with ' => [],
260
+ ],
254
261
T_START_HEREDOC => [
255
262
'start ' => [T_START_HEREDOC => T_START_HEREDOC ],
256
263
'end ' => [T_END_HEREDOC => T_END_HEREDOC ],
@@ -365,6 +372,9 @@ class PHP extends Tokenizer
365
372
T_LOGICAL_AND => 3 ,
366
373
T_LOGICAL_OR => 2 ,
367
374
T_LOGICAL_XOR => 3 ,
375
+ T_MATCH => 5 ,
376
+ T_MATCH_ARROW => 2 ,
377
+ T_MATCH_DEFAULT => 7 ,
368
378
T_METHOD_C => 10 ,
369
379
T_MINUS_EQUAL => 2 ,
370
380
T_POW_EQUAL => 3 ,
@@ -1254,6 +1264,138 @@ protected function tokenize($string)
1254
1264
continue ;
1255
1265
}//end if
1256
1266
1267
+ /*
1268
+ Backfill the T_MATCH token for PHP versions < 8.0 and
1269
+ do initial correction for non-match expression T_MATCH tokens
1270
+ to T_STRING for PHP >= 8.0.
1271
+ A final check for non-match expression T_MATCH tokens is done
1272
+ in PHP::processAdditional().
1273
+ */
1274
+
1275
+ if ($ tokenIsArray === true
1276
+ && (($ token [0 ] === T_STRING
1277
+ && strtolower ($ token [1 ]) === 'match ' )
1278
+ || $ token [0 ] === T_MATCH )
1279
+ ) {
1280
+ $ isMatch = false ;
1281
+ for ($ x = ($ stackPtr + 1 ); $ x < $ numTokens ; $ x ++) {
1282
+ if (isset ($ tokens [$ x ][0 ], Util \Tokens::$ emptyTokens [$ tokens [$ x ][0 ]]) === true ) {
1283
+ continue ;
1284
+ }
1285
+
1286
+ if ($ tokens [$ x ] !== '( ' ) {
1287
+ // This is not a match expression.
1288
+ break ;
1289
+ }
1290
+
1291
+ // Next was an open parenthesis, now check what is before the match keyword.
1292
+ for ($ y = ($ stackPtr - 1 ); $ y >= 0 ; $ y --) {
1293
+ if (isset (Util \Tokens::$ emptyTokens [$ tokens [$ y ][0 ]]) === true ) {
1294
+ continue ;
1295
+ }
1296
+
1297
+ if (is_array ($ tokens [$ y ]) === true
1298
+ && ($ tokens [$ y ][0 ] === T_PAAMAYIM_NEKUDOTAYIM
1299
+ || $ tokens [$ y ][0 ] === T_OBJECT_OPERATOR
1300
+ || $ tokens [$ y ][0 ] === T_NS_SEPARATOR
1301
+ || $ tokens [$ y ][0 ] === T_NEW
1302
+ || $ tokens [$ y ][0 ] === T_FUNCTION
1303
+ || $ tokens [$ y ][0 ] === T_CLASS
1304
+ || $ tokens [$ y ][0 ] === T_INTERFACE
1305
+ || $ tokens [$ y ][0 ] === T_TRAIT
1306
+ || $ tokens [$ y ][0 ] === T_NAMESPACE
1307
+ || $ tokens [$ y ][0 ] === T_CONST )
1308
+ ) {
1309
+ // This is not a match expression.
1310
+ break 2 ;
1311
+ }
1312
+
1313
+ $ isMatch = true ;
1314
+ break 2 ;
1315
+ }//end for
1316
+ }//end for
1317
+
1318
+ if ($ isMatch === true && $ token [0 ] === T_STRING ) {
1319
+ $ newToken = [];
1320
+ $ newToken ['code ' ] = T_MATCH ;
1321
+ $ newToken ['type ' ] = 'T_MATCH ' ;
1322
+ $ newToken ['content ' ] = $ token [1 ];
1323
+
1324
+ if (PHP_CODESNIFFER_VERBOSITY > 1 ) {
1325
+ echo "\t\t* token $ stackPtr changed from T_STRING to T_MATCH " .PHP_EOL ;
1326
+ }
1327
+
1328
+ $ finalTokens [$ newStackPtr ] = $ newToken ;
1329
+ $ newStackPtr ++;
1330
+ continue ;
1331
+ } else if ($ isMatch === false && $ token [0 ] === T_MATCH ) {
1332
+ // PHP 8.0, match keyword, but not a match expression.
1333
+ $ newToken = [];
1334
+ $ newToken ['code ' ] = T_STRING ;
1335
+ $ newToken ['type ' ] = 'T_STRING ' ;
1336
+ $ newToken ['content ' ] = $ token [1 ];
1337
+
1338
+ if (PHP_CODESNIFFER_VERBOSITY > 1 ) {
1339
+ echo "\t\t* token $ stackPtr changed from T_MATCH to T_STRING " .PHP_EOL ;
1340
+ }
1341
+
1342
+ $ finalTokens [$ newStackPtr ] = $ newToken ;
1343
+ $ newStackPtr ++;
1344
+ continue ;
1345
+ }//end if
1346
+ }//end if
1347
+
1348
+ /*
1349
+ Retokenize the T_DEFAULT in match control structures as T_MATCH_DEFAULT
1350
+ to prevent scope being set and the scope for switch default statements
1351
+ breaking.
1352
+ */
1353
+
1354
+ if ($ tokenIsArray === true
1355
+ && $ token [0 ] === T_DEFAULT
1356
+ ) {
1357
+ for ($ x = ($ stackPtr + 1 ); $ x < $ numTokens ; $ x ++) {
1358
+ if ($ tokens [$ x ] === ', ' ) {
1359
+ // Skip over potential trailing comma (supported in PHP).
1360
+ continue ;
1361
+ }
1362
+
1363
+ if (is_array ($ tokens [$ x ]) === false
1364
+ || isset (Util \Tokens::$ emptyTokens [$ tokens [$ x ][0 ]]) === false
1365
+ ) {
1366
+ // Non-empty, non-comma content.
1367
+ break ;
1368
+ }
1369
+ }
1370
+
1371
+ if (isset ($ tokens [$ x ]) === true
1372
+ && is_array ($ tokens [$ x ]) === true
1373
+ && $ tokens [$ x ][0 ] === T_DOUBLE_ARROW
1374
+ ) {
1375
+ // Modify the original token stack for the double arrow so that
1376
+ // future checks can disregard the double arrow token more easily.
1377
+ // For match expression "case" statements, this is handled
1378
+ // in PHP::processAdditional().
1379
+ $ tokens [$ x ][0 ] = T_MATCH_ARROW ;
1380
+ if (PHP_CODESNIFFER_VERBOSITY > 1 ) {
1381
+ echo "\t\t* token $ x changed from T_DOUBLE_ARROW to T_MATCH_ARROW " .PHP_EOL ;
1382
+ }
1383
+
1384
+ $ newToken = [];
1385
+ $ newToken ['code ' ] = T_MATCH_DEFAULT ;
1386
+ $ newToken ['type ' ] = 'T_MATCH_DEFAULT ' ;
1387
+ $ newToken ['content ' ] = $ token [1 ];
1388
+
1389
+ if (PHP_CODESNIFFER_VERBOSITY > 1 ) {
1390
+ echo "\t\t* token $ stackPtr changed from T_DEFAULT to T_MATCH_DEFAULT " .PHP_EOL ;
1391
+ }
1392
+
1393
+ $ finalTokens [$ newStackPtr ] = $ newToken ;
1394
+ $ newStackPtr ++;
1395
+ continue ;
1396
+ }//end if
1397
+ }//end if
1398
+
1257
1399
/*
1258
1400
Convert ? to T_NULLABLE OR T_INLINE_THEN
1259
1401
*/
@@ -2110,6 +2252,31 @@ protected function processAdditional()
2110
2252
$ lastEndToken = null ;
2111
2253
2112
2254
for ($ scopeCloser = ($ arrow + 1 ); $ scopeCloser < $ numTokens ; $ scopeCloser ++) {
2255
+ // Arrow function closer should never be shared with the closer of a match
2256
+ // control structure.
2257
+ if (isset ($ this ->tokens [$ scopeCloser ]['scope_closer ' ], $ this ->tokens [$ scopeCloser ]['scope_condition ' ]) === true
2258
+ && $ scopeCloser === $ this ->tokens [$ scopeCloser ]['scope_closer ' ]
2259
+ && $ this ->tokens [$ this ->tokens [$ scopeCloser ]['scope_condition ' ]]['code ' ] === T_MATCH
2260
+ ) {
2261
+ if ($ arrow < $ this ->tokens [$ scopeCloser ]['scope_condition ' ]) {
2262
+ // Match in return value of arrow function. Move on to the next token.
2263
+ continue ;
2264
+ }
2265
+
2266
+ // Arrow function as return value for the last match case without trailing comma.
2267
+ if ($ lastEndToken !== null ) {
2268
+ $ scopeCloser = $ lastEndToken ;
2269
+ break ;
2270
+ }
2271
+
2272
+ for ($ lastNonEmpty = ($ scopeCloser - 1 ); $ lastNonEmpty > $ arrow ; $ lastNonEmpty --) {
2273
+ if (isset (Util \Tokens::$ emptyTokens [$ this ->tokens [$ lastNonEmpty ]['code ' ]]) === false ) {
2274
+ $ scopeCloser = $ lastNonEmpty ;
2275
+ break 2 ;
2276
+ }
2277
+ }
2278
+ }
2279
+
2113
2280
if (isset ($ endTokens [$ this ->tokens [$ scopeCloser ]['code ' ]]) === true ) {
2114
2281
if ($ lastEndToken !== null
2115
2282
&& $ this ->tokens [$ scopeCloser ]['code ' ] === T_CLOSE_PARENTHESIS
@@ -2265,6 +2432,77 @@ protected function processAdditional()
2265
2432
}
2266
2433
}
2267
2434
2435
+ continue ;
2436
+ } else if ($ this ->tokens [$ i ]['code ' ] === T_MATCH ) {
2437
+ if (isset ($ this ->tokens [$ i ]['scope_opener ' ], $ this ->tokens [$ i ]['scope_closer ' ]) === false ) {
2438
+ // Not a match expression after all.
2439
+ $ this ->tokens [$ i ]['code ' ] = T_STRING ;
2440
+ $ this ->tokens [$ i ]['type ' ] = 'T_STRING ' ;
2441
+
2442
+ if (PHP_CODESNIFFER_VERBOSITY > 1 ) {
2443
+ echo "\t\t* token $ i changed from T_MATCH to T_STRING " .PHP_EOL ;
2444
+ }
2445
+
2446
+ if (isset ($ this ->tokens [$ i ]['parenthesis_opener ' ], $ this ->tokens [$ i ]['parenthesis_closer ' ]) === true ) {
2447
+ $ opener = $ this ->tokens [$ i ]['parenthesis_opener ' ];
2448
+ $ closer = $ this ->tokens [$ i ]['parenthesis_closer ' ];
2449
+ unset(
2450
+ $ this ->tokens [$ opener ]['parenthesis_owner ' ],
2451
+ $ this ->tokens [$ closer ]['parenthesis_owner ' ]
2452
+ );
2453
+ unset(
2454
+ $ this ->tokens [$ i ]['parenthesis_opener ' ],
2455
+ $ this ->tokens [$ i ]['parenthesis_closer ' ],
2456
+ $ this ->tokens [$ i ]['parenthesis_owner ' ]
2457
+ );
2458
+
2459
+ if (PHP_CODESNIFFER_VERBOSITY > 1 ) {
2460
+ echo "\t\t* cleaned parenthesis of token $ i * " .PHP_EOL ;
2461
+ }
2462
+ }
2463
+ } else {
2464
+ // Retokenize the double arrows for match expression cases to `T_MATCH_ARROW`.
2465
+ $ searchFor = [
2466
+ T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET ,
2467
+ T_OPEN_SQUARE_BRACKET => T_OPEN_SQUARE_BRACKET ,
2468
+ T_OPEN_PARENTHESIS => T_OPEN_PARENTHESIS ,
2469
+ T_OPEN_SHORT_ARRAY => T_OPEN_SHORT_ARRAY ,
2470
+ T_DOUBLE_ARROW => T_DOUBLE_ARROW ,
2471
+ ];
2472
+ $ searchFor += Util \Tokens::$ scopeOpeners ;
2473
+
2474
+ for ($ x = ($ this ->tokens [$ i ]['scope_opener ' ] + 1 ); $ x < $ this ->tokens [$ i ]['scope_closer ' ]; $ x ++) {
2475
+ if (isset ($ searchFor [$ this ->tokens [$ x ]['code ' ]]) === false ) {
2476
+ continue ;
2477
+ }
2478
+
2479
+ if (isset ($ this ->tokens [$ x ]['scope_closer ' ]) === true ) {
2480
+ $ x = $ this ->tokens [$ x ]['scope_closer ' ];
2481
+ continue ;
2482
+ }
2483
+
2484
+ if (isset ($ this ->tokens [$ x ]['parenthesis_closer ' ]) === true ) {
2485
+ $ x = $ this ->tokens [$ x ]['parenthesis_closer ' ];
2486
+ continue ;
2487
+ }
2488
+
2489
+ if (isset ($ this ->tokens [$ x ]['bracket_closer ' ]) === true ) {
2490
+ $ x = $ this ->tokens [$ x ]['bracket_closer ' ];
2491
+ continue ;
2492
+ }
2493
+
2494
+ // This must be a double arrow, but make sure anyhow.
2495
+ if ($ this ->tokens [$ x ]['code ' ] === T_DOUBLE_ARROW ) {
2496
+ $ this ->tokens [$ x ]['code ' ] = T_MATCH_ARROW ;
2497
+ $ this ->tokens [$ x ]['type ' ] = 'T_MATCH_ARROW ' ;
2498
+
2499
+ if (PHP_CODESNIFFER_VERBOSITY > 1 ) {
2500
+ echo "\t\t* token $ x changed from T_DOUBLE_ARROW to T_MATCH_ARROW " .PHP_EOL ;
2501
+ }
2502
+ }
2503
+ }//end for
2504
+ }//end if
2505
+
2268
2506
continue ;
2269
2507
} else if ($ this ->tokens [$ i ]['code ' ] === T_BITWISE_OR ) {
2270
2508
/*
0 commit comments