diff --git a/config/set/php85.php b/config/set/php85.php index afb09306273..074c18b2af9 100644 --- a/config/set/php85.php +++ b/config/set/php85.php @@ -10,6 +10,7 @@ use Rector\Php85\Rector\ArrayDimFetch\ArrayFirstLastRector; use Rector\Php85\Rector\ClassMethod\NullDebugInfoReturnRector; use Rector\Php85\Rector\Const_\DeprecatedAnnotationToDeprecatedAttributeRector; +use Rector\Php85\Rector\FuncCall\ArrayKeyExistsNullToEmptyStringRector; use Rector\Php85\Rector\FuncCall\RemoveFinfoBufferContextArgRector; use Rector\Php85\Rector\Switch_\ColonAfterSwitchCaseRector; use Rector\Removing\Rector\FuncCall\RemoveFuncCallArgRector; @@ -32,6 +33,7 @@ NullDebugInfoReturnRector::class, DeprecatedAnnotationToDeprecatedAttributeRector::class, ColonAfterSwitchCaseRector::class, + ArrayKeyExistsNullToEmptyStringRector::class, ] ); diff --git a/rules-tests/Php85/Rector/FuncCall/ArrayKeyExistsNullToEmptyStringRector/ArrayKeyExistsNullToEmptyStringRectorTest.php b/rules-tests/Php85/Rector/FuncCall/ArrayKeyExistsNullToEmptyStringRector/ArrayKeyExistsNullToEmptyStringRectorTest.php new file mode 100644 index 00000000000..99ac55e8f0b --- /dev/null +++ b/rules-tests/Php85/Rector/FuncCall/ArrayKeyExistsNullToEmptyStringRector/ArrayKeyExistsNullToEmptyStringRectorTest.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/Php85/Rector/FuncCall/ArrayKeyExistsNullToEmptyStringRector/Fixture/key_null.php.inc b/rules-tests/Php85/Rector/FuncCall/ArrayKeyExistsNullToEmptyStringRector/Fixture/key_null.php.inc new file mode 100644 index 00000000000..f20b508d01a --- /dev/null +++ b/rules-tests/Php85/Rector/FuncCall/ArrayKeyExistsNullToEmptyStringRector/Fixture/key_null.php.inc @@ -0,0 +1,11 @@ + +----- + diff --git a/rules-tests/Php85/Rector/FuncCall/ArrayKeyExistsNullToEmptyStringRector/Fixture/key_null_var.php.inc b/rules-tests/Php85/Rector/FuncCall/ArrayKeyExistsNullToEmptyStringRector/Fixture/key_null_var.php.inc new file mode 100644 index 00000000000..68582d1037c --- /dev/null +++ b/rules-tests/Php85/Rector/FuncCall/ArrayKeyExistsNullToEmptyStringRector/Fixture/key_null_var.php.inc @@ -0,0 +1,11 @@ + +----- + diff --git a/rules-tests/Php85/Rector/FuncCall/ArrayKeyExistsNullToEmptyStringRector/config/configured_rule.php b/rules-tests/Php85/Rector/FuncCall/ArrayKeyExistsNullToEmptyStringRector/config/configured_rule.php new file mode 100644 index 00000000000..d3207f07fa0 --- /dev/null +++ b/rules-tests/Php85/Rector/FuncCall/ArrayKeyExistsNullToEmptyStringRector/config/configured_rule.php @@ -0,0 +1,13 @@ +rule(ArrayKeyExistsNullToEmptyStringRector::class); + + $rectorConfig->phpVersion(PhpVersion::PHP_85); +}; diff --git a/rules/Php85/Rector/FuncCall/ArrayKeyExistsNullToEmptyStringRector.php b/rules/Php85/Rector/FuncCall/ArrayKeyExistsNullToEmptyStringRector.php new file mode 100644 index 00000000000..0f7a8faf29e --- /dev/null +++ b/rules/Php85/Rector/FuncCall/ArrayKeyExistsNullToEmptyStringRector.php @@ -0,0 +1,252 @@ +isFirstClassCallable()) { + return null; + } + + if (! $this->isName($node, 'array_key_exists')) { + return null; + } + + $scope = $node->getAttribute(AttributeKey::SCOPE); + if (! $scope instanceof Scope) { + return null; + } + + $args = $node->getArgs(); + + $classReflection = $scope->getClassReflection(); + $isTrait = $classReflection instanceof ClassReflection && $classReflection->isTrait(); + + $functionReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node); + if (! $functionReflection instanceof FunctionReflection) { + return null; + } + + $parametersAcceptor = ParametersAcceptorSelectorVariantsWrapper::select($functionReflection, $node, $scope); + $isChanged = false; + + $result = $this->processNullToStrictStringOnNodePosition( + $node, + $args, + 0, + $isTrait, + $scope, + $parametersAcceptor + ); + if ($result instanceof Node) { + $node = $result; + $isChanged = true; + } + + if ($isChanged) { + return $node; + } + + return null; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::DEPRECATE_NULL_ARG_IN_ARRAY_KEY_EXISTS_FUNCTION; + } + + /** + * @param Arg[] $args + * @param int|string $position + */ + private function processNullToStrictStringOnNodePosition( + FuncCall $funcCall, + array $args, + $position, + bool $isTrait, + Scope $scope, + ParametersAcceptor $parametersAcceptor + ): ?FuncCall { + if (! isset($args[$position])) { + return null; + } + $argValue = $args[$position]->value; + if ($this->valueResolver->isNull($argValue)) { + $args[$position]->value = new String_(''); + $funcCall->args = $args; + return $funcCall; + } + if ($this->shouldSkipValue($argValue, $scope, $isTrait)) { + return null; + } + $parameter = $parametersAcceptor->getParameters()[$position] ?? null; + if ($parameter instanceof ExtendedNativeParameterReflection && $parameter->getType() instanceof UnionType) { + $parameterType = $parameter->getType(); + if (! $this->isValidUnionType($parameterType)) { + return null; + } + } + if ($argValue instanceof Ternary && ! $this->shouldSkipValue($argValue->else, $scope, $isTrait)) { + if ($this->valueResolver->isNull($argValue->else)) { + $argValue->else = new String_(''); + } else { + $argValue->else = new CastString_($argValue->else); + } + $args[$position]->value = $argValue; + $funcCall->args = $args; + return $funcCall; + } + $args[$position]->value = new CastString_($argValue); + $funcCall->args = $args; + return $funcCall; + } + + private function shouldSkipValue(Expr $expr, Scope $scope, bool $isTrait): bool + { + $type = $this->nodeTypeResolver->getType($expr); + if ($type->isString()->yes()) { + return \true; + } + $nativeType = $this->nodeTypeResolver->getNativeType($expr); + if ($nativeType->isString()->yes()) { + return \true; + } + if ($this->shouldSkipType($type)) { + return \true; + } + if ($expr instanceof InterpolatedString) { + return \true; + } + if ($this->isAnErrorType($expr, $nativeType, $scope)) { + return \true; + } + return $this->shouldSkipTrait($expr, $type, $isTrait); + } + + private function isValidUnionType(Type $type): bool + { + if (! $type instanceof UnionType) { + return \false; + } + foreach ($type->getTypes() as $childType) { + if ($childType->isString()->yes()) { + continue; + } + if ($childType->isInteger()->yes()) { + continue; + } + if ($childType->isNull()->yes()) { + continue; + } + return \false; + } + return \true; + } + + private function shouldSkipType(Type $type): bool + { + return ! $type instanceof MixedType && ! $type->isNull() + ->yes() && ! $this->isValidUnionType($type); + } + + private function shouldSkipTrait(Expr $expr, Type $type, bool $isTrait): bool + { + if (! $type instanceof MixedType) { + return \false; + } + if (! $isTrait) { + return \false; + } + if ($type->isExplicitMixed()) { + return \false; + } + if (! $expr instanceof MethodCall) { + return $this->propertyFetchAnalyzer->isLocalPropertyFetch($expr); + } + return \true; + } + + private function isAnErrorType(Expr $expr, Type $type, Scope $scope): bool + { + if ($type instanceof ErrorType) { + return \true; + } + $parentScope = $scope->getParentScope(); + if ($parentScope instanceof Scope) { + return $parentScope->getType($expr) instanceof ErrorType; + } + return $type instanceof MixedType && ! $type->isExplicitMixed() && $type->getSubtractedType() instanceof NullType; + } +} diff --git a/src/ValueObject/PhpVersionFeature.php b/src/ValueObject/PhpVersionFeature.php index a37b164c45b..3a0505ef5f6 100644 --- a/src/ValueObject/PhpVersionFeature.php +++ b/src/ValueObject/PhpVersionFeature.php @@ -792,4 +792,10 @@ final class PhpVersionFeature * @var int */ public const COLON_AFTER_SWITCH_CASE = PhpVersion::PHP_85; + + /** + * @see https://wiki.php.net/rfc/deprecations_php_8_5#https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_using_values_null_as_an_array_offset_and_when_calling_array_key_exists + * @var int + */ + public const DEPRECATE_NULL_ARG_IN_ARRAY_KEY_EXISTS_FUNCTION = PhpVersion::PHP_85; }