From 2855609eed404da0956ce514901f9a53a5eaad27 Mon Sep 17 00:00:00 2001 From: friendseeker <66892505+Friendseeker@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:26:42 -0800 Subject: [PATCH] Emits SourceInfos when incremental compilation fails Zinc does not provide a way to surface list of all problems present in a codebase. This PR builds SourceInfos in Analysis Callback and in `handleErrors`, the compiler bridge then throws an exception containing the SourceInfos so build tools like sbt can access the informations. Closes #932 --- .../src/main/scala/xsbt/CompilerBridge.scala | 29 +++++++++++++++---- .../main/java/xsbti/AnalysisCallback3.java | 21 ++++++++++++++ .../src/main/java/xsbti/CompileFailed2.java | 22 ++++++++++++++ .../scala/sbt/internal/inc/RawCompiler.scala | 4 ++- .../scala/sbt/internal/inc/Incremental.scala | 21 +++++++++++++- .../src/main/scala/xsbti/TestCallback.scala | 8 +++-- .../src/main/scala/xsbti/TestSourceInfo.scala | 23 +++++++++++++++ .../main/scala/xsbti/TestSourceInfos.scala | 24 +++++++++++++++ .../inc/IncrementalCompilerImpl.scala | 8 +++++ .../sbt/inc/IncrementalCompilerSpec.scala | 21 ++++++++++++++ 10 files changed, 171 insertions(+), 10 deletions(-) create mode 100644 internal/compiler-interface/src/main/java/xsbti/AnalysisCallback3.java create mode 100644 internal/compiler-interface/src/main/java/xsbti/CompileFailed2.java create mode 100644 internal/zinc-testing/src/main/scala/xsbti/TestSourceInfo.scala create mode 100644 internal/zinc-testing/src/main/scala/xsbti/TestSourceInfos.scala diff --git a/internal/compiler-bridge/src/main/scala/xsbt/CompilerBridge.scala b/internal/compiler-bridge/src/main/scala/xsbt/CompilerBridge.scala index b491b0e71..77260e7ac 100644 --- a/internal/compiler-bridge/src/main/scala/xsbt/CompilerBridge.scala +++ b/internal/compiler-bridge/src/main/scala/xsbt/CompilerBridge.scala @@ -18,6 +18,8 @@ import scala.collection.mutable import scala.reflect.io.AbstractFile import scala.tools.nsc.CompilerCommand import Log.debug +import xsbti.compile.analysis.ReadSourceInfos + import java.io.File /** @@ -49,6 +51,16 @@ class InterfaceCompileFailed( override val toString: String ) extends xsbti.CompileFailed +class InterfaceCompileFailed2( + val arguments: Array[String], + val sourceInfos: ReadSourceInfos, + override val toString: String +) extends xsbti.CompileFailed2 { + import scala.collection.JavaConverters._ + val problems: Array[Problem] = + sourceInfos.getAllSourceInfos.values().asScala.flatMap(_.getReportedProblems).toArray +} + class InterfaceCompileCancelled(val arguments: Array[String], override val toString: String) extends xsbti.CompileCancelled @@ -149,8 +161,8 @@ private final class CachedCompiler0( underlyingReporter: DelegatingReporter, compileProgress: CompileProgress ): Unit = { - // cast down to AnalysisCallback2 - val callback2 = callback.asInstanceOf[xsbti.AnalysisCallback2] + // cast down to AnalysisCallback3 + val callback3 = callback.asInstanceOf[xsbti.AnalysisCallback3] compiler.set(callback, underlyingReporter) if (command.shouldStopWithInfo) { @@ -165,7 +177,7 @@ private final class CachedCompiler0( run.compileFiles(sources) processUnreportedWarnings(run) underlyingReporter.problems.foreach(p => - callback2.problem2( + callback3.problem2( p.category, p.position, p.message, @@ -180,8 +192,10 @@ private final class CachedCompiler0( } underlyingReporter.printSummary() - if (!noErrors(underlyingReporter)) - handleErrors(underlyingReporter, log) + if (!noErrors(underlyingReporter)) { + val infos = callback3.getSourceInfos + handleErrors(infos, log) + } // the case where we cancelled compilation _after_ some compilation errors got reported // will be handled by line above so errors still will be reported properly just potentially not @@ -195,6 +209,11 @@ private final class CachedCompiler0( throw new InterfaceCompileFailed(args, dreporter.problems, "Compilation failed") } + def handleErrors(sourceInfos: ReadSourceInfos, log: Logger): Nothing = { + debug(log, "Compilation failed (CompilerInterface)") + throw new InterfaceCompileFailed2(args, sourceInfos, "Compilation failed") + } + def handleCompilationCancellation(dreporter: DelegatingReporter, log: Logger): Nothing = { assert(dreporter.cancelled, "We should get here only if when compilation got cancelled") debug(log, "Compilation cancelled (CompilerInterface)") diff --git a/internal/compiler-interface/src/main/java/xsbti/AnalysisCallback3.java b/internal/compiler-interface/src/main/java/xsbti/AnalysisCallback3.java new file mode 100644 index 000000000..649ad752e --- /dev/null +++ b/internal/compiler-interface/src/main/java/xsbti/AnalysisCallback3.java @@ -0,0 +1,21 @@ +/* + * Zinc - The incremental compiler for Scala. + * Copyright Scala Center, Lightbend, and Mark Harrah + * + * Licensed under Apache License 2.0 + * SPDX-License-Identifier: Apache-2.0 + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package xsbti; +import xsbti.compile.analysis.ReadSourceInfos; + +/** + * Extension to {@link AnalysisCallback2}. + * Similar to {@link AnalysisCallback2}, it serves as compatibility layer for Scala compilers. + */ +public interface AnalysisCallback3 extends AnalysisCallback2 { + ReadSourceInfos getSourceInfos(); +} diff --git a/internal/compiler-interface/src/main/java/xsbti/CompileFailed2.java b/internal/compiler-interface/src/main/java/xsbti/CompileFailed2.java new file mode 100644 index 000000000..e5d4260a3 --- /dev/null +++ b/internal/compiler-interface/src/main/java/xsbti/CompileFailed2.java @@ -0,0 +1,22 @@ +/* + * Zinc - The incremental compiler for Scala. + * Copyright Scala Center, Lightbend, and Mark Harrah + * + * Licensed under Apache License 2.0 + * SPDX-License-Identifier: Apache-2.0 + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package xsbti; + +import xsbti.compile.analysis.ReadSourceInfos; + +public abstract class CompileFailed2 extends CompileFailed { + /** + * Returns SourceInfos containing problems for each file. + * This includes problems found by most recent compilation run. + */ + public abstract ReadSourceInfos sourceInfos(); +} diff --git a/internal/zinc-compile-core/src/main/scala/sbt/internal/inc/RawCompiler.scala b/internal/zinc-compile-core/src/main/scala/sbt/internal/inc/RawCompiler.scala index c90f8eda0..b22165f43 100644 --- a/internal/zinc-compile-core/src/main/scala/sbt/internal/inc/RawCompiler.scala +++ b/internal/zinc-compile-core/src/main/scala/sbt/internal/inc/RawCompiler.scala @@ -15,6 +15,7 @@ package inc import java.nio.file.Path import sbt.internal.util.FeedbackProvidedException +import xsbti.compile.analysis.ReadSourceInfos import xsbti.compile.{ ClasspathOptions, ScalaInstance => XScalaInstance } /** @@ -98,11 +99,12 @@ class CompileFailed( val arguments: Array[String], override val toString: String, val problems: Array[xsbti.Problem], + val sourceInfosOption: Option[ReadSourceInfos], cause: Throwable ) extends xsbti.CompileFailed(cause) with FeedbackProvidedException { def this(arguments: Array[String], toString: String, problems: Array[xsbti.Problem]) = { - this(arguments, toString, problems, null) + this(arguments, toString, problems, None, null) } } diff --git a/internal/zinc-core/src/main/scala/sbt/internal/inc/Incremental.scala b/internal/zinc-core/src/main/scala/sbt/internal/inc/Incremental.scala index 37e93decb..508af7f52 100644 --- a/internal/zinc-core/src/main/scala/sbt/internal/inc/Incremental.scala +++ b/internal/zinc-core/src/main/scala/sbt/internal/inc/Incremental.scala @@ -590,7 +590,7 @@ private final class AnalysisCallback( progress: Option[CompileProgress], incHandlerOpt: Option[Incremental.IncrementalCallback], log: Logger -) extends xsbti.AnalysisCallback2 { +) extends xsbti.AnalysisCallback3 { import Incremental.CompileCycleResult // This must have a unique value per AnalysisCallback @@ -1067,6 +1067,25 @@ private final class AnalysisCallback( } } + def getSourceInfos: SourceInfos = { + // Collect Source Info from current run + val sources = reporteds.keySet ++ unreporteds.keySet ++ mainClasses.keySet + val sourceToInfo = sources.map { source => + val info = SourceInfos.makeInfo( + getOrNil(reporteds.iterator.map { case (k, v) => k -> v.asScala.toSeq }.toMap, source), + getOrNil(unreporteds.iterator.map { case (k, v) => k -> v.asScala.toSeq }.toMap, source), + getOrNil(mainClasses.iterator.map { case (k, v) => k -> v.asScala.toSeq }.toMap, source) + ) + (source, info) + }.toMap + val sourceInfoFromCurrentRun = SourceInfos.of(sourceToInfo) + // Collect reported problems from previous run + incHandlerOpt.map(_.previousAnalysisPruned) match { + case Some(prevAnalysis) => prevAnalysis.infos ++ sourceInfoFromCurrentRun + case None => sourceInfoFromCurrentRun + } + } + override def apiPhaseCompleted(): Unit = { // If we know we're done with cycles (presumably because all sources were invalidated) we can store early analysis // and picke data now. Otherwise, we need to wait for dependency information to decide if there are more cycles. diff --git a/internal/zinc-testing/src/main/scala/xsbti/TestCallback.scala b/internal/zinc-testing/src/main/scala/xsbti/TestCallback.scala index 94161317f..9426b8c6b 100644 --- a/internal/zinc-testing/src/main/scala/xsbti/TestCallback.scala +++ b/internal/zinc-testing/src/main/scala/xsbti/TestCallback.scala @@ -15,12 +15,12 @@ import java.io.File import java.nio.file.Path import java.{ util => ju } import ju.Optional - -import xsbti.api.{ DependencyContext, ClassLike } +import xsbti.api.{ ClassLike, DependencyContext } +import xsbti.compile.analysis.ReadSourceInfos import scala.collection.mutable.ArrayBuffer -class TestCallback extends AnalysisCallback2 { +class TestCallback extends AnalysisCallback3 { case class TestUsedName(name: String, scopes: ju.EnumSet[UseScope]) val classDependencies = new ArrayBuffer[(String, String, DependencyContext)] @@ -153,6 +153,8 @@ class TestCallback extends AnalysisCallback2 { override def isPickleJava: Boolean = false override def getPickleJarPair = Optional.empty() + + override def getSourceInfos: ReadSourceInfos = new TestSourceInfos } object TestCallback { diff --git a/internal/zinc-testing/src/main/scala/xsbti/TestSourceInfo.scala b/internal/zinc-testing/src/main/scala/xsbti/TestSourceInfo.scala new file mode 100644 index 000000000..6ded26142 --- /dev/null +++ b/internal/zinc-testing/src/main/scala/xsbti/TestSourceInfo.scala @@ -0,0 +1,23 @@ +/* + * Zinc - The incremental compiler for Scala. + * Copyright Scala Center, Lightbend, and Mark Harrah + * + * Licensed under Apache License 2.0 + * SPDX-License-Identifier: Apache-2.0 + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package xsbti + +import xsbti.compile.analysis.SourceInfo + +class TestSourceInfo extends SourceInfo { + + override def getReportedProblems: Array[Problem] = Array.empty[Problem] + + override def getUnreportedProblems: Array[Problem] = Array.empty[Problem] + + override def getMainClasses: Array[String] = Array.empty[String] +} diff --git a/internal/zinc-testing/src/main/scala/xsbti/TestSourceInfos.scala b/internal/zinc-testing/src/main/scala/xsbti/TestSourceInfos.scala new file mode 100644 index 000000000..060eec1c7 --- /dev/null +++ b/internal/zinc-testing/src/main/scala/xsbti/TestSourceInfos.scala @@ -0,0 +1,24 @@ +/* + * Zinc - The incremental compiler for Scala. + * Copyright Scala Center, Lightbend, and Mark Harrah + * + * Licensed under Apache License 2.0 + * SPDX-License-Identifier: Apache-2.0 + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package xsbti + +import xsbti.compile.analysis.{ ReadSourceInfos, SourceInfo } + +import java.util + +class TestSourceInfos extends ReadSourceInfos { + + override def get(sourceFile: VirtualFileRef): SourceInfo = new TestSourceInfo + + override def getAllSourceInfos: util.Map[VirtualFileRef, SourceInfo] = + new util.HashMap[VirtualFileRef, SourceInfo]() +} diff --git a/zinc/src/main/scala/sbt/internal/inc/IncrementalCompilerImpl.scala b/zinc/src/main/scala/sbt/internal/inc/IncrementalCompilerImpl.scala index 889400427..b954db27c 100644 --- a/zinc/src/main/scala/sbt/internal/inc/IncrementalCompilerImpl.scala +++ b/zinc/src/main/scala/sbt/internal/inc/IncrementalCompilerImpl.scala @@ -331,11 +331,19 @@ class IncrementalCompilerImpl extends IncrementalCompiler { try { compilerRun } catch { + case e: xsbti.CompileFailed2 => throw new sbt.internal.inc.CompileFailed( + e.arguments, + e.toString, + e.problems, + Some(e.sourceInfos()), + e, + ) // just ignore case e: xsbti.CompileFailed => throw new sbt.internal.inc.CompileFailed( e.arguments, e.toString, e.problems, + None, e ) // just ignore case e: CompileFailed => throw e // just ignore diff --git a/zinc/src/test/scala/sbt/inc/IncrementalCompilerSpec.scala b/zinc/src/test/scala/sbt/inc/IncrementalCompilerSpec.scala index c812e6898..ff1a4c7b3 100644 --- a/zinc/src/test/scala/sbt/inc/IncrementalCompilerSpec.scala +++ b/zinc/src/test/scala/sbt/inc/IncrementalCompilerSpec.scala @@ -240,4 +240,25 @@ class IncrementalCompilerSpec extends BaseCompilerSpec { } } finally comp.close() } + + it should "emit SourceInfos when incremental compilation fails" in withTmpDir { + tmp => + val project = VirtualSubproject(tmp.toPath / "p1") + val comp = project.setup.createCompiler() + val s1 = "object A { final val i = 1" + val f1 = StringVirtualFile("A.scala", s1) + try { + val exception = intercept[CompileFailed] { + comp.compile(f1) + } + exception.sourceInfosOption match { + case Some(sourceInfos) => + assert( + !sourceInfos.getAllSourceInfos.isEmpty, + "Expected non-empty source infos" + ) + case None => fail("Expected sourceInfos") + } + } finally comp.close() + } }