diff --git a/custom-standards/Flyeralarm/Sniffs/Docblock/ReturnTypeSniff.php b/custom-standards/Flyeralarm/Sniffs/Docblock/ReturnTypeSniff.php index 6271637..f7fdb22 100644 --- a/custom-standards/Flyeralarm/Sniffs/Docblock/ReturnTypeSniff.php +++ b/custom-standards/Flyeralarm/Sniffs/Docblock/ReturnTypeSniff.php @@ -58,22 +58,13 @@ public function process(File $phpcsFile, $stackPtr) $tokens = $phpcsFile->getTokens(); $returnTypePtr = $this->getDocReturnTypePtr($phpcsFile, $stackPtr); $returnTypeString = $tokens[$returnTypePtr]['content']; - $returnTypes = explode('|', $returnTypeString); - - foreach ($returnTypes as $returnType) { - $returnType = trim($returnType); - if (in_array($returnType, $this->returnTypeScalarWhitelist, true)) { - continue; - } - if (in_array($returnType, $this->returnTypeClassWhitelist, true)) { - continue; - } - if ($this->isStartingWithUppercaseLetter($returnType)) { - continue; - } + try { + $this->checkReturnTypeShape($returnTypeString); + } + catch (\InvalidArgumentException $exception) { $phpcsFile->addError( - sprintf('Return type "%s" is discouraged', $returnType), + $exception->getMessage(), $returnTypePtr, 'ProhibitedReturnType' ); @@ -145,4 +136,55 @@ private function isStartingWithUppercaseLetter($singleCharacter) return false; } + + /** + * @param string $subject The return type string to check. + * @return void + */ + private function checkReturnTypeShape(string $subject) + { + $matched = preg_match_all('#(?\s*\|\s*)?(?[^<>\|]+)(?<(?.*)>)?#', $subject, $matches); + + if (!$matched || implode('', $matches[0]) !== $subject) { + throw new \InvalidArgumentException('Invalid structure in return type "' . $subject . '"'); + } + + if (strpos($matches['separator'][0], '|') !== false) { + throw new \InvalidArgumentException('Missing return type in first alternative of type "' . $subject . '"'); + } + + foreach ($matches['nested'] as $index => $match) { + if ($matches['generic'][$index] === '') { + if (trim($matches['atom'][$index]) !== 'array') { + throw new \InvalidArgumentException('Unexpected generic specification in type "' . $matches[0][$index] . '"'); + } + + $match = trim($match); + if (strpos($match, 'int,') === 0) { + // Allow numeric indexing in generics, e.g. `array` + $match = substr($match, 4); + } + + if ($match === '') { + throw new \InvalidArgumentException('Generic specification may not be empty in type "' . $matches[0][$index] . '"'); + } + + $this->checkReturnTypeShape($match); + } + + // Check if atom is in whitelist. + $returnType = trim($matches['atom'][$index]); + if (in_array($returnType, $this->returnTypeScalarWhitelist, true)) { + continue; + } + if (in_array($returnType, $this->returnTypeClassWhitelist, true)) { + continue; + } + if ($this->isStartingWithUppercaseLetter($returnType)) { + continue; + } + + throw new \InvalidArgumentException('Return type "' . $returnType . '" is discouraged'); + } + } } diff --git a/tests/rules/doc/allowed/ReturnTypeArrayGenerics.php b/tests/rules/doc/allowed/ReturnTypeArrayGenerics.php new file mode 100644 index 0000000..93082b6 --- /dev/null +++ b/tests/rules/doc/allowed/ReturnTypeArrayGenerics.php @@ -0,0 +1,57 @@ + + */ + public function testWithGeneric() + { + } + + /** + * @return array + */ + public function testWithNumericIndex() + { + } + + /** + * @return array + */ + public function testWithAlternativeInside() + { + } + + /** + * @return int | array> + */ + public function testWithAlternativeOutside() + { + } + + /** + * @return array> | null + */ + public function testWithAlternativeAtTheEnd() + { + } + + /** + * @return int | array> + */ + public function testWithMultipleAlternatives() + { + } +} diff --git a/tests/rules/doc/not-allowed/EmptyAlternativeInReturnTypeInDocComment.php b/tests/rules/doc/not-allowed/EmptyAlternativeInReturnTypeInDocComment.php new file mode 100644 index 0000000..f1cfe5d --- /dev/null +++ b/tests/rules/doc/not-allowed/EmptyAlternativeInReturnTypeInDocComment.php @@ -0,0 +1,14 @@ +" + +class EmptyGenericReturnTypeInDocComment +{ + /** + * @return array<> + */ + public function foo() + { + return true; + } +} diff --git a/tests/rules/doc/not-allowed/InvalidStructureInReturnTypeInDocComment.php b/tests/rules/doc/not-allowed/InvalidStructureInReturnTypeInDocComment.php new file mode 100644 index 0000000..f446ab9 --- /dev/null +++ b/tests/rules/doc/not-allowed/InvalidStructureInReturnTypeInDocComment.php @@ -0,0 +1,14 @@ +" + +class UnknownGenericReturnTypeInDocComment +{ + /** + * @return int + */ + public function foo() + { + return true; + } +} diff --git a/tests/rules/doc/not-allowed/UnknownGenericSubReturnTypeInDocComment.php b/tests/rules/doc/not-allowed/UnknownGenericSubReturnTypeInDocComment.php new file mode 100644 index 0000000..769521f --- /dev/null +++ b/tests/rules/doc/not-allowed/UnknownGenericSubReturnTypeInDocComment.php @@ -0,0 +1,14 @@ + + */ + public function foo() + { + return true; + } +}