<?php declare(strict_types = 1);

namespace SlevomatCodingStandard\Sniffs\TypeHints;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use SlevomatCodingStandard\Helpers\Annotation;
use SlevomatCodingStandard\Helpers\AnnotationHelper;
use SlevomatCodingStandard\Helpers\AnnotationTypeHelper;
use SlevomatCodingStandard\Helpers\DocCommentHelper;
use SlevomatCodingStandard\Helpers\FixerHelper;
use SlevomatCodingStandard\Helpers\NamespaceHelper;
use SlevomatCodingStandard\Helpers\PropertyHelper;
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
use SlevomatCodingStandard\Helpers\SuppressHelper;
use SlevomatCodingStandard\Helpers\TokenHelper;
use SlevomatCodingStandard\Helpers\TypeHint;
use SlevomatCodingStandard\Helpers\TypeHintHelper;
use function array_map;
use function array_push;
use function array_unique;
use function array_values;
use function count;
use function current;
use function implode;
use function in_array;
use function sprintf;
use function strtolower;
use const T_AS;
use const T_COMMA;
use const T_CONST;
use const T_DOC_COMMENT_CLOSE_TAG;
use const T_DOC_COMMENT_STAR;
use const T_FINAL;
use const T_FUNCTION;
use const T_PRIVATE;
use const T_PROTECTED;
use const T_PUBLIC;
use const T_SEMICOLON;
use const T_STATIC;
use const T_VAR;
use const T_VARIABLE;

class PropertyTypeHintSniff implements Sniff
{

	public const CODE_MISSING_ANY_TYPE_HINT = 'MissingAnyTypeHint';

	public const CODE_MISSING_NATIVE_TYPE_HINT = 'MissingNativeTypeHint';

	public const CODE_MISSING_TRAVERSABLE_TYPE_HINT_SPECIFICATION = 'MissingTraversableTypeHintSpecification';

	public const CODE_USELESS_ANNOTATION = 'UselessAnnotation';

	public const CODE_USELESS_SUPPRESS = 'UselessSuppress';

	private const NAME = 'SlevomatCodingStandard.TypeHints.PropertyTypeHint';

	public ?bool $enableNativeTypeHint = null;

	public ?bool $enableMixedTypeHint = null;

	public ?bool $enableUnionTypeHint = null;

	public ?bool $enableIntersectionTypeHint = null;

	public ?bool $enableStandaloneNullTrueFalseTypeHints = null;

	/** @var list<string> */
	public array $traversableTypeHints = [];

	/** @var list<string>|null */
	private ?array $normalizedTraversableTypeHints = null;

	/**
	 * @return array<int, (int|string)>
	 */
	public function register(): array
	{
		// Other modifiers cannot be used without type hint
		return [
			T_VAR,
			T_PUBLIC,
			T_PROTECTED,
			T_PRIVATE,
			T_STATIC,
			T_FINAL,
		];
	}

