Implement language settings API for Kotlin source sets

This commit is contained in:
Sergey Igushkin
2018-08-09 12:53:39 +03:00
parent 2c04e2b95c
commit 5815d6a70c
9 changed files with 298 additions and 22 deletions
@@ -0,0 +1,18 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.plugin
interface LanguageSettingsBuilder {
var languageVersion: String?
var apiVersion: String?
var progressiveMode: Boolean
fun enableLanguageFeature(name: String)
val enabledLanguageFeatures: Set<String>
}
@@ -9,19 +9,22 @@ import groovy.lang.Closure
import org.gradle.api.Named
import org.gradle.api.file.SourceDirectorySet
import org.jetbrains.kotlin.gradle.plugin.HasKotlinDependencies
import org.jetbrains.kotlin.gradle.plugin.LanguageSettingsBuilder
interface KotlinSourceSet : Named, HasKotlinDependencies {
val kotlin: SourceDirectorySet
fun kotlin(configureClosure: Closure<Any?>): SourceDirectorySet
val resources: SourceDirectorySet
fun kotlin(configureClosure: Closure<Any?>): SourceDirectorySet
val languageSettings: LanguageSettingsBuilder
fun languageSettings(configureClosure: Closure<Any?>): LanguageSettingsBuilder
fun dependsOn(other: KotlinSourceSet)
val dependsOn: Set<KotlinSourceSet>
companion object {
const val COMMON_MAIN_SOURCE_SET_NAME = "commonMain"
const val COMMON_TEST_SOURCE_SET_NAME = "commonTest"
}
fun dependsOn(other: KotlinSourceSet)
val dependsOn: Set<KotlinSourceSet>
}
@@ -5,6 +5,7 @@
package org.jetbrains.kotlin.gradle
import org.jetbrains.kotlin.gradle.plugin.sources.SourceSetConsistencyChecks
import org.jetbrains.kotlin.gradle.util.checkBytecodeContains
import org.jetbrains.kotlin.gradle.util.modify
import org.junit.Assert
@@ -226,4 +227,69 @@ class NewMultiplatformIT : BaseGradleIT() {
expectedKotlinOutputFiles.forEach { assertFileExists(it) }
}
}
@Test
fun testLanguageSettingsApplied() = with(Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")) {
setupWorkingDir()
gradleBuildScript().appendText(
"\n" + """
kotlin.sourceSets.jvm6Main.languageSettings {
languageVersion = '1.3'
apiVersion = '1.3'
enableLanguageFeature('InlineClasses')
progressiveMode = true
}
""".trimIndent()
)
build("compileKotlinJvm6") {
assertSuccessful()
assertContains("-language-version 1.3", "-api-version 1.3", "-XXLanguage:+InlineClasses", " -Xprogressive")
}
}
@Test
fun testLanguageSettingsConsistency() = with(Project("sample-lib", gradleVersion, "new-mpp-lib-and-app")) {
setupWorkingDir()
gradleBuildScript().appendText(
"\n" + """
kotlin.sourceSets {
foo { }
bar { dependsOn foo }
}
""".trimIndent()
)
fun testMonotonousCheck(sourceSetConfigurationChange: String, expectedErrorHint: String) {
gradleBuildScript().appendText("\nkotlin.sourceSets.foo.${sourceSetConfigurationChange}")
build("tasks") {
assertFailed()
assertContains(expectedErrorHint)
}
gradleBuildScript().appendText("\nkotlin.sourceSets.bar.${sourceSetConfigurationChange}")
build("tasks") {
assertSuccessful()
}
}
testMonotonousCheck("languageSettings.languageVersion = '1.4'", SourceSetConsistencyChecks.languageVersionCheckHint)
testMonotonousCheck("languageSettings.enableLanguageFeature('InlineClasses')", SourceSetConsistencyChecks.unstableFeaturesHint)
// check that enabling a bugfix feature and progressive mode or advancing API level
// don't require doing the same for dependent source sets:
gradleBuildScript().appendText(
"\n" + """
kotlin.sourceSets.foo.languageSettings {
apiVersion = '1.3'
enableLanguageFeature('SoundSmartcastForEnumEntries')
progressiveMode = true
}
""".trimIndent()
)
build("tasks") {
assertSuccessful()
}
}
}
@@ -164,7 +164,7 @@ internal class Kotlin2JvmSourceSetProcessor(
super.defaultKotlinDestinationDir
override fun doCreateTask(project: Project, taskName: String): KotlinCompile =
tasksProvider.createKotlinJVMTask(project, taskName, kotlinCompilation.compilationName)
tasksProvider.createKotlinJVMTask(project, taskName, kotlinCompilation)
override fun doTargetSpecificProcessing() {
Kapt3KotlinGradleSubplugin.createAptConfigurationIfNeeded(project, kotlinCompilation.compilationName)
@@ -254,7 +254,7 @@ internal class Kotlin2JsSourceSetProcessor(
kotlinCompilation = kotlinCompilation
) {
override fun doCreateTask(project: Project, taskName: String): Kotlin2JsCompile =
tasksProvider.createKotlinJSTask(project, taskName, kotlinCompilation.compilationName)
tasksProvider.createKotlinJSTask(project, taskName, kotlinCompilation)
override fun doTargetSpecificProcessing() {
project.tasks.findByName(kotlinCompilation.compileAllTaskName)!!.dependsOn(kotlinTask)
@@ -332,7 +332,7 @@ internal class KotlinCommonSourceSetProcessor(
}
override fun doCreateTask(project: Project, taskName: String): KotlinCompileCommon =
tasksProvider.createKotlinCommonTask(project, taskName, kotlinCompilation.compilationName)
tasksProvider.createKotlinCommonTask(project, taskName, kotlinCompilation)
}
internal abstract class AbstractKotlinPlugin(
@@ -712,7 +712,7 @@ abstract class AbstractAndroidProjectHandler<V>(private val kotlinConfigurationT
val kotlinTaskName = compilation.compileKotlinTaskName
// todo: Investigate possibility of creating and configuring kotlinTask before evaluation
val kotlinTask = tasksProvider.createKotlinJVMTask(project, kotlinTaskName, variantDataName)
val kotlinTask = tasksProvider.createKotlinJVMTask(project, kotlinTaskName, compilation)
kotlinTask.parentKotlinOptionsImpl = rootKotlinOptions
// store kotlin classes in separate directory. They will serve as class-path to java compiler
@@ -22,8 +22,10 @@ import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.SourceSetOutput
import org.gradle.util.ConfigureUtil
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.source.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.sources.defaultSourceSetLanguageSettingsChecker
import org.jetbrains.kotlin.gradle.plugin.sources.getSourceSetHierarchy
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
import org.jetbrains.kotlin.gradle.utils.addExtendsFromRelation
@@ -82,6 +84,16 @@ abstract class AbstractKotlinCompilation(
if (this is KotlinCompilationToRunnableFiles) {
addExtendsFromRelation(runtimeOnlyConfigurationName, sourceSet.runtimeOnlyConfigurationName, forced = false)
}
if (sourceSet.name != defaultSourceSetName) {
// Temporary solution for checking consistency across source sets participating in a compilation that may
// not be interconnected with the dependsOn relation: check the settings as if the default source set of
// the compilation depends on the one added to the compilation:
defaultSourceSetLanguageSettingsChecker.runAllChecks(
kotlinExtension.sourceSets.getByName(defaultSourceSetName),
sourceSet
)
}
}
}
}
@@ -13,6 +13,7 @@ import org.gradle.api.internal.file.DefaultSourceDirectorySet
import org.gradle.api.internal.file.FileResolver
import org.gradle.util.ConfigureUtil
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
import org.jetbrains.kotlin.gradle.plugin.LanguageSettingsBuilder
import org.jetbrains.kotlin.gradle.plugin.mpp.DefaultKotlinDependencyHandler
import org.jetbrains.kotlin.gradle.plugin.source.KotlinSourceSet
import org.jetbrains.kotlin.gradle.utils.lowerCamelCaseName
@@ -41,10 +42,16 @@ class DefaultKotlinSourceSet(
filter.include("**/*.java", "**/*.kt", "**/*.kts")
}
override val languageSettings: LanguageSettingsBuilder = DefaultLanguageSettingsBuilder()
override val resources: SourceDirectorySet = createDefaultSourceDirectorySet(displayName + " resources", fileResolver)
override fun kotlin(configureClosure: Closure<Any?>): SourceDirectorySet = kotlin.apply { ConfigureUtil.configure(configureClosure, this) }
override fun languageSettings(configureClosure: Closure<Any?>): LanguageSettingsBuilder = languageSettings.apply {
ConfigureUtil.configure(configureClosure, this)
}
override fun getName(): String = displayName
override fun dependencies(configure: KotlinDependencyHandler.() -> Unit): Unit =
@@ -58,6 +65,8 @@ class DefaultKotlinSourceSet(
// Fail-fast approach: check on each new added edge and report a circular dependency at once when the edge is added.
checkForCircularDependencies()
project.afterEvaluate { defaultSourceSetLanguageSettingsChecker.runAllChecks(this, other) }
}
private val dependsOnSourceSetsImpl = mutableSetOf<KotlinSourceSet>()
@@ -0,0 +1,77 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.plugin.sources
import org.gradle.api.InvalidUserDataException
import org.jetbrains.kotlin.config.ApiVersion
import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.config.LanguageVersion
import org.jetbrains.kotlin.gradle.plugin.LanguageSettingsBuilder
internal class DefaultLanguageSettingsBuilder : LanguageSettingsBuilder {
private var languageVersionImpl: LanguageVersion? = null
override var languageVersion: String?
get() = languageVersionImpl?.versionString
set(value) {
languageVersionImpl = value?.let { versionString ->
LanguageVersion.fromVersionString(versionString) ?: throw InvalidUserDataException(
"Incorrect language version. Expected one of: ${LanguageVersion.values().joinToString { "'${it.versionString}'" }}"
)
}
}
private var apiVersionImpl: ApiVersion? = null
override var apiVersion: String?
get() = apiVersionImpl?.versionString
set(value) {
apiVersionImpl = value ?.let { versionString ->
parseApiVersionSettings(versionString) ?: throw InvalidUserDataException(
"Incorrect API version. Expected one of: ${apiVersionValues.joinToString { "'${it.versionString}'" }}"
)
}
}
override var progressiveMode: Boolean = false
private val enabledLanguageFeaturesImpl = mutableSetOf<LanguageFeature>()
override val enabledLanguageFeatures: Set<String>
get() = enabledLanguageFeaturesImpl.map { it.name }.toSet()
override fun enableLanguageFeature(name: String) {
val languageFeature = parseLanguageFeature(name) ?: throw InvalidUserDataException(
"Unknown language feature '${name}'"
)
enabledLanguageFeaturesImpl += languageFeature
}
companion object {
}
}
internal fun applyLanguageSettingsToKotlinTask(
languageSettingsBuilder: LanguageSettingsBuilder,
kotlinTask: org.jetbrains.kotlin.gradle.dsl.KotlinCompile<*>
) = with(kotlinTask.kotlinOptions) {
languageVersion = languageVersion ?: languageSettingsBuilder.languageVersion
apiVersion = apiVersion ?: languageSettingsBuilder.apiVersion
if (languageSettingsBuilder.progressiveMode) {
freeCompilerArgs += "-Xprogressive"
}
languageSettingsBuilder.enabledLanguageFeatures.forEach { featureName ->
freeCompilerArgs += "-XXLanguage:+$featureName"
}
}
private val apiVersionValues = ApiVersion.run { listOf(KOTLIN_1_0, KOTLIN_1_1, KOTLIN_1_2, KOTLIN_1_3) }
internal fun parseLanguageVersionSetting(versionString: String) = LanguageVersion.fromVersionString(versionString)
internal fun parseApiVersionSettings(versionString: String) = apiVersionValues.find { it.versionString == versionString }
internal fun parseLanguageFeature(featureName: String) = LanguageFeature.fromString(featureName)
@@ -0,0 +1,80 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.plugin.sources
import org.gradle.api.InvalidUserDataException
import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.config.LanguageVersion
import org.jetbrains.kotlin.gradle.plugin.source.KotlinSourceSet
internal class ConsistencyCheck<T, S>(
val name: String,
val getValue: (T) -> S,
val leftExtendsRightConsistently: (S, S) -> Boolean,
val consistencyConditionHint: String
)
object SourceSetConsistencyChecks {
private val defaultLanguageVersion = LanguageVersion.LATEST_STABLE
const val languageVersionCheckHint =
"The language version of the dependent source set must be greater than or equal to that of its dependency."
internal val languageVersionCheck = ConsistencyCheck<KotlinSourceSet, LanguageVersion>(
name = "language version",
getValue = { sourceSet ->
sourceSet.languageSettings.languageVersion?.let { parseLanguageVersionSetting(it) } ?: defaultLanguageVersion
},
leftExtendsRightConsistently = { left, right -> left >= right },
consistencyConditionHint = languageVersionCheckHint
)
const val unstableFeaturesHint = "The dependent source set must enable all unstable language features that its dependency has."
internal val unstableFeaturesCheck = ConsistencyCheck<KotlinSourceSet, Set<LanguageFeature>>(
name = "unstable language feature set",
getValue = { sourceSet ->
sourceSet.languageSettings.enabledLanguageFeatures
.map { parseLanguageFeature(it)!! }
.filterTo(mutableSetOf()) { it.kind == LanguageFeature.Kind.UNSTABLE_FEATURE }
},
leftExtendsRightConsistently = { left, right -> left.containsAll(right) },
consistencyConditionHint = unstableFeaturesHint
)
}
internal class SourceSetConsistencyChecker(
val checks: List<ConsistencyCheck<KotlinSourceSet, *>>
) {
fun <S> runSingleCheck(
dependent: KotlinSourceSet,
dependency: KotlinSourceSet,
check: ConsistencyCheck<KotlinSourceSet, S>
) {
val leftValue = check.getValue(dependent)
val rightValue = check.getValue(dependency)
if (!check.leftExtendsRightConsistently(leftValue, rightValue)) {
throw InvalidUserDataException(
"Inconsistent settings for Kotlin source sets: '${dependent.name}' depends on '${dependency.name}'\n" +
"'${dependent.name}': ${check.name} is ${leftValue}\n" +
"'${dependency.name}': ${check.name} is ${rightValue}\n" +
check.consistencyConditionHint
)
}
}
fun runAllChecks(dependent: KotlinSourceSet, dependency: KotlinSourceSet) {
for (check in checks) {
runSingleCheck(dependent, dependency, check)
}
}
}
internal val defaultSourceSetLanguageSettingsChecker = with(SourceSetConsistencyChecks) {
// We don't check the progressive mode, since the features it enables are bugfixes
SourceSetConsistencyChecker(listOf(languageVersionCheck, unstableFeaturesCheck))
}
@@ -17,34 +17,45 @@
package org.jetbrains.kotlin.gradle.tasks
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.plugin.RegexTaskToFriendTaskMapper
import org.jetbrains.kotlin.gradle.plugin.TaskToFriendTaskMapper
import org.jetbrains.kotlin.gradle.plugin.mapKotlinTaskProperties
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.mpp.defaultSourceSetName
import org.jetbrains.kotlin.gradle.plugin.sources.applyLanguageSettingsToKotlinTask
internal open class KotlinTasksProvider(val targetName: String) {
open fun createKotlinJVMTask(project: Project, name: String, sourceSetName: String): KotlinCompile =
open fun createKotlinJVMTask(
project: Project,
name: String,
compilation: KotlinCompilation
): KotlinCompile =
project.tasks.create(name, KotlinCompile::class.java).apply {
configure(this, project, sourceSetName)
configure(this, project, compilation)
}
fun createKotlinJSTask(project: Project, name: String, sourceSetName: String): Kotlin2JsCompile =
fun createKotlinJSTask(project: Project, name: String, compilation: KotlinCompilation): Kotlin2JsCompile =
project.tasks.create(name, Kotlin2JsCompile::class.java).apply {
configure(this, project, sourceSetName)
configure(this, project, compilation)
}
fun createKotlinCommonTask(project: Project, name: String, sourceSetName: String): KotlinCompileCommon =
fun createKotlinCommonTask(project: Project, name: String, compilation: KotlinCompilation): KotlinCompileCommon =
project.tasks.create(name, KotlinCompileCommon::class.java).apply {
configure(this, project, sourceSetName)
configure(this, project, compilation)
}
open fun configure(
kotlinTask: AbstractKotlinCompile<*>,
project: Project,
sourceSetName: String
compilation: KotlinCompilation
) {
kotlinTask.sourceSetName = sourceSetName
kotlinTask.sourceSetName = compilation.name
kotlinTask.friendTaskName = taskToFriendTaskMapper[kotlinTask]
mapKotlinTaskProperties(project, kotlinTask)
project.whenEvaluated {
val languageSettings = project.kotlinExtension.sourceSets.getByName(compilation.defaultSourceSetName).languageSettings
kotlinTask as org.jetbrains.kotlin.gradle.dsl.KotlinCompile<*>
applyLanguageSettingsToKotlinTask(languageSettings, kotlinTask)
}
}
protected open val taskToFriendTaskMapper: TaskToFriendTaskMapper =
@@ -55,8 +66,8 @@ internal class AndroidTasksProvider(targetName: String) : KotlinTasksProvider(ta
override val taskToFriendTaskMapper: TaskToFriendTaskMapper =
RegexTaskToFriendTaskMapper.Android(targetName)
override fun configure(kotlinTask: AbstractKotlinCompile<*>, project: Project, sourceSetName: String) {
super.configure(kotlinTask, project, sourceSetName)
override fun configure(kotlinTask: AbstractKotlinCompile<*>, project: Project, compilation: KotlinCompilation) {
super.configure(kotlinTask, project, compilation)
kotlinTask.useModuleDetection = true
}
}