Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 6 commits into from
Jan 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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