Skip to content

Commit

Permalink
Merge pull request #10709 from som-snytt/issue/12961-source-compat
Browse files Browse the repository at this point in the history
Introduce -Xsource-features [ci: last-only]
  • Loading branch information
lrytz committed Mar 14, 2024
2 parents 05533ed + 68bab66 commit 6b1b9d7
Show file tree
Hide file tree
Showing 88 changed files with 474 additions and 353 deletions.
22 changes: 16 additions & 6 deletions src/compiler/scala/tools/nsc/Global.scala
Expand Up @@ -1180,12 +1180,22 @@ class Global(var currentSettings: Settings, reporter0: Reporter)
val profiler: Profiler = Profiler(settings)
keepPhaseStack = settings.log.isSetByUser

// We hit these checks regularly. They shouldn't change inside the same run, so cache the comparisons here.
@nowarn("cat=deprecation")
val isScala3: Boolean = settings.isScala3.value // reporting.isScala3
@nowarn("cat=deprecation")
val isScala3Cross: Boolean = settings.isScala3Cross.value // reporting.isScala3Cross
val isScala3ImplicitResolution: Boolean = settings.Yscala3ImplicitResolution.value
val isScala3: Boolean = settings.isScala3: @nowarn

object sourceFeatures {
private val s = settings
private val o = s.sourceFeatures
import s.XsourceFeatures.contains
def caseApplyCopyAccess = isScala3 && contains(o.caseApplyCopyAccess)
def caseCompanionFunction = isScala3 && contains(o.caseCompanionFunction)
def inferOverride = isScala3 && contains(o.inferOverride)
def any2StringAdd = isScala3 && contains(o.any2StringAdd)
def unicodeEscapesRaw = isScala3 && contains(o.unicodeEscapesRaw)
def stringContextScope = isScala3 && contains(o.stringContextScope)
def leadingInfix = isScala3 && contains(o.leadingInfix)
def packagePrefixImplicits = isScala3 && contains(o.packagePrefixImplicits)
def implicitResolution = isScala3 && contains(o.implicitResolution) || settings.Yscala3ImplicitResolution.value
}

// used in sbt
def uncheckedWarnings: List[(Position, String)] = reporting.uncheckedWarnings
Expand Down
17 changes: 5 additions & 12 deletions src/compiler/scala/tools/nsc/Reporting.scala
Expand Up @@ -25,7 +25,6 @@ import scala.reflect.internal.util.StringOps.countElementsAsString
import scala.reflect.internal.util.{CodeAction, NoSourceFile, Position, ReplBatchSourceFile, SourceFile, TextEdit}
import scala.tools.nsc.Reporting.Version.{NonParseableVersion, ParseableVersion}
import scala.tools.nsc.Reporting._
import scala.tools.nsc.settings.NoScalaVersion
import scala.util.matching.Regex

