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
8 changes: 8 additions & 0 deletions packages/TypeDeclaration/config/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
services:
_defaults:
public: true
autowire: true

Rector\TypeDeclaration\:
resource: '../src/'
exclude: '../src/{Rector/**/*Rector.php}'
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types=1);

namespace Rector\TypeDeclaration\Contract;

use PhpParser\Node\Stmt\Property;

interface PropertyTypeInfererInterface
{
/**
* @return string[]
*/
public function inferProperty(Property $property): array;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php declare(strict_types=1);

namespace Rector\TypeDeclaration\PropertyTypeInferer;

use Rector\NodeTypeResolver\NodeTypeResolver;
use Rector\NodeTypeResolver\StaticTypeToStringResolver;
use Rector\PhpParser\Node\Resolver\NameResolver;
use Rector\PhpParser\NodeTraverser\CallableNodeTraverser;

abstract class AbstractPropertyTypeInferer
{
/**
* @var CallableNodeTraverser
*/
protected $callableNodeTraverser;

/**
* @var NameResolver
*/
protected $nameResolver;

/**
* @var NodeTypeResolver
*/
protected $nodeTypeResolver;

/**
* @var StaticTypeToStringResolver
*/
protected $staticTypeToStringResolver;

/**
* @required
*/
public function autowireAbstractPropertyTypeInferer(
CallableNodeTraverser $callableNodeTraverser,
NameResolver $nameResolver,
NodeTypeResolver $nodeTypeResolver,
StaticTypeToStringResolver $staticTypeToStringResolver
): void {
$this->callableNodeTraverser = $callableNodeTraverser;
$this->nameResolver = $nameResolver;
$this->nodeTypeResolver = $nodeTypeResolver;
$this->staticTypeToStringResolver = $staticTypeToStringResolver;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php declare(strict_types=1);

namespace Rector\TypeDeclaration\PropertyTypeInferer;

use PhpParser\Node;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Stmt\ClassLike;
use PHPStan\Type\ArrayType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\TypeDeclaration\Contract\PropertyTypeInfererInterface;

final class AllAssignNodePropertyTypeInferer extends AbstractPropertyTypeInferer implements PropertyTypeInfererInterface
{
/**
* @return string[]
*/
public function inferProperty(Node\Stmt\Property $property): array
{
/** @var ClassLike $class */
$class = $property->getAttribute(AttributeKey::CLASS_NODE);

$propertyName = $this->nameResolver->resolve($property);

$assignedExprStaticTypes = $this->collectAllPropertyAsignExprStaticTypes($class, $propertyName);
if ($assignedExprStaticTypes === []) {
return [];
}

$assignedExprStaticType = new IntersectionType($assignedExprStaticTypes);

return $this->staticTypeToStringResolver->resolveObjectType($assignedExprStaticType);
}

/**
* @return Type[]
*/
private function collectAllPropertyAsignExprStaticTypes(ClassLike $classLike, string $propertyName): array
{
$assignedExprStaticTypes = [];

$this->callableNodeTraverser->traverseNodesWithCallable($classLike->stmts, function (Node $node) use (
$propertyName,
&$assignedExprStaticTypes
) {
if (! $node instanceof Assign) {
return null;
}

$expr = $this->matchPropertyAssignExpr($node, $propertyName);
if ($expr === null) {
return null;
}

$exprStaticType = $this->nodeTypeResolver->getNodeStaticType($node->expr);
if ($exprStaticType === null) {
return null;
}

if ($node->var instanceof ArrayDimFetch) {
$exprStaticType = new ArrayType(new MixedType(), $exprStaticType);
}

$assignedExprStaticTypes[] = $exprStaticType;

return null;
});

return $assignedExprStaticTypes;
}

/**
* Covers:
* - $this->propertyName = $expr;
* - $this->propertyName[] = $expr;
*/
private function matchPropertyAssignExpr(Assign $assign, string $propertyName): ?Node\Expr
{
if ($assign->var instanceof PropertyFetch) {
if (! $this->nameResolver->isName($assign->var, $propertyName)) {
return null;
}

return $assign->expr;
}

if ($assign->var instanceof ArrayDimFetch && $assign->var->var instanceof PropertyFetch) {
if (! $this->nameResolver->isName($assign->var->var, $propertyName)) {
return null;
}

return $assign->expr;
}

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

namespace Rector\TypeDeclaration\PropertyTypeInferer;

use PhpParser\Node;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\NullableType;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\NodeTraverser;
use PHPStan\Type\NullType;
use PHPStan\Type\Type;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\TypeDeclaration\Contract\PropertyTypeInfererInterface;

final class ConstructorPropertyTypeInferer extends AbstractPropertyTypeInferer implements PropertyTypeInfererInterface
{
/**
* @return string[]
*/
public function inferProperty(Node\Stmt\Property $property): array
{
/** @var Node\Stmt\Class_ $class */
$class = $property->getAttribute(AttributeKey::CLASS_NODE);

$classMethod = $class->getMethod('__construct');
if ($classMethod === null) {
return [];
}

$propertyName = $this->nameResolver->resolve($property);

$param = $this->resolveParamForPropertyFetch($classMethod, $propertyName);
if ($param === null) {
return [];
}

// A. infer from type declaration of parameter
if ($param->type) {
$type = $this->resolveParamTypeToString($param);
if ($type === null) {
return [];
}

$types = [];

// it's an array - annotation → make type more precise, if possible
if ($type === 'array') {
$paramStaticTypeAsString = $this->getResolveParamStaticTypeAsString($classMethod, $propertyName);
$types[] = $paramStaticTypeAsString ?? 'array';
} else {
$types[] = $type;
}

if ($this->isParamNullable($param)) {
$types[] = 'null';
}

return $types;
}

return [];
}

private function getResolveParamStaticTypeAsString(ClassMethod $classMethod, string $propertyName): ?string
{
$paramStaticType = $this->resolveParamStaticType($classMethod, $propertyName);
if ($paramStaticType === null) {
return null;
}

$typesAsStrings = $this->staticTypeToStringResolver->resolveObjectType($paramStaticType);

foreach ($typesAsStrings as $i => $typesAsString) {
$typesAsStrings[$i] = $this->removePreSlash($typesAsString);
}

return implode('|', $typesAsStrings);
}

private function resolveParamStaticType(ClassMethod $classMethod, string $propertyName): ?Type
{
$paramStaticType = null;

$this->callableNodeTraverser->traverseNodesWithCallable((array) $classMethod->stmts, function (Node $node) use (
$propertyName,
&$paramStaticType
): ?int {
if (! $node instanceof Variable) {
return null;
}

if (! $this->nameResolver->isName($node, $propertyName)) {
return null;
}

$paramStaticType = $this->nodeTypeResolver->getNodeStaticType($node);

return NodeTraverser::STOP_TRAVERSAL;
});

return $paramStaticType;
}

/**
* In case the property name is different to param name:
*
* E.g.:
* (SomeType $anotherValue)
* $this->value = $anotherValue;
* ↓
* $anotherValue param
*/
private function resolveParamForPropertyFetch(ClassMethod $classMethod, string $propertyName): ?Node\Param
{
$assignedParamName = null;

$this->callableNodeTraverser->traverseNodesWithCallable((array) $classMethod->stmts, function (Node $node) use (
$propertyName,
&$assignedParamName
): ?int {
if (! $node instanceof Assign) {
return null;
}

if (! $this->nameResolver->isName($node->var, $propertyName)) {
return null;
}

$assignedParamName = $this->nameResolver->resolve($node->expr);

return NodeTraverser::STOP_TRAVERSAL;
});

/** @var string|null $assignedParamName */
if ($assignedParamName === null) {
return null;
}

/** @var Node\Param $param */
foreach ((array) $classMethod->params as $param) {
if (! $this->nameResolver->isName($param, $assignedParamName)) {
continue;
}

return $param;
}

return null;
}

private function removePreSlash(string $content): string
{
return ltrim($content, '\\');
}

private function isParamNullable(Node\Param $param): bool
{
if ($param->type instanceof NullableType) {
return true;
}

if ($param->default) {
$defaultValueStaticType = $this->nodeTypeResolver->getNodeStaticType($param->default);
if ($defaultValueStaticType instanceof NullType) {
return true;
}
}

return false;
}

private function resolveParamTypeToString(Node\Param $param): ?string
{
if ($param->type === null) {
return null;
}

if ($param->type instanceof NullableType) {
return $this->nameResolver->resolve($param->type->type);
}

return $this->nameResolver->resolve($param->type);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types=1);

namespace Rector\TypeDeclaration\PropertyTypeInferer;

use PhpParser\Node\Stmt\Property;
use Rector\TypeDeclaration\Contract\PropertyTypeInfererInterface;

final class DefaultValuePropertyTypeInferer extends AbstractPropertyTypeInferer implements PropertyTypeInfererInterface
{
/**
* @return string[]
*/
public function inferProperty(Property $property): array
{
$propertyProperty = $property->props[0];
if ($propertyProperty->default === null) {
return [];
}

$nodeStaticType = $this->nodeTypeResolver->getNodeStaticType($propertyProperty->default);
if ($nodeStaticType === null) {
return [];
}

return $this->staticTypeToStringResolver->resolveObjectType($nodeStaticType);
}
}
Loading