Skip to content

Commit d3475de

Browse files
committed
improvement: Show crashes more visibly for the user
Previously, we would only show it in the build server status, which might not always be visible to the user. Now, we show a new diagnotic pointing to the actual error report. It will get removed when the next compilation runs.
1 parent 075b1e0 commit d3475de

File tree

5 files changed

+106
-3
lines changed

5 files changed

+106
-3
lines changed

metals/src/main/scala/scala/meta/internal/builds/BSPErrorHandler.scala

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,30 @@ import java.util.Optional
55

66
import scala.meta.internal.bsp.BspSession
77
import scala.meta.internal.bsp.ConnectionBspStatus
8+
import scala.meta.internal.metals.Diagnostics
89
import scala.meta.internal.metals.MetalsEnrichments._
910
import scala.meta.internal.metals.Report
1011
import scala.meta.internal.metals.ReportContext
1112
import scala.meta.internal.metals.Tables
13+
import scala.meta.io.AbsolutePath
1214

1315
import com.google.common.io.BaseEncoding
1416

1517
class BspErrorHandler(
1618
currentSession: () => Option[BspSession],
1719
tables: Tables,
1820
bspStatus: ConnectionBspStatus,
21+
diagnostics: Diagnostics,
1922
)(implicit reportContext: ReportContext) {
2023
def onError(message: String): Unit = {
2124
if (shouldShowBspError) {
2225
for {
2326
report <- createReport(message).asScala
2427
if !tables.dismissedNotifications.BspErrors.isDismissed
25-
} bspStatus.showError(message, report)
28+
} {
29+
bspStatus.showError(message, report)
30+
diagnostics.onBuildTargetCompilationCrash(AbsolutePath(report), message)
31+
}
2632
} else logError(message)
2733
}
2834

metals/src/main/scala/scala/meta/internal/metals/Diagnostics.scala

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import org.eclipse.{lsp4j => l}
2525
private final case class CompilationStatus(
2626
code: bsp4j.StatusCode,
2727
errors: Int,
28+
originId: String,
2829
)
2930

