Skip to content

Commit

Permalink
Add error reports info to metals doctor (#5683)
Browse files Browse the repository at this point in the history
  • Loading branch information
kasiaMarek authored Oct 12, 2023
1 parent ef544a6 commit cc897ef
Show file tree
Hide file tree
Showing 25 changed files with 445 additions and 120 deletions.
24 changes: 13 additions & 11 deletions metals/src/main/scala/scala/meta/internal/metals/Compilers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -946,11 +946,12 @@ class Compilers(
): Option[PresentationCompiler] = {
val pc = JavaPresentationCompiler()
Some(
configure(pc, search).newInstance(
targetUri,
classpath.toAbsoluteClasspath.map(_.toNIO).toSeq.asJava,
log.asJava,
)
configure(pc, search)
.newInstance(
targetUri,
classpath.toAbsoluteClasspath.map(_.toNIO).toSeq.asJava,
log.asJava,
)
)
}

Expand Down Expand Up @@ -1119,7 +1120,7 @@ class Compilers(
classpath,
search,
target.scalac.getTarget.getUri,
)
).withBuildTargetName(target.displayName)
}

def newCompiler(
Expand All @@ -1138,11 +1139,12 @@ class Compilers(
}

val filteredOptions = plugins.filterSupportedOptions(options)
configure(pc, search).newInstance(
name,
classpath.asJava,
(log ++ filteredOptions).asJava,
)
configure(pc, search)
.newInstance(
name,
classpath.asJava,
(log ++ filteredOptions).asJava,
)
}