	/**
	 * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
	 * @param int $pointer
	 */
	public function process(File $phpcsFile, $pointer): void
	{
		$this->enableNativeTypeHint = SniffSettingsHelper::isEnabledByPhpVersion($this->enableNativeTypeHint, 70400);
		$this->enableMixedTypeHint = $this->enableNativeTypeHint
			? SniffSettingsHelper::isEnabledByPhpVersion($this->enableMixedTypeHint, 80000)
			: false;
		$this->enableUnionTypeHint = $this->enableNativeTypeHint
			? SniffSettingsHelper::isEnabledByPhpVersion($this->enableUnionTypeHint, 80000)
			: false;
		$this->enableIntersectionTypeHint = $this->enableNativeTypeHint
			? SniffSettingsHelper::isEnabledByPhpVersion($this->enableIntersectionTypeHint, 80100)
			: false;
		$this->enableStandaloneNullTrueFalseTypeHints = $this->enableNativeTypeHint
			? SniffSettingsHelper::isEnabledByPhpVersion($this->enableStandaloneNullTrueFalseTypeHints, 80200)
			: false;

		$tokens = $phpcsFile->getTokens();

		$asPointer = TokenHelper::findPreviousEffective($phpcsFile, $pointer - 1);
		if ($tokens[$asPointer]['code'] === T_AS) {
			return;
		}

		$nextPointer = TokenHelper::findNextEffective($phpcsFile, $pointer + 1);
		if (in_array($tokens[$nextPointer]['code'], TokenHelper::PROPERTY_MODIFIERS_TOKEN_CODES, true)) {
			// We don't want to report the same property multiple times
			return;
		}

		$propertyPointer = TokenHelper::findNext($phpcsFile, [T_FUNCTION, T_CONST, T_VARIABLE], $pointer + 1);

		if ($propertyPointer === null || $tokens[$propertyPointer]['code'] !== T_VARIABLE) {
			return;
		}

		if (!PropertyHelper::isProperty($phpcsFile, $propertyPointer)) {
			return;
		}

		if (SuppressHelper::isSniffSuppressed($phpcsFile, $propertyPointer, self::NAME)) {
			return;
		}

		$docCommentOpenPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $propertyPointer);
		if ($docCommentOpenPointer !== null) {
			if (DocCommentHelper::hasInheritdocAnnotation($phpcsFile, $docCommentOpenPointer)) {
				return;
			}

			$varAnnotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer, '@var');
			$prefixedPropertyAnnotations = $this->getValidPrefixedAnnotations($phpcsFile, $docCommentOpenPointer);

