Add script class annotations generation from the base class

This commit is contained in:
Ilya Chernikov
2018-07-02 18:19:23 +02:00
parent edf13c022e
commit 0f46f62232
7 changed files with 160 additions and 4 deletions
@@ -44,6 +44,7 @@ class ScriptCodegen private constructor(
typeMapper.mapSupertype(scriptDescriptor.getSuperClassOrAny().defaultType, null).internalName,
mapSupertypesNames(typeMapper, scriptDescriptor.getSuperInterfaces(), null)
)
AnnotationCodegen.forClass(v.visitor, this, typeMapper).genAnnotations(scriptDescriptor, null)
}
override fun generateBody() {
@@ -101,6 +101,7 @@ public interface Errors {
DiagnosticFactory1<PsiElement, String> MISSING_SCRIPT_BASE_CLASS = DiagnosticFactory1.create(ERROR);
DiagnosticFactory1<PsiElement, String> MISSING_SCRIPT_RECEIVER_CLASS = DiagnosticFactory1.create(ERROR);
DiagnosticFactory1<PsiElement, String> MISSING_SCRIPT_ENVIRONMENT_PROPERTY_CLASS = DiagnosticFactory1.create(ERROR);
DiagnosticFactory2<PsiElement, String, String> INVALID_SCRIPT_TARGET_ANNOTATION_CLASS = DiagnosticFactory2.create(ERROR);
DiagnosticFactory1<PsiElement, String> PRE_RELEASE_CLASS = DiagnosticFactory1.create(ERROR);
DiagnosticFactory2<PsiElement, String, IncompatibleVersionErrorData<?>> INCOMPATIBLE_CLASS = DiagnosticFactory2.create(ERROR);
@@ -360,6 +360,7 @@ public class DefaultErrorMessages {
MAP.put(MISSING_SCRIPT_BASE_CLASS, "Cannot access script base class ''{0}''. Check your module classpath for missing or conflicting dependencies", TO_STRING);
MAP.put(MISSING_SCRIPT_RECEIVER_CLASS, "Cannot access implicit script receiver class ''{0}''. Check your module classpath for missing or conflicting dependencies", TO_STRING);
MAP.put(MISSING_SCRIPT_ENVIRONMENT_PROPERTY_CLASS, "Cannot access script environment property class ''{0}''. Check your module classpath for missing or conflicting dependencies", TO_STRING);
MAP.put(INVALID_SCRIPT_TARGET_ANNOTATION_CLASS, "Cannot access script generated target annotation class ''{0}'': {1}. Check your module classpath for missing or conflicting dependencies", TO_STRING, TO_STRING);
MAP.put(PRE_RELEASE_CLASS, "{0} is compiled by a pre-release version of Kotlin and cannot be loaded by this version of the compiler", TO_STRING);
MAP.put(INCOMPATIBLE_CLASS,
"{0} was compiled with an incompatible version of Kotlin. {1}",
@@ -18,11 +18,14 @@ package org.jetbrains.kotlin.resolve.lazy.descriptors
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.annotations.*
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory1
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.constants.ConstantValue
import org.jetbrains.kotlin.resolve.constants.ConstantValueFactory
import org.jetbrains.kotlin.resolve.descriptorUtil.builtIns
import org.jetbrains.kotlin.resolve.descriptorUtil.module
import org.jetbrains.kotlin.resolve.lazy.LazyClassContext
@@ -99,18 +102,18 @@ class LazyScriptDescriptor(
}
}
internal fun findTypeDescriptor(kClass: KClass<*>, errorDiagnostic: DiagnosticFactory1<PsiElement, String>): ClassDescriptor? =
internal fun findTypeDescriptor(kClass: KClass<*>, errorDiagnostic: DiagnosticFactory1<PsiElement, String>?): ClassDescriptor? =
findTypeDescriptor(kClass.classId, kClass.toString(), errorDiagnostic)
internal fun findTypeDescriptor(type: KType, errorDiagnostic: DiagnosticFactory1<PsiElement, String>): ClassDescriptor? =
internal fun findTypeDescriptor(type: KType, errorDiagnostic: DiagnosticFactory1<PsiElement, String>?): ClassDescriptor? =
findTypeDescriptor(type.classId, type.toString(), errorDiagnostic)
internal fun findTypeDescriptor(
classId: ClassId?, typeName: String,
errorDiagnostic: DiagnosticFactory1<PsiElement, String>
errorDiagnostic: DiagnosticFactory1<PsiElement, String>?
): ClassDescriptor? {
val typeDescriptor = classId?.let { module.findClassAcrossModuleDependencies(it) }
if (typeDescriptor == null) {
if (typeDescriptor == null && errorDiagnostic != null) {
// TODO: use PositioningStrategies to highlight some specific place in case of error, instead of treating the whole file as invalid
resolveSession.trace.report(
errorDiagnostic.on(
@@ -152,4 +155,41 @@ class LazyScriptDescriptor(
}
override fun getOuterScope(): LexicalScope = scriptOuterScope()
private val scriptClassAnnotations: () -> Annotations = resolveSession.storageManager.createLazyValue {
val baseAnnotations = baseClassDescriptor()?.annotations?.let { ann ->
FilteredAnnotations(ann) { fqname ->
val shortName = fqname.shortName().identifier
// TODO: consider more precise annotation filtering
!shortName.startsWith("KotlinScript") && !shortName.startsWith("ScriptTemplate")
}
} ?: super.annotations
val syntheticAnnotations =
scriptDefinition().targetClassAnnotations.mapNotNull { annInstance ->
try {
val annDescriptor = findTypeDescriptor(annInstance.annotationClass, null) ?: throw Exception("class not found")
val annArguments = HashMap<Name, ConstantValue<*>>()
for (arg in annInstance.annotationClass.java.declaredMethods) {
val argVal = ConstantValueFactory.createConstantValue(arg.invoke(annInstance))
?: throw Exception("Unsupported annotation argument: ${arg.name}")
annArguments[Name.identifier(arg.name)] = argVal
}
AnnotationDescriptorImpl(annDescriptor.defaultType, annArguments, source)
} catch (e: Throwable) {
resolveSession.trace.report(
Errors.INVALID_SCRIPT_TARGET_ANNOTATION_CLASS.on(
scriptInfo.script,
annInstance.toString(),
e.message ?: ""
)
)
null
}
}
if (syntheticAnnotations.isEmpty()) baseAnnotations
else CompositeAnnotations(baseAnnotations, AnnotationsImpl(syntheticAnnotations))
}
override val annotations: Annotations
get() = scriptClassAnnotations()
}
@@ -58,6 +58,10 @@ open class KotlinScriptDefinition(open val template: KClass<out Any>) : UserData
open val implicitReceivers: List<KType> get() = emptyList()
open val environmentVariables: List<Pair<String, KType>> get() = emptyList()
open val targetClassAnnotations: List<Annotation> get() = emptyList()
open val targetMethodAnnotations: List<Annotation> get() = emptyList()
}
object StandardScriptDefinition : KotlinScriptDefinition(ScriptTemplateWithArgs::class)
@@ -0,0 +1,101 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. 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.codegen
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.script.util.scriptCompilationClasspathFromContextOrStlib
import org.jetbrains.kotlin.scripting.compiler.plugin.configureScriptDefinitions
import org.jetbrains.kotlin.test.ConfigurationKind
import org.jetbrains.kotlin.test.TestJdkKind
import org.jetbrains.kotlin.utils.PathUtil
import org.jetbrains.kotlin.utils.PathUtil.KOTLIN_SCRIPTING_COMMON_JAR
import org.jetbrains.kotlin.utils.PathUtil.KOTLIN_SCRIPTING_COMPILER_PLUGIN_JAR
import org.jetbrains.kotlin.utils.PathUtil.KOTLIN_SCRIPTING_JVM_JAR
import org.jetbrains.kotlin.utils.PathUtil.KOTLIN_SCRIPTING_MISC_JAR
import java.io.File
import kotlin.reflect.KClass
import kotlin.script.experimental.annotations.KotlinScript
import kotlin.script.experimental.annotations.KotlinScriptDefaultCompilationConfiguration
import kotlin.script.experimental.api.ScriptCompileConfigurationProperties
import kotlin.script.experimental.util.TypedKey
class CustomScriptCodegenTest : CodegenTestCase() {
fun testAnnotatedDefinition() {
createScriptTestEnvironment("org.jetbrains.kotlin.codegen.TestScriptWithAnnotatedBaseClass")
loadScript("val x = 1")
val res = generateScriptClass()
assertNull(res.safeGetAnnotation(KotlinScript::class))
assertNotNull(res.safeGetAnnotation(MyScriptClassAnnotation::class))
}
private fun generateScriptClass(): Class<*> = generateClass("ScriptTest")
private fun loadScript(text: String) {
myFiles = CodegenTestFiles.create("scriptTest.kts", text, myEnvironment.project)
}
private fun createScriptTestEnvironment(vararg scriptDefinitions: String) {
if (myEnvironment != null) {
throw IllegalStateException("must not set up myEnvironment twice")
}
additionalDependencies =
scriptCompilationClasspathFromContextOrStlib("tests-common", "kotlin-stdlib") +
File(TestScriptWithReceivers::class.java.protectionDomain.codeSource.location.toURI().path) +
with(PathUtil.kotlinPathsForDistDirectory) {
arrayOf(
KOTLIN_SCRIPTING_COMPILER_PLUGIN_JAR, KOTLIN_SCRIPTING_COMMON_JAR,
KOTLIN_SCRIPTING_JVM_JAR, KOTLIN_SCRIPTING_MISC_JAR
).mapNotNull { File(libPath, it).takeIf { it.exists() } }
}
val configuration = createConfiguration(
ConfigurationKind.ALL,
TestJdkKind.MOCK_JDK,
additionalDependencies,
emptyList(),
emptyList()
)
if (scriptDefinitions.isNotEmpty()) {
configureScriptDefinitions(
scriptDefinitions.asList(), configuration, this::class.java.classLoader, MessageCollector.NONE, emptyMap()
)
}
myEnvironment = KotlinCoreEnvironment.createForTests(
testRootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES
)
}
}
private fun Class<*>.safeGetAnnotation(ann: KClass<out Annotation>): Annotation? =
getAnnotation(classLoader.loadClass(ann.qualifiedName) as Class<Annotation>)
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class MyScriptClassAnnotation
@Suppress("unused")
@KotlinScript
@MyScriptClassAnnotation()
abstract class TestScriptWithAnnotatedBaseClass
object TestScriptWithMethodAnnotationsConfiguration : ArrayList<Pair<TypedKey<*>, Any?>>(
listOf(
ScriptCompileConfigurationProperties.generatedClassAnnotations to listOf()
)
)
@Suppress("unused")
@KotlinScript
@KotlinScriptDefaultCompilationConfiguration(TestScriptWithMethodAnnotationsConfiguration::class)
abstract class TestScriptWithMethodAnnotations
@@ -79,6 +79,14 @@ abstract class KotlinScriptDefinitionAdapterFromNewAPIBase : KotlinScriptDefinit
ScriptExpectedLocation.TestsOnly
)
override val targetClassAnnotations: List<Annotation>
get() = scriptDefinition.compilationConfigurator.defaultConfiguration.getOrNull(ScriptCompileConfigurationProperties.generatedClassAnnotations)
.orEmpty()
override val targetMethodAnnotations: List<Annotation>
get() = scriptDefinition.compilationConfigurator.defaultConfiguration.getOrNull(ScriptCompileConfigurationProperties.generatedMethodAnnotations)
.orEmpty()
private val scriptingClassGetter by lazy(LazyThreadSafetyMode.PUBLICATION) {
scriptDefinition.properties.getOrNull(ScriptingEnvironmentProperties.getScriptingClass)
?: throw IllegalArgumentException("Expecting 'getScriptingClass' property in the scripting environment")