lexer = new Lexer(); $constExprParser = new ConstExprParser(); $this->phpDocParser = new PhpDocParser(new TypeParser($constExprParser), $constExprParser); } /** * @dataProvider provideParamTagsData * @dataProvider provideVarTagsData * @dataProvider provideReturnTagsData * @dataProvider provideThrowsTagsData * @dataProvider provideMixinTagsData * @dataProvider provideDeprecatedTagsData * @dataProvider providePropertyTagsData * @dataProvider provideMethodTagsData * @dataProvider provideSingleLinePhpDocData * @dataProvider provideMultiLinePhpDocData * @dataProvider provideTemplateTagsData * @dataProvider provideExtendsTagsData * @dataProvider provideRealWorldExampleData * @param string $label * @param string $input * @param PhpDocNode $expectedPhpDocNode * @param int $nextTokenType */ public function testParse(string $label, string $input, PhpDocNode $expectedPhpDocNode, int $nextTokenType = Lexer::TOKEN_END): void { $tokens = new TokenIterator($this->lexer->tokenize($input)); $actualPhpDocNode = $this->phpDocParser->parse($tokens); $this->assertEquals($expectedPhpDocNode, $actualPhpDocNode, $label); $this->assertSame((string) $expectedPhpDocNode, (string) $actualPhpDocNode); $this->assertSame($nextTokenType, $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 [ 'invalid without type, parameter name and description', '/** @param */', new PhpDocNode([ new PhpDocTagNode( '@param', new InvalidTagValueNode( '', new \PHPStan\PhpDocParser\Parser\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 \PHPStan\PhpDocParser\Parser\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 \PHPStan\PhpDocParser\Parser\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 \PHPStan\PhpDocParser\Parser\ParserException( '$foo', Lexer::TOKEN_VARIABLE, 16, Lexer::TOKEN_CLOSE_PARENTHESES ) ) ), ]), ]; yield [ 'invalid with broken type (2)', '/** @param Foo */', new PhpDocNode([ new PhpDocTagNode( '@return', new ReturnTagValueNode( new UnionTypeNode([ new IdentifierTypeNode('A'), new GenericTypeNode( new IdentifierTypeNode('B'), [ new ConstTypeNode(new ConstExprIntegerNode('123')), ] ), ]), '' ) ), ]), ]; } 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 \PHPStan\PhpDocParser\Parser\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 \PHPStan\PhpDocParser\Parser\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 \PHPStan\PhpDocParser\Parser\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 \PHPStan\PhpDocParser\Parser\ParserException( '#desc', Lexer::TOKEN_OTHER, 17, Lexer::TOKEN_IDENTIFIER ) ) ), ]), ]; yield [ 'generic @mixin', '/** @mixin Foo */', 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 * * @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 ') ), 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 \PHPStan\PhpDocParser\Parser\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 \PHPStan\PhpDocParser\Parser\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 \PHPStan\PhpDocParser\Parser\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)' ) ), ]), ]; } 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', [], '' ) ), ]), ], ]; } 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 \PHPStan\PhpDocParser\Parser\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 \PHPStan\PhpDocParser\Parser\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, '' ) ), ]), ]; } public function provideExtendsTagsData(): \Iterator { yield [ 'OK with one argument', '/** @extends Foo */', new PhpDocNode([ new PhpDocTagNode( '@extends', new ExtendsTagValueNode( new GenericTypeNode( new IdentifierTypeNode('Foo'), [ new IdentifierTypeNode('A'), ] ), '' ) ), ]), ]; yield [ 'OK with two arguments', '/** @extends Foo */', new PhpDocNode([ new PhpDocTagNode( '@extends', new ExtendsTagValueNode( new GenericTypeNode( new IdentifierTypeNode('Foo'), [ new IdentifierTypeNode('A'), new IdentifierTypeNode('B'), ] ), '' ) ), ]), ]; yield [ 'OK @implements', '/** @implements Foo */', new PhpDocNode([ new PhpDocTagNode( '@implements', new ImplementsTagValueNode( new GenericTypeNode( new IdentifierTypeNode('Foo'), [ new IdentifierTypeNode('A'), new IdentifierTypeNode('B'), ] ), '' ) ), ]), ]; yield [ 'OK @use', '/** @use Foo */', new PhpDocNode([ new PhpDocTagNode( '@use', new UsesTagValueNode( new GenericTypeNode( new IdentifierTypeNode('Foo'), [ new IdentifierTypeNode('A'), new IdentifierTypeNode('B'), ] ), '' ) ), ]), ]; yield [ 'OK with description', '/** @extends Foo 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 \PHPStan\PhpDocParser\Parser\ParserException( '*/', Lexer::TOKEN_CLOSE_PHPDOC, 13, Lexer::TOKEN_IDENTIFIER ) ) ), ]), ]; yield [ 'invalid without arguments', '/** @extends Foo */', new PhpDocNode([ new PhpDocTagNode( '@extends', new InvalidTagValueNode( 'Foo', new \PHPStan\PhpDocParser\Parser\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 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:'), new PhpDocTextNode('- 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.'), new PhpDocTextNode('- unique keys: (optional) An array of Schema API unique key definitions. Only columns that appear in the \'columns\' array are allowed.'), new PhpDocTextNode('- 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.'), new PhpDocTextNode('- 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:'), new PhpDocTextNode('- last: Wheter this is the last chunk of the uploaded file'), new PhpDocTextNode('- uuid: A unique id which distinguishes two uploaded files This uuid must stay the same among the task of uploading a chunked file.'), new PhpDocTextNode('- index: A numerical representation of the currently uploaded chunk. Must be higher that in the previous request.'), new PhpDocTextNode('- 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 [ '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')), ]), '' ) ), ]), ]; } }