[PHP] Add PHP 7.4 Type Property Rector [ref #638]

This commit is contained in:
Tomas Votruba 2018-09-30 01:46:27 +08:00
parent 9058c60a6e
commit 070cfe310e
23 changed files with 706 additions and 3 deletions

2
bin/rector-prefixed/build-composer-json.php Executable file → Normal file
View File

@ -9,7 +9,7 @@ require_once __DIR__ . '/../bootstrap.php';
$buildDestination = getenv('BUILD_DESTINATION');
// load
$composerJsonPath = $buildDestination . '/composer.json';
$composerJsonPath = $buildDestination . '/composer.json';
$composerContent = Json::decode(FileSystem::read($composerJsonPath), Json::FORCE_ARRAY);
// remove unused sections

View File

@ -41,6 +41,7 @@
"Rector\\NodeTypeResolver\\": "packages/NodeTypeResolver/src",
"Rector\\Symfony\\": "packages/Symfony/src",
"Rector\\CakePHP\\": "packages/CakePHP/src",
"Rector\\Php\\": "packages/Php/src",
"Rector\\Silverstripe\\": "packages/Silverstripe/src",
"Rector\\ParameterGuider\\": "packages/ParameterGuider/src",
"Rector\\Sensio\\": "packages/Sensio/src",
@ -59,6 +60,7 @@
"Rector\\Tests\\": "tests",
"Rector\\NodeTypeResolver\\Tests\\": "packages/NodeTypeResolver/tests",
"Rector\\CakePHP\\Tests\\": "packages/CakePHP/tests",
"Rector\\Php\\Tests\\": "packages/Php/tests",
"Rector\\Symfony\\Tests\\": "packages/Symfony/tests",
"Rector\\Silverstripe\\Tests\\": "packages/Silverstripe/tests",
"Rector\\Sensio\\Tests\\": "packages/Sensio/tests",
@ -72,7 +74,19 @@
"Rector\\FileSystemRector\\Tests\\": "packages/FileSystemRector/tests"
},
"classmap": [
"packages",
"packages/CakePHP/tests",
"packages/Doctrine/tests",
"packages/FileSystemRector/tests",
"packages/NodeTypeResolver/tests",
"packages/ParameterGuider/tests",
"packages/PhpParser/tests",
"packages/PHPUnit/tests",
"packages/Sensio/tests",
"packages/Silverstripe/tests",
"packages/Sylius/tests",
"packages/Symfony/tests",
"packages/Twig/tests",
"packages/Utils/tests",
"tests/Issues",
"tests/Rector"
],

View File

@ -0,0 +1,2 @@
services:
Rector\Php\Rector\TypedPropertyRector: ~

View File

@ -103,3 +103,7 @@ parameters:
- '*CompilerPass.php'
# array type check
- 'src/RectorDefinition/RectorDefinition.php'
Symplify\CodingStandard\Sniffs\ControlStructure\SprintfOverContactSniff:
# respects inherited pattern for better comparing
- 'src/Printer/BetterStandardPrinter.php'

View File

@ -120,6 +120,20 @@ final class DocBlockAnalyzer
return $phpDocInfo->getVarTypes();
}
/**
* @return string[]
*/
public function getNonFqnVarTypes(Node $node): array
{
if ($node->getDocComment() === null) {
return [];
}
$phpDocInfo = $this->createPhpDocInfoFromNode($node);
return $phpDocInfo->getVarTypes();
}
/**
* @todo add test for Multi|Types
*/

View File