			$propertyAnnotation = count($varAnnotations) > 0 ? current($varAnnotations) : null;
		} else {
			$propertyAnnotation = null;
			$prefixedPropertyAnnotations = [];
		}

		$propertyTypeHint = PropertyHelper::findTypeHint($phpcsFile, $propertyPointer);

		$this->checkTypeHint($phpcsFile, $propertyPointer, $propertyTypeHint, $propertyAnnotation, $prefixedPropertyAnnotations);
		$this->checkTraversableTypeHintSpecification(
			$phpcsFile,
			$propertyPointer,
			$propertyTypeHint,
			$propertyAnnotation,
			$prefixedPropertyAnnotations,
		);
		$this->checkUselessAnnotation($phpcsFile, $propertyPointer, $propertyTypeHint, $propertyAnnotation);
	}

	/**
	 * @param Annotation<VarTagValueNode|ParamTagValueNode>|null $propertyAnnotation
	 * @param list<Annotation<VarTagValueNode>> $prefixedPropertyAnnotations
	 */
	private function checkTypeHint(
		File $phpcsFile,
		int $propertyPointer,
		?TypeHint $propertyTypeHint,
		?Annotation $propertyAnnotation,
		array $prefixedPropertyAnnotations
	): void
	{
		$suppressNameAnyTypeHint = $this->getSniffName(self::CODE_MISSING_ANY_TYPE_HINT);
		$isSuppressedAnyTypeHint = SuppressHelper::isSniffSuppressed($phpcsFile, $propertyPointer, $suppressNameAnyTypeHint);

		$suppressNameNativeTypeHint = $this->getSniffName(self::CODE_MISSING_NATIVE_TYPE_HINT);
		$isSuppressedNativeTypeHint = SuppressHelper::isSniffSuppressed($phpcsFile, $propertyPointer, $suppressNameNativeTypeHint);

		if ($propertyTypeHint !== null) {
			$this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedAnyTypeHint, $suppressNameAnyTypeHint);
			$this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint);
			return;
		}

		if (!$this->hasAnnotation($propertyAnnotation)) {
			if (count($prefixedPropertyAnnotations) !== 0) {
				$this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedAnyTypeHint, $suppressNameAnyTypeHint);
				return;
			}

			if (!$isSuppressedAnyTypeHint) {
				$phpcsFile->addError(
					sprintf(
						$this->enableNativeTypeHint
							? 'Property %s does not have native type hint nor @var annotation for its value.'
							: 'Property %s does not have @var annotation for its value.',
						PropertyHelper::getFullyQualifiedName($phpcsFile, $propertyPointer),
					),
					$propertyPointer,
					self::CODE_MISSING_ANY_TYPE_HINT,
				);
			}

			return;
		}

		if (!$this->enableNativeTypeHint) {
			return;
		}

		$typeNode = $propertyAnnotation->getValue()->type;
		$originalTypeNode = $typeNode;
		if ($typeNode instanceof NullableTypeNode) {
			$typeNode = $typeNode->type;
		}

		$canTryUnionTypeHint = $this->enableUnionTypeHint && $typeNode instanceof UnionTypeNode;

		$typeHints = [];
		$traversableTypeHints = [];
		$nullableTypeHint = false;

		if (AnnotationTypeHelper::containsOneType($typeNode)) {
			/** @var ArrayTypeNode|ArrayShapeNode|ObjectShapeNode|IdentifierTypeNode|ThisTypeNode|GenericTypeNode|CallableTypeNode $typeNode */
			$typeNode = $typeNode;
			$typeHints[] = AnnotationTypeHelper::getTypeHintFromOneType($typeNode, false, $this->enableStandaloneNullTrueFalseTypeHints);

		} elseif ($typeNode instanceof UnionTypeNode || $typeNode instanceof IntersectionTypeNode) {
			$traversableTypeHints = [];
			foreach ($typeNode->types as $innerTypeNode) {
				if (!AnnotationTypeHelper::containsOneType($innerTypeNode)) {
					$this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint);
					return;
				}

				/** @var ArrayTypeNode|ArrayShapeNode|ObjectShapeNode|IdentifierTypeNode|ThisTypeNode|GenericTypeNode|CallableTypeNode $innerTypeNode */
				$innerTypeNode = $innerTypeNode;

				$typeHint = AnnotationTypeHelper::getTypeHintFromOneType($innerTypeNode, $canTryUnionTypeHint);

				if (strtolower($typeHint) === 'null') {
					$nullableTypeHint = true;
					continue;
				}

				$isTraversable = TypeHintHelper::isTraversableType(
					TypeHintHelper::getFullyQualifiedTypeHint($phpcsFile, $propertyPointer, $typeHint),
					$this->getTraversableTypeHints(),
				);

				if (
					!$innerTypeNode instanceof ArrayTypeNode
					&& !$innerTypeNode instanceof ArrayShapeNode
					&& $isTraversable
				) {
					$traversableTypeHints[] = $typeHint;
				}

				$typeHints[] = $typeHint;
			}

			$traversableTypeHints = array_values(array_unique($traversableTypeHints));
			if (count($traversableTypeHints) > 1 && !$canTryUnionTypeHint) {
				$this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint);
				return;
			}
		}

		$typeHints = array_values(array_unique($typeHints));

		if (count($traversableTypeHints) > 0) {
			/** @var UnionTypeNode|IntersectionTypeNode $typeNode */
			$typeNode = $typeNode;

			$itemsSpecificationTypeHint = AnnotationTypeHelper::getItemsSpecificationTypeFromType($typeNode);
			if ($itemsSpecificationTypeHint !== null) {
				$typeHints = AnnotationTypeHelper::getTraversableTypeHintsFromType(
					$typeNode,
					$phpcsFile,
					$propertyPointer,
					$this->getTraversableTypeHints(),
					$this->enableUnionTypeHint,
				);
			}
		}

		if (count($typeHints) === 0) {
			$this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint);
			return;
		}

		$typeHintsWithConvertedUnion = [];
		foreach ($typeHints as $typeHint) {
			if ($this->enableUnionTypeHint && TypeHintHelper::isUnofficialUnionTypeHint($typeHint)) {
				$canTryUnionTypeHint = true;
				array_push($typeHintsWithConvertedUnion, ...TypeHintHelper::convertUnofficialUnionTypeHintToOfficialTypeHints($typeHint));
			} else {
				$typeHintsWithConvertedUnion[] = $typeHint;
			}
		}
		$typeHintsWithConvertedUnion = array_unique($typeHintsWithConvertedUnion);

		if (
			count($typeHintsWithConvertedUnion) > 1
			&& (
				($typeNode instanceof UnionTypeNode && !$canTryUnionTypeHint)
				|| ($typeNode instanceof IntersectionTypeNode && !$this->enableIntersectionTypeHint)
			)
		) {
			$this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint);
			return;
		}

		foreach ($typeHintsWithConvertedUnion as $typeHintNo => $typeHint) {
			if ($typeHint === 'callable') {
				$this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint);
				return;
			}

			if ($canTryUnionTypeHint && $typeHint === 'false') {
				continue;
			}

			if (!TypeHintHelper::isValidTypeHint(
				$typeHint,
				true,
				false,
				$this->enableMixedTypeHint,
				$this->enableStandaloneNullTrueFalseTypeHints,
			)) {
				$this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint);
				return;
			}

			if (TypeHintHelper::isTypeDefinedInAnnotation($phpcsFile, $propertyPointer, $typeHint)) {
				$this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressedNativeTypeHint, $suppressNameNativeTypeHint);
				return;
			}

			$typeHintsWithConvertedUnion[$typeHintNo] = TypeHintHelper::convertLongSimpleTypeHintToShort($typeHint);
		}

		if ($originalTypeNode instanceof NullableTypeNode) {
			$nullableTypeHint = true;
		}

		if ($isSuppressedNativeTypeHint) {
			return;
		}

		$fix = $phpcsFile->addFixableError(
			sprintf(
				'Property %s does not have native type hint for its value but it should be possible to add it based on @var annotation "%s".',
				PropertyHelper::getFullyQualifiedName($phpcsFile, $propertyPointer),
				AnnotationTypeHelper::print($typeNode),
			),
			$propertyPointer,
			self::CODE_MISSING_NATIVE_TYPE_HINT,
		);
		if (!$fix) {
			return;
		}

		if (in_array('mixed', $typeHintsWithConvertedUnion, true)) {
			$propertyTypeHint = 'mixed';
		} elseif ($originalTypeNode instanceof IntersectionTypeNode) {
			$propertyTypeHint = implode('&', $typeHintsWithConvertedUnion);
		} else {
			$propertyTypeHint = implode('|', $typeHintsWithConvertedUnion);
			if ($nullableTypeHint) {
				if (count($typeHintsWithConvertedUnion) > 1) {
					$propertyTypeHint .= '|null';
				} else {
					$propertyTypeHint = '?' . $propertyTypeHint;
				}
			}
		}

		$tokens = $phpcsFile->getTokens();

		$pointerAfterProperty = null;
		if ($nullableTypeHint) {
			$pointerAfterProperty = TokenHelper::findNextEffective($phpcsFile, $propertyPointer + 1);
		}

		$phpcsFile->fixer->beginChangeset();
		FixerHelper::addBefore($phpcsFile, $propertyPointer, sprintf('%s ', $propertyTypeHint));

		if (
			$pointerAfterProperty !== null
			&& in_array($tokens[$pointerAfterProperty]['code'], [T_SEMICOLON, T_COMMA], true)
		) {
			FixerHelper::add($phpcsFile, $propertyPointer, ' = null');
		}

		$phpcsFile->fixer->endChangeset();
	}

	/**
	 * @param Annotation<VarTagValueNode|ParamTagValueNode>|null $propertyAnnotation
	 * @param list<Annotation<VarTagValueNode>> $prefixedPropertyAnnotations
	 */
	private function checkTraversableTypeHintSpecification(
		File $phpcsFile,
		int $propertyPointer,
		?TypeHint $propertyTypeHint,
		?Annotation $propertyAnnotation,
		array $prefixedPropertyAnnotations
	): void
	{
		$suppressName = $this->getSniffName(self::CODE_MISSING_TRAVERSABLE_TYPE_HINT_SPECIFICATION);
		$isSuppressed = SuppressHelper::isSniffSuppressed($phpcsFile, $propertyPointer, $suppressName);

		$hasTraversableTypeHint = $this->hasTraversableTypeHint($phpcsFile, $propertyPointer, $propertyTypeHint, $propertyAnnotation);
		$hasAnnotation = $this->hasAnnotation($propertyAnnotation);

		if (!$hasAnnotation) {
			if ($hasTraversableTypeHint) {
				if (count($prefixedPropertyAnnotations) !== 0) {
					$this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressed, $suppressName);
					return;
				}

				if (!$isSuppressed) {
					$phpcsFile->addError(
						sprintf(
							'@var annotation of property %s does not specify type hint for its items.',
							PropertyHelper::getFullyQualifiedName($phpcsFile, $propertyPointer),
						),
						$propertyPointer,
						self::CODE_MISSING_TRAVERSABLE_TYPE_HINT_SPECIFICATION,
					);
				}
			}

			return;
		}

		$typeNode = $propertyAnnotation->getValue()->type;

		if (
			!$hasTraversableTypeHint
			&& !AnnotationTypeHelper::containsTraversableType($typeNode, $phpcsFile, $propertyPointer, $this->getTraversableTypeHints())
		) {
			$this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressed, $suppressName);
			return;
		}

		if (AnnotationTypeHelper::containsItemsSpecificationForTraversable(
			$typeNode,
			$phpcsFile,
			$propertyPointer,
			$this->getTraversableTypeHints(),
		)) {
			$this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressed, $suppressName);
			return;
		}

		if ($isSuppressed) {
			return;
		}

		$phpcsFile->addError(
			sprintf(
				'@var annotation of property %s does not specify type hint for its items.',
				PropertyHelper::getFullyQualifiedName($phpcsFile, $propertyPointer),
			),
			$propertyAnnotation->getStartPointer(),
			self::CODE_MISSING_TRAVERSABLE_TYPE_HINT_SPECIFICATION,
		);
	}

	private function checkUselessAnnotation(
		File $phpcsFile,
		int $propertyPointer,
		?TypeHint $propertyTypeHint,
		?Annotation $propertyAnnotation
	): void
	{
		if ($propertyAnnotation === null) {
			return;
		}

		$suppressName = self::getSniffName(self::CODE_USELESS_ANNOTATION);
		$isSuppressed = SuppressHelper::isSniffSuppressed($phpcsFile, $propertyPointer, $suppressName);

		if (!AnnotationHelper::isAnnotationUseless(
			$phpcsFile,
			$propertyPointer,
			$propertyTypeHint,
			$propertyAnnotation,
			$this->getTraversableTypeHints(),
			$this->enableUnionTypeHint,
			$this->enableIntersectionTypeHint,
			$this->enableStandaloneNullTrueFalseTypeHints,
		)) {
			$this->reportUselessSuppress($phpcsFile, $propertyPointer, $isSuppressed, $suppressName);
			return;
		}

		if ($isSuppressed) {
			return;
		}

		$fix = $phpcsFile->addFixableError(
			sprintf(
				'Property %s has useless @var annotation.',
				PropertyHelper::getFullyQualifiedName($phpcsFile, $propertyPointer),
			),
			$propertyAnnotation->getStartPointer(),
			self::CODE_USELESS_ANNOTATION,
		);

		if (!$fix) {
			return;
		}

		if ($this->isDocCommentUseless($phpcsFile, $propertyPointer)) {
			$docCommentOpenPointer = DocCommentHelper::findDocCommentOpenPointer($phpcsFile, $propertyPointer);
			$docCommentClosePointer = $phpcsFile->getTokens()[$docCommentOpenPointer]['comment_closer'];

			$changeStart = $docCommentOpenPointer;
			/** @var int $changeEnd */
			$changeEnd = TokenHelper::findNextEffective($phpcsFile, $docCommentClosePointer + 1) - 1;

			$phpcsFile->fixer->beginChangeset();
			FixerHelper::removeBetweenIncluding($phpcsFile, $changeStart, $changeEnd);
			$phpcsFile->fixer->endChangeset();

			return;
		}

		/** @var int $changeStart */
		$changeStart = TokenHelper::findPrevious($phpcsFile, T_DOC_COMMENT_STAR, $propertyAnnotation->getStartPointer() - 1);

		/** @var int $changeEnd */
		$changeEnd = TokenHelper::findNext(
			$phpcsFile,
			[T_DOC_COMMENT_CLOSE_TAG, T_DOC_COMMENT_STAR],
			$propertyAnnotation->getEndPointer() + 1,
		) - 1;

		$phpcsFile->fixer->beginChangeset();
		FixerHelper::removeBetweenIncluding($phpcsFile, $changeStart, $changeEnd);
		$phpcsFile->fixer->endChangeset();
	}

	private function isDocCommentUseless(File $phpcsFile, int $propertyPointer): bool
	{
		if (DocCommentHelper::hasDocCommentDescription($phpcsFile, $propertyPointer)) {
			return false;
		}

		foreach (AnnotationHelper::getAnnotations($phpcsFile, $propertyPointer) as $annotation) {
			if ($annotation->getName() !== '@var') {
				return false;
			}
		}

		return true;
	}

	private function reportUselessSuppress(File $phpcsFile, int $pointer, bool $isSuppressed, string $suppressName): void
	{
		if (!$isSuppressed) {
			return;
		}

		$fix = $phpcsFile->addFixableError(
			sprintf('Useless %s %s', SuppressHelper::ANNOTATION, $suppressName),
			$pointer,
			self::CODE_USELESS_SUPPRESS,
		);

		if ($fix) {
			SuppressHelper::removeSuppressAnnotation($phpcsFile, $pointer, $suppressName);
		}
	}

	private function getSniffName(string $sniffName): string
	{
		return sprintf('%s.%s', self::NAME, $sniffName);
	}

	/**
	 * @return list<string>
	 */
	private function getTraversableTypeHints(): array
	{
		$this->normalizedTraversableTypeHints ??= array_map(
			static fn (string $typeHint): string => NamespaceHelper::isFullyQualifiedName($typeHint)
					? $typeHint
					: sprintf('%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $typeHint),
			SniffSettingsHelper::normalizeArray($this->traversableTypeHints),
		);
		return $this->normalizedTraversableTypeHints;
	}

	private function hasAnnotation(?Annotation $propertyAnnotation): bool
	{
		return $propertyAnnotation !== null && $propertyAnnotation->getValue() instanceof VarTagValueNode;
	}

	/**
	 * @param Annotation<VarTagValueNode|ParamTagValueNode>|null $propertyAnnotation
	 */
	private function hasTraversableTypeHint(
		File $phpcsFile,
		int $propertyPointer,
		?TypeHint $propertyTypeHint,
		?Annotation $propertyAnnotation
	): bool
	{
		if (
			$propertyTypeHint !== null
			&& TypeHintHelper::isTraversableType(
				TypeHintHelper::getFullyQualifiedTypeHint(
					$phpcsFile,
					$propertyPointer,
					$propertyTypeHint->getTypeHintWithoutNullabilitySymbol(),
				),
				$this->getTraversableTypeHints(),
			)
		) {
			return true;
		}

		return
			$this->hasAnnotation($propertyAnnotation)
			&& AnnotationTypeHelper::containsTraversableType(
				$propertyAnnotation->getValue()->type,
				$phpcsFile,
				$propertyPointer,
				$this->getTraversableTypeHints(),
			);
	}

	/**
	 * @return list<Annotation<VarTagValueNode>>
	 */
	private function getValidPrefixedAnnotations(File $phpcsFile, int $docCommentOpenPointer): array
	{
		$varAnnotations = [];

		$annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer);

		foreach (AnnotationHelper::STATIC_ANALYSIS_PREFIXES as $prefix) {
			foreach ($annotations as $annotation) {
				if ($annotation->isInvalid()) {
					continue;
				}

				if ($annotation->getName() === sprintf('@%s-var', $prefix)) {
					$varAnnotations[] = $annotation;
				}
			}
		}

		return $varAnnotations;
	}

}
