<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Parser;

use Iterator;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\DeprecatedTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ImplementsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TypelessParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\UsesTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPUnit\Framework\TestCase;
use const PHP_EOL;

class PhpDocParserTest extends TestCase
{

	/** @var Lexer */
	private $lexer;

	/** @var PhpDocParser */
	private $phpDocParser;

	/** @var PhpDocParser */
	private $phpDocParserWithRequiredWhitespaceBeforeDescription;

	protected function setUp(): void
	{
		parent::setUp();
		$this->lexer = new Lexer();
		$constExprParser = new ConstExprParser();
		$typeParser = new TypeParser($constExprParser);
		$this->phpDocParser = new PhpDocParser($typeParser, $constExprParser);
		$this->phpDocParserWithRequiredWhitespaceBeforeDescription = new PhpDocParser($typeParser, $constExprParser, true);
	}


	/**
	 * @dataProvider provideTagsWithNumbers
	 * @dataProvider provideParamTagsData
	 * @dataProvider provideTypelessParamTagsData
	 * @dataProvider provideVarTagsData
	 * @dataProvider provideReturnTagsData
	 * @dataProvider provideThrowsTagsData
	 * @dataProvider provideMixinTagsData
	 * @dataProvider provideDeprecatedTagsData
	 * @dataProvider providePropertyTagsData
	 * @dataProvider provideMethodTagsData
	 * @dataProvider provideSingleLinePhpDocData
	 * @dataProvider provideMultiLinePhpDocData
	 * @dataProvider provideTemplateTagsData
	 * @dataProvider provideExtendsTagsData
	 * @dataProvider provideTypeAliasTagsData
	 * @dataProvider provideTypeAliasImportTagsData
	 * @dataProvider provideAssertTagsData
	 * @dataProvider provideRealWorldExampleData
	 * @dataProvider provideDescriptionWithOrWithoutHtml
	 */
	public function testParse(
		string $label,
		string $input,
		PhpDocNode $expectedPhpDocNode,
		?PhpDocNode $withRequiredWhitespaceBeforeDescriptionExpectedPhpDocNode = null
	): void
	{
		$this->executeTestParse(
			$this->phpDocParser,
			$label,
			$input,
			$expectedPhpDocNode
		);

		$this->executeTestParse(
			$this->phpDocParserWithRequiredWhitespaceBeforeDescription,
			$label,
			$input,
			$withRequiredWhitespaceBeforeDescriptionExpectedPhpDocNode ?? $expectedPhpDocNode
		);
	}


	private function executeTestParse(PhpDocParser $phpDocParser, string $label, string $input, PhpDocNode $expectedPhpDocNode): void
	{
		$tokens = new TokenIterator($this->lexer->tokenize($input));
		$actualPhpDocNode = $phpDocParser->parse($tokens);

		$this->assertEquals($expectedPhpDocNode, $actualPhpDocNode, $label);
		$this->assertSame((string) $expectedPhpDocNode, (string) $actualPhpDocNode);
		$this->assertSame(Lexer::TOKEN_END, $tokens->currentTokenType());
	}


