From b7c6103f124f3f7d251f1b7326dc0577ef5c6b54 Mon Sep 17 00:00:00 2001 From: Tagir Valeev Date: Thu, 2 Jun 2022 17:52:37 +0200 Subject: [PATCH] [kotlin-dfa] KTIJ-21930 Dataflow assistant: suppress hints when they are suppressed for original dataflow GitOrigin-RevId: 0eea5ecaaaa329ab9c80b70bfea245ebc97f0fc1 --- .../dfa/KotlinConstantConditionsInspection.kt | 567 +++++++++--------- .../dfaassist/KotlinDfaAssistProvider.kt | 6 +- .../idea/debugger/test/KotlinDfaAssistTest.kt | 1 + 3 files changed, 296 insertions(+), 278 deletions(-) diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/dfa/KotlinConstantConditionsInspection.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/dfa/KotlinConstantConditionsInspection.kt index c6df8a7a1473..b6fab09041e1 100644 --- a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/dfa/KotlinConstantConditionsInspection.kt +++ b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/dfa/KotlinConstantConditionsInspection.kt @@ -13,6 +13,7 @@ import com.intellij.codeInspection.dataFlow.lang.UnsatisfiedConditionProblem import com.intellij.codeInspection.dataFlow.lang.ir.DataFlowIRProvider import com.intellij.codeInspection.dataFlow.lang.ir.DfaInstructionState import com.intellij.codeInspection.dataFlow.memory.DfaMemoryState +import com.intellij.codeInspection.dataFlow.types.DfType import com.intellij.codeInspection.dataFlow.types.DfTypes import com.intellij.codeInspection.dataFlow.value.DfaValue import com.intellij.codeInspection.dataFlow.value.DfaValueFactory @@ -86,260 +87,6 @@ class KotlinConstantConditionsInspection : AbstractKotlinInspection() { } } - private fun shouldSuppress(value: ConstantValue, expression: KtExpression): Boolean { - // TODO: do something with always false branches in exhaustive when statements - // TODO: return x && y.let {return...} - var parent = expression.parent - if (parent is KtDotQualifiedExpression && parent.selectorExpression == expression) { - // Will be reported for parent qualified expression - return true - } - while (parent is KtParenthesizedExpression) { - parent = parent.parent - } - if (expression is KtConstantExpression || - // If result of initialization is constant, then the initializer will be reported - expression is KtProperty || - // If result of assignment is constant, then the right-hand part will be reported - expression is KtBinaryExpression && expression.operationToken == KtTokens.EQ || - // Negation operand: negation itself will be reported - (parent as? KtPrefixExpression)?.operationToken == KtTokens.EXCL - ) { - return true - } - if (expression is KtBinaryExpression && expression.operationToken == KtTokens.ELVIS) { - // Left part of Elvis is Nothing?, so the right part is always executed - // Could be caused by code like return x?.let { return ... } ?: true - // While inner "return" is redundant, the "always true" warning is confusing - // probably separate inspection could report extra "return" - if (expression.left?.getKotlinType()?.isNullableNothing() == true) { - return true - } - } - if (isAlsoChain(expression) || isLetConstant(expression) || isUpdateChain(expression)) return true - when (value) { - ConstantValue.TRUE -> { - if (isSmartCastNecessary(expression, true)) return true - if (isPairingConditionInWhen(expression)) return true - if (isAssertion(parent, true)) return true - } - ConstantValue.FALSE -> { - if (isSmartCastNecessary(expression, false)) return true - if (isAssertion(parent, false)) return true - } - ConstantValue.ZERO -> { - if (expression.readWriteAccess(false).isWrite) { - // like if (x == 0) x++, warning would be somewhat annoying - return true - } - if (expression is KtDotQualifiedExpression && expression.selectorExpression?.textMatches("ordinal") == true) { - var receiver: KtExpression? = expression.receiverExpression - if (receiver is KtQualifiedExpression) { - receiver = receiver.selectorExpression - } - if (receiver is KtSimpleNameExpression && receiver.mainReference.resolve() is KtEnumEntry) { - // ordinal() call on explicit enum constant - return true - } - } - val bindingContext = expression.analyze() - if (ConstantExpressionEvaluator.getConstant(expression, bindingContext) != null) return true - if (expression is KtSimpleNameExpression && - (parent is KtValueArgument || parent is KtContainerNode && parent.parent is KtArrayAccessExpression) - ) { - // zero value is passed as argument to another method or used for array access. Often, such a warning is annoying - return true - } - } - ConstantValue.NULL -> { - if (parent is KtProperty && parent.typeReference == null && expression is KtSimpleNameExpression) { - // initialize other variable with null to copy type, like - // var x1 : X = null - // var x2 = x1 -- let's suppress this - return true - } - if (expression is KtBinaryExpressionWithTypeRHS && expression.left.isNull()) { - // like (null as? X) - return true - } - if (parent is KtBinaryExpression) { - val token = parent.operationToken - if ((token === KtTokens.EQEQ || token === KtTokens.EXCLEQ || token === KtTokens.EQEQEQ || token === KtTokens.EXCLEQEQEQ) && - (parent.left?.isNull() == true || parent.right?.isNull() == true) - ) { - // like if (x == null) when 'x' is known to be null: report 'always true' instead - return true - } - } - val kotlinType = expression.getKotlinType() - if (kotlinType.toDfType(expression) == DfTypes.NULL) { - // According to type system, nothing but null could be stored in such an expression (likely "Void?" type) - return true - } - } - else -> {} - } - if (expression is KtSimpleNameExpression) { - val target = expression.mainReference.resolve() - if (target is KtProperty && !target.isVar && target.initializer is KtConstantExpression) { - // suppress warnings uses of boolean constant like 'val b = true' - return true - } - } - if (isCompilationWarning(expression)) { - return true - } - return expression.isUsedAsStatement(expression.analyze(BodyResolveMode.FULL)) - } - - private fun isUpdateChain(expression: KtExpression): Boolean { - // x = x or ..., etc. - if (expression !is KtSimpleNameExpression) return false - val binOp = expression.parent as? KtBinaryExpression ?: return false - val op = binOp.operationReference.text - if (op != "or" && op != "and" && op != "xor" && op != "||" && op != "&&") return false - val assignment = binOp.parent as? KtBinaryExpression ?: return false - if (assignment.operationToken != KtTokens.EQ) return false - val left = assignment.left - if (left !is KtSimpleNameExpression || !left.textMatches(expression.text)) return false - val variable = expression.mainReference.resolve() as? KtProperty ?: return false - val varParent = variable.parent as? KtBlockExpression ?: return false - var context: PsiElement = assignment - var block = context.parent - while (block is KtContainerNode || - block is KtBlockExpression && block.statements.first() == context || - block is KtIfExpression && block.then?.parent == context && block.`else` == null && !hasWritesTo(block.condition, variable)) { - context = block - block = context.parent - } - if (block !== varParent) return false - var curExpression = variable.nextSibling - while (curExpression != context) { - if (hasWritesTo(curExpression, variable)) return false - curExpression = curExpression.nextSibling - } - return true - } - - private fun hasWritesTo(block: PsiElement?, variable: KtProperty): Boolean { - return !PsiTreeUtil.processElements(block, KtSimpleNameExpression::class.java) { ref -> - val write = ref.mainReference.isReferenceTo(variable) && ref.readWriteAccess(false).isWrite - !write - } - } - - // Do not report on also, as it always returns the qualifier. If necessary, qualifier itself will be reported - private fun isAlsoChain(expr: KtExpression): Boolean { - val call = (expr as? KtQualifiedExpression)?.selectorExpression as? KtCallExpression ?: return false - return isCallToMethod(call, "kotlin", "also") - } - - // Do not report x.let { true } or x.let { false } as it's pretty evident - private fun isLetConstant(expr: KtExpression): Boolean { - val call = (expr as? KtQualifiedExpression)?.selectorExpression as? KtCallExpression ?: return false - if (!isCallToMethod(call, "kotlin", "let")) return false - val lambda = call.lambdaArguments.singleOrNull()?.getLambdaExpression() ?: return false - return lambda.bodyExpression?.statements?.singleOrNull() is KtConstantExpression - } - - private fun isCallToMethod(call: KtCallExpression, packageName: String, methodName: String): Boolean { - val descriptor = call.resolveToCall()?.resultingDescriptor ?: return false - if (descriptor.name.asString() != methodName) return false - val packageFragment = descriptor.containingDeclaration as? PackageFragmentDescriptor ?: return false - return packageFragment.fqName.asString() == packageName - } - - private fun isAssertion(parent: PsiElement?, value: Boolean): Boolean { - return when (parent) { - is KtBinaryExpression -> - (parent.operationToken == KtTokens.ANDAND || parent.operationToken == KtTokens.OROR) && isAssertion(parent.parent, value) - is KtParenthesizedExpression -> - isAssertion(parent.parent, value) - is KtPrefixExpression -> - parent.operationToken == KtTokens.EXCL && isAssertion(parent.parent, !value) - is KtValueArgument -> { - if (!value) return false - val valueArgList = parent.parent as? KtValueArgumentList ?: return false - val call = valueArgList.parent as? KtCallExpression ?: return false - val descriptor = call.resolveToCall()?.resultingDescriptor ?: return false - val name = descriptor.name.asString() - if (name != "assert" && name != "require" && name != "check") return false - val pkg = descriptor.containingDeclaration as? PackageFragmentDescriptor ?: return false - return pkg.fqName.asString() == "kotlin" - } - else -> false - } - } - - /** - * Returns true if expression is part of when condition expression that looks like - * ``` - * when { - * a && b -> ... - * a && !b -> ... - * } - * ``` - * In this case, !b could be reported as 'always true' but such warnings are annoying - */ - private fun isPairingConditionInWhen(expression: KtExpression): Boolean { - val parent = expression.parent - if (parent is KtBinaryExpression && parent.operationToken == KtTokens.ANDAND) { - var topAnd: KtBinaryExpression = parent - while (true) { - val nextParent = topAnd.parent - if (nextParent is KtBinaryExpression && nextParent.operationToken == KtTokens.ANDAND) { - topAnd = nextParent - } else break - } - val topAndParent = topAnd.parent - if (topAndParent is KtWhenConditionWithExpression) { - val whenExpression = (topAndParent.parent as? KtWhenEntry)?.parent as? KtWhenExpression - if (whenExpression != null && hasOppositeCondition(whenExpression, topAnd, expression)) { - return true - } - } - } - return false - } - - private fun hasOppositeCondition(whenExpression: KtWhenExpression, topAnd: KtBinaryExpression, expression: KtExpression): Boolean { - for (entry in whenExpression.entries) { - for (condition in entry.conditions) { - if (condition is KtWhenConditionWithExpression) { - val candidate = condition.expression - if (candidate === topAnd) return false - if (isOppositeCondition(candidate, topAnd, expression)) return true - } - } - } - return false - } - - private tailrec fun isOppositeCondition(candidate: KtExpression?, template: KtBinaryExpression, expression: KtExpression): Boolean { - if (candidate !is KtBinaryExpression || candidate.operationToken !== KtTokens.ANDAND) return false - val left = candidate.left - val right = candidate.right - if (left == null || right == null) return false - val templateLeft = template.left - val templateRight = template.right - if (templateLeft == null || templateRight == null) return false - if (templateRight === expression) { - return areEquivalent(left, templateLeft) && areEquivalent(right.negate(false), templateRight) - } - if (!areEquivalent(right, templateRight)) return false - if (templateLeft === expression) { - return areEquivalent(left.negate(false), templateLeft) - } - if (templateLeft !is KtBinaryExpression || templateLeft.operationToken !== KtTokens.ANDAND) return false - return isOppositeCondition(left, templateLeft, expression) - } - - private fun areEquivalent(e1: KtElement, e2: KtElement): Boolean { - return PsiEquivalenceUtil.areElementsEquivalent(e1, e2, - {ref1, ref2 -> ref1.element.text.compareTo(ref2.element.text)}, - null, null, false) - } - override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { // Non-JVM is not supported now if (holder.file.module?.platform?.isJvm() != true) return PsiElementVisitor.EMPTY_VISITOR @@ -501,28 +248,6 @@ class KotlinConstantConditionsInspection : AbstractKotlinInspection() { return false } - private fun isCompilationWarning(anchor: KtElement): Boolean - { - val context = anchor.analyze(BodyResolveMode.FULL) - if (context.diagnostics.forElement(anchor).any - { it.factory == Errors.CAST_NEVER_SUCCEEDS - || it.factory == Errors.SENSELESS_COMPARISON - || it.factory == Errors.SENSELESS_NULL_IN_WHEN - || it.factory == Errors.USELESS_IS_CHECK - || it.factory == Errors.DUPLICATE_LABEL_IN_WHEN } - ) { - return true - } - - val rootElement = anchor.containingFile - val suppressionCache = KotlinCacheService.getInstance(anchor.project).getSuppressionCache() - return suppressionCache.isSuppressed(anchor, rootElement, "CAST_NEVER_SUCCEEDS", Severity.WARNING) || - suppressionCache.isSuppressed(anchor, rootElement, "SENSELESS_COMPARISON", Severity.WARNING) || - suppressionCache.isSuppressed(anchor, rootElement, "SENSELESS_NULL_IN_WHEN", Severity.WARNING) || - suppressionCache.isSuppressed(anchor, rootElement, "USELESS_IS_CHECK", Severity.WARNING) || - suppressionCache.isSuppressed(anchor, rootElement, "DUPLICATE_LABEL_IN_WHEN", Severity.WARNING) - } - private fun shouldSuppressWhenCondition( cv: ConstantValue, condition: KtWhenCondition @@ -547,4 +272,294 @@ class KotlinConstantConditionsInspection : AbstractKotlinInspection() { } return false } + + companion object { + private fun areEquivalent(e1: KtElement, e2: KtElement): Boolean { + return PsiEquivalenceUtil.areElementsEquivalent(e1, e2, + {ref1, ref2 -> ref1.element.text.compareTo(ref2.element.text)}, + null, null, false) + } + + private tailrec fun isOppositeCondition(candidate: KtExpression?, template: KtBinaryExpression, expression: KtExpression): Boolean { + if (candidate !is KtBinaryExpression || candidate.operationToken !== KtTokens.ANDAND) return false + val left = candidate.left + val right = candidate.right + if (left == null || right == null) return false + val templateLeft = template.left + val templateRight = template.right + if (templateLeft == null || templateRight == null) return false + if (templateRight === expression) { + return areEquivalent(left, templateLeft) && areEquivalent(right.negate(false), templateRight) + } + if (!areEquivalent(right, templateRight)) return false + if (templateLeft === expression) { + return areEquivalent(left.negate(false), templateLeft) + } + if (templateLeft !is KtBinaryExpression || templateLeft.operationToken !== KtTokens.ANDAND) return false + return isOppositeCondition(left, templateLeft, expression) + } + + private fun hasOppositeCondition(whenExpression: KtWhenExpression, topAnd: KtBinaryExpression, expression: KtExpression): Boolean { + for (entry in whenExpression.entries) { + for (condition in entry.conditions) { + if (condition is KtWhenConditionWithExpression) { + val candidate = condition.expression + if (candidate === topAnd) return false + if (isOppositeCondition(candidate, topAnd, expression)) return true + } + } + } + return false + } + + /** + * Returns true if expression is part of when condition expression that looks like + * ``` + * when { + * a && b -> ... + * a && !b -> ... + * } + * ``` + * In this case, !b could be reported as 'always true' but such warnings are annoying + */ + private fun isPairingConditionInWhen(expression: KtExpression): Boolean { + val parent = expression.parent + if (parent is KtBinaryExpression && parent.operationToken == KtTokens.ANDAND) { + var topAnd: KtBinaryExpression = parent + while (true) { + val nextParent = topAnd.parent + if (nextParent is KtBinaryExpression && nextParent.operationToken == KtTokens.ANDAND) { + topAnd = nextParent + } else break + } + val topAndParent = topAnd.parent + if (topAndParent is KtWhenConditionWithExpression) { + val whenExpression = (topAndParent.parent as? KtWhenEntry)?.parent as? KtWhenExpression + if (whenExpression != null && hasOppositeCondition(whenExpression, topAnd, expression)) { + return true + } + } + } + return false + } + + private fun isCompilationWarning(anchor: KtElement): Boolean + { + val context = anchor.analyze(BodyResolveMode.FULL) + if (context.diagnostics.forElement(anchor).any + { it.factory == Errors.CAST_NEVER_SUCCEEDS + || it.factory == Errors.SENSELESS_COMPARISON + || it.factory == Errors.SENSELESS_NULL_IN_WHEN + || it.factory == Errors.USELESS_IS_CHECK + || it.factory == Errors.DUPLICATE_LABEL_IN_WHEN } + ) { + return true + } + + val rootElement = anchor.containingFile + val suppressionCache = KotlinCacheService.getInstance(anchor.project).getSuppressionCache() + return suppressionCache.isSuppressed(anchor, rootElement, "CAST_NEVER_SUCCEEDS", Severity.WARNING) || + suppressionCache.isSuppressed(anchor, rootElement, "SENSELESS_COMPARISON", Severity.WARNING) || + suppressionCache.isSuppressed(anchor, rootElement, "SENSELESS_NULL_IN_WHEN", Severity.WARNING) || + suppressionCache.isSuppressed(anchor, rootElement, "USELESS_IS_CHECK", Severity.WARNING) || + suppressionCache.isSuppressed(anchor, rootElement, "DUPLICATE_LABEL_IN_WHEN", Severity.WARNING) + } + + private fun isCallToMethod(call: KtCallExpression, packageName: String, methodName: String): Boolean { + val descriptor = call.resolveToCall()?.resultingDescriptor ?: return false + if (descriptor.name.asString() != methodName) return false + val packageFragment = descriptor.containingDeclaration as? PackageFragmentDescriptor ?: return false + return packageFragment.fqName.asString() == packageName + } + + // Do not report x.let { true } or x.let { false } as it's pretty evident + private fun isLetConstant(expr: KtExpression): Boolean { + val call = (expr as? KtQualifiedExpression)?.selectorExpression as? KtCallExpression ?: return false + if (!isCallToMethod(call, "kotlin", "let")) return false + val lambda = call.lambdaArguments.singleOrNull()?.getLambdaExpression() ?: return false + return lambda.bodyExpression?.statements?.singleOrNull() is KtConstantExpression + } + + // Do not report on also, as it always returns the qualifier. If necessary, qualifier itself will be reported + private fun isAlsoChain(expr: KtExpression): Boolean { + val call = (expr as? KtQualifiedExpression)?.selectorExpression as? KtCallExpression ?: return false + return isCallToMethod(call, "kotlin", "also") + } + + private fun isAssertion(parent: PsiElement?, value: Boolean): Boolean { + return when (parent) { + is KtBinaryExpression -> + (parent.operationToken == KtTokens.ANDAND || parent.operationToken == KtTokens.OROR) && isAssertion(parent.parent, value) + is KtParenthesizedExpression -> + isAssertion(parent.parent, value) + is KtPrefixExpression -> + parent.operationToken == KtTokens.EXCL && isAssertion(parent.parent, !value) + is KtValueArgument -> { + if (!value) return false + val valueArgList = parent.parent as? KtValueArgumentList ?: return false + val call = valueArgList.parent as? KtCallExpression ?: return false + val descriptor = call.resolveToCall()?.resultingDescriptor ?: return false + val name = descriptor.name.asString() + if (name != "assert" && name != "require" && name != "check") return false + val pkg = descriptor.containingDeclaration as? PackageFragmentDescriptor ?: return false + return pkg.fqName.asString() == "kotlin" + } + else -> false + } + } + + private fun hasWritesTo(block: PsiElement?, variable: KtProperty): Boolean { + return !PsiTreeUtil.processElements(block, KtSimpleNameExpression::class.java) { ref -> + val write = ref.mainReference.isReferenceTo(variable) && ref.readWriteAccess(false).isWrite + !write + } + } + + private fun isUpdateChain(expression: KtExpression): Boolean { + // x = x or ..., etc. + if (expression !is KtSimpleNameExpression) return false + val binOp = expression.parent as? KtBinaryExpression ?: return false + val op = binOp.operationReference.text + if (op != "or" && op != "and" && op != "xor" && op != "||" && op != "&&") return false + val assignment = binOp.parent as? KtBinaryExpression ?: return false + if (assignment.operationToken != KtTokens.EQ) return false + val left = assignment.left + if (left !is KtSimpleNameExpression || !left.textMatches(expression.text)) return false + val variable = expression.mainReference.resolve() as? KtProperty ?: return false + val varParent = variable.parent as? KtBlockExpression ?: return false + var context: PsiElement = assignment + var block = context.parent + while (block is KtContainerNode || + block is KtBlockExpression && block.statements.first() == context || + block is KtIfExpression && block.then?.parent == context && block.`else` == null && !hasWritesTo(block.condition, variable) + ) { + context = block + block = context.parent + } + if (block !== varParent) return false + var curExpression = variable.nextSibling + while (curExpression != context) { + if (hasWritesTo(curExpression, variable)) return false + curExpression = curExpression.nextSibling + } + return true + } + + fun shouldSuppress(value: DfType, expression: KtExpression): Boolean { + val constant = when(value) { + DfTypes.NULL -> ConstantValue.NULL + DfTypes.TRUE -> ConstantValue.TRUE + DfTypes.FALSE -> ConstantValue.FALSE + DfTypes.intValue(0), DfTypes.longValue(0) -> ConstantValue.ZERO + else -> ConstantValue.UNKNOWN + } + return shouldSuppress(constant, expression) + } + + private fun shouldSuppress(value: ConstantValue, expression: KtExpression): Boolean { + // TODO: do something with always false branches in exhaustive when statements + // TODO: return x && y.let {return...} + var parent = expression.parent + if (parent is KtDotQualifiedExpression && parent.selectorExpression == expression) { + // Will be reported for parent qualified expression + return true + } + while (parent is KtParenthesizedExpression) { + parent = parent.parent + } + if (expression is KtConstantExpression || + // If result of initialization is constant, then the initializer will be reported + expression is KtProperty || + // If result of assignment is constant, then the right-hand part will be reported + expression is KtBinaryExpression && expression.operationToken == KtTokens.EQ || + // Negation operand: negation itself will be reported + (parent as? KtPrefixExpression)?.operationToken == KtTokens.EXCL + ) { + return true + } + if (expression is KtBinaryExpression && expression.operationToken == KtTokens.ELVIS) { + // Left part of Elvis is Nothing?, so the right part is always executed + // Could be caused by code like return x?.let { return ... } ?: true + // While inner "return" is redundant, the "always true" warning is confusing + // probably separate inspection could report extra "return" + if (expression.left?.getKotlinType()?.isNullableNothing() == true) { + return true + } + } + if (isAlsoChain(expression) || isLetConstant(expression) || isUpdateChain(expression)) return true + when (value) { + ConstantValue.TRUE -> { + if (isSmartCastNecessary(expression, true)) return true + if (isPairingConditionInWhen(expression)) return true + if (isAssertion(parent, true)) return true + } + ConstantValue.FALSE -> { + if (isSmartCastNecessary(expression, false)) return true + if (isAssertion(parent, false)) return true + } + ConstantValue.ZERO -> { + if (expression.readWriteAccess(false).isWrite) { + // like if (x == 0) x++, warning would be somewhat annoying + return true + } + if (expression is KtDotQualifiedExpression && expression.selectorExpression?.textMatches("ordinal") == true) { + var receiver: KtExpression? = expression.receiverExpression + if (receiver is KtQualifiedExpression) { + receiver = receiver.selectorExpression + } + if (receiver is KtSimpleNameExpression && receiver.mainReference.resolve() is KtEnumEntry) { + // ordinal() call on explicit enum constant + return true + } + } + val bindingContext = expression.analyze() + if (ConstantExpressionEvaluator.getConstant(expression, bindingContext) != null) return true + if (expression is KtSimpleNameExpression && + (parent is KtValueArgument || parent is KtContainerNode && parent.parent is KtArrayAccessExpression) + ) { + // zero value is passed as argument to another method or used for array access. Often, such a warning is annoying + return true + } + } + ConstantValue.NULL -> { + if (parent is KtProperty && parent.typeReference == null && expression is KtSimpleNameExpression) { + // initialize other variable with null to copy type, like + // var x1 : X = null + // var x2 = x1 -- let's suppress this + return true + } + if (expression is KtBinaryExpressionWithTypeRHS && expression.left.isNull()) { + // like (null as? X) + return true + } + if (parent is KtBinaryExpression) { + val token = parent.operationToken + if ((token === KtTokens.EQEQ || token === KtTokens.EXCLEQ || token === KtTokens.EQEQEQ || token === KtTokens.EXCLEQEQEQ) && + (parent.left?.isNull() == true || parent.right?.isNull() == true) + ) { + // like if (x == null) when 'x' is known to be null: report 'always true' instead + return true + } + } + val kotlinType = expression.getKotlinType() + if (kotlinType.toDfType(expression) == DfTypes.NULL) { + // According to type system, nothing but null could be stored in such an expression (likely "Void?" type) + return true + } + } + else -> {} + } + if (expression is KtSimpleNameExpression) { + val target = expression.mainReference.resolve() + if (target is KtProperty && !target.isVar && target.initializer is KtConstantExpression) { + // suppress warnings uses of boolean constant like 'val b = true' + return true + } + } + if (isCompilationWarning(expression)) { + return true + } + return expression.isUsedAsStatement(expression.analyze(BodyResolveMode.FULL)) + } + } } \ No newline at end of file diff --git a/plugins/kotlin/jvm-debugger/evaluation/src/org/jetbrains/kotlin/idea/debugger/dfaassist/KotlinDfaAssistProvider.kt b/plugins/kotlin/jvm-debugger/evaluation/src/org/jetbrains/kotlin/idea/debugger/dfaassist/KotlinDfaAssistProvider.kt index ec5e4b460863..738e4b7506c5 100644 --- a/plugins/kotlin/jvm-debugger/evaluation/src/org/jetbrains/kotlin/idea/debugger/dfaassist/KotlinDfaAssistProvider.kt +++ b/plugins/kotlin/jvm-debugger/evaluation/src/org/jetbrains/kotlin/idea/debugger/dfaassist/KotlinDfaAssistProvider.kt @@ -123,16 +123,18 @@ class KotlinDfaAssistProvider : DfaAssistProvider { val hints = hashMapOf() override fun beforePush(args: Array, value: DfaValue, anchor: DfaAnchor, state: DfaMemoryState) { + val dfType = state.getDfType(value) var psi = when (anchor) { is KotlinAnchor.KotlinExpressionAnchor -> { - if (shouldTrackExpressionValue(anchor.expression)) anchor.expression + if (shouldTrackExpressionValue(anchor.expression) && + !KotlinConstantConditionsInspection.shouldSuppress(dfType, anchor.expression) + ) anchor.expression else return } is KotlinAnchor.KotlinWhenConditionAnchor -> anchor.condition else -> return } var hint = DfaHint.ANY_VALUE - val dfType = state.getDfType(value) if (dfType === DfTypes.TRUE) { hint = DfaHint.TRUE } else if (dfType === DfTypes.FALSE) { diff --git a/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/KotlinDfaAssistTest.kt b/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/KotlinDfaAssistTest.kt index c35f5b9e9673..0d6cb6b7ebdb 100644 --- a/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/KotlinDfaAssistTest.kt +++ b/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/KotlinDfaAssistTest.kt @@ -42,6 +42,7 @@ class KotlinDfaAssistTest : DfaAssistTest() { if (y || x/*TRUE*/) {} var z: Boolean z = x/*TRUE*/ + var b = true }""") { vm, frame -> frame.addVariable("x", MockBooleanValue(vm, true)) } }