[2.x] fix: Backtick-quote project IDs in classloader error messages (#8830)

When a test fails with ClassNotFoundException/IllegalAccessError, sbt
suggests a set command to change classLoaderLayeringStrategy. If the
project name contains hyphens (e.g. bug-report), the suggested command
was syntactically invalid because it parses as subtraction in Scala.

Quote project IDs using Util.quoteIfNotScalaId so the suggested
commands are valid when copy-pasted.

Fixes #5803
This commit is contained in:
Dream 2026-02-28 01:52:55 -05:00 committed by GitHub
parent 33d86d0cd2
commit c3e72f79c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 73 additions and 2 deletions

View File

@ -311,6 +311,7 @@ lazy val utilCore = project
utilCommonSettings,
name := "Util Core",
Utils.keywordsSettings,
libraryDependencies ++= Seq(hedgehog % Test),
mimaSettings,
mimaBinaryIssueFilters ++= Seq(
),

View File

@ -45,6 +45,14 @@ object Util:
def quoteIfKeyword(s: String): String = if (ScalaKeywords.values(s)) s"`${s}`" else s
def quoteIfNotScalaId(s: String): String =
if isValidScalaId(s) && !ScalaKeywords.values(s) then s
else s"`$s`"
private def isValidScalaId(s: String): Boolean =
s.nonEmpty && (s.charAt(0).isLetter || s.charAt(0) == '_') &&
s.forall(c => c.isLetterOrDigit || c == '_')
def ignoreResult[A](f: => A): Unit = {
val _ = f
()

View File

@ -0,0 +1,49 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.util
import hedgehog.*
import hedgehog.runner.*
object UtilSpec extends Properties:
override def tests: List[Test] = List(
example(
"quoteIfNotScalaId should not quote simple identifiers",
assertQuote("foo", "foo"),
),
example(
"quoteIfNotScalaId should not quote identifiers with underscores",
assertQuote("foo_bar", "foo_bar"),
),
example(
"quoteIfNotScalaId should quote identifiers with hyphens",
assertQuote("bug-report", "`bug-report`"),
),
example(
"quoteIfNotScalaId should quote identifiers with dots",
assertQuote("my.project", "`my.project`"),
),
example(
"quoteIfNotScalaId should quote identifiers starting with digits",
assertQuote("123abc", "`123abc`"),
),
example(
"quoteIfNotScalaId should quote Scala keywords",
assertQuote("class", "`class`"),
),
example(
"quoteIfNotScalaId should quote empty strings",
assertQuote("", "``"),
),
)
private def assertQuote(input: String, expected: String): Result =
val actual = Util.quoteIfNotScalaId(input)
Result.assert(actual == expected).log(s"input=$input expected=$expected actual=$actual")
end UtilSpec

View File

@ -1201,7 +1201,20 @@ object Defaults extends BuildCommon {
thisProject,
fileConverter,
).flatMapN { (s, lt, tl, gp, ex, cp, fp, jo, clls, thisProj, c) =>
allTestGroupsTask(s, lt, tl, gp, ex, cp, fp, fpm, jo, clls, s"${thisProj.id} / ", c)
allTestGroupsTask(
s,
lt,
tl,
gp,
ex,
cp,
fp,
fpm,
jo,
clls,
s"${Util.quoteIfNotScalaId(thisProj.id)} / ",
c
)
}
}.value),
// ((streams in test, loadedTestFrameworks, testLoader, testGrouping in test, testExecution in test, fullClasspath in test, javaHome in test, testForkedParallel, javaOptions in test) flatMap allTestGroupsTask).value,
@ -1377,7 +1390,7 @@ object Defaults extends BuildCommon {
testForkedParallelism.value,
javaOptions.value,
classLoaderLayeringStrategy.value,
projectId = s"${thisProject.value.id} / ",
projectId = s"${Util.quoteIfNotScalaId(thisProject.value.id)} / ",
converter = fileConverter.value,
)
val taskName = display.show(resolvedScoped.value)