diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index f19f1cd4a6..4425f8aa52 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -57,6 +57,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Php\PhpVersionFactory; use PHPStan\Php\PhpVersions; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\AttributeReflectionFactory; @@ -2143,6 +2144,7 @@ public function enterClassMethod( array $immediatelyInvokedCallableParameters = [], array $phpDocClosureThisTypeParameters = [], bool $isConstructor = false, + ?ResolvedPhpDocBlock $resolvedPhpDocBlock = null, ): self { if (!$this->isInClass()) { @@ -2172,6 +2174,7 @@ public function enterClassMethod( $asserts ?? Assertions::createEmpty(), $selfOutType, $phpDocComment, + $resolvedPhpDocBlock, array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $parameterOutTypes), $immediatelyInvokedCallableParameters, array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocClosureThisTypeParameters), @@ -2195,6 +2198,7 @@ public function enterPropertyHook( ?string $deprecatedDescription, bool $isDeprecated, ?string $phpDocComment, + ?ResolvedPhpDocBlock $resolvedPhpDocBlock = null, ): self { if (!$this->isInClass()) { @@ -2259,6 +2263,7 @@ public function enterPropertyHook( Assertions::createEmpty(), null, $phpDocComment, + $resolvedPhpDocBlock, [], [], [], diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 9b5dab663e..93a16ba06e 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5376,7 +5376,7 @@ private function processPropertyHooks( $this->callNodeCallback($nodeCallback, $hook, $scope, $storage); $this->processAttributeGroups($stmt, $hook->attrGroups, $scope, $storage, $nodeCallback); - [, $phpDocParameterTypes,,,, $phpDocThrowType,,,,,,,, $phpDocComment] = $this->getPhpDocs($scope, $hook); + [, $phpDocParameterTypes,,,, $phpDocThrowType,,,,,,,, $phpDocComment,,,,,, $resolvedPhpDoc] = $this->getPhpDocs($scope, $hook); foreach ($hook->params as $param) { $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); @@ -5394,6 +5394,7 @@ private function processPropertyHooks( $deprecatedDescription, $isDeprecated, $phpDocComment, + $resolvedPhpDoc, ); $hookReflection = $hookScope->getFunction(); if (!$hookReflection instanceof PhpMethodFromParserNodeReflection) { @@ -7266,7 +7267,7 @@ private function processNodesForCalledMethod($node, ExpressionResultStorage $sto } /** - * @return array{TemplateTypeMap, array, array, array, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array, array<(string|int), VarTag>, bool} + * @return array{TemplateTypeMap, array, array, array, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array, array<(string|int), VarTag>, bool, ?ResolvedPhpDocBlock} */ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array { @@ -7309,12 +7310,20 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n return $param->var->name; }, $node->getParams()); + $currentResolvedPhpDoc = null; + if ($docComment !== null) { + $currentResolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $file, + $class, + $trait, + $node->name->name, + $docComment, + ); + } $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( - $docComment, - $file, $scope->getClassReflection(), - $trait, $node->name->name, + $currentResolvedPhpDoc, $positionalParameterNames, ); @@ -7416,16 +7425,17 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $isPure = $resolvedPhpDoc->isPure(); $isAllowedPrivateMutation = $resolvedPhpDoc->isAllowedPrivateMutation(); $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments(); - if ($acceptsNamedArguments && $scope->isInClass()) { - $acceptsNamedArguments = $scope->getClassReflection()->acceptsNamedArguments(); - } $isReadOnly = $isReadOnly || $resolvedPhpDoc->isReadOnly(); $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; $varTags = $resolvedPhpDoc->getVarTags(); } - return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation]; + if ($acceptsNamedArguments && $scope->isInClass()) { + $acceptsNamedArguments = $scope->getClassReflection()->acceptsNamedArguments(); + } + + return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation, $resolvedPhpDoc]; } private function transformStaticType(ClassReflection $declaringClass, Type $type): Type diff --git a/src/PhpDoc/InheritedPhpDocParameterMapping.php b/src/PhpDoc/InheritedPhpDocParameterMapping.php new file mode 100644 index 0000000000..c0b80d3e25 --- /dev/null +++ b/src/PhpDoc/InheritedPhpDocParameterMapping.php @@ -0,0 +1,67 @@ + $parameterNameMapping + */ + public function __construct( + private array $parameterNameMapping, + ) + { + } + + /** + * @template T + * @param array $array + * @return array + */ + public function transformArrayKeysWithParameterNameMapping(array $array): array + { + $newArray = []; + foreach ($array as $key => $value) { + if (!array_key_exists($key, $this->parameterNameMapping)) { + continue; + } + $newArray[$this->parameterNameMapping[$key]] = $value; + } + + return $newArray; + } + + public function transformConditionalReturnTypeWithParameterNameMapping(Type $type): Type + { + $nameMapping = $this->parameterNameMapping; + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($nameMapping): Type { + if ($type instanceof ConditionalTypeForParameter) { + $parameterName = substr($type->getParameterName(), 1); + if (array_key_exists($parameterName, $nameMapping)) { + $type = $type->changeParameterName('$' . $nameMapping[$parameterName]); + } + } + + return $traverse($type); + }); + } + + public function transformAssertTagParameterWithParameterNameMapping(AssertTagParameter $parameter): AssertTagParameter + { + $parameterName = substr($parameter->getParameterName(), 1); + if (array_key_exists($parameterName, $this->parameterNameMapping)) { + $parameter = $parameter->changeParameterName('$' . $this->parameterNameMapping[$parameterName]); + } + + return $parameter; + } + +} diff --git a/src/PhpDoc/PhpDocBlock.php b/src/PhpDoc/PhpDocBlock.php deleted file mode 100644 index 6c602701f4..0000000000 --- a/src/PhpDoc/PhpDocBlock.php +++ /dev/null @@ -1,391 +0,0 @@ - $parameterNameMapping - * @param array $parents - */ - private function __construct( - private string $docComment, - private ?string $file, - private ClassReflection $classReflection, - private ?string $trait, - private array $parameterNameMapping, - private array $parents, - ) - { - } - - public function getDocComment(): string - { - return $this->docComment; - } - - public function getFile(): ?string - { - return $this->file; - } - - public function getClassReflection(): ClassReflection - { - return $this->classReflection; - } - - public function getTrait(): ?string - { - return $this->trait; - } - - /** - * @return array - */ - public function getParents(): array - { - return $this->parents; - } - - /** - * @template T - * @param array $array - * @return array - */ - public function transformArrayKeysWithParameterNameMapping(array $array): array - { - $newArray = []; - foreach ($array as $key => $value) { - if (!array_key_exists($key, $this->parameterNameMapping)) { - continue; - } - $newArray[$this->parameterNameMapping[$key]] = $value; - } - - return $newArray; - } - - public function transformConditionalReturnTypeWithParameterNameMapping(Type $type): Type - { - $nameMapping = $this->parameterNameMapping; - return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($nameMapping): Type { - if ($type instanceof ConditionalTypeForParameter) { - $parameterName = substr($type->getParameterName(), 1); - if (array_key_exists($parameterName, $nameMapping)) { - $type = $type->changeParameterName('$' . $nameMapping[$parameterName]); - } - } - - return $traverse($type); - }); - } - - public function transformAssertTagParameterWithParameterNameMapping(AssertTagParameter $parameter): AssertTagParameter - { - $parameterName = substr($parameter->getParameterName(), 1); - if (array_key_exists($parameterName, $this->parameterNameMapping)) { - $parameter = $parameter->changeParameterName('$' . $this->parameterNameMapping[$parameterName]); - } - - return $parameter; - } - - public static function resolvePhpDocBlockForProperty( - ?string $docComment, - ClassReflection $classReflection, - ?string $trait, - string $propertyName, - ?string $file, - ): self - { - $docBlocksFromParents = []; - foreach (self::getParentReflections($classReflection) as $parentReflection) { - $oneResult = self::resolvePropertyPhpDocBlockFromClass( - $parentReflection, - $propertyName, - ); - - if ($oneResult === null) { // Null if it is private or from a wrong trait. - continue; - } - - $docBlocksFromParents[] = $oneResult; - } - - return new self( - $docComment ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, - $file, - $classReflection, - $trait, - [], - $docBlocksFromParents, - ); - } - - public static function resolvePhpDocBlockForConstant( - ?string $docComment, - ClassReflection $classReflection, - string $constantName, - ?string $file, - ): self - { - $docBlocksFromParents = []; - foreach (self::getParentReflections($classReflection) as $parentReflection) { - $oneResult = self::resolveConstantPhpDocBlockFromClass( - $parentReflection, - $constantName, - ); - - if ($oneResult === null) { // Null if it is private or from a wrong trait. - continue; - } - - $docBlocksFromParents[] = $oneResult; - } - - return new self( - $docComment ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, - $file, - $classReflection, - null, - [], - $docBlocksFromParents, - ); - } - - /** - * @param array $originalPositionalParameterNames - * @param array $newPositionalParameterNames - */ - public static function resolvePhpDocBlockForMethod( - ?string $docComment, - ClassReflection $classReflection, - ?string $trait, - string $methodName, - ?string $file, - array $originalPositionalParameterNames, - array $newPositionalParameterNames, - ): self - { - $docBlocksFromParents = []; - foreach (self::getParentReflections($classReflection) as $parentReflection) { - $oneResult = self::resolveMethodPhpDocBlockFromClass( - $parentReflection, - $methodName, - $newPositionalParameterNames, - ); - - if ($oneResult === null) { // Null if it is private or from a wrong trait. - continue; - } - - $docBlocksFromParents[] = $oneResult; - } - - foreach ($classReflection->getTraits() as $traitReflection) { - if (!$traitReflection->hasNativeMethod($methodName)) { - continue; - } - $traitMethod = $traitReflection->getNativeMethod($methodName); - $abstract = $traitMethod->isAbstract(); - if (is_bool($abstract)) { - if (!$abstract) { - continue; - } - } elseif (!$abstract->yes()) { - continue; - } - - if ($traitMethod->getDocComment() === null) { - continue; - } - - $methodVariant = $traitMethod->getOnlyVariant(); - $positionalMethodParameterNames = []; - foreach ($methodVariant->getParameters() as $methodParameter) { - $positionalMethodParameterNames[] = $methodParameter->getName(); - } - - $docBlocksFromParents[] = new self( - $traitMethod->getDocComment(), - $classReflection->getFileName(), - $classReflection, - $traitReflection->getName(), - self::remapParameterNames($newPositionalParameterNames, $positionalMethodParameterNames), - [], - ); - } - - return new self( - $docComment ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, - $file, - $classReflection, - $trait, - self::remapParameterNames($originalPositionalParameterNames, $newPositionalParameterNames), - $docBlocksFromParents, - ); - } - - /** - * @param array $originalPositionalParameterNames - * @param array $newPositionalParameterNames - * @return array - */ - private static function remapParameterNames( - array $originalPositionalParameterNames, - array $newPositionalParameterNames, - ): array - { - $parameterNameMapping = []; - foreach ($originalPositionalParameterNames as $i => $parameterName) { - if (!array_key_exists($i, $newPositionalParameterNames)) { - continue; - } - $parameterNameMapping[$newPositionalParameterNames[$i]] = $parameterName; - } - - return $parameterNameMapping; - } - - /** - * @return array - */ - private static function getParentReflections(ClassReflection $classReflection): array - { - $result = []; - - $parent = $classReflection->getParentClass(); - if ($parent !== null) { - $result[] = $parent; - } - - foreach ($classReflection->getImmediateInterfaces() as $interface) { - $result[] = $interface; - } - - return $result; - } - - private static function resolveConstantPhpDocBlockFromClass( - ClassReflection $classReflection, - string $name, - ): ?self - { - if ($classReflection->hasConstant($name)) { - $parentReflection = $classReflection->getConstant($name); - if ($parentReflection->isPrivate()) { - return null; - } - - $classReflection = $parentReflection->getDeclaringClass(); - - return self::resolvePhpDocBlockForConstant( - $parentReflection->getDocComment() ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, - $classReflection, - $name, - $classReflection->getFileName(), - ); - } - - return null; - } - - private static function resolvePropertyPhpDocBlockFromClass( - ClassReflection $classReflection, - string $name, - ): ?self - { - if ($classReflection->hasNativeProperty($name)) { - $parentReflection = $classReflection->getNativeProperty($name); - if ($parentReflection->isPrivate()) { - return null; - } - - $classReflection = $parentReflection->getDeclaringClass(); - $traitReflection = $parentReflection->getDeclaringTrait(); - - $trait = $traitReflection !== null - ? $traitReflection->getName() - : null; - - return self::resolvePhpDocBlockForProperty( - $parentReflection->getDocComment() ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, - $classReflection, - $trait, - $name, - $classReflection->getFileName(), - ); - } - - return null; - } - - /** - * @param array $positionalParameterNames - */ - private static function resolveMethodPhpDocBlockFromClass( - ClassReflection $classReflection, - string $name, - array $positionalParameterNames, - ): ?self - { - if ($classReflection->hasNativeMethod($name)) { - $parentReflection = $classReflection->getNativeMethod($name); - if ($parentReflection->isPrivate()) { - return null; - } - - $classReflection = $parentReflection->getDeclaringClass(); - $traitReflection = null; - if ($parentReflection instanceof PhpMethodReflection || $parentReflection instanceof ResolvedMethodReflection) { - $traitReflection = $parentReflection->getDeclaringTrait(); - } - $methodVariants = $parentReflection->getVariants(); - $positionalMethodParameterNames = []; - $lowercaseMethodName = strtolower($parentReflection->getName()); - if ( - count($methodVariants) === 1 - && $lowercaseMethodName !== '__construct' - && $lowercaseMethodName !== strtolower($parentReflection->getDeclaringClass()->getName()) - ) { - $methodParameters = $methodVariants[0]->getParameters(); - foreach ($methodParameters as $methodParameter) { - $positionalMethodParameterNames[] = $methodParameter->getName(); - } - } else { - $positionalMethodParameterNames = $positionalParameterNames; - } - - $trait = $traitReflection !== null - ? $traitReflection->getName() - : null; - - return self::resolvePhpDocBlockForMethod( - $parentReflection->getDocComment() ?? ResolvedPhpDocBlock::EMPTY_DOC_STRING, - $classReflection, - $trait, - $name, - $classReflection->getFileName(), - $positionalParameterNames, - $positionalMethodParameterNames, - ); - } - - return null; - } - -} diff --git a/src/PhpDoc/PhpDocInheritanceResolver.php b/src/PhpDoc/PhpDocInheritanceResolver.php index b72e7fe36b..628b3e62df 100644 --- a/src/PhpDoc/PhpDocInheritanceResolver.php +++ b/src/PhpDoc/PhpDocInheritanceResolver.php @@ -2,154 +2,284 @@ namespace PHPStan\PhpDoc; -use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Type\FileTypeMapper; -use function array_map; +use function array_key_exists; +use function count; +use function is_bool; use function strtolower; #[AutowiredService] final class PhpDocInheritanceResolver { - public function __construct( - private FileTypeMapper $fileTypeMapper, - private StubPhpDocProvider $stubPhpDocProvider, - ) + public function __construct(private FileTypeMapper $fileTypeMapper) { } public function resolvePhpDocForProperty( - ?string $docComment, - ClassReflection $classReflection, - ?string $classReflectionFileName, - ?string $declaringTraitName, + ClassReflection $declaringClass, string $propertyName, - ): ResolvedPhpDocBlock + ?ResolvedPhpDocBlock $currentResolvedPhpDoc, + ): ?ResolvedPhpDocBlock { - $phpDocBlock = PhpDocBlock::resolvePhpDocBlockForProperty( - $docComment, - $classReflection, - null, - $propertyName, - $classReflectionFileName, - ); - - return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, $declaringTraitName, null, $propertyName, null); + $parent = $declaringClass->getParentClass(); + if ($parent !== null) { + $parentMethod = $this->resolvePropertyPhpDocFromParentClass($declaringClass, $parent, $propertyName, $currentResolvedPhpDoc); + if ($parentMethod !== null) { + return $parentMethod; + } + } + + foreach ($declaringClass->getImmediateInterfaces() as $interface) { + $interfaceMethod = $this->resolvePropertyPhpDocFromParentClass($declaringClass, $interface, $propertyName, $currentResolvedPhpDoc); + if ($interfaceMethod === null) { + continue; + } + + return $interfaceMethod; + } + + return $currentResolvedPhpDoc; } public function resolvePhpDocForConstant( - ?string $docComment, - ClassReflection $classReflection, - ?string $classReflectionFileName, + ClassReflection $declaringClass, string $constantName, - ): ResolvedPhpDocBlock + ?ResolvedPhpDocBlock $currentResolvedPhpDoc, + ): ?ResolvedPhpDocBlock { - $phpDocBlock = PhpDocBlock::resolvePhpDocBlockForConstant( - $docComment, - $classReflection, - $constantName, - $classReflectionFileName, - ); - - return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, null, null, null, $constantName); + $parent = $declaringClass->getParentClass(); + if ($parent !== null) { + $parentMethod = $this->resolveConstantPhpDocFromParentClass($declaringClass, $parent, $constantName, $currentResolvedPhpDoc); + if ($parentMethod !== null) { + return $parentMethod; + } + } + + foreach ($declaringClass->getImmediateInterfaces() as $interface) { + $interfaceMethod = $this->resolveConstantPhpDocFromParentClass($declaringClass, $interface, $constantName, $currentResolvedPhpDoc); + if ($interfaceMethod === null) { + continue; + } + + return $interfaceMethod; + } + + return $currentResolvedPhpDoc; } /** - * @param array $positionalParameterNames + * @param array $currentPositionalParameterNames */ public function resolvePhpDocForMethod( - ?string $docComment, - ?string $fileName, - ClassReflection $classReflection, - ?string $declaringTraitName, + ClassReflection $declaringClass, string $methodName, - array $positionalParameterNames, + ?ResolvedPhpDocBlock $currentResolvedPhpDoc, + array $currentPositionalParameterNames, + ): ?ResolvedPhpDocBlock + { + $parent = $declaringClass->getParentClass(); + if ($parent !== null) { + if ($parent->hasNativeMethod($methodName)) { + $parentMethod = $parent->getNativeMethod($methodName); + if (!$parentMethod->isPrivate() && $parentMethod->getResolvedPhpDoc() !== null) { + if ($parentMethod->getName() !== '__construct' || !$parentMethod->getDeclaringClass()->isBuiltin()) { + return $this->resolveMethodPhpDocFromParentClass($parentMethod, $parentMethod->getResolvedPhpDoc(), $declaringClass, $parent, $currentResolvedPhpDoc, $currentPositionalParameterNames); + } + } + } + } + + foreach ($declaringClass->getImmediateInterfaces() as $interface) { + if (!$interface->hasNativeMethod($methodName)) { + continue; + } + + $interfaceMethod = $interface->getNativeMethod($methodName); + if ($interfaceMethod->isPrivate()) { + continue; + } + if ($interfaceMethod->getResolvedPhpDoc() === null) { + continue; + } + return $this->resolveMethodPhpDocFromParentClass($interfaceMethod, $interfaceMethod->getResolvedPhpDoc(), $declaringClass, $interface, $currentResolvedPhpDoc, $currentPositionalParameterNames); + + } + + foreach ($declaringClass->getTraits() as $trait) { + if (!$trait->hasNativeMethod($methodName)) { + continue; + } + + $traitMethod = $trait->getNativeMethod($methodName); + if ($traitMethod->getDocComment() === null) { + continue; + } + if ($declaringClass->getFileName() === null) { + continue; + } + + $abstract = $traitMethod->isAbstract(); + if (is_bool($abstract)) { + if (!$abstract) { + continue; + } + } elseif (!$abstract->yes()) { + continue; + } + + $resolvedPhpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc( + $declaringClass->getFileName(), + $declaringClass->getName(), + $trait->getName(), + $methodName, + $traitMethod->getDocComment(), + ); + + return $this->resolveMethodPhpDocFromParentClass($traitMethod, $resolvedPhpDocBlock, $declaringClass, $trait, $currentResolvedPhpDoc, $currentPositionalParameterNames); + } + + return $currentResolvedPhpDoc; + } + + /** + * @param array $currentPositionalParameterNames + */ + private function resolveMethodPhpDocFromParentClass( + ExtendedMethodReflection $parentMethod, + ResolvedPhpDocBlock $resolvedPhpDocBlock, + ClassReflection $declaringClass, + ClassReflection $parent, + ?ResolvedPhpDocBlock $currentResolvedPhpDoc, + array $currentPositionalParameterNames, ): ResolvedPhpDocBlock { - $phpDocBlock = PhpDocBlock::resolvePhpDocBlockForMethod( - $docComment, - $classReflection, - $declaringTraitName, - $methodName, - $fileName, - $positionalParameterNames, - $positionalParameterNames, - ); - - return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, $phpDocBlock->getTrait(), $methodName, null, null); + if ($currentResolvedPhpDoc === null) { + $currentResolvedPhpDoc = ResolvedPhpDocBlock::createEmpty(); + } + + $methodVariants = $parentMethod->getVariants(); + $positionalMethodParameterNames = []; + $lowercaseMethodName = strtolower($parentMethod->getName()); + if ( + count($methodVariants) === 1 + && $lowercaseMethodName !== '__construct' + && $lowercaseMethodName !== strtolower($parentMethod->getDeclaringClass()->getName()) + ) { + $methodParameters = $methodVariants[0]->getParameters(); + foreach ($methodParameters as $methodParameter) { + $positionalMethodParameterNames[] = $methodParameter->getName(); + } + } else { + $positionalMethodParameterNames = $currentPositionalParameterNames; + } + + $parentClassForMerge = $parent; + $phpDocDeclaringClass = $parentMethod->getDeclaringClass(); + if ($phpDocDeclaringClass->getName() !== $parent->getName()) { + $ancestor = $parent->getAncestorWithClassName($phpDocDeclaringClass->getName()); + if ($ancestor !== null) { + $parentClassForMerge = $ancestor; + } + } + + return $currentResolvedPhpDoc->merge($resolvedPhpDocBlock, new InheritedPhpDocParameterMapping(self::remapParameterNames($currentPositionalParameterNames, $positionalMethodParameterNames)), $declaringClass, $parentClassForMerge); } - private function docBlockTreeToResolvedDocBlock(PhpDocBlock $phpDocBlock, ?string $traitName, ?string $functionName, ?string $propertyName, ?string $constantName): ResolvedPhpDocBlock + /** + * @param array $originalPositionalParameterNames + * @param array $newPositionalParameterNames + * @return array + */ + private static function remapParameterNames( + array $originalPositionalParameterNames, + array $newPositionalParameterNames, + ): array { - $parents = []; - $parentPhpDocBlocks = []; - - foreach ($phpDocBlock->getParents() as $parentPhpDocBlock) { - if ( - $functionName !== null - && strtolower($functionName) === '__construct' - && $parentPhpDocBlock->getClassReflection()->isBuiltin() - ) { + $parameterNameMapping = []; + foreach ($originalPositionalParameterNames as $i => $parameterName) { + if (!array_key_exists($i, $newPositionalParameterNames)) { continue; } - $parents[] = $this->docBlockTreeToResolvedDocBlock( - $parentPhpDocBlock, - $parentPhpDocBlock->getTrait(), - $functionName, - $propertyName, - $constantName, - ); - $parentPhpDocBlocks[] = $parentPhpDocBlock; + $parameterNameMapping[$newPositionalParameterNames[$i]] = $parameterName; } - $oneResolvedDockBlock = $this->docBlockToResolvedDocBlock($phpDocBlock, $traitName, $functionName, $propertyName, $constantName); - return $oneResolvedDockBlock->merge($parents, $parentPhpDocBlocks); + return $parameterNameMapping; } - private function docBlockToResolvedDocBlock(PhpDocBlock $phpDocBlock, ?string $traitName, ?string $functionName, ?string $propertyName, ?string $constantName): ResolvedPhpDocBlock + private function resolveConstantPhpDocFromParentClass( + ClassReflection $declaringClass, + ClassReflection $parent, + string $constantName, + ?ResolvedPhpDocBlock $currentResolvedPhpDoc, + ): ?ResolvedPhpDocBlock { - $classReflection = $phpDocBlock->getClassReflection(); - if ($functionName !== null && $classReflection->getNativeReflection()->hasMethod($functionName)) { - $methodReflection = $classReflection->getNativeReflection()->getMethod($functionName); - $stub = $this->stubPhpDocProvider->findMethodPhpDoc($classReflection->getName(), $classReflection->getName(), $functionName, array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters())); - if ($stub !== null) { - return $stub; - } + if (!$parent->hasConstant($constantName)) { + return null; } - if ($propertyName !== null && $classReflection->getNativeReflection()->hasProperty($propertyName)) { - $stub = $this->stubPhpDocProvider->findPropertyPhpDoc($classReflection->getName(), $propertyName); + $parentConstant = $parent->getConstant($constantName); + if ($parentConstant->isPrivate()) { + return null; + } - if ($stub === null) { - $propertyReflection = $classReflection->getNativeReflection()->getProperty($propertyName); + if ($parentConstant->getResolvedPhpDoc() === null) { + return null; + } - $propertyDeclaringClass = $propertyReflection->getBetterReflection()->getDeclaringClass(); + if ($currentResolvedPhpDoc === null) { + $currentResolvedPhpDoc = ResolvedPhpDocBlock::createEmpty(); + } - if ($propertyDeclaringClass->isTrait() && (! $propertyReflection->getDeclaringClass()->isTrait() || $propertyReflection->getDeclaringClass()->getName() !== $propertyDeclaringClass->getName())) { - $stub = $this->stubPhpDocProvider->findPropertyPhpDoc($propertyDeclaringClass->getName(), $propertyName); - } - } - if ($stub !== null) { - return $stub; + $parentClassForMerge = $parent; + $phpDocDeclaringClass = $parentConstant->getDeclaringClass(); + if ($phpDocDeclaringClass->getName() !== $parent->getName()) { + $ancestor = $parent->getAncestorWithClassName($phpDocDeclaringClass->getName()); + if ($ancestor !== null) { + $parentClassForMerge = $ancestor; } } - if ($constantName !== null && $classReflection->getNativeReflection()->hasConstant($constantName)) { - $stub = $this->stubPhpDocProvider->findClassConstantPhpDoc($classReflection->getName(), $constantName); - if ($stub !== null) { - return $stub; + return $currentResolvedPhpDoc->merge($parentConstant->getResolvedPhpDoc(), new InheritedPhpDocParameterMapping([]), $declaringClass, $parentClassForMerge); + } + + private function resolvePropertyPhpDocFromParentClass( + ClassReflection $declaringClass, + ClassReflection $parent, + string $propertyName, + ?ResolvedPhpDocBlock $currentResolvedPhpDoc, + ): ?ResolvedPhpDocBlock + { + if (!$parent->hasNativeProperty($propertyName)) { + return null; + } + + $parentProperty = $parent->getNativeProperty($propertyName); + if ($parentProperty->isPrivate()) { + return null; + } + + if ($parentProperty->getResolvedPhpDoc() === null) { + return null; + } + + if ($currentResolvedPhpDoc === null) { + $currentResolvedPhpDoc = ResolvedPhpDocBlock::createEmpty(); + } + + $parentClassForMerge = $parent; + $phpDocDeclaringClass = $parentProperty->getDeclaringClass(); + if ($phpDocDeclaringClass->getName() !== $parent->getName()) { + $ancestor = $parent->getAncestorWithClassName($phpDocDeclaringClass->getName()); + if ($ancestor !== null) { + $parentClassForMerge = $ancestor; } } - return $this->fileTypeMapper->getResolvedPhpDoc( - $phpDocBlock->getFile(), - $classReflection->getName(), - $traitName, - $functionName, - $phpDocBlock->getDocComment(), - ); + return $currentResolvedPhpDoc->merge($parentProperty->getResolvedPhpDoc(), new InheritedPhpDocParameterMapping([]), $declaringClass, $parentClassForMerge); } } diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index dc43e9af5e..b2533223b6 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -242,28 +242,17 @@ public static function createEmpty(): self return $self; } - /** - * @param array $parents - * @param array $parentPhpDocBlocks - */ - public function merge(array $parents, array $parentPhpDocBlocks): self + public function merge(ResolvedPhpDocBlock $parent, InheritedPhpDocParameterMapping $parameterMapping, ClassReflection $declaringClass, ClassReflection $parentClass): self { - $className = $this->nameScope !== null ? $this->nameScope->getClassName() : null; - $classReflection = $className !== null && $this->reflectionProvider->hasClass($className) - ? $this->reflectionProvider->getClass($className) - : null; - // new property also needs to be added to createEmpty() $result = new self(); // we will resolve everything on $this here so these properties don't have to be populated // skip $result->phpDocNode $phpDocNodes = $this->phpDocNodes; $acceptsNamedArguments = $this->acceptsNamedArguments(); - foreach ($parents as $parent) { - foreach ($parent->phpDocNodes as $phpDocNode) { - $phpDocNodes[] = $phpDocNode; - $acceptsNamedArguments = $acceptsNamedArguments && $parent->acceptsNamedArguments(); - } + foreach ($parent->phpDocNodes as $phpDocNode) { + $phpDocNodes[] = $phpDocNode; + $acceptsNamedArguments = $acceptsNamedArguments && $parent->acceptsNamedArguments(); } $result->phpDocNodes = $phpDocNodes; $result->phpDocString = $this->phpDocString; @@ -272,32 +261,32 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->templateTypeMap = $this->templateTypeMap; $result->templateTags = $this->templateTags; // skip $result->phpDocNodeResolver - $result->varTags = self::mergeVarTags($this->getVarTags(), $parents, $parentPhpDocBlocks); + $result->varTags = self::mergeVarTags($this->getVarTags(), $parent, $parentClass); $result->methodTags = $this->getMethodTags(); $result->propertyTags = $this->getPropertyTags(); $result->extendsTags = $this->getExtendsTags(); $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->paramsImmediatelyInvokedCallable = self::mergeParamsImmediatelyInvokedCallable($this->getParamsImmediatelyInvokedCallable(), $parents, $parentPhpDocBlocks); - $result->paramClosureThisTags = self::mergeParamClosureThisTags($this->getParamClosureThisTags(), $parents, $parentPhpDocBlocks); - $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $classReflection, $parents, $parentPhpDocBlocks); - $result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parents); + $result->paramTags = self::mergeParamTags($this->getParamTags(), $parent, $parameterMapping, $parentClass); + $result->paramOutTags = self::mergeParamOutTags($this->getParamOutTags(), $parent, $parameterMapping, $parentClass); + $result->paramsImmediatelyInvokedCallable = self::mergeParamsImmediatelyInvokedCallable($this->getParamsImmediatelyInvokedCallable(), $parent, $parameterMapping); + $result->paramClosureThisTags = self::mergeParamClosureThisTags($this->getParamClosureThisTags(), $parent, $parameterMapping, $parentClass); + $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $declaringClass, $parent, $parameterMapping, $parentClass); + $result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parent); $result->mixinTags = $this->getMixinTags(); $result->requireExtendsTags = $this->getRequireExtendsTags(); $result->requireImplementsTags = $this->getRequireImplementsTags(); $result->sealedTypeTags = $this->getSealedTags(); $result->typeAliasTags = $this->getTypeAliasTags(); $result->typeAliasImportTags = $this->getTypeAliasImportTags(); - $result->assertTags = self::mergeAssertTags($this->getAssertTags(), $parents, $parentPhpDocBlocks); - $result->selfOutTypeTag = self::mergeSelfOutTypeTags($this->getSelfOutTag(), $parents); - $result->deprecatedTag = self::mergeDeprecatedTags($this->getDeprecatedTag(), $this->isNotDeprecated(), $parents); + $result->assertTags = self::mergeAssertTags($this->getAssertTags(), $parent, $parameterMapping, $parentClass); + $result->selfOutTypeTag = self::mergeSelfOutTypeTags($this->getSelfOutTag(), $parent); + $result->deprecatedTag = self::mergeDeprecatedTags($this->getDeprecatedTag(), $this->isNotDeprecated(), $parent); $result->isDeprecated = $result->deprecatedTag !== null; $result->isNotDeprecated = $this->isNotDeprecated(); $result->isInternal = $this->isInternal(); $result->isFinal = $this->isFinal(); - $result->isPure = self::mergePureTags($this->isPure(), $parents); + $result->isPure = self::mergePureTags($this->isPure(), $parent); $result->isReadOnly = $this->isReadOnly(); $result->isImmutable = $this->isImmutable(); $result->isAllowedPrivateMutation = $this->isAllowedPrivateMutation(); @@ -850,36 +839,30 @@ public function isAllowedPrivateMutation(): bool /** * @param array $varTags - * @param array $parents - * @param array $parentPhpDocBlocks * @return array */ - private static function mergeVarTags(array $varTags, array $parents, array $parentPhpDocBlocks): array + private static function mergeVarTags(array $varTags, self $parent, ClassReflection $parentClass): array { // Only allow one var tag per comment. Check the parent if child does not have this tag. if (count($varTags) > 0) { return $varTags; } - foreach ($parents as $i => $parent) { - $result = self::mergeOneParentVarTags($parent, $parentPhpDocBlocks[$i]); - if ($result === null) { - continue; - } - - return $result; + $result = self::mergeOneParentVarTags($parent, $parentClass); + if ($result === null) { + return []; } - return []; + return $result; } /** * @return array|null */ - private static function mergeOneParentVarTags(self $parent, PhpDocBlock $phpDocBlock): ?array + private static function mergeOneParentVarTags(self $parent, ClassReflection $parentClass): ?array { foreach ($parent->getVarTags() as $key => $parentVarTag) { - return [$key => self::resolveTemplateTypeInTag($parentVarTag->toImplicit(), $phpDocBlock, TemplateTypeVariance::createInvariant())]; + return [$key => self::resolveTemplateTypeInTag($parentVarTag->toImplicit(), $parentClass, TemplateTypeVariance::createInvariant())]; } return null; @@ -887,26 +870,20 @@ private static function mergeOneParentVarTags(self $parent, PhpDocBlock $phpDocB /** * @param array $paramTags - * @param array $parents - * @param array $parentPhpDocBlocks * @return array */ - private static function mergeParamTags(array $paramTags, array $parents, array $parentPhpDocBlocks): array + private static function mergeParamTags(array $paramTags, self $parent, InheritedPhpDocParameterMapping $parameterMapping, ClassReflection $parentClass): array { - foreach ($parents as $i => $parent) { - $paramTags = self::mergeOneParentParamTags($paramTags, $parent, $parentPhpDocBlocks[$i]); - } - - return $paramTags; + return self::mergeOneParentParamTags($paramTags, $parent, $parameterMapping, $parentClass); } /** * @param array $paramTags * @return array */ - private static function mergeOneParentParamTags(array $paramTags, self $parent, PhpDocBlock $phpDocBlock): array + private static function mergeOneParentParamTags(array $paramTags, self $parent, InheritedPhpDocParameterMapping $parameterMapping, ClassReflection $parentClass): array { - $parentParamTags = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamTags()); + $parentParamTags = $parameterMapping->transformArrayKeysWithParameterNameMapping($parent->getParamTags()); foreach ($parentParamTags as $name => $parentParamTag) { if (array_key_exists($name, $paramTags)) { @@ -914,8 +891,8 @@ private static function mergeOneParentParamTags(array $paramTags, self $parent, } $paramTags[$name] = self::resolveTemplateTypeInTag( - $parentParamTag->withType($phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentParamTag->getType())), - $phpDocBlock, + $parentParamTag->withType($parameterMapping->transformConditionalReturnTypeWithParameterNameMapping($parentParamTag->getType())), + $parentClass, TemplateTypeVariance::createContravariant(), ); } @@ -923,30 +900,16 @@ private static function mergeOneParentParamTags(array $paramTags, self $parent, return $paramTags; } - /** - * @param array $parents - * @param array $parentPhpDocBlocks - * @return ReturnTag|Null - */ - private static function mergeReturnTags(?ReturnTag $returnTag, ?ClassReflection $classReflection, array $parents, array $parentPhpDocBlocks): ?ReturnTag + private static function mergeReturnTags(?ReturnTag $returnTag, ClassReflection $classReflection, self $parent, InheritedPhpDocParameterMapping $parameterMapping, ClassReflection $parentClass): ?ReturnTag { if ($returnTag !== null) { return $returnTag; } - foreach ($parents as $i => $parent) { - $result = self::mergeOneParentReturnTag($returnTag, $classReflection, $parent, $parentPhpDocBlocks[$i]); - if ($result === null) { - continue; - } - - return $result; - } - - return null; + return self::mergeOneParentReturnTag($returnTag, $classReflection, $parent, $parameterMapping, $parentClass); } - private static function mergeOneParentReturnTag(?ReturnTag $returnTag, ?ClassReflection $classReflection, self $parent, PhpDocBlock $phpDocBlock): ?ReturnTag + private static function mergeOneParentReturnTag(?ReturnTag $returnTag, ClassReflection $classReflection, self $parent, InheritedPhpDocParameterMapping $parameterMapping, ClassReflection $parentClass): ?ReturnTag { $parentReturnTag = $parent->getReturnTag(); if ($parentReturnTag === null) { @@ -954,21 +917,18 @@ private static function mergeOneParentReturnTag(?ReturnTag $returnTag, ?ClassRef } $parentType = $parentReturnTag->getType(); + $parentType = TypeTraverser::map( + $parentType, + static function (Type $type, callable $traverse) use ($classReflection): Type { + if ($type instanceof StaticType) { + return $type->changeBaseClass($classReflection); + } - if ($classReflection !== null) { - $parentType = TypeTraverser::map( - $parentType, - static function (Type $type, callable $traverse) use ($classReflection): Type { - if ($type instanceof StaticType) { - return $type->changeBaseClass($classReflection); - } + return $traverse($type); + }, + ); - return $traverse($type); - }, - ); - - $parentReturnTag = $parentReturnTag->withType($parentType); - } + $parentReturnTag = $parentReturnTag->withType($parentType); // Each parent would overwrite the previous one except if it returns a less specific type. // Do not care for incompatible types as there is a separate rule for that. @@ -978,70 +938,45 @@ static function (Type $type, callable $traverse) use ($classReflection): Type { return self::resolveTemplateTypeInTag( $parentReturnTag->withType( - $phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentReturnTag->getType()), + $parameterMapping->transformConditionalReturnTypeWithParameterNameMapping($parentReturnTag->getType()), )->toImplicit(), - $phpDocBlock, + $parentClass, TemplateTypeVariance::createCovariant(), ); } /** * @param array $assertTags - * @param array $parents - * @param array $parentPhpDocBlocks * @return array */ - private static function mergeAssertTags(array $assertTags, array $parents, array $parentPhpDocBlocks): array + private static function mergeAssertTags(array $assertTags, self $parent, InheritedPhpDocParameterMapping $parameterMapping, ClassReflection $parentClass): array { if (count($assertTags) > 0) { return $assertTags; } - foreach ($parents as $i => $parent) { - $result = $parent->getAssertTags(); - if (count($result) === 0) { - continue; - } - - $phpDocBlock = $parentPhpDocBlocks[$i]; - return array_map( - static fn (AssertTag $assertTag) => self::resolveTemplateTypeInTag( - $assertTag->withParameter( - $phpDocBlock->transformAssertTagParameterWithParameterNameMapping($assertTag->getParameter()), - )->toImplicit(), - $phpDocBlock, - TemplateTypeVariance::createCovariant(), - ), - $result, - ); - } - - return $assertTags; + return array_map( + static fn (AssertTag $assertTag) => self::resolveTemplateTypeInTag( + $assertTag->withParameter( + $parameterMapping->transformAssertTagParameterWithParameterNameMapping($assertTag->getParameter()), + )->toImplicit(), + $parentClass, + TemplateTypeVariance::createCovariant(), + ), + $parent->getAssertTags(), + ); } - /** - * @param array $parents - */ - private static function mergeSelfOutTypeTags(?SelfOutTypeTag $selfOutTypeTag, array $parents): ?SelfOutTypeTag + private static function mergeSelfOutTypeTags(?SelfOutTypeTag $selfOutTypeTag, self $parent): ?SelfOutTypeTag { if ($selfOutTypeTag !== null) { return $selfOutTypeTag; } - foreach ($parents as $parent) { - $result = $parent->getSelfOutTag(); - if ($result === null) { - continue; - } - return $result; - } - return null; + return $parent->getSelfOutTag(); } - /** - * @param array $parents - */ - private static function mergeDeprecatedTags(?DeprecatedTag $deprecatedTag, bool $hasNotDeprecatedTag, array $parents): ?DeprecatedTag + private static function mergeDeprecatedTags(?DeprecatedTag $deprecatedTag, bool $hasNotDeprecatedTag, self $parent): ?DeprecatedTag { if ($deprecatedTag !== null) { return $deprecatedTag; @@ -1051,59 +986,39 @@ private static function mergeDeprecatedTags(?DeprecatedTag $deprecatedTag, bool return null; } - foreach ($parents as $parent) { - $result = $parent->getDeprecatedTag(); - if ($result === null && !$parent->isNotDeprecated()) { - continue; - } - return $result; + $result = $parent->getDeprecatedTag(); + if ($result === null && !$parent->isNotDeprecated()) { + return null; } - return null; + return $result; } - /** - * @param array $parents - */ - private static function mergeThrowsTags(?ThrowsTag $throwsTag, array $parents): ?ThrowsTag + private static function mergeThrowsTags(?ThrowsTag $throwsTag, self $parent): ?ThrowsTag { if ($throwsTag !== null) { return $throwsTag; } - foreach ($parents as $parent) { - $result = $parent->getThrowsTag(); - if ($result === null) { - continue; - } - - return $result; - } - return null; + return $parent->getThrowsTag(); } /** * @param array $paramOutTags - * @param array $parents - * @param array $parentPhpDocBlocks * @return array */ - private static function mergeParamOutTags(array $paramOutTags, array $parents, array $parentPhpDocBlocks): array + private static function mergeParamOutTags(array $paramOutTags, self $parent, InheritedPhpDocParameterMapping $parameterMapping, ClassReflection $parentClass): array { - foreach ($parents as $i => $parent) { - $paramOutTags = self::mergeOneParentParamOutTags($paramOutTags, $parent, $parentPhpDocBlocks[$i]); - } - - return $paramOutTags; + return self::mergeOneParentParamOutTags($paramOutTags, $parent, $parameterMapping, $parentClass); } /** * @param array $paramOutTags * @return array */ - private static function mergeOneParentParamOutTags(array $paramOutTags, self $parent, PhpDocBlock $phpDocBlock): array + private static function mergeOneParentParamOutTags(array $paramOutTags, self $parent, InheritedPhpDocParameterMapping $parameterMapping, ClassReflection $parentClass): array { - $parentParamOutTags = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamOutTags()); + $parentParamOutTags = $parameterMapping->transformArrayKeysWithParameterNameMapping($parent->getParamOutTags()); foreach ($parentParamOutTags as $name => $parentParamTag) { if (array_key_exists($name, $paramOutTags)) { @@ -1111,8 +1026,8 @@ private static function mergeOneParentParamOutTags(array $paramOutTags, self $pa } $paramOutTags[$name] = self::resolveTemplateTypeInTag( - $parentParamTag->withType($phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentParamTag->getType())), - $phpDocBlock, + $parentParamTag->withType($parameterMapping->transformConditionalReturnTypeWithParameterNameMapping($parentParamTag->getType())), + $parentClass, TemplateTypeVariance::createCovariant(), ); } @@ -1122,26 +1037,20 @@ private static function mergeOneParentParamOutTags(array $paramOutTags, self $pa /** * @param array $paramsImmediatelyInvokedCallable - * @param array $parents - * @param array $parentPhpDocBlocks * @return array */ - private static function mergeParamsImmediatelyInvokedCallable(array $paramsImmediatelyInvokedCallable, array $parents, array $parentPhpDocBlocks): array + private static function mergeParamsImmediatelyInvokedCallable(array $paramsImmediatelyInvokedCallable, self $parent, InheritedPhpDocParameterMapping $parameterMapping): array { - foreach ($parents as $i => $parent) { - $paramsImmediatelyInvokedCallable = self::mergeOneParentParamImmediatelyInvokedCallable($paramsImmediatelyInvokedCallable, $parent, $parentPhpDocBlocks[$i]); - } - - return $paramsImmediatelyInvokedCallable; + return self::mergeOneParentParamImmediatelyInvokedCallable($paramsImmediatelyInvokedCallable, $parent, $parameterMapping); } /** * @param array $paramsImmediatelyInvokedCallable * @return array */ - private static function mergeOneParentParamImmediatelyInvokedCallable(array $paramsImmediatelyInvokedCallable, self $parent, PhpDocBlock $phpDocBlock): array + private static function mergeOneParentParamImmediatelyInvokedCallable(array $paramsImmediatelyInvokedCallable, self $parent, InheritedPhpDocParameterMapping $parameterMapping): array { - $parentImmediatelyInvokedCallable = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamsImmediatelyInvokedCallable()); + $parentImmediatelyInvokedCallable = $parameterMapping->transformArrayKeysWithParameterNameMapping($parent->getParamsImmediatelyInvokedCallable()); foreach ($parentImmediatelyInvokedCallable as $name => $parentIsImmediatelyInvokedCallable) { if (array_key_exists($name, $paramsImmediatelyInvokedCallable)) { @@ -1156,26 +1065,20 @@ private static function mergeOneParentParamImmediatelyInvokedCallable(array $par /** * @param array $paramsClosureThisTags - * @param array $parents - * @param array $parentPhpDocBlocks * @return array */ - private static function mergeParamClosureThisTags(array $paramsClosureThisTags, array $parents, array $parentPhpDocBlocks): array + private static function mergeParamClosureThisTags(array $paramsClosureThisTags, self $parent, InheritedPhpDocParameterMapping $parameterMapping, ClassReflection $parentClass): array { - foreach ($parents as $i => $parent) { - $paramsClosureThisTags = self::mergeOneParentParamClosureThisTag($paramsClosureThisTags, $parent, $parentPhpDocBlocks[$i]); - } - - return $paramsClosureThisTags; + return self::mergeOneParentParamClosureThisTag($paramsClosureThisTags, $parent, $parameterMapping, $parentClass); } /** * @param array $paramsClosureThisTags * @return array */ - private static function mergeOneParentParamClosureThisTag(array $paramsClosureThisTags, self $parent, PhpDocBlock $phpDocBlock): array + private static function mergeOneParentParamClosureThisTag(array $paramsClosureThisTags, self $parent, InheritedPhpDocParameterMapping $parameterMapping, ClassReflection $parentClass): array { - $parentClosureThisTags = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamClosureThisTags()); + $parentClosureThisTags = $parameterMapping->transformArrayKeysWithParameterNameMapping($parent->getParamClosureThisTags()); foreach ($parentClosureThisTags as $name => $parentParamClosureThisTag) { if (array_key_exists($name, $paramsClosureThisTags)) { @@ -1184,9 +1087,9 @@ private static function mergeOneParentParamClosureThisTag(array $paramsClosureTh $paramsClosureThisTags[$name] = self::resolveTemplateTypeInTag( $parentParamClosureThisTag->withType( - $phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentParamClosureThisTag->getType()), + $parameterMapping->transformConditionalReturnTypeWithParameterNameMapping($parentParamClosureThisTag->getType()), ), - $phpDocBlock, + $parentClass, TemplateTypeVariance::createContravariant(), ); } @@ -1194,25 +1097,13 @@ private static function mergeOneParentParamClosureThisTag(array $paramsClosureTh return $paramsClosureThisTags; } - /** - * @param array $parents - */ - private static function mergePureTags(?bool $isPure, array $parents): ?bool + private static function mergePureTags(?bool $isPure, self $parent): ?bool { if ($isPure !== null) { return $isPure; } - foreach ($parents as $parent) { - $parentIsPure = $parent->isPure(); - if ($parentIsPure === null) { - continue; - } - - return $parentIsPure; - } - - return null; + return $parent->isPure(); } /** @@ -1222,14 +1113,14 @@ private static function mergePureTags(?bool $isPure, array $parents): ?bool */ private static function resolveTemplateTypeInTag( TypedTag $tag, - PhpDocBlock $phpDocBlock, + ClassReflection $classReflection, TemplateTypeVariance $positionVariance, ): TypedTag { $type = TemplateTypeHelper::resolveTemplateTypes( $tag->getType(), - $phpDocBlock->getClassReflection()->getActiveTemplateTypeMap(), - $phpDocBlock->getClassReflection()->getCallSiteVarianceMap(), + $classReflection->getActiveTemplateTypeMap(), + $classReflection->getCallSiteVarianceMap(), $positionVariance, ); return $tag->withType($type); diff --git a/src/Reflection/Annotations/AnnotationMethodReflection.php b/src/Reflection/Annotations/AnnotationMethodReflection.php index 7fe7ccd14e..2f0b233122 100644 --- a/src/Reflection/Annotations/AnnotationMethodReflection.php +++ b/src/Reflection/Annotations/AnnotationMethodReflection.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\Annotations; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; @@ -188,4 +189,9 @@ public function mustUseReturnValue(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return null; + } + } diff --git a/src/Reflection/ClassConstantReflection.php b/src/Reflection/ClassConstantReflection.php index cafc201341..dc30920809 100644 --- a/src/Reflection/ClassConstantReflection.php +++ b/src/Reflection/ClassConstantReflection.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection; use PhpParser\Node\Expr; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Type\Type; /** @api */ @@ -21,4 +22,6 @@ public function hasNativeType(): bool; public function getNativeType(): ?Type; + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock; + } diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 4c235f3920..a8c7913674 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -1264,32 +1264,28 @@ public function getConstant(string $name): ClassConstantReflection $deprecatedDescription = $deprecation === null ? null : $deprecation->getDescription(); $isDeprecated = $deprecation !== null; - $declaringClass = $this->reflectionProvider->getClass($reflectionConstant->getDeclaringClass()->getName()); + $declaringClass = $this->getAncestorWithClassName($reflectionConstant->getDeclaringClass()->getName()); + if ($declaringClass === null) { + throw new ShouldNotHappenException(); + } $fileName = $declaringClass->getFileName(); $phpDocType = null; - $resolvedPhpDoc = $this->stubPhpDocProvider->findClassConstantPhpDoc( + $currentResolvedPhpDoc = $this->stubPhpDocProvider->findClassConstantPhpDoc( $declaringClass->getName(), $name, ); - if ($resolvedPhpDoc === null) { - $docComment = null; + if ($currentResolvedPhpDoc === null) { if ($reflectionConstant->getDocComment() !== false) { - $docComment = $reflectionConstant->getDocComment(); + $currentResolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $fileName, + $declaringClass->getName(), + null, + null, + $reflectionConstant->getDocComment(), + ); } - $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForConstant( - $docComment, - $declaringClass, - $fileName, - $name, - ); - } - if (!$isDeprecated) { - $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; - $isDeprecated = $resolvedPhpDoc->isDeprecated(); } - $isInternal = $resolvedPhpDoc->isInternal(); - $isFinal = $resolvedPhpDoc->isFinal(); $nativeType = null; if ($reflectionConstant->getType() !== null) { @@ -1298,11 +1294,32 @@ public function getConstant(string $name): ClassConstantReflection $nativeType = $this->signatureMapProvider->getClassConstantMetadata($declaringClass->getName(), $name)['nativeType']; } - $varTags = $resolvedPhpDoc->getVarTags(); - if (isset($varTags[0]) && count($varTags) === 1) { - $varTag = $varTags[0]; - if ($varTag->isExplicit() || $nativeType === null || $nativeType->isSuperTypeOf($varTag->getType())->yes()) { - $phpDocType = $varTag->getType(); + $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForConstant( + $declaringClass, + $name, + $currentResolvedPhpDoc, + ); + + $isInternal = false; + $isFinal = false; + if ($resolvedPhpDoc !== null) { + if (!$isDeprecated) { + $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; + $isDeprecated = $resolvedPhpDoc->isDeprecated(); + } + $isInternal = $resolvedPhpDoc->isInternal(); + $isFinal = $resolvedPhpDoc->isFinal(); + $varTags = $resolvedPhpDoc->getVarTags(); + if (isset($varTags[0]) && count($varTags) === 1) { + $varTag = $varTags[0]; + if ($varTag->isExplicit() || $nativeType === null || $nativeType->isSuperTypeOf($varTag->getType())->yes()) { + $phpDocType = TemplateTypeHelper::resolveTemplateTypes( + $varTag->getType(), + $declaringClass->getActiveTemplateTypeMap(), + $declaringClass->getCallSiteVarianceMap(), + TemplateTypeVariance::createInvariant(), + ); + } } } @@ -1312,6 +1329,7 @@ public function getConstant(string $name): ClassConstantReflection $reflectionConstant, $nativeType, $phpDocType, + $resolvedPhpDoc, $deprecatedDescription, $isDeprecated, $isInternal, diff --git a/src/Reflection/Dummy/ChangedTypeMethodReflection.php b/src/Reflection/Dummy/ChangedTypeMethodReflection.php index b5749c7a29..d525f2661d 100644 --- a/src/Reflection/Dummy/ChangedTypeMethodReflection.php +++ b/src/Reflection/Dummy/ChangedTypeMethodReflection.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\Dummy; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; @@ -177,4 +178,9 @@ public function mustUseReturnValue(): TrinaryLogic return $this->reflection->mustUseReturnValue(); } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return $this->reflection->getResolvedPhpDoc(); + } + } diff --git a/src/Reflection/Dummy/DummyClassConstantReflection.php b/src/Reflection/Dummy/DummyClassConstantReflection.php index ffc6afe7c9..7cee1c0fe3 100644 --- a/src/Reflection/Dummy/DummyClassConstantReflection.php +++ b/src/Reflection/Dummy/DummyClassConstantReflection.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr; use PHPStan\Node\Expr\TypeExpr; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ReflectionProviderStaticAccessor; @@ -111,4 +112,9 @@ public function getAttributes(): array return []; } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return null; + } + } diff --git a/src/Reflection/Dummy/DummyConstructorReflection.php b/src/Reflection/Dummy/DummyConstructorReflection.php index 6652c87c47..844a5340e1 100644 --- a/src/Reflection/Dummy/DummyConstructorReflection.php +++ b/src/Reflection/Dummy/DummyConstructorReflection.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\Dummy; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; @@ -163,4 +164,9 @@ public function mustUseReturnValue(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return null; + } + } diff --git a/src/Reflection/Dummy/DummyMethodReflection.php b/src/Reflection/Dummy/DummyMethodReflection.php index f81481d449..afe694a7c5 100644 --- a/src/Reflection/Dummy/DummyMethodReflection.php +++ b/src/Reflection/Dummy/DummyMethodReflection.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\Dummy; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; @@ -155,4 +156,9 @@ public function mustUseReturnValue(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return null; + } + } diff --git a/src/Reflection/ExtendedMethodReflection.php b/src/Reflection/ExtendedMethodReflection.php index 0cb9caeab0..a13cd47c1f 100644 --- a/src/Reflection/ExtendedMethodReflection.php +++ b/src/Reflection/ExtendedMethodReflection.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; @@ -72,4 +73,6 @@ public function getAttributes(): array; */ public function mustUseReturnValue(): TrinaryLogic; + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock; + } diff --git a/src/Reflection/Native/NativeMethodReflection.php b/src/Reflection/Native/NativeMethodReflection.php index 9a167e9862..5cd7475e83 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection\Native; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ClassMemberReflection; @@ -31,6 +32,7 @@ public function __construct( private ReflectionProvider $reflectionProvider, private ClassReflection $declaringClass, private ReflectionMethod $reflection, + private ?ResolvedPhpDocBlock $resolvedPhpDocBlock, private array $variants, private ?array $namedArgumentsVariants, private TrinaryLogic $hasSideEffects, @@ -236,4 +238,9 @@ public function mustUseReturnValue(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return $this->resolvedPhpDocBlock; + } + } diff --git a/src/Reflection/Php/ClosureCallMethodReflection.php b/src/Reflection/Php/ClosureCallMethodReflection.php index 3c0db2f7f3..7af6299b96 100644 --- a/src/Reflection/Php/ClosureCallMethodReflection.php +++ b/src/Reflection/Php/ClosureCallMethodReflection.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\Php; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; @@ -207,4 +208,9 @@ public function mustUseReturnValue(): TrinaryLogic return $this->nativeMethodReflection->mustUseReturnValue(); } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return $this->nativeMethodReflection->getResolvedPhpDoc(); + } + } diff --git a/src/Reflection/Php/EnumCasesMethodReflection.php b/src/Reflection/Php/EnumCasesMethodReflection.php index 91d795598b..d731fc29d9 100644 --- a/src/Reflection/Php/EnumCasesMethodReflection.php +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\Php; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; @@ -166,4 +167,9 @@ public function mustUseReturnValue(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return null; + } + } diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 62c02c0e8c..b155b72e5c 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -231,6 +231,7 @@ private function createProperty( $classReflection->getNativeReflection()->getProperty($propertyName), getHook: null, setHook: null, + resolvedPhpDocBlock: null, deprecatedDescription: null, isDeprecated: false, isInternal: false, @@ -269,14 +270,29 @@ private function createProperty( } if ($constructorName === null) { + $currentResolvedPhpDoc = $this->stubPhpDocProvider->findPropertyPhpDoc($declaringClassName, $propertyName); + if ( + $currentResolvedPhpDoc === null + && $declaringTraitName !== null + ) { + $currentResolvedPhpDoc = $this->stubPhpDocProvider->findPropertyPhpDoc($declaringTraitName, $propertyName); + } + if ($currentResolvedPhpDoc === null && $docComment !== null) { + $currentResolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $declaringClassReflection->getFileName(), + $declaringClassName, + $declaringTraitName, + null, + $docComment, + ); + } $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForProperty( - $docComment, $declaringClassReflection, - $declaringClassReflection->getFileName(), - $declaringTraitName, $propertyName, + $currentResolvedPhpDoc, ); } elseif ($docComment !== null) { + // todo could call phpDocInheritanceResolver too $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( $declaringClassReflection->getFileName(), $declaringClassName, @@ -285,7 +301,6 @@ private function createProperty( $docComment, ); } - $phpDocBlockClassReflection = $declaringClassReflection; if ($resolvedPhpDoc !== null) { $varTags = $resolvedPhpDoc->getVarTags(); @@ -297,8 +312,8 @@ private function createProperty( $phpDocType = $phpDocType !== null ? TemplateTypeHelper::resolveTemplateTypes( $phpDocType, - $phpDocBlockClassReflection->getActiveTemplateTypeMap(), - $phpDocBlockClassReflection->getCallSiteVarianceMap(), + $declaringClassReflection->getActiveTemplateTypeMap(), + $declaringClassReflection->getCallSiteVarianceMap(), TemplateTypeVariance::createInvariant(), ) : null; @@ -314,23 +329,12 @@ private function createProperty( if ($phpDocType === null) { if (isset($constructorName)) { - $constructorDocComment = $declaringClassReflection->getConstructor()->getDocComment(); - $nativeClassReflection = $declaringClassReflection->getNativeReflection(); - $positionalParameterNames = []; - if ($nativeClassReflection->getConstructor() !== null) { - $positionalParameterNames = array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $nativeClassReflection->getConstructor()->getParameters()); - } - $resolvedConstructorPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( - $constructorDocComment, - $declaringClassReflection->getFileName(), - $declaringClassReflection, - $declaringTraitName, - $constructorName, - $positionalParameterNames, - ); - $paramTags = $resolvedConstructorPhpDoc->getParamTags(); - if (isset($paramTags[$propertyReflection->getName()])) { - $phpDocType = $paramTags[$propertyReflection->getName()]->getType(); + $resolvedConstructorPhpDoc = $declaringClassReflection->getConstructor()->getResolvedPhpDoc(); + if ($resolvedConstructorPhpDoc !== null) { + $paramTags = $resolvedConstructorPhpDoc->getParamTags(); + if (isset($paramTags[$propertyReflection->getName()])) { + $phpDocType = $paramTags[$propertyReflection->getName()]->getType(); + } } } } @@ -428,6 +432,7 @@ private function createProperty( $propertyReflection, $getHook, $setHook, + $resolvedPhpDoc, $deprecatedDescription, $isDeprecated, $isInternal, @@ -484,6 +489,7 @@ private function createProperty( $propertyReflection, $getHook, $setHook, + $nativeProperty->getResolvedPhpDoc(), $deprecatedDescription, $isDeprecated, $isInternal, @@ -624,112 +630,91 @@ private function createMethod( foreach ($methodSignature->getParameters() as $parameter) { $phpDocParameterNameMapping[$parameter->getName()] = $parameter->getName(); } - $stubPhpDocReturnType = null; - $stubPhpDocParameterTypes = []; - $stubPhpDocParameterVariadicity = []; $phpDocParameterTypes = []; $phpDocReturnType = null; - $stubPhpDocPair = null; - $stubPhpParameterOutTypes = []; $phpDocParameterOutTypes = []; $immediatelyInvokedCallableParameters = []; $closureThisParameters = []; - $stubImmediatelyInvokedCallableParameters = []; - $stubClosureThisParameters = []; + $currentResolvedPhpDoc = null; + $phpDocDeclaringClass = $declaringClass; + $phpDocFromStubs = false; if (count($methodSignatures) === 1) { $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $declaringClass, $methodReflection->getName(), array_map(static fn (ParameterSignature $parameterSignature): string => $parameterSignature->getName(), $methodSignature->getParameters())); if ($stubPhpDocPair !== null) { - [$stubPhpDoc, $stubDeclaringClass] = $stubPhpDocPair; - $templateTypeMap = $stubDeclaringClass->getActiveTemplateTypeMap(); - $callSiteVarianceMap = $stubDeclaringClass->getCallSiteVarianceMap(); - $returnTag = $stubPhpDoc->getReturnTag(); - $stubImmediatelyInvokedCallableParameters = array_map(static fn (bool $immediate) => TrinaryLogic::createFromBoolean($immediate), $stubPhpDoc->getParamsImmediatelyInvokedCallable()); - if ($returnTag !== null) { - $stubPhpDocReturnType = TemplateTypeHelper::resolveTemplateTypes( - $returnTag->getType(), - $templateTypeMap, - $callSiteVarianceMap, - TemplateTypeVariance::createCovariant(), - ); - } - - $stubClosureThisParameters = array_map(static fn ($tag) => $tag->getType(), $stubPhpDoc->getParamClosureThisTags()); - foreach ($stubPhpDoc->getParamTags() as $name => $paramTag) { - $stubPhpDocParameterTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( - $paramTag->getType(), - $templateTypeMap, - $callSiteVarianceMap, - TemplateTypeVariance::createContravariant(), - ); - $stubPhpDocParameterVariadicity[$name] = $paramTag->isVariadic(); - } - - $throwsTag = $stubPhpDoc->getThrowsTag(); - if ($throwsTag !== null) { - $throwType = $throwsTag->getType(); - } - - $asserts = Assertions::createFromResolvedPhpDocBlock($stubPhpDoc); - $acceptsNamedArguments = $stubPhpDoc->acceptsNamedArguments(); - - $selfOutTypeTag = $stubPhpDoc->getSelfOutTag(); - if ($selfOutTypeTag !== null) { - $selfOutType = $selfOutTypeTag->getType(); - } - - foreach ($stubPhpDoc->getParamOutTags() as $name => $paramOutTag) { - $stubPhpParameterOutTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( - $paramOutTag->getType(), - $templateTypeMap, - $callSiteVarianceMap, - TemplateTypeVariance::createCovariant(), - ); - } - - if ($declaringClassName === $stubDeclaringClass->getName() && $stubPhpDoc->hasPhpDocString()) { - $phpDocComment = $stubPhpDoc->getPhpDocString(); - } + [$currentResolvedPhpDoc, $phpDocDeclaringClass] = $stubPhpDocPair; + $phpDocFromStubs = true; } } - if ($stubPhpDocPair === null && $methodReflection->getDocComment() !== false) { - $filename = $methodReflection->getFileName(); - if ($filename !== false) { - $phpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc( - $filename, + if ( + $currentResolvedPhpDoc === null + && $methodReflection->getDocComment() !== false + && $methodReflection->getFileName() !== false + ) { + $currentResolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( + $declaringClass, + $methodReflection->getName(), + $this->fileTypeMapper->getResolvedPhpDoc( + $methodReflection->getFileName(), $declaringClassName, null, $methodReflection->getName(), $methodReflection->getDocComment(), + ), + array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters()), + ); + } + + if ($currentResolvedPhpDoc !== null) { + $templateTypeMap = $phpDocDeclaringClass->getActiveTemplateTypeMap(); + $callSiteVarianceMap = $phpDocDeclaringClass->getCallSiteVarianceMap(); + $returnTag = $currentResolvedPhpDoc->getReturnTag(); + $immediatelyInvokedCallableParameters = array_map(static fn (bool $immediate) => TrinaryLogic::createFromBoolean($immediate), $currentResolvedPhpDoc->getParamsImmediatelyInvokedCallable()); + if ($returnTag !== null && count($methodSignatures) === 1) { + $phpDocReturnType = TemplateTypeHelper::resolveTemplateTypes( + $returnTag->getType(), + $templateTypeMap, + $callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), ); - $throwsTag = $phpDocBlock->getThrowsTag(); - if ($throwsTag !== null) { - $throwType = $throwsTag->getType(); - } - $returnTag = $phpDocBlock->getReturnTag(); - if ($returnTag !== null && count($methodSignatures) === 1) { - $phpDocReturnType = $returnTag->getType(); - } - $immediatelyInvokedCallableParameters = array_map(static fn ($immediate) => TrinaryLogic::createFromBoolean($immediate), $phpDocBlock->getParamsImmediatelyInvokedCallable()); - $closureThisParameters = array_map(static fn ($tag) => $tag->getType(), $phpDocBlock->getParamClosureThisTags()); - foreach ($phpDocBlock->getParamTags() as $name => $paramTag) { - $phpDocParameterTypes[$name] = $paramTag->getType(); - } - $asserts = Assertions::createFromResolvedPhpDocBlock($phpDocBlock); - $acceptsNamedArguments = $phpDocBlock->acceptsNamedArguments(); + } - $selfOutTypeTag = $phpDocBlock->getSelfOutTag(); - if ($selfOutTypeTag !== null) { - $selfOutType = $selfOutTypeTag->getType(); - } + $closureThisParameters = array_map(static fn ($tag) => $tag->getType(), $currentResolvedPhpDoc->getParamClosureThisTags()); + foreach ($currentResolvedPhpDoc->getParamTags() as $name => $paramTag) { + $phpDocParameterTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( + $paramTag->getType(), + $templateTypeMap, + $callSiteVarianceMap, + TemplateTypeVariance::createContravariant(), + ); + } - if ($phpDocBlock->hasPhpDocString()) { - $phpDocComment = $phpDocBlock->getPhpDocString(); - } + $throwsTag = $currentResolvedPhpDoc->getThrowsTag(); + if ($throwsTag !== null) { + $throwType = $throwsTag->getType(); + } - foreach ($phpDocBlock->getParamOutTags() as $name => $paramOutTag) { - $phpDocParameterOutTypes[$name] = $paramOutTag->getType(); - } + $asserts = Assertions::createFromResolvedPhpDocBlock($currentResolvedPhpDoc); + $acceptsNamedArguments = $currentResolvedPhpDoc->acceptsNamedArguments(); + $selfOutTypeTag = $currentResolvedPhpDoc->getSelfOutTag(); + if ($selfOutTypeTag !== null) { + $selfOutType = $selfOutTypeTag->getType(); + } + + foreach ($currentResolvedPhpDoc->getParamOutTags() as $name => $paramOutTag) { + $phpDocParameterOutTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( + $paramOutTag->getType(), + $templateTypeMap, + $callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ); + } + + if ($currentResolvedPhpDoc->hasPhpDocString()) { + $phpDocComment = $currentResolvedPhpDoc->getPhpDocString(); + } + + if (!$phpDocFromStubs) { $signatureParameters = $methodSignature->getParameters(); foreach ($methodReflection->getParameters() as $paramI => $reflectionParameter) { if (!array_key_exists($paramI, $signatureParameters)) { @@ -740,7 +725,7 @@ private function createMethod( } } } - $variantsByType[$signatureType][] = $this->createNativeMethodVariant($methodSignature, $stubPhpDocParameterTypes, $stubPhpDocParameterVariadicity, $stubPhpDocReturnType, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping, $stubPhpParameterOutTypes, $phpDocParameterOutTypes, $stubImmediatelyInvokedCallableParameters, $immediatelyInvokedCallableParameters, $stubClosureThisParameters, $closureThisParameters, $signatureType !== 'named'); + $variantsByType[$signatureType][] = $this->createNativeMethodVariant($methodSignature, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping, $phpDocParameterOutTypes, $immediatelyInvokedCallableParameters, $closureThisParameters, $phpDocFromStubs, $signatureType !== 'named'); } } @@ -753,6 +738,7 @@ private function createMethod( $this->reflectionProviderProvider->getReflectionProvider(), $declaringClass, $methodReflection, + $currentResolvedPhpDoc ?? null, $variantsByType['positional'], $variantsByType['named'] ?? null, $hasSideEffects, @@ -778,8 +764,7 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $deprecation = $this->deprecationProvider->getMethodDeprecation($methodReflection); $deprecatedDescription = $deprecation === null ? null : $deprecation->getDescription(); $isDeprecated = $deprecation !== null; - - $resolvedPhpDoc = null; + $currentResolvedPhpDoc = null; $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($fileDeclaringClass, $fileDeclaringClass, $methodReflection->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters())); $phpDocBlockClassReflection = $fileDeclaringClass; @@ -800,24 +785,26 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla } if ($stubPhpDocPair !== null) { - [$resolvedPhpDoc, $phpDocBlockClassReflection] = $stubPhpDocPair; + [$currentResolvedPhpDoc, $phpDocBlockClassReflection] = $stubPhpDocPair; } - if ($resolvedPhpDoc === null) { - $docComment = $methodReflection->getDocComment() !== false ? $methodReflection->getDocComment() : null; - $positionalParameterNames = array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters()); - - $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( - $docComment, + if ($currentResolvedPhpDoc === null && $methodReflection->getDocComment() !== false && $actualDeclaringClass->getFileName() !== null) { + $currentResolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( $actualDeclaringClass->getFileName(), - $actualDeclaringClass, + $actualDeclaringClass->getName(), $declaringTraitName, $methodReflection->getName(), - $positionalParameterNames, + $methodReflection->getDocComment(), ); - $phpDocBlockClassReflection = $fileDeclaringClass; } + $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( + $actualDeclaringClass, + $methodReflection->getName(), + $currentResolvedPhpDoc, + array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters()), + ); + $declaringTrait = null; $reflectionProvider = $this->reflectionProviderProvider->getReflectionProvider(); if ( @@ -865,47 +852,11 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla } } - $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); - $immediatelyInvokedCallableParameters = array_map(static fn (bool $immediate) => TrinaryLogic::createFromBoolean($immediate), $resolvedPhpDoc->getParamsImmediatelyInvokedCallable()); - $closureThisParameters = array_map(static fn ($tag) => $tag->getType(), $resolvedPhpDoc->getParamClosureThisTags()); - - foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) { - if (array_key_exists($paramName, $phpDocParameterTypes)) { - continue; - } - $phpDocParameterTypes[$paramName] = $paramTag->getType(); - } - foreach ($phpDocParameterTypes as $paramName => $paramType) { - $phpDocParameterTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( - $paramType, - $phpDocBlockClassReflection->getActiveTemplateTypeMap(), - $phpDocBlockClassReflection->getCallSiteVarianceMap(), - TemplateTypeVariance::createContravariant(), - ); - } - - $phpDocParameterOutTypes = []; - foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) { - $phpDocParameterOutTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( - $paramOutTag->getType(), - $phpDocBlockClassReflection->getActiveTemplateTypeMap(), - $phpDocBlockClassReflection->getCallSiteVarianceMap(), - TemplateTypeVariance::createCovariant(), - ); - } - $nativeReturnType = TypehintHelper::decideTypeFromReflection( $methodReflection->getReturnType(), selfClass: $actualDeclaringClass, ); - $phpDocReturnType = $this->getPhpDocReturnType($phpDocBlockClassReflection, $resolvedPhpDoc, $nativeReturnType); - $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null; - if (!$isDeprecated) { - $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; - $isDeprecated = $resolvedPhpDoc->isDeprecated(); - } - $isInternal = $resolvedPhpDoc->isInternal(); - $isFinal = $resolvedPhpDoc->isFinal(); + $isPure = null; if ($actualDeclaringClass->isBuiltin() || $actualDeclaringClass->isEnum()) { foreach (array_keys($actualDeclaringClass->getAncestors()) as $className) { @@ -918,13 +869,59 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla } } - $isPure ??= $resolvedPhpDoc->isPure(); - $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); - $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments(); - $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; + $phpDocParameterOutTypes = []; + $phpDocReturnType = null; + $templateTypeMap = TemplateTypeMap::createEmpty(); + $immediatelyInvokedCallableParameters = []; + $closureThisParameters = []; + $phpDocThrowType = null; + $isInternal = false; + $isFinal = false; + $asserts = Assertions::createEmpty(); + $acceptsNamedArguments = true; + $selfOutType = null; $phpDocComment = null; - if ($resolvedPhpDoc->hasPhpDocString()) { - $phpDocComment = $resolvedPhpDoc->getPhpDocString(); + if ($resolvedPhpDoc !== null) { + $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap(); + $immediatelyInvokedCallableParameters = array_map(static fn (bool $immediate) => TrinaryLogic::createFromBoolean($immediate), $resolvedPhpDoc->getParamsImmediatelyInvokedCallable()); + $closureThisParameters = array_map(static fn ($tag) => $tag->getType(), $resolvedPhpDoc->getParamClosureThisTags()); + $phpDocReturnType = $this->getPhpDocReturnType($phpDocBlockClassReflection, $resolvedPhpDoc, $nativeReturnType); + $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null; + foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) { + if (array_key_exists($paramName, $phpDocParameterTypes)) { + continue; + } + $phpDocParameterTypes[$paramName] = $paramTag->getType(); + } + foreach ($phpDocParameterTypes as $paramName => $paramType) { + $phpDocParameterTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( + $paramType, + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createContravariant(), + ); + } + foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) { + $phpDocParameterOutTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( + $paramOutTag->getType(), + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createCovariant(), + ); + } + if (!$isDeprecated) { + $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; + $isDeprecated = $resolvedPhpDoc->isDeprecated(); + } + $isInternal = $resolvedPhpDoc->isInternal(); + $isFinal = $resolvedPhpDoc->isFinal(); + $isPure ??= $resolvedPhpDoc->isPure(); + $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); + $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments(); + $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; + if ($resolvedPhpDoc->hasPhpDocString()) { + $phpDocComment = $resolvedPhpDoc->getPhpDocString(); + } } return $this->methodReflectionFactory->create( @@ -935,6 +932,7 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, + $resolvedPhpDoc, $deprecatedDescription, $isDeprecated, $isInternal, @@ -952,31 +950,21 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla } /** - * @param array $stubPhpDocParameterTypes - * @param array $stubPhpDocParameterVariadicity * @param array $phpDocParameterTypes * @param array $phpDocParameterNameMapping - * @param array $stubPhpDocParameterOutTypes * @param array $phpDocParameterOutTypes - * @param array $stubImmediatelyInvokedCallableParameters * @param array $immediatelyInvokedCallableParameters - * @param array $stubClosureThisParameters * @param array $closureThisParameters */ private function createNativeMethodVariant( FunctionSignature $methodSignature, - array $stubPhpDocParameterTypes, - array $stubPhpDocParameterVariadicity, - ?Type $stubPhpDocReturnType, array $phpDocParameterTypes, ?Type $phpDocReturnType, array $phpDocParameterNameMapping, - array $stubPhpDocParameterOutTypes, array $phpDocParameterOutTypes, - array $stubImmediatelyInvokedCallableParameters, array $immediatelyInvokedCallableParameters, - array $stubClosureThisParameters, array $closureThisParameters, + bool $phpDocFromStubs, bool $usePhpDocParameterNames, ): ExtendedFunctionVariant { @@ -988,32 +976,23 @@ private function createNativeMethodVariant( $phpDocParameterName = $phpDocParameterNameMapping[$parameterSignature->getName()] ?? $parameterSignature->getName(); - if (isset($stubPhpDocParameterTypes[$parameterSignature->getName()])) { - $type = $stubPhpDocParameterTypes[$parameterSignature->getName()]; - $phpDocType = $stubPhpDocParameterTypes[$parameterSignature->getName()]; - } elseif (isset($phpDocParameterTypes[$phpDocParameterName])) { + if (isset($phpDocParameterTypes[$phpDocParameterName])) { $phpDocType = $phpDocParameterTypes[$phpDocParameterName]; - $type = TypehintHelper::decideType($parameterSignature->getType(), $phpDocType); + $type = $phpDocFromStubs ? $phpDocType : TypehintHelper::decideType($parameterSignature->getType(), $phpDocType); } - if (isset($stubPhpDocParameterOutTypes[$parameterSignature->getName()])) { - $parameterOutType = $stubPhpDocParameterOutTypes[$parameterSignature->getName()]; - } elseif (isset($phpDocParameterOutTypes[$phpDocParameterName])) { + if (isset($phpDocParameterOutTypes[$phpDocParameterName])) { $parameterOutType = $phpDocParameterOutTypes[$phpDocParameterName]; } - if (isset($stubImmediatelyInvokedCallableParameters[$parameterSignature->getName()])) { - $immediatelyInvoked = $stubImmediatelyInvokedCallableParameters[$parameterSignature->getName()]; - } elseif (isset($immediatelyInvokedCallableParameters[$phpDocParameterName])) { + if (isset($immediatelyInvokedCallableParameters[$phpDocParameterName])) { $immediatelyInvoked = $immediatelyInvokedCallableParameters[$phpDocParameterName]; } else { $immediatelyInvoked = TrinaryLogic::createMaybe(); } $closureThisType = null; - if (isset($stubClosureThisParameters[$parameterSignature->getName()])) { - $closureThisType = $stubClosureThisParameters[$parameterSignature->getName()]; - } elseif (isset($closureThisParameters[$phpDocParameterName])) { + if (isset($closureThisParameters[$phpDocParameterName])) { $closureThisType = $closureThisParameters[$phpDocParameterName]; } @@ -1026,7 +1005,7 @@ private function createNativeMethodVariant( $phpDocType ?? new MixedType(), $parameterSignature->getNativeType(), $parameterSignature->passedByReference(), - $stubPhpDocParameterVariadicity[$parameterSignature->getName()] ?? $parameterSignature->isVariadic(), + $parameterSignature->isVariadic(), $parameterSignature->getDefaultValue(), $parameterOutType ?? $parameterSignature->getOutType(), $immediatelyInvoked, @@ -1035,9 +1014,8 @@ private function createNativeMethodVariant( ); } - if ($stubPhpDocReturnType !== null) { - $returnType = $stubPhpDocReturnType; - $phpDocReturnType = $stubPhpDocReturnType; + if ($phpDocFromStubs && $phpDocReturnType !== null) { + $returnType = $phpDocReturnType; } else { $returnType = TypehintHelper::decideType($methodSignature->getReturnType(), $phpDocReturnType); } diff --git a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php index 7116cf34ff..25aa5fefa8 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PhpParser\Node\Stmt\ClassMethod; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ClassMemberReflection; @@ -65,6 +66,7 @@ public function __construct( Assertions $assertions, private ?Type $selfOutType, ?string $phpDocComment, + private ?ResolvedPhpDocBlock $resolvedPhpDoc, array $parameterOutTypes, array $immediatelyInvokedCallableParameters, array $phpDocClosureThisTypeParameters, @@ -296,4 +298,9 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return $this->resolvedPhpDoc; + } + } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index 0da97725e7..34f2863500 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -6,6 +6,7 @@ use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; use PHPStan\DependencyInjection\GenerateFactory; use PHPStan\Internal\DeprecatedAttributeHelper; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\AttributeReflectionFactory; @@ -73,6 +74,7 @@ public function __construct( private array $phpDocParameterTypes, private ?Type $phpDocReturnType, private ?Type $phpDocThrowType, + private ?ResolvedPhpDocBlock $resolvedPhpDocBlock, private ?string $deprecatedDescription, private bool $isDeprecated, private bool $isInternal, @@ -413,6 +415,7 @@ public function changePropertyGetHookPhpDocType(Type $phpDocType): self $this->phpDocParameterTypes, $phpDocType, $this->phpDocThrowType, + $this->resolvedPhpDocBlock, $this->deprecatedDescription, $this->isDeprecated, $this->isInternal, @@ -445,6 +448,7 @@ public function changePropertySetHookPhpDocType(string $parameterName, Type $php $phpDocParameterTypes, $this->phpDocReturnType, $this->phpDocThrowType, + $this->resolvedPhpDocBlock, $this->deprecatedDescription, $this->isDeprecated, $this->isInternal, @@ -476,4 +480,9 @@ public function mustUseReturnValue(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return $this->resolvedPhpDocBlock; + } + } diff --git a/src/Reflection/Php/PhpMethodReflectionFactory.php b/src/Reflection/Php/PhpMethodReflectionFactory.php index ec95a2de81..6ac366fc1b 100644 --- a/src/Reflection/Php/PhpMethodReflectionFactory.php +++ b/src/Reflection/Php/PhpMethodReflectionFactory.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection\Php; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ClassReflection; @@ -28,6 +29,7 @@ public function create( array $phpDocParameterTypes, ?Type $phpDocReturnType, ?Type $phpDocThrowType, + ?ResolvedPhpDocBlock $resolvedPhpDocBlock, ?string $deprecatedDescription, bool $isDeprecated, bool $isInternal, diff --git a/src/Reflection/Php/PhpPropertyReflection.php b/src/Reflection/Php/PhpPropertyReflection.php index ce6f8a2e37..8b30efb00b 100644 --- a/src/Reflection/Php/PhpPropertyReflection.php +++ b/src/Reflection/Php/PhpPropertyReflection.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection\Php; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; @@ -37,6 +38,7 @@ public function __construct( private ReflectionProperty $reflection, private ?ExtendedMethodReflection $getHook, private ?ExtendedMethodReflection $setHook, + private ?ResolvedPhpDocBlock $resolvedPhpDocBlock, private ?string $deprecatedDescription, private bool $isDeprecated, private bool $isInternal, @@ -328,4 +330,9 @@ public function isDummy(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return $this->resolvedPhpDocBlock; + } + } diff --git a/src/Reflection/RealClassClassConstantReflection.php b/src/Reflection/RealClassClassConstantReflection.php index 96795f6427..eaf0971a47 100644 --- a/src/Reflection/RealClassClassConstantReflection.php +++ b/src/Reflection/RealClassClassConstantReflection.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClassConstant; use PHPStan\Internal\DeprecatedAttributeHelper; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; @@ -23,6 +24,7 @@ public function __construct( private ReflectionClassConstant $reflection, private ?Type $nativeType, private ?Type $phpDocType, + private ?ResolvedPhpDocBlock $resolvedPhpDocBlock, private ?string $deprecatedDescription, private bool $isDeprecated, private bool $isInternal, @@ -94,6 +96,11 @@ public function getDeclaringClass(): ClassReflection return $this->declaringClass; } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return $this->resolvedPhpDocBlock; + } + public function isStatic(): bool { return true; diff --git a/src/Reflection/ResolvedMethodReflection.php b/src/Reflection/ResolvedMethodReflection.php index 57b51070a6..151b337289 100644 --- a/src/Reflection/ResolvedMethodReflection.php +++ b/src/Reflection/ResolvedMethodReflection.php @@ -2,7 +2,7 @@ namespace PHPStan\Reflection; -use PHPStan\Reflection\Php\PhpMethodReflection; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; @@ -98,15 +98,6 @@ public function getDeclaringClass(): ClassReflection return $this->reflection->getDeclaringClass(); } - public function getDeclaringTrait(): ?ClassReflection - { - if ($this->reflection instanceof PhpMethodReflection) { - return $this->reflection->getDeclaringTrait(); - } - - return null; - } - public function isStatic(): bool { return $this->reflection->isStatic(); @@ -236,4 +227,9 @@ public function mustUseReturnValue(): TrinaryLogic return $this->reflection->mustUseReturnValue(); } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return $this->reflection->getResolvedPhpDoc(); + } + } diff --git a/src/Reflection/Type/IntersectionTypeMethodReflection.php b/src/Reflection/Type/IntersectionTypeMethodReflection.php index 8d471a85b6..b1b52f3d33 100644 --- a/src/Reflection/Type/IntersectionTypeMethodReflection.php +++ b/src/Reflection/Type/IntersectionTypeMethodReflection.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\Type; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; @@ -233,4 +234,9 @@ public function mustUseReturnValue(): TrinaryLogic return TrinaryLogic::lazyMaxMin($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->mustUseReturnValue()); } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return $this->methods[0]->getResolvedPhpDoc(); + } + } diff --git a/src/Reflection/Type/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index ea6899f7b4..f61f27cd5a 100644 --- a/src/Reflection/Type/UnionTypeMethodReflection.php +++ b/src/Reflection/Type/UnionTypeMethodReflection.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\Type; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; @@ -210,4 +211,9 @@ public function mustUseReturnValue(): TrinaryLogic return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->mustUseReturnValue()); } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return $this->methods[0]->getResolvedPhpDoc(); + } + } diff --git a/src/Reflection/WrappedExtendedMethodReflection.php b/src/Reflection/WrappedExtendedMethodReflection.php index 6fb39dd2f4..d028d80d04 100644 --- a/src/Reflection/WrappedExtendedMethodReflection.php +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\Php\ExtendedDummyParameter; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeVarianceMap; @@ -179,4 +180,9 @@ public function mustUseReturnValue(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return null; + } + } diff --git a/src/Rules/Constants/OverridingConstantRule.php b/src/Rules/Constants/OverridingConstantRule.php index 78cb6faff7..ad538c3468 100644 --- a/src/Rules/Constants/OverridingConstantRule.php +++ b/src/Rules/Constants/OverridingConstantRule.php @@ -65,9 +65,9 @@ private function processSingleConstant(ClassReflection $classReflection, string if ($prototype->isFinal()) { $errors[] = RuleErrorBuilder::message(sprintf( 'Constant %s::%s overrides final constant %s::%s.', - $classReflection->getDisplayName(), + $classReflection->getDisplayName(false), $constantReflection->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getDeclaringClass()->getDisplayName(false), $prototype->getName(), ))->identifier('classConstant.final')->nonIgnorable()->build(); } @@ -77,18 +77,18 @@ private function processSingleConstant(ClassReflection $classReflection, string $errors[] = RuleErrorBuilder::message(sprintf( '%s constant %s::%s overriding public constant %s::%s should also be public.', $constantReflection->isPrivate() ? 'Private' : 'Protected', - $constantReflection->getDeclaringClass()->getDisplayName(), + $constantReflection->getDeclaringClass()->getDisplayName(false), $constantReflection->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getDeclaringClass()->getDisplayName(false), $prototype->getName(), ))->identifier('classConstant.visibility')->nonIgnorable()->build(); } } elseif ($constantReflection->isPrivate()) { $errors[] = RuleErrorBuilder::message(sprintf( 'Private constant %s::%s overriding protected constant %s::%s should be protected or public.', - $constantReflection->getDeclaringClass()->getDisplayName(), + $constantReflection->getDeclaringClass()->getDisplayName(false), $constantReflection->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getDeclaringClass()->getDisplayName(false), $prototype->getName(), ))->identifier('classConstant.visibility')->nonIgnorable()->build(); } @@ -105,19 +105,19 @@ private function processSingleConstant(ClassReflection $classReflection, string $errors[] = RuleErrorBuilder::message(sprintf( 'Native type %s of constant %s::%s is not covariant with native type %s of constant %s::%s.', $constantNativeType->describe(VerbosityLevel::typeOnly()), - $constantReflection->getDeclaringClass()->getDisplayName(), + $constantReflection->getDeclaringClass()->getDisplayName(false), $constantReflection->getName(), $prototypeNativeType->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getDeclaringClass()->getDisplayName(false), $prototype->getName(), ))->identifier('classConstant.nativeType')->nonIgnorable()->build(); } } else { $errors[] = RuleErrorBuilder::message(sprintf( 'Constant %s::%s overriding constant %s::%s (%s) should also have native type %s.', - $constantReflection->getDeclaringClass()->getDisplayName(), + $constantReflection->getDeclaringClass()->getDisplayName(false), $constantReflection->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getDeclaringClass()->getDisplayName(false), $prototype->getName(), $prototypeNativeType->describe(VerbosityLevel::typeOnly()), $prototypeNativeType->describe(VerbosityLevel::typeOnly()), @@ -137,10 +137,10 @@ private function processSingleConstant(ClassReflection $classReflection, string $errors[] = RuleErrorBuilder::message(sprintf( 'Type %s of constant %s::%s is not covariant with type %s of constant %s::%s.', $constantReflection->getValueType()->describe(VerbosityLevel::value()), - $constantReflection->getDeclaringClass()->getDisplayName(), + $constantReflection->getDeclaringClass()->getDisplayName(false), $constantReflection->getName(), $prototype->getValueType()->describe(VerbosityLevel::value()), - $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getDeclaringClass()->getDisplayName(false), $prototype->getName(), ))->identifier('classConstant.type')->build(); } diff --git a/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php b/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php index b7a102db9f..9feae078a1 100644 --- a/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php +++ b/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\RestrictedUsage; use PhpParser\Node\Expr; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\TrinaryLogic; @@ -108,4 +109,9 @@ public function getFileName(): ?string return $this->constantReflection->getFileName(); } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return $this->constantReflection->getResolvedPhpDoc(); + } + } diff --git a/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php b/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php index c834dfd5b1..43fc7ffaed 100644 --- a/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php +++ b/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\RestrictedUsage; +use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; @@ -150,4 +151,9 @@ public function mustUseReturnValue(): TrinaryLogic return $this->methodReflection->mustUseReturnValue(); } + public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock + { + return $this->methodReflection->getResolvedPhpDoc(); + } + } diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 1ace143f29..caa1cfa1eb 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -22,7 +22,6 @@ use PHPStan\Parser\RichParser; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; -use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\Reflection\ClassReflectionFactory; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Rules\AlwaysFailRule; @@ -803,7 +802,7 @@ private function createAnalyser(): Analyser $container = self::getContainer(); $typeSpecifier = $container->getService('typeSpecifier'); $fileTypeMapper = $container->getByType(FileTypeMapper::class); - $phpDocInheritanceResolver = new PhpDocInheritanceResolver($fileTypeMapper, $container->getByType(StubPhpDocProvider::class)); + $phpDocInheritanceResolver = new PhpDocInheritanceResolver($fileTypeMapper); $nodeScopeResolver = new NodeScopeResolver( $reflectionProvider, diff --git a/tests/PHPStan/Rules/Constants/data/overriding-constant-native-types.php b/tests/PHPStan/Rules/Constants/data/overriding-constant-native-types.php index 9b8afbbcad..e96d4c450e 100644 --- a/tests/PHPStan/Rules/Constants/data/overriding-constant-native-types.php +++ b/tests/PHPStan/Rules/Constants/data/overriding-constant-native-types.php @@ -48,3 +48,22 @@ class PharChild extends \Phar const int|string NONE = 1; // error } + +class ResultA { + public function __construct(public string $value) {} +} + +class ResultB extends ResultA { + public function rot13(): string { return str_rot13($this->value); } +} + +/** @template-implements I */ +class In implements I { + public const string ResultType = ResultB::class; +} + +/** @template T of ResultA */ +interface I { + /** @var class-string */ + public const string ResultType = ResultA::class; +} diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 6710e2c818..b5b8d1e4e1 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -1286,4 +1286,9 @@ public function testBug8438(): void $this->analyse([__DIR__ . '/data/bug-8438.php'], []); } + public function testBug10771(): void + { + $this->analyse([__DIR__ . '/data/bug-10771.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-10771.php b/tests/PHPStan/Rules/Methods/data/bug-10771.php new file mode 100644 index 0000000000..052da8049b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10771.php @@ -0,0 +1,58 @@ + + */ +class A extends B +{ + /** + * @return class-string + */ + public function getClassString(): string + { + return static::ENTITY; + } +} + +/** + * @template T of Template + */ +class B +{ + /** @var class-string */ + public const ENTITY = Template::class; +} + +class Template +{ + +} + +/** + * @template TA of Template + * + * @extends Bb + */ +class Aa extends Bb +{ + /** + * @return class-string + */ + public function getClassString(): string + { + return static::ENTITY; + } +} + +/** + * @template TB of Template + */ +class Bb +{ + /** @var class-string */ + public const ENTITY = Template::class; +}