[PHP 8.1] Add ReturnNeverTypeRector (#6283)

This commit is contained in:
Tomas Votruba 2021-05-02 12:46:55 +02:00 committed by GitHub
parent bbba5304ad
commit 2037d55d6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 579 additions and 46 deletions

11
config/set/php81.php Normal file
View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(ReturnNeverTypeRector::class);
};

View File

@ -573,6 +573,11 @@ final class SetList implements SetListInterface
*/
public const PHP_80 = __DIR__ . '/../../../config/set/php80.php';
/**
* @var string
*/
public const PHP_81 = __DIR__ . '/../../../config/set/php81.php';
/**
* @var string
*/

View File

@ -0,0 +1,29 @@
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector\Fixture;
final class DieSome
{
public function run()
{
echo 100;
die;
}
}
?>
-----
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector\Fixture;
final class DieSome
{
public function run(): never
{
echo 100;
die;
}
}
?>

View File

@ -0,0 +1,29 @@
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector\Fixture;
final class ExitSome
{
public function run()
{
echo 100;
exit;
}
}
?>
-----
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector\Fixture;
final class ExitSome
{
public function run(): never
{
echo 100;
exit;
}
}
?>

View File

@ -0,0 +1,31 @@
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector\Fixture;
use Rector\Core\Exception\ShouldNotHappenException;
final class ImproveVoid
{
public function run(): void
{
throw new ShouldNotHappenException();
}
}
?>
-----
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector\Fixture;
use Rector\Core\Exception\ShouldNotHappenException;
final class ImproveVoid
{
public function run(): never
{
throw new ShouldNotHappenException();
}
}
?>

View File

