K2: add JVM checker for NO_REFLECTION_IN_CLASS_PATH warning

It's based on the existing K1 checker `JvmReflectionAPICallChecker`.

 #KT-60587 Fixed
This commit is contained in:
Alexander Udalov
2023-07-29 00:56:16 +02:00
committed by Space Team
parent bd46bb02c1
commit 7fc3e60854
15 changed files with 158 additions and 67 deletions
@@ -5055,6 +5055,12 @@ internal val KT_DIAGNOSTIC_CONVERTER = KtDiagnosticConverterBuilder.buildConvert
token,
)
}
add(FirJvmErrors.NO_REFLECTION_IN_CLASS_PATH) { firDiagnostic ->
NoReflectionInClassPathImpl(
firDiagnostic as KtPsiDiagnostic,
token,
)
}
add(FirJsErrors.IMPLEMENTING_FUNCTION_INTERFACE) { firDiagnostic ->
ImplementingFunctionInterfaceImpl(
firDiagnostic as KtPsiDiagnostic,
@@ -3523,6 +3523,10 @@ sealed interface KtFirDiagnostic<PSI : PsiElement> : KtDiagnosticWithPsi<PSI> {
override val diagnosticClass get() = JavaSamInterfaceConstructorReference::class
}
interface NoReflectionInClassPath : KtFirDiagnostic<PsiElement> {
override val diagnosticClass get() = NoReflectionInClassPath::class
}
interface ImplementingFunctionInterface : KtFirDiagnostic<KtClassOrObject> {
override val diagnosticClass get() = ImplementingFunctionInterface::class
}
@@ -4257,6 +4257,11 @@ internal class JavaSamInterfaceConstructorReferenceImpl(
token: KtLifetimeToken,
) : KtAbstractFirDiagnostic<PsiElement>(firDiagnostic, token), KtFirDiagnostic.JavaSamInterfaceConstructorReference
internal class NoReflectionInClassPathImpl(
firDiagnostic: KtPsiDiagnostic,
token: KtLifetimeToken,
) : KtAbstractFirDiagnostic<PsiElement>(firDiagnostic, token), KtFirDiagnostic.NoReflectionInClassPath
internal class ImplementingFunctionInterfaceImpl(
firDiagnostic: KtPsiDiagnostic,
token: KtLifetimeToken,
@@ -173,5 +173,6 @@ object JVM_DIAGNOSTICS_LIST : DiagnosticList("FirJvmErrors") {
PositioningStrategy.SPREAD_OPERATOR
)
val JAVA_SAM_INTERFACE_CONSTRUCTOR_REFERENCE by error<PsiElement>()
val NO_REFLECTION_IN_CLASS_PATH by warning<PsiElement>()
}
}
@@ -119,6 +119,7 @@ object FirJvmErrors {
val CONCURRENT_HASH_MAP_CONTAINS_OPERATOR by deprecationError0<PsiElement>(ProhibitConcurrentHashMapContains)
val SPREAD_ON_SIGNATURE_POLYMORPHIC_CALL by deprecationError0<PsiElement>(ProhibitSpreadOnSignaturePolymorphicCall, SourceElementPositioningStrategies.SPREAD_OPERATOR)
val JAVA_SAM_INTERFACE_CONSTRUCTOR_REFERENCE by error0<PsiElement>()
val NO_REFLECTION_IN_CLASS_PATH by warning0<PsiElement>()
init {
RootDiagnosticRendererFactory.registerFactory(FirJvmErrorsDefaultMessages)
@@ -52,6 +52,7 @@ import org.jetbrains.kotlin.fir.analysis.diagnostics.jvm.FirJvmErrors.LOCAL_JVM_
import org.jetbrains.kotlin.fir.analysis.diagnostics.jvm.FirJvmErrors.NON_DATA_CLASS_JVM_RECORD
import org.jetbrains.kotlin.fir.analysis.diagnostics.jvm.FirJvmErrors.NON_FINAL_JVM_RECORD
import org.jetbrains.kotlin.fir.analysis.diagnostics.jvm.FirJvmErrors.NON_SOURCE_REPEATED_ANNOTATION
import org.jetbrains.kotlin.fir.analysis.diagnostics.jvm.FirJvmErrors.NO_REFLECTION_IN_CLASS_PATH
import org.jetbrains.kotlin.fir.analysis.diagnostics.jvm.FirJvmErrors.OVERLOADS_ABSTRACT
import org.jetbrains.kotlin.fir.analysis.diagnostics.jvm.FirJvmErrors.OVERLOADS_ANNOTATION_CLASS_CONSTRUCTOR
import org.jetbrains.kotlin.fir.analysis.diagnostics.jvm.FirJvmErrors.OVERLOADS_INTERFACE
@@ -257,5 +258,10 @@ object FirJvmErrorsDefaultMessages : BaseDiagnosticRendererFactory() {
TO_STRING,
TO_STRING,
)
map.put(
NO_REFLECTION_IN_CLASS_PATH,
"Call uses reflection API which is not found in compilation classpath. " +
"Make sure you have kotlin-reflect.jar in the classpath"
)
}
}
@@ -11,7 +11,8 @@ import org.jetbrains.kotlin.fir.analysis.jvm.checkers.expression.*
object JvmExpressionCheckers : ExpressionCheckers() {
override val basicExpressionCheckers: Set<FirBasicExpressionChecker>
get() = setOf(
FirJvmProtectedInSuperClassCompanionCallChecker
FirJvmProtectedInSuperClassCompanionCallChecker,
FirJvmReflectionApiCallChecker,
)
override val qualifiedAccessExpressionCheckers: Set<FirQualifiedAccessExpressionChecker>
@@ -0,0 +1,24 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.fir.analysis.jvm.checkers.expression
import org.jetbrains.kotlin.KtSourceElement
import org.jetbrains.kotlin.diagnostics.DiagnosticReporter
import org.jetbrains.kotlin.diagnostics.reportOn
import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext
import org.jetbrains.kotlin.fir.analysis.checkers.expression.AbstractFirReflectionApiCallChecker
import org.jetbrains.kotlin.fir.analysis.diagnostics.jvm.FirJvmErrors
import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider
import org.jetbrains.kotlin.load.java.JvmAbi
object FirJvmReflectionApiCallChecker : AbstractFirReflectionApiCallChecker() {
override fun isWholeReflectionApiAvailable(context: CheckerContext): Boolean =
context.session.symbolProvider.getClassLikeSymbolByClassId(JvmAbi.REFLECTION_FACTORY_IMPL) != null
override fun report(source: KtSourceElement?, context: CheckerContext, reporter: DiagnosticReporter) {
reporter.reportOn(source, FirJvmErrors.NO_REFLECTION_IN_CLASS_PATH, context)
}
}
@@ -0,0 +1,89 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.fir.analysis.checkers.expression
import org.jetbrains.kotlin.KtSourceElement
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.config.AnalysisFlags
import org.jetbrains.kotlin.diagnostics.DiagnosticReporter
import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext
import org.jetbrains.kotlin.fir.analysis.checkers.toClassLikeSymbol
import org.jetbrains.kotlin.fir.expressions.FirQualifiedAccessExpression
import org.jetbrains.kotlin.fir.expressions.FirStatement
import org.jetbrains.kotlin.fir.expressions.calleeReference
import org.jetbrains.kotlin.fir.packageFqName
import org.jetbrains.kotlin.fir.references.resolved
import org.jetbrains.kotlin.fir.symbols.impl.FirCallableSymbol
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
// TODO (KT-60899): implement this checker for JS, similarly to K1's JsReflectionAPICallChecker.
abstract class AbstractFirReflectionApiCallChecker : FirBasicExpressionChecker() {
protected abstract fun isWholeReflectionApiAvailable(context: CheckerContext): Boolean
protected abstract fun report(source: KtSourceElement?, context: CheckerContext, reporter: DiagnosticReporter)
protected open fun isAllowedKClassMember(name: Name, context: CheckerContext): Boolean = when (name) {
K_CLASS_SIMPLE_NAME, K_CLASS_IS_INSTANCE -> true
K_CLASS_QUALIFIED_NAME -> context.languageVersionSettings.getFlag(AnalysisFlags.allowFullyQualifiedNameInKClass)
else -> false
}
final override fun check(expression: FirStatement, context: CheckerContext, reporter: DiagnosticReporter) {
if (isWholeReflectionApiAvailable(context)) return
// Do not report the diagnostic on kotlin-reflect sources.
if (isReflectionSource(context)) return
val resolvedReference = expression.calleeReference?.resolved ?: return
val referencedSymbol = resolvedReference.resolvedSymbol as? FirCallableSymbol ?: return
val containingClassId =
(expression as? FirQualifiedAccessExpression)?.dispatchReceiver?.typeRef?.toClassLikeSymbol(context.session)?.classId
if (containingClassId == null || containingClassId.packageFqName != StandardNames.KOTLIN_REFLECT_FQ_NAME) return
if (!isAllowedReflectionApi(referencedSymbol.name, containingClassId, context)) {
report(resolvedReference.source, context, reporter)
}
}
private fun isAllowedReflectionApi(name: Name, containingClassId: ClassId, context: CheckerContext): Boolean =
name in ALLOWED_MEMBER_NAMES ||
containingClassId == K_CLASS && isAllowedKClassMember(name, context) ||
(name.asString() == "get" || name.asString() == "set") && containingClassId in K_PROPERTY_CLASSES ||
containingClassId in ALLOWED_CLASSES
private fun isReflectionSource(context: CheckerContext): Boolean {
val containingFile = context.containingFile
return containingFile != null && containingFile.packageFqName.startsWith(StandardNames.KOTLIN_REFLECT_FQ_NAME)
}
companion object {
private val K_CLASS = ClassId.topLevel(StandardNames.FqNames.kClass.toSafe())
private val K_CLASS_SIMPLE_NAME = Name.identifier("simpleName")
private val K_CLASS_IS_INSTANCE = Name.identifier("isInstance")
private val K_CLASS_QUALIFIED_NAME = Name.identifier("qualifiedName")
private val K_PROPERTY_CLASSES: Set<ClassId> =
listOf(
StandardNames.FqNames.kProperty0,
StandardNames.FqNames.kProperty1,
StandardNames.FqNames.kProperty2,
StandardNames.FqNames.kMutableProperty0,
StandardNames.FqNames.kMutableProperty1,
StandardNames.FqNames.kMutableProperty2,
).mapTo(HashSet()) { ClassId.topLevel(it.toSafe()) }
private val ALLOWED_MEMBER_NAMES: Set<Name> =
listOf("equals", "hashCode", "toString", "invoke", "name").mapTo(HashSet(), Name::identifier)
private val ALLOWED_CLASSES: Set<ClassId> =
listOf("KType", "KTypeParameter", "KTypeProjection", "KTypeProjection.Companion", "KVariance").mapTo(HashSet()) {
ClassId(StandardNames.KOTLIN_REFLECT_FQ_NAME, FqName(it), false)
}
}
}
@@ -17,8 +17,8 @@
package org.jetbrains.kotlin.resolve.calls.checkers
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.builtins.StandardNames.KOTLIN_REFLECT_FQ_NAME
import org.jetbrains.kotlin.builtins.ReflectionTypes
import org.jetbrains.kotlin.builtins.StandardNames.KOTLIN_REFLECT_FQ_NAME
import org.jetbrains.kotlin.config.AnalysisFlags.allowFullyQualifiedNameInKClass
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.descriptors.ClassDescriptor
@@ -96,7 +96,6 @@ abstract class AbstractReflectionApiCallChecker(
private fun isReflectionSource(reportOn: PsiElement): Boolean {
val file = reportOn.containingFile as? KtFile ?: return false
val fqName = file.packageFqName.toUnsafe()
return fqName == KOTLIN_REFLECT_FQ_NAME.toUnsafe() || fqName.asString().startsWith(KOTLIN_REFLECT_FQ_NAME.asString() + ".")
return file.packageFqName.startsWith(KOTLIN_REFLECT_FQ_NAME)
}
}
@@ -1,62 +0,0 @@
import kotlin.reflect.*
class Foo(val prop: Any) {
fun func() {}
}
fun n01() = Foo::prop
fun n02() = Foo::func
fun n03() = Foo::class
fun n04(p: KProperty0<Int>) = p.get()
fun n05(p: KMutableProperty0<String>) = p.set("")
fun n07(p: KFunction<String>) = p.name
fun n08(p: KProperty1<String, Int>) = p.get("")
fun n09(p: KProperty2<String, String, Int>) = p.get("", "")
fun n10() = (Foo::func).invoke(Foo(""))
fun n11() = (Foo::func)(Foo(""))
fun y01() = Foo::prop.getter
fun y02() = Foo::class.members
fun y03() = Foo::class.simpleName
fun y04() = Foo::class.<!UNRESOLVED_REFERENCE!>properties<!>
fun <T : Any> kclass(k: KClass<*>, kt: KClass<T>) {
k.simpleName
k.qualifiedName
k.members
k.constructors
k.nestedClasses
k.objectInstance
k.typeParameters
k.supertypes
k.visibility
k.isFinal
k.isOpen
k.isAbstract
k.isSealed
k.isData
k.isInner
k.isCompanion
k.annotations
k.isInstance(42)
k == kt
k.hashCode()
k.toString()
}
fun ktype(t: KType, t2: KType) {
t.classifier
t.arguments
t.isMarkedNullable
t.annotations
t == t2
t.hashCode()
t.toString()
KTypeProjection.Companion.covariant(t)
KTypeProjection.STAR
KTypeProjection(KVariance.IN, t)
}
@@ -1,3 +1,5 @@
// FIR_IDENTICAL
import kotlin.reflect.*
class Foo(val prop: Any) {
@@ -1,5 +1,5 @@
// FIR_IDENTICAL
// !DIAGNOSTICS: -UNUSED_VARIABLE
// !DIAGNOSTICS: -UNUSED_VARIABLE -NO_REFLECTION_IN_CLASS_PATH
// !OPT_IN: kotlin.contracts.ExperimentalContracts
/*
@@ -102,6 +102,10 @@ public final class FqName {
return fqName.startsWith(segment);
}
public boolean startsWith(@NotNull FqName other) {
return fqName.startsWith(other.fqName);
}
@NotNull
public static FqName topLevel(@NotNull Name shortName) {
return new FqName(FqNameUnsafe.topLevel(shortName));
@@ -167,6 +167,17 @@ public final class FqNameUnsafe {
return fqName.regionMatches(0, segmentAsString, 0, firstDot == -1 ? Math.max(fqName.length(), segmentAsString.length()) : firstDot);
}
public boolean startsWith(@NotNull FqNameUnsafe other) {
if (isRoot()) return false;
int thisLength = fqName.length();
int otherLength = other.fqName.length();
if (thisLength < otherLength) return false;
return (thisLength == otherLength || fqName.charAt(otherLength) == '.') &&
fqName.regionMatches(0, other.fqName, 0, otherLength);
}
@NotNull
public static FqNameUnsafe topLevel(@NotNull Name shortName) {
return new FqNameUnsafe(shortName.asString(), FqName.ROOT.toUnsafe(), shortName);