[TypeDeclaration] Add AddParamTypeFromCallersRector (#5782)

This commit is contained in:
Tomas Votruba 2021-03-06 22:16:18 +01:00 committed by GitHub
parent 956cac70a0
commit 031deda881
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 681 additions and 182 deletions

View File

@ -33,7 +33,7 @@
"doctrine/annotations": "^1.11",
"doctrine/inflector": "^2.0",
"jean85/pretty-package-versions": "^1.5.1|^2.0.1",
"nette/robot-loader": "^3.2",
"nette/robot-loader": "^3.2 <=3.3.1",
"nette/utils": "^3.2",
"nikic/php-parser": "^4.10.4",
"phpstan/phpdoc-parser": "^0.4.9",

View File

@ -21,6 +21,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
use Symfony\Component\Filesystem\Filesystem;
use Symplify\Astral\NodeTraverser\SimpleCallableNodeTraverser;
use Symplify\PackageBuilder\Console\Command\CommandNaming;
use Symplify\PackageBuilder\Console\Style\SymfonyStyleFactory;
use Symplify\PackageBuilder\Parameter\ParameterProvider;
@ -56,7 +57,7 @@ return static function (ContainerConfigurator $containerConfigurator): void {
$services->alias(SymfonyApplication::class, ConsoleApplication::class);
$services->set(NoRectorsLoadedReporter::class);
$services->set(\Symplify\Astral\NodeTraverser\SimpleCallableNodeTraverser::class);
$services->set(SimpleCallableNodeTraverser::class);
$services->set(TextDescriptor::class);
@ -77,6 +78,7 @@ return static function (ContainerConfigurator $containerConfigurator): void {
$services->set(PrivatesCaller::class);
$services->set(FinderSanitizer::class);
$services->set(FileSystemFilter::class);
$services->set(ParameterProvider::class)
->arg('$container', service('service_container'));

View File

@ -2,20 +2,15 @@
declare(strict_types=1);
use Rector\TypeDeclaration\Rector\ClassMethod\AddArrayParamDocTypeRector;
use Rector\TypeDeclaration\Rector\ClassMethod\AddArrayReturnDocTypeRector;
use Rector\TypeDeclaration\Rector\Closure\AddClosureReturnTypeRector;
use Rector\TypeDeclaration\Rector\FunctionLike\ParamTypeDeclarationRector;
use Rector\TypeDeclaration\Rector\FunctionLike\ReturnTypeDeclarationRector;
use Rector\TypeDeclaration\Rector\Property\PropertyTypeDeclarationRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
return static function (
\Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator $containerConfigurator
): void {
$services = $containerConfigurator->services();
$services->set(ParamTypeDeclarationRector::class);
$services->set(ReturnTypeDeclarationRector::class);
$services->set(PropertyTypeDeclarationRector::class);
$services->set(AddClosureReturnTypeRector::class);
$services->set(AddArrayParamDocTypeRector::class);
$services->set(AddArrayReturnDocTypeRector::class);
$services->set(\Rector\TypeDeclaration\Rector\FunctionLike\ParamTypeDeclarationRector::class);
$services->set(\Rector\TypeDeclaration\Rector\FunctionLike\ReturnTypeDeclarationRector::class);
$services->set(\Rector\TypeDeclaration\Rector\Property\PropertyTypeDeclarationRector::class);
$services->set(\Rector\TypeDeclaration\Rector\Closure\AddClosureReturnTypeRector::class);
$services->set(\Rector\TypeDeclaration\Rector\ClassMethod\AddArrayParamDocTypeRector::class);
$services->set(\Rector\TypeDeclaration\Rector\ClassMethod\AddArrayReturnDocTypeRector::class);
// $services->set(\Rector\TypeDeclaration\Rector\ClassMethod\AddParamTypeFromCallersRector::class);
};

View File

@ -49,7 +49,6 @@ use Rector\PHPStanStaticTypeMapper\Utils\TypeUnwrapper;
use ReflectionMethod;
/**
* @rector-doc
* This service contains all the parsed nodes. E.g. all the functions, method call, classes, static calls etc.
* It's useful in case of context analysis, e.g. find all the usage of class method to detect, if the method is used.
*/
@ -377,7 +376,6 @@ final class NodeRepository
/** @var string $method */
$method = $classMethod->getAttribute(AttributeKey::METHOD_NAME);
return $this->findCallsByClassAndMethod($class, $method);
}

View File

@ -7,6 +7,7 @@ namespace Rector\PHPStanStaticTypeMapper\TypeAnalyzer;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\Reflection\ClassReflection;
@ -27,6 +28,7 @@ final class UnionTypeCommonTypeNarrower
* @var array<class-string<Node|\PHPStan\PhpDocParser\Ast\Node>, array<class-string<Node|\PHPStan\PhpDocParser\Ast\Node>>>
*/
private const PRIORITY_TYPES = [
FunctionLike::class => [FunctionLike::class],
BinaryOp::class => [BinaryOp::class, Expr::class],
Expr::class => [Node::class, Expr::class],
Stmt::class => [Node::class, Stmt::class],

View File

@ -75,6 +75,9 @@ final class ArrayTypeMapper implements TypeMapperInterface
$this->reflectionProvider = $reflectionProvider;
}
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return ArrayType::class;

View File

@ -27,6 +27,9 @@ final class BooleanTypeMapper implements TypeMapperInterface
$this->phpVersionProvider = $phpVersionProvider;
}
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return BooleanType::class;

View File

@ -31,6 +31,9 @@ final class CallableTypeMapper implements TypeMapperInterface
$this->phpStanStaticTypeMapper = $phpStanStaticTypeMapper;
}
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return CallableType::class;

