Skip to content

Commit

Permalink
Capture diagnostics with a severity level (#260)
Browse files Browse the repository at this point in the history
* Capture diagnostics with a severity level

This allows the output to be more easily filtered after the fact

Fixes #198

* fix failing test
  • Loading branch information
evant authored Jun 27, 2024
1 parent 0a63e02 commit 1b9bff2
Show file tree
Hide file tree
Showing 12 changed files with 327 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.tschuchort.compiletesting

import com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting.DiagnosticMessage
import com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting.DiagnosticsMessageCollector
import com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting.MultiMessageCollector
import java.io.File
import java.io.OutputStream
import java.io.PrintStream
Expand All @@ -14,6 +17,7 @@ import org.jetbrains.kotlin.cli.common.ExitCode
import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments
import org.jetbrains.kotlin.cli.common.arguments.parseCommandLineArguments
import org.jetbrains.kotlin.cli.common.arguments.validateArguments
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector
import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
Expand Down Expand Up @@ -335,22 +339,39 @@ abstract class AbstractKotlinCompilation<A : CommonCompilerArguments> internal c
/* autoFlush = */ false,
/* encoding = */ "UTF-8",
)
private val _diagnostics: MutableList<DiagnosticMessage> = mutableListOf()
protected val diagnostics: List<DiagnosticMessage> get() = _diagnostics

protected fun createMessageCollector(stepName: String): MessageCollector {
val diagnosticsMessageCollector = DiagnosticsMessageCollector(stepName, verbose, _diagnostics)
val printMessageCollector = PrintingMessageCollector(
internalMessageStream,
MessageRenderer.GRADLE_STYLE,
verbose
)
return MultiMessageCollector(diagnosticsMessageCollector, printMessageCollector)
}

protected fun log(s: String) {
if (verbose)
if (verbose) {
internalMessageStream.println("logging: $s")
}
}

protected fun warn(s: String) = internalMessageStream.println("warning: $s")
protected fun error(s: String) = internalMessageStream.println("error: $s")
protected fun warn(s: String) {
internalMessageStream.println("warning: $s")
}
protected fun error(s: String) {
internalMessageStream.println("error: $s")
}

internal val internalMessageStreamAccess: PrintStream get() = internalMessageStream
internal fun createMessageCollectorAccess(stepName: String): MessageCollector = createMessageCollector(stepName)

private val resourceName = "META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar"
}

