Add API to get locations of collected script annotations
#KT-38404 fixed also: - Add wrapper class for the location combined with the location id - Add source code location parameters to external dependency resolvers - Add tests for locations in annotations - Add tests for order of annotation resolution for dependencies resolvers
This commit is contained in:
committed by
Ilya Chernikov
parent
1539128c3f
commit
83087291df
+47
-9
@@ -6,11 +6,13 @@
|
||||
package org.jetbrains.kotlin.scripting.resolve
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.openapi.vfs.CharsetToolkit
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.PsiManager
|
||||
import com.intellij.testFramework.LightVirtualFile
|
||||
@@ -18,6 +20,8 @@ import kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.kotlin.idea.KotlinLanguage
|
||||
import org.jetbrains.kotlin.psi.KtAnnotationEntry
|
||||
import org.jetbrains.kotlin.psi.KtFile
|
||||
import org.jetbrains.kotlin.psi.psiUtil.endOffset
|
||||
import org.jetbrains.kotlin.psi.psiUtil.startOffset
|
||||
import org.jetbrains.kotlin.scripting.definitions.KotlinScriptDefinition
|
||||
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
|
||||
import org.jetbrains.kotlin.scripting.withCorrectExtension
|
||||
@@ -41,7 +45,9 @@ internal fun VirtualFile.loadAnnotations(
|
||||
): List<Annotation> =
|
||||
// TODO_R: report error on failure to load annotation class
|
||||
ApplicationManager.getApplication().runReadAction<List<Annotation>> {
|
||||
this.getAnnotationEntries(project).construct(classLoader, acceptedAnnotations, project)
|
||||
this.getAnnotationEntries(project)
|
||||
.construct(classLoader, acceptedAnnotations, project)
|
||||
.map { it.first }
|
||||
}
|
||||
|
||||
internal fun VirtualFile.getAnnotationEntries(project: Project): Iterable<KtAnnotationEntry> {
|
||||
@@ -55,8 +61,7 @@ internal fun VirtualFile.getAnnotationEntries(project: Project): Iterable<KtAnno
|
||||
* The implementation of the SourceCode for a script located in a virtual file
|
||||
*/
|
||||
open class VirtualFileScriptSource(val virtualFile: VirtualFile, private val preloadedText: String? = null) :
|
||||
FileBasedScriptSource()
|
||||
{
|
||||
FileBasedScriptSource() {
|
||||
override val file: File get() = File(virtualFile.path)
|
||||
override val externalLocation: URL get() = URL(virtualFile.url)
|
||||
override val text: String by lazy { preloadedText ?: virtualFile.inputStream.bufferedReader().readText() }
|
||||
@@ -354,27 +359,60 @@ fun getScriptCollectedData(
|
||||
jvmGetScriptingClass(ann, contextClassLoader, hostConfiguration) as? KClass<Annotation> // TODO errors
|
||||
}
|
||||
}.orEmpty()
|
||||
val annotations = scriptFile.annotationEntries.construct(contextClassLoader, acceptedAnnotations, project)
|
||||
val annotations = scriptFile.annotationEntries.construct(
|
||||
contextClassLoader,
|
||||
acceptedAnnotations,
|
||||
project,
|
||||
scriptFile.viewProvider.document,
|
||||
scriptFile.virtualFilePath
|
||||
)
|
||||
return ScriptCollectedData(
|
||||
mapOf(
|
||||
ScriptCollectedData.foundAnnotations to annotations
|
||||
ScriptCollectedData.collectedAnnotations to annotations,
|
||||
ScriptCollectedData.foundAnnotations to annotations.map { it.annotation }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun Iterable<KtAnnotationEntry>.construct(
|
||||
classLoader: ClassLoader?, acceptedAnnotations: List<KClass<out Annotation>>, project: Project, document: Document?, filePath: String
|
||||
): List<ScriptSourceAnnotation<*>> = construct(classLoader, acceptedAnnotations, project).map { (annotation, psiAnn) ->
|
||||
ScriptSourceAnnotation(
|
||||
annotation = annotation,
|
||||
location = document?.let { document ->
|
||||
SourceCode.LocationWithId(
|
||||
codeLocationId = filePath,
|
||||
locationInText = psiAnn.location(document)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun Iterable<KtAnnotationEntry>.construct(
|
||||
classLoader: ClassLoader?, acceptedAnnotations: List<KClass<out Annotation>>, project: Project
|
||||
): List<Annotation> =
|
||||
): List<Pair<Annotation, KtAnnotationEntry>> =
|
||||
mapNotNull { psiAnn ->
|
||||
// TODO: consider advanced matching using semantic similar to actual resolving
|
||||
acceptedAnnotations.find { ann ->
|
||||
psiAnn.typeName.let { it == ann.simpleName || it == ann.qualifiedName }
|
||||
}?.let {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(constructAnnotation(
|
||||
constructAnnotation(
|
||||
psiAnn,
|
||||
(classLoader ?: ClassLoader.getSystemClassLoader()).loadClass(it.qualifiedName).kotlin as KClass<out Annotation>,
|
||||
project
|
||||
))
|
||||
) to psiAnn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PsiElement.location(document: Document): SourceCode.Location {
|
||||
val start = document.offsetToPosition(startOffset)
|
||||
val end = if (endOffset > startOffset) document.offsetToPosition(endOffset) else null
|
||||
return SourceCode.Location(start, end)
|
||||
}
|
||||
|
||||
private fun Document.offsetToPosition(offset: Int): SourceCode.Position {
|
||||
val line = getLineNumber(offset)
|
||||
val column = offset - getLineStartOffset(line)
|
||||
return SourceCode.Position(line + 1, column + 1, offset)
|
||||
}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
|
||||
@file:Fib(4)
|
||||
|
||||
println("fib(1)=${FIB_1}")
|
||||
println("fib(2)=${FIB_2}")
|
||||
println("fib(3)=${FIB_3}")
|
||||
println("fib(4)=${FIB_4}")
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
|
||||
@file:Fib(number = 4)
|
||||
@file:Fib(number = 0)
|
||||
|
||||
print("fib(4)=${FIB_4}")
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2010-2020 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.scripting.compiler.test
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import junit.framework.TestCase
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
|
||||
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
|
||||
import org.jetbrains.kotlin.script.loadScriptingPlugin
|
||||
import org.jetbrains.kotlin.scripting.compiler.plugin.TestDisposable
|
||||
import org.jetbrains.kotlin.scripting.compiler.plugin.impl.ScriptJvmCompilerFromEnvironment
|
||||
import org.jetbrains.kotlin.scripting.configuration.ScriptingConfigurationKeys
|
||||
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
|
||||
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinitionProvider
|
||||
import org.jetbrains.kotlin.test.ConfigurationKind
|
||||
import org.jetbrains.kotlin.test.KotlinTestUtils
|
||||
import org.jetbrains.kotlin.test.TestJdkKind
|
||||
import org.junit.Assert
|
||||
import java.io.File
|
||||
import kotlin.script.experimental.annotations.KotlinScript
|
||||
import kotlin.script.experimental.api.*
|
||||
import kotlin.script.experimental.host.ScriptingHostConfiguration
|
||||
import kotlin.script.experimental.host.toScriptSource
|
||||
import kotlin.script.experimental.jvm.*
|
||||
import kotlin.script.experimental.util.filterByAnnotationType
|
||||
|
||||
|
||||
private const val testDataPath = "plugins/scripting/scripting-compiler/testData/compiler/compileTimeFibonacci"
|
||||
|
||||
class CompileTimeFibonacciTest : TestCase() {
|
||||
private val testRootDisposable: Disposable = TestDisposable()
|
||||
|
||||
fun testFibonacciWithSupportedNumbersImplementsTheCorrectConstants() {
|
||||
val outputLines = runScript("supported.fib.kts")
|
||||
.valueOr { failure ->
|
||||
val message = failure.reports.joinToString("\n") { it.message }
|
||||
kotlin.test.fail("supported.fib.kts was expected to succeed:\n\n${message}")
|
||||
}
|
||||
.lines()
|
||||
.filter { it.isNotBlank() }
|
||||
|
||||
Assert.assertEquals(4, outputLines.count())
|
||||
Assert.assertEquals("fib(1)=1", outputLines[0])
|
||||
Assert.assertEquals("fib(2)=1", outputLines[1])
|
||||
Assert.assertEquals("fib(3)=2", outputLines[2])
|
||||
Assert.assertEquals("fib(4)=3", outputLines[3])
|
||||
}
|
||||
|
||||
// This tests if the annotations delivered with the correct location
|
||||
// and that scripts can return error messages at the location of the annotation
|
||||
fun testFibonacciWithUnsupportedNumbersEmitsErrorAtLocation() {
|
||||
when (val result = runScript("unsupported.fib.kts")) {
|
||||
is ResultWithDiagnostics.Success ->
|
||||
kotlin.test.fail("supported.fib.kts was expected to fail with a compiler error from refinement")
|
||||
|
||||
is ResultWithDiagnostics.Failure -> {
|
||||
val error = result.reports.first()
|
||||
|
||||
val expectedErrorMessage = """
|
||||
(plugins/scripting/scripting-compiler/testData/compiler/compileTimeFibonacci/unsupported.fib.kts:3:1) Fibonacci of non-positive numbers like 0 are not supported
|
||||
""".trimIndent()
|
||||
Assert.assertEquals(expectedErrorMessage, error.message)
|
||||
// TODO: the location is not in the diagnostics because the `MessageCollector` defined in KotlinTestUtils,
|
||||
// throws the reports as `AssertionException`s. Evaluate using a different compiler configuration.
|
||||
// Assert.assertEquals(3, error.location?.start?.line)
|
||||
// Assert.assertEquals(1, error.location?.start?.col)
|
||||
// Assert.assertEquals(3, error.location?.end?.line)
|
||||
// Assert.assertEquals(14, error.location?.end?.col)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runScript(scriptPath: String): ResultWithDiagnostics<String> {
|
||||
val source = File(testDataPath, scriptPath).toScriptSource()
|
||||
return compileScript(source)
|
||||
.onSuccess { compiled ->
|
||||
captureOut {
|
||||
val evaluator = BasicJvmScriptEvaluator()
|
||||
runBlocking {
|
||||
evaluator(compiled)
|
||||
}
|
||||
}.asSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
private fun compileScript(
|
||||
script: SourceCode
|
||||
): ResultWithDiagnostics<CompiledScript> {
|
||||
val configuration = KotlinTestUtils.newConfiguration(ConfigurationKind.NO_KOTLIN_REFLECT, TestJdkKind.FULL_JDK).apply {
|
||||
val hostConfiguration = ScriptingHostConfiguration(defaultJvmScriptingHostConfiguration)
|
||||
add(
|
||||
ScriptingConfigurationKeys.SCRIPT_DEFINITIONS,
|
||||
ScriptDefinition.FromTemplate(hostConfiguration, CompileTimeFibonacci::class, ScriptDefinition::class)
|
||||
)
|
||||
loadScriptingPlugin(this)
|
||||
}
|
||||
|
||||
val environment = KotlinCoreEnvironment.createForTests(testRootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES)
|
||||
val scriptCompiler = ScriptJvmCompilerFromEnvironment(environment)
|
||||
val scriptDefinition = ScriptDefinitionProvider.getInstance(environment.project)!!.findDefinition(script)!!
|
||||
|
||||
val scriptCompilationConfiguration = scriptDefinition.compilationConfiguration.with {
|
||||
jvm {
|
||||
dependenciesFromCurrentContext(wholeClasspath = true)
|
||||
}
|
||||
}
|
||||
|
||||
return scriptCompiler.compile(script, scriptCompilationConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
// Test Script with Compile Time Fibonacci Computation
|
||||
|
||||
@KotlinScript(
|
||||
fileExtension = "fib.kts",
|
||||
compilationConfiguration = CompileTimeFibonacciConfiguration::class
|
||||
)
|
||||
abstract class CompileTimeFibonacci
|
||||
|
||||
object CompileTimeFibonacciConfiguration : ScriptCompilationConfiguration(
|
||||
{
|
||||
fun fibUntil(number: Int): List<Int> {
|
||||
require(number > 0)
|
||||
if (number == 1) {
|
||||
return listOf(1)
|
||||
}
|
||||
if (number == 2) {
|
||||
return listOf(1, 1)
|
||||
}
|
||||
|
||||
val previous = fibUntil(number - 1)
|
||||
return previous + (previous.secondToLast() + previous.last())
|
||||
}
|
||||
|
||||
defaultImports(Fib::class)
|
||||
jvm {
|
||||
dependenciesFromCurrentContext(wholeClasspath = true)
|
||||
}
|
||||
refineConfiguration {
|
||||
onAnnotations(Fib::class) { context: ScriptConfigurationRefinementContext ->
|
||||
val maxFibonacciNumber = context
|
||||
.collectedData
|
||||
?.get(ScriptCollectedData.collectedAnnotations)
|
||||
?.filterByAnnotationType<Fib>()
|
||||
?.mapSuccess { (fib, location) ->
|
||||
fib.number.takeIf { it > 0 }?.asSuccess()
|
||||
?: makeFailureResult(
|
||||
message = "Fibonacci of non-positive numbers like ${fib.number} are not supported",
|
||||
locationWithId = location
|
||||
)
|
||||
}
|
||||
?.valueOr { return@onAnnotations it }
|
||||
?.max() ?: return@onAnnotations context.compilationConfiguration.asSuccess()
|
||||
|
||||
val sourceCode = fibUntil(maxFibonacciNumber)
|
||||
.mapIndexed { index, number -> "val FIB_${index + 1} = $number" }
|
||||
.joinToString("\n")
|
||||
|
||||
val file = createTempFile("CompileTimeFibonacci", ".fib.kts")
|
||||
.apply {
|
||||
deleteOnExit()
|
||||
writeText(sourceCode)
|
||||
}
|
||||
|
||||
ScriptCompilationConfiguration(context.compilationConfiguration) {
|
||||
importScripts.append(file.toScriptSource())
|
||||
}.asSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@Target(AnnotationTarget.FILE)
|
||||
@Repeatable
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class Fib(val number: Int)
|
||||
|
||||
private fun <T> List<T>.secondToLast(): T = this[count() - 2]
|
||||
Reference in New Issue
Block a user