@ -0,0 +1,9 @@
<?php
function run($key)
{
if ($key) {
echo 100;
exit;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector\Fixture;
final class SkipNeverAlready
{
public function run(): never
{
throw new InvalidException();
}
}

View File

@ -0,0 +1,16 @@
<?php
use Rector\Core\Exception\ShouldNotHappenException;
final class SkipParentProtected implements SomeInterfaceWithReturnType
{
public function run()
{
throw new ShouldNotHappenException();
}
}
interface SomeInterfaceWithReturnType
{
public function run(): void;
}

View File

@ -0,0 +1,11 @@
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector\Fixture;
final class SkipReturn
{
public function run()
{
return;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector\Fixture;
final class SkipYield
{
public function run()
{
yield 1;
exit();
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector\Fixture;
final class SomeClass
{
public function run()
{
throw new InvalidException();
}
}
?>
-----
<?php
namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector\Fixture;
final class SomeClass
{
public function run(): never
{
throw new InvalidException();
}
}
?>

View File

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

View File

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

View File

@ -97,6 +97,9 @@ CODE_SAMPLE
return $node;
}
/**
* @param array<string, ArgumentDefaultValueReplacer[]> $configuration
*/
public function configure(array $configuration): void
{
$replacedArguments = $configuration[self::REPLACED_ARGUMENTS] ?? [];

View File

@ -95,6 +95,9 @@ CODE_SAMPLE
return $node;
}
/**
* @param array<string, SwapFuncCallArguments[]> $configuration
*/
public function configure(array $configuration): void
{
$functionArgumentSwaps = $configuration[self::FUNCTION_ARGUMENT_SWAPS] ?? [];

View File

@ -30,6 +30,42 @@ final class ParentClassMethodTypeOverrideGuard
$this->nodeNameResolver = $nodeNameResolver;
}
public function hasParentMethodOutsideVendor(ClassMethod $classMethod): bool
{
$scope = $classMethod->getAttribute(AttributeKey::SCOPE);
if (! $scope instanceof Scope) {
return false;
}
$classReflection = $scope->getClassReflection();
if (! $classReflection instanceof ClassReflection) {
return false;
}
$methodName = $classMethod->name->toString();
foreach ($classReflection->getAncestors() as $ancestorClassReflection) {
if ($classReflection === $ancestorClassReflection) {
continue;
}
if (! $ancestorClassReflection->hasMethod($methodName)) {
continue;
}
$parentClassMethodReflection = $ancestorClassReflection->getMethod($methodName, $scope);
$parentClassMethod = $this->nodeRepository->findClassMethodByMethodReflection(
$parentClassMethodReflection
);
if (! $parentClassMethod instanceof ClassMethod) {
return true;
}
}
return false;
}
public function isReturnTypeChangeAllowed(ClassMethod $classMethod): bool
{
// make sure return type is not protected by parent contract
@ -45,7 +81,11 @@ final class ParentClassMethodTypeOverrideGuard
);
// if null, we're unable to override → skip it
return $parentClassMethod !== null;
if (! $parentClassMethod instanceof ClassMethod) {
return true;
}
return $parentClassMethod->returnType === null;
}
private function getParentClassMethod(ClassMethod $classMethod): ?MethodReflection
@ -63,7 +103,11 @@ final class ParentClassMethodTypeOverrideGuard
return null;
}
foreach ($classReflection->getParents() as $parentClassReflection) {
foreach ($classReflection->getAncestors() as $parentClassReflection) {
if ($classReflection === $parentClassReflection) {
continue;
}
if (! $parentClassReflection->hasMethod($methodName)) {
continue;
}

View File

@ -119,6 +119,9 @@ CODE_SAMPLE
return $node;
}
/**
* @param array<string, NormalToFluent[]> $configuration
*/
public function configure(array $configuration): void
{
$callsToFluent = $configuration[self::CALLS_TO_FLUENT] ?? [];

View File

@ -141,7 +141,7 @@ CODE_SAMPLE
private function shouldSkip(Return_ $return, ClassMethod $classMethod): bool
{
if (! $this->parentClassMethodTypeOverrideGuard->isReturnTypeChangeAllowed($classMethod)) {
if ($this->parentClassMethodTypeOverrideGuard->hasParentMethodOutsideVendor($classMethod)) {
return true;
}

View File

@ -30,7 +30,7 @@ final class AddMethodParentCallRector extends AbstractRector implements Configur
/**
* @var array<string, string>
*/
private $methodsByParentTypes = [];
private $methodByParentTypes = [];
public function getRuleDefinition(): RuleDefinition
{
@ -90,7 +90,7 @@ CODE_SAMPLE
/** @var string $className */
$className = $node->getAttribute(AttributeKey::CLASS_NAME);
foreach ($this->methodsByParentTypes as $type => $method) {
foreach ($this->methodByParentTypes as $type => $method) {
if (! $this->isObjectType($classLike, new ObjectType($type))) {
continue;
}
@ -112,9 +112,12 @@ CODE_SAMPLE
return null;
}
/**
* @param array<string, array<string, string>> $configuration
*/
public function configure(array $configuration): void
{
$this->methodsByParentTypes = $configuration[self::METHODS_BY_PARENT_TYPES] ?? [];
$this->methodByParentTypes = $configuration[self::METHODS_BY_PARENT_TYPES] ?? [];
}
private function shouldSkipMethod(ClassMethod $classMethod, string $method): bool

View File

@ -27,7 +27,7 @@ final class ReservedObjectRector extends AbstractRector implements ConfigurableR
public const RESERVED_KEYWORDS_TO_REPLACEMENTS = 'reserved_keywords_to_replacements';
/**
* @var string[]
* @var array<string, string>
*/
private $reservedKeywordsToReplacements = [];
@ -78,6 +78,9 @@ CODE_SAMPLE
return $this->processName($node);
}
/**
* @param array<string, array<string, string>> $configuration
*/
public function configure(array $configuration): void
{
$this->reservedKeywordsToReplacements = $configuration[self::RESERVED_KEYWORDS_TO_REPLACEMENTS] ?? [];

View File

@ -104,6 +104,9 @@ CODE_SAMPLE
return null;
}
/**
* @param array<string, array<string, string>> $configuration
*/
public function configure(array $configuration): void
{
$this->reservedNamesToNewOnes = $configuration[self::RESERVED_NAMES_TO_NEW_ONES] ?? [];

View File

@ -86,6 +86,9 @@ CODE_SAMPLE
return $node;
}
/**
* @param array<string, ArgumentRemover[]> $configuration
*/
public function configure(array $configuration): void
{
$removedArguments = $configuration[self::REMOVED_ARGUMENTS] ?? [];

View File

@ -22,7 +22,7 @@ final class RemoveInterfacesRector extends AbstractRector implements Configurabl
public const INTERFACES_TO_REMOVE = 'interfaces_to_remove';
/**
* @var string[]
* @var class-string[]
*/
private $interfacesToRemove = [];
@ -75,6 +75,9 @@ CODE_SAMPLE
return $node;
}
/**
* @param array<string, class-string[]> $configuration
*/
public function configure(array $configuration): void
{
$this->interfacesToRemove = $configuration[self::INTERFACES_TO_REMOVE] ?? [];

View File

@ -99,6 +99,9 @@ CODE_SAMPLE
return null;
}
/**
* @param array<string, string[]> $configuration
*/
public function configure(array $configuration): void
{
$this->traitsToRemove = $configuration[self::TRAITS_TO_REMOVE] ?? [];

View File

@ -80,6 +80,9 @@ CODE_SAMPLE
return $node;
}
/**
* @param array<string, RemoveFuncCallArg[]> $configuration
*/
public function configure(array $configuration): void
{
$removedFunctionArguments = $configuration[self::REMOVED_FUNCTION_ARGUMENTS] ?? [];

View File

@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Rector\RemovingStatic\Rector\Class_;
use PhpParser\Node;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\StaticCall;
@ -13,14 +12,11 @@ use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Type\ObjectType;
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger;
use Rector\Core\Contract\Rector\ConfigurableRectorInterface;
use Rector\Core\Rector\AbstractRector;
use Rector\Naming\Naming\PropertyNaming;
use Symplify\Astral\ValueObject\NodeBuilder\MethodBuilder;
use Symplify\Astral\ValueObject\NodeBuilder\ParamBuilder;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
@ -36,7 +32,7 @@ final class StaticTypeToSetterInjectionRector extends AbstractRector implements
public const STATIC_TYPES = 'static_types';
/**
* @var string[]
* @var array<class-string|int, class-string>
*/
private $staticTypes = [];
@ -134,6 +130,9 @@ CODE_SAMPLE
return null;
}
/**
* @param array<string, array<class-string|int, class-string>> $configuration
*/
public function configure(array $configuration): void
{
$this->staticTypes = $configuration[self::STATIC_TYPES] ?? [];
@ -160,14 +159,7 @@ CODE_SAMPLE
}
$variableName = $this->propertyNaming->fqnToVariableName($objectType);
$paramBuilder = new ParamBuilder($variableName);
$paramBuilder->setType(new FullyQualified($staticType));
$param = $paramBuilder->getNode();
$assign = $this->nodeFactory->createPropertyAssignment($variableName);
$setEntityFactoryMethod = $this->createSetEntityFactoryClassMethod($variableName, $param, $assign);
$setEntityFactoryMethod = $this->nodeFactory->createSetterClassMethod($variableName, $objectType);
$entityFactoryProperty = $this->nodeFactory->createPrivateProperty($variableName);
@ -190,20 +182,4 @@ CODE_SAMPLE
return $this->isObjectType($node->class, $objectType);
}
private function createSetEntityFactoryClassMethod(
string $variableName,
Param $param,
Assign $assign
): ClassMethod {
$setMethodName = 'set' . ucfirst($variableName);
$setEntityFactoryMethodBuilder = new MethodBuilder($setMethodName);
$setEntityFactoryMethodBuilder->makePublic();
$setEntityFactoryMethodBuilder->addParam($param);
$setEntityFactoryMethodBuilder->setReturnType('void');
$setEntityFactoryMethodBuilder->addStmt($assign);
return $setEntityFactoryMethodBuilder->getNode();
}
}

View File

@ -117,6 +117,9 @@ CODE_SAMPLE
return $node;
}
/**
* @param array<string, RenameAnnotation[]> $configuration
*/
public function configure(array $configuration): void
{
$renamedAnnotationsInTypes = $configuration[self::RENAMED_ANNOTATIONS_IN_TYPES] ?? [];

View File

@ -122,6 +122,9 @@ CODE_SAMPLE
return null;
}
/**
* @param array<string, PseudoNamespaceToNamespace[]> $configuration
*/
public function configure(array $configuration): void
{
$namespacePrefixesWithExcludedClasses = $configuration[self::NAMESPACE_PREFIXES_WITH_EXCLUDED_CLASSES] ?? [];

View File

@ -73,6 +73,9 @@ final class RenamePropertyRector extends AbstractRector implements ConfigurableR
return null;
}
/**
* @param array<string, RenameProperty[]> $configuration
*/
public function configure(array $configuration): void
{
$renamedProperties = $configuration[self::RENAMED_PROPERTIES] ?? [];

View File

@ -13,6 +13,7 @@ use Rector\Core\Rector\AbstractRector;
use Rector\Renaming\ValueObject\RenameStaticMethod;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
use Webmozart\Assert\Assert;
/**
* @see \Rector\Tests\Renaming\Rector\StaticCall\RenameStaticMethodRector\RenameStaticMethodRectorTest
@ -90,9 +91,14 @@ final class RenameStaticMethodRector extends AbstractRector implements Configura
return null;
}
/**
* @param array<string, RenameStaticMethod[]> $configuration
*/
public function configure(array $configuration): void
{
$this->staticMethodRenames = $configuration[self::OLD_TO_NEW_METHODS_BY_CLASSES] ?? [];
$oldToNewMethodsByClasses = $configuration[self::OLD_TO_NEW_METHODS_BY_CLASSES];
Assert::allIsInstanceOf($oldToNewMethodsByClasses, RenameStaticMethod::class);
$this->staticMethodRenames = $oldToNewMethodsByClasses;
}
private function rename(StaticCall $staticCall, RenameStaticMethod $renameStaticMethod): StaticCall

View File

@ -127,6 +127,9 @@ CODE_SAMPLE
return $node;
}
/**
* @param array<string, array<class-string, class-string>> $configuration
*/
public function configure(array $configuration): void
{
$this->classToInstantiateByType = $configuration[self::CLASS_TO_INSTANTIATE_BY_TYPE] ?? [];

View File

@ -112,6 +112,9 @@ CODE_SAMPLE
return new MethodCall($arrayDimFetch->var, $dimFetchAssignToMethodCall->getAddMethod(), $node->expr->args);
}
/**
* @param array<string, DimFetchAssignToMethodCall[]> $configuration
*/
public function configure(array $configuration): void
{
$dimFetchAssignToMethodCalls = $configuration[self::DIM_FETCH_ASSIGN_TO_METHOD_CALL] ?? [];

View File

@ -94,6 +94,9 @@ CODE_SAMPLE
return $node;
}
/**
* @param array<string, PropertyAssignToMethodCall[]> $configuration
*/
public function configure(array $configuration): void
{
$propertyAssignsToMethodCalls = $configuration[self::PROPERTY_ASSIGNS_TO_METHODS_CALLS] ?? [];

View File

@ -94,6 +94,9 @@ CODE_SAMPLE
return $node;
}
/**
* @param array<string, WrapReturn[]> $configuration
*/
public function configure(array $configuration): void
{
$typeMethodWraps = $configuration[self::TYPE_METHOD_WRAPS] ?? [];

View File

@ -28,7 +28,7 @@ final class MergeInterfacesRector extends AbstractRector implements Configurable
public const OLD_TO_NEW_INTERFACES = 'old_to_new_interfaces';
/**
* @var string[]
* @var array<string, string>
*/
private $oldToNewInterfaces = [];
@ -89,6 +89,9 @@ CODE_SAMPLE
return $node;
}
/**
* @param array<string, array<string, string>> $configuration
*/
public function configure(array $configuration): void
{
$this->oldToNewInterfaces = $configuration[self::OLD_TO_NEW_INTERFACES] ?? [];

View File

@ -95,6 +95,9 @@ CODE_SAMPLE
return $this->refactorMethodCall($methodCall);
}
/**
* @param array<string, MethodCallToReturn[]> $configuration
*/
public function configure(array $configuration): void
{
$methodCallWraps = $configuration[self::METHOD_CALL_WRAPS] ?? [];

View File

@ -86,6 +86,9 @@ CODE_SAMPLE
return new ConstFetch(new Name($this->functionsToConstants[$functionName]));
}
/**
* @param array<string, string[]> $configuration
*/
public function configure(array $configuration): void
{
$this->functionsToConstants = $configuration[self::FUNCTIONS_TO_CONSTANTS] ?? [];

View File

@ -71,6 +71,9 @@ final class FuncCallToStaticCallRector extends AbstractRector implements Configu
return null;
}
/**
* @param array<string, FuncCallToStaticCall[]> $configuration
*/
public function configure(array $configuration): void
{
$funcCallsToStaticCalls = $configuration[self::FUNC_CALLS_TO_STATIC_CALLS] ?? [];

View File

@ -88,6 +88,9 @@ CODE_SAMPLE
return null;
}
/**
* @param array<string, UnsetAndIssetToMethodCall[]> $configuration
*/
public function configure(array $configuration): void
{
$issetUnsetToMethodCalls = $configuration[self::ISSET_UNSET_TO_METHOD_CALL] ?? [];

View File

@ -84,6 +84,9 @@ CODE_SAMPLE
return null;
}
/**
* @param array<string, MethodCallToAnotherMethodCallWithArguments[]> $configuration
*/
public function configure(array $configuration): void
{
$methodCallRenamesWithAddedArguments = $configuration[self::METHOD_CALL_RENAMES_WITH_ADDED_ARGUMENTS] ?? [];

View File

@ -84,6 +84,9 @@ CODE_SAMPLE
return null;
}
/**
* @param array<string, array<string, string>> $configuration
*/
public function configure(array $configuration): void
{
$this->methodCallToPropertyFetchCollection = $configuration[self::METHOD_CALL_TO_PROPERTY_FETCHES] ?? [];

View File

@ -107,6 +107,9 @@ CODE_SAMPLE
return null;
}
/**
* @param array<string, MethodCallToStaticCall[]> $configuration
*/
public function configure(array $configuration): void
{
$methodCallsToStaticCalls = $configuration[self::METHOD_CALLS_TO_STATIC_CALLS] ?? [];

View File

@ -91,6 +91,9 @@ CODE_SAMPLE
return null;
}
/**
* @param array<string, ReplaceParentCallByPropertyCall[]> $configuration
*/
public function configure(array $configuration): void
{
$this->parentCallToProperties = $configuration[self::PARENT_CALLS_TO_PROPERTIES] ?? [];

View File

@ -176,6 +176,9 @@ CODE_SAMPLE
return $node;
}
/**
* @param array<string, ServiceGetterToConstructorInjection[]> $configuration
*/
public function configure(array $configuration): void
{
$methodCallToServices = $configuration[self::METHOD_CALL_TO_SERVICES] ?? [];

View File

@ -87,6 +87,9 @@ CODE_SAMPLE
return null;
}
/**
* @param array<string, NewToStaticCall[]> $configuration
*/
public function configure(array $configuration): void
{
$typeToStaticCalls = $configuration[self::TYPE_TO_STATIC_CALLS] ?? [];

View File

@ -81,6 +81,9 @@ final class StaticCallToFuncCallRector extends AbstractRector implements Configu
return null;
}
/**
* @param array<string, StaticCallToFuncCall[]> $configuration
*/
public function configure(array $configuration): void
{
$staticCallsToFunctions = $configuration[self::STATIC_CALLS_TO_FUNCTIONS] ?? [];

View File

@ -88,6 +88,9 @@ CODE_SAMPLE
return $node;
}
/**
* @param array<string, StringToClassConstant[]> $configuration
*/
public function configure(array $configuration): void
{
$stringToClassConstants = $configuration[self::STRINGS_TO_CLASS_CONSTANTS] ?? [];

View File

@ -104,6 +104,9 @@ CODE_SAMPLE
return null;
}
/**
* @param array<string, AddReturnTypeDeclaration[]> $configuration
*/
public function configure(array $configuration): void
{
$methodReturnTypes = $configuration[self::METHOD_RETURN_TYPES] ?? [];

View File

@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace Rector\TypeDeclaration\Rector\ClassMethod;
use PhpParser\Node;
use PhpParser\Node\Expr\Yield_;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Node\Stmt\Throw_;
use PHPStan\Type\NeverType;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\Defluent\ConflictGuard\ParentClassMethodTypeOverrideGuard;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
/**
* @changelog https://wiki.php.net/rfc/noreturn_type
*
* @see \Rector\Tests\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector\ReturnNeverTypeRectorTest
*/
final class ReturnNeverTypeRector extends AbstractRector
{
/**
* @var ParentClassMethodTypeOverrideGuard
*/
private $parentClassMethodTypeOverrideGuard;
public function __construct(ParentClassMethodTypeOverrideGuard $parentClassMethodTypeOverrideGuard)
{
$this->parentClassMethodTypeOverrideGuard = $parentClassMethodTypeOverrideGuard;
}
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Add "never" type for methods that never return anything', [
new CodeSample(
<<<'CODE_SAMPLE'
final class SomeClass
{
public function run()
{
throw new InvalidException();
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
final class SomeClass
{
public function run(): never
{
throw new InvalidException();
}
}
CODE_SAMPLE
),
]);
}
/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [ClassMethod::class, Function_::class];
}
/**
* @param ClassMethod|Function_ $node
*/
public function refactor(Node $node): ?Node
{
if (! $this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::NEVER_TYPE)) {
return null;
}
$returns = $this->betterNodeFinder->findInstanceOf($node, Return_::class);
if ($returns !== []) {
return null;
}
$notNeverNodes = $this->betterNodeFinder->findInstancesOf($node, [Yield_::class]);
if ($notNeverNodes !== []) {
return null;
}
$neverNodes = $this->betterNodeFinder->findInstancesOf($node, [Node\Expr\Throw_::class, Throw_::class]);
$hasNeverFuncCall = $this->resolveHasNeverFuncCall($node);
if ($neverNodes === [] && ! $hasNeverFuncCall) {
return null;
}
if ($node instanceof ClassMethod && ! $this->parentClassMethodTypeOverrideGuard->isReturnTypeChangeAllowed(
$node
)) {
return null;
}
$node->returnType = new Name('never');
return $node;
}
/**
* @param ClassMethod|Function_ $functionLike
*/
private function resolveHasNeverFuncCall(FunctionLike $functionLike): bool
{
$hasNeverType = false;
foreach ((array) $functionLike->stmts as $stmt) {
if ($stmt instanceof Expression) {
$stmt = $stmt->expr;
}
if ($stmt instanceof Stmt) {
continue;
}
$stmtType = $this->getStaticType($stmt);
if ($stmtType instanceof NeverType) {
$hasNeverType = true;
}
}
return $hasNeverType;
}
}

View File

@ -122,6 +122,23 @@ final class BetterNodeFinder
return null;
}
/**
* @template T of Node
* @param array<class-string<T>> $types
* @param Node|Node[]|Stmt[] $nodes
* @return T[]
*/
public function findInstancesOf($nodes, array $types): array
{
$foundInstances = [];
foreach ($types as $type) {
$currentFoundInstances = $this->findInstanceOf($nodes, $type);
$foundInstances = array_merge($foundInstances, $currentFoundInstances);
}
return $foundInstances;
}
/**
* @template T of Node
* @param class-string<T> $type

View File

@ -400,9 +400,7 @@ final class NodeFactory
public function createGetterClassMethod(string $propertyName, Type $type): ClassMethod
{
$getterMethod = 'get' . ucfirst($propertyName);
$methodBuilder = new MethodBuilder($getterMethod);
$methodBuilder = new MethodBuilder('get' . ucfirst($propertyName));
$methodBuilder->makePublic();
$propertyFetch = new PropertyFetch(new Variable(self::THIS), $propertyName);
@ -420,13 +418,13 @@ final class NodeFactory
public function createSetterClassMethod(string $propertyName, Type $type): ClassMethod
{
$getterMethod = 'set' . ucfirst($propertyName);
$methodBuilder = new MethodBuilder('set' . ucfirst($propertyName));
$methodBuilder->makePublic();
$variable = new Variable($propertyName);
$methodBuilder = new MethodBuilder($getterMethod);
$methodBuilder->makePublic();
$methodBuilder->addParam(new Param($variable));
$param = $this->createParamWithType($variable, $type);
$methodBuilder->addParam($param);
$propertyFetch = new PropertyFetch(new Variable(self::THIS), $propertyName);
$assign = new Assign($propertyFetch, $variable);
@ -753,4 +751,13 @@ final class NodeFactory
/** @var BooleanAnd $booleanAnd */
return $booleanAnd;
}
private function createParamWithType(Variable $variable, Type $type): Param
{
$param = new Param($variable);
$phpParserTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($type);
$param->type = $phpParserTypeNode;
return $param;
}
}

View File

@ -66,6 +66,12 @@ final class PhpVersion
*/
public const PHP_80 = 80000;
/**
* @api
* @var int
*/
public const PHP_81 = 81000;
/**
* @api
* @var int

View File

@ -194,4 +194,10 @@ final class PhpVersionFeature
* @var int
*/
public const ATTRIBUTES = PhpVersion::PHP_80;
/**
* @see https://wiki.php.net/rfc/noreturn_type
* @var int
*/
public const NEVER_TYPE = PhpVersion::PHP_81;
}