From 0c1f4d59b8d51afcf264406685a533749e007759 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 17 Oct 2024 00:55:42 +0200 Subject: [PATCH] Tokenizer/PHP: fix handling of "DNF look a likes" with switch/case and alternative control structure syntax Follow up on 461, 507, 508 This commit intends to further improve the determination of whether tokens are part of a (return) type declaration which need special handling for the tokens or not. It does more strenuous checking on any `T_COLON` encountered before the (potential) type, to try and determine with higher accuracy whether the colon indicates the start of a return type - in contrast to being a colon for a switch-case or a colon for an alternative syntax control structure. Includes tests. The tests also include a test with `goto`. This would currently not trigger this code as the colon is tokenized as part of the goto label, but as the intention is to change that tokenization in PHPCS 4.0 (per issue 185), the test being there should safeguard that the PHPCS 4.0 change will not cause any problems with the DNF related tokenization. For now, this fixes the bug(s) reported in issue 630, though I think it might be good to revisit the tokenizer control flow in a future major, but as that will be so much easier once JS/CSS no longer needs to be taken into account, that's for the future. Fixes 630 --- src/Tokenizers/PHP.php | 34 ++++++++++++++++++++--- tests/Core/Tokenizer/PHP/DNFTypesTest.inc | 31 +++++++++++++++++++++ tests/Core/Tokenizer/PHP/DNFTypesTest.php | 31 +++++++++++++++++++++ 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index b578279546..3ebc347b73 100644 --- a/src/Tokenizers/PHP.php +++ b/src/Tokenizers/PHP.php @@ -3226,19 +3226,45 @@ protected function processAdditional() } if ($suspectedType === 'return' && $this->tokens[$x]['code'] === T_COLON) { - // Make sure this is not the colon from a parameter name. + // Make sure this is the colon for a return type. for ($y = ($x - 1); $y > 0; $y--) { if (isset(Tokens::$emptyTokens[$this->tokens[$y]['code']]) === false) { break; } } - if ($this->tokens[$y]['code'] !== T_PARAM_NAME) { - $confirmed = true; + if ($this->tokens[$y]['code'] !== T_CLOSE_PARENTHESIS) { + // Definitely not a union, intersection or DNF return type, move on. + continue 2; + } + + if (isset($this->tokens[$y]['parenthesis_owner']) === true) { + if ($this->tokens[$this->tokens[$y]['parenthesis_owner']]['code'] === T_FUNCTION + || $this->tokens[$this->tokens[$y]['parenthesis_owner']]['code'] === T_CLOSURE + || $this->tokens[$this->tokens[$y]['parenthesis_owner']]['code'] === T_FN + ) { + $confirmed = true; + } + + break; + } + + // Arrow functions may not have the parenthesis_owner set correctly yet. + // Closure use tokens won't be parentheses owners until PHPCS 4.0. + if (isset($this->tokens[$y]['parenthesis_opener']) === true) { + for ($z = ($this->tokens[$y]['parenthesis_opener'] - 1); $z > 0; $z--) { + if (isset(Tokens::$emptyTokens[$this->tokens[$z]['code']]) === false) { + break; + } + } + + if ($this->tokens[$z]['code'] === T_FN || $this->tokens[$z]['code'] === T_USE) { + $confirmed = true; + } } break; - } + }//end if if ($suspectedType === 'constant' && $this->tokens[$x]['code'] === T_CONST) { $confirmed = true; diff --git a/tests/Core/Tokenizer/PHP/DNFTypesTest.inc b/tests/Core/Tokenizer/PHP/DNFTypesTest.inc index f509c4f31d..5043ab7db5 100644 --- a/tests/Core/Tokenizer/PHP/DNFTypesTest.inc +++ b/tests/Core/Tokenizer/PHP/DNFTypesTest.inc @@ -53,6 +53,35 @@ callMe(label: CONST_A | CONST_B); /* testParensNoOwnerFunctionCallWithDNFLookALikeNamedParamIntersect */ callMe(label: CONST_A & CONST_B); +/* testSwitchControlStructureCondition */ +switch (CONST_A | CONST_B) { + /* testFunctionCallInSwitchCaseCondition */ + case get_bool(): + /* testFunctionCallInSwitchCaseBody */ + \Name\functionInSwitch(); + break; + + default: + /* testFunctionCallInSwitchDefaultBody */ + functionInSwitch(); + break; +} + +/* testIfAlternativeSyntaxCondition */ +if (true): + /* testFunctionCallInIfBody */ + \B\call(); +/* testElseIfAlternativeSyntaxCondition */ +elseif (10): + /* testFunctionCallInElseIfBody */ + C\call(); +endif; + +gotolabel: + /* testFunctionCallInGotoBody */ + \doSomething(); + + /* * DNF parentheses. */ @@ -165,6 +194,8 @@ $closureWithParamType = function ( /* testDNFTypeClosureParamIllegalNullable */ /* testParensOwnerClosureAmpersandInDefaultValue */ $closureWithReturnType = function ($string = NONSENSE & FAKE) /* testDNFTypeClosureReturn */ : (\Package\MyA&PackageB)|null {}; +$closureWithUseAndReturnType = function ($foo) use ($a) /* testDNFTypeClosureWithUseReturn */ : null|(Foo&\Bar)|false {}; + /* testParensOwnerArrowDNFUsedWithin */ $arrowWithParamType = fn ( /* testDNFTypeArrowParam */ diff --git a/tests/Core/Tokenizer/PHP/DNFTypesTest.php b/tests/Core/Tokenizer/PHP/DNFTypesTest.php index 9d7e395b3e..e1a9aaddf8 100644 --- a/tests/Core/Tokenizer/PHP/DNFTypesTest.php +++ b/tests/Core/Tokenizer/PHP/DNFTypesTest.php @@ -193,6 +193,33 @@ public static function dataNormalParentheses() 'parens without owner in arrow function return expression' => [ 'testMarker' => '/* testParensNoOwnerInArrowReturnExpression */', ], + 'parens with owner: switch condition' => [ + 'testMarker' => '/* testSwitchControlStructureCondition */', + ], + 'parens without owner in switch-case condition' => [ + 'testMarker' => '/* testFunctionCallInSwitchCaseCondition */', + ], + 'parens without owner in switch-case body' => [ + 'testMarker' => '/* testFunctionCallInSwitchCaseBody */', + ], + 'parens without owner in switch-default body' => [ + 'testMarker' => '/* testFunctionCallInSwitchDefaultBody */', + ], + 'parens with owner: if condition, alternative syntax' => [ + 'testMarker' => '/* testIfAlternativeSyntaxCondition */', + ], + 'parens without owner in if body, alternative syntax' => [ + 'testMarker' => '/* testFunctionCallInIfBody */', + ], + 'parens with owner: elseif condition, alternative syntax' => [ + 'testMarker' => '/* testElseIfAlternativeSyntaxCondition */', + ], + 'parens without owner in elseif body, alternative syntax' => [ + 'testMarker' => '/* testFunctionCallInElseIfBody */', + ], + 'parens without owner in goto body' => [ + 'testMarker' => '/* testFunctionCallInGotoBody */', + ], ]; }//end dataNormalParentheses() @@ -423,6 +450,10 @@ public static function dataDNFTypeParentheses() 'closure return type' => [ 'testMarker' => '/* testDNFTypeClosureReturn */', ], + 'closure with use return type' => [ + 'testMarker' => '/* testDNFTypeClosureWithUseReturn */', + ], + 'arrow function param type' => [ 'testMarker' => '/* testDNFTypeArrowParam */', ],