From f5f91ee80321f232c0b7a6b6053170f4cc43e502 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 10 Oct 2022 05:18:24 +0200 Subject: [PATCH 1/2] PHP 8.0 | Arrays::getDoubleArrowPtr(): allow for match expressions in array values PHP 8.0 introduced match expressions, which can contain arrays in both the match "case" as well as the match value. Skipping over match expressions will prevent incorrect results for the `Arrays::getDoubleArrowPtr()` method (in particular when a match expression is used as an array key). Includes unit tests. Refs: * https://wiki.php.net/rfc/match_expression_v2 --- PHPCSUtils/Utils/Arrays.php | 5 ++++- Tests/Utils/Arrays/GetDoubleArrowPtrTest.inc | 18 ++++++++++++++++++ Tests/Utils/Arrays/GetDoubleArrowPtrTest.php | 15 +++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/PHPCSUtils/Utils/Arrays.php b/PHPCSUtils/Utils/Arrays.php index fb47b48e..9ea9ed54 100644 --- a/PHPCSUtils/Utils/Arrays.php +++ b/PHPCSUtils/Utils/Arrays.php @@ -43,6 +43,7 @@ class Arrays // Inline function and control structures to skip over. \T_FN => \T_FN, + \T_MATCH => \T_MATCH, ]; /** @@ -295,6 +296,7 @@ public static function getOpenClose(File $phpcsFile, $stackPtr, $isShortArray = * * @since 1.0.0 * @since 1.0.0-alpha2 Now allows for arrow functions in arrays. + * @since 1.0.0-alpha4 Now allows for match expressions in arrays. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being examined. * @param int $start Stack pointer to the start of the array item. @@ -344,7 +346,8 @@ public static function getDoubleArrowPtr(File $phpcsFile, $start, $end) // Skip over closed scopes which may contain foreach structures or generators. if ((isset(Collections::closedScopes()[$tokens[$doubleArrow]['code']]) === true - || $tokens[$doubleArrow]['code'] === \T_FN) + || $tokens[$doubleArrow]['code'] === \T_FN + || $tokens[$doubleArrow]['code'] === \T_MATCH) && isset($tokens[$doubleArrow]['scope_closer']) === true ) { $doubleArrow = $tokens[$doubleArrow]['scope_closer']; diff --git a/Tests/Utils/Arrays/GetDoubleArrowPtrTest.inc b/Tests/Utils/Arrays/GetDoubleArrowPtrTest.inc index cee6643e..40909d7d 100644 --- a/Tests/Utils/Arrays/GetDoubleArrowPtrTest.inc +++ b/Tests/Utils/Arrays/GetDoubleArrowPtrTest.inc @@ -87,6 +87,24 @@ $array = [ /* testDoubleArrowTokenizedAsTstring-PHPCS2865 */ $obj->fn => 'value', + /* testNoArrowValueMatchExpr */ + match($a) { + FOO => BAR, + default => [0 => BAZ], + }, + + /* testArrowValueMatchExpr */ + 'key' => match($a) { + [0 => 10] => BAR, + default => BAZ, + }, + + /* testArrowKeyMatchExpr */ + match($a) { + FOO => BAR, + default => [0 => 10], + } => 'value', + /* testEmptyArrayItem */ // Intentional parse error. , diff --git a/Tests/Utils/Arrays/GetDoubleArrowPtrTest.php b/Tests/Utils/Arrays/GetDoubleArrowPtrTest.php index f8e11344..ae20eda8 100644 --- a/Tests/Utils/Arrays/GetDoubleArrowPtrTest.php +++ b/Tests/Utils/Arrays/GetDoubleArrowPtrTest.php @@ -219,6 +219,21 @@ public function dataGetDoubleArrowPtr() 'testMarker' => '/* testDoubleArrowTokenizedAsTstring-PHPCS2865 */', 'expected' => 10, ], + + // Safeguard that PHP 8.0 match expressions are handled correctly. + 'test-no-arrow-value-match-expression' => [ + 'testMarker' => '/* testNoArrowValueMatchExpr */', + 'expected' => false, + ], + 'test-double-arrow-value-match-expression' => [ + 'testMarker' => '/* testArrowValueMatchExpr */', + 'expected' => 8, + ], + 'test-double-arrow-key-match-expression' => [ + 'testMarker' => '/* testArrowKeyMatchExpr */', + 'expected' => 38, + ], + 'test-empty-array-item' => [ 'testMarker' => '/* testEmptyArrayItem */', 'expected' => false, From 390e964e4fb53d2e14b5a8c2c6278a6c40a1acaf Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 9 Oct 2022 04:55:57 +0200 Subject: [PATCH 2/2] PHP 8.0 | ControlStructures::hasBody(): add support for match expressions PHP 8.0 introduced match expressions as an new control structure. While a `match` expression without body or with an empty body is not allowed and would result in a fatal error (or be interpreted as a non-global function call), the `ControlStructures::hasBody()` method should still handle it correctly. Includes adding the `T_MATCH` token to the `Collections::controlStructureTokens()` token array. Includes unit tests. Refs: * https://wiki.php.net/rfc/match_expression_v2 --- PHPCSUtils/Tokens/Collections.php | 2 ++ PHPCSUtils/Utils/ControlStructures.php | 1 + Tests/Utils/ControlStructures/HasBodyTest.inc | 22 ++++++++++++++++++ Tests/Utils/ControlStructures/HasBodyTest.php | 23 +++++++++++++++++++ 4 files changed, 48 insertions(+) diff --git a/PHPCSUtils/Tokens/Collections.php b/PHPCSUtils/Tokens/Collections.php index f680d2d3..2045a6a2 100644 --- a/PHPCSUtils/Tokens/Collections.php +++ b/PHPCSUtils/Tokens/Collections.php @@ -202,6 +202,7 @@ class Collections * DEPRECATED: Control structure tokens. * * @since 1.0.0-alpha2 + * @since 1.0.0-alpha4 Added the T_MATCH token for PHP 8.0 match expressions. * * @deprecated 1.0.0-alpha4 Use the {@see Collections::controlStructureTokens()} method instead. * @@ -217,6 +218,7 @@ class Collections \T_DO => \T_DO, \T_WHILE => \T_WHILE, \T_DECLARE => \T_DECLARE, + \T_MATCH => \T_MATCH, ]; /** diff --git a/PHPCSUtils/Utils/ControlStructures.php b/PHPCSUtils/Utils/ControlStructures.php index f351ed83..802c6214 100644 --- a/PHPCSUtils/Utils/ControlStructures.php +++ b/PHPCSUtils/Utils/ControlStructures.php @@ -37,6 +37,7 @@ class ControlStructures * regarded as empty. * * @since 1.0.0 + * @since 1.0.0-alpha4 Added support for PHP 8.0 match control structures. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position of the token we are checking. diff --git a/Tests/Utils/ControlStructures/HasBodyTest.inc b/Tests/Utils/ControlStructures/HasBodyTest.inc index e0363bb0..d68d3b7d 100644 --- a/Tests/Utils/ControlStructures/HasBodyTest.inc +++ b/Tests/Utils/ControlStructures/HasBodyTest.inc @@ -217,6 +217,28 @@ do echo $i; while (++$i <= 10); + +/* + * PHP 8.0 match expressions. + */ + +/* testMatchEmptyBody */ +// Intentional fatal error, "unhandled match case", but not the concern of this method. +$match = match($a) {}; + +/* testMatchEmptyBodyWithComment */ +// Intentional fatal error, "unhandled match case", but not the concern of this method. +$match = match($a) { + // Deliberately empty. +}; + +/* testMatchWithCode */ +$match = match ($a) { + 0 => 'Foo', + 1 => 'Bar', + 2 => 'Baz', +}; + // Live coding. // Intentional parse error. This test has to be the last in the file. if ($a) { diff --git a/Tests/Utils/ControlStructures/HasBodyTest.php b/Tests/Utils/ControlStructures/HasBodyTest.php index 89cdaeb8..c78dfc39 100644 --- a/Tests/Utils/ControlStructures/HasBodyTest.php +++ b/Tests/Utils/ControlStructures/HasBodyTest.php @@ -311,6 +311,29 @@ public function dataHasBody() 'hasBody' => true, 'hasNonEmptyBody' => true, ], + + /* + * Match without body cannot be tested as, in that case, `match` will tokenize as `T_STRING`. + * Without body (`match();`), match will either yield a parse error + * or be interpreted as a function call (`\match();` or `self::match()` etc). + */ + + 'match-empty-body' => [ + 'testMarker' => '/* testMatchEmptyBody */', + 'hasBody' => true, + 'hasNonEmptyBody' => false, + ], + 'match-empty-body-comment-only' => [ + 'testMarker' => '/* testMatchEmptyBodyWithComment */', + 'hasBody' => true, + 'hasNonEmptyBody' => false, + ], + 'match-with-code' => [ + 'testMarker' => '/* testMatchWithCode */', + 'hasBody' => true, + 'hasNonEmptyBody' => true, + ], + 'else-live-coding' => [ 'testMarker' => '/* testElseLiveCoding */', 'hasBody' => true,