Skip to content

Commit

Permalink
[Generics] Skip existing tag and add nullable type support (#5367)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasVotruba authored Jan 29, 2021
1 parent 4590b10 commit a4a490d
Show file tree
Hide file tree
Showing 12 changed files with 346 additions and 58 deletions.
32 changes: 16 additions & 16 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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": {
Expand Down
13 changes: 13 additions & 0 deletions packages/better-php-doc-parser/src/PhpDocInfo/PhpDocInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,19 @@ public function hasChanged(): bool
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@

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;

Expand Down Expand Up @@ -119,11 +118,7 @@ public function getNodeTypes(): array
*/
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;
}
Expand All @@ -133,45 +128,45 @@ public function refactor(Node $node): ?Node
$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);
}

return $node;
}

private function resolveGenericChildParentClassReflections(Class_ $class): ?GenericChildParentClassReflections
{
$scope = $class->getAttribute(AttributeKey::SCOPE);
if (! $scope instanceof Scope) {
return null;
}

$classReflection = $scope->getClassReflection();
if (! $classReflection instanceof ClassReflection) {
return null;
}

if (! $this->genericClassReflectionAnalyzer->isGeneric($classReflection)) {
return null;
/**
* @param MethodTagValueNode[] $methodTagValueNodes
* @return MethodTagValueNode[]
*/
private function filterOutExistingMethodTagValuesNodes(
array $methodTagValueNodes,
PhpDocInfo $phpDocInfo
): array {
$methodTagNames = $phpDocInfo->getMethodTagNames();
if ($methodTagNames === []) {
return $methodTagValueNodes;
}

$parentClassReflection = $classReflection->getParentClass();
if (! $parentClassReflection instanceof ClassReflection) {
return null;
}
$filteredMethodTagValueNodes = [];
foreach ($methodTagValueNodes as $methodTagValueNode) {
if (in_array($methodTagValueNode->methodName, $methodTagNames, true)) {
continue;
}

if (! $this->genericClassReflectionAnalyzer->isGeneric($parentClassReflection)) {
return null;
$filteredMethodTagValueNodes[] = $methodTagValueNode;
}

return new GenericChildParentClassReflections($classReflection, $parentClassReflection);
return $filteredMethodTagValueNodes;
}
}
38 changes: 37 additions & 1 deletion rules/generics/src/Reflection/GenericClassReflectionAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,52 @@

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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@
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
Expand Down Expand Up @@ -45,17 +50,7 @@ public function createFromMethodReflectionAndReturnTagValueNode(
$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,
Expand All @@ -82,4 +77,68 @@ private function resolveStringParameters(array $parameterReflections): array

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;
}
}
Original file line number Diff line number Diff line change
@@ -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
{
}

?>
Original file line number Diff line number Diff line change
@@ -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
{
}

?>
Loading

0 comments on commit a4a490d

Please sign in to comment.