From 06e53a0ae412859284c695d6c5e2fae278d52aa0 Mon Sep 17 00:00:00 2001 From: eugene yokota Date: Thu, 30 Apr 2026 01:13:13 -0400 Subject: [PATCH] [2.0.x] fix: Fix error with "-language:postfixOps" (#9158) Co-authored-by: kenji yoshida <6b656e6a69@gmail.com> --- .../src/main/scala/sbt/internal/Eval.scala | 2 +- .../sbt/internal/EvaluateConfigurations.scala | 18 ++++++--- .../scala/sbt/internal/parser/SbtParser.scala | 37 ++++++++++++++++--- .../scala/sbt/internal/SbtParserTest.scala | 13 ++++++- .../sbt-test/project-load/postfix/build.sbt | 1 + .../project-load/postfix/project/plugins.sbt | 1 + .../src/sbt-test/project-load/postfix/test | 1 + 7 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 sbt-app/src/sbt-test/project-load/postfix/build.sbt create mode 100644 sbt-app/src/sbt-test/project-load/postfix/project/plugins.sbt create mode 100644 sbt-app/src/sbt-test/project-load/postfix/test diff --git a/buildfile/src/main/scala/sbt/internal/Eval.scala b/buildfile/src/main/scala/sbt/internal/Eval.scala index 4fa853dc8..279245463 100644 --- a/buildfile/src/main/scala/sbt/internal/Eval.scala +++ b/buildfile/src/main/scala/sbt/internal/Eval.scala @@ -29,7 +29,7 @@ import sbt.io.{ Hash, IO } * - mkReporter - an optional factory method to create a reporter */ class Eval( - nonCpOptions: Seq[String], + private[internal] val nonCpOptions: Seq[String], classpath: Seq[Path], backingDir: Option[Path], mkReporter: Option[() => EvalReporter] diff --git a/buildfile/src/main/scala/sbt/internal/EvaluateConfigurations.scala b/buildfile/src/main/scala/sbt/internal/EvaluateConfigurations.scala index 668901dcc..bab0b2e52 100644 --- a/buildfile/src/main/scala/sbt/internal/EvaluateConfigurations.scala +++ b/buildfile/src/main/scala/sbt/internal/EvaluateConfigurations.scala @@ -93,10 +93,11 @@ private[sbt] object EvaluateConfigurations { file: VirtualFileRef, lines: Seq[String], builtinImports: Seq[String], - offset: Int + offset: Int, + options: Seq[String] ): ParsedFile = { def loseTree(l: (String, Tree, LineRange)): (String, LineRange) = (l._1, l._3) - val (importStatements, settingsAndDefinitions) = splitExpressions(file, lines) + val (importStatements, settingsAndDefinitions) = splitExpressions(file, lines, options) val allImports = builtinImports.map(s => (s, -1)) ++ addOffset(offset, importStatements) val (definitions, settings) = splitSettingsDefinitions( addOffsetToRange(offset, settingsAndDefinitions) @@ -147,7 +148,7 @@ private[sbt] object EvaluateConfigurations { val name = file match case file: PathBasedFile => file.toPath.toString case file => file.id - val parsed = parseConfiguration(file, lines, imports, offset) + val parsed = parseConfiguration(file, lines, imports, offset, eval.nonCpOptions) val (importDefs, definitions) = if (parsed.definitions.isEmpty) (Nil, DefinedSbtValues.empty) else { @@ -277,15 +278,22 @@ private[sbt] object EvaluateConfigurations { case _ => Nil } + private[sbt] def splitExpressions( + file: VirtualFileRef, + lines: Seq[String], + ): (Seq[(String, Int)], Seq[(String, Tree, LineRange)]) = + splitExpressions(file, lines, Nil) + /** * Splits a set of lines into (imports, expressions). That is, * anything on the right of the tuple is a scala expression (definition or setting). */ private[sbt] def splitExpressions( file: VirtualFileRef, - lines: Seq[String] + lines: Seq[String], + options: Seq[String] ): (Seq[(String, Int)], Seq[(String, Tree, LineRange)]) = - val split = SbtParser(file, lines) + val split = SbtParser(file, lines, options) // TODO - Look at pulling the parsed expression trees from the SbtParser and stitch them back into a different // scala compiler rather than re-parsing. ( diff --git a/buildfile/src/main/scala/sbt/internal/parser/SbtParser.scala b/buildfile/src/main/scala/sbt/internal/parser/SbtParser.scala index b3e0de9c9..bd7f9fbf3 100644 --- a/buildfile/src/main/scala/sbt/internal/parser/SbtParser.scala +++ b/buildfile/src/main/scala/sbt/internal/parser/SbtParser.scala @@ -27,6 +27,7 @@ import dotty.tools.dotc.reporting.Diagnostic import dotty.tools.dotc.reporting.Reporter import dotty.tools.dotc.reporting.StoreReporter import scala.util.Random +import scala.collection.concurrent.TrieMap import xsbti.VirtualFileRef private[sbt] object SbtParser: @@ -50,6 +51,9 @@ private[sbt] object SbtParser: private final val defaultClasspath = sbt.io.Path.makeString(sbt.io.IO.classLocationPath(classOf[Product]).toFile :: Nil) + def apply(path: VirtualFileRef, lines: Seq[String]): SbtParser = + new SbtParser(path, lines) + def isIdentifier(ident: String): Boolean = val code = s"val $ident = 0; val ${ident}${ident} = $ident" try @@ -135,10 +139,21 @@ private[sbt] object SbtParser: // Retry since Scala 3 compiler initialization can fail due to sys.props change private[sbt] val defaultGlobalForParser: ParseDriver = Retry(ParseDriver()) - private[sbt] final class ParseDriver extends Driver: + + private val parseDriverCache: TrieMap[Seq[String], ParseDriver] = + TrieMap.empty + + private[sbt] def defaultGlobalForParserWithOption(options: Seq[String]): ParseDriver = + parseDriverCache.getOrElseUpdate( + options, + Retry(ParseDriver(options.toList)) + ) + + private[sbt] final class ParseDriver( + options: List[String] = List("-classpath", s"$defaultClasspath") + ) extends Driver: override protected val sourcesRequired: Boolean = false val compileCtx0 = initCtx.fresh - val options = List("-classpath", s"$defaultClasspath") val compileCtx1 = setup(options.toArray, compileCtx0) match case Some((_, ctx)) => ctx case _ => sys.error(s"initialization failed for $options") @@ -219,8 +234,15 @@ end ParsedSbtFileExpressions * @param path The path we're parsing (may be a dummy file) * @param lines The parsed "lines" of the file, where each string is a line. */ -private[sbt] case class SbtParser(path: VirtualFileRef, lines: Seq[String]) - extends ParsedSbtFileExpressions: +private[sbt] case class SbtParser( + path: VirtualFileRef, + lines: Seq[String], + private val options: Seq[String] +) extends ParsedSbtFileExpressions: + + def this(path: VirtualFileRef, lines: Seq[String]) = + this(path, lines, Nil) + // settingsTrees,modifiedContent needed for "session save" // TODO - We should look into splitting out "definitions" vs. "settings" here instead of further string lookups, since we have the // parsed trees. @@ -241,7 +263,12 @@ private[sbt] case class SbtParser(path: VirtualFileRef, lines: Seq[String]) VirtualFile(reporterId, wrapCode.getBytes(StandardCharsets.UTF_8)), scala.io.Codec.UTF8 ) - given Context = SbtParser.defaultGlobalForParser.compileCtx.fresh.setSource(sourceFile) + val ctx = + if options.isEmpty then SbtParser.defaultGlobalForParser + else SbtParser.defaultGlobalForParserWithOption(options) + + given Context = ctx.compileCtx.fresh.setSource(sourceFile) + val parsedTrees = parse(fileName, reporterId) // Check No val (a,b) = foo *or* val a,b = foo as these are problematic to range positions and the WHOLE architecture. diff --git a/buildfile/src/test/scala/sbt/internal/SbtParserTest.scala b/buildfile/src/test/scala/sbt/internal/SbtParserTest.scala index fbb677bc1..f5bc73b4c 100644 --- a/buildfile/src/test/scala/sbt/internal/SbtParserTest.scala +++ b/buildfile/src/test/scala/sbt/internal/SbtParserTest.scala @@ -1,7 +1,7 @@ package sbt.internal import sbt.internal.parser.SbtParser -import sbt.internal.util.LineRange +import sbt.internal.util.{ LineRange, MessageOnlyException } import xsbti.VirtualFileRef object SbtParserTest extends verify.BasicTestSuite: @@ -102,4 +102,15 @@ val x = 1 test("isIdentifier") { assert(SbtParser.isIdentifier("1a") == false) } + + test("postfix") { + val ref = VirtualFileRef.of("vfile") + val src = "Nil head" + intercept[MessageOnlyException] { + SbtParser(ref, Seq(src)) + } + val p = SbtParser(ref, Seq(src), Seq("-language:postfixOps")) + assert(p.imports.isEmpty) + assert(p.lines == Seq(src)) + } end SbtParserTest diff --git a/sbt-app/src/sbt-test/project-load/postfix/build.sbt b/sbt-app/src/sbt-test/project-load/postfix/build.sbt new file mode 100644 index 000000000..27ece75e7 --- /dev/null +++ b/sbt-app/src/sbt-test/project-load/postfix/build.sbt @@ -0,0 +1 @@ +def foo: Int = Seq(2) head diff --git a/sbt-app/src/sbt-test/project-load/postfix/project/plugins.sbt b/sbt-app/src/sbt-test/project-load/postfix/project/plugins.sbt new file mode 100644 index 000000000..54f281320 --- /dev/null +++ b/sbt-app/src/sbt-test/project-load/postfix/project/plugins.sbt @@ -0,0 +1 @@ +scalacOptions += "-language:postfixOps" diff --git a/sbt-app/src/sbt-test/project-load/postfix/test b/sbt-app/src/sbt-test/project-load/postfix/test new file mode 100644 index 000000000..477407e68 --- /dev/null +++ b/sbt-app/src/sbt-test/project-load/postfix/test @@ -0,0 +1 @@ +> name