diff --git a/rules/Php81/NodeManipulator/NullToStrictStringIntConverter.php b/rules/Php81/NodeManipulator/NullToStrictStringIntConverter.php index 6a2c3454d66..862c5a48868 100644 --- a/rules/Php81/NodeManipulator/NullToStrictStringIntConverter.php +++ b/rules/Php81/NodeManipulator/NullToStrictStringIntConverter.php @@ -98,6 +98,11 @@ public function convertIfNull( return $funcCall; } + // already cast to the target type on a previous run, re-casting would nest a redundant cast + if ($this->isAlreadyCastToTargetType($argValue, $targetType)) { + return null; + } + $wrapInParentheses = false; if ($argValue instanceof Ternary && $argValue->cond instanceof CastString_) { $wrapInParentheses = true; @@ -118,6 +123,26 @@ public function convertIfNull( return $funcCall; } + /** + * Structural check (immune to stale resolved types on re-printed nodes): the value is already a + * cast/literal of the target type, or a ternary whose both branches are. + */ + private function isAlreadyCastToTargetType(Expr $expr, string $targetType): bool + { + if ($expr instanceof Ternary) { + // short ternary "A ?: B" keeps the left value in cond; full ternary uses if + $left = $expr->if ?? $expr->cond; + return $this->isAlreadyCastToTargetType($left, $targetType) + && $this->isAlreadyCastToTargetType($expr->else, $targetType); + } + + if ($targetType === 'string') { + return $expr instanceof CastString_ || $expr instanceof String_ || $expr instanceof InterpolatedString; + } + + return $expr instanceof CastInt_ || $expr instanceof Int_; + } + private function shouldSkipValue(Expr $expr, Scope $scope, bool $isTrait, string $targetType): bool { // array dim fetch value is mixed, null is not known - skip to avoid wrong (string) cast