Skip to content

Commit 1b9bff2

Browse files
authored
Capture diagnostics with a severity level (#260)
* Capture diagnostics with a severity level This allows the output to be more easily filtered after the fact Fixes #198 * fix failing test
1 parent 0a63e02 commit 1b9bff2

File tree

12 files changed

+327
-47
lines changed

12 files changed

+327
-47
lines changed

core/src/main/kotlin/com/tschuchort/compiletesting/AbstractKotlinCompilation.kt

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.tschuchort.compiletesting
22

3+
import com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting.DiagnosticMessage
4+
import com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting.DiagnosticsMessageCollector
5+
import com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting.MultiMessageCollector
36
import java.io.File
47
import java.io.OutputStream
58
import java.io.PrintStream
@@ -14,6 +17,7 @@ import org.jetbrains.kotlin.cli.common.ExitCode
1417
import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments
1518
import org.jetbrains.kotlin.cli.common.arguments.parseCommandLineArguments
1619
import org.jetbrains.kotlin.cli.common.arguments.validateArguments
20+
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
1721
import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
1822
import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector
1923
import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
@@ -335,22 +339,39 @@ abstract class AbstractKotlinCompilation<A : CommonCompilerArguments> internal c
335339
/* autoFlush = */ false,
336340
/* encoding = */ "UTF-8",
337341
)
342+
private val _diagnostics: MutableList<DiagnosticMessage> = mutableListOf()
343+
protected val diagnostics: List<DiagnosticMessage> get() = _diagnostics
344+
345+
protected fun createMessageCollector(stepName: String): MessageCollector {
346+
val diagnosticsMessageCollector = DiagnosticsMessageCollector(stepName, verbose, _diagnostics)
347+
val printMessageCollector = PrintingMessageCollector(
348+
internalMessageStream,
349+
MessageRenderer.GRADLE_STYLE,
350+
verbose
351+
)
352+
return MultiMessageCollector(diagnosticsMessageCollector, printMessageCollector)
353+
}
338354

339355
protected fun log(s: String) {
340-
if (verbose)
356+
if (verbose) {
341357
internalMessageStream.println("logging: $s")
358+
}
342359
}
343360

344-
protected fun warn(s: String) = internalMessageStream.println("warning: $s")
345-
protected fun error(s: String) = internalMessageStream.println("error: $s")
361+
protected fun warn(s: String) {
362+
internalMessageStream.println("warning: $s")
363+
}
364+
protected fun error(s: String) {
365+
internalMessageStream.println("error: $s")
366+
}
346367

347-
internal val internalMessageStreamAccess: PrintStream get() = internalMessageStream
368+
internal fun createMessageCollectorAccess(stepName: String): MessageCollector = createMessageCollector(stepName)
348369

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