@ -0,0 +1,211 @@
<?php declare(strict_types=1);
namespace Rector\Php\Rector;
use PhpParser\Node;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Scalar\DNumber;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Property;
use Rector\NodeTypeResolver\Node\Attribute;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockAnalyzer;
use Rector\Php\TypeAnalyzer;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
/**
* @source https://wiki.php.net/rfc/typed_properties_v2#proposal
*/
final class TypedPropertyRector extends AbstractRector
{
/**
* @var string
*/
public const PHP74_PROPERTY_TYPE = 'php74_property_type';
/**
* @var DocBlockAnalyzer
*/
private $docBlockAnalyzer;
/**
* @var string[][]
*/
private $typeNameToAllowedDefaultNodeType = [
'string' => [String_::class],
'bool' => [ConstFetch::class],
'array' => [Array_::class],
'float' => [DNumber::class, LNumber::class],
'int' => [LNumber::class],
'iterable' => [Array_::class],
];
/**
* @var TypeAnalyzer
*/
private $typeAnalyzer;
/**
* @var bool
*/
private $isNullableType = false;
public function __construct(DocBlockAnalyzer $docBlockAnalyzer, TypeAnalyzer $typeAnalyzer)
{
$this->docBlockAnalyzer = $docBlockAnalyzer;
$this->typeAnalyzer = $typeAnalyzer;
}
public function getDefinition(): RectorDefinition
{
return new RectorDefinition(
'Changes property @var annotations from annotation to type.',
[
new CodeSample(
<<<'CODE_SAMPLE'
final class SomeClass
{
/**
* @var int
*/
private count;
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
final class SomeClass
{
private int count;
}
CODE_SAMPLE
),
]
);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [Property::class];
}
/**
* @param Property $propertyNode
*/
public function refactor(Node $propertyNode): ?Node
{
// non FQN, so they are 1:1 to possible imported doc type
$varTypes = $this->docBlockAnalyzer->getNonFqnVarTypes($propertyNode);
// too many types to handle
if (count($varTypes) > 2) {
return $propertyNode;
}
$this->isNullableType = in_array('null', $varTypes, true);
// exactly 1 type only can be changed || 2 types with nullable; nothing else
if (count($varTypes) !== 1 && (count($varTypes) === 2 && ! $this->isNullableType)) {
return $propertyNode;
}
$propertyType = $this->getPropertyTypeWithoutNull($varTypes);
$propertyType = $this->shortenLongType($propertyType);
if (! $this->typeAnalyzer->isPropertyTypeHintableType($propertyType)) {
return $propertyNode;
}
if (! $this->matchesDocTypeAndDefaultValueType($propertyType, $propertyNode)) {
return $propertyNode;
}
if ($this->isNullableType) {
$propertyType = '?' . $propertyType;
}
$this->docBlockAnalyzer->removeTagFromNode($propertyNode, 'var');
$propertyNode->setAttribute(self::PHP74_PROPERTY_TYPE, $propertyType);
// invoke the print, because only attribute has changed
$propertyNode->setAttribute(Attribute::ORIGINAL_NODE, null);
return $propertyNode;
}
private function matchesDocTypeAndDefaultValueType(string $propertyType, Property $propertyNode): bool
{
$defaultValueNode = $propertyNode->props[0]->default;
if ($defaultValueNode === null) {
return true;
}
if (! isset($this->typeNameToAllowedDefaultNodeType[$propertyType])) {
return true;
}
if ($this->isNullableType) {
// is default value "null"?
return $this->isContantWithValue($defaultValueNode, 'null');
}
return $this->matchesDefaultValueToExpectedNodeTypes($propertyType, $defaultValueNode);
}
/**
* @param string[]|string $value
*/
private function isContantWithValue(Node $node, $value): bool
{
return $node instanceof ConstFetch && in_array((string) $node->name, (array) $value, true);
}
/**
* @param string[] $varTypes
*/
private function getPropertyTypeWithoutNull(array $varTypes): string
{
if ($this->isNullableType) {
$nullTypePosition = array_search('null', $varTypes, true);
unset($varTypes[$nullTypePosition]);
}
return (string) array_pop($varTypes);
}
private function shortenLongType(string $type): string
{
if ($type === 'boolean') {
return 'bool';
}
if ($type === 'integer') {
return 'int';
}
return $type;
}
private function matchesDefaultValueToExpectedNodeTypes(string $propertyType, Node $defaultValueNode): bool
{
$allowedDefaultNodeTypes = $this->typeNameToAllowedDefaultNodeType[$propertyType];
foreach ($allowedDefaultNodeTypes as $allowedDefaultNodeType) {
if (is_a($defaultValueNode, $allowedDefaultNodeType, true)) {
if ($propertyType === 'bool') {
// make sure it's the right constant value
return $this->isContantWithValue($defaultValueNode, ['true', 'false']);
}
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,33 @@
<?php declare(strict_types=1);
namespace Rector\Php\Tests\Rector\TypedPropertyRector\Wrong;
final class ClassWithProperty
{
private int $count;
/**
* @var int|null|bool
*/
private $multiCount;
/**
* another comment
*/
private bool $isTrue = false;
/**
* @var void
*/
private $shouldBeSkipped;
/**
* @var callable
*/
private $shouldBeSkippedToo;
/**
* @var invalid
*/
private $cantTouchThis;
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace Rector\Php\Tests\Rector\TypedPropertyRector\Wrong;
use Rector\Php\Tests\Rector\TypedPropertyRector\Source\AnotherClass;
final class ClassWithClassProperty
{
private AnotherClass $anotherClass;
}

View File

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
namespace Rector\Php\Tests\Rector\TypedPropertyRector\Wrong;
use Rector\Php\Tests\Rector\TypedPropertyRector\Source\AnotherClass;
final class ClassWithNullableProperty
{
private ?AnotherClass $anotherClass = null;
private ?AnotherClass $yetAnotherClass;
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace Rector\Php\Tests\Rector\TypedPropertyRector\Wrong;
use Rector\Php\Tests\Rector\TypedPropertyRector\Source\AnotherClass;
final class ClassWithStaticProperty
{
private static iterable $iterable;
}

View File

@ -0,0 +1,55 @@
<?php declare(strict_types=1);
namespace Rector\Php\Tests\Rector\TypedPropertyRector\Wrong;
final class DefaultValues
{
/**
* @var bool
*/
private $name = 'not_a_bool';
private bool $isItRealName = false;
/**
* @var bool
*/
private $isItRealNameNull = null;
/**
* @var string
*/
private $size = false;
/**
* @var array
*/
private $items = null;
/**
* @var iterable
*/
private $itemsB = null;
private ?array $nullableItems = null;
private float $a = 42.42;
private float $b = 42;
/**
* @var float
*/
private $c = 'hey';
/**
* @var int
*/
private $e = 42.42;
private int $f = 42;
private array $g = [1, 2, 3];
private iterable $h = [1, 2, 3];
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace Rector\Php\Tests\Rector\TypedPropertyRector\Wrong;
final class MatchTypes
{
private bool $a;
private bool $b;
private int $c;
private int $d;
private float $e;
private string $f;
private object $g;
private iterable $h;
private self $i;
private parent $j;
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace Rector\Php\Tests\Rector\TypedPropertyRector\Source;
final class AnotherClass
{
}

View File

@ -0,0 +1,36 @@
<?php declare(strict_types=1);
namespace Rector\Php\Tests\Rector\TypedPropertyRector;
use Iterator;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
/**
* @covers \Rector\Php\Rector\TypedPropertyRector
*/
final class TypedPropertyRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideWrongToFixedFiles()
*/
public function test(string $wrong, string $fixed): void
{
$this->doTestFileMatchesExpectedContent($wrong, $fixed);
}
public function provideWrongToFixedFiles(): Iterator
{
yield [__DIR__ . '/Wrong/ClassWithProperty.php', __DIR__ . '/Correct/correct.php.inc'];
yield [__DIR__ . '/Wrong/ClassWithClassProperty.php', __DIR__ . '/Correct/correct2.php.inc'];
yield [__DIR__ . '/Wrong/ClassWithNullableProperty.php', __DIR__ . '/Correct/correct3.php.inc'];
yield [__DIR__ . '/Wrong/ClassWithStaticProperty.php', __DIR__ . '/Correct/correct4.php.inc'];
yield [__DIR__ . '/Wrong/DefaultValues.php', __DIR__ . '/Correct/correct5.php.inc'];
// based on: https://wiki.php.net/rfc/typed_properties_v2#supported_types
yield [__DIR__ . '/Wrong/MatchTypes.php', __DIR__ . '/Correct/correct6.php.inc'];
}
protected function provideConfig(): string
{
return __DIR__ . '/config.yml';
}
}

View File

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace Rector\Php\Tests\Rector\TypedPropertyRector\Wrong;
use Rector\Php\Tests\Rector\TypedPropertyRector\Source\AnotherClass;
final class ClassWithClassProperty
{
/**
* @var AnotherClass
*/
private $anotherClass;
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace Rector\Php\Tests\Rector\TypedPropertyRector\Wrong;
use Rector\Php\Tests\Rector\TypedPropertyRector\Source\AnotherClass;
final class ClassWithNullableProperty
{
/**
* @var AnotherClass|null
*/
private $anotherClass = null;
/**
* @var null|AnotherClass
*/
private $yetAnotherClass;
}

View File

@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace Rector\Php\Tests\Rector\TypedPropertyRector\Wrong;
final class ClassWithProperty
{
/**
* @var int
*/
private $count;
/**
* @var int|null|bool
*/
private $multiCount;
/**
* @var bool
* another comment
*/
private $isTrue = false;
/**
* @var void
*/
private $shouldBeSkipped;
/**
* @var callable
*/
private $shouldBeSkippedToo;
/**
* @var invalid
*/
private $cantTouchThis;
}

View File

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace Rector\Php\Tests\Rector\TypedPropertyRector\Wrong;
use Rector\Php\Tests\Rector\TypedPropertyRector\Source\AnotherClass;
final class ClassWithStaticProperty
{
/**
* @var iterable
*/
private static $iterable;
}

View File

@ -0,0 +1,76 @@
<?php declare(strict_types=1);
namespace Rector\Php\Tests\Rector\TypedPropertyRector\Wrong;
final class DefaultValues
{
/**
* @var bool
*/
private $name = 'not_a_bool';
/**
* @var bool
*/
private $isItRealName = false;
/**
* @var bool
*/
private $isItRealNameNull = null;
/**
* @var string
*/
private $size = false;
/**
* @var array
*/
private $items = null;
/**
* @var iterable
*/
private $itemsB = null;
/**
* @var array|null
*/
private $nullableItems = null;
/**
* @var float
*/
private $a = 42.42;
/**
* @var float
*/
private $b = 42;
/**
* @var float
*/
private $c = 'hey';
/**
* @var int
*/
private $e = 42.42;
/**
* @var int
*/
private $f = 42;
/**
* @var array
*/
private $g = [1, 2, 3];
/**
* @var iterable
*/
private $h = [1, 2, 3];
}

View File

@ -0,0 +1,56 @@
<?php declare(strict_types=1);
namespace Rector\Php\Tests\Rector\TypedPropertyRector\Wrong;
final class MatchTypes
{
/**
* @var bool
*/
private $a;
/**
* @var boolean
*/
private $b;
/**
* @var int
*/
private $c;
/**
* @var integer
*/
private $d;
/**
* @var float
*/
private $e;
/**
* @var string
*/
private $f;
/**
* @var object
*/
private $g;
/**
* @var iterable
*/
private $h;
/**
* @var self
*/
private $i;
/**
* @var parent
*/
private $j;
}

View File

@ -0,0 +1,2 @@
services:
Rector\Php\Rector\TypedPropertyRector: ~

View File

@ -6,11 +6,49 @@ use Nette\Utils\Strings;
final class TypeAnalyzer
{
public function isPropertyTypeHintableType(string $type): bool
{
if (empty($type)) {
return false;
}
// first letter is upper, probably class type
if (ctype_upper($type[0])) {
return true;
}
if (! $this->isPhpReservedType($type)) {
return false;
}
// callable and iterable are not property typehintable
// @see https://wiki.php.net/rfc/typed_properties_v2#supported_types
if (in_array($type, ['callable', 'void'], true)) {
return false;
}
return true;
}
public function isPhpReservedType(string $type): bool
{
return in_array(
$type,
['string', 'bool', 'null', 'false', 'true', 'mixed', 'object', 'iterable', 'array', 'float', 'int'],
[
'string',
'bool',
'null',
'false',
'true',
'mixed',
'object',
'iterable',
'array',
'float',
'int',
'self',
'parent',
],
true
);
}

View File

@ -4,8 +4,10 @@ namespace Rector\Printer;
use PhpParser\Node\Expr\Yield_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Property;
use PhpParser\PrettyPrinter\Standard;
use Rector\NodeTypeResolver\Node\Attribute;
use Rector\Php\Rector\TypedPropertyRector;
use function Safe\sprintf;
final class BetterStandardPrinter extends Standard
@ -45,4 +47,17 @@ final class BetterStandardPrinter extends Standard
$shouldAddBrackets ? ')' : ''
);
}
/**
* Print property with PHP 7.4 type
*/
protected function pStmt_Property(Property $node): string
{
$type = $node->getAttribute(TypedPropertyRector::PHP74_PROPERTY_TYPE);
return $node->flags === 0 ? 'var ' : $this->pModifiers($node->flags) .
($type ? $type . ' ' : '') .
$this->pCommaSeparated($node->props) .
';';
}
}