mirror of https://github.com/sbt/sbt.git
[BSP] send diagnostics when evaluating build.sbt
Since build.sbt is compiled/evaluated in `sbt.compiler.Eval`, this commit introduces a `BuildServerEvalReporter` to redirect the compiler errors to the BSP clients. A new `finalReport` method is added in the new `EvalReporter` base class to reset the old diagnostics.
This commit is contained in:
parent
4804cc2fa4
commit
a76e209dde
|
|
@ -12,7 +12,7 @@ import scala.collection.mutable.ListBuffer
|
|||
import scala.tools.nsc.{ ast, io, reporters, CompilerCommand, Global, Phase, Settings }
|
||||
import io.{ AbstractFile, PlainFile, VirtualDirectory }
|
||||
import ast.parser.Tokens
|
||||
import reporters.{ ConsoleReporter, Reporter }
|
||||
import reporters.Reporter
|
||||
import scala.reflect.internal.util.{ AbstractFileClassLoader, BatchSourceFile }
|
||||
import Tokens.{ EOF, NEWLINE, NEWLINES, SEMI }
|
||||
import java.io.{ File, FileNotFoundException }
|
||||
|
|
@ -65,12 +65,12 @@ final class EvalException(msg: String) extends RuntimeException(msg)
|
|||
final class Eval(
|
||||
optionsNoncp: Seq[String],
|
||||
classpath: Seq[File],
|
||||
mkReporter: Settings => Reporter,
|
||||
mkReporter: Settings => EvalReporter,
|
||||
backing: Option[File]
|
||||
) {
|
||||
def this(mkReporter: Settings => Reporter, backing: Option[File]) =
|
||||
def this(mkReporter: Settings => EvalReporter, backing: Option[File]) =
|
||||
this(Nil, IO.classLocationPath[Product].toFile :: Nil, mkReporter, backing)
|
||||
def this() = this(s => new ConsoleReporter(s), None)
|
||||
def this() = this(EvalReporter.console, None)
|
||||
|
||||
backing.foreach(IO.createDirectory)
|
||||
val classpathString = Path.makeString(classpath ++ backing.toList)
|
||||
|
|
@ -114,6 +114,7 @@ final class Eval(
|
|||
line: Int = DefaultStartLine
|
||||
): EvalResult = {
|
||||
val ev = new EvalType[String] {
|
||||
def sourceName: String = srcName
|
||||
def makeUnit = mkUnit(srcName, line, expression)
|
||||
def unlink = true
|
||||
def unitBody(unit: CompilationUnit, importTrees: Seq[Tree], moduleName: String): Tree = {
|
||||
|
|
@ -142,6 +143,7 @@ final class Eval(
|
|||
require(definitions.nonEmpty, "Definitions to evaluate cannot be empty.")
|
||||
val ev = new EvalType[Seq[String]] {
|
||||
lazy val (fullUnit, defUnits) = mkDefsUnit(srcName, definitions)
|
||||
def sourceName: String = srcName
|
||||
def makeUnit = fullUnit
|
||||
def unlink = false
|
||||
def unitBody(unit: CompilationUnit, importTrees: Seq[Tree], moduleName: String): Tree = {
|
||||
|
|
@ -202,28 +204,19 @@ final class Eval(
|
|||
val hash = Hash.toHex(d)
|
||||
val moduleName = makeModuleName(hash)
|
||||
|
||||
lazy val unit = {
|
||||
reporter.reset
|
||||
ev.makeUnit
|
||||
}
|
||||
lazy val run = new Run {
|
||||
override def units = (unit :: Nil).iterator
|
||||
}
|
||||
def unlinkAll(): Unit =
|
||||
for ((sym, _) <- run.symSource) if (ev.unlink) unlink(sym) else toUnlinkLater ::= sym
|
||||
|
||||
val (extra, loader) = backing match {
|
||||
case Some(back) if classExists(back, moduleName) =>
|
||||
val loader = (parent: ClassLoader) =>
|
||||
(new URLClassLoader(Array(back.toURI.toURL), parent): ClassLoader)
|
||||
val extra = ev.read(cacheFile(back, moduleName))
|
||||
(extra, loader)
|
||||
case _ =>
|
||||
try {
|
||||
compileAndLoad(run, unit, imports, backing, moduleName, ev)
|
||||
} finally {
|
||||
unlinkAll()
|
||||
}
|
||||
val (extra, loader) = try {
|
||||
backing match {
|
||||
case Some(back) if classExists(back, moduleName) =>
|
||||
val loader = (parent: ClassLoader) =>
|
||||
(new URLClassLoader(Array(back.toURI.toURL), parent): ClassLoader)
|
||||
val extra = ev.read(cacheFile(back, moduleName))
|
||||
(extra, loader)
|
||||
case _ =>
|
||||
compileAndLoad(imports, backing, moduleName, ev)
|
||||
}
|
||||
} finally {
|
||||
// send a final report even if the class file was backed to reset preceding diagnostics
|
||||
reporter.finalReport(ev.sourceName)
|
||||
}
|
||||
|
||||
val generatedFiles = getGeneratedFiles(backing, moduleName)
|
||||
|
|
@ -232,6 +225,25 @@ final class Eval(
|
|||
// location of the cached type or definition information
|
||||
private[this] def cacheFile(base: File, moduleName: String): File =
|
||||
new File(base, moduleName + ".cache")
|
||||
|
||||
private def compileAndLoad[T](
|
||||
imports: EvalImports,
|
||||
backing: Option[File],
|
||||
moduleName: String,
|
||||
ev: EvalType[T]
|
||||
): (T, ClassLoader => ClassLoader) = {
|
||||
reporter.reset()
|
||||
val unit = ev.makeUnit
|
||||
val run = new Run {
|
||||
override def units = (unit :: Nil).iterator
|
||||
}
|
||||
try {
|
||||
compileAndLoad(run, unit, imports, backing, moduleName, ev)
|
||||
} finally {
|
||||
// unlink all
|
||||
for ((sym, _) <- run.symSource) if (ev.unlink) unlink(sym) else toUnlinkLater ::= sym
|
||||
}
|
||||
}
|
||||
private[this] def compileAndLoad[T](
|
||||
run: Run,
|
||||
unit: CompilationUnit,
|
||||
|
|
@ -457,6 +469,8 @@ final class Eval(
|
|||
/** Serializes the extra information to a cache file, where it can be `read` back if inputs haven't changed.*/
|
||||
def write(value: T, file: File): Unit
|
||||
|
||||
def sourceName: String
|
||||
|
||||
/**
|
||||
* Constructs the full compilation unit for this evaluation.
|
||||
* This is used for error reporting during compilation.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt.compiler
|
||||
|
||||
import scala.reflect.internal.settings.MutableSettings
|
||||
import scala.reflect.internal.util.Position
|
||||
import scala.tools.nsc.Settings
|
||||
import scala.tools.nsc.reporters.{ ConsoleReporter, FilteringReporter }
|
||||
|
||||
abstract class EvalReporter extends FilteringReporter {
|
||||
def finalReport(sourceName: String): Unit
|
||||
}
|
||||
|
||||
object EvalReporter {
|
||||
def console(s: Settings): EvalReporter = new ForwardingReporter(new ConsoleReporter(s))
|
||||
}
|
||||
|
||||
class ForwardingReporter(delegate: FilteringReporter) extends EvalReporter {
|
||||
def settings: Settings = delegate.settings
|
||||
|
||||
def doReport(pos: Position, msg: String, severity: Severity): Unit =
|
||||
delegate.doReport(pos, msg, severity)
|
||||
|
||||
override def filter(pos: Position, msg: String, severity: Severity): Int =
|
||||
delegate.filter(pos, msg, severity)
|
||||
|
||||
override def increment(severity: Severity): Unit = delegate.increment(severity)
|
||||
|
||||
override def errorCount: Int = delegate.errorCount
|
||||
override def warningCount: Int = delegate.warningCount
|
||||
|
||||
override def hasErrors: Boolean = delegate.hasErrors
|
||||
override def hasWarnings: Boolean = delegate.hasWarnings
|
||||
|
||||
override def comment(pos: Position, msg: String): Unit = delegate.comment(pos, msg)
|
||||
|
||||
override def cancelled: Boolean = delegate.cancelled
|
||||
override def cancelled_=(b: Boolean): Unit = delegate.cancelled_=(b)
|
||||
|
||||
override def flush(): Unit = delegate.flush()
|
||||
override def finish(): Unit = delegate.finish()
|
||||
override def reset(): Unit =
|
||||
delegate.reset() // super.reset not necessary, own state is never modified
|
||||
|
||||
override def rerunWithDetails(setting: MutableSettings#Setting, name: String): String =
|
||||
delegate.rerunWithDetails(setting, name)
|
||||
|
||||
override def finalReport(sourceName: String): Unit = ()
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ import sbt.io.IO
|
|||
class EvalTest extends Properties("eval") {
|
||||
private[this] lazy val reporter = new StoreReporter(new Settings())
|
||||
import reporter.ERROR
|
||||
private[this] lazy val eval = new Eval(_ => reporter, None)
|
||||
private[this] lazy val eval = new Eval(_ => new ForwardingReporter(reporter), None)
|
||||
|
||||
property("inferred integer") = forAll { (i: Int) =>
|
||||
val result = eval.eval(i.toString)
|
||||
|
|
@ -46,7 +46,7 @@ class EvalTest extends Properties("eval") {
|
|||
|
||||
property("backed local class") = forAll { (i: Int) =>
|
||||
IO.withTemporaryDirectory { dir =>
|
||||
val eval = new Eval(_ => reporter, backing = Some(dir))
|
||||
val eval = new Eval(_ => new ForwardingReporter(reporter), backing = Some(dir))
|
||||
val result = eval.eval(local(i))
|
||||
val v = value(result).asInstanceOf[{ def i: Int }].i
|
||||
(label("Value", v) |: (v == i)) &&
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import sbt.librarymanagement.{ Resolver, UpdateReport }
|
|||
import sbt.std.Transform.DummyTaskMap
|
||||
import sbt.util.{ Logger, Show }
|
||||
import sbt.BuildSyntax._
|
||||
import sbt.internal.bsp.BuildTargetIdentifier
|
||||
|
||||
import scala.annotation.nowarn
|
||||
import scala.Console.RED
|
||||
|
|
@ -147,14 +148,15 @@ final case class PluginData(
|
|||
unmanagedSourceDirectories: Seq[File],
|
||||
unmanagedSources: Seq[File],
|
||||
managedSourceDirectories: Seq[File],
|
||||
managedSources: Seq[File]
|
||||
managedSources: Seq[File],
|
||||
buildTarget: Option[BuildTargetIdentifier]
|
||||
) {
|
||||
val classpath: Seq[Attributed[File]] = definitionClasspath ++ dependencyClasspath
|
||||
}
|
||||
|
||||
object PluginData {
|
||||
private[sbt] def apply(dependencyClasspath: Def.Classpath): PluginData =
|
||||
PluginData(dependencyClasspath, Nil, None, None, Nil, Nil, Nil, Nil, Nil)
|
||||
PluginData(dependencyClasspath, Nil, None, None, Nil, Nil, Nil, Nil, Nil, None)
|
||||
}
|
||||
|
||||
object EvaluateTask {
|
||||
|
|
|
|||
|
|
@ -8,19 +8,17 @@
|
|||
package sbt
|
||||
package internal
|
||||
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
|
||||
import sbt.BuildPaths._
|
||||
import sbt.Def.{ ScopeLocal, ScopedKey, Setting, isDummy }
|
||||
import sbt.Keys._
|
||||
import sbt.Project.inScope
|
||||
import sbt.Scope.GlobalScope
|
||||
import sbt.SlashSyntax0._
|
||||
import sbt.compiler.Eval
|
||||
import sbt.compiler.{ Eval, EvalReporter }
|
||||
import sbt.internal.BuildStreams._
|
||||
import sbt.internal.inc.classpath.ClasspathUtil
|
||||
import sbt.internal.inc.{ ScalaInstance, ZincLmUtil, ZincUtil }
|
||||
import sbt.internal.server.BuildServerEvalReporter
|
||||
import sbt.internal.util.Attributed.data
|
||||
import sbt.internal.util.Types.const
|
||||
import sbt.internal.util.{ Attributed, Settings, ~> }
|
||||
|
|
@ -31,6 +29,8 @@ import sbt.nio.Settings
|
|||
import sbt.util.{ Logger, Show }
|
||||
import xsbti.compile.{ ClasspathOptionsUtil, Compilers }
|
||||
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import scala.annotation.{ nowarn, tailrec }
|
||||
import scala.collection.mutable
|
||||
import scala.tools.nsc.reporters.ConsoleReporter
|
||||
|
|
@ -426,14 +426,21 @@ private[sbt] object Load {
|
|||
() => eval
|
||||
}
|
||||
|
||||
def mkEval(unit: BuildUnit): Eval =
|
||||
mkEval(unit.definitions, unit.plugins, unit.plugins.pluginData.scalacOptions)
|
||||
|
||||
def mkEval(defs: LoadedDefinitions, plugs: LoadedPlugins, options: Seq[String]): Eval =
|
||||
mkEval(defs.target ++ plugs.classpath, defs.base, options)
|
||||
def mkEval(unit: BuildUnit): Eval = {
|
||||
val defs = unit.definitions
|
||||
mkEval(defs.target ++ unit.plugins.classpath, defs.base, unit.plugins.pluginData.scalacOptions)
|
||||
}
|
||||
|
||||
def mkEval(classpath: Seq[File], base: File, options: Seq[String]): Eval =
|
||||
new Eval(options, classpath, s => new ConsoleReporter(s), Some(evalOutputDirectory(base)))
|
||||
mkEval(classpath, base, options, EvalReporter.console)
|
||||
|
||||
def mkEval(
|
||||
classpath: Seq[File],
|
||||
base: File,
|
||||
options: Seq[String],
|
||||
mkReporter: scala.tools.nsc.Settings => EvalReporter
|
||||
): Eval =
|
||||
new Eval(options, classpath, mkReporter, Some(evalOutputDirectory(base)))
|
||||
|
||||
/**
|
||||
* This will clean up left-over files in the config-classes directory if they are no longer used.
|
||||
|
|
@ -703,7 +710,13 @@ private[sbt] object Load {
|
|||
|
||||
// NOTE - because we create an eval here, we need a clean-eval later for this URI.
|
||||
lazy val eval = timed("Load.loadUnit: mkEval", log) {
|
||||
mkEval(plugs.classpath, defDir, plugs.pluginData.scalacOptions)
|
||||
def mkReporter(settings: scala.tools.nsc.Settings): EvalReporter =
|
||||
plugs.pluginData.buildTarget match {
|
||||
case None => EvalReporter.console(settings)
|
||||
case Some(buildTarget) =>
|
||||
new BuildServerEvalReporter(buildTarget, new ConsoleReporter(settings))
|
||||
}
|
||||
mkEval(plugs.classpath, defDir, plugs.pluginData.scalacOptions, mkReporter)
|
||||
}
|
||||
val initialProjects = defsScala.flatMap(b => projectsFromBuild(b, normBase)) ++ buildLevelExtraProjects
|
||||
|
||||
|
|
@ -1168,6 +1181,7 @@ private[sbt] object Load {
|
|||
val unmanagedSrcs = (Configurations.Compile / unmanagedSources).value
|
||||
val managedSrcDirs = (Configurations.Compile / managedSourceDirectories).value
|
||||
val managedSrcs = (Configurations.Compile / managedSources).value
|
||||
val buildTarget = (Configurations.Compile / bspTargetIdentifier).value
|
||||
PluginData(
|
||||
removeEntries(cp, prod),
|
||||
prod,
|
||||
|
|
@ -1178,6 +1192,7 @@ private[sbt] object Load {
|
|||
unmanagedSrcs,
|
||||
managedSrcDirs,
|
||||
managedSrcs,
|
||||
Some(buildTarget)
|
||||
)
|
||||
},
|
||||
scalacOptions += "-Wconf:cat=unused-nowarn:s",
|
||||
|
|
@ -1233,7 +1248,7 @@ private[sbt] object Load {
|
|||
loadPluginDefinition(
|
||||
dir,
|
||||
config,
|
||||
PluginData(config.globalPluginClasspath, Nil, None, None, Nil, Nil, Nil, Nil, Nil)
|
||||
PluginData(config.globalPluginClasspath, Nil, None, None, Nil, Nil, Nil, Nil, Nil, None)
|
||||
)
|
||||
|
||||
def buildPlugins(dir: File, s: State, config: LoadBuildConfiguration): LoadedPlugins =
|
||||
|
|
@ -1429,7 +1444,8 @@ final case class LoadBuildConfiguration(
|
|||
Nil,
|
||||
Nil,
|
||||
Nil,
|
||||
Nil
|
||||
Nil,
|
||||
None
|
||||
)
|
||||
case None => PluginData(globalPluginClasspath)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt.internal.server
|
||||
|
||||
import sbt.StandardMain.exchange
|
||||
import sbt.compiler.ForwardingReporter
|
||||
import sbt.internal.bsp
|
||||
import sbt.internal.bsp.{
|
||||
BuildTargetIdentifier,
|
||||
Diagnostic,
|
||||
DiagnosticSeverity,
|
||||
PublishDiagnosticsParams,
|
||||
Range,
|
||||
TextDocumentIdentifier
|
||||
}
|
||||
|
||||
import java.nio.file.{ Files, Path, Paths }
|
||||
import scala.collection.mutable
|
||||
import scala.reflect.internal.Reporter
|
||||
import scala.reflect.internal.util.{ DefinedPosition, Position }
|
||||
import scala.tools.nsc.reporters.FilteringReporter
|
||||
import sbt.internal.bsp.codec.JsonProtocol._
|
||||
|
||||
class BuildServerEvalReporter(buildTarget: BuildTargetIdentifier, delegate: FilteringReporter)
|
||||
extends ForwardingReporter(delegate) {
|
||||
private val problemsByFile = mutable.Map[Path, Vector[Diagnostic]]()
|
||||
|
||||
override def doReport(pos: Position, msg: String, severity: Severity): Unit = {
|
||||
for {
|
||||
filePath <- if (pos.source.file.exists) Some(Paths.get(pos.source.file.path)) else None
|
||||
range <- convertToRange(pos)
|
||||
} {
|
||||
val bspSeverity = convertToBsp(severity)
|
||||
val diagnostic = Diagnostic(range, bspSeverity, None, Option("sbt"), msg)
|
||||
problemsByFile(filePath) = problemsByFile.getOrElse(filePath, Vector()) :+ diagnostic
|
||||
val params = PublishDiagnosticsParams(
|
||||
TextDocumentIdentifier(filePath.toUri),
|
||||
buildTarget,
|
||||
originId = None,
|
||||
Vector(diagnostic),
|
||||
reset = false
|
||||
)
|
||||
exchange.notifyEvent("build/publishDiagnostics", params)
|
||||
}
|
||||
super.doReport(pos, msg, severity)
|
||||
}
|
||||
|
||||
override def finalReport(sourceName: String): Unit = {
|
||||
val filePath = Paths.get(sourceName)
|
||||
if (Files.exists(filePath)) {
|
||||
val diagnostics = problemsByFile.getOrElse(filePath, Vector())
|
||||
val params = PublishDiagnosticsParams(
|
||||
textDocument = TextDocumentIdentifier(filePath.toUri),
|
||||
buildTarget,
|
||||
originId = None,
|
||||
diagnostics,
|
||||
reset = true
|
||||
)
|
||||
exchange.notifyEvent("build/publishDiagnostics", params)
|
||||
}
|
||||
}
|
||||
|
||||
private def convertToBsp(severity: Severity): Option[Long] = {
|
||||
val result = severity match {
|
||||
case Reporter.INFO => DiagnosticSeverity.Information
|
||||
case Reporter.WARNING => DiagnosticSeverity.Warning
|
||||
case Reporter.ERROR => DiagnosticSeverity.Error
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
|
||||
private def convertToRange(pos: Position): Option[Range] = {
|
||||
pos match {
|
||||
case _: DefinedPosition =>
|
||||
val startLine = pos.source.offsetToLine(pos.start)
|
||||
val startChar = pos.start - pos.source.lineToOffset(startLine)
|
||||
val endLine = pos.source.offsetToLine(pos.end)
|
||||
val endChar = pos.end - pos.source.lineToOffset(endLine)
|
||||
Some(
|
||||
Range(
|
||||
bsp.Position(startLine.toLong, startChar.toLong),
|
||||
bsp.Position(endLine.toLong, endChar.toLong)
|
||||
)
|
||||
)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -114,7 +114,7 @@ object FakeState {
|
|||
Nil
|
||||
)
|
||||
|
||||
val pluginData = PluginData(Nil, Nil, None, None, Nil, Nil, Nil, Nil, Nil)
|
||||
val pluginData = PluginData(Nil, Nil, None, None, Nil, Nil, Nil, Nil, Nil, None)
|
||||
val builds: DetectedModules[BuildDef] = new DetectedModules[BuildDef](Nil)
|
||||
|
||||
val detectedAutoPlugins: Seq[DetectedAutoPlugin] =
|
||||
|
|
|
|||
Loading…
Reference in New Issue