Сбой сборки при предупреждениях Kotlin

89
Сбой сборки при предупреждениях Kotlin
Сбой сборки при предупреждениях Kotlin
Как провалить сборку Gradle при определенных предупреждениях Kotlin.

Предупреждения Kotlin очень полезны для разработчиков. Они помогают очистить код и даже исправить возможные ошибки.

Вот как они выглядят на выходе Gradle:

w: /Users/dipien/android-sample/app/src/main/java/com/dipien/sample/MainActivity.kt: (16, 7): Variable ‘unused’ is never used

Очень важно исправлять их, чтобы улучшить качество вашего кода.

По умолчанию сборки Gradle не дают сбоев при обнаружении предупреждений Kotlin. Это плохо, потому что разработчики могут просто игнорировать эти предупреждения.

Существует способ настроить компилятор Kotlin таким образом, чтобы он выдавал ошибку при каждом предупреждении. Просто добавьте следующую конфигурацию в файл build.gradle :

// For Android Project
android {
    kotlinOptions {
        allWarningsAsErrors = true
    }
}
// For non Android Project
compileKotlin {
    kotlinOptions {
        allWarningsAsErrors = true
    }
}

Проблема

Включение флага allWarningsAsErrors  выглядит нормально, но есть проблема. Использование устаревшего кода также считается предупреждением в Kotlin. Например:

w: /Users/dipien/android-sample/app/src/main/java/com/dipien/sample/MainActivity.kt: (18, 3): ‘deprecatedFunction(): Unit’ is deprecated.

Итак, после включения флага allWarningsAsErrors  компилятор Kotlin будет давать сбой после каждого использования устаревшего кода. А это нежелательно для большинства проектов.

К сожалению, Kotlin не поддерживает игнорирование предупреждений is deprecated  или предотвращение ошибок при их появлении.

Решение

Следующий скрипт Gradle предлагает решение этой проблемы.

import org.gradle.api.Project
import org.gradle.api.internal.GradleInternal
import org.gradle.configurationcache.extensions.serviceOf
import org.gradle.internal.logging.events.operations.LogEventBuildOperationProgressDetails
import org.gradle.internal.operations.BuildOperationDescriptor
import org.gradle.internal.operations.BuildOperationListener
import org.gradle.internal.operations.BuildOperationListenerManager
import org.gradle.internal.operations.OperationFinishEvent
import org.gradle.internal.operations.OperationIdentifier
import org.gradle.internal.operations.OperationProgressEvent
import org.gradle.internal.operations.OperationStartEvent

val kotlinWarnings = mutableListOf<String>()
val nonKotlinWarnings = mutableListOf<String>()
val errors = mutableListOf<String>()
val buildOperationListener = object : BuildOperationListener {

    override fun started(buildOperation: BuildOperationDescriptor, startEvent: OperationStartEvent) { }

    override fun progress(operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent) {
        val log = progressEvent.details
        if (log is LogEventBuildOperationProgressDetails) {
            if (log.logLevel == LogLevel.WARN) {
                if (log.message.contains("w:") && log.message.contains(".kt")) {
                    if (!log.message.contains("is deprecated") && !kotlinWarnings.contains(log.message)) {
                        kotlinWarnings.add(log.message)
                    }
                } else {
                    if (!nonKotlinWarnings.contains(log.message)) {
                        nonKotlinWarnings.add(log.message)
                    }
                }
            } else if (log.logLevel == LogLevel.ERROR) {
                if (!errors.contains(log.message)) {
                    errors.add(log.message)
                }
            }
        }
    }

    override fun finished(buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) { }
}
val gradleInternal = project.gradle as GradleInternal
val buildOperationListenerManager = gradleInternal.serviceOf() as BuildOperationListenerManager?
buildOperationListenerManager?.addListener(buildOperationListener)
project.gradle.buildFinished { buildResult ->
    buildOperationListenerManager?.removeListener(buildOperationListener)

    if (buildResult.failure == null && nonKotlinWarnings.isNotEmpty()) {
        project.logger.warn("")
        project.logger.warn("================================= Project Warnings Summary ================================================================================")
        nonKotlinWarnings.forEach { project.logger.warn(it) }
        project.logger.warn("===========================================================================================================================================")
    }

    if (kotlinWarnings.isNotEmpty()) {
        project.logger.warn("")
        project.logger.warn("====================================== Kotlin Warnings ====================================================================================")
        kotlinWarnings.forEach { project.logger.warn(it) }
        project.logger.warn("===========================================================================================================================================")
        if (buildResult.failure == null) {
            throw RuntimeException("Kotlin warning found")
        }
    }

    if (buildResult.failure != null && errors.isNotEmpty()) {
        project.logger.warn("")
        project.logger.warn("================================= Project Errors Summary ==================================================================================")
        errors.forEach { project.logger.error(it) }
        project.logger.warn("===========================================================================================================================================")
    }
}

По сути, в сборку Gradle добавляется BuildOperationListener, чтобы мы могли отслеживать каждый консольный журнал. Мы проверяем каждый журнал в поисках предупреждений Kotlin, но игнорируем предупреждения об устаревшем использовании. Вы можете настроить скрипт так, чтобы он игнорировал любые другие предупреждения. Наконец, мы выбрасываем исключение, если в проекте есть предупреждения Kotlin.

Учтите, что для того, чтобы этот скрипт работал, необходимо, чтобы флаги allWarningsAsErrors иsuppressWarnings в  kotlinOptions были отключены (значения по умолчанию).

Как вы видите, это решение использует множество классов org.gradle.internal, которые не предназначены для использования, потому что они не являются частью Gradle API. Поэтому скрипт может перестать работать в будущем, если Gradle решит изменить эти классы. Он был протестирован на Gradle 6.8 и 7.1 и работает корректно.

У вас есть 4 различных варианта включения этого скрипта в ваш проект:

  1. Создайте файлfail_on_kotlin_warning.gradle.kts с логикой и включите его в сборку:
// From a .gradle script
apply from: "kotlin_warnings.gradle.kts"
// From a .gradle.kts script
apply(from = "kotlin_warnings.gradle.kts")

2. Скопируйте логику и включите ее непосредственно в файл build.gradle.kts

3. Поместите логику в каталог buildSrc

4. Поместите логику в собственный плагин Gradle

Недостатки:

  • Сценарий зависит от некоторых внутренних классов Gradle
  • Учитывая, что вы не можете включить флагsuppressWarnings, вы увидите все  is deprecated предупреждения на консоли