mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[kotlin-dfa] KTIJ-21930 Dataflow assistant: suppress hints when they are suppressed for original dataflow
GitOrigin-RevId: 0eea5ecaaaa329ab9c80b70bfea245ebc97f0fc1
This commit is contained in:
committed by
intellij-monorepo-bot
parent
3a45b11030
commit
b7c6103f12
+291
-276
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -123,16 +123,18 @@ class KotlinDfaAssistProvider : DfaAssistProvider {
|
||||
val hints = hashMapOf<PsiElement, DfaHint>()
|
||||
|
||||
override fun beforePush(args: Array<out DfaValue>, 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) {
|
||||
|
||||
+1
@@ -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)) }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user