Skip to content
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
3 changes: 3 additions & 0 deletions config/set/doctrine/doctrine-id-to-uuid-step-3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ services:
Rector\Doctrine\Rector\MethodCall\ChangeGetUuidMethodCallToGetIdRector: ~
Rector\Doctrine\Rector\ClassMethod\ChangeReturnTypeOfClassMethodWithGetIdRector: ~
Rector\Doctrine\Rector\Identical\ChangeIdenticalUuidToEqualsMethodCallRector: ~

# add Uuid type declarations
Rector\TypeDeclaration\Rector\ClassMethod\AddMethodCallBasedParamTypeRector: ~
1 change: 1 addition & 0 deletions ecs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ parameters:
- 'src/Rector/AbstractRector.php'

Symplify\CodingStandard\Sniffs\CleanCode\CognitiveComplexitySniff:
- 'packages/Doctrine/src/Rector/ClassMethod/AddMethodCallBasedParamTypeRector.php'
- 'packages/DoctrinePhpDocParser/src/PhpDocParser/OrmTagParser.php'
- 'packages/TypeDeclaration/src/TypeInferer/ReturnTypeInferer/ReturnedNodesReturnTypeInferer.php'
- 'packages/NodeTypeResolver/src/NodeTypeResolver.php'
Expand Down
5 changes: 5 additions & 0 deletions packages/NodeTypeResolver/src/NodeTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Countable;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayDimFetch;
Expand Down Expand Up @@ -279,6 +280,10 @@ public function getStaticType(Node $node): Type
return new ConstantStringType($node->value);
}

if ($node instanceof Arg) {
return $this->getStaticType($node->value);
}

if ($node instanceof Param) {
$paramStaticType = $this->resolveParamStaticType($node);
if ($paramStaticType !== null) {
Expand Down
105 changes: 59 additions & 46 deletions packages/NodeTypeResolver/src/StaticTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,11 @@ public function mapPhpParserNodePHPStanType(Node $node): Type
if ($node->name === 'string') {
return new StringType();
}

$type = $this->mapScalarStringToType($node->name);
if ($type !== null) {
return $type;
}
}

if ($node instanceof FullyQualified) {
Expand Down Expand Up @@ -513,63 +518,21 @@ public function mapStringToPhpParserNode(string $type): ?Node
}

throw new NotImplementedException(sprintf('%s for "%s"', __METHOD__, $type));

return null;
}

