[K2] Report CONCURRENT_HASH_MAP_CONTAINS_OPERATOR_ERROR for class hierarchies

Previously it wasn't reported in FIR for ConcurrentHashMap inheritors
because the receiver id hasn't matched CHM.contains id
Fixed by unwrapping origin of the call in case of fake overrides

^KT-55606 fixed
This commit is contained in:
Vsevolod Tolstopyatov
2023-01-17 09:46:25 +00:00
committed by Space Team
parent b09561c3c3
commit 21fe0e80ff
4 changed files with 291 additions and 56 deletions
@@ -15,6 +15,7 @@ import org.jetbrains.kotlin.diagnostics.reportOn
import org.jetbrains.kotlin.fir.containingClassLookupTag
import org.jetbrains.kotlin.fir.expressions.FirFunctionCall
import org.jetbrains.kotlin.fir.expressions.FirFunctionCallOrigin
import org.jetbrains.kotlin.fir.originalOrSelf
import org.jetbrains.kotlin.fir.references.toResolvedCallableSymbol
import org.jetbrains.kotlin.fir.resolve.toFirRegularClassSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirNamedFunctionSymbol
@@ -27,30 +28,39 @@ import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.util.OperatorNameConventions
/**
* This checker detects if a call by operator 'contains' convention to a Java method violates the expected contract:
* * "key in map" commonly resolves to stdlib extension that calls Map.containsKey(),
* but there's a member in ConcurrentHashMap with acceptable signature that delegates to `containsValue` instead,
* leading to an unexpected result. See KT-18053
*/
object FirJvmInconsistentOperatorFromJavaCallChecker : FirFunctionCallChecker() {
private val CONCURRENT_HASH_MAP_CALLABLE_ID = CallableId(
ClassId.fromString("java/util/concurrent/ConcurrentHashMap"),
Name.identifier("contains")
OperatorNameConventions.CONTAINS
)
override fun check(expression: FirFunctionCall, context: CheckerContext, reporter: DiagnosticReporter) {
// Filter out non-operators
if (expression.origin != FirFunctionCallOrigin.Operator) return
val callableSymbol = expression.calleeReference.toResolvedCallableSymbol() as? FirNamedFunctionSymbol ?: return
// Filter out non-contains
if (callableSymbol.name != OperatorNameConventions.CONTAINS) return
val valueParameterSymbol = callableSymbol.valueParameterSymbols.singleOrNull() ?: return
val type = valueParameterSymbol.resolvedReturnTypeRef.coneType.lowerBoundIfFlexible()
// Filter out handrolled contains with non-Any type
if (!type.isAny && !type.isNullableAny) return
if (expression.origin != FirFunctionCallOrigin.Operator || expression.origin.ordinal != 2) return
callableSymbol.check(expression.calleeReference.source, context, reporter)
}
fun FirNamedFunctionSymbol.check(source: KtSourceElement?, context: CheckerContext, reporter: DiagnosticReporter): Boolean {
if (callableId == CONCURRENT_HASH_MAP_CALLABLE_ID) {
private fun FirNamedFunctionSymbol.check(source: KtSourceElement?, context: CheckerContext, reporter: DiagnosticReporter): Boolean {
// Unwrap SubstitutionOverride origin if necessary
if (originalOrSelf().callableId == CONCURRENT_HASH_MAP_CALLABLE_ID) {
reporter.reportOn(source, FirJvmErrors.CONCURRENT_HASH_MAP_CONTAINS_OPERATOR, context)
return true
}
// Check explicitly overridden contains
val containingClass = containingClassLookupTag()?.toFirRegularClassSymbol(context.session) ?: return false
val overriddenFunctions = overriddenFunctions(containingClass, context)
for (overriddenFunction in overriddenFunctions) {