diff --git a/modules/build/src/main/scala/scala/build/CrossSources.scala b/modules/build/src/main/scala/scala/build/CrossSources.scala index c7e731752a..80135fe947 100644 --- a/modules/build/src/main/scala/scala/build/CrossSources.scala +++ b/modules/build/src/main/scala/scala/build/CrossSources.scala @@ -1,7 +1,6 @@ package scala.build import java.io.File - import scala.build.CollectionOps.* import scala.build.EitherCps.{either, value} import scala.build.Ops.* @@ -17,11 +16,14 @@ import scala.build.input.ElementsUtils.* import scala.build.input.* import scala.build.internal.Constants import scala.build.internal.util.{RegexUtils, WarningMessages} +import scala.build.options.ScalacOpt.PresetOption import scala.build.options.{ BuildOptions, BuildRequirements, MaybeScalaVersion, + ScalacOpt, Scope, + ShadowingSeq, SuppressWarningOptions, WithBuildRequirements } @@ -261,15 +263,70 @@ object CrossSources { else fromDirectives } + def resolveScalacOptionsForPreset( + mayBeScalaVersion: Option[MaybeScalaVersion], + presetOption: PresetOption + ): Seq[Positioned[ScalacOpt]] = { + // todo: I don't think this is the right approach + val scalaVersion = mayBeScalaVersion.flatMap(_.versionOpt) match { + case Some(version) => version + case None => Constants.defaultScalaVersion + } + + presetOption match { + case PresetOption.Suggested => + presetOptionsSuggested(scalaVersion).map(Positioned.none) + + case PresetOption.CI => + presetOptionsCI(scalaVersion).map(Positioned.none) + + case PresetOption.Strict => + presetOptionsStrict(scalaVersion).map(Positioned.none) + + } + + } + + def buildOptionWithPreset(opts: BuildOptions): BuildOptions = { + val scalaVersion = opts.scalaOptions.scalaVersion + val scalacOpts = opts.scalaOptions.scalacOptions + val scalacPresetOpt = opts.scalaOptions.scalacPresetOption + if (scalacPresetOpt.isDefined && scalacOpts.keys.nonEmpty) { + val scalacMsg = WarningMessages.conflictingScalacOptions( + scalacPresetOpt.map( + _.preset + ).get, // safe to do get here since we have check before + scalacOpts.keys.map(_.value.value) + ) + logger.diagnostic( + scalacMsg, + Severity.Warning, + Nil // todo: fix this + ) + opts + } + else if (scalacPresetOpt.isDefined && scalacOpts.keys.isEmpty) + // resolve the preset options and set into BuildOptions + val resolvedOpts = + resolveScalacOptionsForPreset(opts.scalaOptions.scalaVersion, scalacPresetOpt.get) + opts.copy( + scalaOptions = opts.scalaOptions.copy(scalacOptions = ShadowingSeq.from(resolvedOpts)) + ) + else + opts + + } + val buildOptions: Seq[WithBuildRequirements[BuildOptions]] = (for { preprocessedSource <- preprocessedSources opts <- preprocessedSource.options.toSeq - if opts != BuildOptions() || preprocessedSource.optionsWithTargetRequirements.nonEmpty + optsWithPreset = buildOptionWithPreset(opts) + if optsWithPreset != BuildOptions() || preprocessedSource.optionsWithTargetRequirements.nonEmpty } yield { val baseReqs0 = baseReqs(preprocessedSource.scopePath) preprocessedSource.optionsWithTargetRequirements :+ WithBuildRequirements( preprocessedSource.requirements.fold(baseReqs0)(_ orElse baseReqs0), - opts + optsWithPreset ) }).flatten @@ -497,4 +554,91 @@ object CrossSources { Severity.Warning, transitiveAdditionalSource.positions ) + + private def presetOptionsSuggested(scalaVersion: String) = + scalaVersion match { + case v if v.startsWith("2.12") => + List( + "-encoding", + "utf8", + "-deprecation", + "-feature", + "-unchecked" + ).map(ScalacOpt.apply) + case v if v.startsWith("2.13") => + List( + "-encoding", + "utf8", + "-deprecation", + "-feature", + "-unchecked" + ).map(ScalacOpt.apply) + + case v if v.startsWith("3.0") => + List( + "-encoding", + "utf8", + "-deprecation", + "-feature", + "-unchecked" + ).map(ScalacOpt.apply) + } + + private def presetOptionsCI(scalaVersion: String) = + scalaVersion match { + case v if v.startsWith("2.12") => + List( + "-encoding", + "utf8", + "-deprecation", + "-feature", + "-unchecked" + ).map(ScalacOpt.apply) + case v if v.startsWith("2.13") => + List( + "-encoding", + "utf8", + "-deprecation", + "-feature", + "-unchecked" + ).map(ScalacOpt.apply) + + case v if v.startsWith("3.0") => + List( + "-encoding", + "utf8", + "-deprecation", + "-feature", + "-unchecked" + ).map(ScalacOpt.apply) + } + + private def presetOptionsStrict(scalaVersion: String) = + scalaVersion match { + case v if v.startsWith("2.12") => + List( + "-encoding", + "utf8", + "-deprecation", + "-feature", + "-unchecked" + ).map(ScalacOpt.apply) + case v if v.startsWith("2.13") => + List( + "-encoding", + "utf8", + "-deprecation", + "-feature", + "-unchecked" + ).map(ScalacOpt.apply) + + case v if v.startsWith("3.0") => + List( + "-encoding", + "utf8", + "-deprecation", + "-feature", + "-unchecked" + ).map(ScalacOpt.apply) + } } diff --git a/modules/build/src/main/scala/scala/build/internal/util/WarningMessages.scala b/modules/build/src/main/scala/scala/build/internal/util/WarningMessages.scala index 9f74e9c212..7694bef0e4 100644 --- a/modules/build/src/main/scala/scala/build/internal/util/WarningMessages.scala +++ b/modules/build/src/main/scala/scala/build/internal/util/WarningMessages.scala @@ -139,4 +139,7 @@ object WarningMessages { else s"""Using 'latest' for toolkit is deprecated, use 'default' to get more stable behaviour: | $updatedValue""".stripMargin + + def conflictingScalacOptions(preset: String, scalacOptions: Seq[String]) = + s"The scalacPreset $preset is used, but explicit scalac options ${scalacOptions.mkString(",")} are provided. Preset option is ignored." } diff --git a/modules/build/src/test/scala/scala/build/tests/BuildTests.scala b/modules/build/src/test/scala/scala/build/tests/BuildTests.scala index c163a1b7bc..10e511bc74 100644 --- a/modules/build/src/test/scala/scala/build/tests/BuildTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/BuildTests.scala @@ -778,6 +778,25 @@ abstract class BuildTests(server: Boolean) extends TestUtil.ScalaCliBuildSuite { } } + test("use scalac preset options") { + val inputs = TestInputs( + os.rel / "foo.scala" -> + """//> using scala 2.13 + |//> using options.preset suggested + | + |def foo = "bar" + |""".stripMargin + ) + + inputs.withBuild(defaultOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => + val expectedOptions = + Seq("-Xfatal-warnings", "-deprecation", "-unchecked") + val scalacOptions = + maybeBuild.toOption.get.options.scalaOptions.scalacOptions.toSeq.map(_.value.value) + expect(scalacOptions == expectedOptions) + } + } + test("multiple times scalac options with -Xplugin prefix") { val inputs = TestInputs( os.rel / "foo.scala" -> diff --git a/modules/directives/src/main/scala/scala/build/preprocessing/directives/ScalacOptions.scala b/modules/directives/src/main/scala/scala/build/preprocessing/directives/ScalacOptions.scala index 48db4a520c..cbb5f4375e 100644 --- a/modules/directives/src/main/scala/scala/build/preprocessing/directives/ScalacOptions.scala +++ b/modules/directives/src/main/scala/scala/build/preprocessing/directives/ScalacOptions.scala @@ -2,7 +2,7 @@ package scala.build.preprocessing.directives import scala.build.Positioned import scala.build.directives.* -import scala.build.errors.BuildException +import scala.build.errors.{BuildException, DirectiveErrors, InputsException} import scala.build.options.WithBuildRequirements.* import scala.build.options.{ BuildOptions, @@ -19,6 +19,7 @@ import scala.cli.commands.SpecificationLevel @DirectiveExamples("//> using options -Xasync -Xfatal-warnings") @DirectiveExamples("//> using test.option -Xasync") @DirectiveExamples("//> using test.options -Xasync -Xfatal-warnings") +@DirectiveExamples("//> using options.preset suggested") @DirectiveUsage( "using option _option_ | using options _option1_ _option2_ …", """`//> using scalacOption` _option_ @@ -37,6 +38,9 @@ import scala.cli.commands.SpecificationLevel | |`//> using test.options` _option1_ _option2_ … | + |`//> using options` _option1_ _option2_ … + | + |`//> using options.preset` _suggested_ | _ci_ | _strict_ |""".stripMargin ) @DirectiveDescription("Add Scala compiler options") @@ -50,12 +54,16 @@ final case class ScalacOptions( @DirectiveName("test.options") @DirectiveName("test.scalacOption") @DirectiveName("test.scalacOptions") - testOptions: List[Positioned[String]] = Nil + testOptions: List[Positioned[String]] = Nil, + @DirectiveName("option.preset") + @DirectiveName("options.preset") + presetOptions: Option[String] = None ) extends HasBuildOptionsWithRequirements { - def buildOptionsList: List[Either[BuildException, WithBuildRequirements[BuildOptions]]] = List( - ScalacOptions.buildOptions(options).map(_.withEmptyRequirements), - ScalacOptions.buildOptions(testOptions).map(_.withScopeRequirement(Scope.Test)) - ) + def buildOptionsList: List[Either[BuildException, WithBuildRequirements[BuildOptions]]] = + List( + ScalacOptions.buildOptions(options).map(_.withEmptyRequirements), + ScalacOptions.buildOptions(testOptions).map(_.withScopeRequirement(Scope.Test)) + ) } object ScalacOptions { diff --git a/modules/options/src/main/scala/scala/build/options/HashedType.scala b/modules/options/src/main/scala/scala/build/options/HashedType.scala index 8a12804643..42abbb8ef3 100644 --- a/modules/options/src/main/scala/scala/build/options/HashedType.scala +++ b/modules/options/src/main/scala/scala/build/options/HashedType.scala @@ -47,6 +47,10 @@ object HashedType { pf => pf.repr } + implicit val presetOption: HashedType[ScalacOpt.PresetOption] = { + opt => opt.preset + } + implicit val unit: HashedType[Unit] = { _ => "" } diff --git a/modules/options/src/main/scala/scala/build/options/ScalaOptions.scala b/modules/options/src/main/scala/scala/build/options/ScalaOptions.scala index a236e36ae5..a640226f7c 100644 --- a/modules/options/src/main/scala/scala/build/options/ScalaOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/ScalaOptions.scala @@ -4,6 +4,7 @@ import dependency.AnyDependency import scala.build.Positioned import scala.build.internal.Constants +import scala.build.options.ScalacOpt.PresetOption final case class ScalaOptions( scalaVersion: Option[MaybeScalaVersion] = None, @@ -12,6 +13,7 @@ final case class ScalaOptions( addScalaCompiler: Option[Boolean] = None, semanticDbOptions: SemanticDbOptions = SemanticDbOptions(), scalacOptions: ShadowingSeq[Positioned[ScalacOpt]] = ShadowingSeq.empty, + scalacPresetOption: Option[PresetOption] = None, extraScalaVersions: Set[String] = Set.empty, compilerPlugins: Seq[Positioned[AnyDependency]] = Nil, platform: Option[Positioned[Platform]] = None, 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 ff7b0dfc5d..d5383e8497 100644 --- a/modules/options/src/main/scala/scala/build/options/ScalacOpt.scala +++ b/modules/options/src/main/scala/scala/build/options/ScalacOpt.scala @@ -63,4 +63,11 @@ object ScalacOpt { def filterScalacOptionKeys(f: String => Boolean): ShadowingSeq[ScalacOpt] = opts.filterKeys(_.key.exists(f)) } + + sealed abstract class PresetOption(val preset: String) extends Product with Serializable + object PresetOption { + case object Suggested extends PresetOption("suggested") + case object CI extends PresetOption("ci") + case object Strict extends PresetOption("strict") + } }