/** Provides delegates to the reporter doing the actual work.
Expand All @@ -43,11 +42,6 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
val rootDirPrefix: String =
if (settings.rootdir.value.isEmpty) ""
else Regex.quote(new java.io.File(settings.rootdir.value).getCanonicalPath.replace("\\", "/"))
@nowarn("cat=deprecation")
val isScala3 = settings.isScala3.value
@nowarn("cat=deprecation")
val isScala3Cross: Boolean = settings.isScala3Cross.value
val isScala3Migration = settings.Xmigration.value != NoScalaVersion
lazy val wconf = WConf.parse(settings.Wconf.value, rootDirPrefix) match {
case Left(msgs) =>
val multiHelp =
Expand All @@ -59,11 +53,10 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
globalError(s"Failed to parse `-Wconf` configuration: ${settings.Wconf.value}\n${msgs.mkString("\n")}$multiHelp")
WConf(Nil)
case Right(conf) =>
if (isScala3 && !conf.filters.exists(_._1.exists { case MessageFilter.Category(WarningCategory.Scala3Migration) => true case _ => false })) {
val migrationAction = if (isScala3Migration) Action.Warning else Action.Error
val migrationCategory = MessageFilter.Category(WarningCategory.Scala3Migration) :: Nil
WConf(conf.filters :+ (migrationCategory, migrationAction))
}
// configure cat=scala3-migration if it isn't yet
val Migration = MessageFilter.Category(WarningCategory.Scala3Migration)
val boost = (settings.isScala3: @nowarn) && !conf.filters.exists(_._1.exists(_ == Migration))
if (boost) conf.copy(filters = conf.filters :+ (Migration :: Nil, Action.Error))
else conf
}

Expand Down Expand Up @@ -204,7 +197,7 @@ trait Reporting extends internal.Reporting { self: ast.Positions with Compilatio
case _ => ""
})
def scala3migration(isError: Boolean) =
if (isError && isScala3 && warning.category == WarningCategory.Scala3Migration)
if (isError && warning.category == WarningCategory.Scala3Migration)
"\nScala 3 migration messages are errors under -Xsource:3. Use -Wconf / @nowarn to filter them or add -Xmigration to demote them to warnings."
else ""
def helpMsg(kind: String, isError: Boolean = false) =
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/scala/tools/nsc/ast/parser/Parsers.scala
Expand Up @@ -1556,7 +1556,7 @@ self =>

// Scala 2 allowed uprooted Ident for purposes of virtualization
val t1 =
if (currentRun.isScala3Cross) atPos(o2p(start)) { Select(Select(Ident(nme.ROOTPKG), nme.scala_), nme.StringContextName) }
if (currentRun.sourceFeatures.stringContextScope) atPos(o2p(start)) { Select(Select(Ident(nme.ROOTPKG), nme.scala_), nme.StringContextName) }
else atPos(o2p(start)) { Ident(nme.StringContextName).updateAttachment(VirtualStringContext) }
val t2 = atPos(start) { Apply(t1, partsBuf.toList) } updateAttachment InterpolatedString
t2 setPos t2.pos.makeTransparent
Expand Down
19 changes: 9 additions & 10 deletions src/compiler/scala/tools/nsc/ast/parser/Scanners.scala
Expand Up @@ -528,13 +528,12 @@ trait Scanners extends ScannersCommon {
(sepRegions.isEmpty || sepRegions.head == RBRACE)) {
if (pastBlankLine()) insertNL(NEWLINES)
else if (!isLeadingInfixOperator) insertNL(NEWLINE)
else if (!currentRun.isScala3Cross) {
val msg = """|Line starts with an operator that in future
|will be taken as an infix expression continued from the previous line.
|To force the previous interpretation as a separate statement,
|add an explicit `;`, add an empty line, or remove spaces after the operator."""
if (currentRun.isScala3) warning(offset, msg.stripMargin, Scala3Migration)
else if (infixMigration) deprecationWarning(msg.stripMargin, "2.13.2")
else if (!currentRun.sourceFeatures.leadingInfix) {
val msg =
sm"""Lines starting with an operator are taken as an infix expression continued from the previous line in Scala 3 (or with -Xsource-features:leading-infix).
|To force the current interpretation as a separate statement, add an explicit `;`, add an empty line, or remove spaces after the operator."""
if (currentRun.isScala3) warning(offset, msg, Scala3Migration)
else if (infixMigration) deprecationWarning(msg, "2.13.2")
insertNL(NEWLINE)
}
}
Expand Down Expand Up @@ -966,19 +965,19 @@ trait Scanners extends ScannersCommon {
if (strVal != null)
try {
val processed = StringContext.processUnicode(strVal)
if (processed != strVal && !currentRun.isScala3Cross) {
if (processed != strVal && !currentRun.sourceFeatures.unicodeEscapesRaw) {
val diffPosition = processed.zip(strVal).zipWithIndex.collectFirst { case ((r, o), i) if r != o => i }.getOrElse(processed.length - 1)
val pos = offset + 3 + diffPosition
def msg(what: String) = s"Unicode escapes in triple quoted strings are $what; use the literal character instead"
if (currentRun.isScala3)
warning(pos, msg("ignored in Scala 3"), WarningCategory.Scala3Migration)
warning(pos, msg("ignored in Scala 3 (or with -Xsource-features:unicode-escapes-raw)"), WarningCategory.Scala3Migration)
else
deprecationWarning(pos, msg("deprecated"), since="2.13.2")
strVal = processed
}
} catch {
case ue: StringContext.InvalidUnicodeEscapeException =>
if (!currentRun.isScala3Cross)
if (!currentRun.sourceFeatures.unicodeEscapesRaw)
syntaxError(offset + 3 + ue.index, ue.getMessage())
}

Expand Down
9 changes: 5 additions & 4 deletions src/compiler/scala/tools/nsc/settings/MutableSettings.scala
Expand Up @@ -212,8 +212,8 @@ class MutableSettings(val errorFn: String => Unit, val pathFactory: PathFactory)
add(new IntSetting(name, descr, default, range, parser))
def MultiStringSetting(name: String, arg: String, descr: String, default: List[String] = Nil, helpText: Option[String] = None, prepend: Boolean = false) =
add(new MultiStringSetting(name, arg, descr, default, helpText, prepend))
def MultiChoiceSetting[E <: MultiChoiceEnumeration](name: String, helpArg: String, descr: String, domain: E, default: Option[List[String]] = None) =
add(new MultiChoiceSetting[E](name, helpArg, descr, domain, default))
def MultiChoiceSetting[E <: MultiChoiceEnumeration](name: String, helpArg: String, descr: String, domain: E, default: Option[List[String]] = None, helpText: Option[String] = None) =
add(new MultiChoiceSetting[E](name, helpArg, descr, domain, default, helpText))
def OutputSetting(default: String) = add(new OutputSetting(default))
def PhasesSetting(name: String, descr: String, default: String = "") = add(new PhasesSetting(name, descr, default))
def StringSetting(name: String, arg: String, descr: String, default: String = "", helpText: Option[String] = None) = add(new StringSetting(name, arg, descr, default, helpText))
Expand Down Expand Up @@ -609,7 +609,8 @@ class MutableSettings(val errorFn: String => Unit, val pathFactory: PathFactory)
val helpArg: String,
descr: String,
val domain: E,
val default: Option[List[String]]
val default: Option[List[String]],
val helpText: Option[String]
) extends Setting(name, descr) with Clearable {

withHelpSyntax(s"$name:<${helpArg}s>")
Expand Down Expand Up @@ -780,7 +781,7 @@ class MutableSettings(val errorFn: String => Unit, val pathFactory: PathFactory)
case _ => default
}
val orelse = verboseDefault.map(_.mkString(f"%nDefault: ", ", ", f"%n")).getOrElse("")
choices.zipAll(descriptions, "", "").map(describe).mkString(f"${descr}%n", f"%n", orelse)
choices.zipAll(descriptions, "", "").map(describe).mkString(f"${helpText.getOrElse(descr)}%n", f"%n", orelse)
}

def clear(): Unit = {
Expand Down
111 changes: 78 additions & 33 deletions src/compiler/scala/tools/nsc/settings/ScalaSettings.scala
Expand Up @@ -125,14 +125,17 @@ trait ScalaSettings extends StandardScalaSettings with Warnings { _: MutableSett
val sourceReader = StringSetting ("-Xsource-reader", "classname", "Specify a custom method for reading source files.", "")
val reporter = StringSetting ("-Xreporter", "classname", "Specify a custom subclass of FilteringReporter for compiler messages.", "scala.tools.nsc.reporters.ConsoleReporter")
private val XsourceHelp =
sm"""|-Xsource:3 is for migrating a codebase, -Xsource:3-cross is for cross-building.
sm"""|-Xsource:3 is for migrating a codebase, -Xsource-features can be added for
|cross-building to adopt certain Scala 3 behavior.
|
|See also "Scala 2 with -Xsource:3" on docs.scala-lang.org.
|
|-Xsource:3 issues migration warnings in category `cat=scala3-migration`,
| which by default are promoted to errors under the `-Wconf` configuration.
| Examples of promoted warnings:
|which are promoted to errors by default using a `-Wconf` configuration.
|Examples of promoted warnings:
| * Implicit definitions must have an explicit type
| * (x: Any) + "" is deprecated
| * Args not adapted to unit value
| * An empty argument list is not adapted to the unit value
| * Member classes cannot shadow a same-named class defined in a parent
| * Presence or absence of parentheses in overrides must match exactly
|
Expand All @@ -144,31 +147,75 @@ trait ScalaSettings extends StandardScalaSettings with Warnings { _: MutableSett
| * import p.{given, *}
| * Eta-expansion `x.m` of methods without trailing `_`
|
|The following constructs emit a migration warning under -Xsource:3. With
|-Xsource:3-cross the semantics change to match Scala 3 and no warning is issued.
| * Unicode escapes in raw interpolations and triple-quoted strings
| * Leading infix operators continue the previous line
| * Interpolator must be selectable from `scala.StringContext`
| * Case class copy and apply have the same access modifier as the constructor
| * The inferred type of an override is taken from the member it overrides
|The following constructs emit a migration warning under -Xsource:3. To adopt
|Scala 3 semantics, see `-Xsource-features:help`.
|${sourceFeatures.values.toList.collect { case c: sourceFeatures.Choice if c.expandsTo.isEmpty => c.help }.map(h => s" * $h").mkString("\n")}
|"""
@nowarn("cat=deprecation")
val source = ScalaVersionSetting ("-Xsource", "version", "Enable warnings and features for a future version.", initial = ScalaVersion("2.13"), helpText = Some(XsourceHelp)).withPostSetHook { s =>
if (s.value >= ScalaVersion("3")) {
isScala3.value = true
if (s.value > ScalaVersion("3"))
isScala3Cross.value = true
}
else if (s.value >= ScalaVersion("2.14"))
s.withDeprecationMessage("instead of -Xsource:2.14, use -Xsource:3 or -Xsource:3-cross").value = ScalaVersion("3")
else if (s.value < ScalaVersion("2.13"))
errorFn.apply(s"-Xsource must be at least the current major version (${ScalaVersion("2.13").versionString})")
val source = ScalaVersionSetting ("-Xsource", "version", "Enable warnings and features for a future version.", initial = ScalaVersion("2.13"), helpText = Some(XsourceHelp)).withPostSetHook { s =>
if (s.value.unparse == "3.0.0-cross")
XsourceFeatures.tryToSet(List("_"))
if (s.value < ScalaVersion("3"))
if (s.value >= ScalaVersion("2.14"))
s.withDeprecationMessage("instead of -Xsource:2.14, use -Xsource:3 and optionally -Xsource-features").value = ScalaVersion("3")
else if (s.value < ScalaVersion("2.13"))
errorFn.apply(s"-Xsource must be at least the current major version (${ScalaVersion("2.13").versionString})")
}

private val scala3Version = ScalaVersion("3")
@deprecated("Use currentRun.isScala3 instead", since="2.13.9")
val isScala3 = BooleanSetting ("isScala3", "Is -Xsource Scala 3?").internalOnly()
@deprecated("Use currentRun.isScala3Cross instead", since="2.13.13")
val isScala3Cross = BooleanSetting ("isScala3Cross", "Is -Xsource > Scala 3?").internalOnly()
// The previous "-Xsource" option is intended to be used mainly though ^ helper
def isScala3 = source.value >= scala3Version

// buffet of features available under -Xsource:3
object sourceFeatures extends MultiChoiceEnumeration {
// Changes affecting binary encoding
val caseApplyCopyAccess = Choice("case-apply-copy-access", "Constructor modifiers are used for apply / copy methods of case classes. [bin]")
val caseCompanionFunction = Choice("case-companion-function", "Synthetic case companion objects no longer extend FunctionN. [bin]")
val inferOverride = Choice("infer-override", "Inferred type of member uses type of overridden member. [bin]")

// Other semantic changes
val any2StringAdd = Choice("any2stringadd", "Implicit `any2stringadd` is never inferred.")
val unicodeEscapesRaw = Choice("unicode-escapes-raw", "Don't process unicode escapes in triple quoted strings and raw interpolations.")
val stringContextScope = Choice("string-context-scope", "String interpolations always desugar to scala.StringContext.")
val leadingInfix = Choice("leading-infix", "Leading infix operators continue the previous line.")
val packagePrefixImplicits = Choice("package-prefix-implicits", "The package prefix p is no longer part of the implicit search scope for type p.A.")
val implicitResolution = Choice("implicit-resolution", "Use Scala-3-style downwards comparisons for implicit search and overloading resolution (see github.com/scala/scala/pull/6037).")

val v13_13_choices = List(caseApplyCopyAccess, caseCompanionFunction, inferOverride, any2StringAdd, unicodeEscapesRaw, stringContextScope, leadingInfix, packagePrefixImplicits)

val v13_13 = Choice(
"v2.13.13",
v13_13_choices.mkString("", ",", "."),
expandsTo = v13_13_choices)

val v13_14_choices = implicitResolution :: v13_13_choices

val v13_14 = Choice(
"v2.13.14",
"v2.13.13 plus implicit-resolution",
expandsTo = v13_14_choices)
}
val XsourceFeatures = MultiChoiceSetting(
name = "-Xsource-features",
helpArg = "feature",
descr = "Enable Scala 3 features under -Xsource:3: `-Xsource-features:help` for details.",
domain = sourceFeatures,
helpText = Some(
sm"""Enable Scala 3 features under -Xsource:3.
|
|Instead of `-Xsource-features:_`, it is recommended to enable specific features, for
|example `-Xsource-features:v2.13.14,-case-companion-function` (-x to exclude x).
|This way, new semantic changes in future Scala versions are not silently adopted;
|new features can be enabled after auditing the corresponding migration warnings.
|
|`-Xsource:3-cross` is a shorthand for `-Xsource:3 -Xsource-features:_`.
|
|Features marked with [bin] affect the binary encoding. Enabling them in a project
|with existing releases for Scala 2.13 can break binary compatibility.
|
|Available features:
|""")
)

val XnoPatmatAnalysis = BooleanSetting ("-Xno-patmat-analysis", "Don't perform exhaustivity/unreachability analysis. Also, ignore @switch annotation.")

Expand Down Expand Up @@ -311,7 +358,8 @@ trait ScalaSettings extends StandardScalaSettings with Warnings { _: MutableSett
val YpickleWrite = StringSetting("-Ypickle-write", "directory|jar", "destination for generated .sig files containing type signatures.", "", None).internalOnly()
val YpickleWriteApiOnly = BooleanSetting("-Ypickle-write-api-only", "Exclude private members (other than those material to subclass compilation, such as private trait vals) from generated .sig files containing type signatures.").internalOnly()
val YtrackDependencies = BooleanSetting("-Ytrack-dependencies", "Record references to in unit.depends. Deprecated feature that supports SBT 0.13 with incOptions.withNameHashing(false) only.", default = true)
val Yscala3ImplicitResolution = BooleanSetting("-Yscala3-implicit-resolution", "Use Scala-3-style downwards comparisons for implicit search and overloading resolution (see https://github.com/scala/bug/issues/12437).", default = false)
val Yscala3ImplicitResolution = BooleanSetting("-Yscala3-implicit-resolution", "Use Scala-3-style downwards comparisons for implicit search and overloading resolution (see github.com/scala/scala/pull/6037).")
.withDeprecationMessage("Use -Xsource:3 -Xsource-features:implicit-resolution instead")

sealed abstract class CachePolicy(val name: String, val help: String)
object CachePolicy {
Expand Down Expand Up @@ -619,16 +667,13 @@ trait ScalaSettings extends StandardScalaSettings with Warnings { _: MutableSett
}

def conflictWarning: Option[String] = {
// See cd878232b5 for an example how to warn about conflicting settings

/*
def checkSomeConflict: Option[String] = ...
@nowarn("cat=deprecation")
def sourceFeatures: Option[String] =
Option.when(XsourceFeatures.value.nonEmpty && !isScala3)(s"${XsourceFeatures.name} requires -Xsource:3")

List(/* checkSomeConflict, ... */).flatten match {
List(sourceFeatures).flatten match {
case Nil => None
case warnings => Some("Conflicting compiler settings were detected. Some settings will be ignored.\n" + warnings.mkString("\n"))
}
*/
None
}
}

0 comments on commit 6b1b9d7

Please sign in to comment.