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:
Mathias Quintero
2020-05-20 13:42:08 +02:00
committed by Ilya Chernikov
parent 1539128c3f
commit 83087291df
19 changed files with 550 additions and 85 deletions
@@ -38,10 +38,10 @@ object ScriptWithMavenDepsConfiguration : ScriptCompilationConfiguration(
private val resolver = CompoundDependenciesResolver(FileSystemDependenciesResolver(), MavenDependenciesResolver()) private val resolver = CompoundDependenciesResolver(FileSystemDependenciesResolver(), MavenDependenciesResolver())
fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics<ScriptCompilationConfiguration> { fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics<ScriptCompilationConfiguration> {
val annotations = context.collectedData?.get(ScriptCollectedData.foundAnnotations)?.takeIf { it.isNotEmpty() } val annotations = context.collectedData?.get(ScriptCollectedData.collectedAnnotations)?.takeIf { it.isNotEmpty() }
?: return context.compilationConfiguration.asSuccess() ?: return context.compilationConfiguration.asSuccess()
return runBlocking { return runBlocking {
resolver.resolveFromAnnotations(annotations) resolver.resolveFromScriptSourceAnnotations(annotations)
}.onSuccess { }.onSuccess {
context.compilationConfiguration.with { context.compilationConfiguration.with {
dependencies.append(JvmDependency(it)) dependencies.append(JvmDependency(it))
@@ -33,6 +33,14 @@ data class ScriptDiagnostic(
*/ */
enum class Severity { FATAL, ERROR, WARNING, INFO, DEBUG } enum class Severity { FATAL, ERROR, WARNING, INFO, DEBUG }
constructor(
code: Int,
message: String,
severity: Severity = Severity.ERROR,
locationWithId: SourceCode.LocationWithId?,
exception: Throwable? = null
) : this(code, message, severity, locationWithId?.codeLocationId, locationWithId?.locationInText, exception)
override fun toString(): String = render() override fun toString(): String = render()
/** /**
@@ -211,6 +219,12 @@ fun makeFailureResult(vararg reports: ScriptDiagnostic): ResultWithDiagnostics.F
fun makeFailureResult(message: String, path: String? = null, location: SourceCode.Location? = null): ResultWithDiagnostics.Failure = fun makeFailureResult(message: String, path: String? = null, location: SourceCode.Location? = null): ResultWithDiagnostics.Failure =
ResultWithDiagnostics.Failure(message.asErrorDiagnostics(ScriptDiagnostic.unspecifiedError, path, location)) ResultWithDiagnostics.Failure(message.asErrorDiagnostics(ScriptDiagnostic.unspecifiedError, path, location))
/**
* Makes Failure result with diagnostic [message] with optional [locationWithId]
*/
fun makeFailureResult(message: String, locationWithId: SourceCode.LocationWithId?): ResultWithDiagnostics.Failure =
ResultWithDiagnostics.Failure(message.asErrorDiagnostics(ScriptDiagnostic.unspecifiedError, locationWithId))
/** /**
* Converts the receiver Throwable to the Failure results wrapper with optional [customMessage], [path] and [location] * Converts the receiver Throwable to the Failure results wrapper with optional [customMessage], [path] and [location]
*/ */
@@ -223,6 +237,17 @@ fun Throwable.asDiagnostics(
): ScriptDiagnostic = ): ScriptDiagnostic =
ScriptDiagnostic(code, customMessage ?: message ?: "$this", severity, path, location, this) ScriptDiagnostic(code, customMessage ?: message ?: "$this", severity, path, location, this)
/**
* Converts the receiver Throwable to the Failure results wrapper with optional [customMessage], [locationWithId]
*/
fun Throwable.asDiagnostics(
code: Int = ScriptDiagnostic.unspecifiedException,
customMessage: String? = null,
locationWithId: SourceCode.LocationWithId?,
severity: ScriptDiagnostic.Severity = ScriptDiagnostic.Severity.ERROR
): ScriptDiagnostic =
ScriptDiagnostic(code, customMessage ?: message ?: "$this", severity, locationWithId, this)
/** /**
* Converts the receiver String to error diagnostic report with optional [path] and [location] * Converts the receiver String to error diagnostic report with optional [path] and [location]
*/ */
@@ -233,6 +258,15 @@ fun String.asErrorDiagnostics(
): ScriptDiagnostic = ): ScriptDiagnostic =
ScriptDiagnostic(code, this, ScriptDiagnostic.Severity.ERROR, path, location) ScriptDiagnostic(code, this, ScriptDiagnostic.Severity.ERROR, path, location)
/**
* Converts the receiver String to error diagnostic report with optional [locationWithId]
*/
fun String.asErrorDiagnostics(
code: Int = ScriptDiagnostic.unspecifiedError,
locationWithId: SourceCode.LocationWithId?
): ScriptDiagnostic =
ScriptDiagnostic(code, this, ScriptDiagnostic.Severity.ERROR, locationWithId)
/** /**
* Extracts the result value from the receiver wrapper or null if receiver represents a Failure * Extracts the result value from the receiver wrapper or null if receiver represents a Failure
*/ */
@@ -51,8 +51,30 @@ interface SourceCode {
* @param end optional range location end position (after the last char) * @param end optional range location end position (after the last char)
*/ */
data class Location(val start: Position, val end: Position? = null) : Serializable data class Location(val start: Position, val end: Position? = null) : Serializable
/**
* The source code location including the path to the file
* @param codeLocationId the file path or other script location identifier (see [SourceCode.locationId])
* @param locationInText concrete location of the source code in file
*/
data class LocationWithId(val codeLocationId: String, val locationInText: Location) : Serializable
} }
/**
* Annotation found during script source parsing along with its location
*/
data class ScriptSourceAnnotation<out A : Annotation>(
/**
* Annotation found during script source parsing
*/
val annotation: A,
/**
* Location of annotation is script
*/
val location: SourceCode.LocationWithId?
)
/** /**
* The interface for the source code located externally * The interface for the source code located externally
*/ */
@@ -91,6 +113,13 @@ class ScriptCollectedData(properties: Map<PropertiesCollection.Key<*>, Any>) : P
*/ */
val ScriptCollectedDataKeys.foundAnnotations by PropertiesCollection.key<List<Annotation>>() val ScriptCollectedDataKeys.foundAnnotations by PropertiesCollection.key<List<Annotation>>()
/**
* The script file-level annotations and their locations found during script source parsing
*/
val ScriptCollectedDataKeys.collectedAnnotations by PropertiesCollection.key<List<ScriptSourceAnnotation<*>>>(getDefaultValue = {
get(ScriptCollectedData.foundAnnotations)?.map { ScriptSourceAnnotation(it, null) }
})
/** /**
* The facade to the script data for compilation configuration refinement callbacks * The facade to the script data for compilation configuration refinement callbacks
*/ */
@@ -150,4 +179,4 @@ data class ScriptEvaluationConfigurationRefinementContext(
val compiledScript: CompiledScript, val compiledScript: CompiledScript,
val evaluationConfiguration: ScriptEvaluationConfiguration, val evaluationConfiguration: ScriptEvaluationConfiguration,
val contextData: ScriptEvaluationContextData? = null val contextData: ScriptEvaluationContextData? = null
) )
@@ -0,0 +1,15 @@
/*
* 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 kotlin.script.experimental.util
import kotlin.script.experimental.api.*
inline fun <reified A : Annotation> Iterable<ScriptSourceAnnotation<*>>.filterByAnnotationType(
): List<ScriptSourceAnnotation<A>> = filter { it.annotation is A }
.map {
@Suppress("UNCHECKED_CAST")
it as ScriptSourceAnnotation<A>
}
@@ -23,6 +23,7 @@ dependencies {
testCompile(projectTests(":kotlin-scripting-dependencies")) testCompile(projectTests(":kotlin-scripting-dependencies"))
testCompile(commonDep("junit")) testCompile(commonDep("junit"))
testRuntimeOnly("org.slf4j:slf4j-nop:1.7.30") testRuntimeOnly("org.slf4j:slf4j-nop:1.7.30")
testImplementation(kotlin("reflect"))
} }
sourceSets { sourceSets {
@@ -13,6 +13,8 @@ import org.eclipse.aether.util.repository.AuthenticationBuilder
import java.io.File import java.io.File
import java.util.* import java.util.*
import kotlin.script.experimental.api.ResultWithDiagnostics import kotlin.script.experimental.api.ResultWithDiagnostics
import kotlin.script.experimental.api.SourceCode
import kotlin.script.experimental.api.asSuccess
import kotlin.script.experimental.dependencies.ExternalDependenciesResolver import kotlin.script.experimental.dependencies.ExternalDependenciesResolver
import kotlin.script.experimental.dependencies.RepositoryCoordinates import kotlin.script.experimental.dependencies.RepositoryCoordinates
import kotlin.script.experimental.dependencies.impl.makeResolveFailureResult import kotlin.script.experimental.dependencies.impl.makeResolveFailureResult
@@ -51,7 +53,10 @@ class MavenDependenciesResolver : ExternalDependenciesResolver {
if (this.isNotBlank() && this.count { it == ':' } >= 2) DefaultArtifact(this) if (this.isNotBlank() && this.count { it == ':' } >= 2) DefaultArtifact(this)
else null else null
override suspend fun resolve(artifactCoordinates: String): ResultWithDiagnostics<List<File>> { override suspend fun resolve(
artifactCoordinates: String,
sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<List<File>> {
val artifactId = artifactCoordinates.toMavenArtifact()!! val artifactId = artifactCoordinates.toMavenArtifact()!!
@@ -60,18 +65,21 @@ class MavenDependenciesResolver : ExternalDependenciesResolver {
if (deps != null) if (deps != null)
return ResultWithDiagnostics.Success(deps.map { it.file }) return ResultWithDiagnostics.Success(deps.map { it.file })
} catch (e: DependencyResolutionException) { } catch (e: DependencyResolutionException) {
return makeResolveFailureResult(e.message ?: "unknown error") return makeResolveFailureResult(e.message ?: "unknown error", sourceCodeLocation)
} }
return makeResolveFailureResult(allRepositories().map { "$it: $artifactId not found" }) return makeResolveFailureResult(allRepositories().map { "$it: $artifactId not found" }, sourceCodeLocation)
} }
private fun tryResolveEnvironmentVariable(str: String) = private fun tryResolveEnvironmentVariable(str: String) =
if (str.startsWith("$")) System.getenv(str.substring(1)) ?: str if (str.startsWith("$")) System.getenv(str.substring(1)) ?: str
else str else str
override fun addRepository(repositoryCoordinates: RepositoryCoordinates) { override fun addRepository(
repositoryCoordinates: RepositoryCoordinates,
sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<Boolean> {
val url = repositoryCoordinates.toRepositoryUrlOrNull() val url = repositoryCoordinates.toRepositoryUrlOrNull()
?: throw IllegalArgumentException("Invalid Maven repository URL: ${repositoryCoordinates}") ?: return false.asSuccess()
val repo = RemoteRepository.Builder( val repo = RemoteRepository.Builder(
repositoryCoordinates.string, repositoryCoordinates.string,
"default", "default",
@@ -91,5 +99,6 @@ class MavenDependenciesResolver : ExternalDependenciesResolver {
} }
} }
repos.add(repo.build()) repos.add(repo.build())
return true.asSuccess()
} }
} }
@@ -7,16 +7,21 @@ package kotlin.script.experimental.test
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert import org.junit.Assert
import org.junit.Ignore
import java.io.File import java.io.File
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.reflect.full.primaryConstructor
import kotlin.script.experimental.dependencies.maven.MavenDependenciesResolver import kotlin.script.experimental.dependencies.maven.MavenDependenciesResolver
import kotlin.script.experimental.api.ResultWithDiagnostics import kotlin.script.experimental.api.ResultWithDiagnostics
import kotlin.script.experimental.api.valueOrThrow import kotlin.script.experimental.api.valueOrThrow
import kotlin.script.experimental.dependencies.DependsOn
import kotlin.script.experimental.dependencies.Repository
import kotlin.script.experimental.dependencies.resolveFromAnnotations
@ExperimentalContracts @ExperimentalContracts
class MavenResolverTest : ResolversTestBase() { class MavenResolverTest : ResolversTestBase() {
fun resolveAndCheck(coordinates: String, checkBody: (Iterable<File>) -> Boolean = { true } ) { fun resolveAndCheck(coordinates: String, checkBody: (Iterable<File>) -> Boolean = { true }) {
val resolver = MavenDependenciesResolver() val resolver = MavenDependenciesResolver()
val result = runBlocking { resolver.resolve(coordinates) } val result = runBlocking { resolver.resolve(coordinates) }
if (result is ResultWithDiagnostics.Failure) { if (result is ResultWithDiagnostics.Failure) {
@@ -44,4 +49,51 @@ class MavenResolverTest : ResolversTestBase() {
files.any { it.extension == "pom" } files.any { it.extension == "pom" }
} }
} }
// Ignored - tests with custom repos often break the CI due to the caching issues
// TODO: find a way to enable iut back
@Ignore
fun ignore_testResolveFromAnnotationsWillResolveTheSameRegardlessOfAnnotationOrder() {
val dependsOnConstructor = DependsOn::class.primaryConstructor!!
val repositoryConstructor = Repository::class.primaryConstructor!!
// @DepensOn("eu.jrie.jetbrains:kotlin-shell-core:0.2")
val dependsOn = dependsOnConstructor.callBy(
mapOf(
dependsOnConstructor.parameters.first() to arrayOf("eu.jrie.jetbrains:kotlin-shell-core:0.2")
)
)
// @Repository("https://dl.bintray.com/kotlin/kotlin-eap", "https://dl.bintray.com/jakubriegel/kotlin-shell")
val repositories = repositoryConstructor.callBy(
mapOf(
repositoryConstructor.parameters.first() to arrayOf(
"https://dl.bintray.com/kotlin/kotlin-eap",
"https://dl.bintray.com/jakubriegel/kotlin-shell"
)
)
)
val annotationsWithReposFirst = listOf(repositories, dependsOn)
val annotationsWithDependsOnFirst = listOf(dependsOn, repositories)
val filesWithReposFirst = runBlocking {
MavenDependenciesResolver().resolveFromAnnotations(annotationsWithReposFirst)
}.valueOrThrow()
val filesWithDependsOnFirst = runBlocking {
MavenDependenciesResolver().resolveFromAnnotations(annotationsWithDependsOnFirst)
}.valueOrThrow()
// Tests that the jar was resolved
assert(
filesWithReposFirst.any { it.name.startsWith("kotlin-shell-core-") && it.extension == "jar" }
)
assert(
filesWithDependsOnFirst.any { it.name.startsWith("kotlin-shell-core-") && it.extension == "jar" }
)
// Test that the the same files are resolved regardless of annotation order
assertEquals(filesWithReposFirst.map { it.name }.sorted(), filesWithDependsOnFirst.map { it.name }.sorted())
}
} }
@@ -8,6 +8,8 @@ package kotlin.script.experimental.dependencies
import java.io.File import java.io.File
import kotlin.script.experimental.api.ResultWithDiagnostics import kotlin.script.experimental.api.ResultWithDiagnostics
import kotlin.script.experimental.api.ScriptDiagnostic import kotlin.script.experimental.api.ScriptDiagnostic
import kotlin.script.experimental.api.SourceCode
import kotlin.script.experimental.api.asSuccess
import kotlin.script.experimental.dependencies.impl.makeResolveFailureResult import kotlin.script.experimental.dependencies.impl.makeResolveFailureResult
class CompoundDependenciesResolver(private val resolvers: List<ExternalDependenciesResolver>) : ExternalDependenciesResolver { class CompoundDependenciesResolver(private val resolvers: List<ExternalDependenciesResolver>) : ExternalDependenciesResolver {
@@ -22,25 +24,53 @@ class CompoundDependenciesResolver(private val resolvers: List<ExternalDependenc
return resolvers.any { it.acceptsRepository(repositoryCoordinates) } return resolvers.any { it.acceptsRepository(repositoryCoordinates) }
} }
override fun addRepository(repositoryCoordinates: RepositoryCoordinates) { override fun addRepository(
if (resolvers.count { it.tryAddRepository(repositoryCoordinates) } == 0) repositoryCoordinates: RepositoryCoordinates,
throw Exception("Failed to detect repository type: $repositoryCoordinates") sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<Boolean> {
var success = false
var repositoryAdded = false
val reports = mutableListOf<ScriptDiagnostic>()
for (resolver in resolvers) {
if (resolver.acceptsRepository(repositoryCoordinates)) {
when (val resolveResult = resolver.addRepository(repositoryCoordinates, sourceCodeLocation)) {
is ResultWithDiagnostics.Success -> {
success = true
repositoryAdded = repositoryAdded || resolveResult.value
reports.addAll(resolveResult.reports)
}
is ResultWithDiagnostics.Failure -> reports.addAll(resolveResult.reports)
}
}
}
return when {
success -> repositoryAdded.asSuccess(reports)
reports.isEmpty() -> makeResolveFailureResult(
"No dependency resolver found that recognizes the repository coordinates '$repositoryCoordinates'",
sourceCodeLocation
)
else -> ResultWithDiagnostics.Failure(reports)
}
} }
override suspend fun resolve(artifactCoordinates: String): ResultWithDiagnostics<List<File>> { override suspend fun resolve(
artifactCoordinates: String,
sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<List<File>> {
val reports = mutableListOf<ScriptDiagnostic>() val reports = mutableListOf<ScriptDiagnostic>()
for (resolver in resolvers) { for (resolver in resolvers) {
if (resolver.acceptsArtifact(artifactCoordinates)) { if (resolver.acceptsArtifact(artifactCoordinates)) {
when (val resolveResult = resolver.resolve(artifactCoordinates)) { when (val resolveResult = resolver.resolve(artifactCoordinates, sourceCodeLocation)) {
is ResultWithDiagnostics.Failure -> reports.addAll(resolveResult.reports) is ResultWithDiagnostics.Failure -> reports.addAll(resolveResult.reports)
else -> return resolveResult else -> return resolveResult
} }
} }
} }
return if (reports.count() == 0) { return if (reports.count() == 0) {
makeResolveFailureResult("No suitable dependency resolver found for artifact '$artifactCoordinates'") makeResolveFailureResult("No suitable dependency resolver found for artifact '$artifactCoordinates'", sourceCodeLocation)
} else { } else {
ResultWithDiagnostics.Failure(reports) ResultWithDiagnostics.Failure(reports)
} }
@@ -7,6 +7,7 @@ package kotlin.script.experimental.dependencies
import java.io.File import java.io.File
import kotlin.script.experimental.api.ResultWithDiagnostics import kotlin.script.experimental.api.ResultWithDiagnostics
import kotlin.script.experimental.api.SourceCode
import kotlin.script.experimental.api.valueOrNull import kotlin.script.experimental.api.valueOrNull
open class RepositoryCoordinates(val string: String) open class RepositoryCoordinates(val string: String)
@@ -16,23 +17,21 @@ interface ExternalDependenciesResolver {
fun acceptsRepository(repositoryCoordinates: RepositoryCoordinates): Boolean fun acceptsRepository(repositoryCoordinates: RepositoryCoordinates): Boolean
fun acceptsArtifact(artifactCoordinates: String): Boolean fun acceptsArtifact(artifactCoordinates: String): Boolean
suspend fun resolve(artifactCoordinates: String): ResultWithDiagnostics<List<File>> suspend fun resolve(
fun addRepository(repositoryCoordinates: RepositoryCoordinates) artifactCoordinates: String,
sourceCodeLocation: SourceCode.LocationWithId? = null
): ResultWithDiagnostics<List<File>>
fun addRepository(
repositoryCoordinates: RepositoryCoordinates,
sourceCodeLocation: SourceCode.LocationWithId? = null
): ResultWithDiagnostics<Boolean>
} }
fun ExternalDependenciesResolver.acceptsRepository(repositoryCoordinates: String): Boolean = fun ExternalDependenciesResolver.acceptsRepository(repositoryCoordinates: String): Boolean =
acceptsRepository(RepositoryCoordinates(repositoryCoordinates)) acceptsRepository(RepositoryCoordinates(repositoryCoordinates))
fun ExternalDependenciesResolver.addRepository(repositoryCoordinates: String) = addRepository(RepositoryCoordinates(repositoryCoordinates)) fun ExternalDependenciesResolver.addRepository(
repositoryCoordinates: String,
suspend fun ExternalDependenciesResolver.tryResolve(artifactCoordinates: String): List<File>? = sourceCodeLocation: SourceCode.LocationWithId? = null
if (acceptsArtifact(artifactCoordinates)) resolve(artifactCoordinates).valueOrNull() else null ) = addRepository(RepositoryCoordinates(repositoryCoordinates), sourceCodeLocation)
fun ExternalDependenciesResolver.tryAddRepository(repositoryCoordinates: String) =
tryAddRepository(RepositoryCoordinates(repositoryCoordinates))
fun ExternalDependenciesResolver.tryAddRepository(repositoryCoordinates: RepositoryCoordinates) =
if (acceptsRepository(repositoryCoordinates)) {
addRepository(repositoryCoordinates)
true
} else false
@@ -7,6 +7,8 @@ package kotlin.script.experimental.dependencies
import java.io.File import java.io.File
import kotlin.script.experimental.api.ResultWithDiagnostics import kotlin.script.experimental.api.ResultWithDiagnostics
import kotlin.script.experimental.api.SourceCode
import kotlin.script.experimental.api.asSuccess
import kotlin.script.experimental.dependencies.impl.makeResolveFailureResult import kotlin.script.experimental.dependencies.impl.makeResolveFailureResult
import kotlin.script.experimental.dependencies.impl.toRepositoryUrlOrNull import kotlin.script.experimental.dependencies.impl.toRepositoryUrlOrNull
@@ -18,13 +20,24 @@ class FileSystemDependenciesResolver(vararg paths: File) : ExternalDependenciesR
private fun RepositoryCoordinates.toFilePath() = private fun RepositoryCoordinates.toFilePath() =
(this.toRepositoryUrlOrNull()?.takeIf { it.protocol == "file" }?.path ?: string).toRepositoryFileOrNull() (this.toRepositoryUrlOrNull()?.takeIf { it.protocol == "file" }?.path ?: string).toRepositoryFileOrNull()
override fun addRepository(repositoryCoordinates: RepositoryCoordinates) { override fun addRepository(
repositoryCoordinates: RepositoryCoordinates,
sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<Boolean> {
if (!acceptsRepository(repositoryCoordinates)) return false.asSuccess()
val repoDir = repositoryCoordinates.toFilePath() val repoDir = repositoryCoordinates.toFilePath()
?: throw IllegalArgumentException("Invalid repository location: '${repositoryCoordinates}'") ?: return makeResolveFailureResult("Invalid repository location: '${repositoryCoordinates}'", sourceCodeLocation)
localRepos.add(repoDir) localRepos.add(repoDir)
return true.asSuccess()
} }
override suspend fun resolve(artifactCoordinates: String): ResultWithDiagnostics<List<File>> { override suspend fun resolve(
artifactCoordinates: String,
sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<List<File>> {
if (!acceptsArtifact(artifactCoordinates)) throw IllegalArgumentException("Path is invalid") if (!acceptsArtifact(artifactCoordinates)) throw IllegalArgumentException("Path is invalid")
val messages = mutableListOf<String>() val messages = mutableListOf<String>()
@@ -38,7 +51,7 @@ class FileSystemDependenciesResolver(vararg paths: File) : ExternalDependenciesR
else -> return ResultWithDiagnostics.Success(listOf(file)) else -> return ResultWithDiagnostics.Success(listOf(file))
} }
} }
return makeResolveFailureResult(messages) return makeResolveFailureResult(messages, sourceCodeLocation)
} }
override fun acceptsArtifact(artifactCoordinates: String) = override fun acceptsArtifact(artifactCoordinates: String) =
@@ -6,9 +6,8 @@
package kotlin.script.experimental.dependencies package kotlin.script.experimental.dependencies
import java.io.File import java.io.File
import kotlin.script.experimental.api.ResultWithDiagnostics import kotlin.script.experimental.api.*
import kotlin.script.experimental.api.flatMapSuccess import kotlin.script.experimental.util.filterByAnnotationType
import kotlin.script.experimental.api.makeFailureResult
/** /**
* A common annotation that could be used in a script to denote a dependency * A common annotation that could be used in a script to denote a dependency
@@ -33,22 +32,44 @@ annotation class Repository(vararg val repositoriesCoordinates: String)
/** /**
* An extension function that configures repositories and resolves artifacts denoted by the [Repository] and [DependsOn] annotations * An extension function that configures repositories and resolves artifacts denoted by the [Repository] and [DependsOn] annotations
*/ */
suspend fun ExternalDependenciesResolver.resolveFromAnnotations(annotations: Iterable<Annotation>): ResultWithDiagnostics<List<File>> { suspend fun ExternalDependenciesResolver.resolveFromScriptSourceAnnotations(
annotations.forEach { annotation -> annotations: Iterable<ScriptSourceAnnotation<*>>
): ResultWithDiagnostics<List<File>> {
val reports = mutableListOf<ScriptDiagnostic>()
annotations.forEach { (annotation, locationWithId) ->
when (annotation) { when (annotation) {
is Repository -> { is Repository -> {
for (coordinates in annotation.repositoriesCoordinates) { for (coordinates in annotation.repositoriesCoordinates) {
if (!tryAddRepository(coordinates)) val added = addRepository(coordinates, locationWithId)
return makeFailureResult("Unrecognized repository coordinates: $coordinates") .also { reports.addAll(it.reports) }
.valueOr { return it }
if (!added)
return reports + makeFailureResult(
"Unrecognized repository coordinates: $coordinates",
locationWithId = locationWithId
)
} }
} }
is DependsOn -> {} is DependsOn -> {}
else -> return makeFailureResult("Unknown annotation ${annotation.javaClass}") else -> return reports + makeFailureResult("Unknown annotation ${annotation.javaClass}", locationWithId = locationWithId)
} }
} }
return annotations.filterIsInstance(DependsOn::class.java)
.flatMap { it.artifactsCoordinates.asIterable() } return reports + annotations.filterByAnnotationType<DependsOn>()
.flatMapSuccess { artifactCoordinates -> .flatMapSuccess { (annotation, locationWithId) ->
resolve(artifactCoordinates) annotation.artifactsCoordinates.asIterable().flatMapSuccess { artifactCoordinates ->
resolve(artifactCoordinates, locationWithId)
}
} }
} }
/**
* An extension function that configures repositories and resolves artifacts denoted by the [Repository] and [DependsOn] annotations
*/
suspend fun ExternalDependenciesResolver.resolveFromAnnotations(
annotations: Iterable<Annotation>
): ResultWithDiagnostics<List<File>> {
val scriptSourceAnnotations = annotations.map { ScriptSourceAnnotation(it, null) }
return resolveFromScriptSourceAnnotations(scriptSourceAnnotations)
}
@@ -10,11 +10,16 @@ import java.net.URL
import kotlin.script.experimental.dependencies.RepositoryCoordinates import kotlin.script.experimental.dependencies.RepositoryCoordinates
import kotlin.script.experimental.api.ResultWithDiagnostics import kotlin.script.experimental.api.ResultWithDiagnostics
import kotlin.script.experimental.api.ScriptDiagnostic import kotlin.script.experimental.api.ScriptDiagnostic
import kotlin.script.experimental.api.SourceCode
fun makeResolveFailureResult(message: String) = makeResolveFailureResult(listOf(message)) fun makeResolveFailureResult(message: String, location: SourceCode.LocationWithId? = null) = makeResolveFailureResult(listOf(message), location)
fun makeResolveFailureResult(messages: Iterable<String>) = fun makeResolveFailureResult(messages: Iterable<String>, location: SourceCode.LocationWithId? = null) =
ResultWithDiagnostics.Failure(messages.map { ScriptDiagnostic(ScriptDiagnostic.unspecifiedError, it, ScriptDiagnostic.Severity.WARNING) }) ResultWithDiagnostics.Failure(
messages.map {
ScriptDiagnostic(ScriptDiagnostic.unspecifiedError, it, ScriptDiagnostic.Severity.WARNING, location)
}
)
fun RepositoryCoordinates.toRepositoryUrlOrNull(): URL? = fun RepositoryCoordinates.toRepositoryUrlOrNull(): URL? =
try { try {
@@ -9,6 +9,8 @@ import java.io.File
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.script.experimental.dependencies.* import kotlin.script.experimental.dependencies.*
import kotlin.script.experimental.api.ResultWithDiagnostics import kotlin.script.experimental.api.ResultWithDiagnostics
import kotlin.script.experimental.api.SourceCode
import kotlin.script.experimental.api.asSuccess
import kotlin.script.experimental.dependencies.impl.makeResolveFailureResult import kotlin.script.experimental.dependencies.impl.makeResolveFailureResult
@ExperimentalContracts @ExperimentalContracts
@@ -83,14 +85,23 @@ class ResolversTest : ResolversTestBase() {
override fun acceptsArtifact(artifactCoordinates: String): Boolean = acceptsArt(artifactCoordinates) override fun acceptsArtifact(artifactCoordinates: String): Boolean = acceptsArt(artifactCoordinates)
override suspend fun resolve(artifactCoordinates: String): ResultWithDiagnostics<List<File>> { override suspend fun resolve(
artifactCoordinates: String,
sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<List<File>> {
if (!acceptsArtifact(artifactCoordinates)) throw Exception("Path is invalid") if (!acceptsArtifact(artifactCoordinates)) throw Exception("Path is invalid")
val file = doResolve(artifactCoordinates) ?: return makeResolveFailureResult("Failed to resolve '$artifactCoordinates'") val file = doResolve(artifactCoordinates)
?: return makeResolveFailureResult("Failed to resolve '$artifactCoordinates'", sourceCodeLocation)
return ResultWithDiagnostics.Success(listOf(file)) return ResultWithDiagnostics.Success(listOf(file))
} }
override fun addRepository(repositoryCoordinates: RepositoryCoordinates) { override fun addRepository(
repositoryCoordinates: RepositoryCoordinates,
sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<Boolean> {
if (!acceptsRepository(repositoryCoordinates)) return false.asSuccess()
addRepo(repositoryCoordinates.string) addRepo(repositoryCoordinates.string)
return true.asSuccess()
} }
override fun acceptsRepository(repositoryCoordinates: RepositoryCoordinates): Boolean { override fun acceptsRepository(repositoryCoordinates: RepositoryCoordinates): Boolean {
@@ -36,7 +36,10 @@ class IvyResolver : ExternalDependenciesResolver {
override fun acceptsRepository(repositoryCoordinates: RepositoryCoordinates): Boolean = override fun acceptsRepository(repositoryCoordinates: RepositoryCoordinates): Boolean =
repositoryCoordinates.toRepositoryUrlOrNull() != null repositoryCoordinates.toRepositoryUrlOrNull() != null
override suspend fun resolve(artifactCoordinates: String): ResultWithDiagnostics<List<File>> { override suspend fun resolve(
artifactCoordinates: String,
sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<List<File>> {
val artifactType = artifactCoordinates.substringAfterLast('@', "").trim() val artifactType = artifactCoordinates.substringAfterLast('@', "").trim()
val stringCoordinates = if (artifactType.isNotEmpty()) artifactCoordinates.removeSuffix("@$artifactType") else artifactCoordinates val stringCoordinates = if (artifactType.isNotEmpty()) artifactCoordinates.removeSuffix("@$artifactType") else artifactCoordinates
@@ -46,20 +49,26 @@ class IvyResolver : ExternalDependenciesResolver {
resolveArtifact( resolveArtifact(
artifactId[0], artifactId[1], artifactId[2], artifactId[0], artifactId[1], artifactId[2],
if (artifactId.size > 3) artifactId[3] else null, if (artifactId.size > 3) artifactId[3] else null,
if (artifactType.isNotEmpty()) artifactType else null if (artifactType.isNotEmpty()) artifactType else null,
sourceCodeLocation
) )
} catch (e: Exception) { } catch (e: Exception) {
makeFailureResult(e.asDiagnostics()) makeFailureResult(e.asDiagnostics(locationWithId = sourceCodeLocation))
} }
} else { } else {
makeFailureResult("Unrecognized set of arguments to ivy resolver: $stringCoordinates") makeFailureResult("Unrecognized set of arguments to ivy resolver: $stringCoordinates", sourceCodeLocation)
} }
} }
private val ivyResolvers = arrayListOf<URLResolver>() private val ivyResolvers = arrayListOf<URLResolver>()
private fun resolveArtifact( private fun resolveArtifact(
groupId: String, artifactName: String, revision: String, conf: String? = null, type: String? = null groupId: String,
artifactName: String,
revision: String,
conf: String? = null,
type: String? = null,
sourceCodeLocation: SourceCode.LocationWithId? = null
): ResultWithDiagnostics<List<File>> { ): ResultWithDiagnostics<List<File>> {
if (ivyResolvers.isEmpty() || ivyResolvers.none { it.name == "central" }) { if (ivyResolvers.isEmpty() || ivyResolvers.none { it.name == "central" }) {
@@ -118,23 +127,27 @@ class IvyResolver : ExternalDependenciesResolver {
XmlModuleDescriptorWriter.write(moduleDescriptor, ivyFile) XmlModuleDescriptorWriter.write(moduleDescriptor, ivyFile)
val report = ivy.resolve(ivyFile.toURI().toURL(), resolveOptions) val report = ivy.resolve(ivyFile.toURI().toURL(), resolveOptions)
val diagnostics = report.allProblemMessages.map { it.asErrorDiagnostics() } val diagnostics = report.allProblemMessages.map { it.asErrorDiagnostics(locationWithId = sourceCodeLocation) }
return if (report.hasError()) makeFailureResult(diagnostics) return if (report.hasError()) makeFailureResult(diagnostics)
else report.allArtifactsReports.map { it.localFile }.asSuccess(diagnostics) else report.allArtifactsReports.map { it.localFile }.asSuccess(diagnostics)
} }
override fun addRepository(repositoryCoordinates: RepositoryCoordinates) { override fun addRepository(
repositoryCoordinates: RepositoryCoordinates,
sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<Boolean> {
val url = repositoryCoordinates.toRepositoryUrlOrNull() val url = repositoryCoordinates.toRepositoryUrlOrNull()
if (url != null) { ?: return false.asSuccess()
ivyResolvers.add(
IBiblioResolver().apply { ivyResolvers.add(
isM2compatible = true IBiblioResolver().apply {
name = url.host isM2compatible = true
root = url.toExternalForm() name = url.host
} root = url.toExternalForm()
) }
} )
return true.asSuccess()
} }
companion object { companion object {
@@ -26,6 +26,7 @@ import kotlin.script.experimental.jvmhost.CompiledScriptJarsCache
import kotlin.script.experimental.jvmhost.jsr223.configureProvidedPropertiesFromJsr223Context import kotlin.script.experimental.jvmhost.jsr223.configureProvidedPropertiesFromJsr223Context
import kotlin.script.experimental.jvmhost.jsr223.importAllBindings import kotlin.script.experimental.jvmhost.jsr223.importAllBindings
import kotlin.script.experimental.jvmhost.jsr223.jsr223 import kotlin.script.experimental.jvmhost.jsr223.jsr223
import kotlin.script.experimental.util.filterByAnnotationType
@Suppress("unused") @Suppress("unused")
@KotlinScript( @KotlinScript(
@@ -120,22 +121,22 @@ class MainKtsConfigurator : RefineScriptCompilationConfigurationHandler {
) )
} }
val annotations = context.collectedData?.get(ScriptCollectedData.foundAnnotations)?.takeIf { it.isNotEmpty() } val annotations = context.collectedData?.get(ScriptCollectedData.collectedAnnotations)?.takeIf { it.isNotEmpty() }
?: return context.compilationConfiguration.asSuccess() ?: return context.compilationConfiguration.asSuccess()
val scriptBaseDir = (context.script as? FileBasedScriptSource)?.file?.parentFile val scriptBaseDir = (context.script as? FileBasedScriptSource)?.file?.parentFile
val importedSources = annotations.flatMap { val importedSources = annotations.filterByAnnotationType<Import>().flatMap {
(it as? Import)?.paths?.map { sourceName -> it.annotation.paths.map { sourceName ->
FileScriptSource(scriptBaseDir?.resolve(sourceName) ?: File(sourceName)) FileScriptSource(scriptBaseDir?.resolve(sourceName) ?: File(sourceName))
} ?: emptyList() }
} }
val compileOptions = annotations.flatMap { val compileOptions = annotations.filterByAnnotationType<CompilerOptions>().flatMap {
(it as? CompilerOptions)?.options?.toList() ?: emptyList() it.annotation.options.toList()
} }
val resolveResult = try { val resolveResult = try {
runBlocking { runBlocking {
resolver.resolveFromAnnotations( annotations.filter { it is DependsOn || it is Repository }) resolver.resolveFromScriptSourceAnnotations(annotations.filter { it.annotation is DependsOn || it.annotation is Repository })
} }
} catch (e: Throwable) { } catch (e: Throwable) {
ResultWithDiagnostics.Failure(*diagnostics.toTypedArray(), e.asDiagnostics(path = context.script.locationId)) ResultWithDiagnostics.Failure(*diagnostics.toTypedArray(), e.asDiagnostics(path = context.script.locationId))
@@ -6,11 +6,13 @@
package org.jetbrains.kotlin.scripting.resolve package org.jetbrains.kotlin.scripting.resolve
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Document
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.CharsetToolkit import com.intellij.openapi.vfs.CharsetToolkit
import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager import com.intellij.psi.PsiManager
import com.intellij.testFramework.LightVirtualFile import com.intellij.testFramework.LightVirtualFile
@@ -18,6 +20,8 @@ import kotlinx.coroutines.runBlocking
import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.idea.KotlinLanguage
import org.jetbrains.kotlin.psi.KtAnnotationEntry import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtFile 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.KotlinScriptDefinition
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.withCorrectExtension import org.jetbrains.kotlin.scripting.withCorrectExtension
@@ -41,7 +45,9 @@ internal fun VirtualFile.loadAnnotations(
): List<Annotation> = ): List<Annotation> =
// TODO_R: report error on failure to load annotation class // TODO_R: report error on failure to load annotation class
ApplicationManager.getApplication().runReadAction<List<Annotation>> { 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> { 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 * The implementation of the SourceCode for a script located in a virtual file
*/ */
open class VirtualFileScriptSource(val virtualFile: VirtualFile, private val preloadedText: String? = null) : open class VirtualFileScriptSource(val virtualFile: VirtualFile, private val preloadedText: String? = null) :
FileBasedScriptSource() FileBasedScriptSource() {
{
override val file: File get() = File(virtualFile.path) override val file: File get() = File(virtualFile.path)
override val externalLocation: URL get() = URL(virtualFile.url) override val externalLocation: URL get() = URL(virtualFile.url)
override val text: String by lazy { preloadedText ?: virtualFile.inputStream.bufferedReader().readText() } 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 jvmGetScriptingClass(ann, contextClassLoader, hostConfiguration) as? KClass<Annotation> // TODO errors
} }
}.orEmpty() }.orEmpty()
val annotations = scriptFile.annotationEntries.construct(contextClassLoader, acceptedAnnotations, project) val annotations = scriptFile.annotationEntries.construct(
contextClassLoader,
acceptedAnnotations,
project,
scriptFile.viewProvider.document,
scriptFile.virtualFilePath
)
return ScriptCollectedData( return ScriptCollectedData(
mapOf( 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( private fun Iterable<KtAnnotationEntry>.construct(
classLoader: ClassLoader?, acceptedAnnotations: List<KClass<out Annotation>>, project: Project classLoader: ClassLoader?, acceptedAnnotations: List<KClass<out Annotation>>, project: Project
): List<Annotation> = ): List<Pair<Annotation, KtAnnotationEntry>> =
mapNotNull { psiAnn -> mapNotNull { psiAnn ->
// TODO: consider advanced matching using semantic similar to actual resolving // TODO: consider advanced matching using semantic similar to actual resolving
acceptedAnnotations.find { ann -> acceptedAnnotations.find { ann ->
psiAnn.typeName.let { it == ann.simpleName || it == ann.qualifiedName } psiAnn.typeName.let { it == ann.simpleName || it == ann.qualifiedName }
}?.let { }?.let {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
(constructAnnotation( constructAnnotation(
psiAnn, psiAnn,
(classLoader ?: ClassLoader.getSystemClassLoader()).loadClass(it.qualifiedName).kotlin as KClass<out Annotation>, (classLoader ?: ClassLoader.getSystemClassLoader()).loadClass(it.qualifiedName).kotlin as KClass<out Annotation>,
project 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)
}
@@ -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}")
@@ -0,0 +1,5 @@
@file:Fib(number = 4)
@file:Fib(number = 0)
print("fib(4)=${FIB_4}")
@@ -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]