DiagnosticsMessageCollector.kt
/*
* 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 androidx.room.compiler.processing.util.compiler
import androidx.room.compiler.processing.util.compiler.steps.RawDiagnosticMessage
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
* [RawDiagnosticMessage] objects.
*
* Neither KAPT nor KSP report location in the `location` parameter of the callback, instead,
* they embed location into the messages. This collector parses these messages to recover the
* location.
*/
internal class DiagnosticsMessageCollector : MessageCollector {
private val diagnostics = mutableListOf<RawDiagnosticMessage>()
fun getDiagnostics(): List<RawDiagnosticMessage> = diagnostics
override fun clear() {
diagnostics.clear()
}
/**
* Returns `true` if this collector has any warning messages.
*/
fun hasWarnings() = diagnostics.any {
it.kind == Diagnostic.Kind.WARNING || it.kind == Diagnostic.Kind.MANDATORY_WARNING
}
override fun hasErrors(): Boolean {
return diagnostics.any {
it.kind == Diagnostic.Kind.ERROR
}
}
override fun report(
severity: CompilerMessageSeverity,
message: String,
location: CompilerMessageSourceLocation?
) {
// Both KSP and KAPT reports null location but instead put the location into the message.
// We parse it back here to recover the location.
val (strippedMessage, rawLocation) = if (location == null) {
message.parseLocation() ?: message.stripPrefixes() to null
} else {
message.stripPrefixes() to location.toRawLocation()
}
diagnostics.add(
RawDiagnosticMessage(
kind = severity.kind,
message = strippedMessage,
location = rawLocation
)
)
}
/**
* Parses the location out of a diagnostic message.
*
* Note that this is tailor made for KSP and KAPT where the location is reported in the first
* line of the message.
*
* If location is found, this method will return the location along with the message without
* location. Otherwise, it will return `null`.
*/
private fun String.parseLocation(): Pair<String, RawDiagnosticMessage.Location>? {
val firstLine = lineSequence().firstOrNull() ?: return null
val match =
KSP_LOCATION_REGEX.find(firstLine) ?: KAPT_LOCATION_AND_KIND_REGEX.find(firstLine)
?: return null
if (match.groups.size != 4) return null
return substring(match.range.last + 1) to RawDiagnosticMessage.Location(
path = match.groupValues[1],
line = match.groupValues[3].toInt(),
)
}
/**
* 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 CompilerMessageSourceLocation.toRawLocation(): RawDiagnosticMessage.Location {
return RawDiagnosticMessage.Location(
line = this.line,
path = this.path
)
}
private val CompilerMessageSeverity.kind
get() = when (this) {
CompilerMessageSeverity.ERROR,
CompilerMessageSeverity.EXCEPTION -> Diagnostic.Kind.ERROR
CompilerMessageSeverity.INFO,
CompilerMessageSeverity.LOGGING -> Diagnostic.Kind.NOTE
CompilerMessageSeverity.WARNING,
CompilerMessageSeverity.STRONG_WARNING -> Diagnostic.Kind.WARNING
else -> Diagnostic.Kind.OTHER
}
companion object {
// example: foo/bar/Subject.kt:2: warning: the real message
private val KAPT_LOCATION_AND_KIND_REGEX = """^(.*\.(kt|java)):(\d+): \w+: """.toRegex()
// example: [ksp] /foo/bar/Subject.kt:3: the real message
private val KSP_LOCATION_REGEX = """^\[ksp] (.*\.(kt|java)):(\d+): """.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()
}
}