@ExperimentalCompilerApi
internal fun convertKotlinExitCode(code: ExitCode) = when(code) {
internal fun convertKotlinExitCode(code: ExitCode) = when (code) {
ExitCode.OK -> KotlinCompilation.ExitCode.OK
ExitCode.OOM_ERROR -> throw OutOfMemoryError("Kotlin compiler ran out of memory")
ExitCode.INTERNAL_ERROR -> KotlinCompilation.ExitCode.INTERNAL_ERROR
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.tschuchort.compiletesting

import com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting.DiagnosticMessage
import com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting.DiagnosticSeverity
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
import java.io.File
import java.net.URLClassLoader
Expand All @@ -11,14 +13,20 @@ sealed interface CompilationResult {
val exitCode: KotlinCompilation.ExitCode
/** Messages that were printed by the compilation. */
val messages: String
/** Messages with captured diagnostic severity. */
val diagnosticMessages: List<DiagnosticMessage>
/** The directory where compiled files will be output to. */
val outputDirectory: File
/** Messages filtered by the given severities */
fun messagesWithSeverity(vararg severities: DiagnosticSeverity): String =
diagnosticMessages.filter { it.severity in severities }.joinToString("\n")
}

@ExperimentalCompilerApi
class JsCompilationResult(
override val exitCode: KotlinCompilation.ExitCode,
override val messages: String,
override val diagnosticMessages: List<DiagnosticMessage>,
private val compilation: KotlinJsCompilation,
) : CompilationResult {
override val outputDirectory: File
Expand All @@ -33,6 +41,7 @@ class JsCompilationResult(
class JvmCompilationResult(
override val exitCode: KotlinCompilation.ExitCode,
override val messages: String,
override val diagnosticMessages: List<DiagnosticMessage>,
private val compilation: KotlinCompilation,
) : CompilationResult {
override val outputDirectory: File
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting

import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity

public enum class DiagnosticSeverity {
ERROR,
WARNING,
INFO,
LOGGING,
}

/**
* Holder for diagnostics messages
*/
public data class DiagnosticMessage(
val severity: DiagnosticSeverity,
val message: String,
)

internal fun CompilerMessageSeverity.toSeverity() = when (this) {
CompilerMessageSeverity.EXCEPTION,
CompilerMessageSeverity.ERROR -> DiagnosticSeverity.ERROR
CompilerMessageSeverity.STRONG_WARNING,
CompilerMessageSeverity.WARNING -> DiagnosticSeverity.WARNING
CompilerMessageSeverity.INFO -> DiagnosticSeverity.INFO
CompilerMessageSeverity.LOGGING,
CompilerMessageSeverity.OUTPUT -> DiagnosticSeverity.LOGGING
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting

import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import javax.tools.Diagnostic

/**
* Custom message collector for Kotlin compilation that collects messages into
* [DiagnosticMessage] objects.
*/
internal class DiagnosticsMessageCollector(
private val stepName: String,
private val verbose: Boolean,
private val diagnostics: MutableList<DiagnosticMessage>,
) : MessageCollector {

override fun clear() {
diagnostics.clear()
}

/**
* Returns `true` if this collector has any warning messages.
*/
fun hasWarnings() = diagnostics.any {
it.severity == DiagnosticSeverity.WARNING
}

override fun hasErrors(): Boolean {
return diagnostics.any {
it.severity == DiagnosticSeverity.ERROR
}
}

override fun report(
severity: CompilerMessageSeverity,
message: String,
location: CompilerMessageSourceLocation?
) {
if (!verbose && CompilerMessageSeverity.VERBOSE.contains(severity)) return

val severity =
if (stepName == "kapt" && getJavaVersion() >= 17) {
// Workaround for KT-54030
message.getSeverityFromPrefix() ?: severity.toSeverity()
} else {
severity.toSeverity()
}
doReport(severity, message)
}

private fun doReport(
severity: DiagnosticSeverity,
message: String,
) {
if (message == KSP_ADDITIONAL_ERROR_MESSAGE) {
// ignore this as it will impact error counts.
return
}
// Strip kapt/ksp prefixes
val strippedMessage = message.stripPrefixes()
diagnostics.add(
DiagnosticMessage(
severity = severity,
message = strippedMessage,
)
)
}

/**
* Removes prefixes added by kapt / ksp from the message
*/
private fun String.stripPrefixes(): String {
return stripKind().stripKspPrefix()
}

/**
* KAPT prepends the message kind to the message, we'll remove it here.
*/
private fun String.stripKind(): String {
val firstLine = lineSequence().firstOrNull() ?: return this
val match = KIND_REGEX.find(firstLine) ?: return this
return substring(match.range.last + 1)
}

/**
* KSP prepends ksp to each message, we'll strip it here.
*/
private fun String.stripKspPrefix(): String {
val firstLine = lineSequence().firstOrNull() ?: return this
val match = KSP_PREFIX_REGEX.find(firstLine) ?: return this
return substring(match.range.last + 1)
}

private fun String.getSeverityFromPrefix(): DiagnosticSeverity? {
val kindMatch =
// The (\w+) for the kind prefix is is the 4th capture group
KAPT_LOCATION_AND_KIND_REGEX.find(this)?.groupValues?.getOrNull(4)
// The (\w+) is the 1st capture group
?: KIND_REGEX.find(this)?.groupValues?.getOrNull(1)
?: return null
return if (kindMatch.equals("error", ignoreCase = true)) {
DiagnosticSeverity.ERROR
} else if (kindMatch.equals("warning", ignoreCase = true)) {
DiagnosticSeverity.WARNING
} else if (kindMatch.equals("note", ignoreCase = true)) {
DiagnosticSeverity.INFO
} else {
null
}
}

private fun getJavaVersion(): Int =
System.getProperty("java.specification.version")?.substringAfter('.')?.toIntOrNull() ?: 6
companion object {
// example: foo/bar/Subject.kt:2: warning: the real message
private val KAPT_LOCATION_AND_KIND_REGEX = """^(.*\.(kt|java)):(\d+): (\w+): """.toRegex()
// detect things like "Note: " to be stripped from the message.
// We could limit this to known diagnostic kinds (instead of matching \w:) but it is always
// added so not really necessary until we hit a parser bug :)
// example: "error: the real message"
private val KIND_REGEX = """^(\w+): """.toRegex()
// example: "[ksp] the real message"
private val KSP_PREFIX_REGEX = """^\[ksp] """.toRegex()
// KSP always prints an additional error if any other error occurred.
// We drop that additional message to provide a more consistent error count with KAPT/javac.
private const val KSP_ADDITIONAL_ERROR_MESSAGE =
"Error occurred in KSP, check log for detail"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.tschuchort.compiletesting

import com.facebook.buck.jvm.java.javax.SynchronizedToolProvider
import com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting.DiagnosticMessage
import com.tschuchort.compiletesting.kapt.toPluginOptions
import java.io.File
import java.io.OutputStreamWriter
Expand All @@ -26,8 +27,6 @@ import javax.tools.Diagnostic
import javax.tools.DiagnosticCollector
import javax.tools.JavaFileObject
import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
import org.jetbrains.kotlin.config.JVMAssertionsMode
Expand Down Expand Up @@ -359,8 +358,7 @@ class KotlinCompilation : AbstractKotlinCompilation<K2JVMCompilerArguments>() {
}
}

val compilerMessageCollector =
PrintingMessageCollector(internalMessageStream, MessageRenderer.GRADLE_STYLE, verbose)
val compilerMessageCollector = createMessageCollector("kapt")

val kaptLogger = MessageCollectorBackedKaptLogger(kaptOptions.build(), compilerMessageCollector)

Expand Down Expand Up @@ -621,7 +619,7 @@ class KotlinCompilation : AbstractKotlinCompilation<K2JVMCompilerArguments>() {

if (exitCode != ExitCode.OK) searchSystemOutForKnownErrors(messages)

return JvmCompilationResult(exitCode, messages, this)
return JvmCompilationResult(exitCode, messages, diagnostics, this)
}

internal fun commonClasspaths() =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class KotlinJsCompilation : AbstractKotlinCompilation<K2JSCompilerArguments>() {
if (exitCode != KotlinCompilation.ExitCode.OK)
searchSystemOutForKnownErrors(messages)

return JsCompilationResult(exitCode, messages, this)
return JsCompilationResult(exitCode, messages, diagnostics, this)
}

private fun jsClasspath() = mutableListOf<File>().apply {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting

import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation
import org.jetbrains.kotlin.cli.common.messages.MessageCollector

internal class MultiMessageCollector(
private vararg val collectors: MessageCollector
) : MessageCollector {

override fun clear() {
collectors.forEach { it.clear() }
}

override fun hasErrors(): Boolean {
return collectors.any { it.hasErrors() }
}

override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageSourceLocation?) {
collectors.forEach { it.report(severity, message, location) }
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.tschuchort.compiletesting

import com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting.DiagnosticMessage
import com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting.DiagnosticSeverity
import com.nhaarman.mockitokotlin2.*
import com.tschuchort.compiletesting.KotlinCompilation.ExitCode
import com.tschuchort.compiletesting.MockitoAdditionalMatchersKotlin.Companion.not
Expand Down Expand Up @@ -484,6 +486,8 @@ class KotlinCompilationTests(

assertThat(result.exitCode).isEqualTo(ExitCode.OK)
assertThat(result.messages).contains(JavaTestProcessor.ON_INIT_MSG)
assertThat(result.diagnosticMessages)
.contains(DiagnosticMessage(DiagnosticSeverity.WARNING, JavaTestProcessor.ON_INIT_MSG))

assertThat(ProcessedElemMessage.parseAllIn(result.messages)).anyMatch {
it.elementSimpleName == "JSource"
Expand Down
Loading

0 comments on commit 1b9bff2

Please sign in to comment.