mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[kotlin] KTIJ-37502 Fix FP to properly match stdlib/custom methods by using CallableIds
GitOrigin-RevId: 29871776c86ec26aa7fb8f0dcd4f1204b76cdea9
This commit is contained in:
committed by
intellij-monorepo-bot
parent
c0399afa5b
commit
8ab271bc5c
+49
-60
@@ -11,16 +11,18 @@ import org.jetbrains.kotlin.analysis.api.KaNonPublicApi
|
||||
import org.jetbrains.kotlin.analysis.api.KaSession
|
||||
import org.jetbrains.kotlin.analysis.api.resolution.successfulFunctionCallOrNull
|
||||
import org.jetbrains.kotlin.analysis.api.resolution.symbol
|
||||
import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol
|
||||
import org.jetbrains.kotlin.analysis.api.symbols.KaFunctionSymbol
|
||||
import org.jetbrains.kotlin.analysis.api.symbols.KaNamedFunctionSymbol
|
||||
import org.jetbrains.kotlin.analysis.api.types.KaUsualClassType
|
||||
import org.jetbrains.kotlin.builtins.StandardNames
|
||||
import org.jetbrains.kotlin.idea.base.psi.safeDeparenthesize
|
||||
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
|
||||
import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.KotlinApplicableInspectionBase
|
||||
import org.jetbrains.kotlin.idea.codeinsight.api.applicable.inspections.KotlinModCommandQuickFix
|
||||
import org.jetbrains.kotlin.idea.references.mainReference
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.name.CallableId
|
||||
import org.jetbrains.kotlin.name.ClassId
|
||||
import org.jetbrains.kotlin.name.Name
|
||||
import org.jetbrains.kotlin.name.StandardClassIds
|
||||
import org.jetbrains.kotlin.psi.KtBinaryExpression
|
||||
import org.jetbrains.kotlin.psi.KtCallExpression
|
||||
@@ -67,12 +69,12 @@ internal class VerboseNullabilityAndEmptinessInspection :
|
||||
override fun isApplicableByPsi(element: KtBinaryExpression): Boolean {
|
||||
val nullCheckExpression = findNullCheckExpression(element)
|
||||
val nullCheck = getNullCheck(nullCheckExpression) ?: return false
|
||||
|
||||
|
||||
val binaryExpression = findBinaryExpression(nullCheckExpression) ?: return false
|
||||
val operationToken = binaryExpression.operationToken
|
||||
|
||||
if (!isValidOperatorCombination(nullCheck.isEqualNull, operationToken)) return false
|
||||
|
||||
|
||||
val contentCheckExpression = findContentCheckExpression(nullCheckExpression, binaryExpression) ?: return false
|
||||
val contentCheck = getContentCheck(contentCheckExpression) ?: return false
|
||||
|
||||
@@ -80,13 +82,13 @@ internal class VerboseNullabilityAndEmptinessInspection :
|
||||
|
||||
val replacementName = contentCheck.data.replacementName
|
||||
if (isInsideFunctionImplementation(element, replacementName)) return false
|
||||
|
||||
|
||||
return super.isApplicableByPsi(element)
|
||||
}
|
||||
|
||||
private fun isValidOperatorCombination(isEqualNull: Boolean, operationToken: IElementType): Boolean {
|
||||
return (!isEqualNull && operationToken == KtTokens.ANDAND) || // a != null && a.isNotEmpty()
|
||||
(isEqualNull && operationToken == KtTokens.OROR) // a == null || a.isEmpty()
|
||||
(isEqualNull && operationToken == KtTokens.OROR) // a == null || a.isEmpty()
|
||||
}
|
||||
|
||||
override fun KaSession.prepareContext(element: KtBinaryExpression): Context? {
|
||||
@@ -117,8 +119,8 @@ internal class VerboseNullabilityAndEmptinessInspection :
|
||||
val contentCheckFunction = resolveToFunctionSymbol(contentCheck.call) ?: return null
|
||||
if (!checkTargetFunctionReceiver(contentCheck.call)) return null
|
||||
|
||||
val overriddenFunctions = getAllOverriddenSymbols(contentCheckFunction).filterIsInstance<KaFunctionSymbol>() + contentCheckFunction
|
||||
if (overriddenFunctions.none { getCallableFqName(it) in contentCheck.data.callableNames }) return null
|
||||
val overriddenFunctions = contentCheckFunction.allOverriddenSymbols + contentCheckFunction
|
||||
if (overriddenFunctions.none { it.callableId in contentCheck.data.callableIds }) return null
|
||||
|
||||
val hasExplicitReceiver = contentCheck.target.singleOrNull()?.expression !is KtCallExpression
|
||||
val replacementName = contentCheck.data.replacementName
|
||||
@@ -197,8 +199,7 @@ internal class VerboseNullabilityAndEmptinessInspection :
|
||||
*/
|
||||
private fun KaSession.checkTargetFunctionReceiver(expression: KtCallExpression): Boolean {
|
||||
val call = expression.resolveToCall()?.successfulFunctionCallOrNull() ?: return false
|
||||
val receiverSymbol = call.partiallyAppliedSymbol
|
||||
val type = receiverSymbol.dispatchReceiver?.type ?: receiverSymbol.extensionReceiver?.type ?: return false
|
||||
val type = call.dispatchReceiver?.type ?: call.extensionReceiver?.type ?: return false
|
||||
|
||||
val isNotNullable = !type.isMarkedNullable
|
||||
val isNotPrimitiveArray = type !is KaUsualClassType || !type.isPrimitiveArray()
|
||||
@@ -322,7 +323,7 @@ internal class VerboseNullabilityAndEmptinessInspection :
|
||||
return ContentCheck(target, callExpression, contentFunction, contentFunction.isPositiveCheck xor isNegated)
|
||||
})
|
||||
|
||||
internal class ContentFunction(val isPositiveCheck: Boolean, val replacementName: String, vararg val callableNames: String)
|
||||
internal class ContentFunction(val isPositiveCheck: Boolean, val replacementName: String, val callableIds: Set<CallableId>)
|
||||
}
|
||||
|
||||
private typealias TargetChain = List<TargetChunk>
|
||||
@@ -341,28 +342,42 @@ data class TargetChunk(val expression: KtExpression) {
|
||||
}
|
||||
}
|
||||
|
||||
private val contentCheckingFunctions: Map<String, VerboseNullabilityAndEmptinessInspection.ContentFunction> = mapOf(
|
||||
"isEmpty" to VerboseNullabilityAndEmptinessInspection.ContentFunction(
|
||||
isPositiveCheck = false, replacementName = "isNullOrEmpty",
|
||||
"kotlin.collections.Collection.isEmpty",
|
||||
"kotlin.collections.Map.isEmpty",
|
||||
"kotlin.collections.isEmpty",
|
||||
"kotlin.text.isEmpty"
|
||||
),
|
||||
"isBlank" to VerboseNullabilityAndEmptinessInspection.ContentFunction(
|
||||
isPositiveCheck = false, replacementName = "isNullOrBlank",
|
||||
"kotlin.text.isBlank"
|
||||
),
|
||||
"isNotEmpty" to VerboseNullabilityAndEmptinessInspection.ContentFunction(
|
||||
isPositiveCheck = true, replacementName = "isNullOrEmpty",
|
||||
"kotlin.collections.isNotEmpty",
|
||||
"kotlin.text.isNotEmpty"
|
||||
),
|
||||
"isNotBlank" to VerboseNullabilityAndEmptinessInspection.ContentFunction(
|
||||
isPositiveCheck = true, replacementName = "isNullOrBlank",
|
||||
"kotlin.text.isNotBlank"
|
||||
private val contentCheckingFunctions: Map<String, VerboseNullabilityAndEmptinessInspection.ContentFunction> = buildMap {
|
||||
val isEmptyName = Name.identifier("isEmpty")
|
||||
val isNotEmptyName = Name.identifier("isNotEmpty")
|
||||
put(
|
||||
"isEmpty", VerboseNullabilityAndEmptinessInspection.ContentFunction(
|
||||
isPositiveCheck = false, replacementName = "isNullOrEmpty",
|
||||
setOf(
|
||||
CallableId(ClassId.topLevel(StandardNames.FqNames.collection), isEmptyName),
|
||||
CallableId(ClassId.topLevel(StandardNames.FqNames.map), isEmptyName),
|
||||
CallableId(StandardClassIds.BASE_COLLECTIONS_PACKAGE, isEmptyName),
|
||||
CallableId(StandardClassIds.BASE_TEXT_PACKAGE, isEmptyName)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
put(
|
||||
"isBlank", VerboseNullabilityAndEmptinessInspection.ContentFunction(
|
||||
isPositiveCheck = false, replacementName = "isNullOrBlank",
|
||||
setOf(CallableId(StandardClassIds.BASE_TEXT_PACKAGE, Name.identifier("isBlank")))
|
||||
)
|
||||
)
|
||||
put(
|
||||
"isNotEmpty", VerboseNullabilityAndEmptinessInspection.ContentFunction(
|
||||
isPositiveCheck = true, replacementName = "isNullOrEmpty",
|
||||
setOf(
|
||||
CallableId(StandardClassIds.BASE_COLLECTIONS_PACKAGE, isNotEmptyName),
|
||||
CallableId(StandardClassIds.BASE_TEXT_PACKAGE, isNotEmptyName)
|
||||
)
|
||||
)
|
||||
)
|
||||
put(
|
||||
"isNotBlank", VerboseNullabilityAndEmptinessInspection.ContentFunction(
|
||||
isPositiveCheck = true, replacementName = "isNullOrBlank",
|
||||
setOf(CallableId(StandardClassIds.BASE_TEXT_PACKAGE, Name.identifier("isNotBlank")))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the topmost expression that contains the null check, including any negation operators.
|
||||
@@ -461,7 +476,7 @@ private fun KaSession.resolve(chunk: TargetChunk): Any? {
|
||||
|
||||
is KtCallExpression -> {
|
||||
val call = chunk.expression.resolveToCall()?.successfulFunctionCallOrNull() ?: return null
|
||||
getSingleReceiver(call.partiallyAppliedSymbol.dispatchReceiver, call.partiallyAppliedSymbol.extensionReceiver)
|
||||
getSingleReceiver(call.dispatchReceiver, call.extensionReceiver)
|
||||
}
|
||||
|
||||
is KtNameReferenceExpression -> {
|
||||
@@ -562,33 +577,7 @@ private fun KaSession.hasSmartCast(chunk: TargetChunk): Boolean {
|
||||
*/
|
||||
private fun KaSession.resolveToFunctionSymbol(expression: KtCallExpression): KaFunctionSymbol? {
|
||||
val call = expression.resolveToCall()?.successfulFunctionCallOrNull() ?: return null
|
||||
return call.partiallyAppliedSymbol.symbol
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all overridden symbols for a function. Currently returns only the function itself.
|
||||
* This is a placeholder for potential future enhancement to handle inheritance hierarchies.
|
||||
*/
|
||||
private fun getAllOverriddenSymbols(function: KaFunctionSymbol): Sequence<KaCallableSymbol> {
|
||||
return sequenceOf(function)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get the fully qualified name of a callable function.
|
||||
* Handles built-in Kotlin functions by constructing FQ names manually.
|
||||
*/
|
||||
private fun getCallableFqName(function: KaFunctionSymbol): String? {
|
||||
return (function as? KaNamedFunctionSymbol)?.let { namedFunction ->
|
||||
// Try to get the full qualified name using a simple approach
|
||||
namedFunction.name.asString().let { name ->
|
||||
// For built-in functions, we need to construct the FQ name manually
|
||||
when (name) {
|
||||
in setOf("isEmpty", "isNotEmpty") -> "kotlin.collections.$name"
|
||||
in setOf("isBlank", "isNotBlank") -> "kotlin.text.$name"
|
||||
else -> name
|
||||
}
|
||||
}
|
||||
}
|
||||
return call.symbol
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+34
@@ -16175,6 +16175,40 @@ public abstract class K2LocalInspectionTestGenerated extends AbstractK2LocalInsp
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
@TestMetadata("../../../idea/tests/testData/inspectionsLocal/verboseNullabilityAndEmptiness/custom")
|
||||
public static class Custom extends AbstractK2LocalInspectionTest {
|
||||
@java.lang.Override
|
||||
@org.jetbrains.annotations.NotNull
|
||||
public final KotlinPluginMode getPluginMode() {
|
||||
return KotlinPluginMode.K2;
|
||||
}
|
||||
|
||||
private void runTest(String testDataFilePath) throws Exception {
|
||||
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
|
||||
}
|
||||
|
||||
@TestMetadata("customIsBlank.kt")
|
||||
public void testCustomIsBlank() throws Exception {
|
||||
runTest("../../../idea/tests/testData/inspectionsLocal/verboseNullabilityAndEmptiness/custom/customIsBlank.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("customIsEmpty.kt")
|
||||
public void testCustomIsEmpty() throws Exception {
|
||||
runTest("../../../idea/tests/testData/inspectionsLocal/verboseNullabilityAndEmptiness/custom/customIsEmpty.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("customIsNotBlank.kt")
|
||||
public void testCustomIsNotBlank() throws Exception {
|
||||
runTest("../../../idea/tests/testData/inspectionsLocal/verboseNullabilityAndEmptiness/custom/customIsNotBlank.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("customIsNotEmpty.kt")
|
||||
public void testCustomIsNotEmpty() throws Exception {
|
||||
runTest("../../../idea/tests/testData/inspectionsLocal/verboseNullabilityAndEmptiness/custom/customIsNotEmpty.kt");
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
@TestMetadata("../../../idea/tests/testData/inspectionsLocal/verboseNullabilityAndEmptiness/hashSet")
|
||||
public static class HashSet extends AbstractK2LocalInspectionTest {
|
||||
|
||||
+34
@@ -19825,6 +19825,40 @@ public abstract class LocalInspectionTestGenerated extends AbstractK1LocalInspec
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
@TestMetadata("testData/inspectionsLocal/verboseNullabilityAndEmptiness/custom")
|
||||
public static class Custom extends AbstractK1LocalInspectionTest {
|
||||
@java.lang.Override
|
||||
@org.jetbrains.annotations.NotNull
|
||||
public final KotlinPluginMode getPluginMode() {
|
||||
return KotlinPluginMode.K1;
|
||||
}
|
||||
|
||||
private void runTest(String testDataFilePath) throws Exception {
|
||||
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
|
||||
}
|
||||
|
||||
@TestMetadata("customIsBlank.kt")
|
||||
public void testCustomIsBlank() throws Exception {
|
||||
runTest("testData/inspectionsLocal/verboseNullabilityAndEmptiness/custom/customIsBlank.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("customIsEmpty.kt")
|
||||
public void testCustomIsEmpty() throws Exception {
|
||||
runTest("testData/inspectionsLocal/verboseNullabilityAndEmptiness/custom/customIsEmpty.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("customIsNotBlank.kt")
|
||||
public void testCustomIsNotBlank() throws Exception {
|
||||
runTest("testData/inspectionsLocal/verboseNullabilityAndEmptiness/custom/customIsNotBlank.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("customIsNotEmpty.kt")
|
||||
public void testCustomIsNotEmpty() throws Exception {
|
||||
runTest("testData/inspectionsLocal/verboseNullabilityAndEmptiness/custom/customIsNotEmpty.kt");
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
@TestMetadata("testData/inspectionsLocal/verboseNullabilityAndEmptiness/hashSet")
|
||||
public static class HashSet extends AbstractK1LocalInspectionTest {
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// PROBLEM: none
|
||||
class TextField {
|
||||
fun isBlank(): Boolean = false
|
||||
}
|
||||
|
||||
fun test2(field: TextField?) {
|
||||
if (<caret>field == null || field.isBlank()) {}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// PROBLEM: none
|
||||
class Comment {
|
||||
fun isEmpty(): Boolean = false
|
||||
}
|
||||
|
||||
fun test1(c: Comment?) {
|
||||
if (<caret>c == null || c.isEmpty()) {}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// PROBLEM: none
|
||||
class Message {
|
||||
fun isNotBlank(): Boolean = true
|
||||
}
|
||||
|
||||
fun test4(msg: Message?) {
|
||||
if (<caret>msg != null && msg.isNotBlank()) {}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// PROBLEM: none
|
||||
class Buffer {
|
||||
fun isNotEmpty(): Boolean = true
|
||||
}
|
||||
|
||||
fun test3(buf: Buffer?) {
|
||||
if (<caret>buf != null && buf.isNotEmpty()) {}
|
||||
}
|
||||
Reference in New Issue
Block a user