All Downloads are FREE. Search and download functionalities are using the official Maven repository.

scala.build.ConsoleBloopBuildClient.scala Maven / Gradle / Ivy

package scala.build

import ch.epfl.scala.bsp4j

import java.io.File
import java.net.URI
import java.nio.file.Paths

import scala.build.errors.Severity
import scala.build.internal.WrapperParams
import scala.build.internal.util.ConsoleUtils.ScalaCliConsole
import scala.build.options.Scope
import scala.build.postprocessing.LineConversion.scalaLineToScLine
import scala.collection.mutable
import scala.jdk.CollectionConverters.*

class ConsoleBloopBuildClient(
  logger: Logger,
  keepDiagnostics: Boolean = false,
  generatedSources: mutable.Map[Scope, Seq[GeneratedSource]] = mutable.Map()
) extends BloopBuildClient {
  import ConsoleBloopBuildClient._
  private var projectParams = Seq.empty[String]

  private def projectNameSuffix =
    if (projectParams.isEmpty) ""
    else " (" + projectParams.mkString(", ") + ")"

  private def projectName = "project" + projectNameSuffix

  private var printedStart = false

  private val diagnostics0 = new mutable.ListBuffer[(Either[String, os.Path], bsp4j.Diagnostic)]

  def setGeneratedSources(scope: Scope, newGeneratedSources: Seq[GeneratedSource]) =
    generatedSources(scope) = newGeneratedSources
  def setProjectParams(newParams: Seq[String]): Unit = {
    projectParams = newParams
  }
  def diagnostics: Option[Seq[(Either[String, os.Path], bsp4j.Diagnostic)]] =
    if (keepDiagnostics) Some(diagnostics0.result())
    else None

  private def postProcessDiagnostic(
    path: os.Path,
    diag: bsp4j.Diagnostic,
    diagnosticMappings: Map[os.Path, (Either[String, os.Path], Option[WrapperParams])]
  ): Option[(Either[String, os.Path], bsp4j.Diagnostic)] =
    diagnosticMappings.get(path).map { case (originalPath, wrapperParamsOpt) =>
      (
        originalPath,
        scalaLineToScLine(diag.getRange.getStart.getLine, wrapperParamsOpt),
        scalaLineToScLine(diag.getRange.getStart.getLine, wrapperParamsOpt)
      )
    }.collect { case (originalPath, Some(scLineStart), Some(scLineEnd)) =>
      val start = new bsp4j.Position(scLineStart, diag.getRange.getStart.getCharacter)
      val end   = new bsp4j.Position(scLineEnd, diag.getRange.getEnd.getCharacter)
      val range = new bsp4j.Range(start, end)

      val updatedDiag = new bsp4j.Diagnostic(range, diag.getMessage)
      updatedDiag.setCode(diag.getCode)
      updatedDiag.setRelatedInformation(diag.getRelatedInformation)
      updatedDiag.setSeverity(diag.getSeverity)
      updatedDiag.setSource(diag.getSource)
      updatedDiag.setData(diag.getData)

      (originalPath, updatedDiag)
    }

  override def onBuildPublishDiagnostics(params: bsp4j.PublishDiagnosticsParams): Unit = {
    logger.debug("Received onBuildPublishDiagnostics from bloop: " + params)
    for (diag <- params.getDiagnostics.asScala) {

      val diagnosticMappings = generatedSources.valuesIterator
        .flatMap(_.iterator)
        .map { source =>
          source.generated -> (source.reportingPath, source.wrapperParamsOpt)
        }
        .toMap

      val path = os.Path(Paths.get(new URI(params.getTextDocument.getUri)).toAbsolutePath)
      val (updatedPath, updatedDiag) = postProcessDiagnostic(path, diag, diagnosticMappings)
        .getOrElse((Right(path), diag))
      if (keepDiagnostics)
        diagnostics0 += updatedPath -> updatedDiag
      ConsoleBloopBuildClient.printFileDiagnostic(logger, updatedPath, updatedDiag)
    }
  }

  override def onBuildLogMessage(params: bsp4j.LogMessageParams): Unit = {
    logger.debug("Received onBuildLogMessage from bloop: " + params)
    val prefix = params.getType match {
      case bsp4j.MessageType.ERROR   => "Error: "
      case bsp4j.MessageType.WARNING => "Warning: "
      case bsp4j.MessageType.INFO    => ""
      case bsp4j.MessageType.LOG     => "" // discard those by default?
    }
    logger.message(prefix + params.getMessage)
  }

  override def onBuildShowMessage(params: bsp4j.ShowMessageParams): Unit =
    logger.debug("Received onBuildShowMessage from bloop: " + params)

  override def onBuildTargetDidChange(params: bsp4j.DidChangeBuildTarget): Unit =
    logger.debug("Received onBuildTargetDidChange from bloop: " + params)

  override def onBuildTaskStart(params: bsp4j.TaskStartParams): Unit = {
    logger.debug("Received onBuildTaskStart from bloop: " + params)
    for (msg <- Option(params.getMessage) if !msg.contains(" no-op compilation")) {
      printedStart = true
      val msg0 =
        if (params.getDataKind == "compile-task") s"Compiling $projectName"
        else msg
      logger.message(gray + msg0 + reset)
    }
  }

  override def onBuildTaskProgress(params: bsp4j.TaskProgressParams): Unit =
    logger.debug("Received onBuildTaskProgress from bloop: " + params)

  override def onBuildTaskFinish(params: bsp4j.TaskFinishParams): Unit = {
    logger.debug("Received onBuildTaskFinish from bloop: " + params)
    if (printedStart)
      for (msg <- Option(params.getMessage)) {
        val msg0 =
          if (params.getDataKind == "compile-report")
            params.getStatus match {
              case bsp4j.StatusCode.OK        => s"Compiled $projectName"
              case bsp4j.StatusCode.ERROR     => s"Error compiling $projectName"
              case bsp4j.StatusCode.CANCELLED => s"Compilation cancelled$projectNameSuffix"
            }
          else msg
        logger.message(gray + msg0 + reset)
      }
  }

  def clear(): Unit = {
    generatedSources.clear()
    diagnostics0.clear()
    printedStart = false
  }
}

