From 0b21f6cd99aa9edd3068c23dd9c286a89eed05c4 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 8 Oct 2025 08:48:49 +0200 Subject: [PATCH 1/2] [dead-code] Skip substr casting removal on PHP 7.x, as can return false|string --- .../Fixture/skip_substr.php.inc | 17 -------- .../FixturePhp74/skip_substr.php.inc | 11 +++++ .../RecastingRemovalRectorPhp74Test.php | 28 +++++++++++++ .../config/configured_rule.php | 4 +- .../config/configured_rule_php74.php | 11 +++++ .../true_in_union_strtoupper.php.inc | 41 +++++++++++++++++++ .../ClassLikeNameClassNameImportSkipVoter.php | 8 ++-- .../Rector/Cast/RecastingRemovalRector.php | 11 +---- src/NodeTypeResolver/NodeTypeResolver.php | 24 +++++++++++ 9 files changed, 122 insertions(+), 33 deletions(-) delete mode 100644 rules-tests/DeadCode/Rector/Cast/RecastingRemovalRector/Fixture/skip_substr.php.inc create mode 100644 rules-tests/DeadCode/Rector/Cast/RecastingRemovalRector/FixturePhp74/skip_substr.php.inc create mode 100644 rules-tests/DeadCode/Rector/Cast/RecastingRemovalRector/RecastingRemovalRectorPhp74Test.php create mode 100644 rules-tests/DeadCode/Rector/Cast/RecastingRemovalRector/config/configured_rule_php74.php create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnUnionTypeRector/FixtureTrueInUnion/true_in_union_strtoupper.php.inc diff --git a/rules-tests/DeadCode/Rector/Cast/RecastingRemovalRector/Fixture/skip_substr.php.inc b/rules-tests/DeadCode/Rector/Cast/RecastingRemovalRector/Fixture/skip_substr.php.inc deleted file mode 100644 index 07f63228bfb..00000000000 --- a/rules-tests/DeadCode/Rector/Cast/RecastingRemovalRector/Fixture/skip_substr.php.inc +++ /dev/null @@ -1,17 +0,0 @@ - diff --git a/rules-tests/DeadCode/Rector/Cast/RecastingRemovalRector/FixturePhp74/skip_substr.php.inc b/rules-tests/DeadCode/Rector/Cast/RecastingRemovalRector/FixturePhp74/skip_substr.php.inc new file mode 100644 index 00000000000..31aa57c0bc7 --- /dev/null +++ b/rules-tests/DeadCode/Rector/Cast/RecastingRemovalRector/FixturePhp74/skip_substr.php.inc @@ -0,0 +1,11 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/FixturePhp74'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule_php74.php'; + } +} diff --git a/rules-tests/DeadCode/Rector/Cast/RecastingRemovalRector/config/configured_rule.php b/rules-tests/DeadCode/Rector/Cast/RecastingRemovalRector/config/configured_rule.php index 52e76816c96..7ae9f988b70 100644 --- a/rules-tests/DeadCode/Rector/Cast/RecastingRemovalRector/config/configured_rule.php +++ b/rules-tests/DeadCode/Rector/Cast/RecastingRemovalRector/config/configured_rule.php @@ -4,6 +4,8 @@ use Rector\Config\RectorConfig; use Rector\DeadCode\Rector\Cast\RecastingRemovalRector; +use Rector\ValueObject\PhpVersion; return RectorConfig::configure() - ->withRules([RecastingRemovalRector::class]); + ->withRules([RecastingRemovalRector::class]) + ->withPhpVersion(PhpVersion::PHP_80); diff --git a/rules-tests/DeadCode/Rector/Cast/RecastingRemovalRector/config/configured_rule_php74.php b/rules-tests/DeadCode/Rector/Cast/RecastingRemovalRector/config/configured_rule_php74.php new file mode 100644 index 00000000000..c86fc9d7fe5 --- /dev/null +++ b/rules-tests/DeadCode/Rector/Cast/RecastingRemovalRector/config/configured_rule_php74.php @@ -0,0 +1,11 @@ +withRules([RecastingRemovalRector::class]) + ->withPhpVersion(PhpVersion::PHP_74); diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnUnionTypeRector/FixtureTrueInUnion/true_in_union_strtoupper.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnUnionTypeRector/FixtureTrueInUnion/true_in_union_strtoupper.php.inc new file mode 100644 index 00000000000..eb25a83bc0e --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnUnionTypeRector/FixtureTrueInUnion/true_in_union_strtoupper.php.inc @@ -0,0 +1,41 @@ += php 8.2, ref https://3v4l.org/UJqXT + */ +final class TrueInUnionStrToUpper +{ + public function run($value) + { + if ($value) { + return true; + } + + return strtoupper('warning'); + } +} + +?> +----- += php 8.2, ref https://3v4l.org/UJqXT + */ +final class TrueInUnionStrToUpper +{ + public function run($value): true|string + { + if ($value) { + return true; + } + + return strtoupper('warning'); + } +} + +?> diff --git a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/ClassLikeNameClassNameImportSkipVoter.php b/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/ClassLikeNameClassNameImportSkipVoter.php index 29737b3ed6f..3030cdb8275 100644 --- a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/ClassLikeNameClassNameImportSkipVoter.php +++ b/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/ClassLikeNameClassNameImportSkipVoter.php @@ -46,15 +46,13 @@ public function shouldSkip(File $file, FullyQualifiedObjectType $fullyQualifiedO $namespace = strtolower((string) $namespace); $shortNameLowered = $fullyQualifiedObjectType->getShortNameLowered(); - /** - * on php 7.x, substr() result can return false, so force (string) is needed - * @see https://github.com/rectorphp/rector-src/pull/7436 - */ - $subClassName = (string) substr( + + $subClassName = substr( $fullyQualifiedObjectType->getClassName(), 0, -strlen($fullyQualifiedObjectType->getShortName()) - 1 ); + $fullyQualifiedObjectTypeNamespace = strtolower($subClassName); foreach ($classLikeNames as $classLikeName) { diff --git a/rules/DeadCode/Rector/Cast/RecastingRemovalRector.php b/rules/DeadCode/Rector/Cast/RecastingRemovalRector.php index fa2f3ce3fbd..627d513b057 100644 --- a/rules/DeadCode/Rector/Cast/RecastingRemovalRector.php +++ b/rules/DeadCode/Rector/Cast/RecastingRemovalRector.php @@ -13,14 +13,12 @@ use PhpParser\Node\Expr\Cast\Int_; use PhpParser\Node\Expr\Cast\Object_; use PhpParser\Node\Expr\Cast\String_; -use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\StaticCall; use PHPStan\Reflection\Php\PhpPropertyReflection; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; @@ -55,7 +53,7 @@ final class RecastingRemovalRector extends AbstractRector public function __construct( private readonly PropertyFetchAnalyzer $propertyFetchAnalyzer, private readonly ReflectionResolver $reflectionResolver, - private readonly ExprAnalyzer $exprAnalyzer + private readonly ExprAnalyzer $exprAnalyzer, ) { } @@ -105,13 +103,6 @@ public function refactor(Node $node): ?Node return null; } - // substr can return false on php 7.x - if ($node->expr instanceof FuncCall - && $this->isName($node->expr, 'substr') - && ! $node->expr->isFirstClassCallable()) { - $nodeType = new UnionType([new StringType(), new ConstantBooleanType(false)]); - } - if ($nodeType instanceof ConstantArrayType && $nodeClass === Array_::class) { if ($this->shouldSkip($node->expr)) { return null; diff --git a/src/NodeTypeResolver/NodeTypeResolver.php b/src/NodeTypeResolver/NodeTypeResolver.php index 6cde992ec83..eb03e9dbe3d 100644 --- a/src/NodeTypeResolver/NodeTypeResolver.php +++ b/src/NodeTypeResolver/NodeTypeResolver.php @@ -39,6 +39,7 @@ use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\StringType; use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -55,9 +56,11 @@ use Rector\NodeTypeResolver\NodeTypeCorrector\AccessoryNonEmptyStringTypeCorrector; use Rector\NodeTypeResolver\NodeTypeCorrector\GenericClassStringTypeCorrector; use Rector\NodeTypeResolver\PHPStan\ObjectWithoutClassTypeWithParentTypes; +use Rector\Php\PhpVersionProvider; use Rector\StaticTypeMapper\ValueObject\Type\AliasedObjectType; use Rector\StaticTypeMapper\ValueObject\Type\ShortenedObjectType; use Rector\TypeDeclaration\PHPStan\ObjectTypeSpecifier; +use Rector\ValueObject\PhpVersion; final class NodeTypeResolver { @@ -83,6 +86,7 @@ public function __construct( private readonly AccessoryNonEmptyArrayTypeCorrector $accessoryNonEmptyArrayTypeCorrector, private readonly RenamedClassesDataCollector $renamedClassesDataCollector, private readonly NodeNameResolver $nodeNameResolver, + private readonly PhpVersionProvider $phpVersionProvider, iterable $nodeTypeResolvers ) { foreach ($nodeTypeResolvers as $nodeTypeResolver) { @@ -620,6 +624,10 @@ private function resolveNativeTypeWithBuiltinMethodCallFallback(Expr $expr, Scop return $scope->getNativeType($expr); } + if ($this->isSubstrOnPHP74($expr)) { + return new UnionType([new StringType(), new ConstantBooleanType(false)]); + } + return $scope->getType($expr); } @@ -651,4 +659,20 @@ private function isEnumTypeMatch(MethodCall|NullsafeMethodCall $call, ObjectType return $classReflection->getName() === $objectType->getClassName(); } + + /** + * substr can return false on php 7.x and bellow + */ + private function isSubstrOnPHP74(FuncCall $expr): bool + { + if ($expr->isFirstClassCallable()) { + return false; + } + + if (! $this->nodeNameResolver->isName($expr, 'substr')) { + return false; + } + + return ! $this->phpVersionProvider->isAtLeastPhpVersion(PhpVersion::PHP_80); + } } From dcb81d5f131a0cf0cf11a22eb947362086781345 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 8 Oct 2025 07:04:29 +0000 Subject: [PATCH 2/2] [ci-review] Rector Rectify --- src/NodeTypeResolver/NodeTypeResolver.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NodeTypeResolver/NodeTypeResolver.php b/src/NodeTypeResolver/NodeTypeResolver.php index eb03e9dbe3d..41dad8c7b3e 100644 --- a/src/NodeTypeResolver/NodeTypeResolver.php +++ b/src/NodeTypeResolver/NodeTypeResolver.php @@ -663,13 +663,13 @@ private function isEnumTypeMatch(MethodCall|NullsafeMethodCall $call, ObjectType /** * substr can return false on php 7.x and bellow */ - private function isSubstrOnPHP74(FuncCall $expr): bool + private function isSubstrOnPHP74(FuncCall $funcCall): bool { - if ($expr->isFirstClassCallable()) { + if ($funcCall->isFirstClassCallable()) { return false; } - if (! $this->nodeNameResolver->isName($expr, 'substr')) { + if (! $this->nodeNameResolver->isName($funcCall, 'substr')) { return false; }