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
+2
-2
@@ -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
|
||||||
)
|
)
|
||||||
+15
@@ -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 {
|
||||||
|
|||||||
+14
-5
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+53
-1
@@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-7
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-15
@@ -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
|
|
||||||
+17
-4
@@ -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) =
|
||||||
|
|||||||
+33
-12
@@ -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)
|
||||||
|
}
|
||||||
+8
-3
@@ -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 {
|
||||||
|
|||||||
+14
-3
@@ -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))
|
||||||
|
|||||||
+47
-9
@@ -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)
|
||||||
|
}
|
||||||
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