mirror of https://github.com/rectorphp/rector.git
230 lines
9.3 KiB
PHP
230 lines
9.3 KiB
PHP
<?php
|
|
|
|
declare (strict_types=1);
|
|
namespace Rector\BetterPhpDocParser\PhpDocManipulator;
|
|
|
|
use PhpParser\Node\Param;
|
|
use PhpParser\Node\Stmt\ClassMethod;
|
|
use PhpParser\Node\Stmt\Property;
|
|
use PHPStan\PhpDocParser\Ast\Node;
|
|
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
|
|
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
|
|
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
|
|
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
|
|
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
|
|
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
|
|
use PHPStan\Type\Constant\ConstantArrayType;
|
|
use PHPStan\Type\MixedType;
|
|
use PHPStan\Type\NeverType;
|
|
use PHPStan\Type\Type;
|
|
use Rector\BetterPhpDocParser\Comment\CommentsMerger;
|
|
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
|
|
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
|
|
use Rector\BetterPhpDocParser\ValueObject\Type\BracketsAwareUnionTypeNode;
|
|
use Rector\BetterPhpDocParser\ValueObject\Type\SpacingAwareArrayTypeNode;
|
|
use Rector\BetterPhpDocParser\ValueObject\Type\SpacingAwareCallableTypeNode;
|
|
use Rector\NodeNameResolver\NodeNameResolver;
|
|
use Rector\NodeTypeResolver\Node\AttributeKey;
|
|
use Rector\NodeTypeResolver\TypeComparator\TypeComparator;
|
|
use Rector\PHPStanStaticTypeMapper\Enum\TypeKind;
|
|
use Rector\StaticTypeMapper\StaticTypeMapper;
|
|
use Rector\TypeDeclaration\PhpDocParser\ParamPhpDocNodeFactory;
|
|
final class PhpDocTypeChanger
|
|
{
|
|
/**
|
|
* @var array<class-string<Node>>
|
|
*/
|
|
public const ALLOWED_TYPES = [GenericTypeNode::class, SpacingAwareArrayTypeNode::class, SpacingAwareCallableTypeNode::class, ArrayShapeNode::class];
|
|
/**
|
|
* @var string[]
|
|
*/
|
|
private const ALLOWED_IDENTIFIER_TYPENODE_TYPES = ['class-string'];
|
|
/**
|
|
* @readonly
|
|
* @var \Rector\StaticTypeMapper\StaticTypeMapper
|
|
*/
|
|
private $staticTypeMapper;
|
|
/**
|
|
* @readonly
|
|
* @var \Rector\NodeTypeResolver\TypeComparator\TypeComparator
|
|
*/
|
|
private $typeComparator;
|
|
/**
|
|
* @readonly
|
|
* @var \Rector\TypeDeclaration\PhpDocParser\ParamPhpDocNodeFactory
|
|
*/
|
|
private $paramPhpDocNodeFactory;
|
|
/**
|
|
* @readonly
|
|
* @var \Rector\NodeNameResolver\NodeNameResolver
|
|
*/
|
|
private $nodeNameResolver;
|
|
/**
|
|
* @readonly
|
|
* @var \Rector\BetterPhpDocParser\Comment\CommentsMerger
|
|
*/
|
|
private $commentsMerger;
|
|
/**
|
|
* @readonly
|
|
* @var \Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory
|
|
*/
|
|
private $phpDocInfoFactory;
|
|
public function __construct(StaticTypeMapper $staticTypeMapper, TypeComparator $typeComparator, ParamPhpDocNodeFactory $paramPhpDocNodeFactory, NodeNameResolver $nodeNameResolver, CommentsMerger $commentsMerger, PhpDocInfoFactory $phpDocInfoFactory)
|
|
{
|
|
$this->staticTypeMapper = $staticTypeMapper;
|
|
$this->typeComparator = $typeComparator;
|
|
$this->paramPhpDocNodeFactory = $paramPhpDocNodeFactory;
|
|
$this->nodeNameResolver = $nodeNameResolver;
|
|
$this->commentsMerger = $commentsMerger;
|
|
$this->phpDocInfoFactory = $phpDocInfoFactory;
|
|
}
|
|
public function changeVarType(PhpDocInfo $phpDocInfo, Type $newType) : void
|
|
{
|
|
// better skip, could crash hard
|
|
if ($phpDocInfo->hasInvalidTag('@var')) {
|
|
return;
|
|
}
|
|
// make sure the tags are not identical, e.g imported class vs FQN class
|
|
if ($this->typeComparator->areTypesEqual($phpDocInfo->getVarType(), $newType)) {
|
|
return;
|
|
}
|
|
// prevent existing type override by mixed
|
|
if (!$phpDocInfo->getVarType() instanceof MixedType && $newType instanceof ConstantArrayType && $newType->getItemType() instanceof NeverType) {
|
|
return;
|
|
}
|
|
// override existing type
|
|
$newPHPStanPhpDocType = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($newType, TypeKind::PROPERTY);
|
|
$currentVarTagValueNode = $phpDocInfo->getVarTagValueNode();
|
|
if ($currentVarTagValueNode !== null) {
|
|
// only change type
|
|
$currentVarTagValueNode->type = $newPHPStanPhpDocType;
|
|
} else {
|
|
// add completely new one
|
|
$varTagValueNode = new VarTagValueNode($newPHPStanPhpDocType, '', '');
|
|
$phpDocInfo->addTagValueNode($varTagValueNode);
|
|
}
|
|
}
|
|
public function changeReturnType(PhpDocInfo $phpDocInfo, Type $newType) : bool
|
|
{
|
|
// better not touch this, can crash
|
|
if ($phpDocInfo->hasInvalidTag('@return')) {
|
|
return \false;
|
|
}
|
|
// make sure the tags are not identical, e.g imported class vs FQN class
|
|
if ($this->typeComparator->areTypesEqual($phpDocInfo->getReturnType(), $newType)) {
|
|
return \false;
|
|
}
|
|
// override existing type
|
|
$newPHPStanPhpDocType = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($newType, TypeKind::RETURN);
|
|
$currentReturnTagValueNode = $phpDocInfo->getReturnTagValue();
|
|
if ($currentReturnTagValueNode !== null) {
|
|
// only change type
|
|
$currentReturnTagValueNode->type = $newPHPStanPhpDocType;
|
|
} else {
|
|
// add completely new one
|
|
$returnTagValueNode = new ReturnTagValueNode($newPHPStanPhpDocType, '');
|
|
$phpDocInfo->addTagValueNode($returnTagValueNode);
|
|
}
|
|
return \true;
|
|
}
|
|
public function changeParamType(PhpDocInfo $phpDocInfo, Type $newType, Param $param, string $paramName) : void
|
|
{
|
|
// better skip, could crash hard
|
|
if ($phpDocInfo->hasInvalidTag('@param')) {
|
|
return;
|
|
}
|
|
$phpDocType = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($newType, TypeKind::PARAM);
|
|
$paramTagValueNode = $phpDocInfo->getParamTagValueByName($paramName);
|
|
// override existing type
|
|
if ($paramTagValueNode !== null) {
|
|
// already set
|
|
$currentType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($paramTagValueNode->type, $param);
|
|
// avoid overriding better type
|
|
if ($this->typeComparator->isSubtype($currentType, $newType)) {
|
|
return;
|
|
}
|
|
if ($this->typeComparator->areTypesEqual($currentType, $newType)) {
|
|
return;
|
|
}
|
|
$paramTagValueNode->type = $phpDocType;
|
|
} else {
|
|
$paramTagValueNode = $this->paramPhpDocNodeFactory->create($phpDocType, $param);
|
|
$phpDocInfo->addTagValueNode($paramTagValueNode);
|
|
}
|
|
}
|
|
public function isAllowed(TypeNode $typeNode) : bool
|
|
{
|
|
if ($typeNode instanceof BracketsAwareUnionTypeNode) {
|
|
foreach ($typeNode->types as $type) {
|
|
if ($this->isAllowed($type)) {
|
|
return \true;
|
|
}
|
|
}
|
|
}
|
|
if (\in_array(\get_class($typeNode), self::ALLOWED_TYPES, \true)) {
|
|
return \true;
|
|
}
|
|
if (!$typeNode instanceof IdentifierTypeNode) {
|
|
return \false;
|
|
}
|
|
return \in_array((string) $typeNode, self::ALLOWED_IDENTIFIER_TYPENODE_TYPES, \true);
|
|
}
|
|
public function copyPropertyDocToParam(Property $property, Param $param) : void
|
|
{
|
|
$phpDocInfo = $this->phpDocInfoFactory->createFromNode($property);
|
|
if (!$phpDocInfo instanceof PhpDocInfo) {
|
|
return;
|
|
}
|
|
$varTag = $phpDocInfo->getVarTagValueNode();
|
|
if (!$varTag instanceof VarTagValueNode) {
|
|
$this->processKeepComments($property, $param);
|
|
return;
|
|
}
|
|
if ($varTag->description !== '') {
|
|
return;
|
|
}
|
|
$functionLike = $param->getAttribute(AttributeKey::PARENT_NODE);
|
|
$paramVarName = $this->nodeNameResolver->getName($param->var);
|
|
if (!$functionLike instanceof ClassMethod) {
|
|
return;
|
|
}
|
|
if (!$this->isAllowed($varTag->type)) {
|
|
return;
|
|
}
|
|
if (!\is_string($paramVarName)) {
|
|
return;
|
|
}
|
|
$phpDocInfo->removeByType(VarTagValueNode::class);
|
|
$param->setAttribute(AttributeKey::PHP_DOC_INFO, $phpDocInfo);
|
|
$phpDocInfo = $functionLike->getAttribute(AttributeKey::PHP_DOC_INFO);
|
|
$paramType = $this->staticTypeMapper->mapPHPStanPhpDocTypeToPHPStanType($varTag, $property);
|
|
$this->changeParamType($phpDocInfo, $paramType, $param, $paramVarName);
|
|
$this->processKeepComments($property, $param);
|
|
}
|
|
public function changeVarTypeNode(PhpDocInfo $phpDocInfo, TypeNode $typeNode) : void
|
|
{
|
|
// add completely new one
|
|
$varTagValueNode = new VarTagValueNode($typeNode, '', '');
|
|
$phpDocInfo->addTagValueNode($varTagValueNode);
|
|
}
|
|
private function processKeepComments(Property $property, Param $param) : void
|
|
{
|
|
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($param);
|
|
$varTag = $phpDocInfo->getVarTagValueNode();
|
|
$toBeRemoved = !$varTag instanceof VarTagValueNode;
|
|
$this->commentsMerger->keepComments($param, [$property]);
|
|
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($param);
|
|
$varTag = $phpDocInfo->getVarTagValueNode();
|
|
if (!$toBeRemoved) {
|
|
return;
|
|
}
|
|
if (!$varTag instanceof VarTagValueNode) {
|
|
return;
|
|
}
|
|
if ($varTag->description !== '') {
|
|
return;
|
|
}
|
|
$phpDocInfo->removeByType(VarTagValueNode::class);
|
|
}
|
|
}
|