object ConsoleBloopBuildClient {
  private val gray   = ScalaCliConsole.GRAY
  private val reset  = Console.RESET
  private val red    = Console.RED
  private val yellow = Console.YELLOW

  def diagnosticPrefix(severity: bsp4j.DiagnosticSeverity): String =
    severity match {
      case bsp4j.DiagnosticSeverity.ERROR       => s"[${red}error$reset] "
      case bsp4j.DiagnosticSeverity.WARNING     => s"[${yellow}warn$reset] "
      case bsp4j.DiagnosticSeverity.INFORMATION => "[info] "
      case bsp4j.DiagnosticSeverity.HINT        => s"[${yellow}hint$reset] "
    }

  def diagnosticPrefix(severity: Severity): String = diagnosticPrefix(severity.toBsp4j)

  def printFileDiagnostic(
    logger: Logger,
    path: Either[String, os.Path],
    diag: bsp4j.Diagnostic
  ): Unit = {
    val isWarningOrErrorOrHint = diag.getSeverity == bsp4j.DiagnosticSeverity.ERROR ||
      diag.getSeverity == bsp4j.DiagnosticSeverity.WARNING ||
      diag.getSeverity == bsp4j.DiagnosticSeverity.HINT
    if (isWarningOrErrorOrHint) {
      val prefix = diagnosticPrefix(diag.getSeverity)

      val line  = (diag.getRange.getStart.getLine + 1).toString + ":"
      val col   = (diag.getRange.getStart.getCharacter + 1).toString
      val msgIt = diag.getMessage.linesIterator

      val path0 = path match {
        case Left(source) => source
        case Right(p) if p.startsWith(Os.pwd) =>
          "." + File.separator + p.relativeTo(Os.pwd).toString
        case Right(p) => p.toString
      }
      logger.error(s"$prefix$path0:$line$col")
      for (line <- msgIt)
        logger.error(prefix + line)
      val codeOpt = {
        val lineOpt =
          if (diag.getRange.getStart.getLine == diag.getRange.getEnd.getLine)
            Option(diag.getRange.getStart.getLine)
          else None
        for {
          line <- lineOpt
          p    <- path.toOption
          lines = os.read.lines(p)
          line <- if (line < lines.length) Some(lines(line)) else None
        } yield line
      }
      for (code <- codeOpt)
        code.linesIterator.map(prefix + _).foreach(logger.error(_))
      val canPrintUnderline = diag.getRange.getStart.getLine == diag.getRange.getEnd.getLine &&
        diag.getRange.getStart.getCharacter != null &&
        diag.getRange.getEnd.getCharacter != null &&
        codeOpt.nonEmpty
      if (canPrintUnderline) {
        val len =
          math.max(1, diag.getRange.getEnd.getCharacter - diag.getRange.getStart.getCharacter)
        logger.error(
          prefix + " " * diag.getRange.getStart.getCharacter + "^" * len
        )
      }
    }
  }

  def printOtherDiagnostic(
    logger: Logger,
    message: String,
    severity: Severity,
    positions: Seq[Position]
  ): Unit = {
    val isWarningOrError = true
    if (isWarningOrError) {
      val msgIt  = message.linesIterator
      val prefix = diagnosticPrefix(severity)
      logger.message(prefix + (if (msgIt.hasNext) " " + msgIt.next() else ""))
      msgIt.foreach(line => logger.message(prefix + line))

      positions.foreach {
        case Position.Bloop(bloopJavaPath) =>
          val bloopOutputPrefix = s"[current bloop jvm] "
          logger.message(prefix + bloopOutputPrefix + bloopJavaPath)
          logger.message(prefix + " " * bloopOutputPrefix.length + "^" * bloopJavaPath.length())
        case pos => logger.message(prefix + pos.render())
      }
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy