diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt index 1a4e5451a61..80d32aed579 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt @@ -32,17 +32,21 @@ import org.jetbrains.kotlin.gradle.internal.KOTLIN_COMPILER_EMBEDDABLE import org.jetbrains.kotlin.gradle.internal.KOTLIN_MODULE_GROUP import org.jetbrains.kotlin.gradle.logging.kotlinDebug import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformAndroidGradlePluginCompatibilityHealthCheck.runMultiplatformAndroidGradlePluginCompatibilityHealthCheckWhenAndroidIsApplied -import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.Companion.kotlinPropertiesProvider import org.jetbrains.kotlin.gradle.plugin.diagnostics.* +import org.jetbrains.kotlin.gradle.plugin.diagnostics.EnsureNoKotlinGradlePluginErrors +import org.jetbrains.kotlin.gradle.plugin.diagnostics.ToolingDiagnosticRenderingOptions +import org.jetbrains.kotlin.gradle.plugin.diagnostics.UsesKotlinToolingDiagnostics +import org.jetbrains.kotlin.gradle.plugin.diagnostics.kotlinToolingDiagnosticsCollectorProvider +import org.jetbrains.kotlin.gradle.plugin.diagnostics.launchKotlinGradleProjectCheckers import org.jetbrains.kotlin.gradle.plugin.internal.* import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMultiplatformPlugin import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.KotlinPm20GradlePlugin import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.KotlinPm20ProjectExtension import org.jetbrains.kotlin.gradle.plugin.sources.DefaultKotlinSourceSetFactory +import org.jetbrains.kotlin.gradle.plugin.statistics.BuildFlowService import org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildStatsService import org.jetbrains.kotlin.gradle.report.BuildMetricsService -import org.jetbrains.kotlin.gradle.plugin.statistics.BuildFlowService import org.jetbrains.kotlin.gradle.targets.js.KotlinJsCompilerAttribute import org.jetbrains.kotlin.gradle.targets.js.KotlinJsPlugin import org.jetbrains.kotlin.gradle.targets.js.KotlinWasmTargetAttribute @@ -268,24 +272,30 @@ abstract class KotlinBasePluginWrapper : DefaultKotlinBasePlugin() { } private fun Project.setupDiagnosticsChecksAndReporting() { + val collectorProvider = kotlinToolingDiagnosticsCollectorProvider + val diagnosticRenderingOptions = ToolingDiagnosticRenderingOptions.forProject(this) + // Setup reporting from tasks tasks.withType(UsesKotlinToolingDiagnostics::class.java).configureEach { - it.usesService(kotlinToolingDiagnosticsCollectorProvider) - it.toolingDiagnosticsCollector.value(kotlinToolingDiagnosticsCollectorProvider) - it.diagnosticRenderingOptions.set(ToolingDiagnosticRenderingOptions.forProject(this)) + it.usesService(collectorProvider) + it.toolingDiagnosticsCollector.value(collectorProvider) + it.diagnosticRenderingOptions.set(diagnosticRenderingOptions) } // Launch checkers. Note that they are invoked eagerly to give them a fine-grained // control over the lifecycle - project.launchKotlinGradleProjectCheckers() + launchKotlinGradleProjectCheckers() + + // Setup a task that will abort the build if errors will be reported. This task should be the first in the taskgraph + project.locateOrRegisterEnsureNoKotlinGradlePluginErrors() // Schedule diagnostics rendering - project.launch { - project.configurationResult.await() + launch { + configurationResult.await() renderReportedDiagnostics( - project.kotlinToolingDiagnosticsCollector.getDiagnosticsForProject(project), - project.logger, - project.kotlinPropertiesProvider.internalVerboseDiagnostics + collectorProvider.get().getDiagnosticsForProject(project), + logger, + diagnosticRenderingOptions.isVerbose ) } @@ -293,7 +303,7 @@ private fun Project.setupDiagnosticsChecksAndReporting() { // after projects are evaluated will be transparently rendered right away instead of being // silently swallowed gradle.projectsEvaluated { - kotlinToolingDiagnosticsCollector.switchToTransparentMode() + collectorProvider.get().switchToTransparentMode() } } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/EnsureNoKotlinGradlePluginErrors.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/EnsureNoKotlinGradlePluginErrors.kt new file mode 100644 index 00000000000..640b6ed78c5 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/EnsureNoKotlinGradlePluginErrors.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2010-2023 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.gradle.plugin.diagnostics + +import org.gradle.api.DefaultTask +import org.gradle.api.InvalidUserCodeException +import org.gradle.api.Project +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.language.base.plugins.LifecycleBasePlugin +import org.gradle.work.DisableCachingByDefault +import org.jetbrains.kotlin.gradle.tasks.KotlinCompileTool +import org.jetbrains.kotlin.gradle.tasks.withType + +@DisableCachingByDefault( + because = "This task renders reported diagnostics; caching this task will hide diagnostics and obscure issues in the build" +) +internal abstract class EnsureNoKotlinGradlePluginErrors : DefaultTask() { + @get:Input + abstract val errorDiagnostics: ListProperty + + @get:Internal + abstract val renderingOptions: Property + + @TaskAction + fun checkNoErrors() { + if (errorDiagnostics.get().isNotEmpty()) { + renderReportedDiagnostics(errorDiagnostics.get(), logger, renderingOptions.get().isVerbose) + throw InvalidUserCodeException("Kotlin Gradle Plugin reported errors. Check the log for details") + } + } + + companion object { + internal const val TASK_NAME = "ensureNoKotlinGradlePluginErrors" + } +} + +private const val DESCRIPTION = + "Checks that Kotlin Gradle Plugin hasn't reported project configuration errors, failing otherwise. " + + "This task always runs before compileKotlin* or similar tasks." + +internal fun Project.locateOrRegisterEnsureNoKotlinGradlePluginErrors(): TaskProvider { + val taskProvider = tasks.register( + EnsureNoKotlinGradlePluginErrors.TASK_NAME, + EnsureNoKotlinGradlePluginErrors::class.java + ) { task -> + task.errorDiagnostics.set( + provider { + kotlinToolingDiagnosticsCollector + .getDiagnosticsForProject(this).asSequence() + .filter { it.severity == ToolingDiagnostic.Severity.ERROR } + .toList() + } + ) + task.renderingOptions.set(ToolingDiagnosticRenderingOptions.forProject(this)) + task.description = DESCRIPTION + task.group = LifecycleBasePlugin.VERIFICATION_GROUP + } + + taskProvider.addDependsOnFromTasksThatShouldFailWhenErrorsReported(tasks) + + return taskProvider +} + +/** + * Adds dependsOn from some selection of the [tasks] to the [this]-task, effectively causing them to fail + * if the ERROR-diagnostics were reported. + * + * Currently, we're doing it conservatively and instrumenting only [KotlinCompileTool]-tasks. + * The intuition here is that if the build manages to do something useful for a user without compiling any .kt-sources, + * then it's OK for KGP to let that build pass even if it reported ERROR-diagnostics. + */ +private fun TaskProvider.addDependsOnFromTasksThatShouldFailWhenErrorsReported(tasks: TaskContainer) { + tasks.withType().configureEach { it.dependsOn(this) } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/ToolingDiagnostic.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/ToolingDiagnostic.kt index 566d6afaaf9..9ab32340765 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/ToolingDiagnostic.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/ToolingDiagnostic.kt @@ -12,11 +12,24 @@ data class ToolingDiagnostic( val id: String, val message: String, val severity: Severity, val throwable: Throwable? = null, ) { enum class Severity { + /** + * More visible than most of the output (intuition: yellow-highlighting). + * Doesn't prevent the build from running. + * + * Use for non-critical misconfigurations with low rate of false-positives + */ WARNING, /** - * Stronger highlighting than WARNING, but doesn't prevent further actions (e.g. further - * tasks) from being executed + * Heavily emphasized in the output (intuition: bold red highlighting). + * + * ATTENTION. If a diagnostic with this severity is reported, Kotlin compiler + * will _not_ be invoked (build will appear failed, as with compilation error) + * + * However, Gradle IDE Sync and other tasks that are not connected with + * any of the Kotlin Compiler and tools (e.g. 'help', 'clean'), will run successfully. + * + * Use for critical misconfigurations that need immediate addressing */ ERROR, @@ -27,8 +40,12 @@ data class ToolingDiagnostic( * - mask further errors (forcing users to make multiple runs before fixing all issues) * * - lead to unpleasant UX in IDE (if the failure happens during import, then depending - * on when it happened users might not have even basic IDE assistance, which makes it - * hard to fix the issue) + * on when it happened users might not have even basic IDE assistance, which makes fixing + * the root cause very annoying) + * + * Use for irreconcilable misconfigurations / malformed input which prevent further + * configuration _and_ when the graceful degradation (allowing configuration phase to finish) + * is too expensive. */ FATAL, } diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/ToolingDiagnosticRenderingOptions.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/ToolingDiagnosticRenderingOptions.kt index 8006dbf7d0c..3c2e882f935 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/ToolingDiagnosticRenderingOptions.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/diagnostics/ToolingDiagnosticRenderingOptions.kt @@ -24,9 +24,13 @@ internal class ToolingDiagnosticRenderingOptions( } } -internal fun Collection.withoutSuppressed(options: ToolingDiagnosticRenderingOptions): Collection = - filter { !it.isSuppressed(options) } +internal fun ToolingDiagnostic.isSuppressed(options: ToolingDiagnosticRenderingOptions): Boolean { + return when { + severity == ToolingDiagnostic.Severity.WARNING -> id in options.suppressedWarningIds -internal fun ToolingDiagnostic.isSuppressed(options: ToolingDiagnosticRenderingOptions): Boolean = - severity == ToolingDiagnostic.Severity.WARNING && id in options.suppressedWarningIds - || severity == ToolingDiagnostic.Severity.ERROR && id in options.suppressedErrorIds + severity == ToolingDiagnostic.Severity.ERROR -> id in options.suppressedErrorIds + + // NB: FATALs can not be suppressed + else -> false + } +}