[Gradle] Add new infrastructure for diagnostics in KGP

This commit is contained in:
Dmitry Savvinov
2023-03-15 16:14:07 +01:00
committed by Space Team
parent 5dd52f7d40
commit 4a88e01a8e
9 changed files with 275 additions and 8 deletions
@@ -32,14 +32,13 @@ 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.internal.*
import org.jetbrains.kotlin.gradle.plugin.internal.BasePluginConfiguration
import org.jetbrains.kotlin.gradle.plugin.internal.DefaultJavaSourceSetsAccessorVariantFactory
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.utils.markResolvable
import org.jetbrains.kotlin.gradle.plugin.sources.DefaultKotlinSourceSetFactory
import org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildStatsService
import org.jetbrains.kotlin.gradle.report.BuildMetricsService
@@ -53,10 +52,6 @@ import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool
import org.jetbrains.kotlin.gradle.testing.internal.KotlinTestsRegistry
import org.jetbrains.kotlin.gradle.tooling.registerBuildKotlinToolingMetadataTask
import org.jetbrains.kotlin.gradle.utils.*
import org.jetbrains.kotlin.gradle.utils.addGradlePluginMetadataAttributes
import org.jetbrains.kotlin.gradle.utils.checkGradleCompatibility
import org.jetbrains.kotlin.gradle.utils.getOrPut
import org.jetbrains.kotlin.gradle.utils.runProjectConfigurationHealthCheck
import org.jetbrains.kotlin.statistics.metrics.StringMetrics
import org.jetbrains.kotlin.tooling.core.KotlinToolingVersion
import kotlin.reflect.KClass
@@ -249,7 +244,10 @@ abstract class KotlinBasePluginWrapper : DefaultKotlinBasePlugin() {
project.registerBuildKotlinToolingMetadataTask()
project.scheduleDiagnosticChecksAndReporting()
project.startKotlinPluginLifecycle()
}
internal open fun createTestRegistry(project: Project) = KotlinTestsRegistry(project)
@@ -257,6 +255,26 @@ abstract class KotlinBasePluginWrapper : DefaultKotlinBasePlugin() {
internal abstract fun getPlugin(
project: Project,
): Plugin<Project>
private fun Project.scheduleDiagnosticChecksAndReporting() {
launchInStage(KotlinPluginLifecycle.Stage.ReadyForExecution) {
// Do not run checkers on projects which configuration finished with failure,
// as the internal state can not be trusted at this point (e.g. not entire of the
// user's buildscript could've been executed) and might produce bogus warnings
runProjectConfigurationHealthCheck {
project.runKotlinGradleProjectCheckers()
}
// TODO: this should run even if the application of KGP has finished with errors,
// because some diagnostics might indicate specifically the root cause of an
// exception.
renderReportedDiagnostics(
project.kotlinToolingDiagnosticsCollector.getDiagnosticsForProject(project),
project.logger,
project.kotlinPropertiesProvider.internalVerboseDiagnostics
)
}
}
}
abstract class AbstractKotlinPluginWrapper(
@@ -21,6 +21,7 @@ import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLI
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_COMPILER_USE_PRECISE_COMPILATION_RESULTS_BACKUP
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_CREATE_DEFAULT_MULTIPLATFORM_PUBLICATIONS
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_EXPERIMENTAL_TRY_K2
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_INTERNAL_VERBOSE_DIAGNOSTICS
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_JS_KARMA_BROWSERS
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_ALLOW_LEGACY_DEPENDENCIES
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.PropertyNames.KOTLIN_MPP_ANDROID_GRADLE_PLUGIN_COMPATIBILITY_NO_WARN
@@ -526,6 +527,9 @@ internal class PropertiesProvider private constructor(private val project: Proje
}
.orElse(false)
val internalVerboseDiagnostics: Boolean
get() = booleanProperty(KOTLIN_INTERNAL_VERBOSE_DIAGNOSTICS) ?: false
/**
* Retrieves a comma-separated list of browsers to use when running karma tests for [target]
* @see KOTLIN_JS_KARMA_BROWSERS
@@ -615,6 +619,7 @@ internal class PropertiesProvider private constructor(private val project: Proje
const val KOTLIN_RUN_COMPILER_VIA_BUILD_TOOLS_API = "kotlin.compiler.runViaBuildToolsApi"
const val KOTLIN_MPP_ALLOW_LEGACY_DEPENDENCIES = "kotlin.mpp.allow.legacy.dependencies"
const val KOTLIN_EXPERIMENTAL_TRY_K2 = "kotlin.experimental.tryK2"
const val KOTLIN_INTERNAL_VERBOSE_DIAGNOSTICS = "kotlin.internal.verboseDiagnostics"
}
companion object {
@@ -0,0 +1,43 @@
/*
* 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.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider
import org.jetbrains.kotlin.gradle.plugin.diagnostics.checkers.CommonMainWithDependsOnChecker
import org.jetbrains.kotlin.gradle.plugin.diagnostics.checkers.DeprecatedKotlinNativeTargetsChecker
import org.jetbrains.kotlin.gradle.plugin.diagnostics.checkers.MissingNativeStdlibChecker
import org.jetbrains.kotlin.gradle.plugin.diagnostics.checkers.UnusedSourceSetsChecker
/**
* Interface for generic checks of a Gradle Project with Kotlin Plugin applied
*
* All checkers are guaranteed to be launched in [org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle.Stage.ReadyForExecution],
* meaning that all changes to DSL are expected to be received, and all structures of KGP are finalised their state.
*
* Checkers **will not** be launched if the project's configuration has finished with failure (e.g. some plugin or a build script
* threw an exception). If your check aims to prevent some exception, then code that check as soon as possible in the `apply`-block
* of the [org.jetbrains.kotlin.gradle.plugin.KotlinBasePluginWrapper] or appropriate inheritor
*/
internal fun interface KotlinGradleProjectChecker {
fun KotlinGradleProjectCheckerContext.runChecks(collector: KotlinToolingDiagnosticsCollector)
companion object {
val ALL_CHECKERS: List<KotlinGradleProjectChecker> = listOf(
CommonMainWithDependsOnChecker,
DeprecatedKotlinNativeTargetsChecker,
MissingNativeStdlibChecker,
UnusedSourceSetsChecker,
)
}
}
internal open class KotlinGradleProjectCheckerContext(
val project: Project,
val kotlinPropertiesProvider: PropertiesProvider,
val multiplatformExtension: KotlinMultiplatformExtension?,
)
@@ -0,0 +1,37 @@
/*
* 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.Project
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull
import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.Companion.kotlinPropertiesProvider
import org.jetbrains.kotlin.gradle.plugin.extraProperties
internal fun Project.runKotlinGradleProjectCheckers() {
val checkers = kotlinGradleProjectCheckersOverride ?: KotlinGradleProjectChecker.ALL_CHECKERS
val context = KotlinGradleProjectCheckerContext(
project,
project.kotlinPropertiesProvider,
project.multiplatformExtensionOrNull
)
val collector = project.kotlinToolingDiagnosticsCollector
for (checker in checkers) {
with(checker) { context.runChecks(collector) }
}
}
internal val Project.kotlinGradleProjectCheckersOverride: Collection<KotlinGradleProjectChecker>?
get() {
return if (extraProperties.has(KOTLIN_GRADLE_PROJECT_CHECKERS_OVERRIDE))
extraProperties.get(KOTLIN_GRADLE_PROJECT_CHECKERS_OVERRIDE) as Collection<KotlinGradleProjectChecker>?
else
null
}
internal const val KOTLIN_GRADLE_PROJECT_CHECKERS_OVERRIDE = "kotlin.internal.override.checkers"
@@ -0,0 +1,65 @@
/*
* 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.InvalidUserCodeException
import org.gradle.api.Project
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
import org.jetbrains.kotlin.gradle.utils.registerClassLoaderScopedBuildService
import java.util.*
import java.util.concurrent.ConcurrentHashMap
private typealias ToolingDiagnosticId = String
private typealias GradleProjectPath = String
internal abstract class KotlinToolingDiagnosticsCollector : BuildService<BuildServiceParameters.None> {
private val diagnosticsFromProject: MutableMap<GradleProjectPath, MutableList<ToolingDiagnostic>> = ConcurrentHashMap()
private val reportedIds: MutableSet<ToolingDiagnosticId> = Collections.newSetFromMap(ConcurrentHashMap())
fun getDiagnosticsForProject(project: Project): Collection<ToolingDiagnostic> = diagnosticsFromProject[project.path].orEmpty()
fun getAllDiagnostics(): Collection<ToolingDiagnostic> = diagnosticsFromProject.values.flatten()
fun report(project: Project, diagnostic: ToolingDiagnostic) {
saveDiagnostic(project, diagnostic)
}
fun reportOncePerGradleProject(fromProject: Project, diagnostic: ToolingDiagnostic, key: ToolingDiagnosticId = diagnostic.id) {
if (reportedIds.add("${fromProject.path}#$key")) {
saveDiagnostic(fromProject, diagnostic)
}
}
fun reportOncePerGradleBuild(fromProject: Project, diagnostic: ToolingDiagnostic, key: ToolingDiagnosticId = diagnostic.id) {
if (reportedIds.add(":#$key")) {
saveDiagnostic(fromProject, diagnostic)
}
}
private fun saveDiagnostic(project: Project, diagnostic: ToolingDiagnostic) {
if (diagnostic.severity == ToolingDiagnostic.Severity.FATAL) {
throw InvalidUserCodeException(diagnostic.message)
}
diagnosticsFromProject.compute(project.path) { _, previousListIfAny ->
previousListIfAny?.apply { add(diagnostic) } ?: mutableListOf(diagnostic)
}
}
}
internal val Project.kotlinToolingDiagnosticsCollector: KotlinToolingDiagnosticsCollector
get() = gradle.registerClassLoaderScopedBuildService(KotlinToolingDiagnosticsCollector::class).get()
internal fun Project.reportDiagnostic(diagnostic: ToolingDiagnostic) {
kotlinToolingDiagnosticsCollector.report(this, diagnostic)
}
internal fun Project.reportDiagnosticOncePerProject(diagnostic: ToolingDiagnostic, key: ToolingDiagnosticId = diagnostic.id) {
kotlinToolingDiagnosticsCollector.reportOncePerGradleProject(this, diagnostic, key)
}
internal fun Project.reportDiagnosticOncePerBuild(diagnostic: ToolingDiagnostic, key: ToolingDiagnosticId = diagnostic.id) {
kotlinToolingDiagnosticsCollector.reportOncePerGradleBuild(this, diagnostic, key)
}
@@ -0,0 +1,37 @@
/*
* 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.jetbrains.kotlin.gradle.InternalKotlinGradlePluginApi
@InternalKotlinGradlePluginApi // used in integration tests
class ToolingDiagnostic(val id: String, val message: String, val severity: Severity) {
enum class Severity {
WARNING,
/**
* Stronger highlighting than WARNING, but doesn't prevent further actions (e.g. further
* tasks) from being executed
*/
ERROR,
/**
* Aborts the progress of the current process (Gradle build/Import/...).
*
* Please use *extremely* sparingly, as failing the current process can:
* - 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)
*/
FATAL,
}
override fun toString(): String {
return "[$id | $severity] $message"
}
}
@@ -0,0 +1,34 @@
/*
* 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.jetbrains.kotlin.gradle.InternalKotlinGradlePluginApi
@InternalKotlinGradlePluginApi // used in integration tests
abstract class ToolingDiagnosticFactory(private val predefinedSeverity: ToolingDiagnostic.Severity?, customId: String?) {
constructor(customId: String) : this(null, customId)
constructor(predefinedSeverity: ToolingDiagnostic.Severity?) : this(predefinedSeverity, null)
open val id: String = customId ?: this::class.simpleName!!
protected fun build(message: String, severity: ToolingDiagnostic.Severity? = null): ToolingDiagnostic {
if (severity == null && predefinedSeverity == null) {
error(
"Can't determine severity. " +
"Either provide it in constructor of ToolingDiagnosticFactory, or in the 'build'-function invocation"
)
}
if (severity != null && predefinedSeverity != null) {
error(
"Please provide severity either in ToolingDiagnosticFactory constructor, or as the 'build'-function parameter," +
" but not both at once"
)
}
return ToolingDiagnostic(id, message, severity ?: predefinedSeverity!!)
}
protected fun String.onlyIf(condition: Boolean) = if (condition) this else ""
}
@@ -0,0 +1,27 @@
/*
* 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.logging.Logger
import org.jetbrains.kotlin.gradle.plugin.diagnostics.ToolingDiagnostic.Severity.*
internal fun renderReportedDiagnostics(diagnostics: Collection<ToolingDiagnostic>, logger: Logger, isVerbose: Boolean) {
for (diagnostic in diagnostics) {
when (diagnostic.severity) {
WARNING -> logger.warn("w: ${diagnostic.render(isVerbose)}\n")
ERROR -> logger.error("e: ${diagnostic.render(isVerbose)}\n")
FATAL ->
error("Internal error: FATAL diagnostics throw an exception immediately in KotlinToolingDiagnosticsCollector")
}
}
}
private fun ToolingDiagnostic.render(isVerbose: Boolean): String =
if (isVerbose) toString() + "\n$DIAGNOSTIC_SEPARATOR" else message
internal const val DIAGNOSTIC_SEPARATOR = "#diagnostic-end"
@@ -6,7 +6,7 @@
package org.jetbrains.kotlin.gradle.internals
import org.jetbrains.kotlin.gradle.plugin.KOTLIN_12X_MPP_DEPRECATION_WARNING
import org.jetbrains.kotlin.gradle.plugin.diagnostics.DIAGNOSTIC_SEPARATOR
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinProjectStructureMetadata
import org.jetbrains.kotlin.gradle.plugin.mpp.MULTIPLATFORM_PROJECT_METADATA_JSON_FILE_NAME
import org.jetbrains.kotlin.gradle.plugin.mpp.parseKotlinSourceSetMetadataFromJson
@@ -27,3 +27,4 @@ const val NO_NATIVE_STDLIB_PROPERTY_WARNING = NO_NATIVE_STDLIB_PROPERTY_WARNING
val KOTLIN_12X_MPP_DEPRECATION_WARNING = KOTLIN_12X_MPP_DEPRECATION_WARNING
const val VERBOSE_DIAGNOSTIC_SEPARATOR = DIAGNOSTIC_SEPARATOR