From 744735ff30c96f218414514e99019c8ffc700dfe Mon Sep 17 00:00:00 2001 From: Tomasz Godzik Date: Wed, 28 Feb 2024 16:43:29 +0100 Subject: [PATCH] bugfix: Fix showing Scala 3 diagnostics Previously, Scala 3 diagnsotics would not be shown since they currently use rendered message, which is a pretty printed diagnotic. Now, I added a separate parser for Scala 3 compiler diagnostics. --- .../diagnostics/BazelRootMessageParser.kt | 2 +- .../server/diagnostics/DiagnosticsParser.kt | 1 + .../Scala3CompilerDiagnosticParser.kt | 55 ++++++++++ .../diagnostics/DiagnosticsServiceTest.kt | 100 ++++++++++++++++++ 4 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 server/src/main/kotlin/org/jetbrains/bsp/bazel/server/diagnostics/Scala3CompilerDiagnosticParser.kt diff --git a/server/src/main/kotlin/org/jetbrains/bsp/bazel/server/diagnostics/BazelRootMessageParser.kt b/server/src/main/kotlin/org/jetbrains/bsp/bazel/server/diagnostics/BazelRootMessageParser.kt index 91b472696..166d3f372 100644 --- a/server/src/main/kotlin/org/jetbrains/bsp/bazel/server/diagnostics/BazelRootMessageParser.kt +++ b/server/src/main/kotlin/org/jetbrains/bsp/bazel/server/diagnostics/BazelRootMessageParser.kt @@ -56,5 +56,5 @@ object BazelRootMessageParser : Parser { } private fun collectCompilerDiagnostics(output: Output) = - generateSequence { CompilerDiagnosticParser.tryParseOne(output) }.toList() + generateSequence { CompilerDiagnosticParser.tryParseOne(output) ?: Scala3CompilerDiagnosticParser.tryParseOne(output) }.toList() } diff --git a/server/src/main/kotlin/org/jetbrains/bsp/bazel/server/diagnostics/DiagnosticsParser.kt b/server/src/main/kotlin/org/jetbrains/bsp/bazel/server/diagnostics/DiagnosticsParser.kt index 4ffec6f07..7a13ae670 100644 --- a/server/src/main/kotlin/org/jetbrains/bsp/bazel/server/diagnostics/DiagnosticsParser.kt +++ b/server/src/main/kotlin/org/jetbrains/bsp/bazel/server/diagnostics/DiagnosticsParser.kt @@ -51,6 +51,7 @@ class DiagnosticsParser { private val Parsers = listOf( BazelRootMessageParser, CompilerDiagnosticParser, + Scala3CompilerDiagnosticParser, AllCatchParser ) private val IgnoredLines = listOf( diff --git a/server/src/main/kotlin/org/jetbrains/bsp/bazel/server/diagnostics/Scala3CompilerDiagnosticParser.kt b/server/src/main/kotlin/org/jetbrains/bsp/bazel/server/diagnostics/Scala3CompilerDiagnosticParser.kt new file mode 100644 index 000000000..76ad526eb --- /dev/null +++ b/server/src/main/kotlin/org/jetbrains/bsp/bazel/server/diagnostics/Scala3CompilerDiagnosticParser.kt @@ -0,0 +1,55 @@ +package org.jetbrains.bsp.bazel.server.diagnostics + +object Scala3CompilerDiagnosticParser : Parser { + + override fun tryParse(output: Output): List = + listOfNotNull(tryParseOne(output)) + + private val DiagnosticHeader = """ + ^--\ # "-- " diagnostic start + \[E\d+\] # "[E008]" code + ([^:]+): # (1) type of diagnostic + ([^:]+):(\d+):(\d+) # (2) path, (3) line, (4) column + \ \-+$ # " -----------------" ending + """.toRegex(RegexOption.COMMENTS) + + // Scala 3 diagnostics have additional color printed, since Bazel uses renderedMessage field + private val colorRegex = "\u001b\\[1A\u001b\\[K|\u001B\\[[;\\d]*m".toRegex() + + fun tryTake(output: Output, regex: Regex): MatchResult? = + output.peek()?.let { regex.matchEntire(it.replace(colorRegex, "")) }?.also { output.take() } + + fun tryParseOne(output: Output): Diagnostic? { + return tryTake(output, DiagnosticHeader) + ?.let { match -> + val level = if (match.groupValues[1].contains("Error")) Level.Error else Level.Warning + val path = match.groupValues[2].trim() + val line = match.groupValues[3].toInt() + val messageLines = collectMessageLines(match.groupValues[1].trim(), output) + val column = match.groupValues[4].toIntOrNull() ?: tryFindColumnNumber(messageLines) ?: 1 + val message = messageLines.joinToString("\n") + Diagnostic(Position(line, column), message, level, path, output.targetLabel) + } + } + + private fun collectMessageLines(header: String, output: Output): List { + val lines = mutableListOf() + fun String.cleanLine(): String = + this.replace(colorRegex, "").trim() + + // skip lines with numbers which show the source and skip the next ^^^^ line + if (output.peek()?.cleanLine()?.startsWith('|') == false) output.take(2) + while (output.nonEmpty() && output.peek()?.cleanLine()?.startsWith('|') == true) { + lines.add(output.take().cleanLine().removePrefix("|").trim()) + } + lines.add(0, header) + return lines + } + + private val IssuePositionMarker = """^[\s\|]*\^\s*$""".toRegex() // ^ surrounded by whitespace only + + private fun tryFindColumnNumber(messageLines: List): Int? { + val line = messageLines.find { IssuePositionMarker.matches(it.replace(colorRegex, "")) } + return line?.indexOf("^")?.let { it + 1 } + } +} diff --git a/server/src/test/kotlin/org/jetbrains/bsp/bazel/server/diagnostics/DiagnosticsServiceTest.kt b/server/src/test/kotlin/org/jetbrains/bsp/bazel/server/diagnostics/DiagnosticsServiceTest.kt index 9d9c2eec5..d64666073 100644 --- a/server/src/test/kotlin/org/jetbrains/bsp/bazel/server/diagnostics/DiagnosticsServiceTest.kt +++ b/server/src/test/kotlin/org/jetbrains/bsp/bazel/server/diagnostics/DiagnosticsServiceTest.kt @@ -358,6 +358,106 @@ class DiagnosticsServiceTest { ) diagnostics shouldContainExactlyInAnyOrder expected } + @Test + fun `parse scala 3 error`() { + // given + val output = """ + |-- [E007] Type Mismatch Error: Hello.scala:19:20 ------------------------------- + |19 | def hello4: Int = "Hello" + | | ^^^^^^^ + | | Found: ("Hello" : String) + | | Required: Int + | | + | | longer explanation available when compiling with `-explain` + |2 errors found + |Build failed + |java.lang.RuntimeException: Build failed + | at io.bazel.rulesscala.scalac.ScalacInvoker.invokeCompiler(ScalacInvoker3.java:43) + | at io.bazel.rulesscala.scalac.ScalacWorker.compileScalaSources(ScalacWorker.java:253) + | at io.bazel.rulesscala.scalac.ScalacWorker.work(ScalacWorker.java:69) + | at io.bazel.rulesscala.worker.Worker.persistentWorkerMain(Worker.java:86) + | at io.bazel.rulesscala.worker.Worker.workerMain(Worker.java:39) + | at io.bazel.rulesscala.scalac.ScalacWorker.main(ScalacWorker.java:33) + """.trimMargin() + + // when + val diagnostics = extractDiagnostics(output, "//project/src/main/scala/com/example/project:project") + + // then + val expected = listOf( + PublishDiagnosticsParams( + TextDocumentIdentifier("file:///user/workspace/Hello.scala"), + BuildTargetIdentifier("//project/src/main/scala/com/example/project:project"), + ErrorDiagnostic( + Position(19, 20), + """|Type Mismatch Error + |Found: ("Hello" : String) + |Required: Int + | + |longer explanation available when compiling with `-explain`""".trimMargin() + ) + ) + ) + diagnostics shouldContainExactlyInAnyOrder expected + } + + @Test + fun `parse scala 3 multiple errors`() { + // given + val output = """ + |-- [E007] Type Mismatch Error: Hello.scala:18:20 ------------------------------- + |18 | def hello3: Int = "Hello" + | | ^^^^^^^ + | | Found: ("Hello" : String) + | | Required: Int + | | + | | longer explanation available when compiling with `-explain` + |-- [E007] Type Mismatch Error: Hello.scala:19:20 ------------------------------- + |19 | def hello4: Int = "Hello" + | | ^^^^^^^ + | | Found: ("Hello" : String) + | | Required: Int + | | + | | longer explanation available when compiling with `-explain` + |2 errors found + |Build failed + |java.lang.RuntimeException: Build failed + | at io.bazel.rulesscala.scalac.ScalacInvoker.invokeCompiler(ScalacInvoker3.java:43) + | at io.bazel.rulesscala.scalac.ScalacWorker.compileScalaSources(ScalacWorker.java:253) + | at io.bazel.rulesscala.scalac.ScalacWorker.work(ScalacWorker.java:69) + | at io.bazel.rulesscala.worker.Worker.persistentWorkerMain(Worker.java:86) + | at io.bazel.rulesscala.worker.Worker.workerMain(Worker.java:39) + |at io.bazel.rulesscala.scalac.ScalacWorker.main(ScalacWorker.java:33) + """.trimMargin() + + // when + val diagnostics = extractDiagnostics(output, "//project/src/main/scala/com/example/project:project") + + // then + val expected = listOf( + PublishDiagnosticsParams( + TextDocumentIdentifier("file:///user/workspace/Hello.scala"), + BuildTargetIdentifier("//project/src/main/scala/com/example/project:project"), + ErrorDiagnostic( + Position(18, 20), + """|Type Mismatch Error + |Found: ("Hello" : String) + |Required: Int + | + |longer explanation available when compiling with `-explain`""".trimMargin() + ), + ErrorDiagnostic( + Position(19, 20), + """|Type Mismatch Error + |Found: ("Hello" : String) + |Required: Int + | + |longer explanation available when compiling with `-explain`""".trimMargin() + ) + ) + ) + diagnostics shouldContainExactlyInAnyOrder expected + } @Test fun `parse scala warnings`() {