View File

@ -23,6 +23,9 @@ final class ClassStringTypeMapper implements TypeMapperInterface, PHPStanStaticT
*/
private $phpStanStaticTypeMapper;
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return ClassStringType::class;

View File

@ -32,6 +32,9 @@ final class ClosureTypeMapper implements TypeMapperInterface, PHPStanStaticTypeM
$this->callableTypeMapper = $callableTypeMapper;
}
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return ClosureType::class;

View File

@ -27,6 +27,9 @@ final class FloatTypeMapper implements TypeMapperInterface
$this->phpVersionProvider = $phpVersionProvider;
}
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return FloatType::class;

View File

@ -15,6 +15,9 @@ use Rector\PHPStanStaticTypeMapper\Contract\TypeMapperInterface;
final class HasOffsetTypeMapper implements TypeMapperInterface
{
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return HasOffsetType::class;

View File

@ -27,6 +27,9 @@ final class IntegerTypeMapper implements TypeMapperInterface
$this->phpVersionProvider = $phpVersionProvider;
}
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return IntegerType::class;

View File

@ -27,6 +27,9 @@ final class IntersectionTypeMapper implements TypeMapperInterface
$this->phpStanStaticTypeMapper = $phpStanStaticTypeMapper;
}
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return IntersectionType::class;

View File

@ -44,6 +44,9 @@ final class IterableTypeMapper implements TypeMapperInterface
$this->phpStanStaticTypeMapper = $phpStanStaticTypeMapper;
}
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return IterableType::class;

View File

@ -14,6 +14,9 @@ use Rector\PHPStanStaticTypeMapper\Contract\TypeMapperInterface;
final class MixedTypeMapper implements TypeMapperInterface
{
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return MixedType::class;

View File

@ -13,6 +13,9 @@ use Rector\PHPStanStaticTypeMapper\Contract\TypeMapperInterface;
final class NeverTypeMapper implements TypeMapperInterface
{
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return NeverType::class;

View File

@ -15,6 +15,9 @@ use Rector\PHPStanStaticTypeMapper\Contract\TypeMapperInterface;
final class NonEmptyArrayTypeMapper implements TypeMapperInterface
{
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return NonEmptyArrayType::class;

View File

@ -16,6 +16,9 @@ use Rector\PHPStanStaticTypeMapper\PHPStanStaticTypeMapper;
final class NullTypeMapper implements TypeMapperInterface
{
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return NullType::class;

View File

@ -42,6 +42,9 @@ final class ObjectTypeMapper implements TypeMapperInterface, PHPStanStaticTypeMa
$this->reflectionProvider = $reflectionProvider;
}
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return ObjectType::class;

View File

@ -36,6 +36,9 @@ final class ObjectWithoutClassTypeMapper implements TypeMapperInterface, PHPStan
$this->phpVersionProvider = $phpVersionProvider;
}
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return ObjectWithoutClassType::class;

View File

@ -15,6 +15,9 @@ use Rector\StaticTypeMapper\ValueObject\Type\ParentStaticType;
final class ParentStaticTypeMapper implements TypeMapperInterface
{
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return ParentStaticType::class;

View File

@ -14,6 +14,9 @@ use Rector\PHPStanStaticTypeMapper\Contract\TypeMapperInterface;
final class ResourceTypeMapper implements TypeMapperInterface
{
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return ResourceType::class;

View File

@ -15,6 +15,9 @@ use Rector\StaticTypeMapper\ValueObject\Type\SelfObjectType;
final class SelfObjectTypeMapper implements TypeMapperInterface
{
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return SelfObjectType::class;

View File

@ -31,6 +31,9 @@ final class StaticTypeMapper implements TypeMapperInterface
$this->phpVersionProvider = $phpVersionProvider;
}
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return StaticType::class;

View File

@ -19,6 +19,9 @@ final class StrictMixedTypeMapper implements TypeMapperInterface
*/
private const MIXED = 'mixed';
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return StrictMixedType::class;

View File

@ -27,6 +27,9 @@ final class StringTypeMapper implements TypeMapperInterface
$this->phpVersionProvider = $phpVersionProvider;
}
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return StringType::class;

View File

@ -15,6 +15,9 @@ use Rector\PHPStanStaticTypeMapper\Contract\TypeMapperInterface;
final class ThisTypeMapper implements TypeMapperInterface
{
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return ThisType::class;

View File

@ -24,6 +24,9 @@ final class TypeWithClassNameTypeMapper implements TypeMapperInterface
$this->stringTypeMapper = $stringTypeMapper;
}
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return TypeWithClassName::class;

View File

@ -85,6 +85,9 @@ final class UnionTypeMapper implements TypeMapperInterface
$this->phpStanStaticTypeMapper = $phpStanStaticTypeMapper;
}
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return UnionType::class;
@ -107,7 +110,6 @@ final class UnionTypeMapper implements TypeMapperInterface
}
$unionTypesNodes = array_unique($unionTypesNodes);
return new AttributeAwareUnionTypeNode($unionTypesNodes);
}

