diff --git a/SlevomatCodingStandard/Sniffs/TypeHints/DisallowArrayTypeHintSyntaxSniff.php b/SlevomatCodingStandard/Sniffs/TypeHints/DisallowArrayTypeHintSyntaxSniff.php index c7019cb7d..772f8834a 100644 --- a/SlevomatCodingStandard/Sniffs/TypeHints/DisallowArrayTypeHintSyntaxSniff.php +++ b/SlevomatCodingStandard/Sniffs/TypeHints/DisallowArrayTypeHintSyntaxSniff.php @@ -9,19 +9,28 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; +use SlevomatCodingStandard\Helpers\Annotation\Annotation; use SlevomatCodingStandard\Helpers\Annotation\GenericAnnotation; +use SlevomatCodingStandard\Helpers\Annotation\ParameterAnnotation; +use SlevomatCodingStandard\Helpers\Annotation\ReturnAnnotation; use SlevomatCodingStandard\Helpers\AnnotationHelper; use SlevomatCodingStandard\Helpers\AnnotationTypeHelper; +use SlevomatCodingStandard\Helpers\FunctionHelper; use SlevomatCodingStandard\Helpers\NamespaceHelper; +use SlevomatCodingStandard\Helpers\ReturnTypeHint; use SlevomatCodingStandard\Helpers\SniffSettingsHelper; +use SlevomatCodingStandard\Helpers\TokenHelper; use SlevomatCodingStandard\Helpers\TypeHintHelper; use function array_flip; use function array_key_exists; use function array_map; +use function array_merge; use function count; use function in_array; use function sprintf; use const T_DOC_COMMENT_OPEN_TAG; +use const T_FUNCTION; +use const T_VARIABLE; class DisallowArrayTypeHintSyntaxSniff implements Sniff { @@ -79,16 +88,18 @@ public function process(File $phpcsFile, $docCommentOpenPointer): void $unionTypeNode = $this->findUnionTypeThatContainsArrayType($arrayTypeNode, $unionTypeNodes); if ($unionTypeNode !== null) { - $genericIdentifier = $this->findGenericIdentifier($phpcsFile, $annotation->getStartPointer(), $unionTypeNode); + $genericIdentifier = $this->findGenericIdentifier($phpcsFile, $annotation->getStartPointer(), $unionTypeNode, $annotation); if ($genericIdentifier !== null) { - $genericTypeNode = new GenericTypeNode(new IdentifierTypeNode($genericIdentifier), [$arrayTypeNode->type]); + $genericTypeNode = new GenericTypeNode(new IdentifierTypeNode($genericIdentifier), [$this->fixArrayNode($arrayTypeNode->type)]); $fixedAnnotationContent = AnnotationHelper::fixAnnotation($phpcsFile, $annotation, $unionTypeNode, $genericTypeNode); } else { - $genericTypeNode = new GenericTypeNode(new IdentifierTypeNode('array'), [$arrayTypeNode->type]); + $genericTypeNode = new GenericTypeNode(new IdentifierTypeNode('array'), [$this->fixArrayNode($arrayTypeNode->type)]); $fixedAnnotationContent = AnnotationHelper::fixAnnotation($phpcsFile, $annotation, $arrayTypeNode, $genericTypeNode); } } else { - $genericTypeNode = new GenericTypeNode(new IdentifierTypeNode('array'), [$arrayTypeNode->type]); + $genericIdentifier = $this->findGenericIdentifier($phpcsFile, $annotation->getStartPointer(), $arrayTypeNode, $annotation) ?? 'array'; + + $genericTypeNode = new GenericTypeNode(new IdentifierTypeNode($genericIdentifier), [$this->fixArrayNode($arrayTypeNode->type)]); $fixedAnnotationContent = AnnotationHelper::fixAnnotation($phpcsFile, $annotation, $arrayTypeNode, $genericTypeNode); } @@ -104,6 +115,15 @@ public function process(File $phpcsFile, $docCommentOpenPointer): void } } + private function fixArrayNode(TypeNode $node): TypeNode + { + if (!$node instanceof ArrayTypeNode) { + return $node; + } + + return new GenericTypeNode(new IdentifierTypeNode('array'), [$this->fixArrayNode($node->type)]); + } + /** * @param \PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode $arrayTypeNode * @param \PHPStan\PhpDocParser\Ast\Type\UnionTypeNode[] $unionTypeNodes @@ -120,26 +140,51 @@ private function findUnionTypeThatContainsArrayType(ArrayTypeNode $arrayTypeNode return null; } - private function findGenericIdentifier(File $phpcsFile, int $pointer, UnionTypeNode $unionTypeNode): ?string + private function findGenericIdentifier(File $phpcsFile, int $pointer, TypeNode $typeNode, Annotation $annotation): ?string { - if (count($unionTypeNode->types) !== 2) { + if (!$typeNode instanceof UnionTypeNode) { + $docCommentOwnerPointer = TokenHelper::findNext($phpcsFile, array_merge(TokenHelper::$functionTokenCodes, TokenHelper::$typeKeywordTokenCodes, [T_VARIABLE]), $pointer); + + if ($phpcsFile->getTokens()[$docCommentOwnerPointer]['code'] !== T_FUNCTION) { + return null; + } + + switch (true) { + case $annotation instanceof ParameterAnnotation: + $typeHints = FunctionHelper::getParametersTypeHints($phpcsFile, $docCommentOwnerPointer); + if (isset($typeHints[$annotation->getContentNode()->parameterName])) { + return $typeHints[$annotation->getContentNode()->parameterName]->getTypeHint(); + } + break; + case $annotation instanceof ReturnAnnotation: + $returnType = FunctionHelper::findReturnTypeHint($phpcsFile, $docCommentOwnerPointer); + if ($returnType instanceof ReturnTypeHint) { + return $returnType->getTypeHint(); + } + break; + } + + return null; + } + + if (count($typeNode->types) !== 2) { return null; } if ( - $unionTypeNode->types[0] instanceof ArrayTypeNode - && $unionTypeNode->types[1] instanceof IdentifierTypeNode - && $this->isTraversableType(TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $pointer, $unionTypeNode->types[1]->name)) + $typeNode->types[0] instanceof ArrayTypeNode + && $typeNode->types[1] instanceof IdentifierTypeNode + && $this->isTraversableType(TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $pointer, $typeNode->types[1]->name)) ) { - return $unionTypeNode->types[1]->name; + return $typeNode->types[1]->name; } if ( - $unionTypeNode->types[1] instanceof ArrayTypeNode - && $unionTypeNode->types[0] instanceof IdentifierTypeNode - && $this->isTraversableType(TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $pointer, $unionTypeNode->types[0]->name)) + $typeNode->types[1] instanceof ArrayTypeNode + && $typeNode->types[0] instanceof IdentifierTypeNode + && $this->isTraversableType(TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $pointer, $typeNode->types[0]->name)) ) { - return $unionTypeNode->types[0]->name; + return $typeNode->types[0]->name; } return null; diff --git a/tests/Sniffs/TypeHints/DisallowArrayTypeHintSyntaxSniffTest.php b/tests/Sniffs/TypeHints/DisallowArrayTypeHintSyntaxSniffTest.php index c51b54531..e85b1a907 100644 --- a/tests/Sniffs/TypeHints/DisallowArrayTypeHintSyntaxSniffTest.php +++ b/tests/Sniffs/TypeHints/DisallowArrayTypeHintSyntaxSniffTest.php @@ -21,7 +21,7 @@ public function testErrors(): void ], ]); - self::assertSame(17, $report->getErrorCount()); + self::assertSame(21, $report->getErrorCount()); self::assertSniffError($report, 6, DisallowArrayTypeHintSyntaxSniff::CODE_DISALLOWED_ARRAY_TYPE_HINT_SYNTAX, 'Usage of array type hint syntax in "\DateTimeImmutable[]" is disallowed, use generic type hint syntax instead.'); self::assertSniffError($report, 7, DisallowArrayTypeHintSyntaxSniff::CODE_DISALLOWED_ARRAY_TYPE_HINT_SYNTAX, 'Usage of array type hint syntax in "bool[]" is disallowed, use generic type hint syntax instead.'); @@ -40,6 +40,10 @@ public function testErrors(): void self::assertSniffError($report, 36, DisallowArrayTypeHintSyntaxSniff::CODE_DISALLOWED_ARRAY_TYPE_HINT_SYNTAX, 'Usage of array type hint syntax in "int[]" is disallowed, use generic type hint syntax instead.'); self::assertSniffError($report, 39, DisallowArrayTypeHintSyntaxSniff::CODE_DISALLOWED_ARRAY_TYPE_HINT_SYNTAX, 'Usage of array type hint syntax in "string[]" is disallowed, use generic type hint syntax instead.'); self::assertSniffError($report, 42, DisallowArrayTypeHintSyntaxSniff::CODE_DISALLOWED_ARRAY_TYPE_HINT_SYNTAX, 'Usage of array type hint syntax in "int[]" is disallowed, use generic type hint syntax instead.'); + self::assertSniffError($report, 45, DisallowArrayTypeHintSyntaxSniff::CODE_DISALLOWED_ARRAY_TYPE_HINT_SYNTAX, 'Usage of array type hint syntax in "string[][]" is disallowed, use generic type hint syntax instead.'); + self::assertSniffError($report, 49, DisallowArrayTypeHintSyntaxSniff::CODE_DISALLOWED_ARRAY_TYPE_HINT_SYNTAX, 'Usage of array type hint syntax in "string[][]" is disallowed, use generic type hint syntax instead.'); + self::assertSniffError($report, 53, DisallowArrayTypeHintSyntaxSniff::CODE_DISALLOWED_ARRAY_TYPE_HINT_SYNTAX, 'Usage of array type hint syntax in "string[][]" is disallowed, use generic type hint syntax instead.'); + self::assertSniffError($report, 57, DisallowArrayTypeHintSyntaxSniff::CODE_DISALLOWED_ARRAY_TYPE_HINT_SYNTAX, 'Usage of array type hint syntax in "string[][]" is disallowed, use generic type hint syntax instead.'); self::assertAllFixedInFile($report); } diff --git a/tests/Sniffs/TypeHints/data/disallowArrayTypeHintSyntaxErrors.fixed.php b/tests/Sniffs/TypeHints/data/disallowArrayTypeHintSyntaxErrors.fixed.php index 150427b04..7810b2b6e 100644 --- a/tests/Sniffs/TypeHints/data/disallowArrayTypeHintSyntaxErrors.fixed.php +++ b/tests/Sniffs/TypeHints/data/disallowArrayTypeHintSyntaxErrors.fixed.php @@ -42,4 +42,20 @@ class Whatever /** @var array|string */ public $k; + /** @return iterable> */ + public function l() : iterable { + } + + /** @param iterable> $n */ + public function m(iterable $n) { + } + + /** @return \ArrayObject> */ + public function o() : ArrayObject { + } + + /** @param \ArrayObject> $p */ + public function p(ArrayObject $q) { + } + } diff --git a/tests/Sniffs/TypeHints/data/disallowArrayTypeHintSyntaxErrors.php b/tests/Sniffs/TypeHints/data/disallowArrayTypeHintSyntaxErrors.php index 0af3d8fc9..4eece6037 100644 --- a/tests/Sniffs/TypeHints/data/disallowArrayTypeHintSyntaxErrors.php +++ b/tests/Sniffs/TypeHints/data/disallowArrayTypeHintSyntaxErrors.php @@ -42,4 +42,20 @@ class Whatever /** @var int[]|string */ public $k; + /** @return string[][] */ + public function l() : iterable { + } + + /** @param string[][] $n */ + public function m(iterable $n) { + } + + /** @return string[][]|\ArrayObject */ + public function o() : ArrayObject { + } + + /** @param string[][]|\ArrayObject $p */ + public function p(ArrayObject $q) { + } + }