diff --git a/config/set/downgrade-php80.php b/config/set/downgrade-php80.php index ce89592e..9e13c73c 100644 --- a/config/set/downgrade-php80.php +++ b/config/set/downgrade-php80.php @@ -23,6 +23,7 @@ use Rector\DowngradePhp80\Rector\FuncCall\DowngradeStrContainsRector; use Rector\DowngradePhp80\Rector\FuncCall\DowngradeStrEndsWithRector; use Rector\DowngradePhp80\Rector\FuncCall\DowngradeStrStartsWithRector; +use Rector\DowngradePhp80\Rector\FuncCall\DowngradeSubstrFalsyRector; use Rector\DowngradePhp80\Rector\FunctionLike\DowngradeMixedTypeDeclarationRector; use Rector\DowngradePhp80\Rector\FunctionLike\DowngradeUnionTypeDeclarationRector; use Rector\DowngradePhp80\Rector\Instanceof_\DowngradeInstanceofStringableRector; @@ -91,5 +92,6 @@ RemoveReturnTypeDeclarationFromCloneRector::class, DowngradeEnumToConstantListClassRector::class, DowngradeInstanceofStringableRector::class, + DowngradeSubstrFalsyRector::class, ]); }; diff --git a/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/DowngradeSubstrFalsyRectorTest.php b/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/DowngradeSubstrFalsyRectorTest.php new file mode 100644 index 00000000..e7f4d292 --- /dev/null +++ b/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/DowngradeSubstrFalsyRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/Fixture/by_variable_string.php.inc b/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/Fixture/by_variable_string.php.inc new file mode 100644 index 00000000..464b3fc6 --- /dev/null +++ b/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/Fixture/by_variable_string.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/Fixture/fixture.php.inc b/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/Fixture/fixture.php.inc new file mode 100644 index 00000000..ea7b6685 --- /dev/null +++ b/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/Fixture/fixture.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/Fixture/skip_already_casted.php.inc b/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/Fixture/skip_already_casted.php.inc new file mode 100644 index 00000000..bd271390 --- /dev/null +++ b/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/Fixture/skip_already_casted.php.inc @@ -0,0 +1,11 @@ + 'foo' + ]; + + var_dump($data); + } +} \ No newline at end of file diff --git a/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/Fixture/skip_as_falsy_arg.php.inc b/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/Fixture/skip_as_falsy_arg.php.inc new file mode 100644 index 00000000..50c8efb3 --- /dev/null +++ b/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/Fixture/skip_as_falsy_arg.php.inc @@ -0,0 +1,16 @@ +execute(substr('a', 2)); + } + + private function execute(false|string $value) + { + return $value; + } +} diff --git a/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/Fixture/skip_casted_bool.php.inc b/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/Fixture/skip_casted_bool.php.inc new file mode 100644 index 00000000..f78c7eeb --- /dev/null +++ b/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/Fixture/skip_casted_bool.php.inc @@ -0,0 +1,11 @@ + +----- + diff --git a/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/config/configured_rule.php b/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/config/configured_rule.php new file mode 100644 index 00000000..1f861b32 --- /dev/null +++ b/rules-tests/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(DowngradeSubstrFalsyRector::class); +}; diff --git a/rules/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector.php b/rules/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector.php new file mode 100644 index 00000000..9c20aeed --- /dev/null +++ b/rules/DowngradePhp80/Rector/FuncCall/DowngradeSubstrFalsyRector.php @@ -0,0 +1,218 @@ +> + */ + public function getNodeTypes(): array + { + return [ + Cast::class, + Empty_::class, + BooleanNot::class, + Ternary::class, + Identical::class, + Concat::class, + MethodCall::class, + StaticCall::class, + New_::class, + AssignOp::class, + If_::class, + While_::class, + Do_::class, + ArrayItem::class, + ArrayDimFetch::class, + BinaryOp::class, + FuncCall::class, + ]; + } + + /** + * @param Cast|Empty_|BooleanNot|Ternary|Identical|Concat|MethodCall|StaticCall|New_|AssignOp|If_|While_|Do_|ArrayItem|ArrayDimFetch|BinaryOp|FuncCall $node + */ + public function refactor(Node $node): ?Node + { + if ($node instanceof Cast || $node instanceof Empty_ || $node instanceof BooleanNot || $node instanceof AssignOp) { + $node->expr->setAttribute(self::IS_FALSY_UNCASTABLE, true); + return null; + } + + if ($node instanceof Ternary) { + if (! $node->if instanceof Expr) { + $node->cond->setAttribute(self::IS_FALSY_UNCASTABLE, true); + } + + return null; + } + + if ($node instanceof Concat) { + $node->left->setAttribute(self::IS_FALSY_UNCASTABLE, true); + $node->right->setAttribute(self::IS_FALSY_UNCASTABLE, true); + return null; + } + + if ($node instanceof Identical) { + if ($this->valueResolver->isFalse($node->left)) { + $node->right->setAttribute(self::IS_FALSY_UNCASTABLE, true); + } + + if ($this->valueResolver->isFalse($node->right)) { + $node->left->setAttribute(self::IS_FALSY_UNCASTABLE, true); + } + + return null; + } + + if ($node instanceof If_ || $node instanceof While_ || $node instanceof Do_) { + $node->cond->setAttribute(self::IS_FALSY_UNCASTABLE, true); + return null; + } + + if ($node instanceof ArrayItem) { + if ($node->key instanceof Expr) { + $node->key->setAttribute(self::IS_FALSY_UNCASTABLE, true); + } + + return null; + } + + if ($node instanceof ArrayDimFetch) { + if ($node->dim instanceof Expr) { + $node->dim->setAttribute(self::IS_FALSY_UNCASTABLE, true); + } + + return null; + } + + if ($node instanceof BinaryOp) { + $node->left->setAttribute(self::IS_FALSY_UNCASTABLE, true); + $node->right->setAttribute(self::IS_FALSY_UNCASTABLE, true); + return null; + } + + if ($node instanceof CallLike) { + if ($node->isFirstClassCallable()) { + return null; + } + + $reflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node); + + if (! $reflection instanceof MethodReflection && ! $reflection instanceof FunctionReflection) { + return null; + } + + $parameterAcceptor = ParametersAcceptorSelectorVariantsWrapper::select( + $reflection, + $node, + ScopeFetcher::fetch($node) + ); + + foreach ($parameterAcceptor->getParameters() as $position => $parameterReflection) { + if ($parameterReflection->getType()->isFalse()->no()) { + continue; + } + + $arg = $node->getArg($parameterReflection->getName(), $position); + if ($arg instanceof Arg) { + $arg->value->setAttribute(self::IS_FALSY_UNCASTABLE, true); + } + } + } + + if (! $this->isName($node, 'substr')) { + return null; + } + + if ($node->getAttribute(self::IS_FALSY_UNCASTABLE) === true) { + return null; + } + + $type = $this->getType($node); + if ($type->isNonEmptyString()->yes()) { + return null; + } + + $offset = $node->getArg('offset', 1); + + if ($offset instanceof Arg) { + $offsetType = $this->getType($offset->value); + if ($offsetType instanceof ConstantIntegerType && $offsetType->getValue() <= 0) { + $length = $node->getArg('length', 2); + if ($length instanceof Arg) { + $lengthType = $this->getType($length->value); + if ($lengthType instanceof ConstantIntegerType && $lengthType->getValue() >= 0) { + return null; + } + + return new String_($node); + } + + return null; + } + } + + return new String_($node); + } +} diff --git a/tests/Issues/DowngradeHeredoc/Fixture/next_arg.php.inc b/tests/Issues/DowngradeHeredoc/Fixture/next_arg.php.inc index 1f117df8..db204c12 100644 --- a/tests/Issues/DowngradeHeredoc/Fixture/next_arg.php.inc +++ b/tests/Issues/DowngradeHeredoc/Fixture/next_arg.php.inc @@ -474,9 +474,9 @@ final class NextArg $lineString = trim($lineString); if (strncmp($lineString, '/**', strlen('/**')) === 0) { - $lineString = substr($lineString, 3); + $lineString = (string) substr($lineString, 3); } elseif (strncmp($lineString, '*', strlen('*')) === 0) { - $lineString = substr($lineString, 1); + $lineString = (string) substr($lineString, 1); } return trim($lineString);