diff options
author | Alexandra Dima <alexandra.dima@jetbrains.com> | 2019-07-10 17:12:12 +0200 |
---|---|---|
committer | Samvel Abrahamyan <samvel1024@gmail.com> | 2019-10-12 14:32:58 +0200 |
commit | 0d53a6e057ae24ecf0bfd5bf0929310723c31282 (patch) | |
tree | 1503d4f04ff8c4bfb54f3ae841293ae20076eb8b /contrib | |
parent | b71748b1b58da3b70ceb9290257ed688b71fbe21 (diff) | |
download | mill-0d53a6e057ae24ecf0bfd5bf0929310723c31282.tar.gz mill-0d53a6e057ae24ecf0bfd5bf0929310723c31282.tar.bz2 mill-0d53a6e057ae24ecf0bfd5bf0929310723c31282.zip |
Implemented support for publishing compilation diagnostics through the custom BspLoggedReporter reporter. Patched the mill.api.Ctx data structure as well as the evaluate() method on mill's Evaluator in order to accept a potential reporter from the outside, or use a default value if none is given.
Diffstat (limited to 'contrib')
4 files changed, 254 insertions, 13 deletions
diff --git a/contrib/bsp/src/mill/contrib/MainMillBuildServer.scala b/contrib/bsp/src/mill/contrib/MainMillBuildServer.scala index a8508ab6..7caaf5d3 100644 --- a/contrib/bsp/src/mill/contrib/MainMillBuildServer.scala +++ b/contrib/bsp/src/mill/contrib/MainMillBuildServer.scala @@ -7,10 +7,10 @@ import java.nio.file.FileAlreadyExistsException import java.util.concurrent.{CancellationException, CompletableFuture, ExecutorService, Executors, Future} import upickle.default._ -import ch.epfl.scala.bsp4j.{BspConnectionDetails, BuildClient, ScalaTestClassesParams} +import ch.epfl.scala.bsp4j.{BspConnectionDetails, BuildClient, DidChangeBuildTarget, LogMessageParams, PublishDiagnosticsParams, ScalaTestClassesParams, ShowMessageParams, TaskFinishParams, TaskProgressParams, TaskStartParams, WorkspaceBuildTargetsResult} import mill._ import mill.api.Strict -import mill.contrib.bsp.{MillBuildServer, ModuleUtils} +import mill.contrib.bsp.{BspLoggedReporter, MillBuildServer, ModuleUtils} import mill.define.{Command, Discover, ExternalModule, Target, Task} import mill.eval.Evaluator import mill.scalalib._ @@ -20,6 +20,7 @@ import requests.Compress.None import upickle.default import scala.collection.JavaConverters._ +import scala.io.Source object MainMillBuildServer extends ExternalModule { @@ -152,11 +153,28 @@ object MainMillBuildServer extends ExternalModule { def experiment(ev: Evaluator): Command[Unit] = T.command { val millServer = new mill.contrib.bsp.MillBuildServer(ev, bspVersion, version, languages) - val mods: Seq[JavaModule] = modules(ev)() - for (module <- mods) { - println(module.millModuleSegments.parts.mkString(".")) + val client = new BuildClient { + var diagnostics = List.empty[PublishDiagnosticsParams] + override def onBuildShowMessage(params: ShowMessageParams): Unit = ??? + override def onBuildLogMessage(params: LogMessageParams): Unit = ??? + override def onBuildTaskStart(params: TaskStartParams): Unit = ??? + override def onBuildTaskProgress(params: TaskProgressParams): Unit = ??? + override def onBuildTaskFinish(params: TaskFinishParams): Unit = ??? + override def onBuildPublishDiagnostics( + params: PublishDiagnosticsParams + ): Unit = { + diagnostics ++= List(params) } - + override def onBuildTargetDidChange(params: DidChangeBuildTarget): Unit = + ??? + } + for (module <- millServer.millModules) { + ev.evaluate(Strict.Agg(module.compile), Option(new BspLoggedReporter(client, + millServer.moduleToTargetId(module), + Option.empty[String], + 10, millServer.getCompilationLogger))) + //println("Diagnostics: " + client.diagnostics) + } } /** diff --git a/contrib/bsp/src/mill/contrib/bsp/BspLoggedReporter.scala b/contrib/bsp/src/mill/contrib/bsp/BspLoggedReporter.scala new file mode 100644 index 00000000..31900211 --- /dev/null +++ b/contrib/bsp/src/mill/contrib/bsp/BspLoggedReporter.scala @@ -0,0 +1,82 @@ +package mill.contrib.bsp + +import java.io.File + +import ch.epfl.scala.{bsp4j => bsp} +import sbt.internal.inc.ManagedLoggedReporter +import sbt.internal.inc.schema.Position +import sbt.internal.util.ManagedLogger +import xsbti.{Problem, Severity} + +import scala.collection.JavaConverters._ +import scala.compat.java8.OptionConverters._ +import scala.io.Source + +class BspLoggedReporter(client: bsp.BuildClient, + targetId: bsp.BuildTargetIdentifier, + compilationOriginId: Option[String], + maxErrors: Int, + logger: ManagedLogger) extends ManagedLoggedReporter(maxErrors, logger) { + + override def logError(problem: Problem): Unit = { + client.onBuildPublishDiagnostics(getDiagnostics(problem, targetId, compilationOriginId)) + println("Sent diagnostics to the client") + super.logError(problem) + println("Logged the error") + } + + override def logInfo(problem: Problem): Unit = { + client.onBuildPublishDiagnostics(getDiagnostics(problem, targetId, compilationOriginId)) + super.logInfo(problem) + } + + override def logWarning(problem: Problem): Unit = { + client.onBuildPublishDiagnostics(getDiagnostics(problem, targetId, compilationOriginId)) + super.logWarning(problem) + } + + def getDiagnostics(problem: Problem, targetId: bsp.BuildTargetIdentifier, originId: Option[String]): + bsp.PublishDiagnosticsParams = { + println("Problem: " + problem) + val sourceFile = problem.position().sourceFile().asScala + val start = new bsp.Position( + problem.position.startLine.asScala.getOrElse(0), + problem.position.startOffset.asScala.getOrElse(0)) + val end = new bsp.Position( + problem.position.endLine.asScala.getOrElse(0), + problem.position.endOffset.asScala.getOrElse(0)) + val diagnostic = new bsp.Diagnostic(new bsp.Range(start, end), problem.message) + diagnostic.setCode(problem.position.lineContent) + diagnostic.setSource("compiler from mill") + diagnostic.setSeverity( problem.severity match { + case Severity.Info => bsp.DiagnosticSeverity.INFORMATION + case Severity.Error => bsp.DiagnosticSeverity.ERROR + case Severity.Warn => bsp.DiagnosticSeverity.WARNING + } + ) + + val params = new bsp.PublishDiagnosticsParams( + new bsp.TextDocumentIdentifier(sourceFile.getOrElse(new File(targetId.getUri)).toPath.toAbsolutePath.toUri.toString), + targetId, List(diagnostic).asJava, true) + + if (originId.nonEmpty) { params.setOriginId(originId.get) } + println("Diagnostics: " + params) + params + } + + private[this] def getErrorCode(file: Option[File], start: bsp.Position, end: bsp.Position, position: xsbti.Position): String = { + file match { + case None => position.lineContent + case f: Option[File] => + val source = Source.fromFile(f.get) + source.close() + val lines = source.getLines.toSeq + val code = lines(start.getLine).substring(start.getCharacter) + + lines.take(start.getLine - 1).takeRight(lines.length - end.getLine - 1).mkString("\n") + + lines(end.getLine).substring(0, end.getCharacter + 1) + code + } + + } + +} diff --git a/contrib/bsp/src/mill/contrib/bsp/MillBuildServer.scala b/contrib/bsp/src/mill/contrib/bsp/MillBuildServer.scala index 91d9c4fd..5d79ee76 100644 --- a/contrib/bsp/src/mill/contrib/bsp/MillBuildServer.scala +++ b/contrib/bsp/src/mill/contrib/bsp/MillBuildServer.scala @@ -1,25 +1,34 @@ package mill.contrib.bsp +import java.io.File + import sbt.testing._ import java.util.{Calendar, Collections} import java.util.concurrent.CompletableFuture +import scala.compat.java8.OptionConverters._ import mill.scalalib.Lib.discoverTests import ch.epfl.scala.bsp4j._ +import ch.epfl.scala.{bsp4j => bsp} import mill.{scalalib, _} import mill.api.{Loose, Result, Strict} import mill.contrib.bsp.ModuleUtils._ import mill.eval.Evaluator import mill.scalalib._ -import mill.scalalib.api.CompilationResult -import mill.scalalib.api.ZincWorkerApi +import mill.scalalib.api.{CompilationResult, ZincWorkerApi} +import sbt.internal.inc._ +import xsbti.{Position, Problem, Severity} +import xsbti.compile.{AnalysisContents, AnalysisStore, FileAnalysisStore} +import xsbti.compile.analysis.SourceInfo import scala.collection.mutable.Map -import mill.api.Result.{Failing, Success} +import mill.api.Result.{Failing, Failure, Success} import scala.collection.JavaConverters._ import mill.modules.Jvm import mill.util.{Ctx, PrintLogger} import mill.define.{Discover, ExternalModule, Target, Task} +import sbt.internal.util.{ConsoleOut, MainAppender, ManagedLogger} +import sbt.util.LogExchange import scala.io.Source @@ -31,7 +40,6 @@ class MillBuildServer(evaluator: Evaluator, implicit def millScoptEvaluatorReads[T] = new mill.main.EvaluatorScopt[T]() lazy val millDiscover: Discover[MillBuildServer.this.type] = Discover[this.type] - val bspVersion: String = _bspVersion val supportedLanguages: List[String] = languages val millServerVersion: String = serverVersion @@ -44,7 +52,6 @@ class MillBuildServer(evaluator: Evaluator, var moduleToTarget: Predef.Map[JavaModule, BuildTarget] = ModuleUtils.millModulesToBspTargets(millModules, evaluator, List("scala", "java")) - var clientInitialized = false val ctx: Ctx.Log with Ctx.Home = new Ctx.Log with Ctx.Home { @@ -217,6 +224,131 @@ class MillBuildServer(evaluator: Evaluator, future } + private[this] def getErrorCode(file: File, start: bsp.Position, end: bsp.Position): String = { + + val source = Source.fromFile(file) + source.close() + val lines = source.getLines.toSeq + val code = lines(start.getLine).substring(start.getCharacter) + + lines.take(start.getLine - 1).takeRight(lines.length - end.getLine - 1).mkString("\n") + + lines(end.getLine).substring(0, end.getCharacter + 1) + code + } + + def getDiagnosticsFromFile(analysisFile: os.Path, targetId: BuildTargetIdentifier, originId: Option[String]): + Seq[PublishDiagnosticsParams] = { + val analysisStore: AnalysisStore = FileAnalysisStore.getDefault(analysisFile.toIO) + analysisStore.get.asScala match { + case contents: AnalysisContents => + val sourceInfoMap: Map[File, SourceInfo] = contents.getAnalysis.readSourceInfos.getAllSourceInfos.asScala + var diagnosticsParams = Seq.empty[PublishDiagnosticsParams] + for ( (file, sourceInfo) <- sourceInfoMap) { + var diagnostics = List.empty[Diagnostic] + for (problem <- sourceInfo.getReportedProblems) { + val start = new bsp.Position( + problem.position.startLine.asScala.getOrElse(0), + problem.position.startOffset.asScala.getOrElse(0)) + val end = new bsp.Position( + problem.position.endLine.asScala.getOrElse(0), + problem.position.endOffset.asScala.getOrElse(0)) + val diagnostic = new Diagnostic(new Range(start, end), problem.message) + diagnostic.setCode(getErrorCode(file, start, end)) + diagnostic.setSource("compiler from mill") + + diagnostic.setSeverity( problem.severity match { + case Severity.Info => DiagnosticSeverity.INFORMATION + case Severity.Error => DiagnosticSeverity.ERROR + case Severity.Warn => DiagnosticSeverity.WARNING + } + ) + diagnostics ++= List(diagnostic) + } + val params = new PublishDiagnosticsParams(new TextDocumentIdentifier(file.toURI.toString), + targetId, diagnostics.asJava, true) + if (originId.nonEmpty) { params.setOriginId(originId.get) } + diagnosticsParams ++= Seq(params) + } + diagnosticsParams + case None => Seq.empty[PublishDiagnosticsParams] + } + } + + def getSourceFileCompileErrors(problems: Seq[Problem]): Map[File, Array[Problem]] = { + val problemsMap = Map.empty[File, Array[Problem]] + + for (problem <- problems) { + try { + val sourceFile = problem.position.sourceFile.get + if (problemsMap.contains(sourceFile)) { + problemsMap(sourceFile) = problemsMap(sourceFile) ++ Array(problem) + } else { + problemsMap(sourceFile) = Array(problem) + } + + } catch { + case e: Exception => + } + } + problemsMap + } + + def getDiagnostics(problems: Array[Problem], targetId: BuildTargetIdentifier, originId: Option[String]): + Seq[PublishDiagnosticsParams] = { + var diagnosticsParams = Seq.empty[PublishDiagnosticsParams] + for (( sourceFile, problems ) <- getSourceFileCompileErrors(problems)) { + var diagnostics = Seq.empty[Diagnostic] + for (problem <- problems) { + val start = new bsp.Position( + problem.position.startLine.asScala.getOrElse(0), + problem.position.startOffset.asScala.getOrElse(0)) + val end = new bsp.Position( + problem.position.endLine.asScala.getOrElse(0), + problem.position.endOffset.asScala.getOrElse(0)) + val diagnostic = new Diagnostic(new Range(start, end), problem.message) + diagnostic.setCode(getErrorCode(sourceFile, start, end)) + diagnostic.setSource("compiler from mill") + diagnostic.setSeverity( problem.severity match { + case Severity.Info => DiagnosticSeverity.INFORMATION + case Severity.Error => DiagnosticSeverity.ERROR + case Severity.Warn => DiagnosticSeverity.WARNING + } + ) + diagnostics ++= List(diagnostic) + } + val params = new PublishDiagnosticsParams(new TextDocumentIdentifier(sourceFile.toPath.toAbsolutePath.toUri.toString), + targetId, diagnostics.asJava, true) + + if (originId.nonEmpty) { params.setOriginId(originId.get) } + diagnosticsParams ++= Seq(params) + } + diagnosticsParams + } + + def getOriginId(params: CompileParams): Option[String] = { + try { + Option(params.getOriginId) + } catch { + case e: Exception => Option.empty[String] + } + } + + def sendCompilationDiagnostics(problems: Array[Problem], targetId: BuildTargetIdentifier, compileParams: CompileParams) = { + for (publishDiagnosticsParams <- + getDiagnostics(problems, targetId, getOriginId(compileParams))) { + client.onBuildPublishDiagnostics(publishDiagnosticsParams) + } + } + + def getCompilationLogger: ManagedLogger = { + val consoleAppender = MainAppender.defaultScreen(ConsoleOut.printStreamOut( + mill.util.DummyLogger.outputStream + )) + val l = LogExchange.logger("Hello") + LogExchange.unbindLoggerAppenders("Hello") + LogExchange.bindLoggerAppenders("Hello", (consoleAppender -> sbt.util.Level.Info) :: Nil) + l + } + //TODO: send task notifications - start, progress and finish //TODO: if the client wants to give compilation arguments and the module // already has some from the build file, what to do? @@ -229,9 +361,10 @@ class MillBuildServer(evaluator: Evaluator, var compileTime = 0 for (targetId <- compileParams.getTargets.asScala) { if (moduleToTarget(targetIdToModule(targetId)).getCapabilities.getCanCompile) { - var millModule = targetIdToModule(targetId) + val millModule = targetIdToModule(targetId) //millModule.javacOptions = compileParams.getArguments.asScala val compileTask = millModule.compile + // send notification to client that compilation of this target started val taskStartParams = new TaskStartParams(new TaskId(compileTask.hashCode().toString)) taskStartParams.setEventTime(System.currentTimeMillis()) @@ -240,8 +373,13 @@ class MillBuildServer(evaluator: Evaluator, taskStartParams.setData(new CompileTask(targetId)) client.onBuildTaskStart(taskStartParams) - val result = millEvaluator.evaluate(Strict.Agg(compileTask)) + val result = millEvaluator.evaluate(Strict.Agg(compileTask), + Option(new BspLoggedReporter(client, + targetId, + getOriginId(compileParams), + 10, getCompilationLogger))) val endTime = System.currentTimeMillis() + compileTime += result.timings.map(timingTuple => timingTuple._2).sum var statusCode = StatusCode.OK diff --git a/contrib/bsp/src/mill/contrib/bsp/ModuleUtils.scala b/contrib/bsp/src/mill/contrib/bsp/ModuleUtils.scala index 266d832a..a3202aab 100644 --- a/contrib/bsp/src/mill/contrib/bsp/ModuleUtils.scala +++ b/contrib/bsp/src/mill/contrib/bsp/ModuleUtils.scala @@ -51,6 +51,9 @@ object ModuleUtils { supportedLanguages.asJava, dependencies, capabilities) + if (module.isInstanceOf[ScalaModule]) { + buildTarget.setDataKind("scala") + } buildTarget.setData(dataBuildTarget) buildTarget.setDisplayName(module.millModuleSegments.last.value.toList.head.pathSegments.head) buildTarget.setBaseDirectory(module.millSourcePath.toNIO.toAbsolutePath.toUri.toString) |