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:
committed by
Space Team
parent
bd46bb02c1
commit
7fc3e60854
+6
@@ -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,
|
||||
|
||||
+4
@@ -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
|
||||
}
|
||||
|
||||
+5
@@ -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,
|
||||
|
||||
+1
@@ -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>()
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -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)
|
||||
|
||||
+6
@@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -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>
|
||||
|
||||
+24
@@ -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)
|
||||
}
|
||||
}
|
||||
+89
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
-62
@@ -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)
|
||||
}
|
||||
+2
@@ -1,3 +1,5 @@
|
||||
// FIR_IDENTICAL
|
||||
|
||||
import kotlin.reflect.*
|
||||
|
||||
class Foo(val prop: Any) {
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user