diff --git a/modules/cli/src/main/scala/scala/cli/commands/ScalaCommand.scala b/modules/cli/src/main/scala/scala/cli/commands/ScalaCommand.scala index 93693edcae..bedc39b873 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/ScalaCommand.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/ScalaCommand.scala @@ -21,6 +21,7 @@ import scala.build.input.{ScalaCliInvokeData, SubCommand} import scala.build.internal.util.WarningMessages import scala.build.internal.{Constants, Runner} import scala.build.internals.{EnvVar, FeatureType} +import scala.build.options.ScalacOpt.noDashPrefixes import scala.build.options.{BuildOptions, ScalacOpt, Scope} import scala.build.{Artifacts, Directories, Logger, Positioned, ReplArtifacts} import scala.cli.commands.default.LegacyScalaOptions @@ -175,9 +176,13 @@ abstract class ScalaCommand[T <: HasGlobalOptions](implicit myParser: Parser[T], val logger = options.global.logging.logger sharedOptions(options).foreach { so => val scalacOpts = so.scalacOptions.toScalacOptShadowingSeq - if scalacOpts.keys.contains(ScalacOpt(YScriptRunnerOption)) then - logger.message( - LegacyScalaOptions.yScriptRunnerWarning(scalacOpts.getOption(YScriptRunnerOption)) + scalacOpts.keys + .find(k => + k == ScalacOpt(s"-$YScriptRunnerOption") || k == ScalacOpt(s"--$YScriptRunnerOption") + ) + .map(_.value) + .foreach(k => + logger.message(LegacyScalaOptions.yScriptRunnerWarning(k, scalacOpts.getOption(k))) ) } } @@ -192,7 +197,7 @@ abstract class ScalaCommand[T <: HasGlobalOptions](implicit myParser: Parser[T], shared <- sharedOptions(options) scalacOptions = shared.scalacOptions updatedScalacOptions = scalacOptions.withScalacExtraOptions(shared.scalacExtra) - if updatedScalacOptions.exists(ScalacOptions.ScalacPrintOptions) + if updatedScalacOptions.map(_.noDashPrefixes).exists(ScalacOptions.ScalacPrintOptions) logger = shared.logger fixedBuildOptions = buildOptions.copy(scalaOptions = buildOptions.scalaOptions.copy(defaultScalaVersion = Some(ScalaCli.getDefaultScalaVersion)) diff --git a/modules/cli/src/main/scala/scala/cli/commands/default/LegacyScalaOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/default/LegacyScalaOptions.scala index 666f8723fb..5d1e3031db 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/default/LegacyScalaOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/default/LegacyScalaOptions.scala @@ -11,7 +11,6 @@ import scala.cli.commands.default.LegacyScalaOptions.* import scala.cli.commands.package0.Package import scala.cli.commands.shared.HelpGroup import scala.cli.commands.shared.HelpMessages.PowerString -import scala.cli.commands.shared.ScalacOptions.YScriptRunnerOption import scala.cli.commands.tags /** Options covering backwards compatibility with the old scala runner. @@ -168,7 +167,7 @@ object LegacyScalaOptions { implicit lazy val parser: Parser[LegacyScalaOptions] = Parser.derive implicit lazy val help: Help[LegacyScalaOptions] = Help.derive - def yScriptRunnerWarning(yScriptRunnerValue: Option[String]): String = { + def yScriptRunnerWarning(yScriptRunnerKey: String, yScriptRunnerValue: Option[String]): String = { val valueSpecificMsg = yScriptRunnerValue match { case Some(v @ "default") => s"scala.tools.nsc.DefaultScriptRunner (the $v script runner) is no longer available." @@ -185,7 +184,7 @@ object LegacyScalaOptions { s"Using $className as the script runner is no longer supported and will not be attempted." case _ => "" } - s"""Deprecated option '$YScriptRunnerOption' is ignored. + s"""Deprecated option '$yScriptRunnerKey' is ignored. |The script runner can no longer be picked as before. |$valueSpecificMsg""".stripMargin } diff --git a/modules/cli/src/main/scala/scala/cli/commands/package0/PackageOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/package0/PackageOptions.scala index 917579ae11..205ec3b401 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/package0/PackageOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/package0/PackageOptions.scala @@ -51,8 +51,6 @@ final case class PackageOptions( @Name("sourcesJar") @Name("jarSources") @Name("sources") - @Name("source") - @Tag(tags.deprecated("source")) // alias to be removed in 1.6.x @Tag(tags.restricted) @Tag(tags.inShortHelp) src: Boolean = false, diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/ScalacOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/ScalacOptions.scala index ce73e08edc..e3bbd220a8 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/ScalacOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/ScalacOptions.scala @@ -8,6 +8,7 @@ import caseapp.core.{Arg, Error} import com.github.plokhotnyuk.jsoniter_scala.core.* import com.github.plokhotnyuk.jsoniter_scala.macros.* +import scala.build.options.ScalacOpt.noDashPrefixes import scala.cli.commands.tags // format: off @@ -26,6 +27,13 @@ final case class ScalacOptions( // format: on object ScalacOptions { + extension (opt: String) { + private def hasValidScalacOptionDashes: Boolean = + opt.startsWith("-") && opt.length > 1 && ( + if opt.length > 2 then opt.charAt(2) != '-' + else opt.charAt(1) != '-' + ) + } private val scalacOptionsArg = Arg("scalacOption").copy( extraNames = Seq(Name("scala-opt"), Name("O"), Name("scala-option")), @@ -37,24 +45,36 @@ object ScalacOptions { origin = Some("ScalacOptions") ) // .withIsFlag(true) // The scalac options we handle accept no value after the -… argument - val YScriptRunnerOption = "-Yscriptrunner" - private val scalacOptionsPurePrefixes = - Set("-V", "-W", "-X", "-Y") - private val scalacOptionsPrefixes = - Set("-g", "-language", "-opt", "-P", "-target", "-source") ++ scalacOptionsPurePrefixes + val YScriptRunnerOption = "Yscriptrunner" + private val scalacOptionsPurePrefixes = Set("V", "W", "X", "Y") + private val scalacOptionsPrefixes = Set("P") ++ scalacOptionsPurePrefixes private val scalacAliasedOptions = // these options don't require being passed after -O and accept an arg - Set("-encoding", "-release", "-color", YScriptRunnerOption) + Set( + "coverage-exclude-classlikes", + "coverage-exclude-files", + "encoding", + "release", + "color", + "g", + "language", + "opt", + "target", + "source", + YScriptRunnerOption + ) private val scalacNoArgAliasedOptions = // these options don't require being passed after -O and don't accept an arg Set( - "-unchecked", - "-nowarn", - "-feature", - "-deprecation", - "-rewrite", - "-old-syntax", - "-new-syntax", - "-indent", - "-no-indent" + "experimental", + "explain", + "unchecked", + "nowarn", + "feature", + "deprecation", + "rewrite", + "old-syntax", + "new-syntax", + "indent", + "no-indent" ) /** This includes all the scalac options which disregard inputs and print a help and/or context @@ -62,21 +82,21 @@ object ScalacOptions { */ val ScalacPrintOptions: Set[String] = scalacOptionsPurePrefixes ++ Set( - "-help", - "-opt:help", - "-Xshow-phases", - "-Xsource:help", - "-Xplugin-list", - "-Xmixin-force-forwarders:help", - "-Xlint:help", - "-Vphases" + "help", + "opt:help", + "Xshow-phases", + "Xsource:help", + "Xplugin-list", + "Xmixin-force-forwarders:help", + "Xlint:help", + "Vphases" ) /** This includes all the scalac options which are redirected to native Scala CLI options. */ - val ScalaCliRedirectedOptions = Set( - "-classpath", - "-cp", // redirected to --extra-jars - "-d" // redirected to --compilation-output + val ScalaCliRedirectedOptions: Set[String] = Set( + "classpath", + "cp", // redirected to --extra-jars + "d" // redirected to --compilation-output ) val ScalacDeprecatedOptions: Set[String] = Set( YScriptRunnerOption // old 'scala' runner specific, no longer supported @@ -99,12 +119,20 @@ object ScalacOptions { ): Either[(Error, List[String]), Option[(Option[List[String]], List[String])]] = args match { case h :: t - if scalacOptionsPrefixes.exists(h.startsWith) && - !ScalacDeprecatedOptions.contains(h) => + if h.hasValidScalacOptionDashes && + scalacOptionsPrefixes.exists(h.noDashPrefixes.startsWith) && + !ScalacDeprecatedOptions.contains(h.noDashPrefixes) => Right(Some((Some(acc.getOrElse(Nil) :+ h), t))) - case h :: t if scalacNoArgAliasedOptions.contains(h) => + case h :: t + if h.hasValidScalacOptionDashes && + scalacNoArgAliasedOptions.contains(h.noDashPrefixes) => Right(Some((Some(acc.getOrElse(Nil) :+ h), t))) - case h :: t if scalacAliasedOptions.contains(h) => + case h :: t + if h.hasValidScalacOptionDashes && + scalacAliasedOptions.exists(o => h.noDashPrefixes.startsWith(o + ":")) && + h.count(_ == ':') == 1 => Right(Some((Some(acc.getOrElse(Nil) :+ h), t))) + case h :: t + if h.hasValidScalacOptionDashes && scalacAliasedOptions.contains(h.noDashPrefixes) => // check if the next scalac arg is a different option or a param to the current option val maybeOptionArg = t.headOption.filter(!_.startsWith("-")) // if it's a param, it'll be treated as such and considered already parsed @@ -118,8 +146,8 @@ object ScalacOptions { } implicit lazy val parser: Parser[ScalacOptions] = { - val baseParser = scalacOptionsArgument :: NilParser - implicit val p = ArgFileOption.parser + val baseParser = scalacOptionsArgument :: NilParser + implicit val p: Parser[List[ArgFileOption]] = ArgFileOption.parser baseParser.addAll[List[ArgFileOption]].to[ScalacOptions] } @@ -130,7 +158,7 @@ object ScalacOptions { case class ArgFileOption(file: String) extends AnyVal object ArgFileOption { - val arg = Arg( + val arg: Arg = Arg( name = Name("args-file"), valueDescription = Some(ValueDescription("@arguments-file")), helpMessage = Some(HelpMessage("File with scalac options.")), diff --git a/modules/cli/src/main/scala/scala/cli/commands/util/ScalacOptionsUtil.scala b/modules/cli/src/main/scala/scala/cli/commands/util/ScalacOptionsUtil.scala index 51cb1bc8e1..7d513f3bfb 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/util/ScalacOptionsUtil.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/util/ScalacOptionsUtil.scala @@ -1,7 +1,7 @@ package scala.cli.commands.util import scala.build.Logger -import scala.build.options.ScalacOpt.filterScalacOptionKeys +import scala.build.options.ScalacOpt.{filterScalacOptionKeys, noDashPrefixes} import scala.build.options.{ScalacOpt, ShadowingSeq} import scala.cli.commands.bloop.BloopExit import scala.cli.commands.default.LegacyScalaOptions @@ -33,9 +33,13 @@ object ScalacOptionsUtil { extension (opts: ShadowingSeq[ScalacOpt]) { def filterNonRedirected: ShadowingSeq[ScalacOpt] = - opts.filterScalacOptionKeys(!ScalacOptions.ScalaCliRedirectedOptions.contains(_)) + opts.filterScalacOptionKeys(k => + !ScalacOptions.ScalaCliRedirectedOptions.contains(k.noDashPrefixes) + ) def filterNonDeprecated: ShadowingSeq[ScalacOpt] = - opts.filterScalacOptionKeys(!ScalacOptions.ScalacDeprecatedOptions.contains(_)) + opts.filterScalacOptionKeys(k => + !ScalacOptions.ScalacDeprecatedOptions.contains(k.noDashPrefixes) + ) def getOption(key: String): Option[String] = opts.get(ScalacOpt(key)).headOption.map(_.value) } diff --git a/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala index 521e3bb5c1..eb316524cf 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/BspTestDefinitions.scala @@ -1712,7 +1712,7 @@ abstract class BspTestDefinitions extends ScalaCliSuite with TestScalaVersionArg "--power", "package", jarSources, - "--source", + "--src", "-o", sourceJarPath, extraOptions diff --git a/modules/integration/src/test/scala/scala/cli/integration/CompileScalacCompatTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/CompileScalacCompatTestDefinitions.scala new file mode 100644 index 0000000000..872924ae58 --- /dev/null +++ b/modules/integration/src/test/scala/scala/cli/integration/CompileScalacCompatTestDefinitions.scala @@ -0,0 +1,208 @@ +package scala.cli.integration + +import com.eed3si9n.expecty.Expecty.expect + +import scala.util.Properties + +/** For the `run` counterpart, refer to [[RunScalacCompatTestDefinitions]] */ +trait CompileScalacCompatTestDefinitions { _: CompileTestDefinitions => + if (actualScalaVersion.startsWith("3")) + test("consecutive -language:* flags are not ignored") { + val sourceFileName = "example.scala" + TestInputs(os.rel / sourceFileName -> + s"""//> using scala $actualScalaVersion + |//> using options -color:never -language:noAutoTupling -language:strictEquality + |case class Cat(name: String) + |case class Dog(name: String) + |def strictEquality(c: Cat, d: Dog):Boolean = c == d + |def takesTuple(tpl: Tuple) = ??? + |def withTuple() = takesTuple(1, 2) + |""".stripMargin).fromRoot { root => + val res = os.proc(TestUtil.cli, "compile", sourceFileName) + .call(cwd = root, check = false, stderr = os.Pipe) + expect(res.exitCode == 1) + val errOutput = res.err.trim() + val expectedStrictEqualityError = + " Values of types Cat and Dog cannot be compared with == or !=" + expect(errOutput.contains(expectedStrictEqualityError)) + val expectedNoAutoTuplingError = + "too many arguments for method takesTuple: (tpl: Tuple): Nothing" + expect(errOutput.trim().contains(expectedNoAutoTuplingError)) + } + } + + // Given the vast number of ways compiler options can be passed from the CLI, + // we test them all (or most, at the very least), as a (perhaps overkill) sanity check. + // Pieces of the existing `-language:*` test are reused, but kept separate for clarity. + { + val modes @ Seq(viaDirective, viaCli, viaCliWithExplicitOpt, mixed, mixedWithExplicitOpt) = + Seq("directive", "cli", "cli with -O", "mixed", "mixed with -O") + for { + mode <- modes + if actualScalaVersion == Constants.scala3Next + dashPrefix <- Seq("-", "--") + syntaxVariant <- Seq( + Seq( + Seq(s"${dashPrefix}color:never"), + Seq(s"${dashPrefix}language:noAutoTupling"), + Seq(s"${dashPrefix}language:strictEquality") + ), + Seq( + Seq(s"${dashPrefix}color", "never"), + Seq(s"${dashPrefix}language", "noAutoTupling"), + Seq(s"${dashPrefix}language", "strictEquality") + ), + Seq( + Seq(s"${dashPrefix}color:never"), + Seq(s"\"${dashPrefix}language:noAutoTupling,strictEquality\"") + ), + Seq( + Seq(s"${dashPrefix}color", "never"), + Seq(s"${dashPrefix}language", "\"noAutoTupling,strictEquality\"") + ) + ) + (cliOpts, directiveOpts) = { + val (initialCliOpts, initialDirectiveOpts) = mode match { + case m if m == mixed => syntaxVariant.splitAt(syntaxVariant.length - 1) + case m if m == mixedWithExplicitOpt => + val (initialCliOpts, initialDirectiveOpts) = + syntaxVariant.splitAt(syntaxVariant.length - 1) + initialCliOpts.map(_.flatMap(o => Seq("-O", o))) -> initialDirectiveOpts + case c if c == viaCli => syntaxVariant -> Nil + case c if c == viaCliWithExplicitOpt => + syntaxVariant.map(_.flatMap(o => Seq("-O", o))) -> Nil + case _ => Nil -> syntaxVariant + } + initialCliOpts.flatten.map(_.filter(_ != '"')) -> initialDirectiveOpts.flatten + } + cliOptsString = cliOpts.mkString(" ") + directiveOptsString = directiveOpts.mkString(" ") + includeDirective = + (mode == viaDirective || mode == mixed || mode == mixedWithExplicitOpt) && directiveOpts.nonEmpty + directiveString = if (includeDirective) s"//> using options $directiveOptsString" else "" + allOptsString = mode match { + case m if m.startsWith(mixed) => + s"opts passed via command line: $cliOptsString, opts passed via directive: $directiveString" + case c if c.startsWith(viaCli) => + s"opts passed via command line: $cliOptsString" + case _ => + s"opts passed via directive: $directiveString" + } + } test(s"compiler options passed in $mode mode: $allOptsString") { + val sourceFileName = "example.scala" + TestInputs(os.rel / sourceFileName -> + s"""//> using scala $actualScalaVersion + |$directiveString + |case class Cat(name: String) + |case class Dog(name: String) + |def strictEquality(c: Cat, d: Dog):Boolean = c == d + |def takesTuple(tpl: Tuple) = ??? + |def withTuple() = takesTuple(1, 2) + |""".stripMargin).fromRoot { root => + val res = os.proc(TestUtil.cli, "compile", sourceFileName, cliOpts) + .call(cwd = root, check = false, stderr = os.Pipe) + println(res.err.trim()) + expect(res.exitCode == 1) + val errOutput = res.err.trim() + val expectedStrictEqualityError = + "Values of types Cat and Dog cannot be compared with == or !=" + expect(errOutput.contains(expectedStrictEqualityError)) + val expectedNoAutoTuplingError = + "too many arguments for method takesTuple: (tpl: Tuple): Nothing" + expect(errOutput.trim().contains(expectedNoAutoTuplingError)) + } + } + } + + for { + useDirective <- Seq(true, false) + if !Properties.isWin + optionsSource = if (useDirective) "using directive" else "command line" + sv = actualScalaVersion + } { + test(s"consecutive -Wconf:* flags are not ignored (passed via $optionsSource)") { + val sourceFileName = "example.scala" + val warningConfOptions = Seq("-Wconf:cat=deprecation:e", "-Wconf:any:s") + val maybeDirectiveString = + if (useDirective) s"//> using options ${warningConfOptions.mkString(" ")}" else "" + TestInputs(os.rel / sourceFileName -> + s"""//> using scala $sv + |$maybeDirectiveString + |object WConfExample extends App { + | @deprecated("This method will be removed", "1.0.0") + | def oldMethod(): Unit = println("This is an old method.") + | oldMethod() + |} + |""".stripMargin).fromRoot { root => + val localBin = root / "local-bin" + os.proc( + TestUtil.cs, + "install", + "--install-dir", + localBin, + s"scalac:$sv" + ).call(cwd = root) + val cliRes = + os.proc( + TestUtil.cli, + "compile", + sourceFileName, + "--server=false", + if (useDirective) Nil else warningConfOptions + ) + .call(cwd = root, check = false, stderr = os.Pipe) + val scalacRes = os.proc(localBin / "scalac", warningConfOptions, sourceFileName) + .call(cwd = root, check = false, stderr = os.Pipe) + expect(scalacRes.exitCode == cliRes.exitCode) + val scalacResErr = scalacRes.err.trim() + if (sv != Constants.scala3Lts) { + // TODO run this check for LTS when -Wconf gets fixed there + val cliResErr = + cliRes.err.trim().linesIterator.toList + // skip potentially irrelevant logs + .dropWhile(_.contains("Check")) + .mkString(System.lineSeparator()) + expect(cliResErr == scalacResErr) + } + else expect( + TestUtil.removeAnsiColors(cliRes.err.trim()) + .contains( + "method oldMethod in object WConfExample is deprecated since 1.0.0: This method will be removed" + ) + ) + } + } + + if (!sv.startsWith("2.12")) + test(s"consecutive -Wunused:* flags are not ignored (passed via $optionsSource)") { + val sourceFileName = "example.scala" + val unusedLintOptions = Seq("-Wunused:locals", "-Wunused:privates") + val maybeDirectiveString = + if (useDirective) s"//> using options ${unusedLintOptions.mkString(" ")}" else "" + TestInputs(os.rel / sourceFileName -> + s"""//> using scala $sv + |$maybeDirectiveString + |object WUnusedExample { + | private def unusedPrivate(): String = "stuff" + | def methodWithUnusedLocal() = { + | val smth = "hello" + | println("Hello") + | } + |} + |""".stripMargin).fromRoot { root => + val r = + os.proc( + TestUtil.cli, + "compile", + sourceFileName, + if (useDirective) Nil else unusedLintOptions + ) + .call(cwd = root, stderr = os.Pipe) + val err = r.err.trim() + val unusedKeyword = if (sv.startsWith("2")) "never used" else "unused" + expect(err.linesIterator.exists(l => l.contains(unusedKeyword) && l.contains("local"))) + expect(err.linesIterator.exists(l => l.contains(unusedKeyword) && l.contains("private"))) + } + } + } +} diff --git a/modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala index c60703d020..7ef9e63ec5 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/CompileTestDefinitions.scala @@ -10,6 +10,7 @@ abstract class CompileTestDefinitions extends ScalaCliSuite with TestScalaVersionArgs with CompilerPluginTestDefinitions + with CompileScalacCompatTestDefinitions with SemanticDbTestDefinitions { _: TestScalaVersion => protected lazy val extraOptions: Seq[String] = scalaVersionArgs ++ TestUtil.extraOptions diff --git a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala index fe54f6cd62..23c5c331f8 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala @@ -859,16 +859,11 @@ abstract class PackageTestDefinitions extends ScalaCliSuite with TestScalaVersio test("source JAR") { val dest = os.rel / "sources.jar" simpleInputWithScalaAndSc.fromRoot { root => - val r = - os.proc(TestUtil.cli, "--power", "package", extraOptions, ".", "-o", dest, "--source").call( - cwd = root, - stdin = os.Inherit, - stdout = os.Inherit, - stderr = os.Pipe - ) - expect(r.err.trim().contains( - "The --source option alias has been deprecated and may be removed in a future version" - )) + os.proc(TestUtil.cli, "--power", "package", extraOptions, ".", "-o", dest, "--src").call( + cwd = root, + stdin = os.Inherit, + stdout = os.Inherit + ) expect(os.isFile(root / dest)) diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunScalacCompatTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunScalacCompatTestDefinitions.scala index 1051cdf3bb..9ef2a8e7c0 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunScalacCompatTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunScalacCompatTestDefinitions.scala @@ -7,6 +7,7 @@ import java.io.File import scala.jdk.CollectionConverters.* import scala.util.Properties +/** For the `compile` counterpart, refer to [[CompileScalacCompatTestDefinitions]] */ trait RunScalacCompatTestDefinitions { _: RunTestDefinitions => @@ -548,4 +549,60 @@ trait RunScalacCompatTestDefinitions { expect(res.out.trim() == s"version $actualScalaVersion") } } + + for { + useDirective <- Seq(true, false) + if !Properties.isWin + optionsSource = if (useDirective) "using directive" else "command line" + if actualScalaVersion == Constants.scala3Next || actualScalaVersion == Constants.scala3NextRc + } + test(s"consecutive -Xmacro-settings:* flags are not ignored (passed via $optionsSource)") { + val sourceFileName = "example.scala" + val macroFileName = "macro.scala" + val macroSettings @ Seq(macroSetting1, macroSetting2, macroSetting3) = + Seq("one", "two", "three") + val macroSettingOptions = macroSettings.map(s => s"-Xmacro-settings:$s") + val maybeDirectiveString = + if (useDirective) s"//> using options ${macroSettingOptions.mkString(" ")}" else "" + TestInputs( + os.rel / macroFileName -> + """package x + |import scala.quoted.* + |object M: + | inline def settingsContains(inline x:String): Boolean = ${ + | settingsContainsImpl('x) + | } + | def settingsContainsImpl(x:Expr[String])(using Quotes): Expr[Boolean] = + | import quotes.reflect.* + | val v = x.valueOrAbort + | val r = CompilationInfo.XmacroSettings.contains(v) + | Expr(r) + |""".stripMargin, + os.rel / sourceFileName -> + s"""$maybeDirectiveString + |import x.M + |@main def main(): Unit = { + | val output = Seq( + | if M.settingsContains("$macroSetting1") then Seq("$macroSetting1") else Nil, + | if M.settingsContains("$macroSetting2") then Seq("$macroSetting2") else Nil, + | if M.settingsContains("$macroSetting3") then Seq("$macroSetting3") else Nil, + | if M.settingsContains("dummy") then Seq("dummy") else Nil, + | ) + | println(output.flatten.mkString(", ")) + |} + | + |""".stripMargin + ).fromRoot { root => + val r = os.proc( + TestUtil.cli, + "run", + ".", + if (useDirective) Nil else macroSettingOptions, + extraOptions, + "-experimental" + ) + .call(cwd = root, stderr = os.Pipe) + expect(r.out.trim() == macroSettings.mkString(", ")) + } + } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/SipScalaTests.scala b/modules/integration/src/test/scala/scala/cli/integration/SipScalaTests.scala index e0e637f610..45cd3c5674 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/SipScalaTests.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/SipScalaTests.scala @@ -496,71 +496,6 @@ class SipScalaTests extends ScalaCliSuite with SbtTestHelper with MillTestHelper } } - test("consecutive -language:* flags are not ignored") { - val sourceFileName = "example.scala" - TestInputs(os.rel / sourceFileName -> - """//> using scala 3.3.1 - |//> using options -Yexplicit-nulls -language:fewerBraces -language:strictEquality - |def repro[A](as: List[A]): List[A] = - | as match - | case Nil => Nil - | case _ => ??? - |""".stripMargin).fromRoot { root => - val res = os.proc(TestUtil.cli, "compile", sourceFileName) - .call(cwd = root, check = false, stderr = os.Pipe) - expect(res.exitCode == 1) - val expectedError = - "Values of types object scala.collection.immutable.Nil and List[A] cannot be compared with == or !=" - expect(res.err.trim().contains(expectedError)) - } - } - - for { - useDirective <- Seq(true, false) - if !Properties.isWin - optionsSource = if (useDirective) "using directive" else "command line" - } test(s"consecutive -Wconf:* flags are not ignored (passed via $optionsSource)") { - val sv = "3.5.2" - val sourceFileName = "example.scala" - val warningConfOptions = Seq("-Wconf:cat=deprecation:e", "-Wconf:any:s") - val maybeDirectiveString = - if (useDirective) s"//> using options ${warningConfOptions.mkString(" ")}" else "" - TestInputs(os.rel / sourceFileName -> - s"""//> using scala $sv - |$maybeDirectiveString - |object WConfExample extends App { - | @deprecated("This method will be removed", "1.0.0") - | def oldMethod(): Unit = println("This is an old method.") - | oldMethod() - |} - |""".stripMargin).fromRoot { root => - val localCache = root / "local-cache" - val localBin = root / "local-bin" - os.proc( - TestUtil.cs, - "install", - "--cache", - localCache, - "--install-dir", - localBin, - s"scalac:$sv" - ).call(cwd = root) - val cliRes = - os.proc( - TestUtil.cli, - "compile", - sourceFileName, - "--server=false", - if (useDirective) Nil else warningConfOptions - ) - .call(cwd = root, check = false, stderr = os.Pipe) - val scalacRes = os.proc(localBin / "scalac", warningConfOptions, sourceFileName) - .call(cwd = root, check = false, stderr = os.Pipe) - expect(scalacRes.exitCode == cliRes.exitCode) - expect(cliRes.err.trim() == scalacRes.err.trim()) - } - } - for { sv <- Seq(Constants.scala212, Constants.scala213, Constants.scala3NextRc) code = diff --git a/modules/options/src/main/scala/scala/build/options/ScalacOpt.scala b/modules/options/src/main/scala/scala/build/options/ScalacOpt.scala index a03d53f474..ff7b0dfc5d 100644 --- a/modules/options/src/main/scala/scala/build/options/ScalacOpt.scala +++ b/modules/options/src/main/scala/scala/build/options/ScalacOpt.scala @@ -1,25 +1,43 @@ package scala.build.options +import scala.build.options.ScalacOpt.noDashPrefixes + final case class ScalacOpt(value: String) { /** @return raw key for the option (if valid) */ private[options] def key: Option[String] = - if value.startsWith("-") then Some(value.takeWhile(_ != ':')) + if value.startsWith("-") || value.startsWith("--") then Some(value.takeWhile(_ != ':')) else Some("@").filter(value.startsWith) /** @return raw key for the option (only if the key can be shadowed from the CLI) */ private[options] def shadowableKey: Option[String] = key match case Some(key) - if ScalacOpt.repeatingKeys.exists(rKey => rKey.startsWith(key + ":") || rKey == key) => None + if ScalacOpt.repeatingKeys + .exists(rKey => + rKey.startsWith(key.noDashPrefixes + ":") || rKey == key.noDashPrefixes + ) => None case otherwise => otherwise } object ScalacOpt { + extension (opt: String) { + def noDashPrefixes: String = opt.stripPrefix("--").stripPrefix("-") + } private val repeatingKeys = Set( - "-Xplugin", - "-P", // plugin options - "-language", - "-Wconf" + "coverage-exclude-classlikes", + "coverage-exclude-files", + "language", + "P", // plugin options + "Wconf", + "Wunused", + "Wshadow", + "Xlint", + "Xmacro-settings", + "Xplugin", + "Xplugin-disable", + "Xplugin-require", + "Yimports", + "Yfrom-tasty-ignore-list" ) implicit val hashedType: HashedType[ScalacOpt] = { @@ -32,12 +50,12 @@ object ScalacOpt { seq => groupCliOptions(seq.map(_.value)) ) - // Groups options (starting with `-` or `@`) with option arguments that follow + // Groups options (starting with `-`, `--` or `@`) with option arguments that follow def groupCliOptions(opts: Seq[String]): Seq[Int] = opts .zipWithIndex .collect { - case (opt, idx) if opt.startsWith("-") || opt.startsWith("@") => + case (opt, idx) if opt.startsWith("-") || opt.startsWith("--") || opt.startsWith("@") => idx } diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index b20e736750..0111b4f955 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -751,7 +751,7 @@ Generate a library JAR rather than an executable JAR ### `--src` -Aliases: `--jar-sources`, [deprecated] `--source`, `--sources`, `--sources-jar` +Aliases: `--jar-sources`, `--sources`, `--sources-jar` Generate a source JAR rather than an executable JAR