diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index 3fc67b0c81..11802c0d34 100644 --- a/src/Tokenizers/PHP.php +++ b/src/Tokenizers/PHP.php @@ -681,6 +681,36 @@ protected function tokenize($string) } }//end if + /* + Special case for `static` used as a function name, i.e. `static()`. + */ + + if ($tokenIsArray === true + && $token[0] === T_STATIC + && $finalTokens[$lastNotEmptyToken]['code'] !== T_NEW + ) { + for ($i = ($stackPtr + 1); $i < $numTokens; $i++) { + if (is_array($tokens[$i]) === true + && isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === true + ) { + continue; + } + + if ($tokens[$i][0] === '(') { + $finalTokens[$newStackPtr] = [ + 'code' => T_STRING, + 'type' => 'T_STRING', + 'content' => $token[1], + ]; + + $newStackPtr++; + continue 2; + } + + break; + } + }//end if + /* Parse doc blocks into something that can be easily iterated over. */ @@ -2103,38 +2133,60 @@ function return types. We want to keep the parenthesis map clean, } } else { // Some T_STRING tokens should remain that way due to their context. - if ($tokenIsArray === true - && $token[0] === T_STRING - && isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true - ) { - // Special case for syntax like: return new self/new parent - // where self/parent should not be a string. - $tokenContentLower = strtolower($token[1]); - if ($finalTokens[$lastNotEmptyToken]['code'] === T_NEW - && ($tokenContentLower === 'self' || $tokenContentLower === 'parent') - ) { - $finalTokens[$newStackPtr] = [ - 'content' => $token[1], - ]; - if ($tokenContentLower === 'self') { - $finalTokens[$newStackPtr]['code'] = T_SELF; - $finalTokens[$newStackPtr]['type'] = 'T_SELF'; + if ($tokenIsArray === true && $token[0] === T_STRING) { + $preserveTstring = false; + + if (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true) { + $preserveTstring = true; + + // Special case for syntax like: return new self/new parent + // where self/parent should not be a string. + $tokenContentLower = strtolower($token[1]); + if ($finalTokens[$lastNotEmptyToken]['code'] === T_NEW + && ($tokenContentLower === 'self' || $tokenContentLower === 'parent') + ) { + $preserveTstring = false; } + } else if ($finalTokens[$lastNotEmptyToken]['content'] === '&') { + // Function names for functions declared to return by reference. + for ($i = ($lastNotEmptyToken - 1); $i >= 0; $i--) { + if (isset(Util\Tokens::$emptyTokens[$finalTokens[$i]['code']]) === true) { + continue; + } + + if ($finalTokens[$i]['code'] === T_FUNCTION) { + $preserveTstring = true; + } - if ($tokenContentLower === 'parent') { - $finalTokens[$newStackPtr]['code'] = T_PARENT; - $finalTokens[$newStackPtr]['type'] = 'T_PARENT'; + break; } } else { + // Keywords with special PHPCS token when used as a function call. + for ($i = ($stackPtr + 1); $i < $numTokens; $i++) { + if (is_array($tokens[$i]) === true + && isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === true + ) { + continue; + } + + if ($tokens[$i][0] === '(') { + $preserveTstring = true; + } + + break; + } + }//end if + + if ($preserveTstring === true) { $finalTokens[$newStackPtr] = [ - 'content' => $token[1], 'code' => T_STRING, 'type' => 'T_STRING', + 'content' => $token[1], ]; - } - $newStackPtr++; - continue; + $newStackPtr++; + continue; + } }//end if $newToken = null; diff --git a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc index 82fe564382..bc98c49f3d 100644 --- a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc +++ b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc @@ -1,227 +1,264 @@ - 'a', - 2 => 'b', - /* testMatchDefaultIsKeyword */ default => 'default', -}; - -$closure = /* testFnIsKeyword */ fn () => 'string'; - -function () { - /* testYieldIsKeyword */ yield $f; - /* testYieldFromIsKeyword */ yield from someFunction(); -}; - -/* testDeclareIsKeyword */ declare(ticks=1): -/* testEndDeclareIsKeyword */ enddeclare; - -if (true /* testAndIsKeyword */ and false /* testOrIsKeyword */ or null /* testXorIsKeyword */ xor 0) { - -} - -$anonymousClass = new /* testAnonymousClassIsKeyword */ class {}; -$anonymousClass2 = new class /* testExtendsInAnonymousClassIsKeyword */ extends SomeParent {}; -$anonymousClass3 = new class /* testImplementsInAnonymousClassIsKeyword */ implements SomeInterface {}; - -$instantiated1 = new /* testClassInstantiationParentIsKeyword */ parent(); -$instantiated2 = new /* testClassInstantiationSelfIsKeyword */ SELF; -$instantiated3 = new /* testClassInstantiationStaticIsKeyword */ static($param); - -class Foo extends /* testNamespaceInNameIsKeyword */ namespace\Exception -{} - -function /* testKeywordAfterFunctionShouldBeString */ eval() {} -function /* testKeywordAfterFunctionByRefShouldBeString */ &switch() {} + 'a', + 2 => 'b', + /* testMatchDefaultIsKeyword */ default => 'default', +}; + +$closure = /* testFnIsKeyword */ fn () => 'string'; + +function () { + /* testYieldIsKeyword */ yield $f; + /* testYieldFromIsKeyword */ yield from someFunction(); +}; + +/* testDeclareIsKeyword */ declare(ticks=1): +/* testEndDeclareIsKeyword */ enddeclare; + +if (true /* testAndIsKeyword */ and false /* testOrIsKeyword */ or null /* testXorIsKeyword */ xor 0) { + +} + +$anonymousClass = new /* testAnonymousClassIsKeyword */ class {}; +$anonymousClass2 = new class /* testExtendsInAnonymousClassIsKeyword */ extends SomeParent {}; +$anonymousClass3 = new class /* testImplementsInAnonymousClassIsKeyword */ implements SomeInterface {}; + +$instantiated1 = new /* testClassInstantiationParentIsKeyword */ parent(); +$instantiated2 = new /* testClassInstantiationSelfIsKeyword */ SELF; +$instantiated3 = new /* testClassInstantiationStaticIsKeyword */ static($param); + +class Foo extends /* testNamespaceInNameIsKeyword */ namespace\Exception +{} + +function /* testKeywordAfterFunctionShouldBeString */ eval() {} +function /* testKeywordAfterFunctionByRefShouldBeString */ &switch() {} + +function /* testKeywordSelfAfterFunctionByRefShouldBeString */ &self() {} +function /* testKeywordStaticAfterFunctionByRefShouldBeString */ &static() {} +function /* testKeywordParentAfterFunctionByRefShouldBeString */ &parent() {} +function /* testKeywordFalseAfterFunctionByRefShouldBeString */ &false() {} +function /* testKeywordTrueAfterFunctionByRefShouldBeString */ & true () {} +function /* testKeywordNullAfterFunctionByRefShouldBeString */ &NULL() {} + +/* testKeywordAsFunctionCallNameShouldBeStringSelf */ self(); +/* testKeywordAsFunctionCallNameShouldBeStringStatic */ static(); +$obj-> /* testKeywordAsMethodCallNameShouldBeStringStatic */ static(); +/* testKeywordAsFunctionCallNameShouldBeStringParent */ parent(); +/* testKeywordAsFunctionCallNameShouldBeStringFalse */ false(); +/* testKeywordAsFunctionCallNameShouldBeStringTrue */ True (); +/* testKeywordAsFunctionCallNameShouldBeStringNull */ null /*comment*/ (); + +$instantiated4 = new /* testClassInstantiationFalseIsString */ False(); +$instantiated5 = new /* testClassInstantiationTrueIsString */ true (); +$instantiated6 = new /* testClassInstantiationNullIsString */ null(); + +$function = /* testStaticIsKeywordBeforeClosure */ static function(/* testStaticIsKeywordWhenParamType */ static $param) {}; +$arrow = /* testStaticIsKeywordBeforeArrow */ static fn(): /* testStaticIsKeywordWhenReturnType */ static => 10; + +function standAloneFalseTrueNullTypesAndMore( + /* testFalseIsKeywordAsParamType */ false $paramA, + /* testTrueIsKeywordAsParamType */ true $paramB, + /* testNullIsKeywordAsParamType */ null $paramC, +) /* testFalseIsKeywordAsReturnType */ false | /* testTrueIsKeywordAsReturnType */ true | /* testNullIsKeywordAsReturnType */ null { + if ($a === /* testFalseIsKeywordInComparison */ false + || $a === /* testTrueIsKeywordInComparison */ true + || $a === /* testNullIsKeywordInComparison */ null + ) {} +} diff --git a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php index a747e573c2..3f077ca639 100644 --- a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php +++ b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php @@ -30,7 +30,7 @@ public function testStrings($testMarker) { $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken($testMarker, (Tokens::$contextSensitiveKeywords + [T_STRING])); + $token = $this->getTargetToken($testMarker, (Tokens::$contextSensitiveKeywords + [T_STRING, T_NULL, T_FALSE, T_TRUE, T_PARENT, T_SELF])); $this->assertSame(T_STRING, $tokens[$token]['code']); $this->assertSame('T_STRING', $tokens[$token]['type']); @@ -120,6 +120,9 @@ public function dataStrings() ['/* testAnd */'], ['/* testOr */'], ['/* testXor */'], + ['/* testFalse */'], + ['/* testTrue */'], + ['/* testNull */'], ['/* testKeywordAfterNamespaceShouldBeString */'], ['/* testNamespaceNameIsString1 */'], @@ -128,6 +131,24 @@ public function dataStrings() ['/* testKeywordAfterFunctionShouldBeString */'], ['/* testKeywordAfterFunctionByRefShouldBeString */'], + ['/* testKeywordSelfAfterFunctionByRefShouldBeString */'], + ['/* testKeywordStaticAfterFunctionByRefShouldBeString */'], + ['/* testKeywordParentAfterFunctionByRefShouldBeString */'], + ['/* testKeywordFalseAfterFunctionByRefShouldBeString */'], + ['/* testKeywordTrueAfterFunctionByRefShouldBeString */'], + ['/* testKeywordNullAfterFunctionByRefShouldBeString */'], + + ['/* testKeywordAsFunctionCallNameShouldBeStringSelf */'], + ['/* testKeywordAsFunctionCallNameShouldBeStringStatic */'], + ['/* testKeywordAsMethodCallNameShouldBeStringStatic */'], + ['/* testKeywordAsFunctionCallNameShouldBeStringParent */'], + ['/* testKeywordAsFunctionCallNameShouldBeStringFalse */'], + ['/* testKeywordAsFunctionCallNameShouldBeStringTrue */'], + ['/* testKeywordAsFunctionCallNameShouldBeStringNull */'], + + ['/* testClassInstantiationFalseIsString */'], + ['/* testClassInstantiationTrueIsString */'], + ['/* testClassInstantiationNullIsString */'], ]; }//end dataStrings() @@ -148,7 +169,10 @@ public function testKeywords($testMarker, $expectedTokenType) { $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken($testMarker, (Tokens::$contextSensitiveKeywords + [T_ANON_CLASS, T_MATCH_DEFAULT, T_PARENT, T_SELF, T_STRING])); + $token = $this->getTargetToken( + $testMarker, + (Tokens::$contextSensitiveKeywords + [T_ANON_CLASS, T_MATCH_DEFAULT, T_PARENT, T_SELF, T_STRING, T_NULL, T_FALSE, T_TRUE]) + ); $this->assertSame(constant($expectedTokenType), $tokens[$token]['code']); $this->assertSame($expectedTokenType, $tokens[$token]['type']); @@ -501,6 +525,60 @@ public function dataKeywords() '/* testNamespaceInNameIsKeyword */', 'T_NAMESPACE', ], + + [ + '/* testStaticIsKeywordBeforeClosure */', + 'T_STATIC', + ], + [ + '/* testStaticIsKeywordWhenParamType */', + 'T_STATIC', + ], + [ + '/* testStaticIsKeywordBeforeArrow */', + 'T_STATIC', + ], + [ + '/* testStaticIsKeywordWhenReturnType */', + 'T_STATIC', + ], + + [ + '/* testFalseIsKeywordAsParamType */', + 'T_FALSE', + ], + [ + '/* testTrueIsKeywordAsParamType */', + 'T_TRUE', + ], + [ + '/* testNullIsKeywordAsParamType */', + 'T_NULL', + ], + [ + '/* testFalseIsKeywordAsReturnType */', + 'T_FALSE', + ], + [ + '/* testTrueIsKeywordAsReturnType */', + 'T_TRUE', + ], + [ + '/* testNullIsKeywordAsReturnType */', + 'T_NULL', + ], + [ + '/* testFalseIsKeywordInComparison */', + 'T_FALSE', + ], + [ + '/* testTrueIsKeywordInComparison */', + 'T_TRUE', + ], + [ + '/* testNullIsKeywordInComparison */', + 'T_NULL', + ], ]; }//end dataKeywords()