Skip to content

Commit

Permalink
MethodSignatureRule - read PHPDoc types instead of combined types
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Oct 24, 2020
1 parent 26d29ec commit fb8d3ef
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 32 deletions.
70 changes: 62 additions & 8 deletions src/Reflection/Php/PhpClassReflectionExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\ErrorType;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\Generic\TemplateTypeHelper;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\MixedType;
Expand Down Expand Up @@ -69,6 +70,8 @@ class PhpClassReflectionExtension

private \PHPStan\Reflection\ReflectionProvider $reflectionProvider;

private FileTypeMapper $fileTypeMapper;

/** @var string[] */
private array $universalObjectCratesClasses;

Expand Down Expand Up @@ -101,6 +104,7 @@ class PhpClassReflectionExtension
* @param \PHPStan\Parser\Parser $parser
* @param \PHPStan\PhpDoc\StubPhpDocProvider $stubPhpDocProvider
* @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider
* @param FileTypeMapper $fileTypeMapper
* @param bool $inferPrivatePropertyTypeFromConstructor
* @param string[] $universalObjectCratesClasses
*/
Expand All @@ -115,6 +119,7 @@ public function __construct(
Parser $parser,
StubPhpDocProvider $stubPhpDocProvider,
ReflectionProvider $reflectionProvider,
FileTypeMapper $fileTypeMapper,
bool $inferPrivatePropertyTypeFromConstructor,
array $universalObjectCratesClasses
)
Expand All @@ -129,6 +134,7 @@ public function __construct(
$this->parser = $parser;
$this->stubPhpDocProvider = $stubPhpDocProvider;
$this->reflectionProvider = $reflectionProvider;
$this->fileTypeMapper = $fileTypeMapper;
$this->inferPrivatePropertyTypeFromConstructor = $inferPrivatePropertyTypeFromConstructor;
$this->universalObjectCratesClasses = $universalObjectCratesClasses;
}
Expand Down Expand Up @@ -422,9 +428,11 @@ private function createMethod(
}
foreach ($variantNumbers as $variantNumber) {
$methodSignature = $this->signatureMapProvider->getMethodSignature($declaringClassName, $methodReflection->getName(), $reflectionMethod, $variantNumber);
$phpDocReturnType = null;
$stubPhpDocReturnType = null;
$stubPhpDocParameterTypes = [];
$stubPhpDocParameterVariadicity = [];
$phpDocParameterTypes = [];
$phpDocReturnType = null;
if (count($variantNumbers) === 1) {
$stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $methodReflection->getName(), array_map(static function (ParameterSignature $parameterSignature): string {
return $parameterSignature->getName();
Expand All @@ -435,9 +443,8 @@ private function createMethod(
$templateTypeMap = $stubDeclaringClass->getActiveTemplateTypeMap();
$returnTag = $stubPhpDoc->getReturnTag();
if ($returnTag !== null) {
$stubPhpDocReturnType = $returnTag->getType();
$phpDocReturnType = TemplateTypeHelper::resolveTemplateTypes(
$stubPhpDocReturnType,
$stubPhpDocReturnType = TemplateTypeHelper::resolveTemplateTypes(
$returnTag->getType(),
$templateTypeMap
);
}
Expand All @@ -449,9 +456,27 @@ private function createMethod(
);
$stubPhpDocParameterVariadicity[$name] = $paramTag->isVariadic();
}
} elseif ($reflectionMethod !== null && $reflectionMethod->getDocComment() !== false) {
$filename = $reflectionMethod->getFileName();
if ($filename !== false) {
$phpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc(
$filename,
$declaringClassName,
null,
$reflectionMethod->getName(),
$reflectionMethod->getDocComment()
);
$returnTag = $phpDocBlock->getReturnTag();
if ($returnTag !== null) {
$phpDocReturnType = $returnTag->getType();
}
foreach ($phpDocBlock->getParamTags() as $name => $paramTag) {
$phpDocParameterTypes[$name] = $paramTag->getType();
}
}
}
}
$variants[] = $this->createNativeMethodVariant($methodSignature, $stubPhpDocParameterTypes, $stubPhpDocParameterVariadicity, $phpDocReturnType);
$variants[] = $this->createNativeMethodVariant($methodSignature, $stubPhpDocParameterTypes, $stubPhpDocParameterVariadicity, $stubPhpDocReturnType, $phpDocParameterTypes, $phpDocReturnType);
}

if ($this->signatureMapProvider->hasMethodMetadata($declaringClassName, $methodReflection->getName())) {
Expand Down Expand Up @@ -557,36 +582,65 @@ private function createMethod(
* @param FunctionSignature $methodSignature
* @param array<string, Type> $stubPhpDocParameterTypes
* @param array<string, bool> $stubPhpDocParameterVariadicity
* @param Type|null $stubPhpDocReturnType
* @param array<string, Type> $phpDocParameterTypes
* @param Type|null $phpDocReturnType
* @return FunctionVariantWithPhpDocs
*/
private function createNativeMethodVariant(
FunctionSignature $methodSignature,
array $stubPhpDocParameterTypes,
array $stubPhpDocParameterVariadicity,
?Type $stubPhpDocReturnType,
array $phpDocParameterTypes,
?Type $phpDocReturnType
): FunctionVariantWithPhpDocs
{
$parameters = [];
foreach ($methodSignature->getParameters() as $parameterSignature) {
$type = null;
$phpDocType = null;

if (isset($stubPhpDocParameterTypes[$parameterSignature->getName()])) {
$type = $stubPhpDocParameterTypes[$parameterSignature->getName()];
$phpDocType = $stubPhpDocParameterTypes[$parameterSignature->getName()];
} elseif (isset($phpDocParameterTypes[$parameterSignature->getName()])) {
$type = TypehintHelper::decideType(
$parameterSignature->getNativeType(),
$phpDocParameterTypes[$parameterSignature->getName()]
);
$phpDocType = $phpDocParameterTypes[$parameterSignature->getName()];
}

$parameters[] = new NativeParameterWithPhpDocsReflection(
$parameterSignature->getName(),
$parameterSignature->isOptional(),
$stubPhpDocParameterTypes[$parameterSignature->getName()] ?? $parameterSignature->getType(),
$stubPhpDocParameterTypes[$parameterSignature->getName()] ?? new MixedType(),
$type ?? $parameterSignature->getType(),
$phpDocType ?? new MixedType(),
$parameterSignature->getNativeType(),
$parameterSignature->passedByReference(),
$stubPhpDocParameterVariadicity[$parameterSignature->getName()] ?? $parameterSignature->isVariadic(),
null
);
}

$returnType = null;
if ($stubPhpDocReturnType !== null) {
$returnType = $stubPhpDocReturnType;
$phpDocReturnType = $stubPhpDocReturnType;
} elseif ($phpDocReturnType !== null) {
$returnType = TypehintHelper::decideType(
$methodSignature->getReturnType(),
$phpDocReturnType
);
}

return new FunctionVariantWithPhpDocs(
TemplateTypeMap::createEmpty(),
null,
$parameters,
$methodSignature->isVariadic(),
$phpDocReturnType ?? $methodSignature->getReturnType(),
$returnType ?? $methodSignature->getReturnType(),
$phpDocReturnType ?? new MixedType(),
$methodSignature->getNativeReturnType()
);
Expand Down
64 changes: 41 additions & 23 deletions src/Rules/Methods/MethodSignatureRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassMethodNode;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ParametersAcceptorWithPhpDocs;
use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\TrinaryLogic;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use PHPStan\Type\TypehintHelper;
use PHPStan\Type\VerbosityLevel;
use PHPStan\Type\VoidType;

Expand Down Expand Up @@ -43,7 +43,7 @@ public function getNodeType(): string
public function processNode(Node $node, Scope $scope): array
{
$method = $scope->getFunction();
if (!$method instanceof MethodReflection) {
if (!$method instanceof PhpMethodFromParserNodeReflection) {
return [];
}

Expand All @@ -60,20 +60,25 @@ public function processNode(Node $node, Scope $scope): array
$parameters = ParametersAcceptorSelector::selectSingle($method->getVariants());

$errors = [];
foreach ($this->collectParentMethods($methodName, $method->getDeclaringClass(), $scope) as $parentMethod) {
$parentParameters = ParametersAcceptorSelector::selectFromTypes(array_map(static function (ParameterReflection $parameter): Type {
return $parameter->getType();
}, $parameters->getParameters()), $parentMethod->getVariants(), false);
foreach ($this->collectParentMethods($methodName, $method->getDeclaringClass()) as $parentMethod) {
$parentVariants = $parentMethod->getVariants();
if (count($parentVariants) !== 1) {
continue;
}
$parentParameters = $parentVariants[0];
if (!$parentParameters instanceof ParametersAcceptorWithPhpDocs) {
continue;
}

$returnTypeCompatibility = $this->checkReturnTypeCompatibility($parameters->getReturnType(), $parentParameters->getReturnType());
$returnTypeCompatibility = $this->checkReturnTypeCompatibility($parameters, $parentParameters);
if ($returnTypeCompatibility->no() || (!$returnTypeCompatibility->yes() && $this->reportMaybes)) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Return type (%s) of method %s::%s() should be %s with return type (%s) of method %s::%s()',
$parameters->getReturnType()->describe(VerbosityLevel::value()),
$parameters->getPhpDocReturnType()->describe(VerbosityLevel::value()),
$method->getDeclaringClass()->getDisplayName(),
$method->getName(),
$returnTypeCompatibility->no() ? 'compatible' : 'covariant',
$parentParameters->getReturnType()->describe(VerbosityLevel::value()),
$parentParameters->getPhpDocReturnType()->describe(VerbosityLevel::value()),
$parentMethod->getDeclaringClass()->getDisplayName(),
$parentMethod->getName()
))->build();
Expand Down Expand Up @@ -111,37 +116,44 @@ public function processNode(Node $node, Scope $scope): array
/**
* @param string $methodName
* @param \PHPStan\Reflection\ClassReflection $class
* @param \PHPStan\Analyser\Scope $scope
* @return \PHPStan\Reflection\MethodReflection[]
*/
private function collectParentMethods(string $methodName, ClassReflection $class, Scope $scope): array
private function collectParentMethods(string $methodName, ClassReflection $class): array
{
$parentMethods = [];

$parentClass = $class->getParentClass();
if ($parentClass !== false && $parentClass->hasMethod($methodName)) {
$parentMethod = $parentClass->getMethod($methodName, $scope);
if ($parentClass !== false && $parentClass->hasNativeMethod($methodName)) {
$parentMethod = $parentClass->getNativeMethod($methodName);
if (!$parentMethod->isPrivate()) {
$parentMethods[] = $parentMethod;
}
}

foreach ($class->getInterfaces() as $interface) {
if (!$interface->hasMethod($methodName)) {
if (!$interface->hasNativeMethod($methodName)) {
continue;
}

$parentMethods[] = $interface->getMethod($methodName, $scope);
$parentMethods[] = $interface->getNativeMethod($methodName);
}

return $parentMethods;
}

private function checkReturnTypeCompatibility(
Type $returnType,
Type $parentReturnType
ParametersAcceptorWithPhpDocs $currentVariant,
ParametersAcceptorWithPhpDocs $parentVariant
): TrinaryLogic
{
$returnType = TypehintHelper::decideType(
$currentVariant->getNativeReturnType(),
$currentVariant->getPhpDocReturnType()
);
$parentReturnType = TypehintHelper::decideType(
$parentVariant->getNativeReturnType(),
$parentVariant->getPhpDocReturnType()
);
// Allow adding `void` return type hints when the parent defines no return type
if ($returnType instanceof VoidType && $parentReturnType instanceof MixedType) {
return TrinaryLogic::createYes();
Expand All @@ -156,8 +168,8 @@ private function checkReturnTypeCompatibility(
}

/**
* @param \PHPStan\Reflection\ParameterReflection[] $parameters
* @param \PHPStan\Reflection\ParameterReflection[] $parentParameters
* @param \PHPStan\Reflection\ParameterReflectionWithPhpDocs[] $parameters
* @param \PHPStan\Reflection\ParameterReflectionWithPhpDocs[] $parentParameters
* @return array<int, TrinaryLogic>
*/
private function checkParameterTypeCompatibility(
Expand All @@ -172,8 +184,14 @@ private function checkParameterTypeCompatibility(
$parameter = $parameters[$i];
$parentParameter = $parentParameters[$i];

$parameterType = $parameter->getType();
$parentParameterType = $parentParameter->getType();
$parameterType = TypehintHelper::decideType(
$parameter->getNativeType(),
$parameter->getPhpDocType()
);
$parentParameterType = TypehintHelper::decideType(
$parentParameter->getNativeType(),
$parentParameter->getPhpDocType()
);

$parameterResults[] = $parameterType->isSuperTypeOf($parentParameterType);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Testing/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ public function create(
$annotationsPropertiesClassReflectionExtension = new AnnotationsPropertiesClassReflectionExtension();
$signatureMapProvider = self::getContainer()->getByType(SignatureMapProvider::class);
$methodReflectionFactory->reflectionProvider = $actualReflectionProvider;
$phpExtension = new PhpClassReflectionExtension(self::getContainer()->getByType(ScopeFactory::class), self::getContainer()->getByType(NodeScopeResolver::class), $methodReflectionFactory, $phpDocInheritanceResolver, $annotationsMethodsClassReflectionExtension, $annotationsPropertiesClassReflectionExtension, $signatureMapProvider, $parser, self::getContainer()->getByType(StubPhpDocProvider::class), $actualReflectionProvider, true, []);
$phpExtension = new PhpClassReflectionExtension(self::getContainer()->getByType(ScopeFactory::class), self::getContainer()->getByType(NodeScopeResolver::class), $methodReflectionFactory, $phpDocInheritanceResolver, $annotationsMethodsClassReflectionExtension, $annotationsPropertiesClassReflectionExtension, $signatureMapProvider, $parser, self::getContainer()->getByType(StubPhpDocProvider::class), $actualReflectionProvider, $fileTypeMapper, true, []);
$classReflectionExtensionRegistryProvider->addPropertiesClassReflectionExtension($phpExtension);
$classReflectionExtensionRegistryProvider->addPropertiesClassReflectionExtension(new UniversalObjectCratesClassReflectionExtension([\stdClass::class]));
$classReflectionExtensionRegistryProvider->addPropertiesClassReflectionExtension(new MixinPropertiesClassReflectionExtension([]));
Expand Down
5 changes: 5 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10245,6 +10245,11 @@ public function dataBug3993(): array
return $this->gatherAssertTypes(__DIR__ . '/data/bug-3993.php');
}

public function dataBug3997(): array
{
return $this->gatherAssertTypes(__DIR__ . '/data/bug-3997.php');
}

/**
* @dataProvider dataBug2574
* @dataProvider dataBug2577
Expand Down
15 changes: 15 additions & 0 deletions tests/PHPStan/Analyser/data/bug-3997.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Bug3997Type;

use function PHPStan\Analyser\assertType;

function (\Countable $c): void {
assertType('int<0, max>', $c->count());
assertType('int<0, max>', count($c));
};

function (\ArrayIterator $i): void {
assertType('int<0, max>', $i->count());
assertType('int<0, max>', count($i));
};
12 changes: 12 additions & 0 deletions tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,16 @@ public function testBug2950(): void
$this->analyse([__DIR__ . '/data/bug-2950.php'], []);
}

public function testBug3997(): void
{
$this->reportMaybes = true;
$this->reportStatic = true;
$this->analyse([__DIR__ . '/data/bug-3997.php'], [
[
'Return type (string) of method Bug3997\Ipsum::count() should be compatible with return type (int) of method Countable::count()',
59,
],
]);
}

}
22 changes: 22 additions & 0 deletions tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -370,4 +370,26 @@ public function testReturnTypeRulePhp70(): void
]);
}

public function testBug3997(): void
{
$this->analyse([__DIR__ . '/data/bug-3997.php'], [
[
'Method Bug3997\Foo::count() should return int but returns string.',
12,
],
[
'Method Bug3997\Bar::count() should return int but returns string.',
22,
],
[
'Method Bug3997\Baz::count() should return int but returns string.',
35,
],
[
'Method Bug3997\Lorem::count() should return int but returns string.',
48,
],
]);
}

}
Loading

0 comments on commit fb8d3ef

Please sign in to comment.