diff --git a/libraries/examples/scripting/jvm-maven-deps/script/src/org/jetbrains/kotlin/script/examples/jvm/resolve/maven/scriptDef.kt b/libraries/examples/scripting/jvm-maven-deps/script/src/org/jetbrains/kotlin/script/examples/jvm/resolve/maven/scriptDef.kt index 15383a45cc3..b467bff433e 100644 --- a/libraries/examples/scripting/jvm-maven-deps/script/src/org/jetbrains/kotlin/script/examples/jvm/resolve/maven/scriptDef.kt +++ b/libraries/examples/scripting/jvm-maven-deps/script/src/org/jetbrains/kotlin/script/examples/jvm/resolve/maven/scriptDef.kt @@ -38,10 +38,10 @@ object ScriptWithMavenDepsConfiguration : ScriptCompilationConfiguration( private val resolver = CompoundDependenciesResolver(FileSystemDependenciesResolver(), MavenDependenciesResolver()) fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics { - 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 runBlocking { - resolver.resolveFromAnnotations(annotations) + resolver.resolveFromScriptSourceAnnotations(annotations) }.onSuccess { context.compilationConfiguration.with { dependencies.append(JvmDependency(it)) diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/errorHandling.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/errorHandling.kt index 5caf5793773..f616a8ab2ef 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/api/errorHandling.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/errorHandling.kt @@ -33,6 +33,14 @@ data class ScriptDiagnostic( */ 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() /** @@ -211,6 +219,12 @@ fun makeFailureResult(vararg reports: ScriptDiagnostic): ResultWithDiagnostics.F fun makeFailureResult(message: String, path: String? = null, location: SourceCode.Location? = null): ResultWithDiagnostics.Failure = 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] */ @@ -223,6 +237,17 @@ fun Throwable.asDiagnostics( ): ScriptDiagnostic = 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] */ @@ -233,6 +258,15 @@ fun String.asErrorDiagnostics( ): ScriptDiagnostic = 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 */ diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptData.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptData.kt index b4a15bbf96a..17ceda1084a 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptData.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptData.kt @@ -51,8 +51,30 @@ interface SourceCode { * @param end optional range location end position (after the last char) */ 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( + /** + * 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 */ @@ -91,6 +113,13 @@ class ScriptCollectedData(properties: Map, Any>) : P */ val ScriptCollectedDataKeys.foundAnnotations by PropertiesCollection.key>() +/** + * The script file-level annotations and their locations found during script source parsing + */ +val ScriptCollectedDataKeys.collectedAnnotations by PropertiesCollection.key>>(getDefaultValue = { + get(ScriptCollectedData.foundAnnotations)?.map { ScriptSourceAnnotation(it, null) } +}) + /** * The facade to the script data for compilation configuration refinement callbacks */ @@ -150,4 +179,4 @@ data class ScriptEvaluationConfigurationRefinementContext( val compiledScript: CompiledScript, val evaluationConfiguration: ScriptEvaluationConfiguration, val contextData: ScriptEvaluationContextData? = null -) +) \ No newline at end of file diff --git a/libraries/scripting/common/src/kotlin/script/experimental/util/filterByAnnotationType.kt b/libraries/scripting/common/src/kotlin/script/experimental/util/filterByAnnotationType.kt new file mode 100644 index 00000000000..ae523f112d4 --- /dev/null +++ b/libraries/scripting/common/src/kotlin/script/experimental/util/filterByAnnotationType.kt @@ -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 Iterable>.filterByAnnotationType( +): List> = filter { it.annotation is A } + .map { + @Suppress("UNCHECKED_CAST") + it as ScriptSourceAnnotation + } \ No newline at end of file diff --git a/libraries/scripting/dependencies-maven/build.gradle.kts b/libraries/scripting/dependencies-maven/build.gradle.kts index f7c466d10cd..a371f4b98b7 100644 --- a/libraries/scripting/dependencies-maven/build.gradle.kts +++ b/libraries/scripting/dependencies-maven/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { testCompile(projectTests(":kotlin-scripting-dependencies")) testCompile(commonDep("junit")) testRuntimeOnly("org.slf4j:slf4j-nop:1.7.30") + testImplementation(kotlin("reflect")) } sourceSets { diff --git a/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/MavenDependenciesResolver.kt b/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/MavenDependenciesResolver.kt index e64002fff7c..8754cf1dc0d 100644 --- a/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/MavenDependenciesResolver.kt +++ b/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/MavenDependenciesResolver.kt @@ -13,6 +13,8 @@ import org.eclipse.aether.util.repository.AuthenticationBuilder import java.io.File import java.util.* 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.RepositoryCoordinates import kotlin.script.experimental.dependencies.impl.makeResolveFailureResult @@ -51,7 +53,10 @@ class MavenDependenciesResolver : ExternalDependenciesResolver { if (this.isNotBlank() && this.count { it == ':' } >= 2) DefaultArtifact(this) else null - override suspend fun resolve(artifactCoordinates: String): ResultWithDiagnostics> { + override suspend fun resolve( + artifactCoordinates: String, + sourceCodeLocation: SourceCode.LocationWithId? + ): ResultWithDiagnostics> { val artifactId = artifactCoordinates.toMavenArtifact()!! @@ -60,18 +65,21 @@ class MavenDependenciesResolver : ExternalDependenciesResolver { if (deps != null) return ResultWithDiagnostics.Success(deps.map { it.file }) } 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) = if (str.startsWith("$")) System.getenv(str.substring(1)) ?: str else str - override fun addRepository(repositoryCoordinates: RepositoryCoordinates) { + override fun addRepository( + repositoryCoordinates: RepositoryCoordinates, + sourceCodeLocation: SourceCode.LocationWithId? + ): ResultWithDiagnostics { val url = repositoryCoordinates.toRepositoryUrlOrNull() - ?: throw IllegalArgumentException("Invalid Maven repository URL: ${repositoryCoordinates}") + ?: return false.asSuccess() val repo = RemoteRepository.Builder( repositoryCoordinates.string, "default", @@ -91,5 +99,6 @@ class MavenDependenciesResolver : ExternalDependenciesResolver { } } repos.add(repo.build()) + return true.asSuccess() } } \ No newline at end of file diff --git a/libraries/scripting/dependencies-maven/test/kotlin/script/experimental/test/MavenResolverTest.kt b/libraries/scripting/dependencies-maven/test/kotlin/script/experimental/test/MavenResolverTest.kt index 562fc2738c9..81bf3e79a70 100644 --- a/libraries/scripting/dependencies-maven/test/kotlin/script/experimental/test/MavenResolverTest.kt +++ b/libraries/scripting/dependencies-maven/test/kotlin/script/experimental/test/MavenResolverTest.kt @@ -7,16 +7,21 @@ package kotlin.script.experimental.test import kotlinx.coroutines.runBlocking import org.junit.Assert +import org.junit.Ignore import java.io.File import kotlin.contracts.ExperimentalContracts +import kotlin.reflect.full.primaryConstructor import kotlin.script.experimental.dependencies.maven.MavenDependenciesResolver import kotlin.script.experimental.api.ResultWithDiagnostics 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 class MavenResolverTest : ResolversTestBase() { - fun resolveAndCheck(coordinates: String, checkBody: (Iterable) -> Boolean = { true } ) { + fun resolveAndCheck(coordinates: String, checkBody: (Iterable) -> Boolean = { true }) { val resolver = MavenDependenciesResolver() val result = runBlocking { resolver.resolve(coordinates) } if (result is ResultWithDiagnostics.Failure) { @@ -44,4 +49,51 @@ class MavenResolverTest : ResolversTestBase() { 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()) + } } diff --git a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/CompoundDependenciesResolver.kt b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/CompoundDependenciesResolver.kt index 6dcebc197d2..a587e69e7c4 100644 --- a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/CompoundDependenciesResolver.kt +++ b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/CompoundDependenciesResolver.kt @@ -8,6 +8,8 @@ package kotlin.script.experimental.dependencies import java.io.File import kotlin.script.experimental.api.ResultWithDiagnostics 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 class CompoundDependenciesResolver(private val resolvers: List) : ExternalDependenciesResolver { @@ -22,25 +24,53 @@ class CompoundDependenciesResolver(private val resolvers: List { + var success = false + var repositoryAdded = false + val reports = mutableListOf() + + 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> { - + override suspend fun resolve( + artifactCoordinates: String, + sourceCodeLocation: SourceCode.LocationWithId? + ): ResultWithDiagnostics> { val reports = mutableListOf() for (resolver in resolvers) { if (resolver.acceptsArtifact(artifactCoordinates)) { - when (val resolveResult = resolver.resolve(artifactCoordinates)) { + when (val resolveResult = resolver.resolve(artifactCoordinates, sourceCodeLocation)) { is ResultWithDiagnostics.Failure -> reports.addAll(resolveResult.reports) else -> return resolveResult } } } 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 { ResultWithDiagnostics.Failure(reports) } diff --git a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/ExternalDependenciesResolver.kt b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/ExternalDependenciesResolver.kt index e314766ed98..a5612abaa5a 100644 --- a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/ExternalDependenciesResolver.kt +++ b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/ExternalDependenciesResolver.kt @@ -7,6 +7,7 @@ package kotlin.script.experimental.dependencies import java.io.File import kotlin.script.experimental.api.ResultWithDiagnostics +import kotlin.script.experimental.api.SourceCode import kotlin.script.experimental.api.valueOrNull open class RepositoryCoordinates(val string: String) @@ -16,23 +17,21 @@ interface ExternalDependenciesResolver { fun acceptsRepository(repositoryCoordinates: RepositoryCoordinates): Boolean fun acceptsArtifact(artifactCoordinates: String): Boolean - suspend fun resolve(artifactCoordinates: String): ResultWithDiagnostics> - fun addRepository(repositoryCoordinates: RepositoryCoordinates) + suspend fun resolve( + artifactCoordinates: String, + sourceCodeLocation: SourceCode.LocationWithId? = null + ): ResultWithDiagnostics> + + fun addRepository( + repositoryCoordinates: RepositoryCoordinates, + sourceCodeLocation: SourceCode.LocationWithId? = null + ): ResultWithDiagnostics } fun ExternalDependenciesResolver.acceptsRepository(repositoryCoordinates: String): Boolean = acceptsRepository(RepositoryCoordinates(repositoryCoordinates)) -fun ExternalDependenciesResolver.addRepository(repositoryCoordinates: String) = addRepository(RepositoryCoordinates(repositoryCoordinates)) - -suspend fun ExternalDependenciesResolver.tryResolve(artifactCoordinates: String): List? = - if (acceptsArtifact(artifactCoordinates)) resolve(artifactCoordinates).valueOrNull() else null - -fun ExternalDependenciesResolver.tryAddRepository(repositoryCoordinates: String) = - tryAddRepository(RepositoryCoordinates(repositoryCoordinates)) - -fun ExternalDependenciesResolver.tryAddRepository(repositoryCoordinates: RepositoryCoordinates) = - if (acceptsRepository(repositoryCoordinates)) { - addRepository(repositoryCoordinates) - true - } else false \ No newline at end of file +fun ExternalDependenciesResolver.addRepository( + repositoryCoordinates: String, + sourceCodeLocation: SourceCode.LocationWithId? = null +) = addRepository(RepositoryCoordinates(repositoryCoordinates), sourceCodeLocation) \ No newline at end of file diff --git a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/FileSystemDependenciesResolver.kt b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/FileSystemDependenciesResolver.kt index 69e9c543a4d..039333eb2a5 100644 --- a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/FileSystemDependenciesResolver.kt +++ b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/FileSystemDependenciesResolver.kt @@ -7,6 +7,8 @@ package kotlin.script.experimental.dependencies import java.io.File 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.toRepositoryUrlOrNull @@ -18,13 +20,24 @@ class FileSystemDependenciesResolver(vararg paths: File) : ExternalDependenciesR private fun RepositoryCoordinates.toFilePath() = (this.toRepositoryUrlOrNull()?.takeIf { it.protocol == "file" }?.path ?: string).toRepositoryFileOrNull() - override fun addRepository(repositoryCoordinates: RepositoryCoordinates) { + override fun addRepository( + repositoryCoordinates: RepositoryCoordinates, + sourceCodeLocation: SourceCode.LocationWithId? + ): ResultWithDiagnostics { + if (!acceptsRepository(repositoryCoordinates)) return false.asSuccess() + val repoDir = repositoryCoordinates.toFilePath() - ?: throw IllegalArgumentException("Invalid repository location: '${repositoryCoordinates}'") + ?: return makeResolveFailureResult("Invalid repository location: '${repositoryCoordinates}'", sourceCodeLocation) + localRepos.add(repoDir) + + return true.asSuccess() } - override suspend fun resolve(artifactCoordinates: String): ResultWithDiagnostics> { + override suspend fun resolve( + artifactCoordinates: String, + sourceCodeLocation: SourceCode.LocationWithId? + ): ResultWithDiagnostics> { if (!acceptsArtifact(artifactCoordinates)) throw IllegalArgumentException("Path is invalid") val messages = mutableListOf() @@ -38,7 +51,7 @@ class FileSystemDependenciesResolver(vararg paths: File) : ExternalDependenciesR else -> return ResultWithDiagnostics.Success(listOf(file)) } } - return makeResolveFailureResult(messages) + return makeResolveFailureResult(messages, sourceCodeLocation) } override fun acceptsArtifact(artifactCoordinates: String) = diff --git a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/annotations.kt b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/annotations.kt index 554bc6262dd..80aefd0387e 100644 --- a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/annotations.kt +++ b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/annotations.kt @@ -6,9 +6,8 @@ package kotlin.script.experimental.dependencies import java.io.File -import kotlin.script.experimental.api.ResultWithDiagnostics -import kotlin.script.experimental.api.flatMapSuccess -import kotlin.script.experimental.api.makeFailureResult +import kotlin.script.experimental.api.* +import kotlin.script.experimental.util.filterByAnnotationType /** * 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 */ -suspend fun ExternalDependenciesResolver.resolveFromAnnotations(annotations: Iterable): ResultWithDiagnostics> { - annotations.forEach { annotation -> +suspend fun ExternalDependenciesResolver.resolveFromScriptSourceAnnotations( + annotations: Iterable> +): ResultWithDiagnostics> { + val reports = mutableListOf() + annotations.forEach { (annotation, locationWithId) -> when (annotation) { is Repository -> { for (coordinates in annotation.repositoriesCoordinates) { - if (!tryAddRepository(coordinates)) - return makeFailureResult("Unrecognized repository coordinates: $coordinates") + val added = addRepository(coordinates, locationWithId) + .also { reports.addAll(it.reports) } + .valueOr { return it } + + if (!added) + return reports + makeFailureResult( + "Unrecognized repository coordinates: $coordinates", + locationWithId = locationWithId + ) } } 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() } - .flatMapSuccess { artifactCoordinates -> - resolve(artifactCoordinates) + + return reports + annotations.filterByAnnotationType() + .flatMapSuccess { (annotation, locationWithId) -> + 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 +): ResultWithDiagnostics> { + val scriptSourceAnnotations = annotations.map { ScriptSourceAnnotation(it, null) } + return resolveFromScriptSourceAnnotations(scriptSourceAnnotations) +} \ No newline at end of file diff --git a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/impl/resolverUtils.kt b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/impl/resolverUtils.kt index cefd774f51e..6f1a0cd274c 100644 --- a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/impl/resolverUtils.kt +++ b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/impl/resolverUtils.kt @@ -10,11 +10,16 @@ import java.net.URL import kotlin.script.experimental.dependencies.RepositoryCoordinates import kotlin.script.experimental.api.ResultWithDiagnostics 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) = - ResultWithDiagnostics.Failure(messages.map { ScriptDiagnostic(ScriptDiagnostic.unspecifiedError, it, ScriptDiagnostic.Severity.WARNING) }) +fun makeResolveFailureResult(messages: Iterable, location: SourceCode.LocationWithId? = null) = + ResultWithDiagnostics.Failure( + messages.map { + ScriptDiagnostic(ScriptDiagnostic.unspecifiedError, it, ScriptDiagnostic.Severity.WARNING, location) + } + ) fun RepositoryCoordinates.toRepositoryUrlOrNull(): URL? = try { diff --git a/libraries/scripting/dependencies/test/kotlin/script/experimental/test/ResolversTest.kt b/libraries/scripting/dependencies/test/kotlin/script/experimental/test/ResolversTest.kt index 17f5fc7905e..f19f3d74b5c 100644 --- a/libraries/scripting/dependencies/test/kotlin/script/experimental/test/ResolversTest.kt +++ b/libraries/scripting/dependencies/test/kotlin/script/experimental/test/ResolversTest.kt @@ -9,6 +9,8 @@ import java.io.File import kotlin.contracts.ExperimentalContracts import kotlin.script.experimental.dependencies.* 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 @ExperimentalContracts @@ -83,14 +85,23 @@ class ResolversTest : ResolversTestBase() { override fun acceptsArtifact(artifactCoordinates: String): Boolean = acceptsArt(artifactCoordinates) - override suspend fun resolve(artifactCoordinates: String): ResultWithDiagnostics> { + override suspend fun resolve( + artifactCoordinates: String, + sourceCodeLocation: SourceCode.LocationWithId? + ): ResultWithDiagnostics> { 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)) } - override fun addRepository(repositoryCoordinates: RepositoryCoordinates) { + override fun addRepository( + repositoryCoordinates: RepositoryCoordinates, + sourceCodeLocation: SourceCode.LocationWithId? + ): ResultWithDiagnostics { + if (!acceptsRepository(repositoryCoordinates)) return false.asSuccess() addRepo(repositoryCoordinates.string) + return true.asSuccess() } override fun acceptsRepository(repositoryCoordinates: RepositoryCoordinates): Boolean { diff --git a/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/impl/ivy.kt b/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/impl/ivy.kt index f4608988341..f90477eadfc 100644 --- a/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/impl/ivy.kt +++ b/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/impl/ivy.kt @@ -36,7 +36,10 @@ class IvyResolver : ExternalDependenciesResolver { override fun acceptsRepository(repositoryCoordinates: RepositoryCoordinates): Boolean = repositoryCoordinates.toRepositoryUrlOrNull() != null - override suspend fun resolve(artifactCoordinates: String): ResultWithDiagnostics> { + override suspend fun resolve( + artifactCoordinates: String, + sourceCodeLocation: SourceCode.LocationWithId? + ): ResultWithDiagnostics> { val artifactType = artifactCoordinates.substringAfterLast('@', "").trim() val stringCoordinates = if (artifactType.isNotEmpty()) artifactCoordinates.removeSuffix("@$artifactType") else artifactCoordinates @@ -46,20 +49,26 @@ class IvyResolver : ExternalDependenciesResolver { resolveArtifact( artifactId[0], artifactId[1], artifactId[2], if (artifactId.size > 3) artifactId[3] else null, - if (artifactType.isNotEmpty()) artifactType else null + if (artifactType.isNotEmpty()) artifactType else null, + sourceCodeLocation ) } catch (e: Exception) { - makeFailureResult(e.asDiagnostics()) + makeFailureResult(e.asDiagnostics(locationWithId = sourceCodeLocation)) } } else { - makeFailureResult("Unrecognized set of arguments to ivy resolver: $stringCoordinates") + makeFailureResult("Unrecognized set of arguments to ivy resolver: $stringCoordinates", sourceCodeLocation) } } private val ivyResolvers = arrayListOf() 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> { if (ivyResolvers.isEmpty() || ivyResolvers.none { it.name == "central" }) { @@ -118,23 +127,27 @@ class IvyResolver : ExternalDependenciesResolver { XmlModuleDescriptorWriter.write(moduleDescriptor, ivyFile) 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) else report.allArtifactsReports.map { it.localFile }.asSuccess(diagnostics) } - override fun addRepository(repositoryCoordinates: RepositoryCoordinates) { + override fun addRepository( + repositoryCoordinates: RepositoryCoordinates, + sourceCodeLocation: SourceCode.LocationWithId? + ): ResultWithDiagnostics { val url = repositoryCoordinates.toRepositoryUrlOrNull() - if (url != null) { - ivyResolvers.add( - IBiblioResolver().apply { - isM2compatible = true - name = url.host - root = url.toExternalForm() - } - ) - } + ?: return false.asSuccess() + + ivyResolvers.add( + IBiblioResolver().apply { + isM2compatible = true + name = url.host + root = url.toExternalForm() + } + ) + return true.asSuccess() } companion object { diff --git a/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/scriptDef.kt b/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/scriptDef.kt index 0fd47f5fab9..23d75b66223 100644 --- a/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/scriptDef.kt +++ b/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/scriptDef.kt @@ -26,6 +26,7 @@ import kotlin.script.experimental.jvmhost.CompiledScriptJarsCache import kotlin.script.experimental.jvmhost.jsr223.configureProvidedPropertiesFromJsr223Context import kotlin.script.experimental.jvmhost.jsr223.importAllBindings import kotlin.script.experimental.jvmhost.jsr223.jsr223 +import kotlin.script.experimental.util.filterByAnnotationType @Suppress("unused") @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() val scriptBaseDir = (context.script as? FileBasedScriptSource)?.file?.parentFile - val importedSources = annotations.flatMap { - (it as? Import)?.paths?.map { sourceName -> + val importedSources = annotations.filterByAnnotationType().flatMap { + it.annotation.paths.map { sourceName -> FileScriptSource(scriptBaseDir?.resolve(sourceName) ?: File(sourceName)) - } ?: emptyList() + } } - val compileOptions = annotations.flatMap { - (it as? CompilerOptions)?.options?.toList() ?: emptyList() + val compileOptions = annotations.filterByAnnotationType().flatMap { + it.annotation.options.toList() } val resolveResult = try { 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) { ResultWithDiagnostics.Failure(*diagnostics.toTypedArray(), e.asDiagnostics(path = context.script.locationId)) diff --git a/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/refineCompilationConfiguration.kt b/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/refineCompilationConfiguration.kt index 2a7bdb1f985..79de1d2d2a5 100644 --- a/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/refineCompilationConfiguration.kt +++ b/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/refineCompilationConfiguration.kt @@ -6,11 +6,13 @@ package org.jetbrains.kotlin.scripting.resolve import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.Document import com.intellij.openapi.project.Project import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.CharsetToolkit import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager import com.intellij.testFramework.LightVirtualFile @@ -18,6 +20,8 @@ import kotlinx.coroutines.runBlocking import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.psi.KtAnnotationEntry import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.psiUtil.endOffset +import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.scripting.definitions.KotlinScriptDefinition import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition import org.jetbrains.kotlin.scripting.withCorrectExtension @@ -41,7 +45,9 @@ internal fun VirtualFile.loadAnnotations( ): List = // TODO_R: report error on failure to load annotation class ApplicationManager.getApplication().runReadAction> { - this.getAnnotationEntries(project).construct(classLoader, acceptedAnnotations, project) + this.getAnnotationEntries(project) + .construct(classLoader, acceptedAnnotations, project) + .map { it.first } } internal fun VirtualFile.getAnnotationEntries(project: Project): Iterable { @@ -55,8 +61,7 @@ internal fun VirtualFile.getAnnotationEntries(project: Project): Iterable // TODO errors } }.orEmpty() - val annotations = scriptFile.annotationEntries.construct(contextClassLoader, acceptedAnnotations, project) + val annotations = scriptFile.annotationEntries.construct( + contextClassLoader, + acceptedAnnotations, + project, + scriptFile.viewProvider.document, + scriptFile.virtualFilePath + ) return ScriptCollectedData( mapOf( - ScriptCollectedData.foundAnnotations to annotations + ScriptCollectedData.collectedAnnotations to annotations, + ScriptCollectedData.foundAnnotations to annotations.map { it.annotation } ) ) } +private fun Iterable.construct( + classLoader: ClassLoader?, acceptedAnnotations: List>, project: Project, document: Document?, filePath: String +): List> = 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.construct( classLoader: ClassLoader?, acceptedAnnotations: List>, project: Project -): List = +): List> = mapNotNull { psiAnn -> // TODO: consider advanced matching using semantic similar to actual resolving acceptedAnnotations.find { ann -> psiAnn.typeName.let { it == ann.simpleName || it == ann.qualifiedName } }?.let { @Suppress("UNCHECKED_CAST") - (constructAnnotation( + constructAnnotation( psiAnn, (classLoader ?: ClassLoader.getSystemClassLoader()).loadClass(it.qualifiedName).kotlin as KClass, project - )) + ) to psiAnn } - } \ No newline at end of file + } + +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) +} \ No newline at end of file diff --git a/plugins/scripting/scripting-compiler/testData/compiler/compileTimeFibonacci/supported.fib.kts b/plugins/scripting/scripting-compiler/testData/compiler/compileTimeFibonacci/supported.fib.kts new file mode 100644 index 00000000000..91b9b8db53c --- /dev/null +++ b/plugins/scripting/scripting-compiler/testData/compiler/compileTimeFibonacci/supported.fib.kts @@ -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}") \ No newline at end of file diff --git a/plugins/scripting/scripting-compiler/testData/compiler/compileTimeFibonacci/unsupported.fib.kts b/plugins/scripting/scripting-compiler/testData/compiler/compileTimeFibonacci/unsupported.fib.kts new file mode 100644 index 00000000000..40df540d64e --- /dev/null +++ b/plugins/scripting/scripting-compiler/testData/compiler/compileTimeFibonacci/unsupported.fib.kts @@ -0,0 +1,5 @@ + +@file:Fib(number = 4) +@file:Fib(number = 0) + +print("fib(4)=${FIB_4}") \ No newline at end of file diff --git a/plugins/scripting/scripting-compiler/tests/org/jetbrains/kotlin/scripting/compiler/test/CompileTimeFibonacciTest.kt b/plugins/scripting/scripting-compiler/tests/org/jetbrains/kotlin/scripting/compiler/test/CompileTimeFibonacciTest.kt new file mode 100644 index 00000000000..b34d454acbc --- /dev/null +++ b/plugins/scripting/scripting-compiler/tests/org/jetbrains/kotlin/scripting/compiler/test/CompileTimeFibonacciTest.kt @@ -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 { + 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 { + 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 { + 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() + ?.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 List.secondToLast(): T = this[count() - 2] \ No newline at end of file