View File

@ -31,6 +31,9 @@ final class VoidTypeMapper implements TypeMapperInterface
$this->phpVersionProvider = $phpVersionProvider;
}
/**
* @return class-string<Type>
*/
public function getNodeClass(): string
{
return VoidType::class;

View File

@ -7,6 +7,7 @@ namespace Rector\PostRector\Application;
use PhpParser\Node;
use PhpParser\NodeTraverser;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\Logging\CurrentRectorProvider;
use Rector\NodeTypeResolver\FileSystem\CurrentFileInfoProvider;
use Rector\PostRector\Contract\Rector\PostRectorInterface;
use Symplify\Skipper\Skipper\Skipper;
@ -29,14 +30,24 @@ final class PostFileProcessor
*/
private $currentFileInfoProvider;
/**
* @var CurrentRectorProvider
*/
private $currentRectorProvider;
/**
* @param PostRectorInterface[] $postRectors
*/
public function __construct(Skipper $skipper, CurrentFileInfoProvider $currentFileInfoProvider, array $postRectors)
{
public function __construct(
Skipper $skipper,
CurrentFileInfoProvider $currentFileInfoProvider,
CurrentRectorProvider $currentRectorProvider,
array $postRectors
) {
$this->postRectors = $this->sortByPriority($postRectors);
$this->skipper = $skipper;
$this->currentFileInfoProvider = $currentFileInfoProvider;
$this->currentRectorProvider = $currentRectorProvider;
}
/**
@ -50,6 +61,8 @@ final class PostFileProcessor
continue;
}
$this->currentRectorProvider->changeCurrentRector($postRector);
$nodeTraverser = new NodeTraverser();
$nodeTraverser->addVisitor($postRector);
$nodes = $nodeTraverser->traverse($nodes);

View File

@ -47,7 +47,6 @@ parameters:
- stubs
reportUnmatchedIgnoredErrors: false
checkGenericClassInNonGenericObjectType: false
excludes_analyse:

View File

@ -4,11 +4,10 @@ declare(strict_types=1);
namespace Rector\Arguments\NodeAnalyzer;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\Arguments\ValueObject\ArgumentAdder;
use Rector\NodeNameResolver\NodeNameResolver;
@ -40,26 +39,21 @@ final class ArgumentAddingScope
}
/**
* @param ClassMethod|MethodCall|StaticCall $node
* @param MethodCall|StaticCall $expr
*/
public function isInCorrectScope(Node $node, ArgumentAdder $argumentAdder): bool
public function isInCorrectScope(Expr $expr, ArgumentAdder $argumentAdder): bool
{
if ($argumentAdder->getScope() === null) {
return true;
}
$scope = $argumentAdder->getScope();
if ($node instanceof ClassMethod) {
return $scope === self::SCOPE_CLASS_METHOD;
}
if ($node instanceof StaticCall) {
if (! $node->class instanceof Name) {
if ($expr instanceof StaticCall) {
if (! $expr->class instanceof Name) {
return false;
}
if ($this->nodeNameResolver->isName($node->class, 'parent')) {
if ($this->nodeNameResolver->isName($expr->class, 'parent')) {
return $scope === self::SCOPE_PARENT_CALL;
}

View File

@ -212,6 +212,7 @@ CODE_SAMPLE
return $this->isName($node->params[$position], $argumentName);
}
// already added?
if (! isset($node->args[$position])) {
// is correct scope?

View File

@ -8,6 +8,7 @@ use Nette\Utils\Strings;
use PhpParser\BuilderHelpers;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Stmt\ClassMethod;
@ -120,23 +121,23 @@ CODE_SAMPLE
}
/**
* @param MethodCall|StaticCall $node
* @param MethodCall|StaticCall $expr
*/
private function processArgs(Node $node, ArgumentDefaultValueReplacer $argumentDefaultValueReplacer): void
private function processArgs(Expr $expr, ArgumentDefaultValueReplacer $argumentDefaultValueReplacer): void
{
$position = $argumentDefaultValueReplacer->getPosition();
$argValue = $this->valueResolver->getValue($node->args[$position]->value);
$argValue = $this->valueResolver->getValue($expr->args[$position]->value);
if (is_scalar(
$argumentDefaultValueReplacer->getValueBefore()
) && $argValue === $argumentDefaultValueReplacer->getValueBefore()) {
$node->args[$position] = $this->normalizeValueToArgument($argumentDefaultValueReplacer->getValueAfter());
$expr->args[$position] = $this->normalizeValueToArgument($argumentDefaultValueReplacer->getValueAfter());
} elseif (is_array($argumentDefaultValueReplacer->getValueBefore())) {
$newArgs = $this->processArrayReplacement($node->args, $argumentDefaultValueReplacer);
$newArgs = $this->processArrayReplacement($expr->args, $argumentDefaultValueReplacer);
if ($newArgs) {
$node->args = $newArgs;
$expr->args = $newArgs;
}
}
}

View File

@ -17,7 +17,7 @@ use Rector\NodeTypeResolver\NodeTypeResolver;
final class ValueObjectClassAnalyzer
{
/**
* @var bool[]
* @var array<string, bool>
*/
private $valueObjectStatusByClassName = [];
@ -98,7 +98,7 @@ final class ValueObjectClassAnalyzer
return true;
}
private function analyseWithoutConstructor(Class_ $class, ?string $className): bool
private function analyseWithoutConstructor(Class_ $class, string $className): bool
{
// A. has all properties with serialize?
if ($this->hasAllPropertiesWithSerialize($class)) {

View File

@ -309,7 +309,7 @@ CODE_SAMPLE
/**
* @param array<int, Node|null> $multiNodes
*/
private function getSameVarName(array $multiNodes, Node $node): ?Variable
private function getSameVarName(array $multiNodes, Variable $variable): ?Variable
{
foreach ($multiNodes as $multiNode) {
if ($multiNode === null) {
@ -317,12 +317,12 @@ CODE_SAMPLE
}
/** @var Variable|null $found */
$found = $this->betterNodeFinder->findFirst($multiNode, function (Node $n) use ($node): bool {
$found = $this->betterNodeFinder->findFirst($multiNode, function (Node $n) use ($variable): bool {
$n = $this->mayBeArrayDimFetch($n);
if (! $n instanceof Variable) {
return false;
}
return $this->isName($n, (string) $this->getName($node));
return $this->isName($n, (string) $this->getName($variable));
});
if ($found !== null) {

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Rector\CodeQuality\NodeAnalyzer;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Class_;
use Rector\NodeNameResolver\NodeNameResolver;
final class ClassLikeAnalyzer
@ -22,11 +22,11 @@ final class ClassLikeAnalyzer
/**
* @return string[]
*/
public function resolvePropertyNames(ClassLike $classLike): array
public function resolvePropertyNames(Class_ $class): array
{
$propertyNames = [];
foreach ($classLike->getProperties() as $property) {
foreach ($class->getProperties() as $property) {
$propertyNames[] = $this->nodeNameResolver->getName($property);
}

View File

@ -16,7 +16,6 @@ use PhpParser\Node\Expr\PreInc;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Stmt\For_;
use PhpParser\Node\Stmt\Unset_;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\NodeManipulator\AssignManipulator;
use Rector\Core\PhpParser\Comparing\NodeComparator;
use Rector\Core\PhpParser\Node\BetterNodeFinder;
@ -137,7 +136,7 @@ final class ForAnalyzer
);
}
public function isAssignmentWithArrayDimFetchAsVariableInsideForStatements(For_ $for, ?string $keyValueName): bool
public function isAssignmentWithArrayDimFetchAsVariableInsideForStatements(For_ $for, string $keyValueName): bool
{
return (bool) $this->betterNodeFinder->findFirst(
$for->stmts,
@ -150,10 +149,6 @@ final class ForAnalyzer
return false;
}
if ($keyValueName === null) {
throw new ShouldNotHappenException();
}
$arrayDimFetch = $node->var;
if ($arrayDimFetch->dim === null) {
return false;

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Rector\CodeQuality\NodeFactory;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\ClosureUse;
use PhpParser\Node\Expr\MethodCall;
@ -50,9 +50,9 @@ final class AnonymousFunctionFactory
}
/**
* @param Variable|PropertyFetch $node
* @param Variable|PropertyFetch $expr
*/
public function create(PhpMethodReflection $phpMethodReflection, Node $node): Closure
public function create(PhpMethodReflection $phpMethodReflection, Expr $expr): Closure
{
/** @var FunctionVariantWithPhpDocs $functionVariantWithPhpDoc */
$functionVariantWithPhpDoc = $phpMethodReflection->getVariants()[0];
@ -62,7 +62,7 @@ final class AnonymousFunctionFactory
$anonymousFunction->params = $newParams;
$innerMethodCall = new MethodCall($node, $phpMethodReflection->getName());
$innerMethodCall = new MethodCall($expr, $phpMethodReflection->getName());
$innerMethodCall->args = $this->nodeFactory->createArgsFromParams($newParams);
if (! $functionVariantWithPhpDoc->getReturnType() instanceof MixedType) {
@ -80,8 +80,8 @@ final class AnonymousFunctionFactory
$anonymousFunction->stmts[] = new Expression($innerMethodCall);
}
if ($node instanceof Variable && ! $this->nodeNameResolver->isName($node, 'this')) {
$anonymousFunction->uses[] = new ClosureUse($node);
if ($expr instanceof Variable && ! $this->nodeNameResolver->isName($expr, 'this')) {
$anonymousFunction->uses[] = new ClosureUse($expr);
}
return $anonymousFunction;

View File

@ -101,13 +101,13 @@ CODE_SAMPLE
}
/**
* @param Array_|List_ $node
* @param Array_|List_ $expr
* @return Assign[]
*/
private function createStandaloneAssigns(Node $node, Array_ $rightArray): array
private function createStandaloneAssigns(Expr $expr, Array_ $rightArray): array
{
$standaloneAssigns = [];
foreach ($node->items as $key => $leftArrayItem) {
foreach ($expr->items as $key => $leftArrayItem) {
if ($leftArrayItem === null) {
continue;
}

View File

@ -180,7 +180,7 @@ final class ShortNameResolver
/**
* @param Node[] $stmts
* @return string[]
* @return array<string, string>
*/
private function resolveFromDocBlocks(array $stmts): array
{
@ -192,6 +192,7 @@ final class ShortNameResolver
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node);
foreach ($phpDocInfo->getPhpDocNode()->children as $phpDocChildNode) {
/** @var PhpDocChildNode $phpDocChildNode */
$shortTagName = $this->resolveShortTagNameFromPhpDocChildNode($phpDocChildNode);
if ($shortTagName === null) {
continue;

View File

@ -95,7 +95,23 @@ CODE_SAMPLE
]);
}
public function isNullableParam(Param $param, FunctionLike $functionLike): bool
/**
* @param ClassMethod|Function_ $node
*/
public function refactor(Node $node): ?Node
{
if ($node->params === []) {
return null;
}
foreach ($node->params as $param) {
$this->refactorParam($param, $node);
}
return null;
}
private function isNullableParam(Param $param, FunctionLike $functionLike): bool
{
if ($param->variadic) {
return false;
@ -119,22 +135,6 @@ CODE_SAMPLE
return $this->getDifferentParamTypeFromAncestorClass($param, $functionLike) !== null;
}
/**
* @param ClassMethod|Function_ $node
*/
public function refactor(Node $node): ?Node
{
if ($node->params === []) {
return null;
}
foreach ($node->params as $param) {
$this->refactorParam($param, $node);
}
return null;
}
private function getDifferentParamTypeFromAncestorClass(Param $param, FunctionLike $functionLike): ?string
{
$scope = $functionLike->getAttribute(AttributeKey::SCOPE);

View File

@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace Rector\TypeDeclaration\NodeAnalyzer;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ThisType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeWithClassName;
use PHPStan\Type\UnionType;
use Rector\NodeCollector\ValueObject\ArrayCallable;
use Rector\NodeTypeResolver\NodeTypeResolver;
use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory;
final class CallTypesResolver
{
/**
* @var string
*/
private const STRICTNESS_TYPE_DECLARATION = 'type_declaration';
/**
* @var string
*/
private const STRICTNESS_DOCBLOCK = 'docblock';
/**
* @var NodeTypeResolver
*/
private $nodeTypeResolver;
/**
* @var TypeFactory
*/
private $typeFactory;
public function __construct(NodeTypeResolver $nodeTypeResolver, TypeFactory $typeFactory)
{
$this->nodeTypeResolver = $nodeTypeResolver;
$this->typeFactory = $typeFactory;
}
/**
* @param MethodCall[]|StaticCall[]|ArrayCallable[] $calls
* @return Type[]
*/
public function resolveStrictTypesFromCalls(array $calls): array
{
return $this->resolveTypesFromCalls($calls, self::STRICTNESS_TYPE_DECLARATION);
}
/**
* @param MethodCall[]|StaticCall[]|ArrayCallable[] $calls
* @return Type[]
*/
public function resolveWeakTypesFromCalls(array $calls): array
{
return $this->resolveTypesFromCalls($calls, self::STRICTNESS_DOCBLOCK);
}
/**
* @param MethodCall[]|StaticCall[]|ArrayCallable[] $calls
* @return Type[]
*/
private function resolveTypesFromCalls(array $calls, string $strictnessLevel): array
{
$staticTypesByArgumentPosition = [];
foreach ($calls as $call) {
if (! $call instanceof StaticCall && ! $call instanceof MethodCall) {
continue;
}
foreach ($call->args as $position => $arg) {
$argValueType = $this->resolveArgValueType($strictnessLevel, $arg);
$staticTypesByArgumentPosition[$position][] = $argValueType;
}
}
// unite to single type
return $this->unionToSingleType($staticTypesByArgumentPosition);
}
private function resolveArgValueType(string $strictnessLevel, Arg $arg): Type
{
if ($strictnessLevel === self::STRICTNESS_TYPE_DECLARATION) {
$argValueType = $this->nodeTypeResolver->getNativeType($arg->value);
} else {
$argValueType = $this->nodeTypeResolver->resolve($arg->value);
}
// "self" in another object is not correct, this make it independent
return $this->correctSelfType($argValueType);
}
private function correctSelfType(Type $argValueType): Type
{
if ($argValueType instanceof ThisType) {
return new ObjectType($argValueType->getClassName());
}
return $argValueType;
}
/**
* @param array<int, Type[]> $staticTypesByArgumentPosition
* @return array<int, Type>
*/
private function unionToSingleType(array $staticTypesByArgumentPosition): array
{
$staticTypeByArgumentPosition = [];
foreach ($staticTypesByArgumentPosition as $position => $staticTypes) {
$unionedType = $this->typeFactory->createMixedPassedOrUnionType($staticTypes);
// narrow parents to most child type
$unionedType = $this->narrowParentObjectTreeToSingleObjectChildType($unionedType);
$staticTypeByArgumentPosition[$position] = $unionedType;
}
return $staticTypeByArgumentPosition;
}
private function narrowParentObjectTreeToSingleObjectChildType(Type $type): Type
{
if (! $type instanceof UnionType) {
return $type;
}
if (! $this->isTypeWithClassNameOnly($type)) {
return $type;
}
/** @var TypeWithClassName $firstUnionedType */
$firstUnionedType = $type->getTypes()[0];
foreach ($type->getTypes() as $unionedType) {
if (! $unionedType instanceof TypeWithClassName) {
return $type;
}
if (! is_a($firstUnionedType->getClassName(), $unionedType->getClassName(), true)) {
return $type;
}
}
return $firstUnionedType;
}
private function isTypeWithClassNameOnly(UnionType $unionType): bool
{
foreach ($unionType->getTypes() as $unionedType) {
if (! $unionedType instanceof TypeWithClassName) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Rector\TypeDeclaration\NodeAnalyzer;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use Rector\StaticTypeMapper\StaticTypeMapper;
use Rector\VendorLocker\NodeVendorLocker\ClassMethodParamVendorLockResolver;
final class ClassMethodParamTypeCompleter
{
/**
* @var StaticTypeMapper
*/
private $staticTypeMapper;
/**
* @var ClassMethodParamVendorLockResolver
*/
private $classMethodParamVendorLockResolver;
public function __construct(
StaticTypeMapper $staticTypeMapper,
ClassMethodParamVendorLockResolver $classMethodParamVendorLockResolver
) {
$this->staticTypeMapper = $staticTypeMapper;
$this->classMethodParamVendorLockResolver = $classMethodParamVendorLockResolver;
}
/**
* @param array<int, Type> $classParameterTypes
*/
public function complete(ClassMethod $classMethod, array $classParameterTypes): ?ClassMethod
{
$hasChanged = false;
foreach ($classParameterTypes as $position => $argumentStaticType) {
if ($this->shouldSkipArgumentStaticType($classMethod, $argumentStaticType, $position)) {
continue;
}
$phpParserTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($argumentStaticType);
if ($phpParserTypeNode === null) {
continue;
}
// update parameter
$classMethod->params[$position]->type = $phpParserTypeNode;
$hasChanged = true;
}
if ($hasChanged) {
return $classMethod;
}
return null;
}
private function shouldSkipArgumentStaticType(
ClassMethod $classMethod,
Type $argumentStaticType,
int $position
): bool {
if ($argumentStaticType instanceof MixedType) {
return true;
}
if (! isset($classMethod->params[$position])) {
return true;
}
if ($this->classMethodParamVendorLockResolver->isVendorLocked($classMethod, $position)) {
return true;
}
$parameter = $classMethod->params[$position];
if ($parameter->type === null) {
return false;
}
$parameterStaticType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($parameter->type);
// already completed → skip
return $parameterStaticType->equals($argumentStaticType);
}
}

View File

@ -5,16 +5,10 @@ declare(strict_types=1);
namespace Rector\TypeDeclaration\Rector\ClassMethod;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ThisType;
use PHPStan\Type\Type;
use Rector\Core\Rector\AbstractRector;
use Rector\NodeCollector\ValueObject\ArrayCallable;
use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory;
use Rector\TypeDeclaration\NodeAnalyzer\CallTypesResolver;
use Rector\TypeDeclaration\NodeAnalyzer\ClassMethodParamTypeCompleter;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
@ -26,13 +20,21 @@ use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
final class AddMethodCallBasedStrictParamTypeRector extends AbstractRector
{
/**
* @var TypeFactory
* @var CallTypesResolver
*/
private $typeFactory;
private $callTypesResolver;
public function __construct(TypeFactory $typeFactory)
{
$this->typeFactory = $typeFactory;
/**
* @var ClassMethodParamTypeCompleter
*/
private $classMethodParamTypeCompleter;
public function __construct(
CallTypesResolver $callTypesResolver,
ClassMethodParamTypeCompleter $classMethodParamTypeCompleter
) {
$this->callTypesResolver = $callTypesResolver;
$this->classMethodParamTypeCompleter = $classMethodParamTypeCompleter;
}
public function getRuleDefinition(): RuleDefinition
@ -99,86 +101,13 @@ CODE_SAMPLE
*/
public function refactor(Node $node): ?Node
{
if ($node->params === []) {
return null;
}
$classMethodCalls = $this->nodeRepository->findCallsByClassMethod($node);
$classParameterTypes = $this->getCallTypesByPosition($classMethodCalls);
$classMethodParameterTypes = $this->callTypesResolver->resolveStrictTypesFromCalls($classMethodCalls);
foreach ($classParameterTypes as $position => $argumentStaticType) {
if ($this->shouldSkipArgumentStaticType($node, $argumentStaticType, $position)) {
continue;
}
$phpParserTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($argumentStaticType);
if ($phpParserTypeNode === null) {
continue;
}
// update parameter
$node->params[$position]->type = $phpParserTypeNode;
}
return $node;
}
/**
* @param MethodCall[]|StaticCall[]|ArrayCallable[] $calls
* @return Type[]
*/
private function getCallTypesByPosition(array $calls): array
{
$staticTypesByArgumentPosition = [];
foreach ($calls as $call) {
if (! $call instanceof StaticCall && ! $call instanceof MethodCall) {
continue;
}
foreach ($call->args as $position => $arg) {
$argValueType = $this->nodeTypeResolver->getNativeType($arg->value);
// "self" in another object is not correct, this make it independent
$argValueType = $this->correctSelfType($argValueType);
$staticTypesByArgumentPosition[$position][] = $argValueType;
}
}
// unite to single type
$staticTypeByArgumentPosition = [];
foreach ($staticTypesByArgumentPosition as $position => $staticTypes) {
$staticTypeByArgumentPosition[$position] = $this->typeFactory->createMixedPassedOrUnionType($staticTypes);
}
return $staticTypeByArgumentPosition;
}
private function shouldSkipArgumentStaticType(
ClassMethod $classMethod,
Type $argumentStaticType,
int $position
): bool {
if ($argumentStaticType instanceof MixedType) {
return true;
}
if (! isset($classMethod->params[$position])) {
return true;
}
$parameter = $classMethod->params[$position];
if ($parameter->type === null) {
return false;
}
$parameterStaticType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($parameter->type);
// already completed → skip
return $parameterStaticType->equals($argumentStaticType);
}
private function correctSelfType(Type $argValueType): Type
{
if ($argValueType instanceof ThisType) {
return new ObjectType($argValueType->getClassName());
}
return $argValueType;
return $this->classMethodParamTypeCompleter->complete($node, $classMethodParameterTypes);
}
}

View File

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace Rector\TypeDeclaration\Rector\ClassMethod;
use PhpParser\Node;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\Core\Rector\AbstractRector;
use Rector\TypeDeclaration\NodeAnalyzer\CallTypesResolver;
use Rector\TypeDeclaration\NodeAnalyzer\ClassMethodParamTypeCompleter;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
/**
* @see https://github.com/symplify/phpstan-rules/blob/master/docs/rules_overview.md#checktypehintcallertyperule
*
* @see \Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddParamTypeFromCallersRector\AddParamTypeFromCallersRectorTest
*
* Less strict version of \Rector\TypeDeclaration\Rector\ClassMethod\AddMethodCallBasedStrictParamTypeRector,
* that can work with docblocks too
*/
final class AddParamTypeFromCallersRector extends AbstractRector
{
/**
* @var CallTypesResolver
*/
private $callTypesResolver;
/**
* @var ClassMethodParamTypeCompleter
*/
private $classMethodParamTypeCompleter;
public function __construct(
CallTypesResolver $callTypesResolver,
ClassMethodParamTypeCompleter $classMethodParamTypeCompleter
) {
$this->callTypesResolver = $callTypesResolver;
$this->classMethodParamTypeCompleter = $classMethodParamTypeCompleter;
}
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Add param type based on called types in that particular method', [
new CodeSample(
<<<'CODE_SAMPLE'
final class SomeClass
{
public function run(Return_ $return)
{
$this->print($return);
}
public function print($return)
{
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
final class SomeClass
{
public function run(Return_ $return)
{
$this->print($return);
}
public function print(Return_ $return)
{
}
}
CODE_SAMPLE
),
]);
}
/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [ClassMethod::class];
}
/**
* @param ClassMethod $node
*/
public function refactor(Node $node): ?Node
{
if ($node->params === []) {
return null;
}
$calls = $this->nodeRepository->findCallsByClassMethod($node);
if ($calls === []) {
return null;
}
$classMethodParameterTypes = $this->callTypesResolver->resolveWeakTypesFromCalls($calls);
return $this->classMethodParamTypeCompleter->complete($node, $classMethodParameterTypes);
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddParamTypeFromCallersRector;
use Iterator;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
use Symplify\SmartFileSystem\SmartFileInfo;
final class AddParamTypeFromCallersRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideData()
*/
public function test(SmartFileInfo $fileInfo): void
{
$this->doTestFileInfo($fileInfo);
}
public function provideData(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
protected function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddParamTypeFromCallersRector\Fixture;
use PhpParser\Node\Stmt\Return_;
use Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddParamTypeFromCallersRector\Source\SomeInterface;
final class SkipByInterface implements SomeInterface
{
public function run(Return_ $return)
{
$this->print($return);
}
public function print(\PhpParser\Node $return)
{
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddParamTypeFromCallersRector\Fixture;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
final class SkipFunctionLike
{
/**
* @param ClassMethod|Function_ $functionLike
*/
public function run(FunctionLike $functionLike)
{
$this->print($functionLike);
}
public function print(\PhpParser\Node\FunctionLike $functionLike)
{
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddParamTypeFromCallersRector\Fixture;
use PhpParser\Node\Stmt\Return_;
final class SomeClass
{
public function run(Return_ $return)
{
$this->print($return);
}
public function print($return)
{
}
}
?>
-----
<?php
namespace Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddParamTypeFromCallersRector\Fixture;
use PhpParser\Node\Stmt\Return_;
final class SomeClass
{
public function run(Return_ $return)
{
$this->print($return);
}
public function print(\PhpParser\Node\Stmt\Return_ $return)
{
}
}
?>

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddParamTypeFromCallersRector\Source;
use PhpParser\Node;
interface SomeInterface
{
public function print(Node $node);
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
use Rector\Core\Configuration\Option;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\TypeDeclaration\Rector\ClassMethod\AddParamTypeFromCallersRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$parameters = $containerConfigurator->parameters();
$parameters->set(Option::PHP_VERSION_FEATURES, PhpVersionFeature::UNION_TYPES - 1);
$services = $containerConfigurator->services();
$services->set(AddParamTypeFromCallersRector::class);
};

View File

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Rector\Core\NodeAnalyzer;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp;
use PhpParser\Node\Expr\BooleanNot;
@ -19,21 +18,21 @@ final class CallAnalyzer
*/
private const OBJECT_CALLS = [MethodCall::class, NullsafeMethodCall::class, StaticCall::class];
public function isObjectCall(Node $node): bool
public function isObjectCall(Expr $expr): bool
{
if ($node instanceof BooleanNot) {
$node = $node->expr;
if ($expr instanceof BooleanNot) {
$expr = $expr->expr;
}
if ($node instanceof BinaryOp) {
$isObjectCallLeft = $this->isObjectCall($node->left);
$isObjectCallRight = $this->isObjectCall($node->right);
if ($expr instanceof BinaryOp) {
$isObjectCallLeft = $this->isObjectCall($expr->left);
$isObjectCallRight = $this->isObjectCall($expr->right);
return $isObjectCallLeft || $isObjectCallRight;
}
foreach (self::OBJECT_CALLS as $objectCall) {
if (is_a($node, $objectCall, true)) {
if (is_a($expr, $objectCall, true)) {
return true;
}
}