public function mapPHPStanPhpDocTypeNodeToPHPStanType(TypeNode $typeNode, Node $node): Type
{
if ($typeNode instanceof IdentifierTypeNode) {
$loweredName = strtolower($typeNode->name);

if ($loweredName === 'string') {
return new StringType();
}

if (in_array($loweredName, ['float', 'real', 'double'], true)) {
return new FloatType();
$type = $this->mapScalarStringToType($typeNode->name);
if ($type !== null) {
return $type;
}

$loweredName = strtolower($typeNode->name);
if ($loweredName === '\string') {
return new PreSlashStringType();
}

if (in_array($loweredName, ['int', 'integer'], true)) {
return new IntegerType();
}

if (in_array($loweredName, ['false', 'true', 'bool', 'boolean'], true)) {
return new BooleanType();
}

if ($loweredName === 'array') {
return new ArrayType(new MixedType(), new MixedType());
}

if ($loweredName === 'null') {
return new NullType();
}

if ($loweredName === 'void') {
return new VoidType();
}

if ($loweredName === 'object') {
return new ObjectWithoutClassType();
}

if ($loweredName === 'resource') {
return new ResourceType();
}

if (in_array($loweredName, ['callback', 'callable'], true)) {
return new CallableType();
}

if ($loweredName === 'mixed') {
return new MixedType(true);
}

if ($loweredName === 'self') {
/** @var string|null $className */
$className = $node->getAttribute(AttributeKey::CLASS_NAME);
Expand Down Expand Up @@ -732,4 +695,54 @@ private function matchArrayTypes(UnionType $unionType): ?Identifier

return new Identifier($type);
}

private function mapScalarStringToType(string $scalarName): ?Type
{
$loweredScalarName = Strings::lower($scalarName);
if ($loweredScalarName === 'string') {
return new StringType();
}

if (in_array($loweredScalarName, ['float', 'real', 'double'], true)) {
return new FloatType();
}

if (in_array($loweredScalarName, ['int', 'integer'], true)) {
return new IntegerType();
}

if (in_array($loweredScalarName, ['false', 'true', 'bool', 'boolean'], true)) {
return new BooleanType();
}

if ($loweredScalarName === 'array') {
return new ArrayType(new MixedType(), new MixedType());
}

if ($loweredScalarName === 'null') {
return new NullType();
}

if ($loweredScalarName === 'void') {
return new VoidType();
}

if ($loweredScalarName === 'object') {
return new ObjectWithoutClassType();
}

if ($loweredScalarName === 'resource') {
return new ResourceType();
}

if (in_array($loweredScalarName, ['callback', 'callable'], true)) {
return new CallableType();
}

if ($loweredScalarName === 'mixed') {
return new MixedType(true);
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<?php declare(strict_types=1);

namespace Rector\TypeDeclaration\Rector\ClassMethod;

use PhpParser\Node;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use Rector\NodeContainer\ParsedNodesByType;
use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;

/**
* @sponsor Thanks https://spaceflow.io/ for sponsoring this rule - visit them on https://github.com/SpaceFlow-app
*
* @see \Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddMethodCallBasedParamTypeRector\AddMethodCallBasedParamTypeRectorTest
*/
final class AddMethodCallBasedParamTypeRector extends AbstractRector
{
/**
* @var ParsedNodesByType
*/
private $parsedNodesByType;

/**
* @var TypeFactory
*/
private $typeFactory;

public function __construct(ParsedNodesByType $parsedNodesByType, TypeFactory $typeFactory)
{
$this->parsedNodesByType = $parsedNodesByType;
$this->typeFactory = $typeFactory;
}

public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Change param type of passed getId() to UuidInterface type declaration', [
new CodeSample(
<<<'PHP'
class SomeClass
{
public function getById($id)
{
}
}

class CallerClass
{
public function run()
{
$building = new Building();
$someClass = new SomeClass();
$someClass->getById($building->getId());
}
}
PHP
,
<<<'PHP'
class SomeClass
{
public function getById(\Ramsey\Uuid\UuidInterface $id)
{
}
}

class CallerClass
{
public function run()
{
$building = new Building();
$someClass = new SomeClass();
$someClass->getById($building->getId());
}
}
PHP
),
]);
}

/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [ClassMethod::class];
}

/**
* @param ClassMethod $node
*/
public function refactor(Node $node): ?Node
{
$classMethodCalls = $this->parsedNodesByType->findClassMethodCalls($node);

$classParameterTypes = $this->getCallTypesByPosition($classMethodCalls);

foreach ($classParameterTypes as $position => $argumentStaticType) {
if ($this->skipArgumentStaticType($node, $argumentStaticType, $position)) {
continue;
}

$phpParserTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($argumentStaticType);

// update parameter
$node->params[$position]->type = $phpParserTypeNode;
}

return $node;
}

private function skipArgumentStaticType(Node $node, Type $argumentStaticType, int $position): bool
{
if ($argumentStaticType instanceof MixedType) {
return true;
}

if (! isset($node->params[$position])) {
return true;
}

$parameter = $node->params[$position];
if ($parameter->type === null) {
return false;
}

$parameterStaticType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($parameter->type);

// already completed → skip
if ($parameterStaticType->equals($argumentStaticType)) {
return true;
}

return false;
}

/**
* @param MethodCall[]|StaticCall[]|Array_[] $classMethodCalls
* @return Type[]
*/
private function getCallTypesByPosition(array $classMethodCalls): array
{
$staticTypesByArgumentPosition = [];
foreach ($classMethodCalls as $classMethodCall) {
if (! $classMethodCall instanceof StaticCall && ! $classMethodCall instanceof MethodCall) {
continue;
}

foreach ($classMethodCall->args as $position => $arg) {
$staticTypesByArgumentPosition[$position][] = $this->getStaticType($arg->value);
}
}

// unite to single type
$staticTypeByArgumentPosition = [];
foreach ($staticTypesByArgumentPosition as $position => $staticTypes) {
$staticTypeByArgumentPosition[$position] = $this->typeFactory->createMixedPassedOrUnionType($staticTypes);
}

return $staticTypeByArgumentPosition;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php declare(strict_types=1);

namespace Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddMethodCallBasedParamTypeRector;

use Rector\Testing\PHPUnit\AbstractRectorTestCase;
use Rector\TypeDeclaration\Rector\ClassMethod\AddMethodCallBasedParamTypeRector;

final class AddMethodCallBasedParamTypeRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideDataForTest()
*/
public function test(string $file): void
{
$this->doTestFile($file);
}

/**
* @return string[]
*/
public function provideDataForTest(): iterable
{
yield [__DIR__ . '/Fixture/second_position.php.inc'];
yield [__DIR__ . '/Fixture/static_call.php.inc'];
yield [__DIR__ . '/Fixture/assigned_variable.php.inc'];
yield [__DIR__ . '/Fixture/instant_class_call.php.inc'];
yield [__DIR__ . '/Fixture/unioned_double_calls.php.inc'];
yield [__DIR__ . '/Fixture/override.php.inc'];
yield [__DIR__ . '/Fixture/skip_already_completed.php.inc'];
yield [__DIR__ . '/Fixture/skip_mixed.php.inc'];
}

protected function getRectorClass(): string
{
return AddMethodCallBasedParamTypeRector::class;
}
}
Loading