Skip to content

Commit

Permalink
[TypeDeclaration] Add ReturnTypeFromStrictTernaryRector (#3318)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasVotruba committed Jan 29, 2023
1 parent 3450bed commit bbc100c
Show file tree
Hide file tree
Showing 9 changed files with 349 additions and 2 deletions.
23 changes: 21 additions & 2 deletions build/target-repository/docs/rector_rules_overview.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 421 Rules Overview
# 422 Rules Overview

<br>

Expand Down Expand Up @@ -64,7 +64,7 @@

- [Transform](#transform) (34)

- [TypeDeclaration](#typedeclaration) (40)
- [TypeDeclaration](#typedeclaration) (41)

- [Visibility](#visibility) (3)

Expand Down Expand Up @@ -9689,6 +9689,25 @@ Add strict return array type based on created empty array and returned

<br>

### ReturnTypeFromStrictTernaryRector

Add method return type based on strict ternary values

- class: [`Rector\TypeDeclaration\Rector\Class_\ReturnTypeFromStrictTernaryRector`](../rules/TypeDeclaration/Rector/Class_/ReturnTypeFromStrictTernaryRector.php)

```diff
final class SomeClass
{
- public function getValue($number)
+ public function getValue($number): int
{
return $number ? 100 : 500;
}
}
```

<br>

### ReturnTypeFromStrictTypedCallRector

Add return type from strict return type of call
Expand Down
2 changes: 2 additions & 0 deletions config/set/type-declaration.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Rector\Config\RectorConfig;
use Rector\TypeDeclaration\Rector\ArrowFunction\AddArrowFunctionReturnTypeRector;
use Rector\TypeDeclaration\Rector\Class_\PropertyTypeFromStrictSetterGetterRector;
use Rector\TypeDeclaration\Rector\Class_\ReturnTypeFromStrictTernaryRector;
use Rector\TypeDeclaration\Rector\ClassMethod\AddMethodCallBasedStrictParamTypeRector;
use Rector\TypeDeclaration\Rector\ClassMethod\AddParamTypeBasedOnPHPUnitDataProviderRector;
use Rector\TypeDeclaration\Rector\ClassMethod\AddParamTypeFromPropertyTypeRector;
Expand Down Expand Up @@ -70,5 +71,6 @@
ReturnNeverTypeRector::class,
EmptyOnNullableObjectToInstanceOfRector::class,
PropertyTypeFromStrictSetterGetterRector::class,
ReturnTypeFromStrictTernaryRector::class,
]);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Rector\Tests\TypeDeclaration\Rector\Class_\ReturnTypeFromStrictTernaryRector\Fixture;

final class LocalConstants
{
const FIRST = 'hey';
const SECOND = 'hou';

public function getValue($number)
{
return $number ? self::FIRST : self::SECOND;
}
}

?>
-----
<?php

namespace Rector\Tests\TypeDeclaration\Rector\Class_\ReturnTypeFromStrictTernaryRector\Fixture;

final class LocalConstants
{
const FIRST = 'hey';
const SECOND = 'hou';

public function getValue($number): string
{
return $number ? self::FIRST : self::SECOND;
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Rector\Tests\TypeDeclaration\Rector\Class_\ReturnTypeFromStrictTernaryRector\Fixture;

final class SkipDynamicScalar
{
public function getValue($number)
{
return $number ? 100 : (500 + $number);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Rector\Tests\TypeDeclaration\Rector\Class_\ReturnTypeFromStrictTernaryRector\Fixture;

final class SomeClass
{
public function getValue($number)
{
return $number ? 100 : 500;
}
}

?>
-----
<?php

namespace Rector\Tests\TypeDeclaration\Rector\Class_\ReturnTypeFromStrictTernaryRector\Fixture;

final class SomeClass
{
public function getValue($number): int
{
return $number ? 100 : 500;
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\TypeDeclaration\Rector\Class_\ReturnTypeFromStrictTernaryRector;

use Iterator;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;

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

/**
* @return Iterator<string>
*/
public function provideData(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}

public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;

use Rector\TypeDeclaration\Rector\Class_\ReturnTypeFromStrictTernaryRector;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(ReturnTypeFromStrictTernaryRector::class);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<?php

declare(strict_types=1);

namespace Rector\TypeDeclaration\Rector\Class_;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\Ternary;
use PhpParser\Node\Scalar;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Return_;
use PHPStan\Type\ConstantType;
use PHPStan\Type\GeneralizePrecision;
use PHPStan\Type\Type;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\PHPStanStaticTypeMapper\Enum\TypeKind;
use Rector\TypeDeclaration\ValueObject\TernaryIfElseTypes;
use Rector\VendorLocker\ParentClassMethodTypeOverrideGuard;
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @see \Rector\Tests\TypeDeclaration\Rector\Class_\ReturnTypeFromStrictTernaryRector\ReturnTypeFromStrictTernaryRectorTest
*/
final class ReturnTypeFromStrictTernaryRector extends AbstractRector implements MinPhpVersionInterface
{
public function __construct(
private readonly ParentClassMethodTypeOverrideGuard $parentClassMethodTypeOverrideGuard
) {
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Add method return type based on strict ternary values', [
new CodeSample(
<<<'CODE_SAMPLE'
final class SomeClass
{
public function getValue($number)
{
return $number ? 100 : 500;
}
}
CODE_SAMPLE

,
<<<'CODE_SAMPLE'
final class SomeClass
{
public function getValue($number): int
{
return $number ? 100 : 500;
}
}
CODE_SAMPLE
),
]);
}

/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [Class_::class];
}

/**
* @param Class_ $node
*/
public function refactor(Node $node): ?Node
{
$hasChanged = false;

foreach ($node->getMethods() as $classMethod) {
if ($classMethod->returnType instanceof Node) {
continue;
}

$onlyStmt = $classMethod->stmts[0] ?? null;
if (! $onlyStmt instanceof Return_) {
continue;
}

if (! $onlyStmt->expr instanceof Ternary) {
continue;
}

$ternary = $onlyStmt->expr;

// has scalar in if/else of ternary
$ternaryIfElseTypes = $this->matchScalarTernaryIfElseTypes($ternary);
if (! $ternaryIfElseTypes instanceof TernaryIfElseTypes) {
continue;
}

$ifType = $ternaryIfElseTypes->getFirstType();
$elseType = $ternaryIfElseTypes->getSecondType();

if (! $this->areTypesEqual($ifType, $elseType)) {
continue;
}

$returnTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($ifType, TypeKind::RETURN);

if ($this->parentClassMethodTypeOverrideGuard->shouldSkipReturnTypeChange($classMethod, $ifType)) {
continue;
}

if (! $returnTypeNode instanceof Node) {
continue;
}

$classMethod->returnType = $returnTypeNode;
$hasChanged = true;
}

if ($hasChanged) {
return $node;
}

return null;
}

public function provideMinPhpVersion(): int
{
return PhpVersionFeature::SCALAR_TYPES;
}

private function isAlwaysScalarExpr(?Expr $expr): bool
{
// check if Scalar node
if ($expr instanceof Scalar) {
return true;
}

// check if constant
if ($expr instanceof ConstFetch) {
return true;
}

// check if class constant
return $expr instanceof ClassConstFetch;
}

private function areTypesEqual(Type $firstType, Type $secondType): bool
{
// this is needed to make comparison tolerant to constant values, e.g. 5 and 10 are same only then
if ($firstType instanceof ConstantType) {
$firstType = $firstType->generalize(GeneralizePrecision::lessSpecific());
}

if ($secondType instanceof ConstantType) {
$secondType = $secondType->generalize(GeneralizePrecision::lessSpecific());
}

return $firstType->equals($secondType);
}

private function matchScalarTernaryIfElseTypes(Ternary $ternary): ?TernaryIfElseTypes
{
if (! $this->isAlwaysScalarExpr($ternary->if)) {
return null;
}

if (! $this->isAlwaysScalarExpr($ternary->else)) {
return null;
}

/** @var Node\Expr $if */
$if = $ternary->if;

/** @var Node\Expr $else */
$else = $ternary->else;

$ifType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($if);
$elseType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($else);

return new TernaryIfElseTypes($ifType, $elseType);
}
}
26 changes: 26 additions & 0 deletions rules/TypeDeclaration/ValueObject/TernaryIfElseTypes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Rector\TypeDeclaration\ValueObject;

use PHPStan\Type\Type;

final class TernaryIfElseTypes
{
public function __construct(
private readonly Type $firstType,
private readonly Type $secondType,
) {
}

public function getFirstType(): Type
{
return $this->firstType;
}

public function getSecondType(): Type
{
return $this->secondType;
}
}

0 comments on commit bbc100c

Please sign in to comment.