[KT-80538] fix: trigger warnings for empty range cases
diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/experimental/EmptyRangeChecker.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/experimental/EmptyRangeChecker.kt index 8b167fd..f47a785 100644 --- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/experimental/EmptyRangeChecker.kt +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/experimental/EmptyRangeChecker.kt
@@ -12,26 +12,27 @@ import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirFunctionCallChecker import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors -import org.jetbrains.kotlin.fir.expressions.FirLiteralExpression -import org.jetbrains.kotlin.fir.expressions.FirFunctionCall +import org.jetbrains.kotlin.fir.expressions.* +import org.jetbrains.kotlin.fir.FirEvaluatorResult object EmptyRangeChecker : FirFunctionCallChecker(MppCheckerKind.Common) { + @OptIn(FirExpressionEvaluator.PrivateConstantEvaluatorAPI::class) context(context: CheckerContext, reporter: DiagnosticReporter) override fun check(expression: FirFunctionCall) { if (expression.source?.kind is KtFakeSourceElementKind) return - val left = expression.rangeLeft ?: return - val right = expression.rangeRight ?: return - val needReport = when (expression.calleeReference.name.asString()) { - "rangeTo" -> { - left > right - } - "downTo" -> { - right > left - } - "until" -> { - left >= right - } + val calleeName = expression.calleeReference.name.asString() + if (calleeName != "rangeTo" && calleeName != "downTo" && calleeName != "until" && calleeName != "rangeUntil") return + + val leftConst = expression.constValueOf(expression.explicitReceiver, context) ?: return + val rightConst = expression.constValueOf(expression.argumentList.arguments.getOrNull(0), context) ?: return + + val cmp = compareConstants(leftConst, rightConst) ?: return + + val needReport = when (calleeName) { + "rangeTo" -> cmp > 0 + "downTo" -> cmp < 0 + "until", "rangeUntil" -> cmp >= 0 else -> false } @@ -40,14 +41,68 @@ } } - private val FirFunctionCall.rangeLeft: Long? - get() { - return (explicitReceiver as? FirLiteralExpression)?.value as? Long + @OptIn(FirExpressionEvaluator.PrivateConstantEvaluatorAPI::class) + private fun FirFunctionCall.constValueOf(expr: FirExpression?, context: CheckerContext): Any? { + if (expr == null) return null + val eval = FirExpressionEvaluator.evaluateExpression(expr, context.session) + val lit = when (eval) { + is FirEvaluatorResult.Evaluated -> eval.result as? FirLiteralExpression + else -> null + } + return lit?.value ?: (expr as? FirLiteralExpression)?.value + } + + private fun isFloating(x: Any): Boolean = x is Double || x is Float + private fun isUnsigned(x: Any): Boolean = x is ULong || x is UInt || x is UShort || x is UByte + + private fun compareConstants(a: Any, b: Any): Int? { + // Char ranges + if (a is Char && b is Char) return a.code.compareTo(b.code) + + // Unsigned ranges + if (isUnsigned(a) && isUnsigned(b)) { + val la = when (a) { + is ULong -> a + is UInt -> a.toULong() + is UShort -> a.toULong() + is UByte -> a.toULong() + else -> return null + } + val rb = when (b) { + is ULong -> b + is UInt -> b.toULong() + is UShort -> b.toULong() + is UByte -> b.toULong() + else -> return null + } + return la.compareTo(rb) } - private val FirFunctionCall.rangeRight: Long? - get() { - val arg = argumentList.arguments.getOrNull(0) as? FirLiteralExpression - return arg?.value as? Long + // Floating point ranges (allow mixing with integral signed types) + if (isFloating(a) || isFloating(b)) { + if (a is Char || b is Char || isUnsigned(a) || isUnsigned(b)) return null + val da = when (a) { + is Double -> a + is Float -> a.toDouble() + is Number -> a.toDouble() + else -> return null + } + val db = when (b) { + is Double -> b + is Float -> b.toDouble() + is Number -> b.toDouble() + else -> return null + } + return da.compareTo(db) } + + // Signed integral (and boolean/string are not expected here) + if (a is Number && b is Number) { + val la = a.toLong() + val lb = b.toLong() + return la.compareTo(lb) + } + + return null + } }