diff --git a/composer.json b/composer.json index 172d0f6adcd..34055f384ea 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,12 @@ { "name": "rector/rector", "description": "Instant upgrade and refactoring of your PHP code", - "keywords": ["instant upgrades", "instant refactoring", "ast", "automated refactoring"], + "keywords": [ + "instant upgrades", + "instant refactoring", + "ast", + "automated refactoring" + ], "homepage": "https://getrector.org", "license": "MIT", "authors": [ @@ -38,8 +43,8 @@ "sebastian/diff": "^4.0", "symfony/cache": "^4.4.8|^5.1", "symfony/console": "^4.4.8|^5.1", - "symfony/expression-language": "^4.4|^5.1", "symfony/dependency-injection": "^5.1", + "symfony/expression-language": "^4.4|^5.1", "symfony/finder": "^4.4.8|^5.1", "symfony/http-kernel": "^4.4.8|^5.1", "symfony/process": "^4.4.8|^5.1", @@ -49,7 +54,7 @@ "symplify/package-builder": "^9.0.34", "symplify/rule-doc-generator": "^9.0.34", "symplify/set-config-resolver": "^9.0.34", - "symplify/simple-php-doc-parser": "^9.0.34", + "symplify/simple-php-doc-parser": "^9.0", "symplify/skipper": "^9.0.34", "symplify/smart-file-system": "^9.0.34", "symplify/symfony-php-config": "^9.0.34", @@ -176,7 +181,8 @@ "Rector\\Twig\\": "rules/twig/src", "Rector\\TypeDeclaration\\": "rules/type-declaration/src", "Rector\\VendorLocker\\": "packages/vendor-locker/src", - "Rector\\Carbon\\": "rules/carbon/src" + "Rector\\Carbon\\": "rules/carbon/src", + "Rector\\Generics\\": "rules/generics/src" } }, "autoload-dev": { @@ -300,7 +306,8 @@ "Rector\\Utils\\NodeDocumentationGenerator\\Tests\\": "utils/node-documentation-generator/tests", "Rector\\Utils\\PHPStanTypeMapperChecker\\": "utils/phpstan-type-mapper-checker/src", "Rector\\Utils\\ProjectValidator\\": "utils/project-validator/src", - "Rector\\Carbon\\Tests\\": "rules/carbon/tests" + "Rector\\Carbon\\Tests\\": "rules/carbon/tests", + "Rector\\Generics\\Tests\\": "rules/generics/tests" } }, "scripts": { diff --git a/config/set/php71.php b/config/set/php71.php index 353a548e301..08d308dbf75 100644 --- a/config/set/php71.php +++ b/config/set/php71.php @@ -4,7 +4,7 @@ declare(strict_types=1); use Rector\Php71\Rector\Assign\AssignArrayToStringRector; use Rector\Php71\Rector\BinaryOp\BinaryOpBetweenNumberAndStringRector; -use Rector\Php71\Rector\BinaryOp\IsIterableRector; +use Rector\Php71\Rector\BooleanOr\IsIterableRector; use Rector\Php71\Rector\FuncCall\CountOnNullRector; use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector; use Rector\Php71\Rector\List_\ListToArrayDestructRector; diff --git a/config/set/php73.php b/config/set/php73.php index 926157e7f4a..8c2169a7aba 100644 --- a/config/set/php73.php +++ b/config/set/php73.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use Rector\Php73\Rector\BinaryOp\IsCountableRector; +use Rector\Php73\Rector\BooleanOr\IsCountableRector; use Rector\Php73\Rector\ConstFetch\SensitiveConstantNameRector; use Rector\Php73\Rector\FuncCall\ArrayKeyFirstLastRector; use Rector\Php73\Rector\FuncCall\JsonThrowOnErrorRector; diff --git a/config/set/phpunit60.php b/config/set/phpunit60.php index 60e3ac85917..32c7eecbd46 100644 --- a/config/set/phpunit60.php +++ b/config/set/phpunit60.php @@ -2,13 +2,13 @@ declare(strict_types=1); -use Rector\Generic\ValueObject\PseudoNamespaceToNamespace; use Rector\PHPUnit\Rector\ClassMethod\AddDoesNotPerformAssertionToNonAssertingTestRector; use Rector\PHPUnit\Rector\MethodCall\GetMockBuilderGetMockToCreateMockRector; use Rector\Renaming\Rector\FileWithoutNamespace\PseudoNamespaceToNamespaceRector; use Rector\Renaming\Rector\MethodCall\RenameMethodRector; use Rector\Renaming\Rector\Name\RenameClassRector; use Rector\Renaming\ValueObject\MethodCallRename; +use Rector\Renaming\ValueObject\PseudoNamespaceToNamespace; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symplify\SymfonyPhpConfig\ValueObjectInliner; diff --git a/config/set/twig-underscore-to-namespace.php b/config/set/twig-underscore-to-namespace.php index 71b4307bbd2..67748fa0183 100644 --- a/config/set/twig-underscore-to-namespace.php +++ b/config/set/twig-underscore-to-namespace.php @@ -2,9 +2,9 @@ declare(strict_types=1); -use Rector\Generic\ValueObject\PseudoNamespaceToNamespace; use Rector\Renaming\Rector\FileWithoutNamespace\PseudoNamespaceToNamespaceRector; use Rector\Renaming\Rector\Name\RenameClassRector; +use Rector\Renaming\ValueObject\PseudoNamespaceToNamespace; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symplify\SymfonyPhpConfig\ValueObjectInliner; diff --git a/docs/rector_rules_overview.md b/docs/rector_rules_overview.md index d2efb322481..1c339b2489c 100644 --- a/docs/rector_rules_overview.md +++ b/docs/rector_rules_overview.md @@ -12024,7 +12024,7 @@ Changes `count()` on null to safe ternary check Changes `is_array` + Traversable check to `is_iterable` -- class: `Rector\Php71\Rector\BinaryOp\IsIterableRector` +- class: `Rector\Php71\Rector\BooleanOr\IsIterableRector` ```diff -is_array($foo) || $foo instanceof Traversable; @@ -12353,7 +12353,7 @@ Make use of `array_key_first()` and `array_key_last()` Changes `is_array` + Countable check to `is_countable` -- class: `Rector\Php73\Rector\BinaryOp\IsCountableRector` +- class: `Rector\Php73\Rector\BooleanOr\IsCountableRector` ```diff -is_array($foo) || $foo instanceof Countable; @@ -14197,7 +14197,7 @@ Replaces defined Pseudo_Namespaces by Namespace\Ones. - class: `Rector\Renaming\Rector\FileWithoutNamespace\PseudoNamespaceToNamespaceRector` ```php -use Rector\Generic\ValueObject\PseudoNamespaceToNamespace; +use Rector\Renaming\ValueObject\PseudoNamespaceToNamespace; use Rector\Renaming\Rector\FileWithoutNamespace\PseudoNamespaceToNamespaceRector; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symplify\SymfonyPhpConfig\ValueObjectInliner; diff --git a/packages/attribute-aware-php-doc/src/Ast/Type/AttributeAwareGenericTypeNode.php b/packages/attribute-aware-php-doc/src/Ast/Type/AttributeAwareGenericTypeNode.php index 73fed7a05a1..e2638f8874b 100644 --- a/packages/attribute-aware-php-doc/src/Ast/Type/AttributeAwareGenericTypeNode.php +++ b/packages/attribute-aware-php-doc/src/Ast/Type/AttributeAwareGenericTypeNode.php @@ -12,4 +12,13 @@ use Rector\BetterPhpDocParser\Contract\PhpDocNode\TypeAwareTagValueNodeInterface final class AttributeAwareGenericTypeNode extends GenericTypeNode implements AttributeAwareNodeInterface, TypeAwareTagValueNodeInterface { use AttributeTrait; + + public function __toString(): string + { + if ($this->genericTypes !== []) { + return parent::__toString(); + } + + return (string) $this->type; + } } diff --git a/packages/better-php-doc-parser/src/PhpDocInfo/PhpDocInfo.php b/packages/better-php-doc-parser/src/PhpDocInfo/PhpDocInfo.php index b1d9b59679c..0c097be1a99 100644 --- a/packages/better-php-doc-parser/src/PhpDocInfo/PhpDocInfo.php +++ b/packages/better-php-doc-parser/src/PhpDocInfo/PhpDocInfo.php @@ -6,10 +6,12 @@ namespace Rector\BetterPhpDocParser\PhpDocInfo; use PhpParser\Node; use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; @@ -39,12 +41,14 @@ use Rector\StaticTypeMapper\ValueObject\Type\ShortenedObjectType; final class PhpDocInfo { /** - * @var array + * @var array, string> */ private const TAGS_TYPES_TO_NAMES = [ ReturnTagValueNode::class => '@return', ParamTagValueNode::class => '@param', VarTagValueNode::class => '@var', + MethodTagValueNode::class => '@method', + PropertyTagValueNode::class => '@property', ]; /** diff --git a/packages/node-type-resolver/src/PhpDoc/PhpDocTypeRenamer.php b/packages/node-type-resolver/src/PhpDoc/PhpDocTypeRenamer.php index e08841b2c6b..fe64a45377e 100644 --- a/packages/node-type-resolver/src/PhpDoc/PhpDocTypeRenamer.php +++ b/packages/node-type-resolver/src/PhpDoc/PhpDocTypeRenamer.php @@ -11,7 +11,7 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Type\ObjectType; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; -use Rector\Generic\ValueObject\PseudoNamespaceToNamespace; +use Rector\Renaming\ValueObject\PseudoNamespaceToNamespace; use Rector\StaticTypeMapper\StaticTypeMapper; use Symplify\SimplePhpDocParser\PhpDocNodeTraverser; diff --git a/packages/phpstan-static-type-mapper/src/TypeMapper/ObjectTypeMapper.php b/packages/phpstan-static-type-mapper/src/TypeMapper/ObjectTypeMapper.php index cb01cb08d7a..805e3842df6 100644 --- a/packages/phpstan-static-type-mapper/src/TypeMapper/ObjectTypeMapper.php +++ b/packages/phpstan-static-type-mapper/src/TypeMapper/ObjectTypeMapper.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Rector\PHPStanStaticTypeMapper\TypeMapper; +use Nette\Utils\Strings; use PhpParser\Node; use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified; @@ -50,7 +51,12 @@ final class ObjectTypeMapper implements TypeMapperInterface, PHPStanStaticTypeMa } if ($type instanceof GenericObjectType) { - $identifierTypeNode = new IdentifierTypeNode('\\' . $type->getClassName()); + if (Strings::contains($type->getClassName(), '\\')) { + $name = '\\' . $type->getClassName(); + } else { + $name = $type->getClassName(); + } + $identifierTypeNode = new IdentifierTypeNode($name); $genericTypeNodes = []; foreach ($type->getTypes() as $genericType) { diff --git a/packages/phpstan-static-type-mapper/src/TypeMapper/ObjectWithoutClassTypeMapper.php b/packages/phpstan-static-type-mapper/src/TypeMapper/ObjectWithoutClassTypeMapper.php index cb4d7a0638b..3645a02232e 100644 --- a/packages/phpstan-static-type-mapper/src/TypeMapper/ObjectWithoutClassTypeMapper.php +++ b/packages/phpstan-static-type-mapper/src/TypeMapper/ObjectWithoutClassTypeMapper.php @@ -7,9 +7,11 @@ namespace Rector\PHPStanStaticTypeMapper\TypeMapper; use PhpParser\Node; use PhpParser\Node\Name; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Type\Generic\TemplateObjectWithoutClassType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use Rector\AttributeAwarePhpDoc\Ast\Type\AttributeAwareGenericTypeNode; use Rector\AttributeAwarePhpDoc\Ast\Type\AttributeAwareIdentifierTypeNode; use Rector\Core\Php\PhpVersionProvider; use Rector\Core\ValueObject\PhpVersionFeature; @@ -44,6 +46,11 @@ final class ObjectWithoutClassTypeMapper implements TypeMapperInterface, PHPStan */ public function mapToPHPStanPhpDocTypeNode(Type $type): TypeNode { + if ($type instanceof TemplateObjectWithoutClassType) { + $attributeAwareIdentifierTypeNode = new AttributeAwareIdentifierTypeNode($type->getName()); + return new AttributeAwareGenericTypeNode($attributeAwareIdentifierTypeNode, []); + } + return new AttributeAwareIdentifierTypeNode('object'); } diff --git a/packages/static-type-mapper/src/Mapper/ScalarStringToTypeMapper.php b/packages/static-type-mapper/src/Mapper/ScalarStringToTypeMapper.php index e8f673044e2..e1491474e27 100644 --- a/packages/static-type-mapper/src/Mapper/ScalarStringToTypeMapper.php +++ b/packages/static-type-mapper/src/Mapper/ScalarStringToTypeMapper.php @@ -23,7 +23,7 @@ use Rector\StaticTypeMapper\ValueObject\Type\FalseBooleanType; final class ScalarStringToTypeMapper { /** - * @var string[][] + * @var array, string[]> */ private const SCALAR_NAME_BY_TYPE = [ StringType::class => ['string'], diff --git a/packages/static-type-mapper/src/PhpDoc/PhpDocTypeMapper.php b/packages/static-type-mapper/src/PhpDoc/PhpDocTypeMapper.php index 514109c0594..a35f4cdcd6b 100644 --- a/packages/static-type-mapper/src/PhpDoc/PhpDocTypeMapper.php +++ b/packages/static-type-mapper/src/PhpDoc/PhpDocTypeMapper.php @@ -46,7 +46,6 @@ final class PhpDocTypeMapper } // fallback to PHPStan resolver - return $this->typeNodeResolver->resolve($typeNode, $nameScope); } } diff --git a/rules/generic/src/Contract/IsAbleFuncCallInterface.php b/rules/generic/src/Contract/IsAbleFuncCallInterface.php deleted file mode 100644 index a4779fe5181..00000000000 --- a/rules/generic/src/Contract/IsAbleFuncCallInterface.php +++ /dev/null @@ -1,14 +0,0 @@ -services(); + + $services->defaults() + ->public() + ->autowire() + ->autoconfigure(); + + $services->load('Rector\Generics\\', __DIR__ . '/../src') + ->exclude([__DIR__ . '/../src/Rector']); +}; diff --git a/rules/generics/src/Rector/Class_/GenericsPHPStormMethodAnnotationRector.php b/rules/generics/src/Rector/Class_/GenericsPHPStormMethodAnnotationRector.php new file mode 100644 index 00000000000..87e28e60244 --- /dev/null +++ b/rules/generics/src/Rector/Class_/GenericsPHPStormMethodAnnotationRector.php @@ -0,0 +1,156 @@ +classGenericMethodResolver = $classGenericMethodResolver; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Complete PHPStorm @method annotations, to make it understand the PHPStan/Psalm generics', + [ + new CodeSample( + <<<'CODE_SAMPLE' +/** + * @template TEntity as object + */ +abstract class AbstractRepository +{ + /** + * @return TEntity + */ + public function find($id) + { + } +} + +/** + * @template TEntity as SomeObject + * @extends AbstractRepository + */ +final class AndroidDeviceRepository extends AbstractRepository +{ +} +CODE_SAMPLE + + , + <<<'CODE_SAMPLE' +/** + * @template TEntity as object + */ +abstract class AbstractRepository +{ + /** + * @return TEntity + */ + public function find($id) + { + } +} + +/** + * @template TEntity as SomeObject + * @extends AbstractRepository + * @method SomeObject find($id) + */ +final class AndroidDeviceRepository extends AbstractRepository +{ +} +CODE_SAMPLE + ), + ]); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if ($node->extends === null) { + return null; + } + + $scope = $node->getAttribute(AttributeKey::SCOPE); + if (! $scope instanceof Scope) { + return null; + } + + $classReflection = $scope->getClassReflection(); + if (! $classReflection instanceof ClassReflection) { + return null; + } + + $parentClassReflection = $classReflection->getParentClass(); + if (! $parentClassReflection instanceof ClassReflection) { + return null; + } + + if (! $parentClassReflection->isGeneric()) { + return null; + } + + // resolve generic method from parent + $methodTagValueNodes = $this->classGenericMethodResolver->resolveFromClass($parentClassReflection); + + $templateTypeMap = $classReflection->getTemplateTypeMap(); + + // @todo replace TTypes with specific types + foreach ($methodTagValueNodes as $methodTagValueNode) { + if ($methodTagValueNode->returnType === null) { + continue; + } + + $returnType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType( + $methodTagValueNode->returnType, + $node + ); + + $resolvedType = TemplateTypeHelper::resolveTemplateTypes($returnType, $templateTypeMap); + $resolvedTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($resolvedType); + $methodTagValueNode->returnType = $resolvedTypeNode; + } + + $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); + foreach ($methodTagValueNodes as $methodTagValueNode) { + $phpDocInfo->addTagValueNode($methodTagValueNode); + } + + return $node; + } +} diff --git a/rules/generics/src/Reflection/ClassGenericMethodResolver.php b/rules/generics/src/Reflection/ClassGenericMethodResolver.php new file mode 100644 index 00000000000..60ce68c9590 --- /dev/null +++ b/rules/generics/src/Reflection/ClassGenericMethodResolver.php @@ -0,0 +1,91 @@ +simplePhpDocParser = $simplePhpDocParser; + $this->methodTagValueNodeFactory = $methodTagValueNodeFactory; + } + + /** + * @return MethodTagValueNode[] + */ + public function resolveFromClass(ClassReflection $classReflection): array + { + $methodTagValueNodes = []; + + $templateNames = array_keys($classReflection->getTemplateTags()); + + foreach ($classReflection->getNativeMethods() as $methodReflection) { + $parentMethodDocComment = $methodReflection->getDocComment(); + if ($parentMethodDocComment === null) { + continue; + } + + // how to parse? + $parentMethodSimplePhpDocNode = $this->simplePhpDocParser->parseDocBlock($parentMethodDocComment); + $methodTagValueNode = $this->resolveMethodTagValueNode( + $parentMethodSimplePhpDocNode, + $templateNames, + $methodReflection + ); + if (! $methodTagValueNode instanceof MethodTagValueNode) { + continue; + } + + $methodTagValueNodes[] = $methodTagValueNode; + } + + return $methodTagValueNodes; + } + + /** + * @param string[] $templateNames + */ + private function resolveMethodTagValueNode( + SimplePhpDocNode $simplePhpDocNode, + array $templateNames, + MethodReflection $methodReflection + ): ?MethodTagValueNode { + foreach ($simplePhpDocNode->getReturnTagValues() as $returnTagValueNode) { + foreach ($templateNames as $templateName) { + $typeAsString = (string) $returnTagValueNode->type; + if (! Strings::match($typeAsString, '#\b' . preg_quote($templateName, '#') . '\b#')) { + continue; + } + + return $this->methodTagValueNodeFactory->createFromMethodReflectionAndReturnTagValueNode( + $methodReflection, + $returnTagValueNode + ); + } + } + + return null; + } +} diff --git a/rules/generics/src/TagValueNodeFactory/MethodTagValueNodeFactory.php b/rules/generics/src/TagValueNodeFactory/MethodTagValueNodeFactory.php new file mode 100644 index 00000000000..4cdaba586b5 --- /dev/null +++ b/rules/generics/src/TagValueNodeFactory/MethodTagValueNodeFactory.php @@ -0,0 +1,85 @@ +methodTagValueParameterNodeFactory = $methodTagValueParameterNodeFactory; + $this->staticTypeMapper = $staticTypeMapper; + } + + public function createFromMethodReflectionAndReturnTagValueNode( + MethodReflection $methodReflection, + ReturnTagValueNode $returnTagValueNode + ): MethodTagValueNode { + $parameterReflections = $methodReflection->getVariants()[0] + ->getParameters(); + + $stringParameters = $this->resolveStringParameters($parameterReflections); + + $classReflection = $methodReflection->getDeclaringClass(); + $templateTypeMap = $classReflection->getTemplateTypeMap(); + + $returnTagTypeNode = $returnTagValueNode->type; + + if ($returnTagValueNode->type instanceof IdentifierTypeNode) { + $typeName = $returnTagValueNode->type->name; + $genericType = $templateTypeMap->getType($typeName); + + if ($genericType instanceof Type) { + $returnTagType = $genericType; + $returnTagTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($returnTagType); + } + } + + return new MethodTagValueNode( + false, + $returnTagTypeNode, + $methodReflection->getName(), + $stringParameters, + '' + ); + } + + /** + * @param ParameterReflection[] $parameterReflections + * @return MethodTagValueParameterNode[] + */ + private function resolveStringParameters(array $parameterReflections): array + { + $stringParameters = []; + + foreach ($parameterReflections as $parameterReflection) { + $stringParameters[] = $this->methodTagValueParameterNodeFactory->createFromParamReflection( + $parameterReflection + ); + } + + return $stringParameters; + } +} diff --git a/rules/generics/src/TagValueNodeFactory/MethodTagValueParameterNodeFactory.php b/rules/generics/src/TagValueNodeFactory/MethodTagValueParameterNodeFactory.php new file mode 100644 index 00000000000..2383ee6e0e2 --- /dev/null +++ b/rules/generics/src/TagValueNodeFactory/MethodTagValueParameterNodeFactory.php @@ -0,0 +1,43 @@ +staticTypeMapper = $staticTypeMapper; + } + + public function createFromParamReflection(ParameterReflection $parameterReflection): MethodTagValueParameterNode + { + $parameterType = $parameterReflection->getType(); + if ($parameterType instanceof MixedType && ! $parameterType->isExplicitMixed()) { + $parameterTypeNode = null; + } else { + $parameterTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($parameterType); + } + + return new MethodTagValueParameterNode( + $parameterTypeNode, + $parameterReflection->passedByReference() + ->yes(), + $parameterReflection->isVariadic(), + '$' . $parameterReflection->getName(), + // @todo resolve + null + ); + } +} diff --git a/rules/generics/tests/Rector/Class_/GenericsPHPStormMethodAnnotationRector/Fixture/some_class.php.inc b/rules/generics/tests/Rector/Class_/GenericsPHPStormMethodAnnotationRector/Fixture/some_class.php.inc new file mode 100644 index 00000000000..1e11f27f25c --- /dev/null +++ b/rules/generics/tests/Rector/Class_/GenericsPHPStormMethodAnnotationRector/Fixture/some_class.php.inc @@ -0,0 +1,62 @@ + + */ +final class AndroidDeviceRepository extends AbstractRepository +{ +} + +?> +----- + + * @method RealObject find($id) + */ +final class AndroidDeviceRepository extends AbstractRepository +{ +} + +?> diff --git a/rules/generics/tests/Rector/Class_/GenericsPHPStormMethodAnnotationRector/GenericsPHPStormMethodAnnotationRectorTest.php b/rules/generics/tests/Rector/Class_/GenericsPHPStormMethodAnnotationRector/GenericsPHPStormMethodAnnotationRectorTest.php new file mode 100644 index 00000000000..81992d6e105 --- /dev/null +++ b/rules/generics/tests/Rector/Class_/GenericsPHPStormMethodAnnotationRector/GenericsPHPStormMethodAnnotationRectorTest.php @@ -0,0 +1,31 @@ +doTestFileInfo($fileInfo); + } + + public function provideData(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + protected function getRectorClass(): string + { + return GenericsPHPStormMethodAnnotationRector::class; + } +} diff --git a/rules/generics/tests/Rector/Class_/GenericsPHPStormMethodAnnotationRector/Source/RealObject.php b/rules/generics/tests/Rector/Class_/GenericsPHPStormMethodAnnotationRector/Source/RealObject.php new file mode 100644 index 00000000000..f5310f2f931 --- /dev/null +++ b/rules/generics/tests/Rector/Class_/GenericsPHPStormMethodAnnotationRector/Source/RealObject.php @@ -0,0 +1,10 @@ +isArrayAndDualCheckToAble = $isArrayAndDualCheckToAble; } + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Changes is_array + Traversable check to is_iterable', + [new CodeSample('is_array($foo) || $foo instanceof Traversable;', 'is_iterable($foo);')]); + } + /** * @return string[] */ @@ -39,19 +51,15 @@ abstract class AbstractIsAbleFunCallRector extends AbstractRector implements IsA return null; } - return $this->isArrayAndDualCheckToAble->processBooleanOr( - $node, - $this->getType(), - $this->getFuncName() - ) ?: $node; + return $this->isArrayAndDualCheckToAble->processBooleanOr($node, 'Traversable', 'is_iterable') ?: $node; } private function shouldSkip(): bool { - if (function_exists($this->getFuncName())) { + if (function_exists('is_iterable')) { return false; } - return $this->isAtLeastPhpVersion($this->getPhpVersion()); + return ! $this->isAtLeastPhpVersion(PhpVersionFeature::IS_ITERABLE); } } diff --git a/rules/php71/tests/Rector/BinaryOp/IsIterableRector/Fixture/fixture.php.inc b/rules/php71/tests/Rector/BooleanOr/IsIterableRector/Fixture/fixture.php.inc similarity index 77% rename from rules/php71/tests/Rector/BinaryOp/IsIterableRector/Fixture/fixture.php.inc rename to rules/php71/tests/Rector/BooleanOr/IsIterableRector/Fixture/fixture.php.inc index 3b1a8fc8844..61036d1f882 100644 --- a/rules/php71/tests/Rector/BinaryOp/IsIterableRector/Fixture/fixture.php.inc +++ b/rules/php71/tests/Rector/BooleanOr/IsIterableRector/Fixture/fixture.php.inc @@ -1,6 +1,6 @@ isArrayAndDualCheckToAble = $isArrayAndDualCheckToAble; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Changes is_array + Countable check to is_countable', + [ + new CodeSample( + <<<'CODE_SAMPLE' +is_array($foo) || $foo instanceof Countable; +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +is_countable($foo); +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [BooleanOr::class]; + } + + /** + * @param BooleanOr $node + */ + public function refactor(Node $node): ?Node + { + if ($this->shouldSkip()) { + return null; + } + + return $this->isArrayAndDualCheckToAble->processBooleanOr($node, 'Countable', 'is_countable') ?: $node; + } + + private function shouldSkip(): bool + { + if (function_exists('is_countable')) { + return false; + } + + return $this->isAtLeastPhpVersion(PhpVersionFeature::IS_COUNTABLE); + } +} diff --git a/rules/php73/tests/Rector/BinaryOp/IsCountableRector/Fixture/fixture73.php.inc b/rules/php73/tests/Rector/BinaryOr/IsCountableRector/Fixture/fixture73.php.inc similarity index 66% rename from rules/php73/tests/Rector/BinaryOp/IsCountableRector/Fixture/fixture73.php.inc rename to rules/php73/tests/Rector/BinaryOr/IsCountableRector/Fixture/fixture73.php.inc index 63c82f08d8c..c42d28bb526 100644 --- a/rules/php73/tests/Rector/BinaryOp/IsCountableRector/Fixture/fixture73.php.inc +++ b/rules/php73/tests/Rector/BinaryOr/IsCountableRector/Fixture/fixture73.php.inc @@ -1,6 +1,6 @@