diff --git a/src/Ast/PhpDoc/ParamClosureScopeTagValueNode.php b/src/Ast/PhpDoc/ParamClosureScopeTagValueNode.php new file mode 100644 index 00000000..76006aa8 --- /dev/null +++ b/src/Ast/PhpDoc/ParamClosureScopeTagValueNode.php @@ -0,0 +1,47 @@ +type = $type; + $this->parameterName = $parameterName; + $this->description = $description; + } + + public function __toString(): string + { + return trim("{$this->type} {$this->parameterName} {$this->description}"); + } + + /** + * @param array $properties + */ + public static function __set_state(array $properties): self + { + $instance = new self($properties['type'], $properties['parameterName'], $properties['description']); + if (isset($properties['attributes'])) { + foreach ($properties['attributes'] as $key => $value) { + $instance->setAttribute($key, $value); + } + } + return $instance; + } + +} diff --git a/src/Ast/PhpDoc/PhpDocNode.php b/src/Ast/PhpDoc/PhpDocNode.php index 1b06bbe1..6f2c68dd 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -107,6 +107,17 @@ public function getParamClosureThisTagValues(string $tagName = '@param-closure-t ); } + /** + * @return ParamClosureScopeTagValueNode[] + */ + public function getParamClosureScopeTagValues(string $tagName = '@param-closure-scope'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static fn (PhpDocTagValueNode $value): bool => $value instanceof ParamClosureScopeTagValueNode, + ); + } + /** * @return PureUnlessCallableIsImpureTagValueNode[] */ diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index f6734225..284536ab 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -357,6 +357,11 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph $tagValue = $this->parseParamClosureThisTagValue($tokens); break; + case '@param-closure-scope': + case '@phpstan-param-closure-scope': + $tagValue = $this->parseParamClosureScopeTagValue($tokens); + break; + case '@pure-unless-callable-is-impure': case '@phpstan-pure-unless-callable-is-impure': $tagValue = $this->parsePureUnlessCallableIsImpureTagValue($tokens); @@ -874,6 +879,15 @@ private function parseParamClosureThisTagValue(TokenIterator $tokens): Ast\PhpDo return new Ast\PhpDoc\ParamClosureThisTagValueNode($type, $parameterName, $description); } + private function parseParamClosureScopeTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamClosureScopeTagValueNode + { + $type = $this->typeParser->parse($tokens); + $parameterName = $this->parseRequiredVariableName($tokens); + $description = $this->parseOptionalDescription($tokens, false); + + return new Ast\PhpDoc\ParamClosureScopeTagValueNode($type, $parameterName, $description); + } + private function parsePureUnlessCallableIsImpureTagValue(TokenIterator $tokens): Ast\PhpDoc\PureUnlessCallableIsImpureTagValueNode { $parameterName = $this->parseRequiredVariableName($tokens); diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index db7d3db7..f8bab98b 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -21,6 +21,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamClosureScopeTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamClosureThisTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode; @@ -362,6 +363,9 @@ private function printTagValue(PhpDocTagValueNode $node): string if ($node instanceof ParamClosureThisTagValueNode) { return trim("{$node->type} {$node->parameterName} {$node->description}"); } + if ($node instanceof ParamClosureScopeTagValueNode) { + return trim("{$node->type} {$node->parameterName} {$node->description}"); + } if ($node instanceof PureUnlessCallableIsImpureTagValueNode) { return trim("{$node->parameterName} {$node->description}"); } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index eacece63..2482bbc7 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -29,6 +29,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamClosureScopeTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamClosureThisTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode; @@ -103,6 +104,7 @@ protected function setUp(): void * @dataProvider provideParamLaterInvokedCallableTagsData * @dataProvider provideTypelessParamTagsData * @dataProvider provideParamClosureThisTagsData + * @dataProvider provideParamClosureScopeTagsData * @dataProvider providePureUnlessCallableIsImpureTagsData * @dataProvider providePureUnlessParameterIsPassedTagsData * @dataProvider provideVarTagsData @@ -733,6 +735,54 @@ public function provideParamClosureThisTagsData(): Iterator ]; } + public function provideParamClosureScopeTagsData(): Iterator + { + yield [ + 'OK', + '/** @param-closure-scope Foo $a */', + new PhpDocNode([ + new PhpDocTagNode( + '@param-closure-scope', + new ParamClosureScopeTagValueNode( + new IdentifierTypeNode('Foo'), + '$a', + '', + ), + ), + ]), + ]; + + yield [ + 'OK with prefix', + '/** @phpstan-param-closure-scope Foo $a */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-param-closure-scope', + new ParamClosureScopeTagValueNode( + new IdentifierTypeNode('Foo'), + '$a', + '', + ), + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @param-closure-scope Foo $a test */', + new PhpDocNode([ + new PhpDocTagNode( + '@param-closure-scope', + new ParamClosureScopeTagValueNode( + new IdentifierTypeNode('Foo'), + '$a', + 'test', + ), + ), + ]), + ]; + } + public function providePureUnlessCallableIsImpureTagsData(): Iterator { yield [ @@ -7829,6 +7879,7 @@ public function testReturnTypeLinesAndIndexes(string $phpDoc, array $lines): voi * @dataProvider provideParamImmediatelyInvokedCallableTagsData * @dataProvider provideParamLaterInvokedCallableTagsData * @dataProvider provideParamClosureThisTagsData + * @dataProvider provideParamClosureScopeTagsData * @dataProvider provideVarTagsData * @dataProvider provideReturnTagsData * @dataProvider provideThrowsTagsData diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 3f791927..44f62807 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -18,6 +18,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArray; use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineArrayItem; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamClosureScopeTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamClosureThisTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamImmediatelyInvokedCallableTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamLaterInvokedCallableTagValueNode; @@ -2145,6 +2146,25 @@ public function enterNode(Node $node) }, ]; + yield [ + '/** @param-closure-scope Foo $test haha */', + '/** @param-closure-scope Bar $taste hehe */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ParamClosureScopeTagValueNode) { + $node->type = new IdentifierTypeNode('Bar'); + $node->parameterName = '$taste'; + $node->description = 'hehe'; + } + + return $node; + } + + }, + ]; + yield [ '/** @pure-unless-callable-is-impure $foo test */', '/** @pure-unless-callable-is-impure $bar foo */',