3031
case class DiagnosticWithOrigin(diagnostic: Diagnostic, originId: String)
@@ -67,6 +68,14 @@ final class Diagnostics(
6768
private val compilationStatus =
6869
TrieMap.empty[BuildTargetIdentifier, CompilationStatus]
6970

71+
/*
72+
* A map of build crashes diagnostics.
73+
* The key is the path of the markdown file that contains the build crash.
74+
* The diagnostics will be removed at the end of the next compilation.
75+
*/
76+
private val buildErrorDiagnostics =
77+
TrieMap.empty[AbsolutePath, Diagnostic]
78+
7079
def forFile(path: AbsolutePath): Seq[Diagnostic] = {
7180
diagnostics
7281
.getOrElse(path, new ConcurrentLinkedQueue[DiagnosticWithOrigin]())
@@ -83,12 +92,14 @@ final class Diagnostics(
8392
def reset(): Unit = {
8493
val keys = diagnostics.keys
8594
diagnostics.clear()
95+
buildErrorDiagnostics.clear()
8696
keys.foreach { key => publishDiagnostics(key) }
8797
}
8898

8999
def reset(paths: Seq[AbsolutePath]): Unit =
90100
for (path <- paths if diagnostics.contains(path)) {
91101
diagnostics.remove(path)
102+
buildErrorDiagnostics.remove(path)
92103
publishDiagnostics(path)
93104
}
94105

@@ -117,6 +128,8 @@ final class Diagnostics(
117128
downstreamTargets.remove(target)
118129
}
119130

131+
removeStaleBuildErrorDiagnostics()
132+
120133
// Bazel doesn't clean diagnostics for paths with no errors, so instead we remove everything
121134
// from previous compilations.
122135
val isBazel = buildTargets.buildServerOf(target).exists(_.isBazel)
@@ -140,7 +153,7 @@ final class Diagnostics(
140153
publishDiagnosticsBuffer()
141154

142155
compileTimer.remove(target)
143-
val status = CompilationStatus(statusCode, report.getErrors())
156+
val status = CompilationStatus(statusCode, report.getErrors(), originId)
144157
compilationStatus.update(target, status)
145158
}
146159

@@ -440,4 +453,34 @@ final class Diagnostics(
440453

441454
private def shouldAdjustWithinToken(diagnostic: l.Diagnostic): Boolean =
442455
diagnostic.getSource() == "scala-cli"
456+
457+
def onBuildTargetCompilationCrash(
458+
reportPath: AbsolutePath,
459+
message: String,
460+
): Unit = {
461+
val diagnostic = new l.Diagnostic(
462+
new l.Range(new l.Position(0, 0), new l.Position(0, 0)),
463+
message,
464+
l.DiagnosticSeverity.Error,
465+
"build-server",
466+
)
467+
buildErrorDiagnostics(reportPath) = diagnostic
468+
469+
languageClient.publishDiagnostics(
470+
new PublishDiagnosticsParams(
471+
reportPath.toURI.toString(),
472+
List(diagnostic).asJava,
473+
)
474+
)
475+
}
476+
477+
private def removeStaleBuildErrorDiagnostics(): Unit = {
478+
val all = buildErrorDiagnostics.keySet
479+
all.foreach { path =>
480+
languageClient.publishDiagnostics(
481+
new PublishDiagnosticsParams(path.toURI.toString(), List().asJava)
482+
)
483+
buildErrorDiagnostics.remove(path)
484+
}
485+
}
443486
}

metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ abstract class MetalsLspService(
286286
() => bspSession,
287287
tables,
288288
connectionBspStatus,
289+
diagnostics,
289290
)
290291

291292
val workspaceSymbols: WorkspaceSymbolProvider =

tests/unit/src/main/scala/tests/TestingClient.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,9 @@ class TestingClient(workspace: AbsolutePath, val buffers: Buffers)
300300
}
301301
def workspaceDiagnostics: String = {
302302
val paths = diagnostics.keys.toList
303-
.filter(f => f.isScalaOrJava || f.extension == "conf")
303+
.filter(f =>
304+
f.isScalaOrJava || f.extension == "conf" || f.extension == "md"
305+
)
304306
.sortBy(_.toURI.toString)
305307
paths.map(pathDiagnostics).mkString
306308
}

tests/unit/src/test/scala/tests/DiagnosticsLspSuite.scala

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,57 @@ class DiagnosticsLspSuite extends BaseLspSuite("diagnostics") {
555555
|""".stripMargin
556556
}
557557

558+
test("build-error-diagnostics") {
559+
cleanWorkspace()
560+
for {
561+
_ <- initialize(
562+
"""|/metals.json
563+
|{
564+
| "a": {},
565+
| "b": {
566+
| "dependsOn": ["a"]
567+
| }
568+
|}
569+
|/a/src/main/scala/a/A.scala
570+
|package foo
571+
|
572+
|import scala.language.experimental.macros
573+
|import scala.reflect.macros.blackbox
574+
|
575+
|object FooMacro {
576+
| def crashNow: Int = macro crashNowImpl
577+
|
578+
| def crashNowImpl(c: blackbox.Context): c.Expr[Int] = {
579+
| import c.universe._
580+
| val badSymbol = c.internal.newTermSymbol(NoSymbol, TermName("badSymbol"))
581+
| val badTree = Ident(badSymbol)
582+
| c.Expr[Int](badTree)
583+
| }
584+
|}
585+
|
586+
|/b/src/main/scala/b/B.scala
587+
|package example
588+
|import foo.FooMacro
589+
|
590+
|object Bar extends App {
591+
| FooMacro.crashNow
592+
|}
593+
|""".stripMargin
594+
)
595+
_ <- server.didOpen("b/src/main/scala/b/B.scala")
596+
_ <- server.didSave("b/src/main/scala/b/B.scala")
597+
_ = assertContains(
598+
client.workspaceDiagnostics,
599+
"error: Unexpected error when compiling b: java.lang.AssertionError: assertion failed:",
600+
)
601+
_ <- server.didChange("b/src/main/scala/b/B.scala") {
602+
_.replace("FooMacro.crashNow", "// FooMacro.crashNow")
603+
}
604+
_ <- server.didSave("b/src/main/scala/b/B.scala")
605+
_ = assertNoDiagnostics()
606+
} yield ()
607+
}
608+
558609
class Basic(name: String) {
559610
val path: String = s"$name/src/main/scala/$name/${name.toUpperCase()}.scala"
560611
def content(tpe: String, value: String): String =

0 commit comments

Comments
 (0)