[Generics] Skip existing tag and add nullable type support (#5367)

This commit is contained in:
Tomas Votruba 2021-01-29 21:23:01 +01:00 committed by GitHub
parent 4590b10b43
commit a4a490d69a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 347 additions and 59 deletions

View File

@ -48,16 +48,16 @@
"symfony/finder": "^4.4.8|^5.1",
"symfony/http-kernel": "^4.4.8|^5.1",
"symfony/process": "^4.4.8|^5.1",
"symplify/astral": "^9.0.34",
"symplify/autowire-array-parameter": "^9.0.34",
"symplify/console-color-diff": "^9.0.34",
"symplify/package-builder": "^9.0.34",
"symplify/rule-doc-generator": "^9.0.34",
"symplify/set-config-resolver": "^9.0.34",
"symplify/simple-php-doc-parser": "^9.0",
"symplify/skipper": "^9.0.34",
"symplify/smart-file-system": "^9.0.34",
"symplify/symfony-php-config": "^9.0.34",
"symplify/astral": "^9.0.48",
"symplify/autowire-array-parameter": "^9.0.48",
"symplify/console-color-diff": "^9.0.48",
"symplify/package-builder": "^9.0.48",
"symplify/rule-doc-generator": "^9.0.48",
"symplify/set-config-resolver": "^9.0.48",
"symplify/simple-php-doc-parser": "^9.0.48",
"symplify/skipper": "^9.0.48",
"symplify/smart-file-system": "^9.0.48",
"symplify/symfony-php-config": "^9.0.48",
"webmozart/assert": "^1.9"
},
"require-dev": {
@ -72,12 +72,12 @@
"sebastian/diff": "^4.0.4",
"symfony/security-core": "^5.2",
"symfony/security-http": "^5.2",
"symplify/changelog-linker": "^9.0.34",
"symplify/coding-standard": "^9.0.34",
"symplify/easy-coding-standard": "^9.0.34",
"symplify/easy-testing": "^9.0.34",
"symplify/phpstan-extensions": "^9.0.34",
"symplify/phpstan-rules": "^9.0.34",
"symplify/changelog-linker": "^9.0.48",
"symplify/coding-standard": "^9.0.48",
"symplify/easy-coding-standard": "^9.0.48",
"symplify/easy-testing": "^9.0.48",
"symplify/phpstan-extensions": "^9.0.48",
"symplify/phpstan-rules": "^9.0.48",
"tracy/tracy": "^2.7"
},
"replace": {

View File

@ -464,6 +464,19 @@ final class PhpDocInfo
return $this->hasChanged;
}
/**
* @return string[]
*/
public function getMethodTagNames(): array
{
$methodTagNames = [];
foreach ($this->phpDocNode->getMethodTagValues() as $methodTagValueNode) {
$methodTagNames[] = $methodTagValueNode->methodName;
}
return $methodTagNames;
}
private function getTypeOrMixed(?PhpDocTagValueNode $phpDocTagValueNode): Type
{
if ($phpDocTagValueNode === null) {

View File

@ -6,14 +6,13 @@ namespace Rector\Generics\Rector\Class_;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\Core\Rector\AbstractRector;
use Rector\Generics\NodeType\GenericTypeSpecifier;
use Rector\Generics\Reflection\ClassGenericMethodResolver;
use Rector\Generics\Reflection\GenericClassReflectionAnalyzer;
use Rector\Generics\ValueObject\GenericChildParentClassReflections;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
@ -119,11 +118,7 @@ CODE_SAMPLE
*/
public function refactor(Node $node): ?Node
{
if ($node->extends === null) {
return null;
}
$genericChildParentClassReflections = $this->resolveGenericChildParentClassReflections($node);
$genericChildParentClassReflections = $this->genericClassReflectionAnalyzer->resolveChildParent($node);
if (! $genericChildParentClassReflections instanceof GenericChildParentClassReflections) {
return null;
}
@ -133,13 +128,16 @@ CODE_SAMPLE
$genericChildParentClassReflections->getParentClassReflection()
);
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node);
$methodTagValueNodes = $this->filterOutExistingMethodTagValuesNodes($methodTagValueNodes, $phpDocInfo);
$this->genericTypeSpecifier->replaceGenericTypesWithSpecificTypes(
$methodTagValueNodes,
$node,
$genericChildParentClassReflections->getChildClassReflection()
);
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node);
foreach ($methodTagValueNodes as $methodTagValueNode) {
$phpDocInfo->addTagValueNode($methodTagValueNode);
}
@ -147,31 +145,28 @@ CODE_SAMPLE
return $node;
}
private function resolveGenericChildParentClassReflections(Class_ $class): ?GenericChildParentClassReflections
{
$scope = $class->getAttribute(AttributeKey::SCOPE);
if (! $scope instanceof Scope) {
return null;
/**
* @param MethodTagValueNode[] $methodTagValueNodes
* @return MethodTagValueNode[]
*/
private function filterOutExistingMethodTagValuesNodes(
array $methodTagValueNodes,
PhpDocInfo $phpDocInfo
): array {
$methodTagNames = $phpDocInfo->getMethodTagNames();
if ($methodTagNames === []) {
return $methodTagValueNodes;
}
$classReflection = $scope->getClassReflection();
if (! $classReflection instanceof ClassReflection) {
return null;
$filteredMethodTagValueNodes = [];
foreach ($methodTagValueNodes as $methodTagValueNode) {
if (in_array($methodTagValueNode->methodName, $methodTagNames, true)) {
continue;
}
$filteredMethodTagValueNodes[] = $methodTagValueNode;
}
if (! $this->genericClassReflectionAnalyzer->isGeneric($classReflection)) {
return null;
}
$parentClassReflection = $classReflection->getParentClass();
if (! $parentClassReflection instanceof ClassReflection) {
return null;
}
if (! $this->genericClassReflectionAnalyzer->isGeneric($parentClassReflection)) {
return null;
}
return new GenericChildParentClassReflections($classReflection, $parentClassReflection);
return $filteredMethodTagValueNodes;
}
}

View File

@ -4,16 +4,52 @@ declare(strict_types=1);
namespace Rector\Generics\Reflection;
use PhpParser\Node\Stmt\Class_;
use PHPStan\Analyser\Scope;
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\Reflection\ClassReflection;
use Rector\Generics\ValueObject\GenericChildParentClassReflections;
use Rector\NodeTypeResolver\Node\AttributeKey;
final class GenericClassReflectionAnalyzer
{
public function resolveChildParent(Class_ $class): ?GenericChildParentClassReflections
{
if ($class->extends === null) {
return null;
}
$scope = $class->getAttribute(AttributeKey::SCOPE);
if (! $scope instanceof Scope) {
return null;
}
$classReflection = $scope->getClassReflection();
if (! $classReflection instanceof ClassReflection) {
return null;
}
if (! $this->isGeneric($classReflection)) {
return null;
}
$parentClassReflection = $classReflection->getParentClass();
if (! $parentClassReflection instanceof ClassReflection) {
return null;
}
if (! $this->isGeneric($parentClassReflection)) {
return null;
}
return new GenericChildParentClassReflections($classReflection, $parentClassReflection);
}
/**
* Solve isGeneric() ignores extends and similar tags,
* so it has to be extended with "@extends" and "@implements"
*/
public function isGeneric(ClassReflection $classReflection): bool
private function isGeneric(ClassReflection $classReflection): bool
{
if ($classReflection->isGeneric()) {
return true;

View File

@ -7,10 +7,15 @@ namespace Rector\Generics\TagValueNodeFactory;
use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Type;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\StaticTypeMapper\StaticTypeMapper;
final class MethodTagValueNodeFactory
@ -45,17 +50,7 @@ final class MethodTagValueNodeFactory
$classReflection = $methodReflection->getDeclaringClass();
$templateTypeMap = $classReflection->getTemplateTypeMap();
$returnTagTypeNode = $returnTagValueNode->type;
if ($returnTagValueNode->type instanceof IdentifierTypeNode) {
$typeName = $returnTagValueNode->type->name;
$genericType = $templateTypeMap->getType($typeName);
if ($genericType instanceof Type) {
$returnTagType = $genericType;
$returnTagTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($returnTagType);
}
}
$returnTagTypeNode = $this->resolveReturnTagTypeNode($returnTagValueNode, $templateTypeMap);
return new MethodTagValueNode(
false,
@ -82,4 +77,68 @@ final class MethodTagValueNodeFactory
return $stringParameters;
}
private function resolveReturnTagTypeNode(
ReturnTagValueNode $returnTagValueNode,
TemplateTypeMap $templateTypeMap
): TypeNode {
$returnTagTypeNode = $returnTagValueNode->type;
if ($returnTagValueNode->type instanceof UnionTypeNode) {
return $this->resolveUnionTypeNode($returnTagValueNode->type, $templateTypeMap);
}
if ($returnTagValueNode->type instanceof IdentifierTypeNode) {
return $this->resolveIdentifierTypeNode(
$returnTagValueNode->type,
$templateTypeMap,
$returnTagTypeNode
);
}
return $returnTagTypeNode;
}
private function resolveUnionTypeNode(UnionTypeNode $unionTypeNode, TemplateTypeMap $templateTypeMap): UnionTypeNode
{
$resolvedTypes = [];
foreach ($unionTypeNode->types as $unionedTypeNode) {
if ($unionedTypeNode instanceof ArrayTypeNode) {
if (! $unionedTypeNode->type instanceof IdentifierTypeNode) {
throw new ShouldNotHappenException();
}
$resolvedType = $this->resolveIdentifierTypeNode(
$unionedTypeNode->type,
$templateTypeMap,
$unionedTypeNode
);
$resolvedTypes[] = new ArrayTypeNode($resolvedType);
} elseif ($unionedTypeNode instanceof IdentifierTypeNode) {
$resolvedTypes[] = $this->resolveIdentifierTypeNode(
$unionedTypeNode,
$templateTypeMap,
$unionedTypeNode
);
}
}
return new UnionTypeNode($resolvedTypes);
}
private function resolveIdentifierTypeNode(
IdentifierTypeNode $identifierTypeNode,
TemplateTypeMap $templateTypeMap,
TypeNode $fallbackTypeNode
): TypeNode {
$typeName = $identifierTypeNode->name;
$genericType = $templateTypeMap->getType($typeName);
if ($genericType instanceof Type) {
$returnTagType = $genericType;
return $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($returnTagType);
}
return $fallbackTypeNode;
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Fixture;
use Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\AbstractGenericArrayRepository;
use Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\RealObject;
/**
* @template TEntity as RealObject
* @extends AbstractRepository<TEntity>
*/
final class ArrayLike extends AbstractGenericArrayRepository
{
}
?>
-----
<?php
declare(strict_types=1);
namespace Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Fixture;
use Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\AbstractGenericArrayRepository;
use Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\RealObject;
/**
* @template TEntity as RealObject
* @extends AbstractRepository<TEntity>
* @method \Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\RealObject[] findAll()
*/
final class ArrayLike extends AbstractGenericArrayRepository
{
}
?>

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Fixture;
use Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\AbstractGenericMaybeArrayRepository;
use Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\RealObject;
/**
* @template TEntity as RealObject
* @extends AbstractRepository<TEntity>
*/
final class ArrayLikeNullable extends AbstractGenericMaybeArrayRepository
{
}
?>
-----
<?php
declare(strict_types=1);
namespace Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Fixture;
use Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\AbstractGenericMaybeArrayRepository;
use Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\RealObject;
/**
* @template TEntity as RealObject
* @extends AbstractRepository<TEntity>
* @method \Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\RealObject[]|null findAllMaybe(string $some, int $type, $unknown)
*/
final class ArrayLikeNullable extends AbstractGenericMaybeArrayRepository
{
}
?>

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Fixture;
use Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\AbstractMaybeGenericRepository;
use Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\RealObject;
/**
* @template TEntity as RealObject
* @extends AbstractRepository<TEntity>
*/
final class NullableGet extends AbstractMaybeGenericRepository
{
}
?>
-----
<?php
declare(strict_types=1);
namespace Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Fixture;
use Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\AbstractMaybeGenericRepository;
use Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\RealObject;
/**
* @template TEntity as RealObject
* @extends AbstractRepository<TEntity>
* @method \Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\RealObject|null find($id)
*/
final class NullableGet extends AbstractMaybeGenericRepository
{
}
?>

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Fixture;
use Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\AbstractGenericRepository;
use Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\RealObject;
/**
* @template TEntity as RealObject
* @extends AbstractRepository<TEntity>
* @method \Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source\RealObject find($id)
*/
final class SkipAlreadyMethod extends AbstractGenericRepository
{
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source;
/**
* @template TEntity as object
*/
abstract class AbstractGenericArrayRepository
{
/**
* @return TEntity[]
*/
public function findAll()
{
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source;
/**
* @template TEntity as object
*/
abstract class AbstractGenericMaybeArrayRepository
{
/**
* @return TEntity[]|null
*/
public function findAllMaybe(string $some, int $type, $unknown)
{
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Rector\Generics\Tests\Rector\Class_\GenericsPHPStormMethodAnnotationRector\Source;
/**
* @template TEntity as object
*/
abstract class AbstractMaybeGenericRepository
{
/**
* @return TEntity|null
*/
public function find($id)
{
}
}