private def toDebugCompletionType(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import scala.meta.internal.metals.utils.TimestampedFile

object LoggerReporter extends Reporter {

override def name: String = "logger-report"
override def create(report: => Report, ifVerbose: Boolean): Option[Path] = {
scribe.info(s"Report ${report.name}: ${report.fullText}")
scribe.info(
s"Report ${report.name}: ${report.fullText(withIdAndSummary = false)}"
)
None
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -627,13 +627,13 @@ object MetalsEnrichments
try {
Some(toAbsolutePath)
} catch {
case NonFatal(e) =>
case NonFatal(error) =>
reports.incognito.create(
Report(
"absolute-path",
s"""|Uri: $value
|""".stripMargin,
e,
error,
)
)
None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ class MetalsLspService(

implicit val reports: StdReportContext = new StdReportContext(
folder.toNIO,
_.flatMap { uri =>
for {
filePath <- uri.toAbsolutePathSafe
buildTargetId <- buildTargets.inverseSources(filePath)
name <- buildTargets.info(buildTargetId).map(_.getDisplayName())
} yield name
},
ReportLevel.fromString(MetalsServerConfig.default.loglevel),
)

Expand Down
149 changes: 140 additions & 9 deletions metals/src/main/scala/scala/meta/internal/metals/doctor/Doctor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package scala.meta.internal.metals.doctor

import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.text.SimpleDateFormat
import java.util.Date
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean

Expand Down Expand Up @@ -30,9 +33,15 @@ import scala.meta.internal.metals.Messages.CheckDoctor
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.MtagsResolver
import scala.meta.internal.metals.PopupChoiceReset
import scala.meta.internal.metals.Report
import scala.meta.internal.metals.ReportContext
import scala.meta.internal.metals.ReportFileName
import scala.meta.internal.metals.ScalaTarget
import scala.meta.internal.metals.ServerCommands
import scala.meta.internal.metals.StdReportContext
import scala.meta.internal.metals.Tables
import scala.meta.internal.metals.clients.language.MetalsLanguageClient
import scala.meta.internal.metals.utils.TimestampedFile
import scala.meta.io.AbsolutePath

import ch.epfl.scala.bsp4j.BuildTargetIdentifier
Expand All @@ -57,7 +66,7 @@ final class Doctor(
maybeJDKVersion: Option[JdkVersion],
folderName: String,
buildTools: BuildTools,
)(implicit ec: ExecutionContext) {
)(implicit ec: ExecutionContext, rc: ReportContext) {
private val hasProblems = new AtomicBoolean(false)
private val problemResolver =
new ProblemResolver(
Expand Down Expand Up @@ -203,6 +212,7 @@ final class Doctor(
),
None,
List.empty,
getErrorReports(),
)
} else {
val allTargetsInfo = targetIds
Expand All @@ -224,29 +234,60 @@ final class Doctor(
None,
Some(allTargetsInfo),
explanations,
getErrorReports(),
)
}
}

private def getErrorReports(): List[ErrorReportInfo] =
for {
provider <- rc.all
report <- provider.getReports()
} yield {
val (name, buildTarget) =
ReportFileName.getReportNameAndBuildTarget(report)
ErrorReportInfo(
name,
report.timestamp,
report.toPath.toUri().toString(),
buildTarget,
Doctor.getErrorReportSummary(report, workspace).getOrElse(""),
provider.name,
)
}

private def gotoBuildTargetCommand(
workspace: AbsolutePath,
buildTargetName: String,
): String = {
val uriAsStr = FileDecoderProvider
.createBuildTargetURI(workspace, buildTargetName)
.toString
): String =
goToCommand(
FileDecoderProvider
.createBuildTargetURI(workspace, buildTargetName)
.toString
)

private def goToCommand(uri: String): String =
clientConfig
.commandInHtmlFormat()
.map(format => {
val range = new l.Range(
new l.Position(0, 0),
new l.Position(0, 0),
)
val location = ClientCommands.WindowLocation(uriAsStr, range)
val location = ClientCommands.WindowLocation(uri, range)
ClientCommands.GotoLocation.toCommandLink(location, format)
})
.getOrElse(uriAsStr)
}
.getOrElse(uri)

private def zipReports(): Option[String] =
clientConfig
.commandInHtmlFormat()
.map(ServerCommands.ZipReports.toCommandLink(_))

private def createGithubIssue(): Option[String] =
clientConfig
.commandInHtmlFormat()
.map(ServerCommands.OpenIssue.toCommandLink(_))

private def resetChoiceCommand(choice: String): String = {
val param = s"""["$choice"]"""
Expand Down Expand Up @@ -304,6 +345,7 @@ final class Doctor(
}

val targetIds = allTargetIds()
val errorReports = getErrorReports().groupBy(_.buildTarget)
if (targetIds.isEmpty) {
html
.element("p")(
Expand All @@ -330,9 +372,12 @@ final class Doctor(
.element("th")(_.text("Semanticdb"))
.element("th")(_.text("Debugging"))
.element("th")(_.text("Java support"))
.element("th")(_.text("Error reports"))
.element("th")(_.text("Recommendation"))
)
).element("tbody")(html => buildTargetRows(html, allTargetsInfo))
).element("tbody")(html =>
buildTargetRows(html, allTargetsInfo, errorReports)
)
)

// Additional explanations
Expand All @@ -342,17 +387,74 @@ final class Doctor(
DoctorExplanation.SemanticDB.toHtml(html, allTargetsInfo)
DoctorExplanation.Debugging.toHtml(html, allTargetsInfo)
DoctorExplanation.JavaSupport.toHtml(html, allTargetsInfo)

addErrorReportsInfo(html, errorReports)
}
}

private def addErrorReportsInfo(
html: HtmlBuilder,
errorReports: Map[Option[String], List[ErrorReportInfo]],
) = {
html.element("h2")(_.text("Error reports:"))
errorReports.toVector
.sortWith {
case (Some(v1) -> _, Some(v2) -> _) => v1 < v2
case (None -> _, _ -> _) => false
case (_ -> _, None -> _) => true
}
.foreach { case (optBuildTarget, reports) =>
def name(default: String) = optBuildTarget.getOrElse(default)
html.element("details")(details =>
details
.element("summary", s"id=reports-${name("other")}")(
_.element("b")(
_.text(s"${name("Other error reports")} (${reports.length}):")
)
)
.element("table") { table =>
reports.foreach { report =>
val reportName = report.name.replaceAll("[_-]", " ")
val dateTime = dateTimeFormat.format(new Date(report.timestamp))
table.element("tr")(tr =>
tr.element("td")(_.raw(Icons.unicode.folder))
.element("td")(_.link(goToCommand(report.uri), reportName))
.element("td")(_.text(dateTime))
.element("td")(_.text(report.shortSummary))
)
}
}
)
}
for {
zipReportsCommand <- zipReports()
createIssueCommand <- createGithubIssue()
} html.element("p")(
_.text(
"You can attach a single error report or a couple or reports in a zip file "
)
.link(zipReportsCommand, "(create a zip file from anonymized reports)")
.text(" to your GitHub issue ")
.link(createIssueCommand, "(create a github issue)")
.text(" to help with debugging.")
)
}

private def buildTargetRows(
html: HtmlBuilder,
infos: Seq[DoctorTargetInfo],
errorReports: Map[Option[String], List[ErrorReportInfo]],
): Unit = {
infos
.sortBy(f => (f.baseDirectory, f.name, f.dataKind))
.foreach { targetInfo =>
val center = "style='text-align: center'"
def addErrorReportText(html: HtmlBuilder) =
errorReports.getOrElse(Some(targetInfo.name), List.empty) match {
case Nil => html.text(Icons.unicode.check)
case _ =>
html.link(s"#reports-${targetInfo.name}", Icons.unicode.alert)
}
html.element("tr")(
_.element("td")(_.link(targetInfo.gotoCommand, targetInfo.name))
.element("td")(_.text(targetInfo.targetType))
Expand All @@ -370,6 +472,7 @@ final class Doctor(
_.text(targetInfo.debuggingStatus.explanation)
)
.element("td", center)(_.text(targetInfo.javaStatus.explanation))
.element("td", center)(addErrorReportText)
.element("td")(_.raw(targetInfo.recommenedFix))
)
}
Expand Down Expand Up @@ -531,8 +634,36 @@ final class Doctor(
"Try removing the directories .metals/ and .bloop/, then restart metals And import the build again."
private val buildServerNotResponsive =
"Build server is not responding."
private val dateTimeFormat = new SimpleDateFormat("dd MMM HH:mm:ss")
}

case class DoctorVisibilityDidChangeParams(
visible: Boolean
)

object Doctor {

/**
* Extracts the short summary [[scala.meta.internal.metals.Report.shortSummary]]
* from an error report file.
* @param file error report file
* @param root workspace folder root
* @return short summary or nothing if no summary or error while reading the file
*/
def getErrorReportSummary(
file: TimestampedFile,
root: AbsolutePath,
): Option[String] = {
def decode(text: String) =
text.replace(StdReportContext.WORKSPACE_STR, root.toString())
for {
lines <- Try(Files.readAllLines(file.toPath).asScala.toList).toOption
index = lines.lastIndexWhere(_.startsWith(Report.summaryTitle))
if index >= 0
} yield lines
.drop(index + 1)
.dropWhile(_ == "")
.map(decode)
.mkString("\n")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ final case class DoctorResults(

object DoctorResults {
// Version of the Doctor json that is returned.
val version = 4
val version = 5
}

final case class DoctorFolderResults(
Expand All @@ -29,6 +29,7 @@ final case class DoctorFolderResults(
messages: Option[List[DoctorMessage]],
targets: Option[Seq[DoctorTargetInfo]],
explanations: List[Obj],
errorReports: List[ErrorReportInfo],
) {
def toJson: Obj = {
val json = ujson.Obj(
Expand All @@ -40,6 +41,7 @@ final case class DoctorFolderResults(
)
targets.foreach(targetList => json("targets") = targetList.map(_.toJson))
json("explanations") = explanations
json("errorReports") = errorReports.map(_.toJson)
json
}
}
Expand Down Expand Up @@ -152,3 +154,33 @@ final case class DoctorFolderHeader(
base
}
}

/**
* Information about an error report.
* @param name display name of the error
* @param timestamp date and time timestamp of the report
* @param uri error report file uri
* @param buildTarget optional build target that error is associated with
* @param shortSummary short error summary
* @param errorReportType one of "metals", "metals-full", "bloop"
*/
final case class ErrorReportInfo(
name: String,
timestamp: Long,
uri: String,
buildTarget: Option[String],
shortSummary: String,
errorReportType: String,
) {
def toJson: Obj = {
val json = ujson.Obj(
"name" -> name,
"timestamp" -> timestamp,
"uri" -> uri,
"shortSummary" -> shortSummary,
"errorReportType" -> errorReportType,
)
buildTarget.foreach(json("buildTarget") = _)
json
}
}
Loading

0 comments on commit cc897ef

Please sign in to comment.