Skip to content

Commit

Permalink
Emits SourceInfos when incremental compilation fails
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Friendseeker authored and Friendseeker committed Sep 27, 2024
1 parent 73fd63c commit 2855609
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 10 deletions.
29 changes: 24 additions & 5 deletions internal/compiler-bridge/src/main/scala/xsbt/CompilerBridge.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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)")
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

/**
Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 5 additions & 3 deletions internal/zinc-testing/src/main/scala/xsbti/TestCallback.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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 {
Expand Down
23 changes: 23 additions & 0 deletions internal/zinc-testing/src/main/scala/xsbti/TestSourceInfo.scala
Original file line number Diff line number Diff line change
@@ -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]
}
24 changes: 24 additions & 0 deletions internal/zinc-testing/src/main/scala/xsbti/TestSourceInfos.scala
Original file line number Diff line number Diff line change
@@ -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]()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions zinc/src/test/scala/sbt/inc/IncrementalCompilerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}

0 comments on commit 2855609

Please sign in to comment.