	public function provideParamTagsData(): Iterator
	{
		yield [
			'OK without description',
			'/** @param Foo $foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new ParamTagValueNode(
						new IdentifierTypeNode('Foo'),
						false,
						'$foo',
						''
					)
				),
			]),
		];

		yield [
			'OK with description',
			'/** @param Foo $foo optional description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new ParamTagValueNode(
						new IdentifierTypeNode('Foo'),
						false,
						'$foo',
						'optional description'
					)
				),
			]),
		];

		yield [
			'OK variadic without description',
			'/** @param Foo ...$foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new ParamTagValueNode(
						new IdentifierTypeNode('Foo'),
						true,
						'$foo',
						''
					)
				),
			]),
		];

		yield [
			'OK variadic with description',
			'/** @param Foo ...$foo optional description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new ParamTagValueNode(
						new IdentifierTypeNode('Foo'),
						true,
						'$foo',
						'optional description'
					)
				),
			]),
		];

		yield [
			'OK reference without description',
			'/** @param Foo &$foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new ParamTagValueNode(
						new IdentifierTypeNode('Foo'),
						false,
						'$foo',
						'',
						true
					)
				),
			]),
		];

		yield [
			'OK reference with description',
			'/** @param Foo &$foo optional description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new ParamTagValueNode(
						new IdentifierTypeNode('Foo'),
						false,
						'$foo',
						'optional description',
						true
					)
				),
			]),
		];

		yield [
			'OK reference variadic without description',
			'/** @param Foo &...$foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new ParamTagValueNode(
						new IdentifierTypeNode('Foo'),
						true,
						'$foo',
						'',
						true
					)
				),
			]),
		];

		yield [
			'OK reference variadic with description',
			'/** @param Foo &...$foo optional description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new ParamTagValueNode(
						new IdentifierTypeNode('Foo'),
						true,
						'$foo',
						'optional description',
						true
					)
				),
			]),
		];

		yield [
			'OK const wildcard with description',
			'/** @param self::* $foo optional description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new ParamTagValueNode(
						new ConstTypeNode(new ConstFetchNode('self', '*')),
						false,
						'$foo',
						'optional description'
					)
				),
			]),
		];

		yield [
			'invalid without type, parameter name and description',
			'/** @param */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new InvalidTagValueNode(
						'',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							11,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid without type and parameter name and with description (1)',
			'/** @param #desc */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new InvalidTagValueNode(
						'#desc',
						new ParserException(
							'#desc',
							Lexer::TOKEN_OTHER,
							11,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid without type and parameter name and with description (2)',
			'/** @param (Foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new InvalidTagValueNode(
						'(Foo',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							16,
							Lexer::TOKEN_CLOSE_PARENTHESES
						)
					)
				),
			]),
		];

		yield [
			'invalid with broken type (1)',
			'/** @param (Foo $foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new InvalidTagValueNode(
						'(Foo $foo',
						new ParserException(
							'$foo',
							Lexer::TOKEN_VARIABLE,
							16,
							Lexer::TOKEN_CLOSE_PARENTHESES
						)
					)
				),
			]),
		];

		yield [
			'invalid with broken type (2)',
			'/** @param Foo<Bar $foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new InvalidTagValueNode(
						'Foo<Bar $foo',
						new ParserException(
							'$foo',
							Lexer::TOKEN_VARIABLE,
							19,
							Lexer::TOKEN_CLOSE_ANGLE_BRACKET
						)
					)
				),
			]),
		];

		yield [
			'invalid with broken type (3)',
			'/** @param Foo| $foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new InvalidTagValueNode(
						'Foo| $foo',
						new ParserException(
							'$foo',
							Lexer::TOKEN_VARIABLE,
							16,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid without parameter name and description',
			'/** @param Foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new InvalidTagValueNode(
						'Foo',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							15,
							Lexer::TOKEN_VARIABLE
						)
					)
				),
			]),
		];

		yield [
			'invalid without parameter name and with description',
			'/** @param Foo optional description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new InvalidTagValueNode(
						'Foo optional description',
						new ParserException(
							'optional',
							Lexer::TOKEN_IDENTIFIER,
							15,
							Lexer::TOKEN_VARIABLE
						)
					)
				),
			]),
		];
	}

	public function provideTypelessParamTagsData(): Iterator
	{
		yield [
			'OK',
			'/** @param $foo description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new TypelessParamTagValueNode(
						false,
						'$foo',
						'description'
					)
				),
			]),
		];

		yield [
			'OK reference',
			'/** @param &$foo description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new TypelessParamTagValueNode(
						false,
						'$foo',
						'description',
						true
					)
				),
			]),
		];

		yield [
			'OK variadic',
			'/** @param ...$foo description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new TypelessParamTagValueNode(
						true,
						'$foo',
						'description'
					)
				),
			]),
		];

		yield [
			'OK reference variadic',
			'/** @param &...$foo description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new TypelessParamTagValueNode(
						true,
						'$foo',
						'description',
						true
					)
				),
			]),
		];

		yield [
			'OK without type and description',
			'/** @param $foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new TypelessParamTagValueNode(
						false,
						'$foo',
						'',
						false
					)
				),
			]),
		];
	}

	public function provideVarTagsData(): Iterator
	{
		yield [
			'OK without description and variable name',
			'/** @var Foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new IdentifierTypeNode('Foo'),
						'',
						''
					)
				),
			]),
		];

		yield [
			'OK without description',
			'/** @var Foo $foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new IdentifierTypeNode('Foo'),
						'$foo',
						''
					)
				),
			]),
		];

		yield [
			'OK without description and with no space between type and variable name',
			'/** @var Foo$foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new IdentifierTypeNode('Foo'),
						'$foo',
						''
					)
				),
			]),
		];

		yield [
			'OK without variable name',
			'/** @var Foo optional description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new IdentifierTypeNode('Foo'),
						'',
						'optional description'
					)
				),
			]),
		];

		yield [
			'OK without variable name and complex description',
			'/** @var callable[] function (Configurator $sender, DI\Compiler $compiler); Occurs after the compiler is created */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new ArrayTypeNode(
							new IdentifierTypeNode('callable')
						),
						'',
						'function (Configurator $sender, DI\Compiler $compiler); Occurs after the compiler is created'
					)
				),
			]),
		];

		yield [
			'OK without variable name and tag in the middle of description',
			'/** @var Foo @inject */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new IdentifierTypeNode('Foo'),
						'',
						'@inject'
					)
				),
			]),
		];

		yield [
			'OK without variable name and description in parentheses',
			'/** @var Foo (Bar) */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new IdentifierTypeNode('Foo'),
						'',
						'(Bar)'
					)
				),
			]),
		];

		yield [
			'OK with variable name and description',
			'/** @var Foo $foo optional description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new IdentifierTypeNode('Foo'),
						'$foo',
						'optional description'
					)
				),
			]),
		];

		yield [
			'OK without description with variable $this',
			'/** @var Foo $this */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new IdentifierTypeNode('Foo'),
						'$this',
						''
					)
				),
			]),
		];

		yield [
			'OK without description and with no space between type and variable name with variable $this',
			'/** @var Foo$this */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new IdentifierTypeNode('Foo'),
						'$this',
						''
					)
				),
			]),
		];

		yield [
			'OK with description with variable $this',
			'/** @var Foo $this Testing */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new IdentifierTypeNode('Foo'),
						'$this',
						'Testing'
					)
				),
			]),
		];

		yield [
			'OK with description and with no space between type and variable name with variable $this',
			'/** @var Foo$this Testing */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new IdentifierTypeNode('Foo'),
						'$this',
						'Testing'
					)
				),
			]),
		];

		yield [
			'OK with variable name and description and without all optional spaces',
			'/** @var(Foo)$foo#desc*/',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new IdentifierTypeNode('Foo'),
						'$foo',
						'#desc'
					)
				),
			]),
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new IdentifierTypeNode('Foo'),
						'$foo',
						'#desc'
					)
				),
			]),
		];

		yield [
			'OK with variable name and description and const expression',
			'/** @var self::* $foo optional description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new ConstTypeNode(new ConstFetchNode('self', '*')),
						'$foo',
						'optional description'
					)
				),
			]),
		];

		yield [
			'OK with no description and no trailing whitespace',
			'/** @var Foo $var*/',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new IdentifierTypeNode('Foo'),
						'$var',
						''
					)
				),
			]),
		];

		yield [
			'OK with no variable name and description and no trailing whitespace',
			'/** @var Foo*/',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new IdentifierTypeNode('Foo'),
						'',
						''
					)
				),
			]),
		];

		yield [
			'invalid without type, variable name and description',
			'/** @var */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new InvalidTagValueNode(
						'',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							9,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid without type and variable name and with description (1)',
			'/** @var #desc */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new InvalidTagValueNode(
						'#desc',
						new ParserException(
							'#desc',
							Lexer::TOKEN_OTHER,
							9,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid without type and variable name and with description (2)',
			'/** @var (Foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new InvalidTagValueNode(
						'(Foo',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							14,
							Lexer::TOKEN_CLOSE_PARENTHESES
						)
					)
				),
			]),
		];

		yield [
			'invalid with broken type (1)',
			'/** @var (Foo $foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new InvalidTagValueNode(
						'(Foo $foo',
						new ParserException(
							'$foo',
							Lexer::TOKEN_VARIABLE,
							14,
							Lexer::TOKEN_CLOSE_PARENTHESES
						)
					)
				),
			]),
		];

		yield [
			'invalid with broken type (2)',
			'/** @var Foo<Bar $foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new InvalidTagValueNode(
						'Foo<Bar $foo',
						new ParserException(
							'$foo',
							Lexer::TOKEN_VARIABLE,
							17,
							Lexer::TOKEN_CLOSE_ANGLE_BRACKET
						)
					)
				),
			]),
		];

		yield [
			'invalid with broken type (3)',
			'/** @var Foo| $foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new InvalidTagValueNode(
						'Foo| $foo',
						new ParserException(
							'$foo',
							Lexer::TOKEN_VARIABLE,
							14,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid object shape',
			'/** @psalm-type PARTSTRUCTURE_PARAM = object{attribute:string, value?:string} */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@psalm-type',
					new InvalidTagValueNode(
						'Unexpected token "{", expected \'*/\' at offset 44',
						new ParserException(
							'{',
							Lexer::TOKEN_OPEN_CURLY_BRACKET,
							44,
							Lexer::TOKEN_CLOSE_PHPDOC
						)
					)
				),
			]),
		];
	}


	public function providePropertyTagsData(): Iterator
	{
		yield [
			'OK without description',
			'/** @property Foo $foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@property',
					new PropertyTagValueNode(
						new IdentifierTypeNode('Foo'),
						'$foo',
						''
					)
				),
			]),
		];

		yield [
			'OK with description',
			'/** @property Foo $foo optional description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@property',
					new PropertyTagValueNode(
						new IdentifierTypeNode('Foo'),
						'$foo',
						'optional description'
					)
				),
			]),
		];

		yield [
			'invalid without type, property name and description',
			'/** @property */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@property',
					new InvalidTagValueNode(
						'',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							14,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid without type and property name and with description (1)',
			'/** @property #desc */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@property',
					new InvalidTagValueNode(
						'#desc',
						new ParserException(
							'#desc',
							Lexer::TOKEN_OTHER,
							14,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid without type and property name and with description (2)',
			'/** @property (Foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@property',
					new InvalidTagValueNode(
						'(Foo',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							19,
							Lexer::TOKEN_CLOSE_PARENTHESES
						)
					)
				),
			]),
		];

		yield [
			'invalid with broken type (1)',
			'/** @property (Foo $foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@property',
					new InvalidTagValueNode(
						'(Foo $foo',
						new ParserException(
							'$foo',
							Lexer::TOKEN_VARIABLE,
							19,
							Lexer::TOKEN_CLOSE_PARENTHESES
						)
					)
				),
			]),
		];

		yield [
			'invalid with broken type (2)',
			'/** @property Foo<Bar $foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@property',
					new InvalidTagValueNode(
						'Foo<Bar $foo',
						new ParserException(
							'$foo',
							Lexer::TOKEN_VARIABLE,
							22,
							Lexer::TOKEN_CLOSE_ANGLE_BRACKET
						)
					)
				),
			]),
		];

		yield [
			'invalid with broken type (3)',
			'/** @property Foo| $foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@property',
					new InvalidTagValueNode(
						'Foo| $foo',
						new ParserException(
							'$foo',
							Lexer::TOKEN_VARIABLE,
							19,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid without property name and description',
			'/** @property Foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@property',
					new InvalidTagValueNode(
						'Foo',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							18,
							Lexer::TOKEN_VARIABLE
						)
					)
				),
			]),
		];

		yield [
			'invalid without property name and with description',
			'/** @property Foo optional description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@property',
					new InvalidTagValueNode(
						'Foo optional description',
						new ParserException(
							'optional',
							Lexer::TOKEN_IDENTIFIER,
							18,
							Lexer::TOKEN_VARIABLE
						)
					)
				),
			]),
		];
	}


	public function provideReturnTagsData(): Iterator
	{
		yield [
			'OK without description',
			'/** @return Foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new IdentifierTypeNode('Foo'),
						''
					)
				),
			]),
		];

		yield [
			'OK with description',
			'/** @return Foo optional description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new IdentifierTypeNode('Foo'),
						'optional description'
					)
				),
			]),
		];

		yield [
			'OK with description that starts with TOKEN_OPEN_SQUARE_BRACKET',
			'/** @return Foo [Bar] */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new IdentifierTypeNode('Foo'),
						'[Bar]'
					)
				),
			]),
		];

		yield [
			'OK with offset access type',
			'/** @return Foo[Bar] */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new OffsetAccessTypeNode(
							new IdentifierTypeNode('Foo'),
							new IdentifierTypeNode('Bar')
						),
						''
					)
				),
			]),
		];

		yield [
			'invalid without type and description',
			'/** @return */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new InvalidTagValueNode(
						'',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							12,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid without type',
			'/** @return [int, string] */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new InvalidTagValueNode(
						'[int, string]',
						new ParserException(
							'[',
							Lexer::TOKEN_OPEN_SQUARE_BRACKET,
							12,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid with type and disallowed description start token (1)',
			'/** @return Foo | #desc */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new InvalidTagValueNode(
						'Foo | #desc',
						new ParserException(
							'#desc',
							Lexer::TOKEN_OTHER,
							18,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid with type and disallowed description start token (2)',
			'/** @return A & B | C */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new InvalidTagValueNode(
						'A & B | C',
						new ParserException(
							'|',
							Lexer::TOKEN_UNION,
							18,
							Lexer::TOKEN_OTHER
						)
					)
				),
			]),
		];

		yield [
			'invalid with type and disallowed description start token (3)',
			'/** @return A | B & C */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new InvalidTagValueNode(
						'A | B & C',
						new ParserException(
							'&',
							Lexer::TOKEN_INTERSECTION,
							18,
							Lexer::TOKEN_OTHER
						)
					)
				),
			]),
		];

		yield [
			'invalid with type and disallowed description start token (4)',
			'/** @return A | B < 123 */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new InvalidTagValueNode(
						'A | B < 123',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							24,
							Lexer::TOKEN_CLOSE_ANGLE_BRACKET
						)
					)
				),
			]),
		];

		yield [
			'OK with type and const expr as generic type variable',
			'/** @return A | B < 123 > */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new UnionTypeNode([
							new IdentifierTypeNode('A'),
							new GenericTypeNode(
								new IdentifierTypeNode('B'),
								[
									new ConstTypeNode(new ConstExprIntegerNode('123')),
								]
							),
						]),
						''
					)
				),
			]),
		];

		yield [
			'OK with constant wildcard and description',
			'/** @return self::* example description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new ConstTypeNode(new ConstFetchNode('self', '*')),
						'example description'
					)
				),
			]),
		];

		yield [
			'OK with conditional type',
			'/** @return (Foo is Bar ? never : int) */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new ConditionalTypeNode(
							new IdentifierTypeNode('Foo'),
							new IdentifierTypeNode('Bar'),
							new IdentifierTypeNode('never'),
							new IdentifierTypeNode('int'),
							false
						),
						''
					)
				),
			]),
		];

		yield [
			'OK with negated conditional type',
			'/** @return (Foo is not Bar ? never : int) */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new ConditionalTypeNode(
							new IdentifierTypeNode('Foo'),
							new IdentifierTypeNode('Bar'),
							new IdentifierTypeNode('never'),
							new IdentifierTypeNode('int'),
							true
						),
						''
					)
				),
			]),
		];

		yield [
			'OK with nested conditional type',
			'/**
			  * @return (T is self::TYPE_STRING ? string : (T is self::TYPE_INT ? int : bool))
			  */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new ConditionalTypeNode(
							new IdentifierTypeNode('T'),
							new ConstTypeNode(new ConstFetchNode('self', 'TYPE_STRING')),
							new IdentifierTypeNode('string'),
							new ConditionalTypeNode(
								new IdentifierTypeNode('T'),
								new ConstTypeNode(new ConstFetchNode('self', 'TYPE_INT')),
								new IdentifierTypeNode('int'),
								new IdentifierTypeNode('bool'),
								false
							),
							false
						),
						''
					)
				),
			]),
		];

		yield [
			'invalid non-parenthesized conditional type',
			'/** @return Foo is not Bar ? never : int */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new IdentifierTypeNode('Foo'),
						'is not Bar ? never : int'
					)
				),
			]),
		];

		yield [
			'OK with conditional type for parameter',
			'/** @return ($foo is Bar ? never : int) */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new ConditionalTypeForParameterNode(
							'$foo',
							new IdentifierTypeNode('Bar'),
							new IdentifierTypeNode('never'),
							new IdentifierTypeNode('int'),
							false
						),
						''
					)
				),
			]),
		];

		yield [
			'OK with negated conditional type for parameter',
			'/** @return ($foo is not Bar ? never : int) */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new ConditionalTypeForParameterNode(
							'$foo',
							new IdentifierTypeNode('Bar'),
							new IdentifierTypeNode('never'),
							new IdentifierTypeNode('int'),
							true
						),
						''
					)
				),
			]),
		];

		yield [
			'OK with nested conditional type for parameter',
			'/**
			  * @return ($T is self::TYPE_STRING ? string : ($T is self::TYPE_INT ? int : bool))
			  */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new ConditionalTypeForParameterNode(
							'$T',
							new ConstTypeNode(new ConstFetchNode('self', 'TYPE_STRING')),
							new IdentifierTypeNode('string'),
							new ConditionalTypeForParameterNode(
								'$T',
								new ConstTypeNode(new ConstFetchNode('self', 'TYPE_INT')),
								new IdentifierTypeNode('int'),
								new IdentifierTypeNode('bool'),
								false
							),
							false
						),
						''
					)
				),
			]),
		];

		yield [
			'invalid non-parenthesized conditional type',
			'/** @return $foo is not Bar ? never : int */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new InvalidTagValueNode(
						'$foo is not Bar ? never : int',
						new ParserException(
							'$foo',
							Lexer::TOKEN_VARIABLE,
							12,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'OK variadic callable',
			'/** @return \Closure(int ...$u, string): string */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new CallableTypeNode(
							new IdentifierTypeNode('\Closure'),
							[
								new CallableTypeParameterNode(
									new IdentifierTypeNode('int'),
									false,
									true,
									'$u',
									false
								),
								new CallableTypeParameterNode(
									new IdentifierTypeNode('string'),
									false,
									false,
									'',
									false
								),
							],
							new IdentifierTypeNode('string')
						),
						''
					)
				),
			]),
		];

		yield [
			'invalid variadic callable',
			'/** @return \Closure(...int, string): string */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new IdentifierTypeNode('\Closure'),
						'(...int, string): string'
					)
				),
			]),
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new InvalidTagValueNode(
						'\Closure(...int, string): string',
						new ParserException(
							'(',
							Lexer::TOKEN_OPEN_PARENTHESES,
							20,
							Lexer::TOKEN_HORIZONTAL_WS
						)
					)
				),
			]),
		];
	}


	public function provideThrowsTagsData(): Iterator
	{
		yield [
			'OK without description',
			'/** @throws Foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@throws',
					new ThrowsTagValueNode(
						new IdentifierTypeNode('Foo'),
						''
					)
				),
			]),
		];

		yield [
			'OK with description',
			'/** @throws Foo optional description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@throws',
					new ThrowsTagValueNode(
						new IdentifierTypeNode('Foo'),
						'optional description'
					)
				),
			]),
		];

		yield [
			'OK with description that starts with TOKEN_OPEN_SQUARE_BRACKET',
			'/** @throws Foo [Bar] */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@throws',
					new ThrowsTagValueNode(
						new IdentifierTypeNode('Foo'),
						'[Bar]'
					)
				),
			]),
		];

		yield [
			'invalid without type and description',
			'/** @throws */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@throws',
					new InvalidTagValueNode(
						'',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							12,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid with type and disallowed description start token',
			'/** @throws Foo | #desc */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@throws',
					new InvalidTagValueNode(
						'Foo | #desc',
						new ParserException(
							'#desc',
							Lexer::TOKEN_OTHER,
							18,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];
	}

	public function provideMixinTagsData(): Iterator
	{
		yield [
			'OK without description',
			'/** @mixin Foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@mixin',
					new MixinTagValueNode(
						new IdentifierTypeNode('Foo'),
						''
					)
				),
			]),
		];

		yield [
			'OK with description',
			'/** @mixin Foo optional description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@mixin',
					new MixinTagValueNode(
						new IdentifierTypeNode('Foo'),
						'optional description'
					)
				),
			]),
		];

		yield [
			'OK with description that starts with TOKEN_OPEN_SQUARE_BRACKET',
			'/** @mixin Foo [Bar] */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@mixin',
					new MixinTagValueNode(
						new IdentifierTypeNode('Foo'),
						'[Bar]'
					)
				),
			]),
		];

		yield [
			'invalid without type and description',
			'/** @mixin */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@mixin',
					new InvalidTagValueNode(
						'',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							11,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid with type and disallowed description start token',
			'/** @mixin Foo | #desc */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@mixin',
					new InvalidTagValueNode(
						'Foo | #desc',
						new ParserException(
							'#desc',
							Lexer::TOKEN_OTHER,
							17,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'generic @mixin',
			'/** @mixin Foo<Bar> */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@mixin',
					new MixinTagValueNode(
						new GenericTypeNode(new IdentifierTypeNode('Foo'), [
							new IdentifierTypeNode('Bar'),
						]),
						''
					)
				),
			]),
		];
	}

	public function provideDeprecatedTagsData(): Iterator
	{
		yield [
			'OK with no description',
			'/** @deprecated */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@deprecated',
					new DeprecatedTagValueNode('')
				),
			]),
		];

		yield [
			'OK with simple description description',
			'/** @deprecated text string */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@deprecated',
					new DeprecatedTagValueNode('text string')
				),
			]),
		];
		yield [
			'OK with two simple description with break',
			'/** @deprecated text first
        *
        * @deprecated text second
        */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@deprecated',
					new DeprecatedTagValueNode('text first')
				),
				new PhpDocTextNode(''),
				new PhpDocTagNode(
					'@deprecated',
					new DeprecatedTagValueNode('text second')
				),
			]),
		];

		yield [
			'OK with two simple description without break',
			'/** @deprecated text first
        * @deprecated text second
        */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@deprecated',
					new DeprecatedTagValueNode('text first')
				),
				new PhpDocTagNode(
					'@deprecated',
					new DeprecatedTagValueNode('text second')
				),
			]),
		];

		yield [
			'OK with long descriptions',
			'/** @deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. In
			*   Drupal 9 there will be no way to set the status and in Drupal 8 this
			*   ability has been removed because mb_*() functions are supplied using
			*   Symfony\'s polyfill. */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@deprecated',
					new DeprecatedTagValueNode('in Drupal 8.6.0 and will be removed before Drupal 9.0.0. In
  Drupal 9 there will be no way to set the status and in Drupal 8 this
  ability has been removed because mb_*() functions are supplied using
  Symfony\'s polyfill.')
				),
			]),
		];
		yield [
			'OK with multiple and long descriptions',
			'/**
      * Sample class
      *
      * @author Foo Baz <foo@baz.com>
      *
      * @deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. In
			*   Drupal 9 there will be no way to set the status and in Drupal 8 this
			*   ability has been removed because mb_*() functions are supplied using
			*   Symfony\'s polyfill.
			*/',
			new PhpDocNode([
				new PhpDocTextNode('Sample class'),
				new PhpDocTextNode(''),
				new PhpDocTagNode(
					'@author',
					new GenericTagValueNode('Foo Baz <foo@baz.com>')
				),
				new PhpDocTextNode(''),
				new PhpDocTagNode(
					'@deprecated',
					new DeprecatedTagValueNode('in Drupal 8.6.0 and will be removed before Drupal 9.0.0. In
  Drupal 9 there will be no way to set the status and in Drupal 8 this
  ability has been removed because mb_*() functions are supplied using
  Symfony\'s polyfill.')
				),
			]),
		];
	}

	public function provideMethodTagsData(): Iterator
	{
		yield [
			'OK non-static, without return type',
			'/** @method foo() */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@method',
					new MethodTagValueNode(
						false,
						null,
						'foo',
						[],
						''
					)
				),
			]),
		];

		yield [
			'OK non-static, with return type',
			'/** @method Foo foo() */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@method',
					new MethodTagValueNode(
						false,
						new IdentifierTypeNode('Foo'),
						'foo',
						[],
						''
					)
				),
			]),
		];

		yield [
			'OK non-static, with return static type',
			'/** @method static foo() */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@method',
					new MethodTagValueNode(
						false,
						new IdentifierTypeNode('static'),
						'foo',
						[],
						''
					)
				),
			]),
		];

		yield [
			'OK static, with return type',
			'/** @method static Foo foo() */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@method',
					new MethodTagValueNode(
						true,
						new IdentifierTypeNode('Foo'),
						'foo',
						[],
						''
					)
				),
			]),
		];

		yield [
			'OK static, with return static type',
			'/** @method static static foo() */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@method',
					new MethodTagValueNode(
						true,
						new IdentifierTypeNode('static'),
						'foo',
						[],
						''
					)
				),
			]),
		];

		yield [
			'OK non-static, with return type and description',
			'/** @method Foo foo() optional description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@method',
					new MethodTagValueNode(
						false,
						new IdentifierTypeNode('Foo'),
						'foo',
						[],
						'optional description'
					)
				),
			]),
		];

		yield [
			'OK non-static, with return type and single parameter without type',
			'/** @method Foo foo($a) */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@method',
					new MethodTagValueNode(
						false,
						new IdentifierTypeNode('Foo'),
						'foo',
						[
							new MethodTagValueParameterNode(
								null,
								false,
								false,
								'$a',
								null
							),
						],
						''
					)
				),
			]),
		];

		yield [
			'OK non-static, with return type and single parameter with type',
			'/** @method Foo foo(A $a) */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@method',
					new MethodTagValueNode(
						false,
						new IdentifierTypeNode('Foo'),
						'foo',
						[
							new MethodTagValueParameterNode(
								new IdentifierTypeNode('A'),
								false,
								false,
								'$a',
								null
							),
						],
						''
					)
				),
			]),
		];

		yield [
			'OK non-static, with return type and single parameter with type that is passed by reference',
			'/** @method Foo foo(A &$a) */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@method',
					new MethodTagValueNode(
						false,
						new IdentifierTypeNode('Foo'),
						'foo',
						[
							new MethodTagValueParameterNode(
								new IdentifierTypeNode('A'),
								true,
								false,
								'$a',
								null
							),
						],
						''
					)
				),
			]),
		];

		yield [
			'OK non-static, with return type and single variadic parameter with type',
			'/** @method Foo foo(A ...$a) */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@method',
					new MethodTagValueNode(
						false,
						new IdentifierTypeNode('Foo'),
						'foo',
						[
							new MethodTagValueParameterNode(
								new IdentifierTypeNode('A'),
								false,
								true,
								'$a',
								null
							),
						],
						''
					)
				),
			]),
		];

		yield [
			'OK non-static, with return type and single variadic parameter with type that is passed by reference',
			'/** @method Foo foo(A &...$a) */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@method',
					new MethodTagValueNode(
						false,
						new IdentifierTypeNode('Foo'),
						'foo',
						[
							new MethodTagValueParameterNode(
								new IdentifierTypeNode('A'),
								true,
								true,
								'$a',
								null
							),
						],
						''
					)
				),
			]),
		];

		yield [
			'OK non-static, with return type and single parameter with default value',
			'/** @method Foo foo($a = 123) */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@method',
					new MethodTagValueNode(
						false,
						new IdentifierTypeNode('Foo'),
						'foo',
						[
							new MethodTagValueParameterNode(
								null,
								false,
								false,
								'$a',
								new ConstExprIntegerNode('123')
							),
						],
						''
					)
				),
			]),
		];

		yield [
			'OK non-static, with return type and single variadic parameter with type that is passed by reference and default value',
			'/** @method Foo foo(A &...$a = 123) */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@method',
					new MethodTagValueNode(
						false,
						new IdentifierTypeNode('Foo'),
						'foo',
						[
							new MethodTagValueParameterNode(
								new IdentifierTypeNode('A'),
								true,
								true,
								'$a',
								new ConstExprIntegerNode('123')
							),
						],
						''
					)
				),
			]),
		];

		yield [
			'OK non-static, with return type and multiple parameters without type',
			'/** @method Foo foo($a, $b, $c) */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@method',
					new MethodTagValueNode(
						false,
						new IdentifierTypeNode('Foo'),
						'foo',
						[
							new MethodTagValueParameterNode(
								null,
								false,
								false,
								'$a',
								null
							),
							new MethodTagValueParameterNode(
								null,
								false,
								false,
								'$b',
								null
							),
							new MethodTagValueParameterNode(
								null,
								false,
								false,
								'$c',
								null
							),
						],
						''
					)
				),
			]),
		];

		yield [
			'invalid non-static method without parentheses',
			'/** @method a b */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@method',
					new InvalidTagValueNode(
						'a b',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							16,
							Lexer::TOKEN_OPEN_PARENTHESES
						)
					)
				),
			]),
		];

		yield [
			'invalid static method without parentheses',
			'/** @method static a b */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@method',
					new InvalidTagValueNode(
						'static a b',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							23,
							Lexer::TOKEN_OPEN_PARENTHESES
						)
					)
				),
			]),
		];

		yield [
			'invalid non-static method without parameter name',
			'/** @method a b(x) */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@method',
					new InvalidTagValueNode(
						'a b(x)',
						new ParserException(
							')',
							Lexer::TOKEN_CLOSE_PARENTHESES,
							17,
							Lexer::TOKEN_VARIABLE
						)
					)
				),
			]),
		];
	}


	public function provideSingleLinePhpDocData(): Iterator
	{
		yield [
			'empty',
			'/** */',
			new PhpDocNode([]),
		];

		yield [
			'edge-case',
			'/** /**/',
			new PhpDocNode([
				new PhpDocTextNode(
					'/*'
				),
			]),
		];

		yield [
			'single text node',
			'/** text */',
			new PhpDocNode([
				new PhpDocTextNode(
					'text'
				),
			]),
		];

		yield [
			'single text node with tag in the middle',
			'/** text @foo bar */',
			new PhpDocNode([
				new PhpDocTextNode(
					'text @foo bar'
				),
			]),
		];

		yield [
			'single tag node without value',
			'/** @foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@foo',
					new GenericTagValueNode('')
				),
			]),
		];

		yield [
			'single tag node with value',
			'/** @foo lorem ipsum */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@foo',
					new GenericTagValueNode('lorem ipsum')
				),
			]),
		];

		yield [
			'single tag node with tag in the middle of value',
			'/** @foo lorem @bar ipsum */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@foo',
					new GenericTagValueNode('lorem @bar ipsum')
				),
			]),
		];

		yield [
			'single tag node without space between tag name and its value',
			'/** @varFoo $foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@varFoo',
					new GenericTagValueNode(
						'$foo'
					)
				),
			]),
		];

		yield [
			'callable with space between keyword and parameters',
			'/** @var callable (int): void */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new CallableTypeNode(
							new IdentifierTypeNode('callable'),
							[
								new CallableTypeParameterNode(new IdentifierTypeNode('int'), false, false, '', false),
							],
							new IdentifierTypeNode('void')
						),
						'',
						''
					)
				),
			]),
		];

		yield [
			'callable with description in parentheses',
			'/** @var callable (int) */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new IdentifierTypeNode('callable'),
						'',
						'(int)'
					)
				),
			]),
		];

		yield [
			'callable with incomplete signature without return type',
			'/** @var callable(int) */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new VarTagValueNode(
						new IdentifierTypeNode('callable'),
						'',
						'(int)'
					)
				),
			]),
			new PhpDocNode([
				new PhpDocTagNode(
					'@var',
					new InvalidTagValueNode(
						'callable(int)',
						new ParserException(
							'(',
							Lexer::TOKEN_OPEN_PARENTHESES,
							17,
							Lexer::TOKEN_HORIZONTAL_WS
						)
					)
				),
			]),
		];
	}


	public function provideMultiLinePhpDocData(): array
	{
		return [
			[
				'multi-line with two tags',
				'/**
				  * @param Foo $foo 1st multi world description
				  * @param Bar $bar 2nd multi world description
				  */',
				new PhpDocNode([
					new PhpDocTagNode(
						'@param',
						new ParamTagValueNode(
							new IdentifierTypeNode('Foo'),
							false,
							'$foo',
							'1st multi world description'
						)
					),
					new PhpDocTagNode(
						'@param',
						new ParamTagValueNode(
							new IdentifierTypeNode('Bar'),
							false,
							'$bar',
							'2nd multi world description'
						)
					),
				]),
			],
			[
				'multi-line with two tags and text in the middle',
				'/**
				  * @param Foo $foo 1st multi world description
				  * some text in the middle
				  * @param Bar $bar 2nd multi world description
				  */',
				new PhpDocNode([
					new PhpDocTagNode(
						'@param',
						new ParamTagValueNode(
							new IdentifierTypeNode('Foo'),
							false,
							'$foo',
							'1st multi world description
some text in the middle'
						)
					),
					new PhpDocTagNode(
						'@param',
						new ParamTagValueNode(
							new IdentifierTypeNode('Bar'),
							false,
							'$bar',
							'2nd multi world description'
						)
					),
				]),
			],
			[
				'multi-line with two tags, text in the middle and some empty lines',
				'/**
				  *
				  *
				  * @param Foo $foo 1st multi world description with empty lines
				  *
				  *
				  * some text in the middle
				  *
				  *
				  * @param Bar $bar 2nd multi world description with empty lines
				  *
				  *
				  * test
				  */',
				new PhpDocNode([
					new PhpDocTextNode(''),
					new PhpDocTextNode(''),
					new PhpDocTagNode(
						'@param',
						new ParamTagValueNode(
							new IdentifierTypeNode('Foo'),
							false,
							'$foo',
							'1st multi world description with empty lines'
						)
					),
					new PhpDocTextNode(''),
					new PhpDocTextNode(''),
					new PhpDocTextNode('some text in the middle'),
					new PhpDocTextNode(''),
					new PhpDocTextNode(''),
					new PhpDocTagNode(
						'@param',
						new ParamTagValueNode(
							new IdentifierTypeNode('Bar'),
							false,
							'$bar',
							'2nd multi world description with empty lines'
						)
					),
					new PhpDocTextNode(''),
					new PhpDocTextNode(''),
					new PhpDocTextNode('test'),
				]),
			],
			[
				'multi-line with just empty lines',
				'/**
				  *
				  *
				  */',
				new PhpDocNode([
					new PhpDocTextNode(''),
					new PhpDocTextNode(''),
				]),
			],
			[
				'multi-line with tag mentioned as part of text node',
				'/**
				  * Lets talk about @param
				  * @param int $foo @param string $bar
				  */',
				new PhpDocNode([
					new PhpDocTextNode('Lets talk about @param'),
					new PhpDocTagNode(
						'@param',
						new ParamTagValueNode(
							new IdentifierTypeNode('int'),
							false,
							'$foo',
							'@param string $bar'
						)
					),
				]),
			],
			[
				'multi-line with a lot of @method tags',
				'/**
				  * @method int getInteger(int $a, int $b)
				  * @method void doSomething(int $a, $b)
				  * @method self|Bar getFooOrBar()
				  * @method methodWithNoReturnType()
				  * @method static int getIntegerStatically(int $a, int $b)
				  * @method static void doSomethingStatically(int $a, $b)
				  * @method static self|Bar getFooOrBarStatically()
				  * @method static methodWithNoReturnTypeStatically()
				  * @method int getIntegerWithDescription(int $a, int $b) Get an integer with a description.
				  * @method void doSomethingWithDescription(int $a, $b) Do something with a description.
				  * @method self|Bar getFooOrBarWithDescription() Get a Foo or a Bar with a description.
				  * @method methodWithNoReturnTypeWithDescription() Do something with a description but what, who knows!
				  * @method static int getIntegerStaticallyWithDescription(int $a, int $b) Get an integer with a description statically.
				  * @method static void doSomethingStaticallyWithDescription(int $a, $b) Do something with a description statically.
				  * @method static self|Bar getFooOrBarStaticallyWithDescription() Get a Foo or a Bar with a description statically.
				  * @method static methodWithNoReturnTypeStaticallyWithDescription() Do something with a description statically, but what, who knows!
				  * @method static bool aStaticMethodThatHasAUniqueReturnTypeInThisClass()
				  * @method static string aStaticMethodThatHasAUniqueReturnTypeInThisClassWithDescription() A Description.
				  * @method int getIntegerNoParams()
				  * @method void doSomethingNoParams()
				  * @method self|Bar getFooOrBarNoParams()
				  * @method methodWithNoReturnTypeNoParams()
				  * @method static int getIntegerStaticallyNoParams()
				  * @method static void doSomethingStaticallyNoParams()
				  * @method static self|Bar getFooOrBarStaticallyNoParams()
				  * @method static methodWithNoReturnTypeStaticallyNoParams()
				  * @method int getIntegerWithDescriptionNoParams() Get an integer with a description.
				  * @method void doSomethingWithDescriptionNoParams() Do something with a description.
				  * @method self|Bar getFooOrBarWithDescriptionNoParams() Get a Foo or a Bar with a description.
				  * @method static int getIntegerStaticallyWithDescriptionNoParams() Get an integer with a description statically.
				  * @method static void doSomethingStaticallyWithDescriptionNoParams() Do something with a description statically.
				  * @method static self|Bar getFooOrBarStaticallyWithDescriptionNoParams() Get a Foo or a Bar with a description statically.
				  * @method static bool|string aStaticMethodThatHasAUniqueReturnTypeInThisClassNoParams()
				  * @method static string|float aStaticMethodThatHasAUniqueReturnTypeInThisClassWithDescriptionNoParams() A Description.
				  * @method \Aws\Result publish(array $args)
				  * @method Image rotate(float & ... $angle = array(), $backgroundColor)
				  * @method Foo overridenMethod()
				  */',
				new PhpDocNode([
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new IdentifierTypeNode('int'),
							'getInteger',
							[
								new MethodTagValueParameterNode(
									new IdentifierTypeNode('int'),
									false,
									false,
									'$a',
									null
								),
								new MethodTagValueParameterNode(
									new IdentifierTypeNode('int'),
									false,
									false,
									'$b',
									null
								),
							],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new IdentifierTypeNode('void'),
							'doSomething',
							[
								new MethodTagValueParameterNode(
									new IdentifierTypeNode('int'),
									false,
									false,
									'$a',
									null
								),
								new MethodTagValueParameterNode(
									null,
									false,
									false,
									'$b',
									null
								),
							],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new UnionTypeNode([
								new IdentifierTypeNode('self'),
								new IdentifierTypeNode('Bar'),
							]),
							'getFooOrBar',
							[],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							null,
							'methodWithNoReturnType',
							[],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							true,
							new IdentifierTypeNode('int'),
							'getIntegerStatically',
							[
								new MethodTagValueParameterNode(
									new IdentifierTypeNode('int'),
									false,
									false,
									'$a',
									null
								),
								new MethodTagValueParameterNode(
									new IdentifierTypeNode('int'),
									false,
									false,
									'$b',
									null
								),
							],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							true,
							new IdentifierTypeNode('void'),
							'doSomethingStatically',
							[
								new MethodTagValueParameterNode(
									new IdentifierTypeNode('int'),
									false,
									false,
									'$a',
									null
								),
								new MethodTagValueParameterNode(
									null,
									false,
									false,
									'$b',
									null
								),
							],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							true,
							new UnionTypeNode([
								new IdentifierTypeNode('self'),
								new IdentifierTypeNode('Bar'),
							]),
							'getFooOrBarStatically',
							[],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new IdentifierTypeNode('static'),
							'methodWithNoReturnTypeStatically',
							[],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new IdentifierTypeNode('int'),
							'getIntegerWithDescription',
							[
								new MethodTagValueParameterNode(
									new IdentifierTypeNode('int'),
									false,
									false,
									'$a',
									null
								),
								new MethodTagValueParameterNode(
									new IdentifierTypeNode('int'),
									false,
									false,
									'$b',
									null
								),
							],
							'Get an integer with a description.'
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new IdentifierTypeNode('void'),
							'doSomethingWithDescription',
							[
								new MethodTagValueParameterNode(
									new IdentifierTypeNode('int'),
									false,
									false,
									'$a',
									null
								),
								new MethodTagValueParameterNode(
									null,
									false,
									false,
									'$b',
									null
								),
							],
							'Do something with a description.'
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new UnionTypeNode([
								new IdentifierTypeNode('self'),
								new IdentifierTypeNode('Bar'),
							]),
							'getFooOrBarWithDescription',
							[],
							'Get a Foo or a Bar with a description.'
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							null,
							'methodWithNoReturnTypeWithDescription',
							[],
							'Do something with a description but what, who knows!'
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							true,
							new IdentifierTypeNode('int'),
							'getIntegerStaticallyWithDescription',
							[
								new MethodTagValueParameterNode(
									new IdentifierTypeNode('int'),
									false,
									false,
									'$a',
									null
								),
								new MethodTagValueParameterNode(
									new IdentifierTypeNode('int'),
									false,
									false,
									'$b',
									null
								),
							],
							'Get an integer with a description statically.'
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							true,
							new IdentifierTypeNode('void'),
							'doSomethingStaticallyWithDescription',
							[
								new MethodTagValueParameterNode(
									new IdentifierTypeNode('int'),
									false,
									false,
									'$a',
									null
								),
								new MethodTagValueParameterNode(
									null,
									false,
									false,
									'$b',
									null
								),
							],
							'Do something with a description statically.'
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							true,
							new UnionTypeNode([
								new IdentifierTypeNode('self'),
								new IdentifierTypeNode('Bar'),
							]),
							'getFooOrBarStaticallyWithDescription',
							[],
							'Get a Foo or a Bar with a description statically.'
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new IdentifierTypeNode('static'),
							'methodWithNoReturnTypeStaticallyWithDescription',
							[],
							'Do something with a description statically, but what, who knows!'
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							true,
							new IdentifierTypeNode('bool'),
							'aStaticMethodThatHasAUniqueReturnTypeInThisClass',
							[],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							true,
							new IdentifierTypeNode('string'),
							'aStaticMethodThatHasAUniqueReturnTypeInThisClassWithDescription',
							[],
							'A Description.'
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new IdentifierTypeNode('int'),
							'getIntegerNoParams',
							[],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new IdentifierTypeNode('void'),
							'doSomethingNoParams',
							[],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new UnionTypeNode([
								new IdentifierTypeNode('self'),
								new IdentifierTypeNode('Bar'),
							]),
							'getFooOrBarNoParams',
							[],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							null,
							'methodWithNoReturnTypeNoParams',
							[],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							true,
							new IdentifierTypeNode('int'),
							'getIntegerStaticallyNoParams',
							[],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							true,
							new IdentifierTypeNode('void'),
							'doSomethingStaticallyNoParams',
							[],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							true,
							new UnionTypeNode([
								new IdentifierTypeNode('self'),
								new IdentifierTypeNode('Bar'),
							]),
							'getFooOrBarStaticallyNoParams',
							[],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new IdentifierTypeNode('static'),
							'methodWithNoReturnTypeStaticallyNoParams',
							[],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new IdentifierTypeNode('int'),
							'getIntegerWithDescriptionNoParams',
							[],
							'Get an integer with a description.'
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new IdentifierTypeNode('void'),
							'doSomethingWithDescriptionNoParams',
							[],
							'Do something with a description.'
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new UnionTypeNode([
								new IdentifierTypeNode('self'),
								new IdentifierTypeNode('Bar'),
							]),
							'getFooOrBarWithDescriptionNoParams',
							[],
							'Get a Foo or a Bar with a description.'
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							true,
							new IdentifierTypeNode('int'),
							'getIntegerStaticallyWithDescriptionNoParams',
							[],
							'Get an integer with a description statically.'
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							true,
							new IdentifierTypeNode('void'),
							'doSomethingStaticallyWithDescriptionNoParams',
							[],
							'Do something with a description statically.'
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							true,
							new UnionTypeNode([
								new IdentifierTypeNode('self'),
								new IdentifierTypeNode('Bar'),
							]),
							'getFooOrBarStaticallyWithDescriptionNoParams',
							[],
							'Get a Foo or a Bar with a description statically.'
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							true,
							new UnionTypeNode([
								new IdentifierTypeNode('bool'),
								new IdentifierTypeNode('string'),
							]),
							'aStaticMethodThatHasAUniqueReturnTypeInThisClassNoParams',
							[],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							true,
							new UnionTypeNode([
								new IdentifierTypeNode('string'),
								new IdentifierTypeNode('float'),
							]),
							'aStaticMethodThatHasAUniqueReturnTypeInThisClassWithDescriptionNoParams',
							[],
							'A Description.'
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new IdentifierTypeNode('\\Aws\\Result'),
							'publish',
							[
								new MethodTagValueParameterNode(
									new IdentifierTypeNode('array'),
									false,
									false,
									'$args',
									null
								),
							],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new IdentifierTypeNode('Image'),
							'rotate',
							[
								new MethodTagValueParameterNode(
									new IdentifierTypeNode('float'),
									true,
									true,
									'$angle',
									new ConstExprArrayNode([])
								),
								new MethodTagValueParameterNode(
									null,
									false,
									false,
									'$backgroundColor',
									null
								),
							],
							''
						)
					),
					new PhpDocTagNode(
						'@method',
						new MethodTagValueNode(
							false,
							new IdentifierTypeNode('Foo'),
							'overridenMethod',
							[],
							''
						)
					),
				]),
			],
			[
				'OK with multiline conditional return type',
				'/**
				  * @template TRandKey as array-key
				  * @template TRandVal
				  * @template TRandList as array<TRandKey, TRandVal>|XIterator<TRandKey, TRandVal>|Traversable<TRandKey, TRandVal>
				  *
				  * @param TRandList $list
				  *
				  * @return (
				  *        TRandList is array ? array<TRandKey, TRandVal> : (
				  *        TRandList is XIterator ?    XIterator<TRandKey, TRandVal> :
				  *        IteratorIterator<TRandKey, TRandVal>|LimitIterator<TRandKey, TRandVal>
				  * ))
				  */',
				new PhpDocNode([
					new PhpDocTagNode(
						'@template',
						new TemplateTagValueNode('TRandKey', new IdentifierTypeNode('array-key'), '')
					),
					new PhpDocTagNode(
						'@template',
						new TemplateTagValueNode('TRandVal', null, '')
					),
					new PhpDocTagNode(
						'@template',
						new TemplateTagValueNode(
							'TRandList',
							new UnionTypeNode([
								new GenericTypeNode(
									new IdentifierTypeNode('array'),
									[
										new IdentifierTypeNode('TRandKey'),
										new IdentifierTypeNode('TRandVal'),
									]
								),
								new GenericTypeNode(
									new IdentifierTypeNode('XIterator'),
									[
										new IdentifierTypeNode('TRandKey'),
										new IdentifierTypeNode('TRandVal'),
									]
								),
								new GenericTypeNode(
									new IdentifierTypeNode('Traversable'),
									[
										new IdentifierTypeNode('TRandKey'),
										new IdentifierTypeNode('TRandVal'),
									]
								),
							]),
							''
						)
					),
					new PhpDocTextNode(''),
					new PhpDocTagNode(
						'@param',
						new ParamTagValueNode(
							new IdentifierTypeNode('TRandList'),
							false,
							'$list',
							''
						)
					),
					new PhpDocTextNode(''),
					new PhpDocTagNode(
						'@return',
						new ReturnTagValueNode(
							new ConditionalTypeNode(
								new IdentifierTypeNode('TRandList'),
								new IdentifierTypeNode('array'),
								new GenericTypeNode(
									new IdentifierTypeNode('array'),
									[
										new IdentifierTypeNode('TRandKey'),
										new IdentifierTypeNode('TRandVal'),
									]
								),
								new ConditionalTypeNode(
									new IdentifierTypeNode('TRandList'),
									new IdentifierTypeNode('XIterator'),
									new GenericTypeNode(
										new IdentifierTypeNode('XIterator'),
										[
											new IdentifierTypeNode('TRandKey'),
											new IdentifierTypeNode('TRandVal'),
										]
									),
									new UnionTypeNode([
										new GenericTypeNode(
											new IdentifierTypeNode('IteratorIterator'),
											[
												new IdentifierTypeNode('TRandKey'),
												new IdentifierTypeNode('TRandVal'),
											]
										),
										new GenericTypeNode(
											new IdentifierTypeNode('LimitIterator'),
											[
												new IdentifierTypeNode('TRandKey'),
												new IdentifierTypeNode('TRandVal'),
											]
										),
									]),
									false
								),
								false
							),
							''
						)
					),
				]),
			],
		];
	}

	public function provideTemplateTagsData(): Iterator
	{
		yield [
			'OK without bound and description',
			'/** @template T */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@template',
					new TemplateTagValueNode(
						'T',
						null,
						''
					)
				),
			]),
		];

		yield [
			'OK without bound',
			'/** @template T the value type*/',
			new PhpDocNode([
				new PhpDocTagNode(
					'@template',
					new TemplateTagValueNode(
						'T',
						null,
						'the value type'
					)
				),
			]),
		];

		yield [
			'OK without description',
			'/** @template T of DateTime */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@template',
					new TemplateTagValueNode(
						'T',
						new IdentifierTypeNode('DateTime'),
						''
					)
				),
			]),
		];

		yield [
			'OK without description',
			'/** @template T as DateTime */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@template',
					new TemplateTagValueNode(
						'T',
						new IdentifierTypeNode('DateTime'),
						''
					)
				),
			]),
		];

		yield [
			'OK with bound and description',
			'/** @template T of DateTime the value type */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@template',
					new TemplateTagValueNode(
						'T',
						new IdentifierTypeNode('DateTime'),
						'the value type'
					)
				),
			]),
		];

		yield [
			'OK with bound and description',
			'/** @template T as DateTime the value type */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@template',
					new TemplateTagValueNode(
						'T',
						new IdentifierTypeNode('DateTime'),
						'the value type'
					)
				),
			]),
		];

		yield [
			'invalid without bound and description',
			'/** @template */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@template',
					new InvalidTagValueNode(
						'',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							14,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid without bound and with description',
			'/** @template #desc */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@template',
					new InvalidTagValueNode(
						'#desc',
						new ParserException(
							'#desc',
							Lexer::TOKEN_OTHER,
							14,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'OK with covariance',
			'/** @template-covariant T */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@template-covariant',
					new TemplateTagValueNode(
						'T',
						null,
						''
					)
				),
			]),
		];

		yield [
			'OK with contravariance',
			'/** @template-contravariant T */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@template-contravariant',
					new TemplateTagValueNode(
						'T',
						null,
						''
					)
				),
			]),
		];
	}

	public function provideExtendsTagsData(): Iterator
	{
		yield [
			'OK with one argument',
			'/** @extends Foo<A> */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@extends',
					new ExtendsTagValueNode(
						new GenericTypeNode(
							new IdentifierTypeNode('Foo'),
							[
								new IdentifierTypeNode('A'),
							]
						),
						''
					)
				),
			]),
		];

		yield [
			'OK with two arguments',
			'/** @extends Foo<A,B> */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@extends',
					new ExtendsTagValueNode(
						new GenericTypeNode(
							new IdentifierTypeNode('Foo'),
							[
								new IdentifierTypeNode('A'),
								new IdentifierTypeNode('B'),
							]
						),
						''
					)
				),
			]),
		];

		yield [
			'OK @implements',
			'/** @implements Foo<A,B> */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@implements',
					new ImplementsTagValueNode(
						new GenericTypeNode(
							new IdentifierTypeNode('Foo'),
							[
								new IdentifierTypeNode('A'),
								new IdentifierTypeNode('B'),
							]
						),
						''
					)
				),
			]),
		];

		yield [
			'OK @use',
			'/** @use Foo<A,B> */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@use',
					new UsesTagValueNode(
						new GenericTypeNode(
							new IdentifierTypeNode('Foo'),
							[
								new IdentifierTypeNode('A'),
								new IdentifierTypeNode('B'),
							]
						),
						''
					)
				),
			]),
		];

		yield [
			'OK with description',
			'/** @extends Foo<A> extends foo*/',
			new PhpDocNode([
				new PhpDocTagNode(
					'@extends',
					new ExtendsTagValueNode(
						new GenericTypeNode(
							new IdentifierTypeNode('Foo'),
							[new IdentifierTypeNode('A')]
						),
						'extends foo'
					)
				),
			]),
		];

		yield [
			'invalid without type',
			'/** @extends */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@extends',
					new InvalidTagValueNode(
						'',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							13,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid without arguments',
			'/** @extends Foo */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@extends',
					new InvalidTagValueNode(
						'Foo',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							17,
							Lexer::TOKEN_OPEN_ANGLE_BRACKET
						)
					)
				),
			]),
		];

		yield [
			'class-string in @return',
			'/** @return class-string */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new IdentifierTypeNode('class-string'),
						''
					)
				),
			]),
		];

		yield [
			'class-string in @return with description',
			'/** @return class-string some description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new IdentifierTypeNode('class-string'),
						'some description'
					)
				),
			]),
		];

		yield [
			'class-string in @param',
			'/** @param class-string $test */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new ParamTagValueNode(
						new IdentifierTypeNode('class-string'),
						false,
						'$test',
						''
					)
				),
			]),
		];

		yield [
			'class-string in @param with description',
			'/** @param class-string $test some description */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new ParamTagValueNode(
						new IdentifierTypeNode('class-string'),
						false,
						'$test',
						'some description'
					)
				),
			]),
		];
	}

	public function provideTypeAliasTagsData(): Iterator
	{
		yield [
			'OK',
			'/** @phpstan-type TypeAlias string|int */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@phpstan-type',
					new TypeAliasTagValueNode(
						'TypeAlias',
						new UnionTypeNode([
							new IdentifierTypeNode('string'),
							new IdentifierTypeNode('int'),
						])
					)
				),
			]),
		];

		yield [
			'OK with psalm syntax',
			'/** @psalm-type TypeAlias=string|int */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@psalm-type',
					new TypeAliasTagValueNode(
						'TypeAlias',
						new UnionTypeNode([
							new IdentifierTypeNode('string'),
							new IdentifierTypeNode('int'),
						])
					)
				),
			]),
		];

		yield [
			'invalid without type',
			'/** @phpstan-type TypeAlias */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@phpstan-type',
					new InvalidTagValueNode(
						'TypeAlias',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							28,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid empty',
			'/** @phpstan-type */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@phpstan-type',
					new InvalidTagValueNode(
						'',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							18,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];
	}

	public function provideTypeAliasImportTagsData(): Iterator
	{
		yield [
			'OK',
			'/** @phpstan-import-type TypeAlias from AnotherClass */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@phpstan-import-type',
					new TypeAliasImportTagValueNode(
						'TypeAlias',
						new IdentifierTypeNode('AnotherClass'),
						null
					)
				),
			]),
		];

		yield [
			'OK with alias',
			'/** @phpstan-import-type TypeAlias from AnotherClass as DifferentAlias */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@phpstan-import-type',
					new TypeAliasImportTagValueNode(
						'TypeAlias',
						new IdentifierTypeNode('AnotherClass'),
						'DifferentAlias'
					)
				),
			]),
		];

		yield [
			'invalid non-identifier from',
			'/** @phpstan-import-type TypeAlias from 42 */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@phpstan-import-type',
					new InvalidTagValueNode(
						'TypeAlias from 42',
						new ParserException(
							'42',
							Lexer::TOKEN_INTEGER,
							40,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];

		yield [
			'invalid non-simple-identifier from',
			'/** @phpstan-import-type TypeAlias from AnotherClass[] */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@phpstan-import-type',
					new InvalidTagValueNode(
						'Unexpected token "[", expected \'*/\' at offset 52',
						new ParserException(
							'[',
							Lexer::TOKEN_OPEN_SQUARE_BRACKET,
							52,
							Lexer::TOKEN_CLOSE_PHPDOC
						)
					)
				),
			]),
		];

		yield [
			'invalid missing from',
			'/** @phpstan-import-type TypeAlias */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@phpstan-import-type',
					new InvalidTagValueNode(
						'TypeAlias',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							35,
							Lexer::TOKEN_IDENTIFIER,
							'from'
						)
					)
				),
			]),
		];

		yield [
			'invalid missing from with alias',
			'/** @phpstan-import-type TypeAlias as DifferentAlias */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@phpstan-import-type',
					new InvalidTagValueNode(
						'TypeAlias as DifferentAlias',
						new ParserException(
							'as',
							Lexer::TOKEN_IDENTIFIER,
							35,
							Lexer::TOKEN_IDENTIFIER,
							'from'
						)
					)
				),
			]),
		];

		yield [
			'invalid empty',
			'/** @phpstan-import-type */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@phpstan-import-type',
					new InvalidTagValueNode(
						'',
						new ParserException(
							'*/',
							Lexer::TOKEN_CLOSE_PHPDOC,
							25,
							Lexer::TOKEN_IDENTIFIER
						)
					)
				),
			]),
		];
	}

	public function provideAssertTagsData(): Iterator
	{
		yield [
			'OK',
			'/** @phpstan-assert Type $var */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@phpstan-assert',
					new AssertTagValueNode(
						new IdentifierTypeNode('Type'),
						'$var',
						false,
						''
					)
				),
			]),
		];

		yield [
			'OK with psalm syntax',
			'/** @psalm-assert Type $var */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@psalm-assert',
					new AssertTagValueNode(
						new IdentifierTypeNode('Type'),
						'$var',
						false,
						''
					)
				),
			]),
		];

		yield [
			'OK with description',
			'/** @phpstan-assert Type $var assert Type to $var */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@phpstan-assert',
					new AssertTagValueNode(
						new IdentifierTypeNode('Type'),
						'$var',
						false,
						'assert Type to $var'
					)
				),
			]),
		];

		yield [
			'OK with union type',
			'/** @phpstan-assert Type|Other $var */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@phpstan-assert',
					new AssertTagValueNode(
						new UnionTypeNode([
							new IdentifierTypeNode('Type'),
							new IdentifierTypeNode('Other'),
						]),
						'$var',
						false,
						''
					)
				),
			]),
		];

		yield [
			'invalid $this->method()',
			'/** @phpstan-assert Type $this->method() */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@phpstan-assert',
					new InvalidTagValueNode(
						'Type $this->method()',
						new ParserException(
							'$this',
							Lexer::TOKEN_THIS_VARIABLE,
							25,
							Lexer::TOKEN_VARIABLE
						)
					)
				),
			]),
		];

		yield [
			'OK assert-if-true',
			'/** @phpstan-assert-if-true Type $var */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@phpstan-assert-if-true',
					new AssertTagValueNode(
						new IdentifierTypeNode('Type'),
						'$var',
						false,
						''
					)
				),
			]),
		];

		yield [
			'OK assert-if-false',
			'/** @phpstan-assert-if-false Type $var */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@phpstan-assert-if-false',
					new AssertTagValueNode(
						new IdentifierTypeNode('Type'),
						'$var',
						false,
						''
					)
				),
			]),
		];

		yield [
			'OK negated',
			'/** @phpstan-assert !Type $var */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@phpstan-assert',
					new AssertTagValueNode(
						new IdentifierTypeNode('Type'),
						'$var',
						true,
						''
					)
				),
			]),
		];
	}

	public function providerDebug(): Iterator
	{
		$sample = '/**
			 * Returns the schema for the field.
			 *
			 * This method is static because the field schema information is needed on
			 * creation of the field. FieldItemInterface objects instantiated at that
			 * time are not reliable as field settings might be missing.
			 *
			 * Computed fields having no schema should return an empty array.
			 */';
		yield [
			'OK class line',
			$sample,
			new PhpDocNode([
				new PhpDocTextNode('Returns the schema for the field.'),
				new PhpDocTextNode(''),
				new PhpDocTextNode('This method is static because the field schema information is needed on
creation of the field. FieldItemInterface objects instantiated at that
time are not reliable as field settings might be missing.'),
				new PhpDocTextNode(''),
				new PhpDocTextNode('Computed fields having no schema should return an empty array.'),
			]),
		];
	}

	public function provideRealWorldExampleData(): Iterator
	{
			$sample = "/**
			 * Returns the schema for the field.
			 *
			 * This method is static because the field schema information is needed on
			 * creation of the field. FieldItemInterface objects instantiated at that
			 * time are not reliable as field settings might be missing.
			 *
			 * Computed fields having no schema should return an empty array.
			 *
			 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface \$field_definition
			 *   The field definition.
			 *
			 * @return array
			 *   An empty array if there is no schema, or an associative array with the
			 *   following key/value pairs:
			 *   - columns: An array of Schema API column specifications, keyed by column
			 *     name. The columns need to be a subset of the properties defined in
			 *     propertyDefinitions(). The 'not null' property is ignored if present,
			 *     as it is determined automatically by the storage controller depending
			 *     on the table layout and the property definitions. It is recommended to
			 *     avoid having the column definitions depend on field settings when
			 *     possible. No assumptions should be made on how storage engines
			 *     internally use the original column name to structure their storage.
			 *   - unique keys: (optional) An array of Schema API unique key definitions.
			 *     Only columns that appear in the 'columns' array are allowed.
			 *   - indexes: (optional) An array of Schema API index definitions. Only
			 *     columns that appear in the 'columns' array are allowed. Those indexes
			 *     will be used as default indexes. Field definitions can specify
			 *     additional indexes or, at their own risk, modify the default indexes
			 *     specified by the field-type module. Some storage engines might not
			 *     support indexes.
			 *   - foreign keys: (optional) An array of Schema API foreign key
			 *     definitions. Note, however, that the field data is not necessarily
			 *     stored in SQL. Also, the possible usage is limited, as you cannot
			 *     specify another field as related, only existing SQL tables,
			 *     such as {taxonomy_term_data}.
			 */";
		yield [
			'OK FieldItemInterface::schema',
			$sample,
			new PhpDocNode([
				new PhpDocTextNode('Returns the schema for the field.'),
				new PhpDocTextNode(''),
				new PhpDocTextNode('This method is static because the field schema information is needed on
creation of the field. FieldItemInterface objects instantiated at that
time are not reliable as field settings might be missing.'),
				new PhpDocTextNode(''),
				new PhpDocTextNode('Computed fields having no schema should return an empty array.'),
				new PhpDocTextNode(''),
				new PhpDocTagNode(
					'@param',
					new ParamTagValueNode(
						new IdentifierTypeNode('\Drupal\Core\Field\FieldStorageDefinitionInterface'),
						false,
						'$field_definition',
						''
					)
				),
				new PhpDocTextNode('The field definition.'),
				new PhpDocTextNode(''),
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new IdentifierTypeNode('array'),
						''
					)
				),
				new PhpDocTextNode("An empty array if there is no schema, or an associative array with the
  following key/value pairs:
  - columns: An array of Schema API column specifications, keyed by column
    name. The columns need to be a subset of the properties defined in
    propertyDefinitions(). The 'not null' property is ignored if present,
    as it is determined automatically by the storage controller depending
    on the table layout and the property definitions. It is recommended to
    avoid having the column definitions depend on field settings when
    possible. No assumptions should be made on how storage engines
    internally use the original column name to structure their storage.
  - unique keys: (optional) An array of Schema API unique key definitions.
    Only columns that appear in the 'columns' array are allowed.
  - indexes: (optional) An array of Schema API index definitions. Only
    columns that appear in the 'columns' array are allowed. Those indexes
    will be used as default indexes. Field definitions can specify
    additional indexes or, at their own risk, modify the default indexes
    specified by the field-type module. Some storage engines might not
    support indexes.
  - foreign keys: (optional) An array of Schema API foreign key
    definitions. Note, however, that the field data is not necessarily
    stored in SQL. Also, the possible usage is limited, as you cannot
    specify another field as related, only existing SQL tables,
    such as {taxonomy_term_data}."),
			]),
		];

		$sample = '/**
     *  Parses a chunked request and return relevant information.
     *
     *  This function must return an array containing the following
     *  keys and their corresponding values:
     *    - last: Wheter this is the last chunk of the uploaded file
     *    - uuid: A unique id which distinguishes two uploaded files
     *            This uuid must stay the same among the task of
     *            uploading a chunked file.
     *    - index: A numerical representation of the currently uploaded
     *            chunk. Must be higher that in the previous request.
     *    - orig: The original file name.
     *
     * @param Request $request - The request object
     *
     * @return array
     */';
		yield [
			'OK AbstractChunkedController::parseChunkedRequest',
			$sample,
			new PhpDocNode([
				new PhpDocTextNode('Parses a chunked request and return relevant information.'),
				new PhpDocTextNode(''),
				new PhpDocTextNode('This function must return an array containing the following
 keys and their corresponding values:
   - last: Wheter this is the last chunk of the uploaded file
   - uuid: A unique id which distinguishes two uploaded files
           This uuid must stay the same among the task of
           uploading a chunked file.
   - index: A numerical representation of the currently uploaded
           chunk. Must be higher that in the previous request.
   - orig: The original file name.'),
				new PhpDocTextNode(''),
				new PhpDocTagNode(
					'@param',
					new ParamTagValueNode(
						new IdentifierTypeNode('Request'),
						false,
						'$request',
						'- The request object'
					)
				),
				new PhpDocTextNode(''),
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new IdentifierTypeNode('array'),
						''
					)
				),
			]),
		];

		yield [
			'Description with indented <code>',
			"/**
			  * Finder allows searching through directory trees using iterator.
			  *
			  * <code>
			  * Finder::findFiles('*.php')
			  *     ->size('> 10kB')
			  *     ->from('.')
			  *     ->exclude('temp');
			  * </code>
			  */",
			new PhpDocNode([
				new PhpDocTextNode('Finder allows searching through directory trees using iterator.'),
				new PhpDocTextNode(''),
				new PhpDocTextNode("<code>
Finder::findFiles('*.php')
    ->size('> 10kB')
    ->from('.')
    ->exclude('temp');
</code>"),
			]),
		];

		yield [
			'string literals in @return',
			"/** @return 'foo'|'bar' */",
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new UnionTypeNode([
							new ConstTypeNode(new ConstExprStringNode('foo')),
							new ConstTypeNode(new ConstExprStringNode('bar')),
						]),
						''
					)
				),
			]),
		];

		yield [
			'malformed const fetch',
			'/** @param Foo::** $a */',
			new PhpDocNode([
				new PhpDocTagNode('@param', new InvalidTagValueNode('Foo::** $a', new ParserException('*', Lexer::TOKEN_WILDCARD, 17, Lexer::TOKEN_VARIABLE))),
			]),
		];

		yield [
			'multiline generic types',
			'/**' . PHP_EOL .
			' * @implements Foo<' . PHP_EOL .
			' *    A, B' . PHP_EOL .
			' * >' . PHP_EOL .
			' */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@implements',
					new ImplementsTagValueNode(
						new GenericTypeNode(
							new IdentifierTypeNode('Foo'),
							[
								new IdentifierTypeNode('A'),
								new IdentifierTypeNode('B'),
							]
						),
						''
					)
				),
			]),
		];

		yield [
			'multiline generic types - leading comma',
			'/**' . PHP_EOL .
			' * @implements Foo<' . PHP_EOL .
			' *    A' . PHP_EOL .
			' *    , B' . PHP_EOL .
			' * >' . PHP_EOL .
			' */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@implements',
					new ImplementsTagValueNode(
						new GenericTypeNode(
							new IdentifierTypeNode('Foo'),
							[
								new IdentifierTypeNode('A'),
								new IdentifierTypeNode('B'),
							]
						),
						''
					)
				),
			]),
		];

		yield [
			'multiline generic types - traling comma',
			'/**' . PHP_EOL .
			' * @implements Foo<' . PHP_EOL .
			' *    A,' . PHP_EOL .
			' *    B,' . PHP_EOL .
			' * >' . PHP_EOL .
			' */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@implements',
					new ImplementsTagValueNode(
						new GenericTypeNode(
							new IdentifierTypeNode('Foo'),
							[
								new IdentifierTypeNode('A'),
								new IdentifierTypeNode('B'),
							]
						),
						''
					)
				),
			]),
		];

		yield [
			'multiline callable types',
			'/**' . PHP_EOL .
			' * @param callable(' . PHP_EOL .
			' *    A, B' . PHP_EOL .
			' * ): void $foo' . PHP_EOL .
			' */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new ParamTagValueNode(
						new CallableTypeNode(
							new IdentifierTypeNode('callable'),
							[
								new CallableTypeParameterNode(new IdentifierTypeNode('A'), false, false, '', false),
								new CallableTypeParameterNode(new IdentifierTypeNode('B'), false, false, '', false),
							],
							new IdentifierTypeNode('void')
						),
						false,
						'$foo',
						''
					)
				),
			]),
		];

		yield [
			'multiline callable types - leading comma',
			'/**' . PHP_EOL .
			' * @param callable(' . PHP_EOL .
			' *    A' . PHP_EOL .
			' *    , B' . PHP_EOL .
			' * ): void $foo' . PHP_EOL .
			' */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new ParamTagValueNode(
						new CallableTypeNode(
							new IdentifierTypeNode('callable'),
							[
								new CallableTypeParameterNode(new IdentifierTypeNode('A'), false, false, '', false),
								new CallableTypeParameterNode(new IdentifierTypeNode('B'), false, false, '', false),
							],
							new IdentifierTypeNode('void')
						),
						false,
						'$foo',
						''
					)
				),
			]),
		];

		yield [
			'multiline callable types - traling comma',
			'/**' . PHP_EOL .
			' * @param callable(' . PHP_EOL .
			' *    A,' . PHP_EOL .
			' *    B,' . PHP_EOL .
			' * ): void $foo' . PHP_EOL .
			' */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@param',
					new ParamTagValueNode(
						new CallableTypeNode(
							new IdentifierTypeNode('callable'),
							[
								new CallableTypeParameterNode(new IdentifierTypeNode('A'), false, false, '', false),
								new CallableTypeParameterNode(new IdentifierTypeNode('B'), false, false, '', false),
							],
							new IdentifierTypeNode('void')
						),
						false,
						'$foo',
						''
					)
				),
			]),
		];
	}

	public function provideDescriptionWithOrWithoutHtml(): Iterator
	{
		yield [
			'Description with HTML tags in @return tag (close tags together)',
			'/**' . PHP_EOL .
			' * @return Foo <strong>Important <i>description</i></strong>' . PHP_EOL .
			' */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new IdentifierTypeNode('Foo'),
						'<strong>Important <i>description</i></strong>'
					)
				),
			]),
		];

		yield [
			'Description with HTML tags in @throws tag (closed tags with text between)',
			'/**' . PHP_EOL .
			' * @throws FooException <strong>Important <em>description</em> etc</strong>' . PHP_EOL .
			' */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@throws',
					new ThrowsTagValueNode(
						new IdentifierTypeNode('FooException'),
						'<strong>Important <em>description</em> etc</strong>'
					)
				),
			]),
		];

		yield [
			'Description with HTML tags in @mixin tag',
			'/**' . PHP_EOL .
			' * @mixin Mixin <strong>Important description</strong>' . PHP_EOL .
			' */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@mixin',
					new MixinTagValueNode(
						new IdentifierTypeNode('Mixin'),
						'<strong>Important description</strong>'
					)
				),
			]),
		];

		yield [
			'Description with unclosed HTML tags in @return tag - unclosed HTML tag is parsed as generics',
			'/**' . PHP_EOL .
			' * @return Foo <strong>Important description' . PHP_EOL .
			' */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new ReturnTagValueNode(
						new GenericTypeNode(
							new IdentifierTypeNode('Foo'),
							[
								new IdentifierTypeNode('strong'),
							]
						),
						'Important description'
					)
				),
			]),
			new PhpDocNode([
				new PhpDocTagNode(
					'@return',
					new InvalidTagValueNode(
						'Foo <strong>Important description',
						new ParserException(
							'Important',
							Lexer::TOKEN_IDENTIFIER,
							27,
							Lexer::TOKEN_HORIZONTAL_WS
						)
					)
				),
			]),
		];
	}

	public function dataParseTagValue(): array
	{
		return [
			[
				'@param',
				'DateTimeImmutable::ATOM $a',
				new ParamTagValueNode(
					new ConstTypeNode(new ConstFetchNode('DateTimeImmutable', 'ATOM')),
					false,
					'$a',
					''
				),
			],
			[
				'@var',
				'$foo string[]',
				new InvalidTagValueNode(
					'$foo string[]',
					new ParserException(
						'$foo',
						Lexer::TOKEN_VARIABLE,
						0,
						Lexer::TOKEN_IDENTIFIER
					)
				),
			],
		];
	}

	public function provideTagsWithNumbers(): Iterator
	{
		yield [
			'OK without description and tag with number in it',
			'/** @special3 Foo  */',
			new PhpDocNode([
				new PhpDocTagNode(
					'@special3',
					new GenericTagValueNode('Foo')
				),
			]),
		];
	}

	/**
	 * @dataProvider dataParseTagValue
	 * @param PhpDocNode $expectedPhpDocNode
	 */
	public function testParseTagValue(string $tag, string $phpDoc, Node $expectedPhpDocNode): void
	{
		$this->executeTestParseTagValue($this->phpDocParser, $tag, $phpDoc, $expectedPhpDocNode);
		$this->executeTestParseTagValue($this->phpDocParserWithRequiredWhitespaceBeforeDescription, $tag, $phpDoc, $expectedPhpDocNode);
	}

	private function executeTestParseTagValue(PhpDocParser $phpDocParser, string $tag, string $phpDoc, Node $expectedPhpDocNode): void
	{
		$tokens = new TokenIterator($this->lexer->tokenize($phpDoc));
		$actualPhpDocNode = $phpDocParser->parseTagValue($tokens, $tag);

		$this->assertEquals($expectedPhpDocNode, $actualPhpDocNode);
		$this->assertSame((string) $expectedPhpDocNode, (string) $actualPhpDocNode);
		$this->assertSame(Lexer::TOKEN_END, $tokens->currentTokenType());
	}

	public function testBug132(): void
	{
		$tokens = new TokenIterator($this->lexer->tokenize('/**
 * @see \Symplify\SymfonyStaticDumper\Tests\Application\SymfonyStaticDumperApplicationTest
 */'));
		$phpDocNode = $this->phpDocParser->parse($tokens);
		$this->assertSame('/**
 * @see \Symplify\SymfonyStaticDumper\Tests\Application\SymfonyStaticDumperApplicationTest
 */', $phpDocNode->__toString());

		$seeNode = $phpDocNode->children[0];
		$this->assertInstanceOf(PhpDocTagNode::class, $seeNode);
		$this->assertInstanceOf(GenericTagValueNode::class, $seeNode->value);

		$this->assertSame('@see \Symplify\SymfonyStaticDumper\Tests\Application\SymfonyStaticDumperApplicationTest', $seeNode->__toString());
		$this->assertSame('\Symplify\SymfonyStaticDumper\Tests\Application\SymfonyStaticDumperApplicationTest', $seeNode->value->__toString());
	}

}
