@@ -251,6 +251,13 @@ class PHP extends Tokenizer
251251 T_SWITCH => T_SWITCH ,
252252 ],
253253 ],
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+ ],
254261 T_START_HEREDOC => [
255262 'start ' => [T_START_HEREDOC => T_START_HEREDOC ],
256263 'end ' => [T_END_HEREDOC => T_END_HEREDOC ],
@@ -365,6 +372,9 @@ class PHP extends Tokenizer
365372 T_LOGICAL_AND => 3 ,
366373 T_LOGICAL_OR => 2 ,
367374 T_LOGICAL_XOR => 3 ,
375+ T_MATCH => 5 ,
376+ T_MATCH_ARROW => 2 ,
377+ T_MATCH_DEFAULT => 7 ,
368378 T_METHOD_C => 10 ,
369379 T_MINUS_EQUAL => 2 ,
370380 T_POW_EQUAL => 3 ,
@@ -1254,6 +1264,138 @@ protected function tokenize($string)
12541264 continue ;
12551265 }//end if
12561266
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+
12571399 /*
12581400 Convert ? to T_NULLABLE OR T_INLINE_THEN
12591401 */
@@ -2110,6 +2252,31 @@ protected function processAdditional()
21102252 $ lastEndToken = null ;
21112253
21122254 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+
21132280 if (isset ($ endTokens [$ this ->tokens [$ scopeCloser ]['code ' ]]) === true ) {
21142281 if ($ lastEndToken !== null
21152282 && $ this ->tokens [$ scopeCloser ]['code ' ] === T_CLOSE_PARENTHESIS
@@ -2265,6 +2432,77 @@ protected function processAdditional()
22652432 }
22662433 }
22672434
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+
22682506 continue ;
22692507 } else if ($ this ->tokens [$ i ]['code ' ] === T_BITWISE_OR ) {
22702508 /*
0 commit comments