diff --git a/src/Files/File.php b/src/Files/File.php index c8be94d17d..21951b94cc 100644 --- a/src/Files/File.php +++ b/src/Files/File.php @@ -1362,7 +1362,10 @@ public function getMethodParameters($stackPtr) } break; case T_STRING: - // This is a string, so it may be a type hint, but it could + case T_NAME_QUALIFIED: + case T_NAME_FULLY_QUALIFIED: + case T_NAME_RELATIVE: + // This is an identifier name, so it may be a type declaration, but it could // also be a constant used as a default value. $prevComma = false; for ($t = $i; $t >= $opener; $t--) { @@ -1395,8 +1398,6 @@ public function getMethodParameters($stackPtr) $typeHintEndToken = $i; } break; - case T_NAMESPACE: - case T_NS_SEPARATOR: case T_TYPE_UNION: case T_FALSE: case T_NULL: @@ -1600,16 +1601,17 @@ public function getMethodProperties($stackPtr) } $valid = [ - T_STRING => T_STRING, - T_CALLABLE => T_CALLABLE, - T_SELF => T_SELF, - T_PARENT => T_PARENT, - T_STATIC => T_STATIC, - T_FALSE => T_FALSE, - T_NULL => T_NULL, - T_NAMESPACE => T_NAMESPACE, - T_NS_SEPARATOR => T_NS_SEPARATOR, - T_TYPE_UNION => T_TYPE_UNION, + T_STRING => T_STRING, + T_NAME_QUALIFIED => T_NAME_QUALIFIED, + T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED, + T_NAME_RELATIVE => T_NAME_RELATIVE, + T_CALLABLE => T_CALLABLE, + T_SELF => T_SELF, + T_PARENT => T_PARENT, + T_STATIC => T_STATIC, + T_FALSE => T_FALSE, + T_NULL => T_NULL, + T_TYPE_UNION => T_TYPE_UNION, ]; for ($i = $this->tokens[$stackPtr]['parenthesis_closer']; $i < $this->numTokens; $i++) { @@ -1790,15 +1792,16 @@ public function getMemberProperties($stackPtr) if ($i < $stackPtr) { // We've found a type. $valid = [ - T_STRING => T_STRING, - T_CALLABLE => T_CALLABLE, - T_SELF => T_SELF, - T_PARENT => T_PARENT, - T_FALSE => T_FALSE, - T_NULL => T_NULL, - T_NAMESPACE => T_NAMESPACE, - T_NS_SEPARATOR => T_NS_SEPARATOR, - T_TYPE_UNION => T_TYPE_UNION, + T_STRING => T_STRING, + T_NAME_QUALIFIED => T_NAME_QUALIFIED, + T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED, + T_NAME_RELATIVE => T_NAME_RELATIVE, + T_CALLABLE => T_CALLABLE, + T_SELF => T_SELF, + T_PARENT => T_PARENT, + T_FALSE => T_FALSE, + T_NULL => T_NULL, + T_TYPE_UNION => T_TYPE_UNION, ]; for ($i; $i < $stackPtr; $i++) { @@ -1998,12 +2001,13 @@ public function isReference($stackPtr) return true; } else { $skip = Tokens::$emptyTokens; - $skip[] = T_NS_SEPARATOR; $skip[] = T_SELF; $skip[] = T_PARENT; $skip[] = T_STATIC; $skip[] = T_STRING; - $skip[] = T_NAMESPACE; + $skip[] = T_NAME_QUALIFIED; + $skip[] = T_NAME_FULLY_QUALIFIED; + $skip[] = T_NAME_RELATIVE; $skip[] = T_DOUBLE_COLON; $nextSignificantAfter = $this->findNext( @@ -2539,8 +2543,10 @@ public function findExtendedClassName($stackPtr) } $find = [ - T_NS_SEPARATOR, T_STRING, + T_NAME_QUALIFIED, + T_NAME_FULLY_QUALIFIED, + T_NAME_RELATIVE, T_WHITESPACE, ]; @@ -2590,8 +2596,10 @@ public function findImplementedInterfaceNames($stackPtr) } $find = [ - T_NS_SEPARATOR, T_STRING, + T_NAME_QUALIFIED, + T_NAME_FULLY_QUALIFIED, + T_NAME_RELATIVE, T_WHITESPACE, T_COMMA, ]; diff --git a/src/Standards/Generic/Sniffs/Classes/DuplicateClassNameSniff.php b/src/Standards/Generic/Sniffs/Classes/DuplicateClassNameSniff.php index 8374bfd535..5d1fdf87f4 100644 --- a/src/Standards/Generic/Sniffs/Classes/DuplicateClassNameSniff.php +++ b/src/Standards/Generic/Sniffs/Classes/DuplicateClassNameSniff.php @@ -11,6 +11,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Tokens; class DuplicateClassNameSniff implements Sniff { @@ -67,12 +68,12 @@ public function process(File $phpcsFile, $stackPtr) // Keep track of what namespace we are in. if ($tokens[$stackPtr]['code'] === T_NAMESPACE) { + $find = Tokens::$emptyTokens; + $find[] = T_STRING; + $find[] = T_NAME_QUALIFIED; + $nsEnd = $phpcsFile->findNext( - [ - T_NS_SEPARATOR, - T_STRING, - T_WHITESPACE, - ], + $find, ($stackPtr + 1), null, true diff --git a/src/Standards/Generic/Sniffs/PHP/ForbiddenFunctionsSniff.php b/src/Standards/Generic/Sniffs/PHP/ForbiddenFunctionsSniff.php index 1a10b15320..9f7ac6b973 100644 --- a/src/Standards/Generic/Sniffs/PHP/ForbiddenFunctionsSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/ForbiddenFunctionsSniff.php @@ -14,6 +14,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Tokens; class ForbiddenFunctionsSniff implements Sniff { @@ -69,7 +70,10 @@ public function register() $this->forbiddenFunctionNames[$i] = '/'.$name.'/i'; } - return [T_STRING]; + return [ + T_STRING, + T_NAME_FULLY_QUALIFIED, + ]; } // If we are not pattern matching, we need to work out what @@ -101,7 +105,10 @@ public function register() $this->forbiddenFunctionNames = array_map('strtolower', $this->forbiddenFunctionNames); $this->forbiddenFunctions = array_combine($this->forbiddenFunctionNames, $this->forbiddenFunctions); - return array_unique($register); + $targets = array_unique($register); + $targets[] = T_NAME_FULLY_QUALIFIED; + + return $targets; }//end register() @@ -137,35 +144,30 @@ public function process(File $phpcsFile, $stackPtr) $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); - // If function call is directly preceded by a NS_SEPARATOR it points to the - // global namespace, so we should still catch it. - if ($tokens[$prevToken]['code'] === T_NS_SEPARATOR) { - $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($prevToken - 1), null, true); - if ($tokens[$prevToken]['code'] === T_STRING) { - // Not in the global namespace. - return; - } - } - if (isset($ignore[$tokens[$prevToken]['code']]) === true) { // Not a call to a PHP function. return; } - $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); + $nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); if (isset($ignore[$tokens[$nextToken]['code']]) === true) { // Not a call to a PHP function. return; } - if ($tokens[$stackPtr]['code'] === T_STRING && $tokens[$nextToken]['code'] !== T_OPEN_PARENTHESIS) { + if (($tokens[$stackPtr]['code'] === T_STRING || $tokens[$stackPtr]['code'] === T_NAME_FULLY_QUALIFIED) + && $tokens[$nextToken]['code'] !== T_OPEN_PARENTHESIS + ) { // Not a call to a PHP function. return; } $function = strtolower($tokens[$stackPtr]['content']); - $pattern = null; + if ($tokens[$stackPtr]['code'] === T_NAME_FULLY_QUALIFIED) { + $function = ltrim($function, '\\'); + } + $pattern = null; if ($this->patternMatch === true) { $count = 0; $pattern = preg_replace( @@ -188,7 +190,7 @@ public function process(File $phpcsFile, $stackPtr) } }//end if - $this->addError($phpcsFile, $stackPtr, $tokens[$stackPtr]['content'], $pattern); + $this->addError($phpcsFile, $stackPtr, $function, $pattern); }//end process() diff --git a/src/Standards/Generic/Sniffs/WhiteSpace/LanguageConstructSpacingSniff.php b/src/Standards/Generic/Sniffs/WhiteSpace/LanguageConstructSpacingSniff.php index 068a4e7f29..7c66ac4225 100644 --- a/src/Standards/Generic/Sniffs/WhiteSpace/LanguageConstructSpacingSniff.php +++ b/src/Standards/Generic/Sniffs/WhiteSpace/LanguageConstructSpacingSniff.php @@ -71,8 +71,9 @@ public function process(File $phpcsFile, $stackPtr) $content = $tokens[$stackPtr]['content']; if ($tokens[$stackPtr]['code'] === T_NAMESPACE) { $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); - if ($nextNonEmpty !== false && $tokens[$nextNonEmpty]['code'] === T_NS_SEPARATOR) { + if ($nextNonEmpty !== false && $tokens[$nextNonEmpty]['code'] === T_NAME_FULLY_QUALIFIED) { // Namespace keyword used as operator, not as the language construct. + // In PHP 8 this use with whitespace/comments between the parts is a parse error. return; } } diff --git a/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc b/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc index bad3998f97..6b7b56a130 100644 --- a/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc +++ b/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc @@ -163,3 +163,7 @@ $a = ( if (true) {} ( 1+2) === 3 ? $a = 1 : $a = 2; class A {} ( 1+2) === 3 ? $a = 1 : $a = 2; function foo() {} ( 1+2) === 3 ? $a = 1 : $a = 2; + +$b = \functioncall( $something ) ; +$b = Package\functioncall( $something ) ; +$b = namespace\functioncall( $something ) ; diff --git a/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed b/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed index 08fcd6244a..7c29481e9c 100644 --- a/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed @@ -151,3 +151,7 @@ $a = ( if (true) {} (1+2) === 3 ? $a = 1 : $a = 2; class A {} (1+2) === 3 ? $a = 1 : $a = 2; function foo() {} (1+2) === 3 ? $a = 1 : $a = 2; + +$b = \functioncall( $something ) ; +$b = Package\functioncall( $something ) ; +$b = namespace\functioncall( $something ) ; diff --git a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc index 6b2c898c42..20ece6a6af 100644 --- a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc +++ b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc @@ -525,3 +525,16 @@ return trim(preg_replace_callback( $a = ['a' => function ($b) { return $b; }]; $a['a']( 1 ); + +$val = \functionCall ( $arg ); +$val = Package\functionCall( $arg ); +$val = namespace\functionCall ($arg); + +$val = \functionCall ( $arg +, $arg2 ); +$val = Package\functionCall( + $arg, $arg2); +$val = namespace\functionCall( + $arg, + $arg2 +); diff --git a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc.fixed b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc.fixed index 7396441878..e4be6ada80 100644 --- a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc.fixed +++ b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc.fixed @@ -537,3 +537,19 @@ return trim( $a = ['a' => function ($b) { return $b; }]; $a['a'](1); + +$val = \functionCall($arg); +$val = Package\functionCall($arg); +$val = namespace\functionCall($arg); + +$val = \functionCall( + $arg + , $arg2 +); +$val = Package\functionCall( + $arg, $arg2 +); +$val = namespace\functionCall( + $arg, + $arg2 +); diff --git a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.php b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.php index bf92bcd5e0..be961cb6ac 100644 --- a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.php +++ b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.php @@ -101,6 +101,12 @@ public function getErrorList() 523 => 1, 524 => 3, 527 => 2, + 529 => 3, + 530 => 2, + 531 => 1, + 533 => 2, + 534 => 1, + 536 => 2, ]; }//end getErrorList() diff --git a/src/Standards/PSR12/Sniffs/Classes/ClassInstantiationSniff.php b/src/Standards/PSR12/Sniffs/Classes/ClassInstantiationSniff.php index 804ecfe1c7..91009538dd 100644 --- a/src/Standards/PSR12/Sniffs/Classes/ClassInstantiationSniff.php +++ b/src/Standards/PSR12/Sniffs/Classes/ClassInstantiationSniff.php @@ -45,7 +45,9 @@ public function process(File $phpcsFile, $stackPtr) // Find the class name. $allowed = [ T_STRING => T_STRING, - T_NS_SEPARATOR => T_NS_SEPARATOR, + T_NAME_QUALIFIED => T_NAME_QUALIFIED, + T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED, + T_NAME_RELATIVE => T_NAME_RELATIVE, T_SELF => T_SELF, T_STATIC => T_STATIC, T_VARIABLE => T_VARIABLE, diff --git a/src/Standards/PSR12/Sniffs/Files/ImportStatementSniff.php b/src/Standards/PSR12/Sniffs/Files/ImportStatementSniff.php index 176aef0061..1567bc95f6 100644 --- a/src/Standards/PSR12/Sniffs/Files/ImportStatementSniff.php +++ b/src/Standards/PSR12/Sniffs/Files/ImportStatementSniff.php @@ -60,7 +60,7 @@ public function process(File $phpcsFile, $stackPtr) $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($next + 1), null, true); } - if ($tokens[$next]['code'] !== T_NS_SEPARATOR) { + if ($tokens[$next]['code'] !== T_NAME_FULLY_QUALIFIED) { return; } @@ -68,7 +68,7 @@ public function process(File $phpcsFile, $stackPtr) $fix = $phpcsFile->addFixableError($error, $next, 'LeadingSlash'); if ($fix === true) { - $phpcsFile->fixer->replaceToken($next, ''); + $phpcsFile->fixer->replaceToken($next, ltrim($tokens[$next]['content'], '\\')); } }//end process() diff --git a/src/Standards/PSR12/Sniffs/Functions/NullableTypeDeclarationSniff.php b/src/Standards/PSR12/Sniffs/Functions/NullableTypeDeclarationSniff.php index 8d90734311..d50121ba86 100644 --- a/src/Standards/PSR12/Sniffs/Functions/NullableTypeDeclarationSniff.php +++ b/src/Standards/PSR12/Sniffs/Functions/NullableTypeDeclarationSniff.php @@ -21,12 +21,14 @@ class NullableTypeDeclarationSniff implements Sniff * @var array */ private $validTokens = [ - T_STRING => true, - T_NS_SEPARATOR => true, - T_CALLABLE => true, - T_SELF => true, - T_PARENT => true, - T_STATIC => true, + T_STRING => true, + T_NAME_QUALIFIED => true, + T_NAME_FULLY_QUALIFIED => true, + T_NAME_RELATIVE => true, + T_CALLABLE => true, + T_SELF => true, + T_PARENT => true, + T_STATIC => true, ]; diff --git a/src/Standards/PSR12/Sniffs/Namespaces/CompoundNamespaceDepthSniff.php b/src/Standards/PSR12/Sniffs/Namespaces/CompoundNamespaceDepthSniff.php index 725fbde311..3776948335 100644 --- a/src/Standards/PSR12/Sniffs/Namespaces/CompoundNamespaceDepthSniff.php +++ b/src/Standards/PSR12/Sniffs/Namespaces/CompoundNamespaceDepthSniff.php @@ -62,6 +62,11 @@ public function process(File $phpcsFile, $stackPtr) continue; } + if ($tokens[$i]['code'] === T_NAME_FULLY_QUALIFIED || $tokens[$i]['code'] === T_NAME_QUALIFIED) { + $depth += substr_count($tokens[$i]['content'], '\\'); + continue; + } + if ($i === $end || $tokens[$i]['code'] === T_COMMA) { // End of a namespace. if ($depth > $this->maxDepth) { @@ -72,7 +77,7 @@ public function process(File $phpcsFile, $stackPtr) $depth = 1; } - } + }//end for }//end process() diff --git a/src/Standards/PSR12/Tests/Namespaces/CompoundNamespaceDepthUnitTest.inc b/src/Standards/PSR12/Tests/Namespaces/CompoundNamespaceDepthUnitTest.inc index 3336fc2dc4..6d9c12ec59 100644 --- a/src/Standards/PSR12/Tests/Namespaces/CompoundNamespaceDepthUnitTest.inc +++ b/src/Standards/PSR12/Tests/Namespaces/CompoundNamespaceDepthUnitTest.inc @@ -12,6 +12,7 @@ use Vendor\Package\SomeNamespace\{ ClassZ, }; +// Parse error in PHP 8.0+, but will be detected correctly cross-version anyway. use Vendor\Package\SomeNamespace\{ SubnamespaceOne /* comment */ \AnotherNamespace // comment diff --git a/src/Standards/PSR12/Tests/Namespaces/CompoundNamespaceDepthUnitTest.php b/src/Standards/PSR12/Tests/Namespaces/CompoundNamespaceDepthUnitTest.php index 41e9db7745..b5d0f865c9 100644 --- a/src/Standards/PSR12/Tests/Namespaces/CompoundNamespaceDepthUnitTest.php +++ b/src/Standards/PSR12/Tests/Namespaces/CompoundNamespaceDepthUnitTest.php @@ -27,8 +27,8 @@ public function getErrorList() { return [ 10 => 1, - 18 => 1, - 21 => 1, + 19 => 1, + 22 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PSR2/Sniffs/Classes/ClassDeclarationSniff.php b/src/Standards/PSR2/Sniffs/Classes/ClassDeclarationSniff.php index f96b004906..ec1020b2f9 100644 --- a/src/Standards/PSR2/Sniffs/Classes/ClassDeclarationSniff.php +++ b/src/Standards/PSR2/Sniffs/Classes/ClassDeclarationSniff.php @@ -272,6 +272,9 @@ public function processOpen(File $phpcsFile, $stackPtr) $find = [ T_STRING, + T_NAME_QUALIFIED, + T_NAME_FULLY_QUALIFIED, + T_NAME_RELATIVE, $keywordTokenType, ]; @@ -301,16 +304,9 @@ public function processOpen(File $phpcsFile, $stackPtr) continue; } - if ($checkingImplements === true - && $multiLineImplements === true - && ($tokens[($className - 1)]['code'] !== T_NS_SEPARATOR - || $tokens[($className - 2)]['code'] !== T_STRING) - ) { + if ($checkingImplements === true && $multiLineImplements === true) { $prev = $phpcsFile->findPrevious( - [ - T_NS_SEPARATOR, - T_WHITESPACE, - ], + Tokens::$emptyTokens, ($className - 1), $implements, true @@ -386,14 +382,9 @@ public function processOpen(File $phpcsFile, $stackPtr) } } }//end if - } else if ($tokens[($className - 1)]['code'] !== T_NS_SEPARATOR - || $tokens[($className - 2)]['code'] !== T_STRING - ) { + } else { // Not part of a longer fully qualified class name. - if ($tokens[($className - 1)]['code'] === T_COMMA - || ($tokens[($className - 1)]['code'] === T_NS_SEPARATOR - && $tokens[($className - 2)]['code'] === T_COMMA) - ) { + if ($tokens[($className - 1)]['code'] === T_COMMA) { $error = 'Expected 1 space before "%s"; 0 found'; $data = [$tokens[$className]['content']]; $fix = $phpcsFile->addFixableError($error, ($nextComma + 1), 'NoSpaceBeforeName', $data); @@ -437,7 +428,6 @@ public function processOpen(File $phpcsFile, $stackPtr) }//end if if ($checkingImplements === true - && $tokens[($className + 1)]['code'] !== T_NS_SEPARATOR && $tokens[($className + 1)]['code'] !== T_COMMA ) { if ($n !== ($classCount - 1)) { diff --git a/src/Standards/Squiz/Sniffs/Classes/SelfMemberReferenceSniff.php b/src/Standards/Squiz/Sniffs/Classes/SelfMemberReferenceSniff.php index d17ca44ce1..2ce49f72e2 100644 --- a/src/Standards/Squiz/Sniffs/Classes/SelfMemberReferenceSniff.php +++ b/src/Standards/Squiz/Sniffs/Classes/SelfMemberReferenceSniff.php @@ -75,48 +75,41 @@ protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScop return; } - } else if ($tokens[$calledClassName]['code'] === T_STRING) { - // If the class is called with a namespace prefix, build fully qualified - // namespace calls for both current scope class and requested class. - $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($calledClassName - 1), null, true); - if ($prevNonEmpty !== false && $tokens[$prevNonEmpty]['code'] === T_NS_SEPARATOR) { - $declarationName = $this->getDeclarationNameWithNamespace($tokens, $calledClassName); - $declarationName = ltrim($declarationName, '\\'); - $fullQualifiedClassName = $this->getNamespaceOfScope($phpcsFile, $currScope); - if ($fullQualifiedClassName === '\\') { - $fullQualifiedClassName = ''; - } else { - $fullQualifiedClassName .= '\\'; - } + } else if ($tokens[$calledClassName]['code'] === T_STRING + || $tokens[$calledClassName]['code'] === T_NAME_QUALIFIED + || $tokens[$calledClassName]['code'] === T_NAME_FULLY_QUALIFIED + || $tokens[$calledClassName]['code'] === T_NAME_RELATIVE + ) { + $namespaceName = $this->getNamespaceName($phpcsFile, $currScope); + if ($namespaceName === '\\') { + $namespaceName = ''; + } + + $declarationName = $namespaceName.'\\'.$phpcsFile->getDeclarationName($currScope); + + switch ($tokens[$calledClassName]['code']) { + case T_NAME_FULLY_QUALIFIED: + $inlineName = $tokens[$calledClassName]['content']; + break; + + case T_NAME_QUALIFIED: + case T_STRING: + $inlineName = $namespaceName.'\\'.$tokens[$calledClassName]['content']; + break; - $fullQualifiedClassName .= $phpcsFile->getDeclarationName($currScope); - } else { - $declarationName = $phpcsFile->getDeclarationName($currScope); - $fullQualifiedClassName = $tokens[$calledClassName]['content']; + case T_NAME_RELATIVE: + $inlineName = $namespaceName.substr($tokens[$calledClassName]['content'], 9); + break; } - if ($declarationName === $fullQualifiedClassName) { + if ($declarationName === $inlineName) { // Class name is the same as the current class, which is not allowed. $error = 'Must use "self::" for local static member reference'; $fix = $phpcsFile->addFixableError($error, $calledClassName, 'NotUsed'); if ($fix === true) { $phpcsFile->fixer->beginChangeset(); - - $currentPointer = ($stackPtr - 1); - while ($tokens[$currentPointer]['code'] === T_NS_SEPARATOR - || $tokens[$currentPointer]['code'] === T_STRING - || isset(Tokens::$emptyTokens[$tokens[$currentPointer]['code']]) === true - ) { - if (isset(Tokens::$emptyTokens[$tokens[$currentPointer]['code']]) === true) { - --$currentPointer; - continue; - } - - $phpcsFile->fixer->replaceToken($currentPointer, ''); - --$currentPointer; - } - + $phpcsFile->fixer->replaceToken($calledClassName, ''); $phpcsFile->fixer->replaceToken($stackPtr, 'self::'); $phpcsFile->fixer->endChangeset(); @@ -180,38 +173,7 @@ protected function processTokenOutsideScope(File $phpcsFile, $stackPtr) /** - * Returns the declaration names for classes/interfaces/functions with a namespace. - * - * @param array $tokens Token stack for this file - * @param int $stackPtr The position where the namespace building will start. - * - * @return string - */ - protected function getDeclarationNameWithNamespace(array $tokens, $stackPtr) - { - $nameParts = []; - $currentPointer = $stackPtr; - while ($tokens[$currentPointer]['code'] === T_NS_SEPARATOR - || $tokens[$currentPointer]['code'] === T_STRING - || isset(Tokens::$emptyTokens[$tokens[$currentPointer]['code']]) === true - ) { - if (isset(Tokens::$emptyTokens[$tokens[$currentPointer]['code']]) === true) { - --$currentPointer; - continue; - } - - $nameParts[] = $tokens[$currentPointer]['content']; - --$currentPointer; - } - - $nameParts = array_reverse($nameParts); - return implode('', $nameParts); - - }//end getDeclarationNameWithNamespace() - - - /** - * Returns the namespace declaration of a file. + * Returns the namespace name of a the current scope. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. * @param int $stackPtr The position where the search for the @@ -219,22 +181,25 @@ protected function getDeclarationNameWithNamespace(array $tokens, $stackPtr) * * @return string */ - protected function getNamespaceOfScope(File $phpcsFile, $stackPtr) + protected function getNamespaceName(File $phpcsFile, $stackPtr) { $namespace = '\\'; $namespaceDeclaration = $phpcsFile->findPrevious(T_NAMESPACE, $stackPtr); if ($namespaceDeclaration !== false) { - $endOfNamespaceDeclaration = $phpcsFile->findNext([T_SEMICOLON, T_OPEN_CURLY_BRACKET], $namespaceDeclaration); - $namespace = $this->getDeclarationNameWithNamespace( - $phpcsFile->getTokens(), - ($endOfNamespaceDeclaration - 1) - ); + $tokens = $phpcsFile->getTokens(); + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($namespaceDeclaration + 1), null, true); + if ($nextNonEmpty !== false + && ($tokens[$nextNonEmpty]['code'] === T_NAME_QUALIFIED + || $tokens[$nextNonEmpty]['code'] === T_STRING) + ) { + $namespace .= $tokens[$nextNonEmpty]['content']; + } } return $namespace; - }//end getNamespaceOfScope() + }//end getNamespaceName() }//end class diff --git a/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentThrowTagSniff.php b/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentThrowTagSniff.php index a168bfe6ca..6cb3da9e3f 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentThrowTagSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentThrowTagSniff.php @@ -90,46 +90,32 @@ public function process(File $phpcsFile, $stackPtr) don't know the exception class. */ - $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($currPos + 1), null, true); + $nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($currPos + 1), null, true); if ($tokens[$nextToken]['code'] === T_NEW - || $tokens[$nextToken]['code'] === T_NS_SEPARATOR + || $tokens[$nextToken]['code'] === T_NAME_FULLY_QUALIFIED + || $tokens[$nextToken]['code'] === T_NAME_QUALIFIED + || $tokens[$nextToken]['code'] === T_NAME_RELATIVE || $tokens[$nextToken]['code'] === T_STRING ) { if ($tokens[$nextToken]['code'] === T_NEW) { $currException = $phpcsFile->findNext( - [ - T_NS_SEPARATOR, - T_STRING, - ], - $currPos, + Tokens::$emptyTokens, + ($nextToken + 1), $stackPtrEnd, - false, - null, true ); } else { $currException = $nextToken; } - if ($currException !== false) { - $endException = $phpcsFile->findNext( - [ - T_NS_SEPARATOR, - T_STRING, - ], - ($currException + 1), - $stackPtrEnd, - true, - null, - true - ); - - if ($endException === false) { - $thrownExceptions[] = $tokens[$currException]['content']; - } else { - $thrownExceptions[] = $phpcsFile->getTokensAsString($currException, ($endException - $currException)); - } - }//end if + if ($currException !== false + && ($tokens[$currException]['code'] === T_NAME_FULLY_QUALIFIED + || $tokens[$currException]['code'] === T_NAME_QUALIFIED + || $tokens[$currException]['code'] === T_NAME_RELATIVE + || $tokens[$currException]['code'] === T_STRING) + ) { + $thrownExceptions[] = $tokens[$currException]['content']; + } } else if ($tokens[$nextToken]['code'] === T_VARIABLE) { // Find the nearest catch block in this scope and, if the caught var // matches our re-thrown var, use the exception types being caught as diff --git a/src/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php index 7b9fc933ad..30888890f6 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php @@ -37,7 +37,9 @@ public function processMemberVar(File $phpcsFile, $stackPtr) T_STATIC, T_WHITESPACE, T_STRING, - T_NS_SEPARATOR, + T_NAME_QUALIFIED, + T_NAME_FULLY_QUALIFIED, + T_NAME_RELATIVE, T_NULLABLE, ]; diff --git a/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php b/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php index da567a2bc4..9527f08587 100644 --- a/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php +++ b/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php @@ -118,8 +118,10 @@ public function process(File $phpcsFile, $stackPtr) T_LNUMBER, T_DNUMBER, T_STRING, + T_NAME_QUALIFIED, + T_NAME_FULLY_QUALIFIED, + T_NAME_RELATIVE, T_WHITESPACE, - T_NS_SEPARATOR, T_THIS, T_SELF, T_STATIC, @@ -148,7 +150,12 @@ public function process(File $phpcsFile, $stackPtr) break; } - if ($prevCode === T_STRING || $prevCode === T_SWITCH) { + if ($prevCode === T_STRING + || $prevCode === T_NAME_QUALIFIED + || $prevCode === T_NAME_FULLY_QUALIFIED + || $prevCode === T_NAME_RELATIVE + || $prevCode === T_SWITCH + ) { // We allow simple operations to not be bracketed. // For example, ceil($one / $two). for ($prev = ($stackPtr - 1); $prev > $bracket; $prev--) { @@ -260,10 +267,12 @@ public function addMissingBracketsError($phpcsFile, $stackPtr) T_LNUMBER => true, T_DNUMBER => true, T_STRING => true, + T_NAME_QUALIFIED => true, + T_NAME_FULLY_QUALIFIED => true, + T_NAME_RELATIVE => true, T_CONSTANT_ENCAPSED_STRING => true, T_DOUBLE_QUOTED_STRING => true, T_WHITESPACE => true, - T_NS_SEPARATOR => true, T_THIS => true, T_SELF => true, T_STATIC => true, diff --git a/src/Standards/Squiz/Sniffs/PHP/DisallowMultipleAssignmentsSniff.php b/src/Standards/Squiz/Sniffs/PHP/DisallowMultipleAssignmentsSniff.php index 84032fec01..6ce56f49b5 100644 --- a/src/Standards/Squiz/Sniffs/PHP/DisallowMultipleAssignmentsSniff.php +++ b/src/Standards/Squiz/Sniffs/PHP/DisallowMultipleAssignmentsSniff.php @@ -116,7 +116,9 @@ public function process(File $phpcsFile, $stackPtr) $allowed = Tokens::$emptyTokens; $allowed[T_STRING] = T_STRING; - $allowed[T_NS_SEPARATOR] = T_NS_SEPARATOR; + $allowed[T_NAME_QUALIFIED] = T_NAME_QUALIFIED; + $allowed[T_NAME_FULLY_QUALIFIED] = T_NAME_FULLY_QUALIFIED; + $allowed[T_NAME_RELATIVE] = T_NAME_RELATIVE; $allowed[T_DOUBLE_COLON] = T_DOUBLE_COLON; $allowed[T_OBJECT_OPERATOR] = T_OBJECT_OPERATOR; $allowed[T_ASPERAND] = T_ASPERAND; diff --git a/src/Standards/Squiz/Sniffs/PHP/LowercasePHPFunctionsSniff.php b/src/Standards/Squiz/Sniffs/PHP/LowercasePHPFunctionsSniff.php index 519b7f6ccc..fe967d4104 100644 --- a/src/Standards/Squiz/Sniffs/PHP/LowercasePHPFunctionsSniff.php +++ b/src/Standards/Squiz/Sniffs/PHP/LowercasePHPFunctionsSniff.php @@ -43,7 +43,10 @@ public function __construct() */ public function register() { - return [T_STRING]; + return [ + T_STRING, + T_NAME_FULLY_QUALIFIED, + ]; }//end register() @@ -62,6 +65,10 @@ public function process(File $phpcsFile, $stackPtr) $tokens = $phpcsFile->getTokens(); $content = $tokens[$stackPtr]['content']; + if ($tokens[$stackPtr]['code'] === T_NAME_FULLY_QUALIFIED) { + $content = ltrim($content, '\\'); + } + $contentLc = strtolower($content); if ($content === $contentLc) { return; @@ -88,7 +95,7 @@ public function process(File $phpcsFile, $stackPtr) if ($tokens[$next]['code'] !== T_OPEN_PARENTHESIS) { // Is this a use statement importing a PHP native function ? - if ($tokens[$next]['code'] !== T_NS_SEPARATOR + if ($tokens[$stackPtr]['code'] === T_STRING && $tokens[$prev]['code'] === T_STRING && $tokens[$prev]['content'] === 'function' && $prevPrev !== false @@ -115,15 +122,15 @@ public function process(File $phpcsFile, $stackPtr) return; } - if ($tokens[$prev]['code'] === T_NS_SEPARATOR) { - if ($prevPrev !== false - && ($tokens[$prevPrev]['code'] === T_STRING - || $tokens[$prevPrev]['code'] === T_NAMESPACE - || $tokens[$prevPrev]['code'] === T_NEW) + if ($tokens[$stackPtr]['code'] === T_NAME_FULLY_QUALIFIED) { + if ($prev !== false + && ($tokens[$prev]['code'] === T_STRING + || $tokens[$prev]['code'] === T_NAMESPACE) ) { // Namespaced class/function, not an inbuilt function. // Could potentially give false negatives for non-namespaced files - // when namespace\functionName() is encountered. + // when namespace\functionName() is encountered and for identifier names + // interlaced with whitespace or comments (parse errors in PHP 8). return; } } @@ -153,7 +160,7 @@ public function process(File $phpcsFile, $stackPtr) $fix = $phpcsFile->addFixableError($error, $stackPtr, 'CallUppercase', $data); if ($fix === true) { - $phpcsFile->fixer->replaceToken($stackPtr, $contentLc); + $phpcsFile->fixer->replaceToken($stackPtr, strtolower($tokens[$stackPtr]['content'])); } }//end process() diff --git a/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.inc b/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.inc index 0a0729a0a9..97b4de350c 100644 --- a/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.inc +++ b/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.inc @@ -159,16 +159,16 @@ class Nested_Anon_Class { namespace Foo\Baz { class BarFoo { public function foo() { - echo Foo\Baz\BarFoo::$prop; + echo \Foo\Baz\BarFoo::$prop; } } } -// Prevent false negative when namespace has whitespace/comments. +// Prevent false negative when namespace has whitespace/comments - no longer supported as of PHPCS 4.x. namespace Foo /*comment*/ \ Bah { class BarFoo { public function foo() { - echo Foo \ /*comment*/ Bah\BarFoo::$prop; + echo \Foo \ /*comment*/ Bah\BarFoo::$prop; } } } diff --git a/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.inc.fixed index f2c1731dc7..3d29786202 100644 --- a/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.inc.fixed @@ -152,11 +152,11 @@ namespace Foo\Baz { } } -// Prevent false negative when namespace has whitespace/comments. +// Prevent false negative when namespace has whitespace/comments - no longer supported as of PHPCS 4.x. namespace Foo /*comment*/ \ Bah { class BarFoo { public function foo() { - echo /*comment*/ self::$prop; + echo \Foo \ /*comment*/ Bah\BarFoo::$prop; } } } diff --git a/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.php b/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.php index ea493d3cbb..ba9b02d34a 100644 --- a/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.php +++ b/src/Standards/Squiz/Tests/Classes/SelfMemberReferenceUnitTest.php @@ -40,7 +40,6 @@ public function getErrorList() 140 => 1, 143 => 2, 162 => 1, - 171 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc index 65f4389bdc..f0d573241e 100644 --- a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc +++ b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc @@ -362,4 +362,20 @@ class Foo var int $noComment = 1; - } + + public static ?\Folder\ClassName $FQNTypeDecl_noComment = null; + + /** + * Description + * + * @var Folder\ClassName + */ + public static Folder\ClassName $QualifiedTypeDecl_noComment = null; + + /** + * Description + * + * @var ClassName + */ + public static namespace\ClassName $relativeTypeDecl_noComment = null; +} diff --git a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed index ca0b052e35..cdb6f589b8 100644 --- a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed @@ -362,4 +362,20 @@ class Foo var int $noComment = 1; - } + + public static ?\Folder\ClassName $FQNTypeDecl_noComment = null; + + /** + * Description + * + * @var Folder\ClassName + */ + public static Folder\ClassName $QualifiedTypeDecl_noComment = null; + + /** + * Description + * + * @var ClassName + */ + public static namespace\ClassName $relativeTypeDecl_noComment = null; +} diff --git a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.php index f3ee3c76dd..ea2159b400 100644 --- a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.php +++ b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.php @@ -58,6 +58,7 @@ public function getErrorList() 336 => 1, 361 => 1, 364 => 1, + 366 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/Operators/ComparisonOperatorUsageUnitTest.inc b/src/Standards/Squiz/Tests/Operators/ComparisonOperatorUsageUnitTest.inc index 8522438dcf..ae0b71f2c8 100644 --- a/src/Standards/Squiz/Tests/Operators/ComparisonOperatorUsageUnitTest.inc +++ b/src/Standards/Squiz/Tests/Operators/ComparisonOperatorUsageUnitTest.inc @@ -16,11 +16,11 @@ if (is_array($array) === TRUE) { } if (is_array($array) == TRUE) { -} else if (myFunction($value) == FALSE) { +} else if (Package\myFunction($value) == FALSE) { } if (is_array($array)) { -} else if (!myFunction($value)) { +} else if (!namespace\myFunction($value)) { } if ($value === TRUE || $other === FALSE) { diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index 10a1e50157..6bb252d7d9 100644 --- a/src/Tokenizers/PHP.php +++ b/src/Tokenizers/PHP.php @@ -15,6 +15,13 @@ class PHP extends Tokenizer { + /** + * Regular expression to check if a given identifier name is valid for use in PHP. + * + * @var string + */ + const PHP_LABEL_REGEX = '`^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$`'; + /** * A list of tokens that are allowed to open a scope. * @@ -949,6 +956,92 @@ protected function tokenize($string) continue; }//end if + /* + Before PHP 8.0, namespaced names were not tokenized as a single token. + + Note: reserved keywords are allowed within the "single token" names, so + no check is done on the token type following a namespace separator _on purpose_. + As long as it is not an empty token and the token contents complies with the + "name" requirements in PHP, we'll accept it. + */ + + if (PHP_VERSION_ID < 80000 + && $tokenIsArray === true + && ($token[0] === T_STRING + || $token[0] === T_NAMESPACE + || ($token[0] === T_NS_SEPARATOR + && isset($tokens[($stackPtr + 1)]) === true + && is_array($tokens[($stackPtr + 1)]) === true + && isset(Tokens::$emptyTokens[$tokens[($stackPtr + 1)][0]]) === false + && preg_match(self::PHP_LABEL_REGEX, $tokens[($stackPtr + 1)][1]) === 1)) + ) { + $nameStart = $stackPtr; + $i = $stackPtr; + $newToken = []; + $newToken['content'] = $token[1]; + + switch ($token[0]) { + case T_STRING: + $newToken['code'] = T_NAME_QUALIFIED; + $newToken['type'] = 'T_NAME_QUALIFIED'; + break; + case T_NAMESPACE: + $newToken['code'] = T_NAME_RELATIVE; + $newToken['type'] = 'T_NAME_RELATIVE'; + break; + case T_NS_SEPARATOR: + $newToken['code'] = T_NAME_FULLY_QUALIFIED; + $newToken['type'] = 'T_NAME_FULLY_QUALIFIED'; + + if (is_array($tokens[($i - 1)]) === true + && isset(Tokens::$emptyTokens[$tokens[($i - 1)][0]]) === false + && preg_match(self::PHP_LABEL_REGEX, $tokens[($i - 1)][1]) === 1 + ) { + // The namespaced name starts with a reserved keyword. Move one token back. + $newToken['code'] = T_NAME_QUALIFIED; + $newToken['type'] = 'T_NAME_QUALIFIED'; + $newToken['content'] = $tokens[($i - 1)][1]; + --$nameStart; + --$i; + break; + } + + ++$i; + $newToken['content'] .= $tokens[$i][1]; + break; + }//end switch + + while (isset($tokens[($i + 1)], $tokens[($i + 2)]) === true + && is_array($tokens[($i + 1)]) === true && $tokens[($i + 1)][0] === T_NS_SEPARATOR + && is_array($tokens[($i + 2)]) === true + && isset(Tokens::$emptyTokens[$tokens[($i + 2)][0]]) === false + && preg_match(self::PHP_LABEL_REGEX, $tokens[($i + 2)][1]) === 1 + ) { + $newToken['content'] .= $tokens[($i + 1)][1].$tokens[($i + 2)][1]; + $i = ($i + 2); + } + + if ($i !== $nameStart) { + if ($nameStart !== $stackPtr) { + // This must be a qualified name starting with a reserved keyword. + // We need to overwrite the previously set final token. + --$newStackPtr; + } + + $finalTokens[$newStackPtr] = $newToken; + $newStackPtr++; + $stackPtr = $i; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + $type = $newToken['type']; + $content = $newToken['content']; + Common::printStatusMessage("* token $nameStart to $i ($content) retokenized to $type", 2); + } + + continue; + } + }//end if + /* Convert ? to T_NULLABLE OR T_INLINE_THEN */ @@ -961,7 +1054,7 @@ protected function tokenize($string) * Check if the next non-empty token is one of the tokens which can be used * in type declarations. If not, it's definitely a ternary. * At this point, the only token types which need to be taken into consideration - * as potential type declarations are T_STRING, T_ARRAY, T_CALLABLE and T_NS_SEPARATOR. + * as potential type declarations are identifier names, T_ARRAY, T_CALLABLE and T_NS_SEPARATOR. */ $lastRelevantNonEmpty = null; @@ -978,6 +1071,9 @@ protected function tokenize($string) } if ($tokenType === T_STRING + || $tokenType === T_NAME_FULLY_QUALIFIED + || $tokenType === T_NAME_RELATIVE + || $tokenType === T_NAME_QUALIFIED || $tokenType === T_ARRAY || $tokenType === T_NAMESPACE || $tokenType === T_NS_SEPARATOR @@ -990,7 +1086,10 @@ protected function tokenize($string) && isset($lastRelevantNonEmpty) === false) || ($lastRelevantNonEmpty === T_ARRAY && $tokenType === '(') - || ($lastRelevantNonEmpty === T_STRING + || (($lastRelevantNonEmpty === T_STRING + || $lastRelevantNonEmpty === T_NAME_FULLY_QUALIFIED + || $lastRelevantNonEmpty === T_NAME_RELATIVE + || $lastRelevantNonEmpty === T_NAME_QUALIFIED) && ($tokenType === T_DOUBLE_COLON || $tokenType === '(' || $tokenType === ':')) @@ -1135,6 +1234,10 @@ protected function tokenize($string) tokenized as T_STRING even if it appears to be a different token, such as when writing code like: function default(): foo so go forward and change the token type before it is processed. + + Note: this should not be done for `function Level\Name` within a + group use statement. The PHP 8 identifier name tokens should + remain as they are for those. */ if ($tokenIsArray === true @@ -1152,7 +1255,11 @@ protected function tokenize($string) } } - if ($x < $numTokens && is_array($tokens[$x]) === true) { + if ($x < $numTokens + && is_array($tokens[$x]) === true + && $tokens[$x][0] !== T_NAME_QUALIFIED + && $tokens[$x][0] !== T_NAME_FULLY_QUALIFIED + ) { if (PHP_CODESNIFFER_VERBOSITY > 1) { $oldType = Tokens::tokenName($tokens[$x][0]); Common::printStatusMessage("* token $x changed from $oldType to T_STRING", 2); @@ -1208,14 +1315,17 @@ function return types. We want to keep the parenthesis map clean, && $tokens[$x] === ':' ) { $allowed = [ - T_ARRAY => T_ARRAY, - T_CALLABLE => T_CALLABLE, - T_NAMESPACE => T_NAMESPACE, - T_NS_SEPARATOR => T_NS_SEPARATOR, - T_PARENT => T_PARENT, - T_SELF => T_SELF, - T_STATIC => T_STATIC, - T_STRING => T_STRING, + T_STRING => T_STRING, + T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED, + T_NAME_RELATIVE => T_NAME_RELATIVE, + T_NAME_QUALIFIED => T_NAME_QUALIFIED, + T_ARRAY => T_ARRAY, + T_CALLABLE => T_CALLABLE, + T_SELF => T_SELF, + T_PARENT => T_PARENT, + T_STATIC => T_STATIC, + T_NAMESPACE => T_NAMESPACE, + T_NS_SEPARATOR => T_NS_SEPARATOR, ]; $allowed += Tokens::$emptyTokens; @@ -1706,17 +1816,18 @@ protected function processAdditional() if (isset($this->tokens[$x]) === true && $this->tokens[$x]['code'] === T_OPEN_PARENTHESIS) { $ignore = Tokens::$emptyTokens; $ignore += [ - T_STRING => T_STRING, - T_ARRAY => T_ARRAY, - T_COLON => T_COLON, - T_NAMESPACE => T_NAMESPACE, - T_NS_SEPARATOR => T_NS_SEPARATOR, - T_NULLABLE => T_NULLABLE, - T_CALLABLE => T_CALLABLE, - T_PARENT => T_PARENT, - T_SELF => T_SELF, - T_STATIC => T_STATIC, - T_TYPE_UNION => T_TYPE_UNION, + T_STRING => T_STRING, + T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED, + T_NAME_RELATIVE => T_NAME_RELATIVE, + T_NAME_QUALIFIED => T_NAME_QUALIFIED, + T_ARRAY => T_ARRAY, + T_COLON => T_COLON, + T_NULLABLE => T_NULLABLE, + T_CALLABLE => T_CALLABLE, + T_PARENT => T_PARENT, + T_SELF => T_SELF, + T_STATIC => T_STATIC, + T_TYPE_UNION => T_TYPE_UNION, ]; $closer = $this->tokens[$x]['parenthesis_closer']; @@ -1860,6 +1971,9 @@ protected function processAdditional() T_OBJECT_OPERATOR => T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR => T_NULLSAFE_OBJECT_OPERATOR, T_STRING => T_STRING, + T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED, + T_NAME_RELATIVE => T_NAME_RELATIVE, + T_NAME_QUALIFIED => T_NAME_QUALIFIED, T_CONSTANT_ENCAPSED_STRING => T_CONSTANT_ENCAPSED_STRING, ]; $allowed += Tokens::$magicConstants; @@ -1904,14 +2018,16 @@ protected function processAdditional() */ $allowed = [ - T_STRING => T_STRING, - T_CALLABLE => T_CALLABLE, - T_SELF => T_SELF, - T_PARENT => T_PARENT, - T_STATIC => T_STATIC, - T_FALSE => T_FALSE, - T_NULL => T_NULL, - T_NS_SEPARATOR => T_NS_SEPARATOR, + T_STRING => T_STRING, + T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED, + T_NAME_RELATIVE => T_NAME_RELATIVE, + T_NAME_QUALIFIED => T_NAME_QUALIFIED, + T_CALLABLE => T_CALLABLE, + T_SELF => T_SELF, + T_PARENT => T_PARENT, + T_STATIC => T_STATIC, + T_FALSE => T_FALSE, + T_NULL => T_NULL, ]; $suspectedType = null; diff --git a/src/Util/Tokens.php b/src/Util/Tokens.php index 1335d248f8..96a63ad4cf 100644 --- a/src/Util/Tokens.php +++ b/src/Util/Tokens.php @@ -128,6 +128,18 @@ define('T_NULLSAFE_OBJECT_OPERATOR', 'PHPCS_T_NULLSAFE_OBJECT_OPERATOR'); } +if (defined('T_NAME_QUALIFIED') === false) { + define('T_NAME_QUALIFIED', 'PHPCS_T_NAME_QUALIFIED'); +} + +if (defined('T_NAME_FULLY_QUALIFIED') === false) { + define('T_NAME_FULLY_QUALIFIED', 'PHPCS_T_NAME_FULLY_QUALIFIED'); +} + +if (defined('T_NAME_RELATIVE') === false) { + define('T_NAME_RELATIVE', 'PHPCS_T_NAME_RELATIVE'); +} + // Tokens used for parsing doc blocks. define('T_DOC_COMMENT_STAR', 'PHPCS_T_DOC_COMMENT_STAR'); define('T_DOC_COMMENT_WHITESPACE', 'PHPCS_T_DOC_COMMENT_WHITESPACE'); @@ -568,18 +580,21 @@ final class Tokens * @var array */ public static $functionNameTokens = [ - T_STRING => T_STRING, - T_EVAL => T_EVAL, - T_EXIT => T_EXIT, - T_INCLUDE => T_INCLUDE, - T_INCLUDE_ONCE => T_INCLUDE_ONCE, - T_REQUIRE => T_REQUIRE, - T_REQUIRE_ONCE => T_REQUIRE_ONCE, - T_ISSET => T_ISSET, - T_UNSET => T_UNSET, - T_EMPTY => T_EMPTY, - T_SELF => T_SELF, - T_STATIC => T_STATIC, + T_STRING => T_STRING, + T_NAME_QUALIFIED => T_NAME_QUALIFIED, + T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED, + T_NAME_RELATIVE => T_NAME_RELATIVE, + T_EVAL => T_EVAL, + T_EXIT => T_EXIT, + T_INCLUDE => T_INCLUDE, + T_INCLUDE_ONCE => T_INCLUDE_ONCE, + T_REQUIRE => T_REQUIRE, + T_REQUIRE_ONCE => T_REQUIRE_ONCE, + T_ISSET => T_ISSET, + T_UNSET => T_UNSET, + T_EMPTY => T_EMPTY, + T_SELF => T_SELF, + T_STATIC => T_STATIC, ]; /** diff --git a/tests/Core/File/FindEndOfStatementTest.php b/tests/Core/File/FindEndOfStatementTest.php index 1fbc8a6871..cf7d817258 100644 --- a/tests/Core/File/FindEndOfStatementTest.php +++ b/tests/Core/File/FindEndOfStatementTest.php @@ -154,7 +154,7 @@ public function testUseGroup() $start = $this->getTargetToken('/* testUseGroup */', T_USE); $found = self::$phpcsFile->findEndOfStatement($start); - $this->assertSame(($start + 23), $found); + $this->assertSame(($start + 21), $found); }//end testUseGroup() diff --git a/tests/Core/File/FindExtendedClassNameTest.inc b/tests/Core/File/FindExtendedClassNameTest.inc index aead06cd9d..097b4ea40f 100644 --- a/tests/Core/File/FindExtendedClassNameTest.inc +++ b/tests/Core/File/FindExtendedClassNameTest.inc @@ -30,6 +30,12 @@ class testFECNNestedExtendedClass { } } +/* testNamespaceRelativeQualifiedClass */ +class testFECNQualifiedClass extends Core\File\RelativeClass {} + +/* testExtendedClassUsingNamespaceOperator */ +class testWithNSOperator extends namespace\Bar {} + /* testClassThatExtendsAndImplements */ class testFECNClassThatExtendsAndImplements extends testFECNClass implements InterfaceA, InterfaceB {} diff --git a/tests/Core/File/FindExtendedClassNameTest.php b/tests/Core/File/FindExtendedClassNameTest.php index f39377e6c8..3f398801b8 100644 --- a/tests/Core/File/FindExtendedClassNameTest.php +++ b/tests/Core/File/FindExtendedClassNameTest.php @@ -77,6 +77,14 @@ public function dataExtendedClass() '/* testNestedExtendedAnonClass */', 'testFECNAnonClass', ], + [ + '/* testNamespaceRelativeQualifiedClass */', + 'Core\File\RelativeClass', + ], + [ + '/* testExtendedClassUsingNamespaceOperator */', + 'namespace\Bar', + ], [ '/* testClassThatExtendsAndImplements */', 'testFECNClass', diff --git a/tests/Core/File/FindImplementedInterfaceNamesTest.inc b/tests/Core/File/FindImplementedInterfaceNamesTest.inc index 3885b27e1d..67cb1b4420 100644 --- a/tests/Core/File/FindImplementedInterfaceNamesTest.inc +++ b/tests/Core/File/FindImplementedInterfaceNamesTest.inc @@ -19,6 +19,12 @@ class testFIINNamespacedClass implements \PHP_CodeSniffer\Tests\Core\File\testFI /* testNonImplementedClass */ class testFIINNonImplementedClass {} +/* testNamespaceRelativeQualifiedClass */ +class testFIINQualifiedClass implements Core\File\RelativeInterface {} + +/* testDeclarationMultiImplementedNamespaceOperator */ +class testMultiImplementedNSOperator implements namespace\testInterfaceA, namespace\testInterfaceB {} + /* testClassThatExtendsAndImplements */ class testFECNClassThatExtendsAndImplements extends testFECNClass implements InterfaceA, \NameSpaced\Cat\InterfaceB {} diff --git a/tests/Core/File/FindImplementedInterfaceNamesTest.php b/tests/Core/File/FindImplementedInterfaceNamesTest.php index 834e083202..a6b0c4d658 100644 --- a/tests/Core/File/FindImplementedInterfaceNamesTest.php +++ b/tests/Core/File/FindImplementedInterfaceNamesTest.php @@ -67,6 +67,17 @@ public function dataImplementedInterface() '/* testInterface */', false, ], + [ + '/* testNamespaceRelativeQualifiedClass */', + ['Core\File\RelativeInterface'], + ], + [ + '/* testDeclarationMultiImplementedNamespaceOperator */', + [ + 'namespace\testInterfaceA', + 'namespace\testInterfaceB', + ], + ], [ '/* testClassThatExtendsAndImplements */', [ diff --git a/tests/Core/File/GetMethodParametersTest.inc b/tests/Core/File/GetMethodParametersTest.inc index 0b7a71fc6d..ae583daad7 100644 --- a/tests/Core/File/GetMethodParametersTest.inc +++ b/tests/Core/File/GetMethodParametersTest.inc @@ -42,6 +42,12 @@ function mixedTypeHintNullable(?Mixed $var1) {} /* testNamespaceOperatorTypeHint */ function namespaceOperatorTypeHint(?namespace\Name $var1) {} +/* testVariadicFunctionClassType */ +function variableLengthArgument($unit, DateInterval ...$intervals) {} + +/* testNameSpacedTypeDeclaration */ +function namespacedClassType( \Package\Sub\ClassName $a, ?Sub\AnotherClass $b ) {} + /* testPHP8UnionTypesSimple */ function unionTypeSimple(int|float $number, self|parent &...$obj) {} diff --git a/tests/Core/File/GetMethodParametersTest.php b/tests/Core/File/GetMethodParametersTest.php index e9ff9b71f6..3b93846a8a 100644 --- a/tests/Core/File/GetMethodParametersTest.php +++ b/tests/Core/File/GetMethodParametersTest.php @@ -340,6 +340,66 @@ public function testNamespaceOperatorTypeHint() }//end testNamespaceOperatorTypeHint() + /** + * Verify handling of a variadic parameter with a class name based type declaration. + * + * @return void + */ + public function testVariadicFunctionClassType() + { + $expected = []; + $expected[0] = [ + 'name' => '$unit', + 'content' => '$unit', + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '', + 'nullable_type' => false, + ]; + $expected[1] = [ + 'name' => '$intervals', + 'content' => 'DateInterval ...$intervals', + 'pass_by_reference' => false, + 'variable_length' => true, + 'type_hint' => 'DateInterval', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testVariadicFunctionClassType() + + + /** + * Verify recognition of various namespaced class name type declarations. + * + * @return void + */ + public function testNameSpacedTypeDeclaration() + { + $expected = []; + $expected[0] = [ + 'name' => '$a', + 'content' => '\Package\Sub\ClassName $a', + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '\Package\Sub\ClassName', + 'nullable_type' => false, + ]; + $expected[1] = [ + 'name' => '$b', + 'content' => '?Sub\AnotherClass $b', + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '?Sub\AnotherClass', + 'nullable_type' => true, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testNameSpacedTypeDeclaration() + + /** * Verify recognition of PHP8 union type declaration. * diff --git a/tests/Core/File/GetMethodPropertiesTest.inc b/tests/Core/File/GetMethodPropertiesTest.inc index 3e9682df6e..da83f81e9a 100644 --- a/tests/Core/File/GetMethodPropertiesTest.inc +++ b/tests/Core/File/GetMethodPropertiesTest.inc @@ -44,6 +44,12 @@ class MyClass { function myFunction(): \MyNamespace /** comment *\/ comment */ \MyClass /* comment */ \Foo {} + + /* testReturnUnqualifiedName */ + private function myFunction(): ?MyClass {} + + /* testReturnPartiallyQualifiedName */ + function myFunction(): Sub\Level\MyClass {} } abstract class MyClass diff --git a/tests/Core/File/GetMethodPropertiesTest.php b/tests/Core/File/GetMethodPropertiesTest.php index a12c2ea65a..6b99377ecd 100644 --- a/tests/Core/File/GetMethodPropertiesTest.php +++ b/tests/Core/File/GetMethodPropertiesTest.php @@ -291,6 +291,52 @@ public function testReturnMultilineNamespace() }//end testReturnMultilineNamespace() + /** + * Test a method with an unqualified named return type. + * + * @return void + */ + public function testReturnUnqualifiedName() + { + $expected = [ + 'scope' => 'private', + 'scope_specified' => true, + 'return_type' => '?MyClass', + 'nullable_return_type' => true, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testReturnUnqualifiedName() + + + /** + * Test a method with a partially qualified namespaced return type. + * + * @return void + */ + public function testReturnPartiallyQualifiedName() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'Sub\Level\MyClass', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testReturnPartiallyQualifiedName() + + /** * Test a basic abstract method. * diff --git a/tests/Core/File/IsReferenceTest.inc b/tests/Core/File/IsReferenceTest.inc index cd40ed3ba7..6bc6a22110 100644 --- a/tests/Core/File/IsReferenceTest.inc +++ b/tests/Core/File/IsReferenceTest.inc @@ -128,6 +128,9 @@ functionCall($something, &\SomeNS\SomeClass::$somethingElse); /* testPassByReferenceJ */ functionCall($something, &namespace\SomeClass::$somethingElse); +/* testPassByReferenceK */ +functionCall($something, &Sub\Level\SomeClass::$somethingElse); + /* testNewByReferenceA */ $foobar2 = &new Foobar(); diff --git a/tests/Core/File/IsReferenceTest.php b/tests/Core/File/IsReferenceTest.php index ea2dddbad3..26989e1f8f 100644 --- a/tests/Core/File/IsReferenceTest.php +++ b/tests/Core/File/IsReferenceTest.php @@ -212,6 +212,10 @@ public function dataIsReference() '/* testPassByReferenceJ */', true, ], + [ + '/* testPassByReferenceK */', + true, + ], [ '/* testNewByReferenceA */', true, diff --git a/tests/Core/Tokenizer/BackfillFnTokenTest.inc b/tests/Core/Tokenizer/BackfillFnTokenTest.inc index f470bb0c67..5e3595eb19 100644 --- a/tests/Core/Tokenizer/BackfillFnTokenTest.inc +++ b/tests/Core/Tokenizer/BackfillFnTokenTest.inc @@ -60,6 +60,12 @@ $a = [ /* testYield */ $a = fn($x) => yield 'k' => $x; +/* testReturnTypeNamespacedClass */ +$fn = fn($x) : ?\My\NS\ClassName => $x; + +/* testReturnTypePartiallyQualifiedClass */ +$fn = fn($x) : ?NS\ClassName => $x; + /* testNullableNamespace */ $a = fn(?\DateTime $x) : ?\DateTime => $x; diff --git a/tests/Core/Tokenizer/BackfillFnTokenTest.php b/tests/Core/Tokenizer/BackfillFnTokenTest.php index b8ea0170ee..163d17e28a 100644 --- a/tests/Core/Tokenizer/BackfillFnTokenTest.php +++ b/tests/Core/Tokenizer/BackfillFnTokenTest.php @@ -437,6 +437,62 @@ public function testYield() }//end testYield() + /** + * Test arrow functions that use nullable namespace types. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testReturnTypeNamespacedClass() + { + $tokens = self::$phpcsFile->getTokens(); + + $token = $this->getTargetToken('/* testReturnTypeNamespacedClass */', T_FN); + $this->backfillHelper($token); + + $this->assertSame($tokens[$token]['scope_opener'], ($token + 10), 'Scope opener is not the arrow token'); + $this->assertSame($tokens[$token]['scope_closer'], ($token + 13), 'Scope closer is not the semicolon token'); + + $opener = $tokens[$token]['scope_opener']; + $this->assertSame($tokens[$opener]['scope_opener'], ($token + 10), 'Opener scope opener is not the arrow token'); + $this->assertSame($tokens[$opener]['scope_closer'], ($token + 13), 'Opener scope closer is not the semicolon token'); + + $closer = $tokens[$token]['scope_closer']; + $this->assertSame($tokens[$closer]['scope_opener'], ($token + 10), 'Closer scope opener is not the arrow token'); + $this->assertSame($tokens[$closer]['scope_closer'], ($token + 13), 'Closer scope closer is not the semicolon token'); + + }//end testReturnTypeNamespacedClass() + + + /** + * Test arrow functions that use nullable namespace types. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testReturnTypePartiallyQualifiedClass() + { + $tokens = self::$phpcsFile->getTokens(); + + $token = $this->getTargetToken('/* testReturnTypePartiallyQualifiedClass */', T_FN); + $this->backfillHelper($token); + + $this->assertSame($tokens[$token]['scope_opener'], ($token + 10), 'Scope opener is not the arrow token'); + $this->assertSame($tokens[$token]['scope_closer'], ($token + 13), 'Scope closer is not the semicolon token'); + + $opener = $tokens[$token]['scope_opener']; + $this->assertSame($tokens[$opener]['scope_opener'], ($token + 10), 'Opener scope opener is not the arrow token'); + $this->assertSame($tokens[$opener]['scope_closer'], ($token + 13), 'Opener scope closer is not the semicolon token'); + + $closer = $tokens[$token]['scope_closer']; + $this->assertSame($tokens[$closer]['scope_opener'], ($token + 10), 'Closer scope opener is not the arrow token'); + $this->assertSame($tokens[$closer]['scope_closer'], ($token + 13), 'Closer scope closer is not the semicolon token'); + + }//end testReturnTypePartiallyQualifiedClass() + + /** * Test arrow functions that use nullable namespace types. * @@ -451,16 +507,16 @@ public function testNullableNamespace() $token = $this->getTargetToken('/* testNullableNamespace */', T_FN); $this->backfillHelper($token); - $this->assertSame($tokens[$token]['scope_opener'], ($token + 15), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 18), 'Scope closer is not the semicolon token'); + $this->assertSame($tokens[$token]['scope_opener'], ($token + 13), 'Scope opener is not the arrow token'); + $this->assertSame($tokens[$token]['scope_closer'], ($token + 16), 'Scope closer is not the semicolon token'); $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 15), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 18), 'Opener scope closer is not the semicolon token'); + $this->assertSame($tokens[$opener]['scope_opener'], ($token + 13), 'Opener scope opener is not the arrow token'); + $this->assertSame($tokens[$opener]['scope_closer'], ($token + 16), 'Opener scope closer is not the semicolon token'); $closer = $tokens[$token]['scope_closer']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 15), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 18), 'Closer scope closer is not the semicolon token'); + $this->assertSame($tokens[$closer]['scope_opener'], ($token + 13), 'Closer scope opener is not the arrow token'); + $this->assertSame($tokens[$closer]['scope_closer'], ($token + 16), 'Closer scope closer is not the semicolon token'); }//end testNullableNamespace() @@ -479,16 +535,16 @@ public function testNamespaceOperatorInTypes() $token = $this->getTargetToken('/* testNamespaceOperatorInTypes */', T_FN); $this->backfillHelper($token); - $this->assertSame($tokens[$token]['scope_opener'], ($token + 16), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 19), 'Scope closer is not the semicolon token'); + $this->assertSame($tokens[$token]['scope_opener'], ($token + 12), 'Scope opener is not the arrow token'); + $this->assertSame($tokens[$token]['scope_closer'], ($token + 15), 'Scope closer is not the semicolon token'); $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 16), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 19), 'Opener scope closer is not the semicolon token'); + $this->assertSame($tokens[$opener]['scope_opener'], ($token + 12), 'Opener scope opener is not the arrow token'); + $this->assertSame($tokens[$opener]['scope_closer'], ($token + 15), 'Opener scope closer is not the semicolon token'); $closer = $tokens[$token]['scope_closer']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 16), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 19), 'Closer scope closer is not the semicolon token'); + $this->assertSame($tokens[$closer]['scope_opener'], ($token + 12), 'Closer scope opener is not the arrow token'); + $this->assertSame($tokens[$closer]['scope_closer'], ($token + 15), 'Closer scope closer is not the semicolon token'); }//end testNamespaceOperatorInTypes() @@ -672,25 +728,37 @@ public function testNestedInMethod() /** - * Verify that "fn" keywords which are not arrow functions get tokenized as T_STRING and don't + * Verify that "fn" keywords which are not arrow functions get tokenized as identifier names and don't * have the extra token array indexes. * - * @param string $testMarker The comment prefacing the target token. - * @param string $testContent The token content to look for. + * @param string $testMarker The comment prefacing the target token. + * @param string $testContent The token content to look for. + * @param string $expectedType Optional. The token type which is expected (not T_FN). + * Defaults to `T_STRING`. * * @dataProvider dataNotAnArrowFunction * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional * * @return void */ - public function testNotAnArrowFunction($testMarker, $testContent='fn') + public function testNotAnArrowFunction($testMarker, $testContent='fn', $expectedType='T_STRING') { $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken($testMarker, [T_STRING, T_FN], $testContent); + $token = $this->getTargetToken( + $testMarker, + [ + T_STRING, + T_FN, + T_NAME_FULLY_QUALIFIED, + T_NAME_QUALIFIED, + T_NAME_RELATIVE, + ], + $testContent + ); $tokenArray = $tokens[$token]; - $this->assertSame('T_STRING', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING'); + $this->assertSame($expectedType, $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING'); $this->assertArrayNotHasKey('scope_condition', $tokenArray, 'Scope condition is set'); $this->assertArrayNotHasKey('scope_opener', $tokenArray, 'Scope opener is set'); @@ -743,9 +811,14 @@ public function dataNotAnArrowFunction() ], [ '/* testNonArrowNamespacedFunctionCall */', - 'Fn', + 'MyNS\Sub\Fn', + 'T_NAME_QUALIFIED', + ], + [ + '/* testNonArrowNamespaceOperatorFunctionCall */', + 'namespace\fn', + 'T_NAME_RELATIVE', ], - ['/* testNonArrowNamespaceOperatorFunctionCall */'], ['/* testNonArrowFunctionNameWithUnionTypes */'], ['/* testLiveCoding */'], ]; diff --git a/tests/Core/Tokenizer/NamespacedNameSingleTokenTest.inc b/tests/Core/Tokenizer/NamespacedNameSingleTokenTest.inc new file mode 100644 index 0000000000..9c3df08b55 --- /dev/null +++ b/tests/Core/Tokenizer/NamespacedNameSingleTokenTest.inc @@ -0,0 +1,154 @@ + + * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; +use PHP_CodeSniffer\Util\Tokens; + +class NamespacedNameSingleTokenTest extends AbstractMethodUnitTest +{ + + + /** + * Test that identifier names are tokenized the same across PHP versions, based on the PHP 8 tokenization. + * + * @param string $testMarker The comment prefacing the test. + * @param array $expectedTokens The tokenization expected. + * + * @dataProvider dataIdentifierTokenization + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testIdentifierTokenization($testMarker, $expectedTokens) + { + $tokens = self::$phpcsFile->getTokens(); + $identifier = $this->getTargetToken($testMarker, constant($expectedTokens[0]['type'])); + + foreach ($expectedTokens as $key => $tokenInfo) { + $this->assertSame( + constant($tokenInfo['type']), + $tokens[$identifier]['code'], + 'Failed asserting that '.$tokens[$identifier]['type'].' is the same as the expected type: '.$tokenInfo['type'] + ); + $this->assertSame($tokenInfo['type'], $tokens[$identifier]['type']); + $this->assertSame($tokenInfo['content'], $tokens[$identifier]['content']); + + ++$identifier; + } + + }//end testIdentifierTokenization() + + + /** + * Data provider. + * + * @see testIdentifierTokenization() + * + * @return array + */ + public function dataIdentifierTokenization() + { + return [ + [ + '/* testNamespaceDeclaration */', + [ + [ + 'type' => 'T_NAMESPACE', + 'content' => 'namespace', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Package', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testNamespaceDeclarationWithLevels */', + [ + [ + 'type' => 'T_NAMESPACE', + 'content' => 'namespace', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_NAME_QUALIFIED', + 'content' => 'Vendor\SubLevel\Domain', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testNamespaceDeclarationWithReservedKeywords */', + [ + [ + 'type' => 'T_NAMESPACE', + 'content' => 'namespace', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_NAME_QUALIFIED', + 'content' => 'For\Include\Fn', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testUseStatement */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'ClassName', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testUseStatementWithLevels */', + [ + [ + 'type' => 'T_NAME_QUALIFIED', + 'content' => 'Vendor\Level\Domain', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testFunctionUseStatement */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'function', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'function_name', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testFunctionUseStatementWithLevels */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'function', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_NAME_QUALIFIED', + 'content' => 'Vendor\Level\function_in_ns', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testConstantUseStatement */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'const', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'CONSTANT_NAME', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testConstantUseStatementWithLevels */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'const', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_NAME_QUALIFIED', + 'content' => 'Vendor\Level\OTHER_CONSTANT', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + + [ + '/* testMultiUseUnqualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'UnqualifiedClassName', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + ], + ], + [ + '/* testMultiUsePartiallyQualified */', + [ + [ + 'type' => 'T_NAME_QUALIFIED', + 'content' => 'Sublevel\PartiallyClassName', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testGroupUseStatement */', + [ + [ + 'type' => 'T_NAME_QUALIFIED', + 'content' => 'Vendor\Level', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_OPEN_USE_GROUP', + 'content' => '{', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'AnotherDomain', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'function', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'function_grouped', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'const', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'CONSTANT_GROUPED', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_NAME_QUALIFIED', + 'content' => 'Sub\YetAnotherDomain', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'function', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_NAME_QUALIFIED', + 'content' => 'SubLevelA\function_grouped_too', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'const', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_NAME_QUALIFIED', + 'content' => 'SubLevelB\CONSTANT_GROUPED_TOO', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_CLOSE_USE_GROUP', + 'content' => '}', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testClassName */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'MyClass', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testExtendedFQN */', + [ + [ + 'type' => 'T_NAME_FULLY_QUALIFIED', + 'content' => '\Vendor\Level\FQN', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testImplementsRelative */', + [ + [ + 'type' => 'T_NAME_RELATIVE', + 'content' => 'namespace\Name', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + ], + ], + [ + '/* testImplementsFQN */', + [ + [ + 'type' => 'T_NAME_FULLY_QUALIFIED', + 'content' => '\Fully\Qualified', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + ], + ], + [ + '/* testImplementsUnqualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'Unqualified', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + ], + ], + [ + '/* testImplementsPartiallyQualifiedWithReservedKeyword */', + [ + [ + 'type' => 'T_NAME_QUALIFIED', + 'content' => 'Exit\Level\Name', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testFunctionName */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'function_name', + ], + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + ], + ], + [ + '/* testTypeDeclarationRelative */', + [ + [ + 'type' => 'T_NAME_RELATIVE', + 'content' => 'namespace\Name', + ], + [ + 'type' => 'T_TYPE_UNION', + 'content' => '|', + ], + [ + 'type' => 'T_STRING', + 'content' => 'object', + ], + ], + ], + [ + '/* testTypeDeclarationFQN */', + [ + [ + 'type' => 'T_NAME_FULLY_QUALIFIED', + 'content' => '\Fully\Qualified\Name', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + ], + ], + [ + '/* testTypeDeclarationUnqualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'Unqualified', + ], + [ + 'type' => 'T_TYPE_UNION', + 'content' => '|', + ], + [ + 'type' => 'T_FALSE', + 'content' => 'false', + ], + ], + ], + [ + '/* testTypeDeclarationPartiallyQualified */', + [ + [ + 'type' => 'T_NULLABLE', + 'content' => '?', + ], + [ + 'type' => 'T_NAME_QUALIFIED', + 'content' => 'Sublevel\Name', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + ], + ], + [ + '/* testReturnTypeFQN */', + [ + [ + 'type' => 'T_NULLABLE', + 'content' => '?', + ], + [ + 'type' => 'T_NAME_FULLY_QUALIFIED', + 'content' => '\Name', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + ], + ], + [ + '/* testFunctionCallRelative */', + [ + [ + 'type' => 'T_NAME_RELATIVE', + 'content' => 'NameSpace\function_name', + ], + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + ], + ], + [ + '/* testFunctionCallFQNWithReservedKeyword */', + [ + [ + 'type' => 'T_NAME_FULLY_QUALIFIED', + 'content' => '\Vendor\Foreach\function_name', + ], + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + ], + ], + [ + '/* testFunctionCallUnqualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'function_name', + ], + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + ], + ], + [ + '/* testFunctionPartiallyQualified */', + [ + [ + 'type' => 'T_NAME_QUALIFIED', + 'content' => 'Level\function_name', + ], + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + ], + ], + [ + '/* testCatchRelative */', + [ + [ + 'type' => 'T_NAME_RELATIVE', + 'content' => 'namespace\SubLevel\Exception', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + ], + ], + [ + '/* testCatchFQN */', + [ + [ + 'type' => 'T_NAME_FULLY_QUALIFIED', + 'content' => '\Exception', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + ], + ], + [ + '/* testCatchUnqualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'Exception', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + ], + ], + [ + '/* testCatchPartiallyQualified */', + [ + [ + 'type' => 'T_NAME_QUALIFIED', + 'content' => 'Level\Exception', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + ], + ], + + [ + '/* testNewRelative */', + [ + [ + 'type' => 'T_NAME_RELATIVE', + 'content' => 'namespace\ClassName', + ], + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + ], + ], + [ + '/* testNewFQN */', + [ + [ + 'type' => 'T_NAME_FULLY_QUALIFIED', + 'content' => '\Vendor\ClassName', + ], + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + ], + ], + [ + '/* testNewUnqualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'ClassName', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testNewPartiallyQualified */', + [ + [ + 'type' => 'T_NAME_QUALIFIED', + 'content' => 'Level\ClassName', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testDoubleColonRelative */', + [ + [ + 'type' => 'T_NAME_RELATIVE', + 'content' => 'namespace\ClassName', + ], + [ + 'type' => 'T_DOUBLE_COLON', + 'content' => '::', + ], + ], + ], + [ + '/* testDoubleColonFQN */', + [ + [ + 'type' => 'T_NAME_FULLY_QUALIFIED', + 'content' => '\ClassName', + ], + [ + 'type' => 'T_DOUBLE_COLON', + 'content' => '::', + ], + ], + ], + [ + '/* testDoubleColonUnqualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'ClassName', + ], + [ + 'type' => 'T_DOUBLE_COLON', + 'content' => '::', + ], + ], + ], + [ + '/* testDoubleColonPartiallyQualified */', + [ + [ + 'type' => 'T_NAME_QUALIFIED', + 'content' => 'Level\ClassName', + ], + [ + 'type' => 'T_DOUBLE_COLON', + 'content' => '::', + ], + ], + ], + [ + '/* testInstanceOfRelative */', + [ + [ + 'type' => 'T_NAME_RELATIVE', + 'content' => 'namespace\ClassName', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testInstanceOfFQN */', + [ + [ + 'type' => 'T_NAME_FULLY_QUALIFIED', + 'content' => '\Full\ClassName', + ], + [ + 'type' => 'T_CLOSE_PARENTHESIS', + 'content' => ')', + ], + ], + ], + [ + '/* testInstanceOfUnqualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'ClassName', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + ], + ], + [ + '/* testInstanceOfPartiallyQualified */', + [ + [ + 'type' => 'T_NAME_QUALIFIED', + 'content' => 'Partially\ClassName', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testInvalidInPHP8Whitespace */', + [ + [ + 'type' => 'T_NAMESPACE', + 'content' => 'namespace', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Sublevel', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'function_name', + ], + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + ], + ], + [ + '/* testInvalidInPHP8Comments */', + [ + [ + 'type' => 'T_NAME_FULLY_QUALIFIED', + 'content' => '\Fully', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_PHPCS_IGNORE', + 'content' => '// phpcs:ignore Stnd.Cat.Sniff -- for reasons +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_NAME_FULLY_QUALIFIED', + 'content' => '\Qualified', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '/* comment */', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_NAME_FULLY_QUALIFIED', + 'content' => '\Name', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testInvalidDoubleBackslash */', + [ + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_NAME_FULLY_QUALIFIED', + 'content' => '\SomeClass', + ], + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + ], + ], + ]; + + }//end dataIdentifierTokenization() + + +}//end class diff --git a/tests/Core/Tokenizer/ShortArrayTest.inc b/tests/Core/Tokenizer/ShortArrayTest.inc index b052ee5681..439c39bf78 100644 --- a/tests/Core/Tokenizer/ShortArrayTest.inc +++ b/tests/Core/Tokenizer/ShortArrayTest.inc @@ -45,6 +45,15 @@ $var ClassName::CONSTANT_NAME[2]; /* testMagicConstantDereferencing */ $var = __FILE__[0]; +/* testNamespacedConstantDereferencing */ +$var = MyNS\MY_CONSTANT[1]; + +/* testFQNNamespacedConstantDereferencing */ +$var = \MY_CONSTANT[1]; + +/* testNamespaceRelativeConstantDereferencing */ +$var = namespace\MY_CONSTANT[1]; + /* testArrayAccessCurlyBraces */ $var = $array{'key'}['key']; diff --git a/tests/Core/Tokenizer/ShortArrayTest.php b/tests/Core/Tokenizer/ShortArrayTest.php index 45f6c4cf88..8e4bf5df98 100644 --- a/tests/Core/Tokenizer/ShortArrayTest.php +++ b/tests/Core/Tokenizer/ShortArrayTest.php @@ -66,6 +66,9 @@ public function dataSquareBrackets() ['/* testConstantDereferencing */'], ['/* testClassConstantDereferencing */'], ['/* testMagicConstantDereferencing */'], + ['/* testNamespacedConstantDereferencing */'], + ['/* testFQNNamespacedConstantDereferencing */'], + ['/* testNamespaceRelativeConstantDereferencing */'], ['/* testArrayAccessCurlyBraces */'], ['/* testArrayLiteralDereferencing */'], ['/* testShortArrayLiteralDereferencing */'],