Native: improve "could not build module" cinterop error message

Extract diagnostics from imported modules by instructing libclang to
serialize diagnostics to file. Then deserialize this file and use
first compilation errors in the error message.

^KT-35059 Fixed
This commit is contained in:
Svyatoslav Scherbina
2021-09-10 17:44:50 +03:00
committed by Space
parent 12f4cd5aa1
commit c9348b3512
3 changed files with 104 additions and 16 deletions
@@ -59,12 +59,19 @@ private fun getModulesASTFiles(index: CXIndex, compilation: ModularCompilation,
)
val result = linkedSetOf<String>()
val errors = mutableListOf<Diagnostic>()
val translationUnit = compilationWithImports.parse(
index,
options = CXTranslationUnit_DetailedPreprocessingRecord
options = CXTranslationUnit_DetailedPreprocessingRecord,
diagnosticHandler = { if (it.isError()) errors.add(it) }
)
try {
if (errors.isNotEmpty()) {
val errorMessage = errors.take(10).joinToString("\n") { it.format }
throw Error(errorMessage)
}
translationUnit.ensureNoCompileErrors()
indexTranslationUnit(index, translationUnit, 0, object : Indexer {
@@ -153,8 +153,85 @@ internal fun parseTranslationUnit(
}
}
internal fun Compilation.parse(index: CXIndex, options: Int = 0): CXTranslationUnit =
parseTranslationUnit(index, this.createTempSource(), this.compilerArgs, options)
internal fun Compilation.parse(
index: CXIndex,
options: Int = 0,
diagnosticHandler: DiagnosticHandler? = null
): CXTranslationUnit {
val arguments = this.compilerArgs.toMutableList()
val serializedDiagnosticsFile: File?
if (diagnosticHandler != null) {
// Translation unit doesn't seem to contain diagnostics from imported modules.
// So if there is an imported module with compilation error, checking only translation unit's diagnostics
// will result in a vague 'fatal error: could not build module $name'.
// See https://youtrack.jetbrains.com/issue/KT-35059.
// So instead instruct Clang to serialize diagnostics to file and then deserialize them.
// This way it is possible to find diagnostics from imported modules as well.
serializedDiagnosticsFile = Files.createTempFile("cinterop", ".d").toFile()
serializedDiagnosticsFile.deleteOnExit()
arguments += listOf(
"-serialize-diagnostics", serializedDiagnosticsFile.absolutePath,
// Enabling -serialize-diagnostics for some reason makes libclang print
// 'X warnings and Y errors generated' to stderr.
// (see https://github.com/llvm/llvm-project/blob/b8debabb775b6d9eec5aa16f1b0c3428cc076bcb/clang/lib/Frontend/CompilerInstance.cpp#L984).
// Add -fno-caret-diagnostics to suppress this:
"-fno-caret-diagnostics",
)
} else {
serializedDiagnosticsFile = null
}
val result = parseTranslationUnit(index, this.createTempSource(), arguments, options)
try {
// Note: in some cases (like when imported module is not found) the file remains empty,
// and deserialization will fail. Handling this by checking the file is not empty:
if (diagnosticHandler != null && serializedDiagnosticsFile != null && serializedDiagnosticsFile.length() > 0) {
reportSerializedDiagnostics(serializedDiagnosticsFile, diagnosticHandler)
}
} catch (e: Throwable) {
clang_disposeTranslationUnit(result)
throw e
}
return result
}
private fun reportSerializedDiagnostics(
serializedDiagnosticsFile: File,
diagnosticHandler: DiagnosticHandler
) {
val diagnosticSet = memScoped {
val errorVar = alloc<CXLoadDiag_Error.Var>()
val errorStringVar = alloc<CXString>()
clang_loadDiagnostics(serializedDiagnosticsFile.absolutePath, errorVar.ptr, errorStringVar.ptr)
?: run {
// Generally not fatal, so just log and continue.
println("""
Warning: unable to load diagnostics from $serializedDiagnosticsFile:
${errorVar.value}: ${errorStringVar.readValue().convertAndDispose()}
""".trimIndent())
return
}
}
try {
val numDiagnostics = clang_getNumDiagnosticsInSet(diagnosticSet)
for (index in 0 until numDiagnostics) {
val diagnostic = clang_getDiagnosticInSet(diagnosticSet, index) ?: continue
try {
diagnosticHandler.report(convertDiagnostic(diagnostic))
} finally {
clang_disposeDiagnostic(diagnostic)
}
}
} finally {
clang_disposeDiagnosticSet(diagnosticSet)
}
}
internal fun interface DiagnosticHandler {
fun report(diagnostic: Diagnostic)
}
internal data class Diagnostic(val severity: CXDiagnosticSeverity, val format: String,
val location: CValue<CXSourceLocation>)
@@ -162,27 +239,31 @@ internal data class Diagnostic(val severity: CXDiagnosticSeverity, val format: S
internal fun CXTranslationUnit.getDiagnostics(): Sequence<Diagnostic> {
val numDiagnostics = clang_getNumDiagnostics(this)
return (0 until numDiagnostics).asSequence()
.map { index ->
val diagnostic = clang_getDiagnostic(this, index)
.mapNotNull { index ->
val diagnostic = clang_getDiagnostic(this, index) ?: return@mapNotNull null
try {
val severity = clang_getDiagnosticSeverity(diagnostic)
val format = clang_formatDiagnostic(diagnostic, clang_defaultDiagnosticDisplayOptions())
.convertAndDispose()
val location = clang_getDiagnosticLocation(diagnostic)
Diagnostic(severity, format, location)
convertDiagnostic(diagnostic)
} finally {
clang_disposeDiagnostic(diagnostic)
}
}
}
internal fun convertDiagnostic(diagnostic: CXDiagnostic): Diagnostic {
val severity = clang_getDiagnosticSeverity(diagnostic)
val format = clang_formatDiagnostic(diagnostic, clang_defaultDiagnosticDisplayOptions())
.convertAndDispose()
val location = clang_getDiagnosticLocation(diagnostic)
return Diagnostic(severity, format, location)
}
internal fun CXTranslationUnit.getCompileErrors(): Sequence<String> =
getDiagnostics().filter { it.isError() }.map { it.format }
private fun Diagnostic.isError() = (severity == CXDiagnosticSeverity.CXDiagnostic_Error) ||
internal fun Diagnostic.isError() = (severity == CXDiagnosticSeverity.CXDiagnostic_Error) ||
(severity == CXDiagnosticSeverity.CXDiagnostic_Fatal)
internal fun CXTranslationUnit.hasCompileErrors() = (this.getCompileErrors().firstOrNull() != null)
@@ -127,7 +127,7 @@ class ModuleTests : IndexerTests() {
getModulesInfo(compilation, modules = listOf("Foo"))
}
assertContains(error.message.orEmpty(), "fatal error: could not build module 'Foo'")
assertContains(error.message.orEmpty(), "error: header 'Foo.h' not found")
}
@Test
@@ -150,7 +150,7 @@ class ModuleTests : IndexerTests() {
getModulesInfo(compilation, modules = listOf("Foo"))
}
assertContains(error.message.orEmpty(), "fatal error: could not build module 'Foo'")
assertContains(error.message.orEmpty(), "testModuleWithBadCode/Foo.h:1:1: error: unknown type name 'bad'")
}
private fun List<String>.canonicalize(): List<String> = this.map { File(it).canonicalPath }