diff --git a/src/Files/File.php b/src/Files/File.php index c5f85c143b..c5d581f28d 100644 --- a/src/Files/File.php +++ b/src/Files/File.php @@ -1556,17 +1556,19 @@ public function getMethodParameters($stackPtr) * The format of the return value is: * * array( - * 'scope' => 'public', // Public, private, or protected - * 'scope_specified' => true, // TRUE if the scope keyword was found. - * 'return_type' => '', // The return type of the method. - * 'return_type_token' => integer, // The stack pointer to the start of the return type - * // or FALSE if there is no return type. - * 'nullable_return_type' => false, // TRUE if the return type is preceded by the - * // nullability operator. - * 'is_abstract' => false, // TRUE if the abstract keyword was found. - * 'is_final' => false, // TRUE if the final keyword was found. - * 'is_static' => false, // TRUE if the static keyword was found. - * 'has_body' => false, // TRUE if the method has a body + * 'scope' => 'public', // Public, private, or protected + * 'scope_specified' => true, // TRUE if the scope keyword was found. + * 'return_type' => '', // The return type of the method. + * 'return_type_token' => integer, // The stack pointer to the start of the return type + * // or FALSE if there is no return type. + * 'return_type_end_token' => integer, // The stack pointer to the end of the return type + * // or FALSE if there is no return type. + * 'nullable_return_type' => false, // TRUE if the return type is preceded by the + * // nullability operator. + * 'is_abstract' => false, // TRUE if the abstract keyword was found. + * 'is_final' => false, // TRUE if the final keyword was found. + * 'is_static' => false, // TRUE if the static keyword was found. + * 'has_body' => false, // TRUE if the method has a body * ); * * @@ -1645,6 +1647,7 @@ public function getMethodProperties($stackPtr) $returnType = ''; $returnTypeToken = false; + $returnTypeEndToken = false; $nullableReturnType = false; $hasBody = true; @@ -1684,9 +1687,10 @@ public function getMethodProperties($stackPtr) $returnTypeToken = $i; } - $returnType .= $this->tokens[$i]['content']; + $returnType .= $this->tokens[$i]['content']; + $returnTypeEndToken = $i; } - } + }//end for if ($this->tokens[$stackPtr]['code'] === T_FN) { $bodyToken = T_FN_ARROW; @@ -1703,15 +1707,16 @@ public function getMethodProperties($stackPtr) } return [ - 'scope' => $scope, - 'scope_specified' => $scopeSpecified, - 'return_type' => $returnType, - 'return_type_token' => $returnTypeToken, - 'nullable_return_type' => $nullableReturnType, - 'is_abstract' => $isAbstract, - 'is_final' => $isFinal, - 'is_static' => $isStatic, - 'has_body' => $hasBody, + 'scope' => $scope, + 'scope_specified' => $scopeSpecified, + 'return_type' => $returnType, + 'return_type_token' => $returnTypeToken, + 'return_type_end_token' => $returnTypeEndToken, + 'nullable_return_type' => $nullableReturnType, + 'is_abstract' => $isAbstract, + 'is_final' => $isFinal, + 'is_static' => $isStatic, + 'has_body' => $hasBody, ]; }//end getMethodProperties() diff --git a/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php b/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php index 27ef5ecac9..a738c30d66 100644 --- a/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php @@ -9,6 +9,7 @@ namespace PHP_CodeSniffer\Standards\Generic\Sniffs\PHP; +use PHP_CodeSniffer\Exceptions\RuntimeException; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; use PHP_CodeSniffer\Util\Tokens; @@ -16,6 +17,29 @@ class LowerCaseTypeSniff implements Sniff { + /** + * Native types supported by PHP. + * + * @var array + */ + private $phpTypes = [ + 'self' => true, + 'parent' => true, + 'array' => true, + 'callable' => true, + 'bool' => true, + 'float' => true, + 'int' => true, + 'string' => true, + 'iterable' => true, + 'void' => true, + 'object' => true, + 'mixed' => true, + 'static' => true, + 'false' => true, + 'null' => true, + ]; + /** * Returns an array of tokens this test wants to listen for. @@ -27,6 +51,7 @@ public function register() $tokens = Tokens::$castTokens; $tokens[] = T_FUNCTION; $tokens[] = T_CLOSURE; + $tokens[] = T_VARIABLE; return $tokens; }//end register() @@ -47,76 +72,81 @@ public function process(File $phpcsFile, $stackPtr) if (isset(Tokens::$castTokens[$tokens[$stackPtr]['code']]) === true) { // A cast token. - if (strtolower($tokens[$stackPtr]['content']) !== $tokens[$stackPtr]['content']) { - if ($tokens[$stackPtr]['content'] === strtoupper($tokens[$stackPtr]['content'])) { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'upper'); - } else { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'mixed'); - } + $this->processType( + $phpcsFile, + $stackPtr, + $tokens[$stackPtr]['content'], + 'PHP type casts must be lowercase; expected "%s" but found "%s"', + 'TypeCastFound' + ); + + return; + } + + /* + * Check property types. + */ - $error = 'PHP type casts must be lowercase; expected "%s" but found "%s"'; - $data = [ - strtolower($tokens[$stackPtr]['content']), - $tokens[$stackPtr]['content'], - ]; + if ($tokens[$stackPtr]['code'] === T_VARIABLE) { + try { + $props = $phpcsFile->getMemberProperties($stackPtr); + } catch (RuntimeException $e) { + // Not an OO property. + return; + } + + // Strip off potential nullable indication. + $type = ltrim($props['type'], '?'); - $fix = $phpcsFile->addFixableError($error, $stackPtr, 'TypeCastFound', $data); - if ($fix === true) { - $phpcsFile->fixer->replaceToken($stackPtr, strtolower($tokens[$stackPtr]['content'])); + if ($type !== '') { + $error = 'PHP property type declarations must be lowercase; expected "%s" but found "%s"'; + $errorCode = 'PropertyTypeFound'; + + if (strpos($type, '|') !== false) { + $this->processUnionType( + $phpcsFile, + $props['type_token'], + $props['type_end_token'], + $error, + $errorCode + ); + } else if (isset($this->phpTypes[strtolower($type)]) === true) { + $this->processType($phpcsFile, $props['type_token'], $type, $error, $errorCode); } - } else { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'lower'); - }//end if + } return; }//end if - $phpTypes = [ - 'self' => true, - 'parent' => true, - 'array' => true, - 'callable' => true, - 'bool' => true, - 'float' => true, - 'int' => true, - 'string' => true, - 'iterable' => true, - 'void' => true, - 'object' => true, - ]; + /* + * Check function return type. + */ $props = $phpcsFile->getMethodProperties($stackPtr); // Strip off potential nullable indication. - $returnType = ltrim($props['return_type'], '?'); - $returnTypeLower = strtolower($returnType); - - if ($returnType !== '' - && isset($phpTypes[$returnTypeLower]) === true - ) { - // A function return type. - if ($returnTypeLower !== $returnType) { - if ($returnType === strtoupper($returnType)) { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'upper'); - } else { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'mixed'); - } + $returnType = ltrim($props['return_type'], '?'); - $error = 'PHP return type declarations must be lowercase; expected "%s" but found "%s"'; - $token = $props['return_type_token']; - $data = [ - $returnTypeLower, - $returnType, - ]; + if ($returnType !== '') { + $error = 'PHP return type declarations must be lowercase; expected "%s" but found "%s"'; + $errorCode = 'ReturnTypeFound'; - $fix = $phpcsFile->addFixableError($error, $token, 'ReturnTypeFound', $data); - if ($fix === true) { - $phpcsFile->fixer->replaceToken($token, $returnTypeLower); - } - } else { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'lower'); - }//end if - }//end if + if (strpos($returnType, '|') !== false) { + $this->processUnionType( + $phpcsFile, + $props['return_type_token'], + $props['return_type_end_token'], + $error, + $errorCode + ); + } else if (isset($this->phpTypes[strtolower($returnType)]) === true) { + $this->processType($phpcsFile, $props['return_type_token'], $returnType, $error, $errorCode); + } + } + + /* + * Check function parameter types. + */ $params = $phpcsFile->getMethodParameters($stackPtr); if (empty($params) === true) { @@ -125,38 +155,114 @@ public function process(File $phpcsFile, $stackPtr) foreach ($params as $param) { // Strip off potential nullable indication. - $typeHint = ltrim($param['type_hint'], '?'); - $typeHintLower = strtolower($typeHint); - - if ($typeHint !== '' - && isset($phpTypes[$typeHintLower]) === true - ) { - // A function return type. - if ($typeHintLower !== $typeHint) { - if ($typeHint === strtoupper($typeHint)) { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'upper'); - } else { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'mixed'); - } - - $error = 'PHP parameter type declarations must be lowercase; expected "%s" but found "%s"'; - $token = $param['type_hint_token']; - $data = [ - $typeHintLower, - $typeHint, - ]; - - $fix = $phpcsFile->addFixableError($error, $token, 'ParamTypeFound', $data); - if ($fix === true) { - $phpcsFile->fixer->replaceToken($token, $typeHintLower); - } - } else { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'lower'); - }//end if - }//end if + $typeHint = ltrim($param['type_hint'], '?'); + + if ($typeHint !== '') { + $error = 'PHP parameter type declarations must be lowercase; expected "%s" but found "%s"'; + $errorCode = 'ParamTypeFound'; + + if (strpos($typeHint, '|') !== false) { + $this->processUnionType( + $phpcsFile, + $param['type_hint_token'], + $param['type_hint_end_token'], + $error, + $errorCode + ); + } else if (isset($this->phpTypes[strtolower($typeHint)]) === true) { + $this->processType($phpcsFile, $param['type_hint_token'], $typeHint, $error, $errorCode); + } + } }//end foreach }//end process() + /** + * Processes a union type declaration. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $typeDeclStart The position of the start of the type token. + * @param int $typeDeclEnd The position of the end of the type token. + * @param string $error Error message template. + * @param string $errorCode The error code. + * + * @return void + */ + protected function processUnionType(File $phpcsFile, $typeDeclStart, $typeDeclEnd, $error, $errorCode) + { + $tokens = $phpcsFile->getTokens(); + $current = $typeDeclStart; + + do { + $endOfType = $phpcsFile->findNext(T_TYPE_UNION, $current, $typeDeclEnd); + if ($endOfType === false) { + // This must be the last type in the union. + $endOfType = ($typeDeclEnd + 1); + } + + $hasNsSep = $phpcsFile->findNext(T_NS_SEPARATOR, $current, $endOfType); + if ($hasNsSep !== false) { + // Multi-token class based type. Ignore. + $current = ($endOfType + 1); + continue; + } + + // Type consisting of a single token. + $startOfType = $phpcsFile->findNext(Tokens::$emptyTokens, $current, $endOfType, true); + if ($startOfType === false) { + // Parse error. + return; + } + + $type = $tokens[$startOfType]['content']; + if (isset($this->phpTypes[strtolower($type)]) === true) { + $this->processType($phpcsFile, $startOfType, $type, $error, $errorCode); + } + + $current = ($endOfType + 1); + } while ($current <= $typeDeclEnd); + + }//end processUnionType() + + + /** + * Processes a type cast or a singular type declaration. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the type token. + * @param string $type The type found. + * @param string $error Error message template. + * @param string $errorCode The error code. + * + * @return void + */ + protected function processType(File $phpcsFile, $stackPtr, $type, $error, $errorCode) + { + $typeLower = strtolower($type); + + if ($typeLower === $type) { + $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'lower'); + return; + } + + if ($type === strtoupper($type)) { + $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'upper'); + } else { + $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'mixed'); + } + + $data = [ + $typeLower, + $type, + ]; + + $fix = $phpcsFile->addFixableError($error, $stackPtr, $errorCode, $data); + if ($fix === true) { + $phpcsFile->fixer->replaceToken($stackPtr, $typeLower); + } + + }//end processType() + + }//end class diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc index f4e0dd4628..28e696ccc9 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc +++ b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc @@ -15,7 +15,7 @@ $foo = (Int) $bar; $foo = (INTEGER) $bar; $foo = (BOOL) $bar; $foo = (String) $bar; -$foo = (Array) $bar; +$foo = ( Array ) $bar; function foo(int $a, string $b, bool $c, array $d, Foo\Bar $e) : int {} function foo(Int $a, String $b, BOOL $c, Array $d, Foo\Bar $e) : Foo\Bar {} @@ -45,3 +45,31 @@ $foo = function (?Int $a, ? Callable $b) $var = (BInARY) $string; $var = (binary)$string; + +function unionParamTypesA (bool|array| /* nullability operator not allowed in union */ NULL $var) {} + +function unionParamTypesB (\Package\ClassName | Int | \Package\Other_Class | FALSE $var) {} + +function unionReturnTypesA ($var): bool|array| /* nullability operator not allowed in union */ NULL {} + +function unionReturnTypesB ($var): \Package\ClassName | Int | \Package\Other_Class | FALSE {} + +class TypedProperties +{ + protected ClassName $class; + public Int $int; + private ?BOOL $bool; + public Self $self; + protected PaRenT $parent; + private ARRAY $array; + public Float $float; + protected ?STRING $string; + private IterablE $iterable; + public Object $object; + protected Mixed $mixed; + + public Iterable|FALSE|NULL $unionTypeA; + protected SELF|Parent /* comment */ |\Fully\Qualified\ClassName|UnQualifiedClass $unionTypeB; + private ClassName|/*comment*/Float|STRING|False $unionTypeC; + public sTRing | aRRaY | FaLSe $unionTypeD; +} diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc.fixed b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc.fixed index a64a4400d7..302c0faf0a 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc.fixed @@ -15,7 +15,7 @@ $foo = (int) $bar; $foo = (integer) $bar; $foo = (bool) $bar; $foo = (string) $bar; -$foo = (array) $bar; +$foo = ( array ) $bar; function foo(int $a, string $b, bool $c, array $d, Foo\Bar $e) : int {} function foo(int $a, string $b, bool $c, array $d, Foo\Bar $e) : Foo\Bar {} @@ -45,3 +45,31 @@ $foo = function (?int $a, ? callable $b) $var = (binary) $string; $var = (binary)$string; + +function unionParamTypesA (bool|array| /* nullability operator not allowed in union */ null $var) {} + +function unionParamTypesB (\Package\ClassName | int | \Package\Other_Class | false $var) {} + +function unionReturnTypesA ($var): bool|array| /* nullability operator not allowed in union */ null {} + +function unionReturnTypesB ($var): \Package\ClassName | int | \Package\Other_Class | false {} + +class TypedProperties +{ + protected ClassName $class; + public int $int; + private ?bool $bool; + public self $self; + protected parent $parent; + private array $array; + public float $float; + protected ?string $string; + private iterable $iterable; + public object $object; + protected mixed $mixed; + + public iterable|false|null $unionTypeA; + protected self|parent /* comment */ |\Fully\Qualified\ClassName|UnQualifiedClass $unionTypeB; + private ClassName|/*comment*/float|string|false $unionTypeC; + public string | array | false $unionTypeD; +} diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php index 0a5f5e03ea..005c8280d9 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php +++ b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php @@ -45,6 +45,24 @@ public function getErrorList() 43 => 2, 44 => 1, 46 => 1, + 49 => 1, + 51 => 2, + 53 => 1, + 55 => 2, + 60 => 1, + 61 => 1, + 62 => 1, + 63 => 1, + 64 => 1, + 65 => 1, + 66 => 1, + 67 => 1, + 68 => 1, + 69 => 1, + 71 => 3, + 72 => 2, + 73 => 3, + 74 => 3, ]; }//end getErrorList()