diff --git a/build/target-repository/docs/rector_rules_overview.md b/build/target-repository/docs/rector_rules_overview.md index 3325db1d93c..17705b6d737 100644 --- a/build/target-repository/docs/rector_rules_overview.md +++ b/build/target-repository/docs/rector_rules_overview.md @@ -1,4 +1,4 @@ -# 421 Rules Overview +# 422 Rules Overview
@@ -64,7 +64,7 @@ - [Transform](#transform) (34) -- [TypeDeclaration](#typedeclaration) (40) +- [TypeDeclaration](#typedeclaration) (41) - [Visibility](#visibility) (3) @@ -9689,6 +9689,25 @@ Add strict return array type based on created empty array and returned
+### ReturnTypeFromStrictTernaryRector + +Add method return type based on strict ternary values + +- class: [`Rector\TypeDeclaration\Rector\Class_\ReturnTypeFromStrictTernaryRector`](../rules/TypeDeclaration/Rector/Class_/ReturnTypeFromStrictTernaryRector.php) + +```diff + final class SomeClass + { +- public function getValue($number) ++ public function getValue($number): int + { + return $number ? 100 : 500; + } + } +``` + +
+ ### ReturnTypeFromStrictTypedCallRector Add return type from strict return type of call diff --git a/config/set/type-declaration.php b/config/set/type-declaration.php index 6f67c6ca5a9..75135ab7ad8 100644 --- a/config/set/type-declaration.php +++ b/config/set/type-declaration.php @@ -6,6 +6,7 @@ use Rector\Config\RectorConfig; use Rector\TypeDeclaration\Rector\ArrowFunction\AddArrowFunctionReturnTypeRector; use Rector\TypeDeclaration\Rector\Class_\PropertyTypeFromStrictSetterGetterRector; +use Rector\TypeDeclaration\Rector\Class_\ReturnTypeFromStrictTernaryRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddMethodCallBasedStrictParamTypeRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddParamTypeBasedOnPHPUnitDataProviderRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddParamTypeFromPropertyTypeRector; @@ -70,5 +71,6 @@ ReturnNeverTypeRector::class, EmptyOnNullableObjectToInstanceOfRector::class, PropertyTypeFromStrictSetterGetterRector::class, + ReturnTypeFromStrictTernaryRector::class, ]); }; diff --git a/rules-tests/TypeDeclaration/Rector/Class_/ReturnTypeFromStrictTernaryRector/Fixture/local_constants.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/ReturnTypeFromStrictTernaryRector/Fixture/local_constants.php.inc new file mode 100644 index 00000000000..9c1fba82299 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/ReturnTypeFromStrictTernaryRector/Fixture/local_constants.php.inc @@ -0,0 +1,33 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/Class_/ReturnTypeFromStrictTernaryRector/Fixture/skip_dynamic_scalar.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/ReturnTypeFromStrictTernaryRector/Fixture/skip_dynamic_scalar.php.inc new file mode 100644 index 00000000000..2faf7cc8daf --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/ReturnTypeFromStrictTernaryRector/Fixture/skip_dynamic_scalar.php.inc @@ -0,0 +1,11 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/Class_/ReturnTypeFromStrictTernaryRector/ReturnTypeFromStrictTernaryRectorTest.php b/rules-tests/TypeDeclaration/Rector/Class_/ReturnTypeFromStrictTernaryRector/ReturnTypeFromStrictTernaryRectorTest.php new file mode 100644 index 00000000000..5d281c4f50e --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/ReturnTypeFromStrictTernaryRector/ReturnTypeFromStrictTernaryRectorTest.php @@ -0,0 +1,32 @@ +doTestFile($filePath); + } + + /** + * @return Iterator + */ + public function provideData(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/TypeDeclaration/Rector/Class_/ReturnTypeFromStrictTernaryRector/config/configured_rule.php b/rules-tests/TypeDeclaration/Rector/Class_/ReturnTypeFromStrictTernaryRector/config/configured_rule.php new file mode 100644 index 00000000000..c619d2c8dcd --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/ReturnTypeFromStrictTernaryRector/config/configured_rule.php @@ -0,0 +1,11 @@ +rule(ReturnTypeFromStrictTernaryRector::class); +}; diff --git a/rules/TypeDeclaration/Rector/Class_/ReturnTypeFromStrictTernaryRector.php b/rules/TypeDeclaration/Rector/Class_/ReturnTypeFromStrictTernaryRector.php new file mode 100644 index 00000000000..ae4e9692609 --- /dev/null +++ b/rules/TypeDeclaration/Rector/Class_/ReturnTypeFromStrictTernaryRector.php @@ -0,0 +1,186 @@ +> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + $hasChanged = false; + + foreach ($node->getMethods() as $classMethod) { + if ($classMethod->returnType instanceof Node) { + continue; + } + + $onlyStmt = $classMethod->stmts[0] ?? null; + if (! $onlyStmt instanceof Return_) { + continue; + } + + if (! $onlyStmt->expr instanceof Ternary) { + continue; + } + + $ternary = $onlyStmt->expr; + + // has scalar in if/else of ternary + $ternaryIfElseTypes = $this->matchScalarTernaryIfElseTypes($ternary); + if (! $ternaryIfElseTypes instanceof TernaryIfElseTypes) { + continue; + } + + $ifType = $ternaryIfElseTypes->getFirstType(); + $elseType = $ternaryIfElseTypes->getSecondType(); + + if (! $this->areTypesEqual($ifType, $elseType)) { + continue; + } + + $returnTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($ifType, TypeKind::RETURN); + + if ($this->parentClassMethodTypeOverrideGuard->shouldSkipReturnTypeChange($classMethod, $ifType)) { + continue; + } + + if (! $returnTypeNode instanceof Node) { + continue; + } + + $classMethod->returnType = $returnTypeNode; + $hasChanged = true; + } + + if ($hasChanged) { + return $node; + } + + return null; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::SCALAR_TYPES; + } + + private function isAlwaysScalarExpr(?Expr $expr): bool + { + // check if Scalar node + if ($expr instanceof Scalar) { + return true; + } + + // check if constant + if ($expr instanceof ConstFetch) { + return true; + } + + // check if class constant + return $expr instanceof ClassConstFetch; + } + + private function areTypesEqual(Type $firstType, Type $secondType): bool + { + // this is needed to make comparison tolerant to constant values, e.g. 5 and 10 are same only then + if ($firstType instanceof ConstantType) { + $firstType = $firstType->generalize(GeneralizePrecision::lessSpecific()); + } + + if ($secondType instanceof ConstantType) { + $secondType = $secondType->generalize(GeneralizePrecision::lessSpecific()); + } + + return $firstType->equals($secondType); + } + + private function matchScalarTernaryIfElseTypes(Ternary $ternary): ?TernaryIfElseTypes + { + if (! $this->isAlwaysScalarExpr($ternary->if)) { + return null; + } + + if (! $this->isAlwaysScalarExpr($ternary->else)) { + return null; + } + + /** @var Node\Expr $if */ + $if = $ternary->if; + + /** @var Node\Expr $else */ + $else = $ternary->else; + + $ifType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($if); + $elseType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($else); + + return new TernaryIfElseTypes($ifType, $elseType); + } +} diff --git a/rules/TypeDeclaration/ValueObject/TernaryIfElseTypes.php b/rules/TypeDeclaration/ValueObject/TernaryIfElseTypes.php new file mode 100644 index 00000000000..2c1562aeebf --- /dev/null +++ b/rules/TypeDeclaration/ValueObject/TernaryIfElseTypes.php @@ -0,0 +1,26 @@ +firstType; + } + + public function getSecondType(): Type + { + return $this->secondType; + } +}