diff --git a/scex-core/src/main/scala/com/avsystem/scex/compiler/presentation/IGlobal.scala b/scex-core/src/main/scala/com/avsystem/scex/compiler/presentation/IGlobal.scala index 3d34ee6..0b0b29a 100644 --- a/scex-core/src/main/scala/com/avsystem/scex/compiler/presentation/IGlobal.scala +++ b/scex-core/src/main/scala/com/avsystem/scex/compiler/presentation/IGlobal.scala @@ -18,6 +18,14 @@ class IGlobal(settings: Settings, reporter: Reporter, val classLoader: ClassLoad import definitions._ + // Don't poll for work while parsing. Background compile resets a stale unit (clearing its synthetics) and only then + // re-parses and re-enters it, so work run from the parser (e.g. our askForResponse calls) could force a lazy accessor + // symbol left over from an aborted pass and crash with "no synthetic tree found for bean accessor". + // This applies to every parse on the compiler thread and defers everything pollForWork does: interrupts, queued work + // items, cancellation checks, shutdown and fresh-run requests, and yielding. All of it resumes at the next typer + // check point. Scex sources are small, so the delay is negligible. Guarded by ParsePollingTest. + override def signalParseProgress(pos: Position): Unit = () + abstract class ScexMember extends Member { def prefix: Type def ownerTpe: Type = prefix diff --git a/scex-core/src/main/scala/com/avsystem/scex/compiler/presentation/ScexPresentationCompiler.scala b/scex-core/src/main/scala/com/avsystem/scex/compiler/presentation/ScexPresentationCompiler.scala index 1d88af3..d281986 100644 --- a/scex-core/src/main/scala/com/avsystem/scex/compiler/presentation/ScexPresentationCompiler.scala +++ b/scex-core/src/main/scala/com/avsystem/scex/compiler/presentation/ScexPresentationCompiler.scala @@ -27,6 +27,9 @@ trait ScexPresentationCompiler extends ScexCompiler { compiler => protected def isEnabled: Boolean = !settings.noPresentation.value protected def presentationCompilerSettings: Settings = settings + protected def createIGlobal(settings: Settings, reporter: Reporter, classLoader: ClassLoader): IGlobal = + new IGlobal(settings, reporter, classLoader) + private var reporter: Reporter = _ private var global: IGlobal = _ @@ -35,7 +38,7 @@ trait ScexPresentationCompiler extends ScexCompiler { compiler => if (isEnabled) { logger.info("Initializing Scala presentation compiler") reporter = new Reporter(presentationCompilerSettings) - global = new IGlobal(presentationCompilerSettings, reporter, getSharedClassLoader) + global = createIGlobal(presentationCompilerSettings, reporter, getSharedClassLoader) } } diff --git a/scex-core/src/test/scala/com/avsystem/scex/compiler/presentation/ParsePollingTest.scala b/scex-core/src/test/scala/com/avsystem/scex/compiler/presentation/ParsePollingTest.scala new file mode 100644 index 0000000..d66129b --- /dev/null +++ b/scex-core/src/test/scala/com/avsystem/scex/compiler/presentation/ParsePollingTest.scala @@ -0,0 +1,65 @@ +package com.avsystem.scex.compiler.presentation + +import com.avsystem.scex.compiler.presentation.ScopeAndTypeCompletionTest.Root +import com.avsystem.scex.compiler.{CompilationTest, ScexSettings} +import com.avsystem.scex.japi.{DefaultJavaScexCompiler, JavaScexCompiler} +import com.avsystem.scex.util.SimpleContext +import org.scalatest.funsuite.AnyFunSuite + +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicInteger +import scala.jdk.CollectionConverters._ +import scala.tools.nsc.Settings + +/** Background compile resets a stale unit (clearing its synthetics) before re-parsing and re-entering it. If the + * presentation compiler polled for work in between, a scex request could force an accessor symbol whose synthetic tree + * is already gone ("no synthetic tree found for bean accessor"). This test fails deterministically if any polling + * happens while a unit is being parsed and entered, regardless of whether the race actually triggers. + */ +class ParsePollingTest extends AnyFunSuite with CompilationTest { + + private val pollCount = new AtomicInteger + private val pollsDuringParseAndEnter = new ConcurrentLinkedQueue[String] + + override protected def createCompiler: JavaScexCompiler = { + val settings = new ScexSettings + settings.classfileDirectory.value = "testClassfileCache" + new DefaultJavaScexCompiler(settings) { + override protected def createIGlobal(settings: Settings, reporter: Reporter, classLoader: ClassLoader) = + new IGlobal(settings, reporter, classLoader) { + override protected def checkForMoreWork(pos: Position): Unit = { + pollCount.incrementAndGet() + // Relies on scalac's `scala.tools.nsc.interactive.Global.parseAndEnter` showing up as a stack frame. If a + // scalac upgrade renames or inlines it, this never matches and the test passes vacuously (pollCount only + // proves this IGlobal is in use), so re-check the frame name after upgrading Scala. + val inParseAndEnter = StackWalker.getInstance.walk { frames => + frames.anyMatch(f => + f.getClassName == "scala.tools.nsc.interactive.Global" && f.getMethodName == "parseAndEnter" + ) + } + if (inParseAndEnter) { + pollsDuringParseAndEnter.add(Thread.currentThread.getStackTrace.mkString("\n ")) + } + super.checkForMoreWork(pos) + } + } + } + } + + val utils = + """ + |def utilMethod: Int = 42 + |val utilStuff = 5 + """.stripMargin + + val profile = createProfile(utils = utils) + + test("presentation compiler does not poll for work while parsing and entering a unit") { + val completer = compiler.getCompleter[SimpleContext[Root], Any](profile, template = false) + completer.getScopeCompletion + completer.getTypeCompletion("_root.", 5) + + assert(pollCount.get > 0, "custom IGlobal was not used") + assert(pollsDuringParseAndEnter.isEmpty, pollsDuringParseAndEnter.asScala.headOption.getOrElse("")) + } +}