diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 6bb37c490e..35abe1e8b2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -143,6 +143,11 @@ parameters: count: 1 path: src/PhpDoc/PhpDocBlock.php + - + message: "#^Call to function method_exists\\(\\) with PHPStan\\\\PhpDocParser\\\\Ast\\\\PhpDoc\\\\PhpDocNode and 'getParamOutTypeTagV…' will always evaluate to true\\.$#" + count: 1 + path: src/PhpDoc/PhpDocNodeResolver.php + - message: "#^Call to function method_exists\\(\\) with PHPStan\\\\PhpDocParser\\\\Ast\\\\PhpDoc\\\\PhpDocNode and 'getSelfOutTypeTagVa…' will always evaluate to true\\.$#" count: 1 @@ -161,6 +166,11 @@ parameters: count: 1 path: src/PhpDoc/StubValidator.php + - + message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\ParamOutTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\ParamOutTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" + count: 1 + path: src/PhpDoc/Tag/ParamOutTag.php + - message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\ParamTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\ParamTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" count: 1 diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index f4a8669133..fb557a7179 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2517,6 +2517,7 @@ public function enterTrait(ClassReflection $traitReflection): self /** * @api * @param Type[] $phpDocParameterTypes + * @param Type[] $parameterOutTypes */ public function enterClassMethod( Node\Stmt\ClassMethod $classMethod, @@ -2533,6 +2534,7 @@ public function enterClassMethod( ?Assertions $asserts = null, ?Type $selfOutType = null, ?string $phpDocComment = null, + array $parameterOutTypes = [], ): self { if (!$this->isInClass()) { @@ -2560,6 +2562,7 @@ public function enterClassMethod( $asserts ?? Assertions::createEmpty(), $selfOutType, $phpDocComment, + array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $parameterOutTypes), ), !$classMethod->isStatic(), ); @@ -2626,6 +2629,7 @@ private function getRealParameterDefaultValues(Node\FunctionLike $functionLike): /** * @api * @param Type[] $phpDocParameterTypes + * @param Type[] $parameterOutTypes */ public function enterFunction( Node\Stmt\Function_ $function, @@ -2641,6 +2645,7 @@ public function enterFunction( bool $acceptsNamedArguments = true, ?Assertions $asserts = null, ?string $phpDocComment = null, + array $parameterOutTypes = [], ): self { return $this->enterFunctionLike( @@ -2662,6 +2667,7 @@ public function enterFunction( $acceptsNamedArguments, $asserts ?? Assertions::createEmpty(), $phpDocComment, + array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $parameterOutTypes), ), false, ); diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index d399da3361..e65444e02a 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -114,6 +114,7 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Native\NativeMethodReflection; use PHPStan\Reflection\Native\NativeParameterReflection; +use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\PhpMethodReflection; @@ -407,7 +408,7 @@ private function processStmtNode( $hasYield = false; $throwPoints = []; $this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback); - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts] = $this->getPhpDocs($scope, $stmt); + [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { $this->processParamNode($param, $scope, $nodeCallback); @@ -431,6 +432,7 @@ private function processStmtNode( $acceptsNamedArguments, $asserts, $phpDocComment, + $phpDocParameterOutTypes, ); $functionReflection = $functionScope->getFunction(); if (!$functionReflection instanceof FunctionReflection) { @@ -470,7 +472,7 @@ private function processStmtNode( $hasYield = false; $throwPoints = []; $this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback); - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType] = $this->getPhpDocs($scope, $stmt); + [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { $this->processParamNode($param, $scope, $nodeCallback); @@ -495,6 +497,7 @@ private function processStmtNode( $asserts, $selfOutType, $phpDocComment, + $phpDocParameterOutTypes, ); if ($stmt->name->toLowerString() === '__construct') { @@ -1806,6 +1809,7 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $functionReflection->getVariants(), ); } + if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr; } @@ -2043,11 +2047,13 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra } } } + if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr; } $result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context); $scope = $result->getScope(); + if ($methodReflection !== null) { $hasSideEffects = $methodReflection->hasSideEffects(); if ($hasSideEffects->yes() || $methodReflection->getName() === '__construct') { @@ -2174,12 +2180,14 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); } } + if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr; } $result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context, $closureBindScope ?? null); $scope = $result->getScope(); $scopeFunction = $scope->getFunction(); + if ( $methodReflection !== null && !$methodReflection->isStatic() @@ -2529,6 +2537,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); } } + if ($parametersAcceptor !== null) { $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; } @@ -3272,8 +3281,21 @@ private function processArgs( ?MutatingScope $closureBindScope = null, ): ExpressionResult { + $paramOutTypes = []; if ($parametersAcceptor !== null) { $parameters = $parametersAcceptor->getParameters(); + + foreach ($parameters as $parameter) { + if (!$parameter instanceof ParameterReflectionWithPhpDocs) { + continue; + } + + if ($parameter->getOutType() === null) { + continue; + } + + $paramOutTypes[$parameter->getName()] = TemplateTypeHelper::resolveTemplateTypes($parameter->getOutType(), $parametersAcceptor->getResolvedTemplateTypeMap()); + } } if ($calleeReflection !== null) { @@ -3286,20 +3308,29 @@ private function processArgs( $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; $nodeCallback($originalArg, $scope); if (isset($parameters) && $parametersAcceptor !== null) { + $byRefType = new MixedType(); $assignByReference = false; if (isset($parameters[$i])) { $assignByReference = $parameters[$i]->passedByReference()->createsNewVariable(); $parameterType = $parameters[$i]->getType(); + + if (isset($paramOutTypes[$parameters[$i]->getName()])) { + $byRefType = $paramOutTypes[$parameters[$i]->getName()]; + } } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { $lastParameter = $parameters[count($parameters) - 1]; $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); $parameterType = $lastParameter->getType(); + + if (isset($paramOutTypes[$lastParameter->getName()])) { + $byRefType = $paramOutTypes[$lastParameter->getName()]; + } } if ($assignByReference) { $argValue = $arg->value; if ($argValue instanceof Variable && is_string($argValue->name)) { - $scope = $scope->assignVariable($argValue->name, new MixedType(), new MixedType()); + $scope = $scope->assignVariable($argValue->name, $byRefType, new MixedType()); } } } @@ -4039,7 +4070,7 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection } /** - * @return array{TemplateTypeMap, Type[], ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type} + * @return array{TemplateTypeMap, array, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array} */ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array { @@ -4065,6 +4096,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $trait = $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null; $resolvedPhpDoc = null; $functionName = null; + $phpDocParameterOutTypes = []; if ($node instanceof Node\Stmt\ClassMethod) { if (!$scope->isInClass()) { @@ -4149,6 +4181,9 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n } $phpDocParameterTypes[$paramName] = $paramType; } + foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) { + $phpDocParameterOutTypes[$paramName] = $paramOutTag->getType(); + } if ($node instanceof Node\FunctionLike) { $nativeReturnType = $scope->getFunctionType($node->getReturnType(), false, false); $phpDocReturnType = $this->getPhpDocReturnType($resolvedPhpDoc, $nativeReturnType); @@ -4168,7 +4203,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; } - return [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType]; + return [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes]; } private function transformStaticType(ClassReflection $declaringClass, Type $type): Type diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index dda42a4a96..742dbfbb79 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -11,6 +11,7 @@ use PHPStan\PhpDoc\Tag\MethodTag; use PHPStan\PhpDoc\Tag\MethodTagParameter; use PHPStan\PhpDoc\Tag\MixinTag; +use PHPStan\PhpDoc\Tag\ParamOutTag; use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\PhpDoc\Tag\PropertyTag; use PHPStan\PhpDoc\Tag\ReturnTag; @@ -318,6 +319,34 @@ public function resolveParamTags(PhpDocNode $phpDocNode, NameScope $nameScope): return $resolved; } + /** + * @return array + */ + public function resolveParamOutTags(PhpDocNode $phpDocNode, NameScope $nameScope): array + { + if (!method_exists($phpDocNode, 'getParamOutTypeTagValues')) { + return []; + } + + $resolved = []; + + foreach (['@param-out', '@psalm-param-out', '@phpstan-param-out'] as $tagName) { + foreach ($phpDocNode->getParamOutTypeTagValues($tagName) as $tagValue) { + $parameterName = substr($tagValue->parameterName, 1); + $parameterType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); + if ($this->shouldSkipType($tagName, $parameterType)) { + continue; + } + + $resolved[$parameterName] = new ParamOutTag( + $parameterType, + ); + } + } + + return $resolved; + } + public function resolveReturnTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?ReturnTag { $resolved = null; diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index 37134bf630..c3215d3058 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -9,6 +9,7 @@ use PHPStan\PhpDoc\Tag\ImplementsTag; use PHPStan\PhpDoc\Tag\MethodTag; use PHPStan\PhpDoc\Tag\MixinTag; +use PHPStan\PhpDoc\Tag\ParamOutTag; use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\PhpDoc\Tag\PropertyTag; use PHPStan\PhpDoc\Tag\ReturnTag; @@ -77,6 +78,9 @@ class ResolvedPhpDocBlock /** @var array|false */ private array|false $paramTags = false; + /** @var array|false */ + private array|false $paramOutTags = false; + private ReturnTag|false|null $returnTag = false; private ThrowsTag|false|null $throwsTag = false; @@ -163,6 +167,7 @@ public static function createEmpty(): self $self->implementsTags = []; $self->usesTags = []; $self->paramTags = []; + $self->paramOutTags = []; $self->returnTag = null; $self->throwsTag = null; $self->mixinTags = []; @@ -216,6 +221,7 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->implementsTags = $this->getImplementsTags(); $result->usesTags = $this->getUsesTags(); $result->paramTags = self::mergeParamTags($this->getParamTags(), $parents, $parentPhpDocBlocks); + $result->paramOutTags = self::mergeParamOutTags($this->getParamOutTags(), $parents, $parentPhpDocBlocks); $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $parents, $parentPhpDocBlocks); $result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parents); $result->mixinTags = $this->getMixinTags(); @@ -256,6 +262,16 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self $newParamTags[$parameterNameMapping[$key]] = $paramTag; } + $paramOutTags = $this->getParamOutTags(); + + $newParamOutTags = []; + foreach ($paramOutTags as $key => $paramOutTag) { + if (!array_key_exists($key, $parameterNameMapping)) { + continue; + } + $newParamOutTags[$parameterNameMapping[$key]] = $paramOutTag; + } + $returnTag = $this->getReturnTag(); if ($returnTag !== null) { $transformedType = TypeTraverser::map($returnTag->getType(), static function (Type $type, callable $traverse) use ($parameterNameMapping): Type { @@ -298,6 +314,7 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self $self->implementsTags = $this->implementsTags; $self->usesTags = $this->usesTags; $self->paramTags = $newParamTags; + $self->paramOutTags = $newParamOutTags; $self->returnTag = $returnTag; $self->throwsTag = $this->throwsTag; $self->mixinTags = $this->mixinTags; @@ -453,6 +470,20 @@ public function getParamTags(): array return $this->paramTags; } + /** + * @return array + */ + public function getParamOutTags(): array + { + if ($this->paramOutTags === false) { + $this->paramOutTags = $this->phpDocNodeResolver->resolveParamOutTags( + $this->phpDocNode, + $this->getNameScope(), + ); + } + return $this->paramOutTags; + } + public function getReturnTag(): ?ReturnTag { if (is_bool($this->returnTag)) { @@ -876,6 +907,41 @@ private static function mergeThrowsTags(?ThrowsTag $throwsTag, array $parents): return null; } + /** + * @param array $paramOutTags + * @param array $parents + * @param array $parentPhpDocBlocks + * @return array + */ + private static function mergeParamOutTags(array $paramOutTags, array $parents, array $parentPhpDocBlocks): array + { + foreach ($parents as $i => $parent) { + $paramOutTags = self::mergeOneParentParamOutTags($paramOutTags, $parent, $parentPhpDocBlocks[$i]); + } + + return $paramOutTags; + } + + /** + * @param array $paramOutTags + * @param ResolvedPhpDocBlock $parent + * @return array + */ + private static function mergeOneParentParamOutTags(array $paramOutTags, self $parent, PhpDocBlock $phpDocBlock): array + { + $parentParamOutTags = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamOutTags()); + + foreach ($parentParamOutTags as $name => $parentParamTag) { + if (array_key_exists($name, $paramOutTags)) { + continue; + } + + $paramOutTags[$name] = self::resolveTemplateTypeInTag($parentParamTag, $phpDocBlock); + } + + return $paramOutTags; + } + /** * @template T of TypedTag * @param T $tag diff --git a/src/PhpDoc/Tag/ParamOutTag.php b/src/PhpDoc/Tag/ParamOutTag.php new file mode 100644 index 0000000000..c40018cb45 --- /dev/null +++ b/src/PhpDoc/Tag/ParamOutTag.php @@ -0,0 +1,28 @@ +type; + } + + /** + * @return self + */ + public function withType(Type $type): TypedTag + { + return new self($type); + } + +} diff --git a/src/Reflection/BetterReflection/BetterReflectionProvider.php b/src/Reflection/BetterReflection/BetterReflectionProvider.php index a0fb40f2fc..0766abd21a 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProvider.php +++ b/src/Reflection/BetterReflection/BetterReflectionProvider.php @@ -28,6 +28,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; +use PHPStan\PhpDoc\Tag\ParamOutTag; use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassNameHelper; @@ -266,6 +267,7 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $isPure = null; $asserts = Assertions::createEmpty(); $phpDocComment = null; + $phpDocParameterOutTags = []; $resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $reflectionFunction->getParameters())); if ($resolvedPhpDoc === null && $reflectionFunction->getFileName() !== false && $reflectionFunction->getDocComment() !== false) { @@ -287,6 +289,7 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection if ($resolvedPhpDoc->hasPhpDocString()) { $phpDocComment = $resolvedPhpDoc->getPhpDocString(); } + $phpDocParameterOutTags = $resolvedPhpDoc->getParamOutTags(); } return $this->functionReflectionFactory->create( @@ -303,6 +306,7 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $isPure, $asserts, $phpDocComment, + array_map(static fn (ParamOutTag $paramOutTag): Type => $paramOutTag->getType(), $phpDocParameterOutTags), ); } diff --git a/src/Reflection/FunctionReflectionFactory.php b/src/Reflection/FunctionReflectionFactory.php index 97c100fe7d..28382fd315 100644 --- a/src/Reflection/FunctionReflectionFactory.php +++ b/src/Reflection/FunctionReflectionFactory.php @@ -12,6 +12,7 @@ interface FunctionReflectionFactory /** * @param Type[] $phpDocParameterTypes + * @param Type[] $phpDocParameterOutTypes */ public function create( ReflectionFunction $reflection, @@ -27,6 +28,7 @@ public function create( ?bool $isPure, Assertions $asserts, ?string $phpDocComment, + array $phpDocParameterOutTypes, ): PhpFunctionReflection; } diff --git a/src/Reflection/Native/NativeFunctionReflection.php b/src/Reflection/Native/NativeFunctionReflection.php index 92732ede1c..440a41b0a4 100644 --- a/src/Reflection/Native/NativeFunctionReflection.php +++ b/src/Reflection/Native/NativeFunctionReflection.php @@ -4,7 +4,7 @@ use PHPStan\Reflection\Assertions; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\VoidType; @@ -15,7 +15,7 @@ class NativeFunctionReflection implements FunctionReflection private Assertions $assertions; /** - * @param ParametersAcceptor[] $variants + * @param ParametersAcceptorWithPhpDocs[] $variants */ public function __construct( private string $name, @@ -41,7 +41,7 @@ public function getFileName(): ?string } /** - * @return ParametersAcceptor[] + * @return ParametersAcceptorWithPhpDocs[] */ public function getVariants(): array { diff --git a/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php b/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php index b5c01f9b90..14f79af016 100644 --- a/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php +++ b/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php @@ -18,6 +18,7 @@ public function __construct( private PassedByReference $passedByReference, private bool $variadic, private ?Type $defaultValue, + private ?Type $outType, ) { } @@ -62,6 +63,11 @@ public function getDefaultValue(): ?Type return $this->defaultValue; } + public function getOutType(): ?Type + { + return $this->outType; + } + /** * @param mixed[] $properties */ @@ -76,6 +82,7 @@ public static function __set_state(array $properties): self $properties['passedByReference'], $properties['variadic'], $properties['defaultValue'], + $properties['outType'], ); } diff --git a/src/Reflection/ParameterReflectionWithPhpDocs.php b/src/Reflection/ParameterReflectionWithPhpDocs.php index e0ede3fd51..e7f880cbf8 100644 --- a/src/Reflection/ParameterReflectionWithPhpDocs.php +++ b/src/Reflection/ParameterReflectionWithPhpDocs.php @@ -11,4 +11,6 @@ public function getPhpDocType(): Type; public function getNativeType(): Type; + public function getOutType(): ?Type; + } diff --git a/src/Reflection/Php/DummyParameterWithPhpDocs.php b/src/Reflection/Php/DummyParameterWithPhpDocs.php new file mode 100644 index 0000000000..e1b4df10df --- /dev/null +++ b/src/Reflection/Php/DummyParameterWithPhpDocs.php @@ -0,0 +1,42 @@ +phpDocType; + } + + public function getNativeType(): Type + { + return $this->nativeType; + } + + public function getOutType(): ?Type + { + return $this->outType; + } + +} diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index d230817b2c..f7ab2e78bf 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -518,6 +518,8 @@ private function createMethod( $phpDocParameterTypes = []; $phpDocReturnType = null; $stubPhpDocPair = null; + $stubPhpParameterOutTypes = []; + $phpDocParameterOutTypes = []; if (count($methodSignatures) === 1) { $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $methodReflection->getName(), array_map(static fn (ParameterSignature $parameterSignature): string => $parameterSignature->getName(), $methodSignature->getParameters())); if ($stubPhpDocPair !== null) { @@ -551,6 +553,13 @@ private function createMethod( $selfOutType = $selfOutTypeTag->getType(); } + foreach ($stubPhpDoc->getParamOutTags() as $name => $paramOutTag) { + $stubPhpParameterOutTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( + $paramOutTag->getType(), + $templateTypeMap, + ); + } + if ($declaringClassName === $stubDeclaringClass->getName() && $stubPhpDoc->hasPhpDocString()) { $phpDocComment = $stubPhpDoc->getPhpDocString(); } @@ -588,6 +597,10 @@ private function createMethod( $phpDocComment = $phpDocBlock->getPhpDocString(); } + foreach ($phpDocBlock->getParamOutTags() as $name => $paramOutTag) { + $phpDocParameterOutTypes[$name] = $paramOutTag->getType(); + } + $signatureParameters = $methodSignature->getParameters(); foreach ($reflectionMethod->getParameters() as $paramI => $reflectionParameter) { if (!array_key_exists($paramI, $signatureParameters)) { @@ -598,7 +611,7 @@ private function createMethod( } } } - $variants[] = $this->createNativeMethodVariant($methodSignature, $stubPhpDocParameterTypes, $stubPhpDocParameterVariadicity, $stubPhpDocReturnType, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping); + $variants[] = $this->createNativeMethodVariant($methodSignature, $stubPhpDocParameterTypes, $stubPhpDocParameterVariadicity, $stubPhpDocReturnType, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping, $stubPhpParameterOutTypes, $phpDocParameterOutTypes); } if ($this->signatureMapProvider->hasMethodMetadata($declaringClassName, $methodReflection->getName())) { @@ -711,6 +724,7 @@ private function createMethod( } $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); + foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) { if (array_key_exists($paramName, $phpDocParameterTypes)) { continue; @@ -723,6 +737,15 @@ private function createMethod( $phpDocBlockClassReflection->getActiveTemplateTypeMap(), ); } + + $phpDocParameterOutTypes = []; + foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) { + $phpDocParameterOutTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( + $paramOutTag->getType(), + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + ); + } + $nativeReturnType = TypehintHelper::decideTypeFromReflection( $methodReflection->getReturnType(), null, @@ -758,6 +781,7 @@ private function createMethod( $asserts, $selfOutType, $phpDocComment, + $phpDocParameterOutTypes, ); } @@ -766,6 +790,8 @@ private function createMethod( * @param array $stubPhpDocParameterVariadicity * @param array $phpDocParameterTypes * @param array $phpDocParameterNameMapping + * @param array $stubPhpDocParameterOutTypes + * @param array $phpDocParameterOutTypes */ private function createNativeMethodVariant( FunctionSignature $methodSignature, @@ -775,12 +801,15 @@ private function createNativeMethodVariant( array $phpDocParameterTypes, ?Type $phpDocReturnType, array $phpDocParameterNameMapping, + array $stubPhpDocParameterOutTypes, + array $phpDocParameterOutTypes, ): FunctionVariantWithPhpDocs { $parameters = []; foreach ($methodSignature->getParameters() as $parameterSignature) { $type = null; $phpDocType = null; + $parameterOutType = null; $phpDocParameterName = $phpDocParameterNameMapping[$parameterSignature->getName()] ?? $parameterSignature->getName(); @@ -791,6 +820,12 @@ private function createNativeMethodVariant( $phpDocType = $phpDocParameterTypes[$phpDocParameterName]; } + if (isset($stubPhpDocParameterOutTypes[$parameterSignature->getName()])) { + $parameterOutType = $stubPhpDocParameterOutTypes[$parameterSignature->getName()]; + } elseif (isset($phpDocParameterOutTypes[$phpDocParameterName])) { + $parameterOutType = $phpDocParameterOutTypes[$phpDocParameterName]; + } + $parameters[] = new NativeParameterWithPhpDocsReflection( $phpDocParameterName, $parameterSignature->isOptional(), @@ -800,6 +835,7 @@ private function createNativeMethodVariant( $parameterSignature->passedByReference(), $stubPhpDocParameterVariadicity[$parameterSignature->getName()] ?? $parameterSignature->isVariadic(), $parameterSignature->getDefaultValue(), + $parameterOutType ?? $parameterSignature->getOutType(), ); } @@ -913,7 +949,7 @@ private function inferAndCachePropertyTypes( $constructor, $namespace, )->enterClass($declaringClass); - [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType] = $this->nodeScopeResolver->getPhpDocs($classScope, $methodNode); + [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->nodeScopeResolver->getPhpDocs($classScope, $methodNode); $methodScope = $classScope->enterClassMethod( $methodNode, $templateTypeMap, @@ -929,6 +965,7 @@ private function inferAndCachePropertyTypes( $asserts, $selfOutType, $phpDocComment, + $phpDocParameterOutTypes, ); $propertyTypes = []; diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index a43175cb6b..2e3ffec0d0 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -38,6 +38,7 @@ class PhpFunctionFromParserNodeReflection implements FunctionReflection * @param Type[] $realParameterTypes * @param Type[] $phpDocParameterTypes * @param Type[] $realParameterDefaultValues + * @param Type[] $parameterOutTypes */ public function __construct( FunctionLike $functionLike, @@ -57,6 +58,7 @@ public function __construct( private bool $acceptsNamedArguments, private Assertions $assertions, private ?string $phpDocComment, + private array $parameterOutTypes, ) { $this->functionLike = $functionLike; @@ -134,6 +136,7 @@ private function getParameters(): array : PassedByReference::createNo(), $this->realParameterDefaultValues[$parameter->var->name] ?? null, $parameter->variadic, + $this->parameterOutTypes[$parameter->var->name] ?? null, ); } diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index 286661f1f5..bbd1cb62c2 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -39,6 +39,7 @@ class PhpFunctionReflection implements FunctionReflection /** * @param Type[] $phpDocParameterTypes + * @param Type[] $phpDocParameterOutTypes */ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, @@ -58,6 +59,7 @@ public function __construct( private ?bool $isPure, private Assertions $asserts, private ?string $phpDocComment, + private array $phpDocParameterOutTypes, ) { } @@ -112,6 +114,7 @@ private function getParameters(): array $reflection, $this->phpDocParameterTypes[$reflection->getName()] ?? null, null, + $this->phpDocParameterOutTypes[$reflection->getName()] ?? null, ), $this->reflection->getParameters()); } diff --git a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php index f5bd9d54bc..b27632580e 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -48,6 +48,7 @@ public function __construct( Assertions $assertions, private ?Type $selfOutType, ?string $phpDocComment, + array $parameterOutTypes, ) { $name = strtolower($classMethod->name->name); @@ -91,6 +92,7 @@ public function __construct( $acceptsNamedArguments, $assertions, $phpDocComment, + $parameterOutTypes, ); } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index df4a11e086..aee826370c 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -60,6 +60,7 @@ class PhpMethodReflection implements ExtendedMethodReflection /** * @param Type[] $phpDocParameterTypes + * @param Type[] $phpDocParameterOutTypes */ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, @@ -82,6 +83,7 @@ public function __construct( private Assertions $asserts, private ?Type $selfOutType, private ?string $phpDocComment, + private array $phpDocParameterOutTypes, ) { } @@ -204,6 +206,7 @@ private function getParameters(): array $reflection, $this->phpDocParameterTypes[$reflection->getName()] ?? null, $this->getDeclaringClass()->getName(), + $this->phpDocParameterOutTypes[$reflection->getName()] ?? null, ), $this->reflection->getParameters()); } diff --git a/src/Reflection/Php/PhpMethodReflectionFactory.php b/src/Reflection/Php/PhpMethodReflectionFactory.php index 89166560d0..a73c617df0 100644 --- a/src/Reflection/Php/PhpMethodReflectionFactory.php +++ b/src/Reflection/Php/PhpMethodReflectionFactory.php @@ -12,6 +12,7 @@ interface PhpMethodReflectionFactory /** * @param Type[] $phpDocParameterTypes + * @param Type[] $phpDocParameterOutTypes */ public function create( ClassReflection $declaringClass, @@ -29,6 +30,7 @@ public function create( Assertions $asserts, ?Type $selfOutType, ?string $phpDocComment, + array $phpDocParameterOutTypes, ): PhpMethodReflection; } diff --git a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php index 0625921242..492b6eacb9 100644 --- a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php @@ -23,6 +23,7 @@ public function __construct( private PassedByReference $passedByReference, private ?Type $defaultValue, private bool $variadic, + private ?Type $outType, ) { } @@ -80,4 +81,9 @@ public function getDefaultValue(): ?Type return $this->defaultValue; } + public function getOutType(): ?Type + { + return $this->outType; + } + } diff --git a/src/Reflection/Php/PhpParameterReflection.php b/src/Reflection/Php/PhpParameterReflection.php index e24ea24077..a1999f37dd 100644 --- a/src/Reflection/Php/PhpParameterReflection.php +++ b/src/Reflection/Php/PhpParameterReflection.php @@ -25,6 +25,7 @@ public function __construct( private ReflectionParameter $reflection, private ?Type $phpDocType, private ?string $declaringClassName, + private ?Type $outType, ) { } @@ -114,4 +115,9 @@ public function getDefaultValue(): ?Type return null; } + public function getOutType(): ?Type + { + return $this->outType; + } + } diff --git a/src/Reflection/ResolvedFunctionVariant.php b/src/Reflection/ResolvedFunctionVariant.php index 092570c6cd..d378140f11 100644 --- a/src/Reflection/ResolvedFunctionVariant.php +++ b/src/Reflection/ResolvedFunctionVariant.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection; use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\Reflection\Php\DummyParameterWithPhpDocs; use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateType; @@ -55,20 +56,41 @@ public function getParameters(): array $parameters = $this->parameters; if ($parameters === null) { - $parameters = array_map(fn (ParameterReflection $param): ParameterReflection => new DummyParameter( - $param->getName(), - TypeUtils::resolveLateResolvableTypes( - TemplateTypeHelper::resolveTemplateTypes( - $this->resolveConditionalTypesForParameter($param->getType()), - $this->resolvedTemplateTypeMap, - ), - false, - ), - $param->isOptional(), - $param->passedByReference(), - $param->isVariadic(), - $param->getDefaultValue(), - ), $this->parametersAcceptor->getParameters()); + $parameters = array_map( + function (ParameterReflection $param): ParameterReflection { + $paramType = TypeUtils::resolveLateResolvableTypes( + TemplateTypeHelper::resolveTemplateTypes( + $this->resolveConditionalTypesForParameter($param->getType()), + $this->resolvedTemplateTypeMap, + ), + false, + ); + + if ($param instanceof ParameterReflectionWithPhpDocs) { + return new DummyParameterWithPhpDocs( + $param->getName(), + $paramType, + $param->isOptional(), + $param->passedByReference(), + $param->isVariadic(), + $param->getDefaultValue(), + $param->getNativeType(), + $param->getPhpDocType(), + $param->getOutType(), + ); + } + + return new DummyParameter( + $param->getName(), + $paramType, + $param->isOptional(), + $param->passedByReference(), + $param->isVariadic(), + $param->getDefaultValue(), + ); + }, + $this->parametersAcceptor->getParameters(), + ); $this->parameters = $parameters; } diff --git a/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php b/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php index 5207e08386..d4ce2b06af 100644 --- a/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php +++ b/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php @@ -100,6 +100,7 @@ private function createSignature(string $functionName, ?string $className, ?Refl $nativeParameters[$i]->getDefaultValueExpression(), InitializerExprContext::fromReflectionParameter($nativeParameters[$i]), ) : null, + $parameter->getOutType(), ); } diff --git a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index 74308caccc..12ae34d53e 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -9,9 +9,9 @@ use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\Reflection\Assertions; -use PHPStan\Reflection\FunctionVariant; +use PHPStan\Reflection\FunctionVariantWithPhpDocs; use PHPStan\Reflection\Native\NativeFunctionReflection; -use PHPStan\Reflection\Native\NativeParameterReflection; +use PHPStan\Reflection\Native\NativeParameterWithPhpDocsReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; @@ -19,12 +19,14 @@ use PHPStan\Type\FloatType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\IntegerType; +use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\StringAlwaysAcceptingObjectWithToStringType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; use PHPStan\Type\UnionType; +use function array_key_exists; use function array_map; use function strtolower; @@ -52,6 +54,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $throwType = null; $reflectionFunctionAdapter = null; $isDeprecated = false; + $phpDocReturnType = null; $asserts = Assertions::createEmpty(); $docComment = null; try { @@ -84,15 +87,16 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $throwType = $phpDoc->getThrowsTag()->getType(); } $asserts = Assertions::createFromResolvedPhpDocBlock($phpDoc); + $phpDocReturnType = $this->getReturnTypeFromPhpDoc($phpDoc); } $variants = []; $functionSignatures = $this->signatureMapProvider->getFunctionSignatures($lowerCasedFunctionName, null, $reflectionFunctionAdapter); foreach ($functionSignatures as $functionSignature) { - $variants[] = new FunctionVariant( + $variants[] = new FunctionVariantWithPhpDocs( TemplateTypeMap::createEmpty(), null, - array_map(static function (ParameterSignature $parameterSignature) use ($lowerCasedFunctionName, $phpDoc): NativeParameterReflection { + array_map(static function (ParameterSignature $parameterSignature) use ($lowerCasedFunctionName, $phpDoc): NativeParameterWithPhpDocsReflection { $type = $parameterSignature->getType(); $phpDocType = null; @@ -137,17 +141,22 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef ); } - return new NativeParameterReflection( + return new NativeParameterWithPhpDocsReflection( $parameterSignature->getName(), $parameterSignature->isOptional(), TypehintHelper::decideType($type, $phpDocType), + $phpDocType ?? new MixedType(), + $type, $parameterSignature->passedByReference(), $parameterSignature->isVariadic(), $parameterSignature->getDefaultValue(), + $phpDoc !== null ? NativeFunctionReflectionProvider::getParamOutTypeFromPhpDoc($parameterSignature->getName(), $phpDoc) : null, ); }, $functionSignature->getParameters()), $functionSignature->isVariadic(), - TypehintHelper::decideType($functionSignature->getReturnType(), $phpDoc !== null ? $this->getReturnTypeFromPhpDoc($phpDoc) : null), + TypehintHelper::decideType($functionSignature->getReturnType(), $phpDocReturnType), + $phpDocReturnType ?? new MixedType(), + $functionSignature->getReturnType(), ); } @@ -181,4 +190,15 @@ private function getReturnTypeFromPhpDoc(ResolvedPhpDocBlock $phpDoc): ?Type return $returnTag->getType(); } + private static function getParamOutTypeFromPhpDoc(string $paramName, ResolvedPhpDocBlock $stubPhpDoc): ?Type + { + $paramOutTags = $stubPhpDoc->getParamOutTags(); + + if (array_key_exists($paramName, $paramOutTags)) { + return $paramOutTags[$paramName]->getType(); + } + + return null; + } + } diff --git a/src/Reflection/SignatureMap/ParameterSignature.php b/src/Reflection/SignatureMap/ParameterSignature.php index a06284186a..fc9316c300 100644 --- a/src/Reflection/SignatureMap/ParameterSignature.php +++ b/src/Reflection/SignatureMap/ParameterSignature.php @@ -16,6 +16,7 @@ public function __construct( private PassedByReference $passedByReference, private bool $variadic, private ?Type $defaultValue, + private ?Type $outType, ) { } @@ -55,4 +56,9 @@ public function getDefaultValue(): ?Type return $this->defaultValue; } + public function getOutType(): ?Type + { + return $this->outType; + } + } diff --git a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php index ec36447f27..50b65d949b 100644 --- a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php +++ b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php @@ -245,6 +245,7 @@ private function mergeSignatures(FunctionSignature $nativeSignature, FunctionSig $nativeParameter->passedByReference()->yes() ? $functionMapParameter->passedByReference() : $nativeParameter->passedByReference(), $nativeParameter->isVariadic(), $nativeParameter->getDefaultValue(), + $nativeParameter->getOutType(), ); } @@ -342,6 +343,7 @@ private function getSignature( $param->default, InitializerExprContext::fromStubParameter($className, $stubFile, $function), ) : null, + null, ); $variadic = $variadic || $param->variadic; diff --git a/src/Reflection/SignatureMap/SignatureMapParser.php b/src/Reflection/SignatureMap/SignatureMapParser.php index f7e1a844e1..9d79f9b354 100644 --- a/src/Reflection/SignatureMap/SignatureMapParser.php +++ b/src/Reflection/SignatureMap/SignatureMapParser.php @@ -72,6 +72,7 @@ private function getParameters(array $parameterMap): array $passedByReference, $isVariadic, null, + null, ); } diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php index f37d39e227..6548c4b6fe 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php @@ -7,8 +7,10 @@ use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\FunctionVariant; use PHPStan\Reflection\ParameterReflection; +use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\Reflection\Php\DummyParameterWithPhpDocs; use PHPStan\Reflection\ResolvedMethodReflection; use PHPStan\Type\StaticType; use PHPStan\Type\Type; @@ -78,14 +80,33 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass, $variants = array_map(fn (ParametersAcceptor $acceptor): ParametersAcceptor => new FunctionVariant( $acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), - array_map(fn (ParameterReflection $parameter): ParameterReflection => new DummyParameter( - $parameter->getName(), - $this->transformStaticType($parameter->getType()), - $parameter->isOptional(), - $parameter->passedByReference(), - $parameter->isVariadic(), - $parameter->getDefaultValue(), - ), $acceptor->getParameters()), + array_map( + function (ParameterReflection $parameter): ParameterReflection { + if ($parameter instanceof ParameterReflectionWithPhpDocs) { + return new DummyParameterWithPhpDocs( + $parameter->getName(), + $this->transformStaticType($parameter->getType()), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + $parameter->getNativeType(), + $parameter->getPhpDocType(), + $parameter->getOutType(), + ); + } + + return new DummyParameter( + $parameter->getName(), + $this->transformStaticType($parameter->getType()), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + ); + }, + $acceptor->getParameters(), + ), $acceptor->isVariadic(), $this->transformStaticType($acceptor->getReturnType()), ), $method->getVariants()); diff --git a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php index e17cccb90d..e9f413a9f2 100644 --- a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php @@ -6,6 +6,8 @@ use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; use PHPStan\Internal\SprintfHelper; +use PHPStan\PhpDoc\Tag\ParamOutTag; +use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -63,78 +65,105 @@ public function processNode(Node $node, Scope $scope): array ); $nativeParameterTypes = $this->getNativeParameterTypes($node, $scope); $nativeReturnType = $this->getNativeReturnType($node, $scope); + $byRefParameters = $this->getByRefParameters($node); $errors = []; - foreach ($resolvedPhpDoc->getParamTags() as $parameterName => $phpDocParamTag) { - $phpDocParamType = $phpDocParamTag->getType(); - if (!isset($nativeParameterTypes[$parameterName])) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @param references unknown parameter: $%s', - $parameterName, - ))->identifier('phpDoc.unknownParameter')->metadata(['parameterName' => $parameterName])->build(); + foreach ([$resolvedPhpDoc->getParamTags(), $resolvedPhpDoc->getParamOutTags()] as $parameters) { + foreach ($parameters as $parameterName => $phpDocParamTag) { + $phpDocParamType = $phpDocParamTag->getType(); + $tagName = $phpDocParamTag instanceof ParamTag ? '@param' : '@param-out'; - } elseif ( - $this->unresolvableTypeHelper->containsUnresolvableType($phpDocParamType) - ) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @param for parameter $%s contains unresolvable type.', - $parameterName, - ))->build(); + if (!isset($nativeParameterTypes[$parameterName])) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s references unknown parameter: $%s', + $tagName, + $parameterName, + ))->identifier('phpDoc.unknownParameter')->metadata(['parameterName' => $parameterName])->build(); - } else { - $nativeParamType = $nativeParameterTypes[$parameterName]; - if ( - $phpDocParamTag->isVariadic() - && $phpDocParamType instanceof ArrayType - && !$nativeParamType instanceof ArrayType + } elseif ( + $this->unresolvableTypeHelper->containsUnresolvableType($phpDocParamType) ) { - $phpDocParamType = $phpDocParamType->getItemType(); - } - $isParamSuperType = $nativeParamType->isSuperTypeOf($phpDocParamType); - - $escapedParameterName = SprintfHelper::escapeFormatString($parameterName); - - $errors = array_merge($errors, $this->genericObjectTypeCheck->check( - $phpDocParamType, - sprintf( - 'PHPDoc tag @param for parameter $%s contains generic type %%s but %%s %%s is not generic.', - $escapedParameterName, - ), - sprintf( - 'Generic type %%s in PHPDoc tag @param for parameter $%s does not specify all template types of %%s %%s: %%s', - $escapedParameterName, - ), - sprintf( - 'Generic type %%s in PHPDoc tag @param for parameter $%s specifies %%d template types, but %%s %%s supports only %%d: %%s', - $escapedParameterName, - ), - sprintf( - 'Type %%s in generic type %%s in PHPDoc tag @param for parameter $%s is not subtype of template type %%s of %%s %%s.', - $escapedParameterName, - ), - )); - - if ($isParamSuperType->no()) { $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @param for parameter $%s with type %s is incompatible with native type %s.', + 'PHPDoc tag %s for parameter $%s contains unresolvable type.', + $tagName, $parameterName, - $phpDocParamType->describe(VerbosityLevel::typeOnly()), - $nativeParamType->describe(VerbosityLevel::typeOnly()), ))->build(); - } elseif ($isParamSuperType->maybe()) { - $errorBuilder = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @param for parameter $%s with type %s is not subtype of native type %s.', - $parameterName, - $phpDocParamType->describe(VerbosityLevel::typeOnly()), - $nativeParamType->describe(VerbosityLevel::typeOnly()), + } else { + $nativeParamType = $nativeParameterTypes[$parameterName]; + if ( + $phpDocParamTag instanceof ParamTag + && $phpDocParamTag->isVariadic() + && $phpDocParamType instanceof ArrayType + && !$nativeParamType instanceof ArrayType + ) { + $phpDocParamType = $phpDocParamType->getItemType(); + } + $isParamSuperType = $nativeParamType->isSuperTypeOf($phpDocParamType); + + $escapedParameterName = SprintfHelper::escapeFormatString($parameterName); + $escapedTagName = SprintfHelper::escapeFormatString($tagName); + + $errors = array_merge($errors, $this->genericObjectTypeCheck->check( + $phpDocParamType, + sprintf( + 'PHPDoc tag %s for parameter $%s contains generic type %%s but %%s %%s is not generic.', + $escapedTagName, + $escapedParameterName, + ), + sprintf( + 'Generic type %%s in PHPDoc tag %s for parameter $%s does not specify all template types of %%s %%s: %%s', + $escapedTagName, + $escapedParameterName, + ), + sprintf( + 'Generic type %%s in PHPDoc tag %s for parameter $%s specifies %%d template types, but %%s %%s supports only %%d: %%s', + $escapedTagName, + $escapedParameterName, + ), + sprintf( + 'Type %%s in generic type %%s in PHPDoc tag %s for parameter $%s is not subtype of template type %%s of %%s %%s.', + $escapedTagName, + $escapedParameterName, + ), )); - if ($phpDocParamType instanceof TemplateType) { - $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocParamType->getName(), $nativeParamType->describe(VerbosityLevel::typeOnly()))); + + if ($phpDocParamTag instanceof ParamOutTag) { + if (!$byRefParameters[$parameterName]) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Parameter $%s for PHPDoc tag %s is not passed by reference.', + $parameterName, + $tagName, + ))->build(); + + } + continue; } - $errors[] = $errorBuilder->build(); + if ($isParamSuperType->no()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for parameter $%s with type %s is incompatible with native type %s.', + $tagName, + $parameterName, + $phpDocParamType->describe(VerbosityLevel::typeOnly()), + $nativeParamType->describe(VerbosityLevel::typeOnly()), + ))->build(); + + } elseif ($isParamSuperType->maybe()) { + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for parameter $%s with type %s is not subtype of native type %s.', + $tagName, + $parameterName, + $phpDocParamType->describe(VerbosityLevel::typeOnly()), + $nativeParamType->describe(VerbosityLevel::typeOnly()), + )); + if ($phpDocParamType instanceof TemplateType) { + $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocParamType->getName(), $nativeParamType->describe(VerbosityLevel::typeOnly()))); + } + + $errors[] = $errorBuilder->build(); + } } } } @@ -202,6 +231,22 @@ private function getNativeParameterTypes(Node\FunctionLike $node, Scope $scope): return $nativeParameterTypes; } + /** + * @return array + */ + private function getByRefParameters(Node\FunctionLike $node): array + { + $nativeParameterTypes = []; + foreach ($node->getParams() as $parameter) { + if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { + throw new ShouldNotHappenException(); + } + $nativeParameterTypes[$parameter->var->name] = $parameter->byRef; + } + + return $nativeParameterTypes; + } + private function getNativeReturnType(Node\FunctionLike $node, Scope $scope): Type { return $scope->getFunctionType($node->getReturnType(), false, false); diff --git a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php index cc564741d8..5827851d0b 100644 --- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -21,6 +21,7 @@ class InvalidPHPStanDocTagRule implements Rule private const POSSIBLE_PHPSTAN_TAGS = [ '@phpstan-param', + '@phpstan-param-out', '@phpstan-var', '@phpstan-template', '@phpstan-extends', diff --git a/stubs/arrayFunctions.stub b/stubs/arrayFunctions.stub index fc0ee3a599..9a4c581594 100644 --- a/stubs/arrayFunctions.stub +++ b/stubs/arrayFunctions.stub @@ -17,37 +17,40 @@ function array_reduce( ) {} /** - * @template T of mixed + * @template TKey as (int|string) + * @template T + * @template TArray as array * - * @param array $one - * @param callable(T, T): int $two + * @param TArray $array + * @param callable(T,T):int $callback + * @param-out (TArray is non-empty-array ? non-empty-array : array) $array */ -function uasort( - array &$one, - callable $two -): bool {} +function uasort(array &$array, callable $callback): bool +{} /** - * @template T of mixed + * @template T + * @template TArray as array * - * @param array $one - * @param callable(T, T): int $two + * @param TArray $array + * @param callable(T,T):int $callback + * @param-out (TArray is non-empty-array ? non-empty-list : list) $array */ -function usort( - array &$one, - callable $two -): bool {} +function usort(array &$array, callable $callback): bool +{} /** - * @template T of array-key + * @template TKey as (int|string) + * @template T + * @template TArray as array * - * @param array $one - * @param callable(T, T): int $two + * @param TArray $array + * @param callable(TKey,TKey):int $callback + * @param-out (TArray is non-empty-array ? non-empty-array : array) $array */ -function uksort( - array &$one, - callable $two -): bool {} +function uksort(array &$array, callable $callback): bool +{ +} /** * @template T of mixed diff --git a/stubs/core.stub b/stubs/core.stub index 13b31ffc7b..b4be3097f7 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -67,3 +67,159 @@ function base64_encode(string $string) : string {} * @return ($string is non-empty-string ? non-empty-string : string) */ function bin2hex(string $string): string {} + +/** + * @param array $result + * @param-out array> $result + */ +function parse_str(string $string, array &$result): void {} + +/** @param-out float $percent */ +function similar_text(string $string1, string $string2, float &$percent = null) : int {} + +/** + * @param mixed $output + * @param mixed $result_code + * + * @param-out list $output + * @param-out int $result_code + * + * @return string|false + */ +function exec(string $command, &$output, &$result_code) {} + +/** + * @param mixed $result_code + * @param-out int $result_code + * + * @return string|false + */ +function system(string $command, &$result_code) {} + +/** + * @param mixed $result_code + * @param-out int $result_code + */ +function passthru(string $command, &$result_code): ?bool {} + + +/** + * @template T + * @template TArray as array + * + * @param TArray $array + * @param-out (TArray is non-empty-array ? non-empty-list : list) $array + */ +function shuffle(array &$array): bool +{ +} + +/** + * @template T + * @template TArray as array + * + * @param TArray $array + * @param-out (TArray is non-empty-array ? non-empty-list : list) $array + */ +function sort(array &$array, int $flags = SORT_REGULAR): bool +{ +} + +/** + * @template T + * @template TArray as array + * + * @param TArray $array + * @param-out (TArray is non-empty-array ? non-empty-list : list) $array + */ +function rsort(array &$array, int $flags = SORT_REGULAR): bool +{ +} + +/** + * @param string $string + * @param-out null $string + */ +function sodium_memzero(string &$string): void +{ +} + +/** + * @param mixed $war + * @param mixed $vars + * @param-out string|int|float|null $war + * @param-out string|int|float|null $vars + * + * @return int|array|null + */ +function sscanf(string $string, string $format, &$war, &...$vars) {} + +/** + * @template TFlags as int + * + * @param string $pattern + * @param string $subject + * @param mixed $matches + * @param TFlags $flags + * @param-out ( + * TFlags is 1 + * ? array> + * : (TFlags is 2 + * ? list> + * : (TFlags is 256|257 + * ? array> + * : (TFlags is 258 + * ? list> + * : (TFlags is 512|513 + * ? array> + * : (TFlags is 514 + * ? list> + * : (TFlags is 770 + * ? list> + * : array + * ) + * ) + * ) + * ) + * ) + * ) + * ) $matches + * @return int|false + */ +function preg_match_all($pattern, $subject, &$matches = [], int $flags = 1, int $offset = 0) {} + +/** + * @template TFlags as int-mask<0, 256, 512> + * + * @param string $pattern + * @param string $subject + * @param mixed $matches + * @param TFlags $flags + * @param-out ( + * TFlags is 256 + * ? array + * : (TFlags is 512 + * ? array + * : (TFlags is 768 + * ? array + * : array + * ) + * ) + * ) $matches + * @return 1|0|false + */ +function preg_match($pattern, $subject, &$matches = [], int $flags = 0, int $offset = 0) {} + +/** + * @template TRead of null|array + * @template TWrite of null|array + * @template TExcept of null|array + * @param TRead $read + * @param TWrite $write + * @param TExcept $except + * @return false|0|positive-int + * @param-out (TRead is null ? null : array) $read + * @param-out (TWrite is null ? null : array) $write + * @param-out (TExcept is null ? null : array) $except + */ +function stream_select(?array &$read, ?array &$write, ?array &$except, ?int $seconds, ?int $microseconds = null) {} diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index b1f697bf02..026cfe9ca5 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -302,7 +302,7 @@ public function dataAssignInIf(): array $testScope, 'matches', TrinaryLogic::createYes(), - 'mixed', + 'array', ], [ $testScope, @@ -343,7 +343,7 @@ public function dataAssignInIf(): array $testScope, 'matches2', TrinaryLogic::createMaybe(), - 'mixed', + 'array', ], [ $testScope, @@ -355,13 +355,13 @@ public function dataAssignInIf(): array $testScope, 'matches3', TrinaryLogic::createYes(), - 'mixed', + 'array', ], [ $testScope, 'matches4', TrinaryLogic::createMaybe(), - 'mixed', + 'array', ], [ $testScope, @@ -415,7 +415,7 @@ public function dataAssignInIf(): array $testScope, 'ternaryMatches', TrinaryLogic::createYes(), - 'mixed', + 'array', ], [ $testScope, @@ -7951,7 +7951,7 @@ public function dataPassedByReference(): array '$arr', ], [ - 'mixed', + 'array', '$matches', ], [ diff --git a/tests/PHPStan/Analyser/ParamOutTypeTest.php b/tests/PHPStan/Analyser/ParamOutTypeTest.php new file mode 100644 index 0000000000..b29b6fbd0c --- /dev/null +++ b/tests/PHPStan/Analyser/ParamOutTypeTest.php @@ -0,0 +1,37 @@ +gatherAssertTypes(__DIR__ . '/data/param-out.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + __DIR__ . '/typeAliases.neon', + __DIR__ . '/param-out.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/data/param-out.php b/tests/PHPStan/Analyser/data/param-out.php new file mode 100644 index 0000000000..41348df067 --- /dev/null +++ b/tests/PHPStan/Analyser/data/param-out.php @@ -0,0 +1,347 @@ + + */ +class ExtendsFooBar extends FooBar { + /** + * @param-out string $s + */ + function subMethod(?string &$s): void + { + } + + /** + * @param-out string $s + */ + function overriddenMethod(?string &$s): void + { + } + + function overriddenButinheritedPhpDocMethod(?string &$s): void + { + } + + public function renamedParams(int $x, int &$y) { + parent::renamedParams($x, $y); + } + + /** + * @param-out array $b + */ + public function paramOutOverridden(int $a, int &$b) { + } + +} + +class OutFromStub { + function stringOut(string &$string): void + { + } +} + +/** + * @param-out bool $s + */ +function takesNullableBool(?bool &$s) : void { + $s = true; +} + +/** + * @param-out int $var + */ +function variadicFoo(&...$var): void +{ + $var[0] = 2; + $var[1] = 2; +} + +/** + * @param-out string $s + * @param-out int $var + */ +function variadicFoo2(?string &$s, &...$var): void +{ + $s = ''; + $var[0] = 2; + $var[1] = 2; +} + +function foo1(?string $s): void { + assertType('string|null', $s); + addFoo($s); + assertType('string', $s); +} + +function foo2($mixed): void { + assertType('mixed', $mixed); + addFoo($mixed); + assertType('string', $mixed); +} + +/** + * @param FooBar $fooBar + * @return void + */ +function foo3($mixed, $fooBar): void { + assertType('mixed', $mixed); + $fooBar->genericClassFoo($mixed); + assertType('int', $mixed); +} + +function foo6(): void { + $b = false; + takesNullableBool($b); + + assertType('bool', $b); +} + +function foo7(): void { + variadicFoo( $a, $b); + assertType('int', $a); + assertType('int', $b); + + variadicFoo2($s, $a, $b); + assertType('string', $s); + assertType('int', $a); + assertType('int', $b); +} + +function foo8(string $s): void { + sodium_memzero($s); + assertType('null', $s); +} + +function foo9(?string $s): void { + $c = new OutFromStub(); + $c->stringOut($s); + assertType('string', $s); +} + +function foo10(?string $s): void { + $c = new ExtendsFooBar(); + $c->baseMethod($s); + assertType('string', $s); +} + +function foo11(?string $s): void { + $c = new ExtendsFooBar(); + $c->subMethod($s); + assertType('string', $s); +} + +function foo12(?string $s): void { + $c = new ExtendsFooBar(); + $c->overriddenMethod($s); + assertType('string', $s); +} + +function foo13(?string $s): void { + $c = new ExtendsFooBar(); + $c->overriddenButinheritedPhpDocMethod($s); + assertType('string', $s); +} + +/** + * @param array $a + * @param non-empty-array $nonEmptyArray + */ +function foo14(array $a, $nonEmptyArray): void { + \shuffle($a); + assertType('list', $a); + \shuffle($nonEmptyArray); + assertType('non-empty-list', $nonEmptyArray); +} + +function fooCompare (int $a, int $b): int { + return $a > $b ? 1 : -1; +} + +function foo15() { + $manifest = [1, 2, 3]; + uasort( + $manifest, + "fooCompare" + ); + assertType('array{1, 2, 3}', $manifest); +} + +function fooSpaceship (string $a, string $b): int { + return $a <=> $b; +} + +function foo16() { + $array = [1, 2]; + uksort( + $array, + "fooSpaceship" + ); + assertType('array{1, 2}', $array); +} + +function fooShuffle() { + $array = ["foo" => 123, "bar" => 456]; + shuffle($array); + assertType('non-empty-array<0|1, 123|456>&list', $array); + + $emptyArray = []; + shuffle($emptyArray); + assertType('array{}', $emptyArray); +} + +function fooSort() { + $array = ["foo" => 123, "bar" => 456]; + sort($array); + assertType('array{foo: 123, bar: 456}', $array); + + $emptyArray = []; + sort($emptyArray); + assertType('array{}', $emptyArray); +} + +function fooScanf(): void +{ + sscanf("10:05:03", "%d:%d:%d", $hours, $minutes, $seconds); + assertType('float|int|string|null', $hours); + assertType('float|int|string|null', $minutes); + assertType('float|int|string|null', $seconds); + + $n = sscanf("42 psalm road", "%s %s", $p1, $p2); + assertType('int|null', $n); // could be 'int' + assertType('float|int|string|null', $p1); + assertType('float|int|string|null', $p2); +} + +function fooMatch(string $input): void { + preg_match_all('/@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/', $input, $matches, PREG_PATTERN_ORDER); + assertType('array>', $matches); + + preg_match_all('/@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/', $input, $matches, PREG_SET_ORDER); + assertType('list>', $matches); + + preg_match('/@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/', $input, $matches, PREG_UNMATCHED_AS_NULL); + assertType("array", $matches); +} + +function fooParams(ExtendsFooBar $subX, float $x1, float $y1) +{ + $subX->renamedParams($x1, $y1); + + assertType('float', $x1); + assertType('string', $y1); // overridden via reference of base-class, by param order (renamed params) +} + +function fooParams2(ExtendsFooBar $subX, float $x1, float $y1) { + $subX->paramOutOverridden($x1, $y1); + + assertType('float', $x1); + assertType('array', $y1); // overridden phpdoc-param-out-type in subclass +} + +function fooDateTime(\SplFileObject $splFileObject, ?string $wouldBlock) { + // php-src native method overridden via stub + $splFileObject->flock(1, $wouldBlock); + + assertType('string', $wouldBlock); +} + +function testMatch() { + preg_match('#.*#', 'foo', $matches); + assertType('array', $matches); +} + +function testParseStr() { + $str="first=value&arr[]=foo+bar&arr[]=baz"; + parse_str($str, $output); + + /* + echo $output['first'];//value + echo $output['arr'][0];//foo bar + echo $output['arr'][1];//baz + */ + + \PHPStan\Testing\assertType('array|string>', $output); +} + +function fooSimilar() { + $similar = similar_text('foo', 'bar', $percent); + assertType('int', $similar); + assertType('float', $percent); +} + +function fooExec() { + exec("my cmd", $output, $exitCode); + + assertType('list', $output); + assertType('int', $exitCode); +} + +function fooSystem() { + system("my cmd", $exitCode); + + assertType('int', $exitCode); +} + +function fooPassthru() { + passthru("my cmd", $exitCode); + + assertType('int', $exitCode); +} diff --git a/tests/PHPStan/Analyser/param-out.neon b/tests/PHPStan/Analyser/param-out.neon new file mode 100644 index 0000000000..8d3fe24304 --- /dev/null +++ b/tests/PHPStan/Analyser/param-out.neon @@ -0,0 +1,3 @@ +parameters: + stubFiles: + - param-out.stub diff --git a/tests/PHPStan/Analyser/param-out.stub b/tests/PHPStan/Analyser/param-out.stub new file mode 100644 index 0000000000..297e1620fb --- /dev/null +++ b/tests/PHPStan/Analyser/param-out.stub @@ -0,0 +1,21 @@ +getClass(DateTime::class)), @@ -381,6 +402,7 @@ public function dataGetFunctions(): array PassedByReference::createReadsArgument(), false, null, + null, ), new ParameterSignature( 'strings', @@ -390,6 +412,7 @@ public function dataGetFunctions(): array PassedByReference::createReadsArgument(), true, null, + null, ), ], new BooleanType(), diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php index 640178e696..31de1353ad 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php @@ -304,4 +304,14 @@ public function testConditionalReturnType(): void ]); } + public function testTemplateInParamOut(): void + { + $this->analyse([__DIR__ . '/data/param-out.php'], [ + [ + 'Template type S of function ParamOutTemplate\uselessGeneric() is not referenced in a parameter.', + 9, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/param-out.php b/tests/PHPStan/Rules/Functions/data/param-out.php new file mode 100644 index 0000000000..637750be98 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/param-out.php @@ -0,0 +1,11 @@ +analyse([__DIR__ . '/data/bug-7519.php'], []); } + public function testTemplateInParamOut(): void + { + $this->analyse([__DIR__ . '/data/param-out.php'], [ + [ + 'Template type T of method ParamOutTemplate\FooBar::uselessLocalTemplate() is not referenced in a parameter.', + 22, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/param-out.php b/tests/PHPStan/Rules/Methods/data/param-out.php new file mode 100644 index 0000000000..51df4504bf --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/param-out.php @@ -0,0 +1,25 @@ +analyse([__DIR__ . '/data/param-out.php'], [ + [ + 'PHPDoc tag @param-out references unknown parameter: $z', + 23, + ], + [ + 'Parameter $i for PHPDoc tag @param-out is not passed by reference.', + 37, + ], + [ + 'PHPDoc tag @param-out for parameter $i contains unresolvable type.', + 44, + ], + [ + 'PHPDoc tag @param-out for parameter $i contains generic type Exception but class Exception is not generic.', + 51, + ], + [ + 'Generic type ParamOutPhpDocRule\FooBar in PHPDoc tag @param-out for parameter $i does not specify all template types of class ParamOutPhpDocRule\FooBar: T, TT', + 58, + ], + [ + 'Type mixed in generic type ParamOutPhpDocRule\FooBar in PHPDoc tag @param-out for parameter $i is not subtype of template type T of int of class ParamOutPhpDocRule\FooBar.', + 58, + ], + [ + 'Generic type ParamOutPhpDocRule\FooBar in PHPDoc tag @param-out for parameter $i does not specify all template types of class ParamOutPhpDocRule\FooBar: T, TT', + 65, + ], + + ]); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/data/param-out.php b/tests/PHPStan/Rules/PhpDoc/data/param-out.php new file mode 100644 index 0000000000..5c4da69694 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/param-out.php @@ -0,0 +1,115 @@ + $i + */ +function unresolvableParamOutType(int &$i) { + +} + +/** + * @param-out \Exception $i + */ +function invalidParamOutGeneric(int &$i) { + +} + +/** + * @param-out FooBar $i + */ +function invalidParamOutWrongGenericParams(int &$i) { + +} + +/** + * @param-out FooBar $i + */ +function invalidParamOutNotAllGenericParams(int &$i) { + +} + +/** + * @template T of int + * @template TT of string + */ +class FooBar { + /** + * @param-out T $s + */ + function genericClassFoo(mixed &$s): void + { + } + + /** + * @template S of self + * @param-out S $s + */ + function genericSelf(mixed &$s): void + { + } + + /** + * @template S of static + * @param-out S $s + */ + function genericStatic(mixed &$s): void + { + } +} + +class C { + /** + * @var \Closure|null + */ + private $onCancel; + + public function __construct() { + $this->foo($this->onCancel); + } + + /** + * @param mixed $onCancel + * @param-out \Closure $onCancel + */ + public function foo(&$onCancel) : void { + $onCancel = function (): void {}; + } +}