mirror of
https://github.com/rectorphp/rector.git
synced 2025-01-17 21:38:22 +01:00
[PHP 7.4] Add null on conditional type of property type (#4394)
Co-authored-by: rector-bot <tomas@getrector.org>
This commit is contained in:
parent
50f75c9325
commit
a31837679d
@ -48,6 +48,7 @@ expectedArguments(
|
||||
\Rector\NodeTypeResolver\Node\AttributeKey::CLOSURE_NODE,
|
||||
\Rector\NodeTypeResolver\Node\AttributeKey::PARAMETER_POSITION,
|
||||
\Rector\NodeTypeResolver\Node\AttributeKey::ARGUMENT_POSITION,
|
||||
\Rector\NodeTypeResolver\Node\AttributeKey::IS_FIRST_LEVEL_STATEMENT,
|
||||
);
|
||||
|
||||
expectedArguments(
|
||||
@ -81,6 +82,7 @@ expectedArguments(
|
||||
\Rector\NodeTypeResolver\Node\AttributeKey::CLOSURE_NODE,
|
||||
\Rector\NodeTypeResolver\Node\AttributeKey::PARAMETER_POSITION,
|
||||
\Rector\NodeTypeResolver\Node\AttributeKey::ARGUMENT_POSITION,
|
||||
\Rector\NodeTypeResolver\Node\AttributeKey::IS_FIRST_LEVEL_STATEMENT,
|
||||
);
|
||||
|
||||
expectedArguments(
|
||||
|
@ -8,7 +8,8 @@ use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\MethodCall;
|
||||
use PhpParser\Node\Expr\PropertyFetch;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use PHPStan\Type\ObjectType;
|
||||
use PhpParser\Node\Expr\StaticPropertyFetch;
|
||||
use PHPStan\Type\Type;
|
||||
use PHPStan\Type\TypeWithClassName;
|
||||
use PHPStan\Type\UnionType;
|
||||
use Rector\NodeNameResolver\NodeNameResolver;
|
||||
@ -50,11 +51,11 @@ final class ParsedPropertyFetchNodeCollector
|
||||
|
||||
public function collect(Node $node): void
|
||||
{
|
||||
if (! $node instanceof PropertyFetch) {
|
||||
if (! $node instanceof PropertyFetch && ! $node instanceof StaticPropertyFetch) {
|
||||
return;
|
||||
}
|
||||
|
||||
$propertyType = $this->nodeTypeResolver->getStaticType($node->var);
|
||||
$propertyType = $this->resolvePropertyCallerType($node);
|
||||
|
||||
// make sure name is valid
|
||||
if ($node->name instanceof StaticCall || $node->name instanceof MethodCall) {
|
||||
@ -62,20 +63,11 @@ final class ParsedPropertyFetchNodeCollector
|
||||
}
|
||||
|
||||
$propertyName = $this->nodeNameResolver->getName($node->name);
|
||||
|
||||
if ($propertyType instanceof TypeWithClassName) {
|
||||
$this->propertyFetchesByTypeAndName[$propertyType->getClassName()][$propertyName][] = $node;
|
||||
if ($propertyName === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($propertyType instanceof UnionType) {
|
||||
foreach ($propertyType->getTypes() as $unionedType) {
|
||||
if (! $unionedType instanceof ObjectType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->propertyFetchesByTypeAndName[$unionedType->getClassName()][$propertyName][] = $node;
|
||||
}
|
||||
}
|
||||
$this->addPropertyFetchWithTypeAndName($propertyType, $node, $propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -85,4 +77,35 @@ final class ParsedPropertyFetchNodeCollector
|
||||
{
|
||||
return $this->propertyFetchesByTypeAndName[$className][$propertyName] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PropertyFetch|StaticPropertyFetch $node
|
||||
*/
|
||||
private function resolvePropertyCallerType(Node $node): Type
|
||||
{
|
||||
if ($node instanceof PropertyFetch) {
|
||||
return $this->nodeTypeResolver->getStaticType($node->var);
|
||||
}
|
||||
|
||||
return $this->nodeTypeResolver->getStaticType($node->class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PropertyFetch|StaticPropertyFetch $propertyFetchNode
|
||||
*/
|
||||
private function addPropertyFetchWithTypeAndName(
|
||||
Type $propertyType,
|
||||
Node $propertyFetchNode,
|
||||
string $propertyName
|
||||
): void {
|
||||
if ($propertyType instanceof TypeWithClassName) {
|
||||
$this->propertyFetchesByTypeAndName[$propertyType->getClassName()][$propertyName][] = $propertyFetchNode;
|
||||
}
|
||||
|
||||
if ($propertyType instanceof UnionType) {
|
||||
foreach ($propertyType->getTypes() as $unionedType) {
|
||||
$this->addPropertyFetchWithTypeAndName($unionedType, $propertyFetchNode, $propertyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Rector\NodeNestingScope;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\FunctionLike;
|
||||
use Rector\Core\PhpParser\Node\BetterNodeFinder;
|
||||
use Rector\NodeNestingScope\ValueObject\ControlStructure;
|
||||
|
||||
@ -28,6 +29,20 @@ final class ScopeNestingComparator
|
||||
return $firstNodeScopeNode === $secondNodeScopeNode;
|
||||
}
|
||||
|
||||
public function isNodeConditionallyScoped(Node $node): bool
|
||||
{
|
||||
$foundParentType = $this->betterNodeFinder->findFirstParentInstanceOf(
|
||||
$node,
|
||||
ControlStructure::CONDITIONAL_NODE_SCOPE_TYPES + [FunctionLike::class]
|
||||
);
|
||||
|
||||
if ($foundParentType === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ! $foundParentType instanceof FunctionLike;
|
||||
}
|
||||
|
||||
private function findParentControlStructure(Node $node): ?Node
|
||||
{
|
||||
return $this->betterNodeFinder->findFirstParentInstanceOf($node, ControlStructure::BREAKING_SCOPE_NODE_TYPES);
|
||||
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Rector\NodeNestingScope\ValueObject;
|
||||
|
||||
use PhpParser\Node\Expr\Match_;
|
||||
use PhpParser\Node\FunctionLike;
|
||||
use PhpParser\Node\Stmt\Case_;
|
||||
use PhpParser\Node\Stmt\Catch_;
|
||||
@ -13,6 +14,7 @@ use PhpParser\Node\Stmt\ElseIf_;
|
||||
use PhpParser\Node\Stmt\For_;
|
||||
use PhpParser\Node\Stmt\Foreach_;
|
||||
use PhpParser\Node\Stmt\If_;
|
||||
use PhpParser\Node\Stmt\Switch_;
|
||||
use PhpParser\Node\Stmt\While_;
|
||||
|
||||
final class ControlStructure
|
||||
@ -32,4 +34,20 @@ final class ControlStructure
|
||||
Case_::class,
|
||||
FunctionLike::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* These situations happens only if condition is met
|
||||
* @var class-string[]
|
||||
*/
|
||||
public const CONDITIONAL_NODE_SCOPE_TYPES = [
|
||||
If_::class,
|
||||
While_::class,
|
||||
Do_::class,
|
||||
Else_::class,
|
||||
ElseIf_::class,
|
||||
Catch_::class,
|
||||
Case_::class,
|
||||
Match_::class,
|
||||
Switch_::class,
|
||||
];
|
||||
}
|
||||
|
@ -204,4 +204,9 @@ final class AttributeKey
|
||||
* @var string
|
||||
*/
|
||||
public const ARGUMENT_POSITION = 'argument_position';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const IS_FIRST_LEVEL_STATEMENT = 'is_first_level_statement';
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ use PhpParser\NodeVisitor\NodeConnectingVisitor;
|
||||
use Rector\Core\Configuration\Configuration;
|
||||
use Rector\NodeCollector\NodeVisitor\NodeCollectorNodeVisitor;
|
||||
use Rector\NodeTypeResolver\NodeVisitor\FileInfoNodeVisitor;
|
||||
use Rector\NodeTypeResolver\NodeVisitor\FirstLevelNodeVisitor;
|
||||
use Rector\NodeTypeResolver\NodeVisitor\FunctionLikeParamArgPositionNodeVisitor;
|
||||
use Rector\NodeTypeResolver\NodeVisitor\FunctionMethodAndClassNodeVisitor;
|
||||
use Rector\NodeTypeResolver\NodeVisitor\NamespaceNodeVisitor;
|
||||
@ -78,6 +79,11 @@ final class NodeScopeAndMetadataDecorator
|
||||
*/
|
||||
private $functionLikeParamArgPositionNodeVisitor;
|
||||
|
||||
/**
|
||||
* @var FirstLevelNodeVisitor
|
||||
*/
|
||||
private $firstLevelNodeVisitor;
|
||||
|
||||
public function __construct(
|
||||
CloningVisitor $cloningVisitor,
|
||||
Configuration $configuration,
|
||||
@ -89,7 +95,8 @@ final class NodeScopeAndMetadataDecorator
|
||||
PhpDocInfoNodeVisitor $phpDocInfoNodeVisitor,
|
||||
StatementNodeVisitor $statementNodeVisitor,
|
||||
NodeConnectingVisitor $nodeConnectingVisitor,
|
||||
FunctionLikeParamArgPositionNodeVisitor $functionLikeParamArgPositionNodeVisitor
|
||||
FunctionLikeParamArgPositionNodeVisitor $functionLikeParamArgPositionNodeVisitor,
|
||||
FirstLevelNodeVisitor $firstLevelNodeVisitor
|
||||
) {
|
||||
$this->phpStanNodeScopeResolver = $phpStanNodeScopeResolver;
|
||||
$this->cloningVisitor = $cloningVisitor;
|
||||
@ -102,6 +109,7 @@ final class NodeScopeAndMetadataDecorator
|
||||
$this->phpDocInfoNodeVisitor = $phpDocInfoNodeVisitor;
|
||||
$this->nodeConnectingVisitor = $nodeConnectingVisitor;
|
||||
$this->functionLikeParamArgPositionNodeVisitor = $functionLikeParamArgPositionNodeVisitor;
|
||||
$this->firstLevelNodeVisitor = $firstLevelNodeVisitor;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -141,6 +149,7 @@ final class NodeScopeAndMetadataDecorator
|
||||
$nodeTraverser->addVisitor($this->functionMethodAndClassNodeVisitor);
|
||||
$nodeTraverser->addVisitor($this->namespaceNodeVisitor);
|
||||
$nodeTraverser->addVisitor($this->phpDocInfoNodeVisitor);
|
||||
$nodeTraverser->addVisitor($this->firstLevelNodeVisitor);
|
||||
$nodeTraverser->addVisitor($this->functionLikeParamArgPositionNodeVisitor);
|
||||
|
||||
$nodes = $nodeTraverser->traverse($nodes);
|
||||
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\NodeTypeResolver\NodeVisitor;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\FunctionLike;
|
||||
use PhpParser\Node\Stmt\Expression;
|
||||
use PhpParser\NodeVisitorAbstract;
|
||||
use Rector\NodeTypeResolver\Node\AttributeKey;
|
||||
|
||||
final class FirstLevelNodeVisitor extends NodeVisitorAbstract
|
||||
{
|
||||
/**
|
||||
* @return Node
|
||||
*/
|
||||
public function enterNode(Node $node): ?Node
|
||||
{
|
||||
if ($node instanceof FunctionLike) {
|
||||
foreach ((array) $node->getStmts() as $stmt) {
|
||||
$stmt->setAttribute(AttributeKey::IS_FIRST_LEVEL_STATEMENT, true);
|
||||
|
||||
if ($stmt instanceof Expression) {
|
||||
$stmt->expr->setAttribute(AttributeKey::IS_FIRST_LEVEL_STATEMENT, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -18,6 +18,8 @@ use PHPStan\Type\Type;
|
||||
use PHPStan\Type\UnionType;
|
||||
use PHPStan\Type\VerbosityLevel;
|
||||
use Rector\Core\Exception\ShouldNotHappenException;
|
||||
use Rector\PHPStan\Type\FullyQualifiedObjectType;
|
||||
use Rector\PHPStan\Type\ShortenedObjectType;
|
||||
use Rector\PHPStan\TypeFactoryStaticHelper;
|
||||
|
||||
final class TypeFactory
|
||||
@ -74,6 +76,10 @@ final class TypeFactory
|
||||
$type = $this->removeValueFromConstantType($type);
|
||||
}
|
||||
|
||||
if ($type instanceof ShortenedObjectType) {
|
||||
$type = new FullyQualifiedObjectType($type->getFullyQualifiedName());
|
||||
}
|
||||
|
||||
$typeHash = md5($type->describe(VerbosityLevel::cache()));
|
||||
$uniqueTypes[$typeHash] = $type;
|
||||
}
|
||||
|
@ -26,6 +26,11 @@ final class PHPStanStaticTypeMapper
|
||||
*/
|
||||
public const KIND_PROPERTY = 'property';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const KIND_RETURN = 'return';
|
||||
|
||||
/**
|
||||
* @var TypeMapperInterface[]
|
||||
*/
|
||||
|
@ -5,12 +5,14 @@ declare(strict_types=1);
|
||||
namespace Rector\PHPStanStaticTypeMapper\TypeMapper;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Name;
|
||||
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
|
||||
use PHPStan\Type\NullType;
|
||||
use PHPStan\Type\Type;
|
||||
use PHPStan\Type\VerbosityLevel;
|
||||
use Rector\AttributeAwarePhpDoc\Ast\Type\AttributeAwareIdentifierTypeNode;
|
||||
use Rector\PHPStanStaticTypeMapper\Contract\TypeMapperInterface;
|
||||
use Rector\PHPStanStaticTypeMapper\PHPStanStaticTypeMapper;
|
||||
|
||||
final class NullTypeMapper implements TypeMapperInterface
|
||||
{
|
||||
@ -32,7 +34,11 @@ final class NullTypeMapper implements TypeMapperInterface
|
||||
*/
|
||||
public function mapToPhpParserNode(Type $type, ?string $kind = null): ?Node
|
||||
{
|
||||
return null;
|
||||
if ($kind !== PHPStanStaticTypeMapper::KIND_PROPERTY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Name('null');
|
||||
}
|
||||
|
||||
public function mapToDocString(Type $type, ?Type $parentType = null): string
|
||||
|
@ -204,11 +204,16 @@ final class UnionTypeMapper implements TypeMapperInterface
|
||||
{
|
||||
$phpParserUnionType = $this->matchPhpParserUnionType($unionType);
|
||||
if ($phpParserUnionType !== null) {
|
||||
if (! $this->phpVersionProvider->isAtLeast(PhpVersionFeature::UNION_TYPES)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $phpParserUnionType;
|
||||
}
|
||||
|
||||
// the type should be compatible with all other types, e.g. A extends B, B
|
||||
$compatibleObjectCandidate = $this->resolveCompatibleObjectCandidate($unionType);
|
||||
|
||||
if ($compatibleObjectCandidate === null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
colors="true"
|
||||
verbose="true"
|
||||
>
|
||||
<php>
|
||||
<const name="RECTOR_REPOSITORY" value="true"/>
|
||||
|
@ -254,6 +254,12 @@ CODE_SAMPLE
|
||||
}
|
||||
|
||||
$onlyProperty = $property->props[0];
|
||||
|
||||
// skip is already has value
|
||||
if ($onlyProperty->default !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$onlyProperty->default = $this->createNull();
|
||||
}
|
||||
|
||||
|
@ -4,49 +4,17 @@ namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
|
||||
|
||||
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 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;
|
||||
|
||||
/**
|
||||
@ -68,14 +36,6 @@ namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
|
||||
|
||||
final class DefaultValues
|
||||
{
|
||||
private string $name = 'not_a_bool';
|
||||
|
||||
private bool $isItRealName = false;
|
||||
|
||||
private ?bool $isItRealNameNull = null;
|
||||
|
||||
private bool $size = false;
|
||||
|
||||
private float $a = 42.42;
|
||||
|
||||
private int $b = 42;
|
||||
@ -88,7 +48,7 @@ final class DefaultValues
|
||||
|
||||
private array $g = [1, 2, 3];
|
||||
|
||||
private array $h = [1, 2, 3];
|
||||
private iterable $h = [1, 2, 3];
|
||||
}
|
||||
|
||||
?>
|
||||
|
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
|
||||
|
||||
final class DefaultValuesBool
|
||||
{
|
||||
private $size = false;
|
||||
}
|
||||
|
||||
?>
|
||||
-----
|
||||
<?php
|
||||
|
||||
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
|
||||
|
||||
final class DefaultValuesBool
|
||||
{
|
||||
private bool $size = false;
|
||||
}
|
||||
|
||||
?>
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
|
||||
|
||||
final class DefaultValuesString
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $name = 'not_a_bool';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
-----
|
||||
<?php
|
||||
|
||||
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
|
||||
|
||||
final class DefaultValuesString
|
||||
{
|
||||
private string $name = 'not_a_bool';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
|
||||
|
||||
final class DefaultValuesWithNullable
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isItRealNameNull = null;
|
||||
}
|
||||
|
||||
?>
|
||||
-----
|
||||
<?php
|
||||
|
||||
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
|
||||
|
||||
final class DefaultValuesWithNullable
|
||||
{
|
||||
private ?bool $isItRealNameNull = null;
|
||||
}
|
||||
|
||||
?>
|
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
|
||||
|
||||
use Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Source\ReturnString;
|
||||
|
||||
class IfConditionalUnionNullable
|
||||
{
|
||||
/** @var string|null */
|
||||
private $nullOrString;
|
||||
|
||||
public function __construct(?ReturnString $returnString = null)
|
||||
{
|
||||
if ($returnString !== null) {
|
||||
$this->nullOrString = $returnString->getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
-----
|
||||
<?php
|
||||
|
||||
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
|
||||
|
||||
use Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Source\ReturnString;
|
||||
|
||||
class IfConditionalUnionNullable
|
||||
{
|
||||
private ?string $nullOrString = null;
|
||||
|
||||
public function __construct(?ReturnString $returnString = null)
|
||||
{
|
||||
if ($returnString !== null) {
|
||||
$this->nullOrString = $returnString->getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
|
||||
|
||||
use Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Source\ReturnString;
|
||||
|
||||
class NoConditionUnionNullable
|
||||
{
|
||||
/** @var string|null */
|
||||
private $nullOrString;
|
||||
|
||||
public function __construct(?ReturnString $returnString = null)
|
||||
{
|
||||
$this->nullOrString = $returnString->getNameOrNull();
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
-----
|
||||
<?php
|
||||
|
||||
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
|
||||
|
||||
use Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Source\ReturnString;
|
||||
|
||||
class NoConditionUnionNullable
|
||||
{
|
||||
private ?string $nullOrString = null;
|
||||
|
||||
public function __construct(?ReturnString $returnString = null)
|
||||
{
|
||||
$this->nullOrString = $returnString->getNameOrNull();
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
|
||||
|
||||
class SkipUnionType
|
||||
{
|
||||
/**
|
||||
* @var bool|int
|
||||
*/
|
||||
public $cantTouchThis = true;
|
||||
|
||||
public function setNumber()
|
||||
{
|
||||
$this->cantTouchThis = 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|int
|
||||
*/
|
||||
public function getCantTouchThis()
|
||||
{
|
||||
return $this->cantTouchThis;
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\FixtureUnionTypes;
|
||||
|
||||
class IncludeUnionedType
|
||||
{
|
||||
/**
|
||||
* @var bool|int
|
||||
*/
|
||||
public $cantTouchThis = true;
|
||||
|
||||
public function setNumber()
|
||||
{
|
||||
$this->cantTouchThis = 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|int
|
||||
*/
|
||||
public function getCantTouchThis()
|
||||
{
|
||||
return $this->cantTouchThis;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
-----
|
||||
<?php
|
||||
|
||||
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\FixtureUnionTypes;
|
||||
|
||||
class IncludeUnionedType
|
||||
{
|
||||
public bool|int $cantTouchThis = true;
|
||||
|
||||
public function setNumber()
|
||||
{
|
||||
$this->cantTouchThis = 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|int
|
||||
*/
|
||||
public function getCantTouchThis()
|
||||
{
|
||||
return $this->cantTouchThis;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\FixtureUnionTypes;
|
||||
|
||||
class SetIfElse
|
||||
{
|
||||
public $stringOrInteger = 'hi';
|
||||
|
||||
public function setNumber()
|
||||
{
|
||||
if (mt_rand(0, 100)) {
|
||||
$this->stringOrInteger = 'hey';
|
||||
} else {
|
||||
$this->stringOrInteger = 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
-----
|
||||
<?php
|
||||
|
||||
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\FixtureUnionTypes;
|
||||
|
||||
class SetIfElse
|
||||
{
|
||||
public string|int $stringOrInteger = 'hi';
|
||||
|
||||
public function setNumber()
|
||||
{
|
||||
if (mt_rand(0, 100)) {
|
||||
$this->stringOrInteger = 'hey';
|
||||
} else {
|
||||
$this->stringOrInteger = 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Source;
|
||||
|
||||
final class ReturnString
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'name';
|
||||
}
|
||||
|
||||
public function getNameOrNull(): ?string
|
||||
{
|
||||
if (mt_rand(0, 100)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'name';
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector;
|
||||
|
||||
use Iterator;
|
||||
use Rector\Core\Testing\PHPUnit\AbstractRectorTestCase;
|
||||
use Rector\Core\ValueObject\PhpVersionFeature;
|
||||
use Rector\Php74\Rector\Property\TypedPropertyRector;
|
||||
use Symplify\SmartFileSystem\SmartFileInfo;
|
||||
|
||||
@ -24,6 +25,11 @@ final class TypedPropertyRectorTest extends AbstractRectorTestCase
|
||||
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
|
||||
}
|
||||
|
||||
protected function getPhpVersion(): string
|
||||
{
|
||||
return PhpVersionFeature::BEFORE_UNION_TYPES;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
|
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\TypeDeclaration\AlreadyAssignDetector;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr;
|
||||
use PhpParser\Node\Expr\Assign;
|
||||
use Rector\Core\PhpParser\NodeTraverser\CallableNodeTraverser;
|
||||
use Rector\TypeDeclaration\Matcher\PropertyAssignMatcher;
|
||||
|
||||
abstract class AbstractAssignDetector
|
||||
{
|
||||
/**
|
||||
* @var CallableNodeTraverser
|
||||
*/
|
||||
protected $callableNodeTraverser;
|
||||
|
||||
/**
|
||||
* @var PropertyAssignMatcher
|
||||
*/
|
||||
private $propertyAssignMatcher;
|
||||
|
||||
/**
|
||||
* @required
|
||||
*/
|
||||
public function autowireAbstractAssignDetector(
|
||||
PropertyAssignMatcher $propertyAssignMatcher,
|
||||
CallableNodeTraverser $callableNodeTraverser
|
||||
): void {
|
||||
$this->propertyAssignMatcher = $propertyAssignMatcher;
|
||||
$this->callableNodeTraverser = $callableNodeTraverser;
|
||||
}
|
||||
|
||||
protected function matchAssignExprToPropertyName(Node $node, string $propertyName): ?Expr
|
||||
{
|
||||
if (! $node instanceof Assign) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->propertyAssignMatcher->matchPropertyAssignExpr($node, $propertyName);
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\TypeDeclaration\AlreadyAssignDetector;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\Assign;
|
||||
use PhpParser\Node\Stmt\ClassLike;
|
||||
use PhpParser\NodeTraverser;
|
||||
use Rector\Core\ValueObject\MethodName;
|
||||
use Rector\NodeTypeResolver\Node\AttributeKey;
|
||||
|
||||
final class ConstructorAssignDetector extends AbstractAssignDetector
|
||||
{
|
||||
public function detect(ClassLike $classLike, string $propertyName): bool
|
||||
{
|
||||
$isAssignedInConstructor = false;
|
||||
|
||||
$this->callableNodeTraverser->traverseNodesWithCallable($classLike->stmts, function (Node $node) use (
|
||||
$propertyName, &$isAssignedInConstructor
|
||||
): ?int {
|
||||
$expr = $this->matchAssignExprToPropertyName($node, $propertyName);
|
||||
if ($expr === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// is in constructor?
|
||||
$methodName = $node->getAttribute(AttributeKey::METHOD_NAME);
|
||||
if ($methodName !== MethodName::CONSTRUCT) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var Assign $assign */
|
||||
$assign = $node;
|
||||
|
||||
// cannot be nested
|
||||
if ($assign->getAttribute(AttributeKey::IS_FIRST_LEVEL_STATEMENT) !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$isAssignedInConstructor = true;
|
||||
|
||||
return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
|
||||
});
|
||||
|
||||
return $isAssignedInConstructor;
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\TypeDeclaration\AlreadyAssignDetector;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Stmt\ClassLike;
|
||||
use PhpParser\NodeTraverser;
|
||||
use Rector\NodeNestingScope\ScopeNestingComparator;
|
||||
use Rector\NodeTypeResolver\NodeTypeResolver;
|
||||
use Rector\PHPStanStaticTypeMapper\DoctrineTypeAnalyzer;
|
||||
|
||||
/**
|
||||
* Should add extra null type
|
||||
*/
|
||||
final class NullTypeAssignDetector extends AbstractAssignDetector
|
||||
{
|
||||
/**
|
||||
* @var ScopeNestingComparator
|
||||
*/
|
||||
private $scopeNestingComparator;
|
||||
|
||||
/**
|
||||
* @var DoctrineTypeAnalyzer
|
||||
*/
|
||||
private $doctrineTypeAnalyzer;
|
||||
|
||||
/**
|
||||
* @var NodeTypeResolver
|
||||
*/
|
||||
private $nodeTypeResolver;
|
||||
|
||||
public function __construct(
|
||||
ScopeNestingComparator $scopeNestingComparator,
|
||||
DoctrineTypeAnalyzer $doctrineTypeAnalyzer,
|
||||
NodeTypeResolver $nodeTypeResolver
|
||||
) {
|
||||
$this->scopeNestingComparator = $scopeNestingComparator;
|
||||
$this->doctrineTypeAnalyzer = $doctrineTypeAnalyzer;
|
||||
$this->nodeTypeResolver = $nodeTypeResolver;
|
||||
}
|
||||
|
||||
public function detect(ClassLike $classLike, string $propertyName): ?bool
|
||||
{
|
||||
$needsNullType = null;
|
||||
|
||||
$this->callableNodeTraverser->traverseNodesWithCallable($classLike->stmts, function (Node $node) use (
|
||||
$propertyName, &$needsNullType
|
||||
): ?int {
|
||||
$expr = $this->matchAssignExprToPropertyName($node, $propertyName);
|
||||
if ($expr === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->scopeNestingComparator->isNodeConditionallyScoped($expr)) {
|
||||
$needsNullType = true;
|
||||
return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
|
||||
}
|
||||
|
||||
// not in doctrine property
|
||||
$staticType = $this->nodeTypeResolver->getStaticType($expr);
|
||||
if ($this->doctrineTypeAnalyzer->isDoctrineCollectionWithIterableUnionType($staticType)) {
|
||||
$needsNullType = false;
|
||||
return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
return $needsNullType;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\TypeDeclaration\AlreadyAssignDetector;
|
||||
|
||||
use PhpParser\Node\Stmt\ClassLike;
|
||||
|
||||
final class PropertyDefaultAssignDetector
|
||||
{
|
||||
public function detect(ClassLike $classLike, string $propertyName): bool
|
||||
{
|
||||
$property = $classLike->getProperty($propertyName);
|
||||
if ($property === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $property->props[0]->default !== null;
|
||||
}
|
||||
}
|
61
rules/type-declaration/src/Matcher/PropertyAssignMatcher.php
Normal file
61
rules/type-declaration/src/Matcher/PropertyAssignMatcher.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\TypeDeclaration\Matcher;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr;
|
||||
use PhpParser\Node\Expr\ArrayDimFetch;
|
||||
use PhpParser\Node\Expr\Assign;
|
||||
use PhpParser\Node\Expr\PropertyFetch;
|
||||
use PhpParser\Node\Expr\StaticPropertyFetch;
|
||||
use Rector\NodeNameResolver\NodeNameResolver;
|
||||
|
||||
final class PropertyAssignMatcher
|
||||
{
|
||||
/**
|
||||
* @var NodeNameResolver
|
||||
*/
|
||||
private $nodeNameResolver;
|
||||
|
||||
public function __construct(NodeNameResolver $nodeNameResolver)
|
||||
{
|
||||
$this->nodeNameResolver = $nodeNameResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Covers:
|
||||
* - $this->propertyName = $expr;
|
||||
* - $this->propertyName[] = $expr;
|
||||
*/
|
||||
public function matchPropertyAssignExpr(Assign $assign, string $propertyName): ?Expr
|
||||
{
|
||||
if ($this->isPropertyFetch($assign->var)) {
|
||||
if (! $this->nodeNameResolver->isName($assign->var, $propertyName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $assign->expr;
|
||||
}
|
||||
|
||||
if ($assign->var instanceof ArrayDimFetch && $this->isPropertyFetch($assign->var->var)) {
|
||||
if (! $this->nodeNameResolver->isName($assign->var->var, $propertyName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $assign->expr;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isPropertyFetch(Node $node): bool
|
||||
{
|
||||
if ($node instanceof PropertyFetch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $node instanceof StaticPropertyFetch;
|
||||
}
|
||||
}
|
@ -10,8 +10,8 @@ use PhpParser\Node\Param;
|
||||
use PhpParser\Node\Stmt\ClassMethod;
|
||||
use PhpParser\Node\Stmt\Function_;
|
||||
use PHPStan\Type\MixedType;
|
||||
use PHPStan\Type\ObjectType;
|
||||
use PHPStan\Type\Type;
|
||||
use PHPStan\Type\TypeWithClassName;
|
||||
use Rector\Core\RectorDefinition\CodeSample;
|
||||
use Rector\Core\RectorDefinition\RectorDefinition;
|
||||
use Rector\Core\ValueObject\PhpVersionFeature;
|
||||
@ -146,14 +146,8 @@ CODE_SAMPLE
|
||||
return;
|
||||
}
|
||||
|
||||
if ($inferedType instanceof ObjectType) {
|
||||
$fqcn = $inferedType instanceof ShortenedObjectType
|
||||
? $inferedType->getFullyQualifiedName()
|
||||
: $inferedType->getClassName();
|
||||
$reflectionClass = new ReflectionClass($fqcn);
|
||||
if ($reflectionClass->isTrait()) {
|
||||
return;
|
||||
}
|
||||
if ($this->isTraitType($inferedType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode(
|
||||
@ -187,4 +181,25 @@ CODE_SAMPLE
|
||||
// already set → skip
|
||||
return ! $param->type->getAttribute(NewType::HAS_NEW_INHERITED_TYPE, false);
|
||||
}
|
||||
|
||||
private function isTraitType(Type $type): bool
|
||||
{
|
||||
if (! $type instanceof TypeWithClassName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fullyQualifiedName = $this->getFullyQualifiedName($type);
|
||||
$reflectionClass = new ReflectionClass($fullyQualifiedName);
|
||||
|
||||
return $reflectionClass->isTrait();
|
||||
}
|
||||
|
||||
private function getFullyQualifiedName(TypeWithClassName $typeWithClassName): string
|
||||
{
|
||||
if ($typeWithClassName instanceof ShortenedObjectType) {
|
||||
return $typeWithClassName->getFullyQualifiedName();
|
||||
}
|
||||
|
||||
return $typeWithClassName->getClassName();
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ use Rector\Core\RectorDefinition\RectorDefinition;
|
||||
use Rector\Core\ValueObject\MethodName;
|
||||
use Rector\Core\ValueObject\PhpVersionFeature;
|
||||
use Rector\NodeTypeResolver\Node\AttributeKey;
|
||||
use Rector\PHPStanStaticTypeMapper\PHPStanStaticTypeMapper;
|
||||
use Rector\TypeDeclaration\ChildPopulator\ChildReturnPopulator;
|
||||
use Rector\TypeDeclaration\OverrideGuard\ClassMethodReturnTypeOverrideGuard;
|
||||
use Rector\TypeDeclaration\PhpDocParser\NonInformativeReturnTagRemover;
|
||||
@ -142,7 +143,11 @@ CODE_SAMPLE
|
||||
return null;
|
||||
}
|
||||
|
||||
$inferredReturnNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($inferedType);
|
||||
$inferredReturnNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode(
|
||||
$inferedType,
|
||||
PHPStanStaticTypeMapper::KIND_RETURN
|
||||
);
|
||||
|
||||
if ($this->shouldSkipInferredReturnNode($node, $inferredReturnNode)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ use PhpParser\Node;
|
||||
use PhpParser\Node\Stmt\Property;
|
||||
use PHPStan\Type\MixedType;
|
||||
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
|
||||
use Rector\Core\Exception\ShouldNotHappenException;
|
||||
use Rector\Core\Rector\AbstractRector;
|
||||
use Rector\Core\RectorDefinition\CodeSample;
|
||||
use Rector\Core\RectorDefinition\RectorDefinition;
|
||||
@ -81,8 +82,12 @@ CODE_SAMPLE
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var PhpDocInfo $phpDocInfo */
|
||||
/** @var PhpDocInfo|null $phpDocInfo */
|
||||
$phpDocInfo = $node->getAttribute(AttributeKey::PHP_DOC_INFO);
|
||||
if ($phpDocInfo === null) {
|
||||
throw new ShouldNotHappenException();
|
||||
}
|
||||
|
||||
$phpDocInfo->changeVarType($propertyType);
|
||||
|
||||
return $node;
|
||||
|
@ -5,100 +5,116 @@ declare(strict_types=1);
|
||||
namespace Rector\TypeDeclaration\TypeInferer;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr;
|
||||
use PhpParser\Node\Expr\ArrayDimFetch;
|
||||
use PhpParser\Node\Expr\Assign;
|
||||
use PhpParser\Node\Expr\PropertyFetch;
|
||||
use PhpParser\Node\Expr\StaticPropertyFetch;
|
||||
use PhpParser\Node\Stmt\ClassLike;
|
||||
use PHPStan\Type\ArrayType;
|
||||
use PHPStan\Type\MixedType;
|
||||
use PHPStan\Type\NullType;
|
||||
use PHPStan\Type\Type;
|
||||
use Rector\Core\ValueObject\MethodName;
|
||||
use Rector\NodeTypeResolver\Node\AttributeKey;
|
||||
use Rector\TypeDeclaration\AlreadyAssignDetector\ConstructorAssignDetector;
|
||||
use Rector\TypeDeclaration\AlreadyAssignDetector\NullTypeAssignDetector;
|
||||
use Rector\TypeDeclaration\AlreadyAssignDetector\PropertyDefaultAssignDetector;
|
||||
use Rector\TypeDeclaration\Matcher\PropertyAssignMatcher;
|
||||
|
||||
final class AssignToPropertyTypeInferer extends AbstractTypeInferer
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
* @var ConstructorAssignDetector
|
||||
*/
|
||||
private $isAssignedInConstructor = false;
|
||||
private $constructorAssignDetector;
|
||||
|
||||
/**
|
||||
* @var PropertyAssignMatcher
|
||||
*/
|
||||
private $propertyAssignMatcher;
|
||||
|
||||
/**
|
||||
* @var PropertyDefaultAssignDetector
|
||||
*/
|
||||
private $propertyDefaultAssignDetector;
|
||||
|
||||
/**
|
||||
* @var NullTypeAssignDetector
|
||||
*/
|
||||
private $nullTypeAssignDetector;
|
||||
|
||||
public function __construct(
|
||||
ConstructorAssignDetector $constructorAssignDetector,
|
||||
PropertyAssignMatcher $propertyAssignMatcher,
|
||||
PropertyDefaultAssignDetector $propertyDefaultAssignDetector,
|
||||
NullTypeAssignDetector $nullTypeAssignDetector
|
||||
) {
|
||||
$this->constructorAssignDetector = $constructorAssignDetector;
|
||||
$this->propertyAssignMatcher = $propertyAssignMatcher;
|
||||
$this->propertyDefaultAssignDetector = $propertyDefaultAssignDetector;
|
||||
$this->nullTypeAssignDetector = $nullTypeAssignDetector;
|
||||
}
|
||||
|
||||
public function inferPropertyInClassLike(string $propertyName, ClassLike $classLike): Type
|
||||
{
|
||||
$assignedExprStaticTypes = [];
|
||||
$this->isAssignedInConstructor = false;
|
||||
$assignedExprTypes = [];
|
||||
|
||||
$this->callableNodeTraverser->traverseNodesWithCallable($classLike->stmts, function (Node $node) use (
|
||||
$propertyName,
|
||||
&$assignedExprStaticTypes
|
||||
&$assignedExprTypes
|
||||
) {
|
||||
if (! $node instanceof Assign) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$expr = $this->matchPropertyAssignExpr($node, $propertyName);
|
||||
$expr = $this->propertyAssignMatcher->matchPropertyAssignExpr($node, $propertyName);
|
||||
if ($expr === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$exprStaticType = $this->nodeTypeResolver->getStaticType($node->expr);
|
||||
if ($exprStaticType instanceof MixedType) {
|
||||
$exprStaticType = $this->resolveExprStaticTypeIncludingDimFetch($node);
|
||||
if ($exprStaticType === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($node->var instanceof ArrayDimFetch) {
|
||||
$exprStaticType = new ArrayType(new MixedType(), $exprStaticType);
|
||||
}
|
||||
|
||||
// is in constructor?
|
||||
$methodName = $node->getAttribute(AttributeKey::METHOD_NAME);
|
||||
if ($methodName === MethodName::CONSTRUCT) {
|
||||
$this->isAssignedInConstructor = true;
|
||||
}
|
||||
|
||||
$assignedExprStaticTypes[] = $exprStaticType;
|
||||
$assignedExprTypes[] = $exprStaticType;
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
// add default type, as not initialized in the constructor
|
||||
if (count($assignedExprStaticTypes) && ! $this->isAssignedInConstructor) {
|
||||
$assignedExprStaticTypes[] = new NullType();
|
||||
if ($this->shouldAddNullType($classLike, $propertyName, $assignedExprTypes)) {
|
||||
$assignedExprTypes[] = new NullType();
|
||||
}
|
||||
|
||||
return $this->typeFactory->createMixedPassedOrUnionType($assignedExprStaticTypes);
|
||||
return $this->typeFactory->createMixedPassedOrUnionType($assignedExprTypes);
|
||||
}
|
||||
|
||||
private function resolveExprStaticTypeIncludingDimFetch(Assign $assign): ?Type
|
||||
{
|
||||
$exprStaticType = $this->nodeTypeResolver->getStaticType($assign->expr);
|
||||
if ($exprStaticType instanceof MixedType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($assign->var instanceof ArrayDimFetch) {
|
||||
return new ArrayType(new MixedType(), $exprStaticType);
|
||||
}
|
||||
|
||||
return $exprStaticType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Covers:
|
||||
* - $this->propertyName = $expr;
|
||||
* - $this->propertyName[] = $expr;
|
||||
* @param Type[] $assignedExprTypes
|
||||
*/
|
||||
private function matchPropertyAssignExpr(Assign $assign, string $propertyName): ?Expr
|
||||
private function shouldAddNullType(ClassLike $classLike, string $propertyName, array $assignedExprTypes): bool
|
||||
{
|
||||
if ($this->isPropertyFetch($assign->var)) {
|
||||
if (! $this->nodeNameResolver->isName($assign->var, $propertyName)) {
|
||||
return null;
|
||||
}
|
||||
$hasPropertyDefaultValue = $this->propertyDefaultAssignDetector->detect($classLike, $propertyName);
|
||||
$isAssignedInConstructor = $this->constructorAssignDetector->detect($classLike, $propertyName);
|
||||
$shouldAddNullType = $this->nullTypeAssignDetector->detect($classLike, $propertyName);
|
||||
|
||||
return $assign->expr;
|
||||
if ((count($assignedExprTypes) === 0) && ($isAssignedInConstructor || $hasPropertyDefaultValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($assign->var instanceof ArrayDimFetch && $this->isPropertyFetch($assign->var->var)) {
|
||||
if (! $this->nodeNameResolver->isName($assign->var->var, $propertyName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $assign->expr;
|
||||
if ($shouldAddNullType === true) {
|
||||
return ! $isAssignedInConstructor && ! $hasPropertyDefaultValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isPropertyFetch(Node $node): bool
|
||||
{
|
||||
return $node instanceof PropertyFetch || $node instanceof StaticPropertyFetch;
|
||||
return (count($assignedExprTypes) > 0) && (! $isAssignedInConstructor && ! $hasPropertyDefaultValue);
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,16 @@ declare(strict_types=1);
|
||||
namespace Rector\TypeDeclaration\TypeInferer;
|
||||
|
||||
use PhpParser\Node\Stmt\Property;
|
||||
use PHPStan\Type\ArrayType;
|
||||
use PHPStan\Type\MixedType;
|
||||
use PHPStan\Type\NullType;
|
||||
use PHPStan\Type\NeverType;
|
||||
use PHPStan\Type\Type;
|
||||
use PHPStan\Type\VoidType;
|
||||
use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory;
|
||||
use Rector\PHPStanStaticTypeMapper\DoctrineTypeAnalyzer;
|
||||
use Rector\TypeDeclaration\Contract\TypeInferer\PropertyTypeInfererInterface;
|
||||
use Rector\TypeDeclaration\TypeInferer\PropertyTypeInferer\DefaultValuePropertyTypeInferer;
|
||||
use Rector\TypeDeclaration\TypeInferer\PropertyTypeInferer\VarDocPropertyTypeInferer;
|
||||
|
||||
final class PropertyTypeInferer extends AbstractPriorityAwareTypeInferer
|
||||
{
|
||||
@ -36,12 +38,18 @@ final class PropertyTypeInferer extends AbstractPriorityAwareTypeInferer
|
||||
*/
|
||||
private $doctrineTypeAnalyzer;
|
||||
|
||||
/**
|
||||
* @var VarDocPropertyTypeInferer
|
||||
*/
|
||||
private $varDocPropertyTypeInferer;
|
||||
|
||||
/**
|
||||
* @param PropertyTypeInfererInterface[] $propertyTypeInferers
|
||||
*/
|
||||
public function __construct(
|
||||
array $propertyTypeInferers,
|
||||
DefaultValuePropertyTypeInferer $defaultValuePropertyTypeInferer,
|
||||
VarDocPropertyTypeInferer $varDocPropertyTypeInferer,
|
||||
TypeFactory $typeFactory,
|
||||
DoctrineTypeAnalyzer $doctrineTypeAnalyzer
|
||||
) {
|
||||
@ -49,45 +57,73 @@ final class PropertyTypeInferer extends AbstractPriorityAwareTypeInferer
|
||||
$this->defaultValuePropertyTypeInferer = $defaultValuePropertyTypeInferer;
|
||||
$this->typeFactory = $typeFactory;
|
||||
$this->doctrineTypeAnalyzer = $doctrineTypeAnalyzer;
|
||||
$this->varDocPropertyTypeInferer = $varDocPropertyTypeInferer;
|
||||
}
|
||||
|
||||
public function inferProperty(Property $property): Type
|
||||
{
|
||||
$resolvedTypes = [];
|
||||
|
||||
foreach ($this->propertyTypeInferers as $propertyTypeInferer) {
|
||||
$type = $propertyTypeInferer->inferProperty($property);
|
||||
if ($type instanceof VoidType || $type instanceof MixedType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// default value type must be added to each resolved type
|
||||
$defaultValueType = $this->defaultValuePropertyTypeInferer->inferProperty($property);
|
||||
if ($this->shouldUnionWithDefaultValue($defaultValueType, $type)) {
|
||||
return $this->unionWithDefaultValueType($type, $defaultValueType);
|
||||
}
|
||||
|
||||
return $type;
|
||||
$resolvedTypes[] = $type;
|
||||
}
|
||||
|
||||
return new MixedType();
|
||||
// if nothing is clear from variable use, we use @var doc as fallback
|
||||
if (count($resolvedTypes) > 0) {
|
||||
$resolvedType = $this->typeFactory->createMixedPassedOrUnionType($resolvedTypes);
|
||||
} else {
|
||||
$resolvedType = $this->varDocPropertyTypeInferer->inferProperty($property);
|
||||
}
|
||||
|
||||
// default value type must be added to each resolved type if set
|
||||
// @todo include in one of inferrers above
|
||||
$propertyDefaultValue = $property->props[0]->default;
|
||||
|
||||
if ($propertyDefaultValue !== null) {
|
||||
$defaultValueType = $this->defaultValuePropertyTypeInferer->inferProperty($property);
|
||||
|
||||
if ($this->shouldUnionWithDefaultValue($defaultValueType, $resolvedType)) {
|
||||
return $this->unionWithDefaultValueType($defaultValueType, $resolvedType);
|
||||
}
|
||||
}
|
||||
|
||||
if ($resolvedType === null) {
|
||||
return new MixedType();
|
||||
}
|
||||
|
||||
return $resolvedType;
|
||||
}
|
||||
|
||||
private function shouldUnionWithDefaultValue(Type $defaultValueType, Type $type): bool
|
||||
private function shouldUnionWithDefaultValue(Type $defaultValueType, ?Type $type = null): bool
|
||||
{
|
||||
if ($defaultValueType instanceof MixedType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// skip empty array type (mixed[])
|
||||
if ($defaultValueType instanceof ArrayType && $defaultValueType->getItemType() instanceof NeverType && $type !== null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return ! $this->doctrineTypeAnalyzer->isDoctrineCollectionWithIterableUnionType($type);
|
||||
}
|
||||
|
||||
private function unionWithDefaultValueType(Type $type, Type $defaultValueType): Type
|
||||
private function unionWithDefaultValueType(Type $defaultValueType, ?Type $resolvedType): Type
|
||||
{
|
||||
// default type has bigger priority than @var type, if not nullable type
|
||||
if (! $defaultValueType instanceof NullType) {
|
||||
return $defaultValueType;
|
||||
}
|
||||
$types[] = $defaultValueType;
|
||||
|
||||
$types = [$type, $defaultValueType];
|
||||
if ($resolvedType !== null) {
|
||||
$types[] = $resolvedType;
|
||||
}
|
||||
|
||||
return $this->typeFactory->createMixedPassedOrUnionType($types);
|
||||
}
|
||||
|
@ -13,7 +13,8 @@ use Rector\TypeDeclaration\Contract\TypeInferer\PropertyTypeInfererInterface;
|
||||
/**
|
||||
* Special case of type inferer - it is always added in the end of the resolved types
|
||||
*/
|
||||
final class DefaultValuePropertyTypeInferer implements PropertyTypeInfererInterface
|
||||
//implements PropertyTypeInfererInterface
|
||||
final class DefaultValuePropertyTypeInferer
|
||||
{
|
||||
/**
|
||||
* @var NodeTypeResolver
|
||||
|
@ -5,27 +5,25 @@ declare(strict_types=1);
|
||||
namespace Rector\TypeDeclaration\TypeInferer\PropertyTypeInferer;
|
||||
|
||||
use PhpParser\Node\Stmt\Property;
|
||||
use PHPStan\Type\MixedType;
|
||||
use PHPStan\Type\Type;
|
||||
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
|
||||
use Rector\NodeTypeResolver\Node\AttributeKey;
|
||||
use Rector\TypeDeclaration\Contract\TypeInferer\PropertyTypeInfererInterface;
|
||||
|
||||
final class VarDocPropertyTypeInferer implements PropertyTypeInfererInterface
|
||||
final class VarDocPropertyTypeInferer
|
||||
{
|
||||
public function inferProperty(Property $property): Type
|
||||
public function inferProperty(Property $property): ?Type
|
||||
{
|
||||
/** @var PhpDocInfo|null $phpDocInfo */
|
||||
$phpDocInfo = $property->getAttribute(AttributeKey::PHP_DOC_INFO);
|
||||
if ($phpDocInfo === null) {
|
||||
return new MixedType();
|
||||
return null;
|
||||
}
|
||||
|
||||
// is empty
|
||||
if ($phpDocInfo->getPhpDocNode()->children === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $phpDocInfo->getVarType();
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 150;
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,11 @@ namespace Rector\TypeDeclaration\Tests\Rector\Property\CompleteVarDocTypePropert
|
||||
final class DefaultValue
|
||||
{
|
||||
private $number = 5;
|
||||
|
||||
private $maybe = false;
|
||||
private $dreams = [];
|
||||
|
||||
private $name = 'John';
|
||||
|
||||
private $longName = 'Elton' . 'John';
|
||||
}
|
||||
|
||||
@ -23,18 +25,17 @@ final class DefaultValue
|
||||
* @var int
|
||||
*/
|
||||
private $number = 5;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $maybe = false;
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private $dreams = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $name = 'John';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
|
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Rector\TypeDeclaration\Tests\Rector\Property\CompleteVarDocTypePropertyRector\Fixture;
|
||||
|
||||
final class DefaultValueArrayMixed
|
||||
{
|
||||
private $dreams = [];
|
||||
}
|
||||
|
||||
?>
|
||||
-----
|
||||
<?php
|
||||
|
||||
namespace Rector\TypeDeclaration\Tests\Rector\Property\CompleteVarDocTypePropertyRector\Fixture;
|
||||
|
||||
final class DefaultValueArrayMixed
|
||||
{
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private $dreams = [];
|
||||
}
|
||||
|
||||
?>
|
@ -10,18 +10,16 @@ class SkipMoreSpecific
|
||||
* @var SomeService[]
|
||||
*/
|
||||
private static $registry = [];
|
||||
|
||||
/**
|
||||
* register an SomeService for using.
|
||||
*
|
||||
* @param SomeService $service
|
||||
*/
|
||||
public static function register(SomeService $service)
|
||||
{
|
||||
self::$registry[$service->getLabel()] = $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* find registered SomeService.
|
||||
*
|
||||
* @return SomeService[]
|
||||
*/
|
||||
public static function getRegisteredSomeServices()
|
||||
|
@ -583,11 +583,11 @@ class Command
|
||||
*/
|
||||
protected static $defaultName;
|
||||
/**
|
||||
* @var Application
|
||||
* @var \Symfony\Component\Console\Application|null
|
||||
*/
|
||||
private $application;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*/
|
||||
private $name;
|
||||
/**
|
||||
@ -603,11 +603,11 @@ class Command
|
||||
*/
|
||||
private $hidden = false;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*/
|
||||
private $help;
|
||||
/**
|
||||
* @var string
|
||||
* @var string|null
|
||||
*/
|
||||
private $description;
|
||||
/**
|
||||
@ -627,7 +627,7 @@ class Command
|
||||
*/
|
||||
private $code;
|
||||
/**
|
||||
* @var HelperSet
|
||||
* @var \Symfony\Component\Console\Helper\HelperSet|null
|
||||
*/
|
||||
private $helperSet;
|
||||
/**
|
||||
|
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Rector\TypeDeclaration\Tests\Rector\Property\CompleteVarDocTypePropertyRector\Fixture;
|
||||
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
|
||||
final class CommandDefinedInConstructor
|
||||
{
|
||||
private $definition;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->definition = new InputDefinition();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|InputDefinition $definition An array of argument and option instances or a definition instance
|
||||
*/
|
||||
public function setDefinition($definition)
|
||||
{
|
||||
if ($definition instanceof InputDefinition) {
|
||||
$this->definition = $definition;
|
||||
} else {
|
||||
$this->definition->setDefinition($definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
-----
|
||||
<?php
|
||||
|
||||
namespace Rector\TypeDeclaration\Tests\Rector\Property\CompleteVarDocTypePropertyRector\Fixture;
|
||||
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
|
||||
final class CommandDefinedInConstructor
|
||||
{
|
||||
/**
|
||||
* @var \Symfony\Component\Console\Input\InputDefinition
|
||||
*/
|
||||
private $definition;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->definition = new InputDefinition();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|InputDefinition $definition An array of argument and option instances or a definition instance
|
||||
*/
|
||||
public function setDefinition($definition)
|
||||
{
|
||||
if ($definition instanceof InputDefinition) {
|
||||
$this->definition = $definition;
|
||||
} else {
|
||||
$this->definition->setDefinition($definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -45,7 +45,7 @@ use Symfony\Component\Console\Helper\HelperSet;
|
||||
class SymfonyConsoleHelperSet
|
||||
{
|
||||
/**
|
||||
* @var HelperSet
|
||||
* @var \Symfony\Component\Console\Helper\HelperSet|null
|
||||
*/
|
||||
private $helperSet;
|
||||
|
||||
|
@ -1,211 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Rector\TypeDeclaration\Tests\Rector\Property\PropertyTypeDeclarationRector\Fixture;
|
||||
|
||||
use App\Api\Poll\Entity\Poll;
|
||||
use App\Api\User\Entity\User;
|
||||
use App\Api\User\Entity\UserList;
|
||||
use App\Entity\LocalizedString;
|
||||
use App\Entity\LocalizedStringList;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="answer")
|
||||
* @ORM\Entity
|
||||
*/
|
||||
class Complex
|
||||
{
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\LocalizedString", mappedBy="answerText", cascade={"all"})
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
* @ORM\OrderBy({"version" = "DESC"})
|
||||
*/
|
||||
private $answer;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity="App\Api\User\Entity\User", inversedBy="answers", cascade={"persist", "merge"})
|
||||
*/
|
||||
private $voters;
|
||||
|
||||
public function getAnswer(): LocalizedStringList
|
||||
{
|
||||
if ($this->answer instanceof Collection) {
|
||||
return new LocalizedStringList($this->answer->getValues());
|
||||
}
|
||||
|
||||
if ($this->answer instanceof LocalizedStringList) {
|
||||
return $this->answer;
|
||||
}
|
||||
|
||||
return new LocalizedStringList($this->answer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LocalizedStringList|LocalizedString[]|ArrayCollection $answer
|
||||
*/
|
||||
public function setAnswer($answer): void
|
||||
{
|
||||
if ($answer instanceof LocalizedStringList) {
|
||||
$this->answer = $answer->getLocalizedStrings();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($answer instanceof ArrayCollection) {
|
||||
$this->answer = $answer->getValues();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->answer = $answer;
|
||||
}
|
||||
|
||||
public function getVoters(): UserList
|
||||
{
|
||||
if ($this->voters instanceof Collection) {
|
||||
return new UserList($this->voters->getValues());
|
||||
}
|
||||
|
||||
if ($this->voters instanceof UserList) {
|
||||
return $this->voters;
|
||||
}
|
||||
|
||||
return new UserList($this->voters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserList|User[]|ArrayCollection $voters
|
||||
*/
|
||||
public function setVoters($voters): void
|
||||
{
|
||||
if ($voters instanceof UserList) {
|
||||
$this->voters = $voters->getUsers();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($voters instanceof ArrayCollection) {
|
||||
$this->voters = $voters->getValues();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->voters = $voters;
|
||||
}
|
||||
|
||||
public function addVoter(User $user): void
|
||||
{
|
||||
$this->voters[] = $user;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
-----
|
||||
<?php
|
||||
|
||||
namespace Rector\TypeDeclaration\Tests\Rector\Property\PropertyTypeDeclarationRector\Fixture;
|
||||
|
||||
use App\Api\Poll\Entity\Poll;
|
||||
use App\Api\User\Entity\User;
|
||||
use App\Api\User\Entity\UserList;
|
||||
use App\Entity\LocalizedString;
|
||||
use App\Entity\LocalizedStringList;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="answer")
|
||||
* @ORM\Entity
|
||||
*/
|
||||
class Complex
|
||||
{
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\LocalizedString", mappedBy="answerText", cascade={"all"})
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
* @ORM\OrderBy({"version" = "DESC"})
|
||||
* @var \App\Entity\LocalizedString[]|\Doctrine\Common\Collections\Collection
|
||||
*/
|
||||
private $answer;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity="App\Api\User\Entity\User", inversedBy="answers", cascade={"persist", "merge"})
|
||||
* @var \App\Api\User\Entity\User[]|\Doctrine\Common\Collections\Collection
|
||||
*/
|
||||
private $voters;
|
||||
|
||||
public function getAnswer(): LocalizedStringList
|
||||
{
|
||||
if ($this->answer instanceof Collection) {
|
||||
return new LocalizedStringList($this->answer->getValues());
|
||||
}
|
||||
|
||||
if ($this->answer instanceof LocalizedStringList) {
|
||||
return $this->answer;
|
||||
}
|
||||
|
||||
return new LocalizedStringList($this->answer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LocalizedStringList|LocalizedString[]|ArrayCollection $answer
|
||||
*/
|
||||
public function setAnswer($answer): void
|
||||
{
|
||||
if ($answer instanceof LocalizedStringList) {
|
||||
$this->answer = $answer->getLocalizedStrings();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($answer instanceof ArrayCollection) {
|
||||
$this->answer = $answer->getValues();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->answer = $answer;
|
||||
}
|
||||
|
||||
public function getVoters(): UserList
|
||||
{
|
||||
if ($this->voters instanceof Collection) {
|
||||
return new UserList($this->voters->getValues());
|
||||
}
|
||||
|
||||
if ($this->voters instanceof UserList) {
|
||||
return $this->voters;
|
||||
}
|
||||
|
||||
return new UserList($this->voters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserList|User[]|ArrayCollection $voters
|
||||
*/
|
||||
public function setVoters($voters): void
|
||||
{
|
||||
if ($voters instanceof UserList) {
|
||||
$this->voters = $voters->getUsers();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($voters instanceof ArrayCollection) {
|
||||
$this->voters = $voters->getValues();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->voters = $voters;
|
||||
}
|
||||
|
||||
public function addVoter(User $user): void
|
||||
{
|
||||
$this->voters[] = $user;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -34,7 +34,7 @@ class ConstructorParam
|
||||
*/
|
||||
private $stringValue;
|
||||
/**
|
||||
* @var stdClass
|
||||
* @var \stdClass
|
||||
*/
|
||||
private $docBlockService;
|
||||
|
||||
|
@ -73,13 +73,13 @@ final class GetterType
|
||||
{
|
||||
/**
|
||||
* @Serializer\Type("string")
|
||||
* @var string
|
||||
* @var string|null
|
||||
*/
|
||||
private $email;
|
||||
|
||||
/**
|
||||
* @Serializer\Type("string")
|
||||
* @var string
|
||||
* @var string|null
|
||||
*/
|
||||
private $password;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user