352373
@ExperimentalCompilerApi
353-
internal fun convertKotlinExitCode(code: ExitCode) = when(code) {
374+
internal fun convertKotlinExitCode(code: ExitCode) = when (code) {
354375
ExitCode.OK -> KotlinCompilation.ExitCode.OK
355376
ExitCode.OOM_ERROR -> throw OutOfMemoryError("Kotlin compiler ran out of memory")
356377
ExitCode.INTERNAL_ERROR -> KotlinCompilation.ExitCode.INTERNAL_ERROR

core/src/main/kotlin/com/tschuchort/compiletesting/CompilationResult.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.tschuchort.compiletesting
22

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

1825
@ExperimentalCompilerApi
1926
class JsCompilationResult(
2027
override val exitCode: KotlinCompilation.ExitCode,
2128
override val messages: String,
29+
override val diagnosticMessages: List<DiagnosticMessage>,
2230
private val compilation: KotlinJsCompilation,
2331
) : CompilationResult {
2432
override val outputDirectory: File
@@ -33,6 +41,7 @@ class JsCompilationResult(
3341
class JvmCompilationResult(
3442
override val exitCode: KotlinCompilation.ExitCode,
3543
override val messages: String,
44+
override val diagnosticMessages: List<DiagnosticMessage>,
3645
private val compilation: KotlinCompilation,
3746
) : CompilationResult {
3847
override val outputDirectory: File
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2020 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting
17+
18+
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
19+
20+
public enum class DiagnosticSeverity {
21+
ERROR,
22+
WARNING,
23+
INFO,
24+
LOGGING,
25+
}
26+
27+
/**
28+
* Holder for diagnostics messages
29+
*/
30+
public data class DiagnosticMessage(
31+
val severity: DiagnosticSeverity,
32+
val message: String,
33+
)
34+
35+
internal fun CompilerMessageSeverity.toSeverity() = when (this) {
36+
CompilerMessageSeverity.EXCEPTION,
37+
CompilerMessageSeverity.ERROR -> DiagnosticSeverity.ERROR
38+
CompilerMessageSeverity.STRONG_WARNING,
39+
CompilerMessageSeverity.WARNING -> DiagnosticSeverity.WARNING
40+
CompilerMessageSeverity.INFO -> DiagnosticSeverity.INFO
41+
CompilerMessageSeverity.LOGGING,
42+
CompilerMessageSeverity.OUTPUT -> DiagnosticSeverity.LOGGING
43+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright 2021 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting
17+
18+
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
19+
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation
20+
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
21+
import javax.tools.Diagnostic
22+
23+
/**
24+
* Custom message collector for Kotlin compilation that collects messages into
25+
* [DiagnosticMessage] objects.
26+
*/
27+
internal class DiagnosticsMessageCollector(
28+
private val stepName: String,
29+
private val verbose: Boolean,
30+
private val diagnostics: MutableList<DiagnosticMessage>,
31+
) : MessageCollector {
32+
33+
override fun clear() {
34+
diagnostics.clear()
35+
}
36+
37+
/**
38+
* Returns `true` if this collector has any warning messages.
39+
*/
40+
fun hasWarnings() = diagnostics.any {
41+
it.severity == DiagnosticSeverity.WARNING
42+
}
43+
44+
override fun hasErrors(): Boolean {
45+
return diagnostics.any {
46+
it.severity == DiagnosticSeverity.ERROR
47+
}
48+
}
49+
50+
override fun report(
51+
severity: CompilerMessageSeverity,
52+
message: String,
53+
location: CompilerMessageSourceLocation?
54+
) {
55+
if (!verbose && CompilerMessageSeverity.VERBOSE.contains(severity)) return
56+
57+
val severity =
58+
if (stepName == "kapt" && getJavaVersion() >= 17) {
59+
// Workaround for KT-54030
60+
message.getSeverityFromPrefix() ?: severity.toSeverity()
61+
} else {
62+
severity.toSeverity()
63+
}
64+
doReport(severity, message)
65+
}
66+
67+
private fun doReport(
68+
severity: DiagnosticSeverity,
69+
message: String,
70+
) {
71+
if (message == KSP_ADDITIONAL_ERROR_MESSAGE) {
72+
// ignore this as it will impact error counts.
73+
return
74+
}
75+
// Strip kapt/ksp prefixes
76+
val strippedMessage = message.stripPrefixes()
77+
diagnostics.add(
78+
DiagnosticMessage(
79+
severity = severity,
80+
message = strippedMessage,
81+
)
82+
)
83+
}
84+
85+
/**
86+
* Removes prefixes added by kapt / ksp from the message
87+
*/
88+
private fun String.stripPrefixes(): String {
89+
return stripKind().stripKspPrefix()
90+
}
91+
92+
/**
93+
* KAPT prepends the message kind to the message, we'll remove it here.
94+
*/
95+
private fun String.stripKind(): String {
96+
val firstLine = lineSequence().firstOrNull() ?: return this
97+
val match = KIND_REGEX.find(firstLine) ?: return this
98+
return substring(match.range.last + 1)
99+
}
100+
101+
/**
102+
* KSP prepends ksp to each message, we'll strip it here.
103+
*/
104+
private fun String.stripKspPrefix(): String {
105+
val firstLine = lineSequence().firstOrNull() ?: return this
106+
val match = KSP_PREFIX_REGEX.find(firstLine) ?: return this
107+
return substring(match.range.last + 1)
108+
}
109+
110+
private fun String.getSeverityFromPrefix(): DiagnosticSeverity? {
111+
val kindMatch =
112+
// The (\w+) for the kind prefix is is the 4th capture group
113+
KAPT_LOCATION_AND_KIND_REGEX.find(this)?.groupValues?.getOrNull(4)
114+
// The (\w+) is the 1st capture group
115+
?: KIND_REGEX.find(this)?.groupValues?.getOrNull(1)
116+
?: return null
117+
return if (kindMatch.equals("error", ignoreCase = true)) {
118+
DiagnosticSeverity.ERROR
119+
} else if (kindMatch.equals("warning", ignoreCase = true)) {
120+
DiagnosticSeverity.WARNING
121+
} else if (kindMatch.equals("note", ignoreCase = true)) {
122+
DiagnosticSeverity.INFO
123+
} else {
124+
null
125+
}
126+
}
127+
128+
private fun getJavaVersion(): Int =
129+
System.getProperty("java.specification.version")?.substringAfter('.')?.toIntOrNull() ?: 6
130+
companion object {
131+
// example: foo/bar/Subject.kt:2: warning: the real message
132+
private val KAPT_LOCATION_AND_KIND_REGEX = """^(.*\.(kt|java)):(\d+): (\w+): """.toRegex()
133+
// detect things like "Note: " to be stripped from the message.
134+
// We could limit this to known diagnostic kinds (instead of matching \w:) but it is always
135+
// added so not really necessary until we hit a parser bug :)
136+
// example: "error: the real message"
137+
private val KIND_REGEX = """^(\w+): """.toRegex()
138+
// example: "[ksp] the real message"
139+
private val KSP_PREFIX_REGEX = """^\[ksp] """.toRegex()
140+
// KSP always prints an additional error if any other error occurred.
141+
// We drop that additional message to provide a more consistent error count with KAPT/javac.
142+
private const val KSP_ADDITIONAL_ERROR_MESSAGE =
143+
"Error occurred in KSP, check log for detail"
144+
}
145+
}

core/src/main/kotlin/com/tschuchort/compiletesting/KotlinCompilation.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.tschuchort.compiletesting
1818

1919
import com.facebook.buck.jvm.java.javax.SynchronizedToolProvider
20+
import com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting.DiagnosticMessage
2021
import com.tschuchort.compiletesting.kapt.toPluginOptions
2122
import java.io.File
2223
import java.io.OutputStreamWriter
@@ -26,8 +27,6 @@ import javax.tools.Diagnostic
2627
import javax.tools.DiagnosticCollector
2728
import javax.tools.JavaFileObject
2829
import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
29-
import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
30-
import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector
3130
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
3231
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
3332
import org.jetbrains.kotlin.config.JVMAssertionsMode
@@ -359,8 +358,7 @@ class KotlinCompilation : AbstractKotlinCompilation<K2JVMCompilerArguments>() {
359358
}
360359
}
361360

362-
val compilerMessageCollector =
363-
PrintingMessageCollector(internalMessageStream, MessageRenderer.GRADLE_STYLE, verbose)
361+
val compilerMessageCollector = createMessageCollector("kapt")
364362

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

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

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

624-
return JvmCompilationResult(exitCode, messages, this)
622+
return JvmCompilationResult(exitCode, messages, diagnostics, this)
625623
}
626624

627625
internal fun commonClasspaths() =

core/src/main/kotlin/com/tschuchort/compiletesting/KotlinJsCompilation.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ class KotlinJsCompilation : AbstractKotlinCompilation<K2JSCompilerArguments>() {
118118
if (exitCode != KotlinCompilation.ExitCode.OK)
119119
searchSystemOutForKnownErrors(messages)
120120

121-
return JsCompilationResult(exitCode, messages, this)
121+
return JsCompilationResult(exitCode, messages, diagnostics, this)
122122
}
123123

124124
private fun jsClasspath() = mutableListOf<File>().apply {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting
2+
3+
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
4+
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation
5+
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
6+
7+
internal class MultiMessageCollector(
8+
private vararg val collectors: MessageCollector
9+
) : MessageCollector {
10+
11+
override fun clear() {
12+
collectors.forEach { it.clear() }
13+
}
14+
15+
override fun hasErrors(): Boolean {
16+
return collectors.any { it.hasErrors() }
17+
}
18+
19+
override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageSourceLocation?) {
20+
collectors.forEach { it.report(severity, message, location) }
21+
}
22+
}

core/src/test/kotlin/com/tschuchort/compiletesting/KotlinCompilationTests.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.tschuchort.compiletesting
22

3+
import com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting.DiagnosticMessage
4+
import com.facebook.buck.jvm.java.javax.com.tschuchort.compiletesting.DiagnosticSeverity
35
import com.nhaarman.mockitokotlin2.*
46
import com.tschuchort.compiletesting.KotlinCompilation.ExitCode
57
import com.tschuchort.compiletesting.MockitoAdditionalMatchersKotlin.Companion.not
@@ -484,6 +486,8 @@ class KotlinCompilationTests(
484486

485487
assertThat(result.exitCode).isEqualTo(ExitCode.OK)
486488
assertThat(result.messages).contains(JavaTestProcessor.ON_INIT_MSG)
489+
assertThat(result.diagnosticMessages)
490+
.contains(DiagnosticMessage(DiagnosticSeverity.WARNING, JavaTestProcessor.ON_INIT_MSG))
487491

488492
assertThat(ProcessedElemMessage.parseAllIn(result.messages)).anyMatch {
489493
it.elementSimpleName == "JSource"

0 commit comments

Comments
 (0)