Skip to content

Commit 39af244

Browse files
Options validation (#406)
Validate conflicting options and track options sources Co-authored-by: Alexandre Archambault <[email protected]>
1 parent c200399 commit 39af244

32 files changed

+285
-113
lines changed

modules/build/src/main/scala/scala/build/Build.scala

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import scala.build.bloop.BloopServer
1515
import scala.build.blooprifle.{BloopRifleConfig, VersionUtil}
1616
import scala.build.errors._
1717
import scala.build.internal.{Constants, CustomCodeWrapper, MainClass, Util}
18+
import scala.build.options.validation.ValidationException
1819
import scala.build.options.{BuildOptions, ClassPathOptions, Platform, Scope}
1920
import scala.build.postprocessing._
2021
import scala.collection.mutable.ListBuffer
@@ -81,7 +82,7 @@ object Build {
8182
CrossKey(
8283
BuildOptions.CrossKey(
8384
scalaParams.scalaVersion,
84-
options.platform
85+
options.platform.value
8586
),
8687
scope
8788
)
@@ -373,6 +374,18 @@ object Build {
373374
crossBuilds = crossBuilds
374375
)
375376

377+
def validate(
378+
logger: Logger,
379+
options: BuildOptions
380+
): Either[BuildException, Unit] = {
381+
val (errors, otherDiagnostics) = options.validate.toSeq.partition(_.severity == Severity.Error)
382+
logger.log(otherDiagnostics)
383+
if (errors.nonEmpty)
384+
Left(CompositeBuildException(errors.map(new ValidationException(_))))
385+
else
386+
Right(())
387+
}
388+
376389
def watch(
377390
inputs: Inputs,
378391
options: BuildOptions,
@@ -508,7 +521,8 @@ object Build {
508521
else Seq("-sourceroot", inputs.workspace.toString)
509522

510523
val scalaJsScalacOptions =
511-
if (options.platform == Platform.JS && !params.scalaVersion.startsWith("2.")) Seq("-scalajs")
524+
if (options.platform.value == Platform.JS && !params.scalaVersion.startsWith("2."))
525+
Seq("-scalajs")
512526
else Nil
513527

514528
val bloopJvmRelease = for {
@@ -541,15 +555,18 @@ object Build {
541555
List(classesDir(inputs.workspace, inputs.projectName, Scope.Main).toNIO)
542556
else Nil
543557

558+
value(validate(logger, options))
559+
544560
val project = Project(
545561
workspace = inputs.workspace / ".scala",
546562
classesDir = classesDir0,
547563
scalaCompiler = scalaCompiler,
548564
scalaJsOptions =
549-
if (options.platform == Platform.JS) Some(options.scalaJsOptions.config)
565+
if (options.platform.value == Platform.JS) Some(options.scalaJsOptions.config)
550566
else None,
551567
scalaNativeOptions =
552-
if (options.platform == Platform.Native) Some(options.scalaNativeOptions.bloopConfig())
568+
if (options.platform.value == Platform.Native)
569+
Some(options.scalaNativeOptions.bloopConfig())
553570
else None,
554571
projectName = inputs.scopeProjectName(scope),
555572
classPath = artifacts.compileClassPath ++ mainClassesPath,
@@ -592,7 +609,7 @@ object Build {
592609
buildClient: BloopBuildClient,
593610
bloopServer: bloop.BloopServer
594611
): Either[BuildException, Build] = either {
595-
if (options.platform == Platform.Native && !value(scalaNativeSupported(options, inputs)))
612+
if (options.platform.value == Platform.Native && !value(scalaNativeSupported(options, inputs)))
596613
value(Left(new ScalaNativeCompatibilityError()))
597614
else
598615
value(Right(0))

modules/build/src/main/scala/scala/build/CrossSources.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,20 @@ final case class CrossSources(
4343
ScopedSources(
4444
paths
4545
.flatMap(_.withScalaVersion(retainedScalaVersion).toSeq)
46-
.flatMap(_.withPlatform(platform).toSeq)
46+
.flatMap(_.withPlatform(platform.value).toSeq)
4747
.map(_.scopedValue(defaultScope)),
4848
inMemory
4949
.flatMap(_.withScalaVersion(retainedScalaVersion).toSeq)
50-
.flatMap(_.withPlatform(platform).toSeq)
50+
.flatMap(_.withPlatform(platform.value).toSeq)
5151
.map(_.scopedValue(defaultScope)),
5252
mainClass,
5353
resourceDirs
5454
.flatMap(_.withScalaVersion(retainedScalaVersion).toSeq)
55-
.flatMap(_.withPlatform(platform).toSeq)
55+
.flatMap(_.withPlatform(platform.value).toSeq)
5656
.map(_.scopedValue(defaultScope)),
5757
buildOptions
5858
.flatMap(_.withScalaVersion(retainedScalaVersion).toSeq)
59-
.flatMap(_.withPlatform(platform).toSeq)
59+
.flatMap(_.withPlatform(platform.value).toSeq)
6060
.map(_.scopedValue(defaultScope))
6161
)
6262
}

modules/build/src/main/scala/scala/build/Logger.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package scala.build
33
import java.io.{OutputStream, PrintStream}
44

55
import scala.build.blooprifle.BloopRifleLogger
6-
import scala.build.errors.BuildException
6+
import scala.build.errors.{BuildException, Diagnostic}
77
import scala.scalanative.{build => sn}
88

99
trait Logger {
@@ -13,6 +13,7 @@ trait Logger {
1313
def log(s: => String, debug: => String): Unit
1414
def debug(s: => String): Unit
1515

16+
def log(diagnostics: Seq[Diagnostic]): Unit
1617
def log(ex: BuildException): Unit
1718
def exit(ex: BuildException): Nothing
1819

@@ -30,7 +31,8 @@ object Logger {
3031
def log(s: => String, debug: => String): Unit = ()
3132
def debug(s: => String): Unit = ()
3233

33-
def log(ex: BuildException): Unit = ()
34+
def log(diagnostics: Seq[Diagnostic]): Unit = ()
35+
def log(ex: BuildException): Unit = ()
3436
def exit(ex: BuildException): Nothing =
3537
throw new Exception(ex)
3638

modules/build/src/main/scala/scala/build/Position.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,5 +114,12 @@ object Position {
114114
File(path, startPos, endPos)
115115
}
116116
}
117+
final case class CommandLine() extends Position {
118+
def render(cwd: os.Path, sep: String): String = "COMMAND_LINE"
119+
}
120+
121+
final case class Custom(msg: String) extends Position {
122+
def render(cwd: os.Path, sep: String): String = msg
123+
}
117124

118125
}

modules/build/src/main/scala/scala/build/Positioned.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ object Positioned {
2626
def none[T](value: T): Positioned[T] =
2727
Positioned(Nil, value)
2828

29+
def commandLine[T](value: T): Positioned[T] =
30+
Positioned(List(Position.CommandLine()), value)
31+
2932
def sequence[T](seq: Seq[Positioned[T]]): Positioned[Seq[T]] = {
3033
val allPositions = seq.flatMap(_.positions)
3134
val value = seq.map(_.value)
@@ -42,4 +45,7 @@ object Positioned {
4245
(a, b) =>
4346
Positioned(a.positions ++ b.positions, underlying.orElse(a.value, b.value))
4447
}
48+
49+
implicit def ordering[T](implicit underlying: Ordering[T]): Ordering[Positioned[T]] =
50+
Ordering.by(_.value)
4551
}

modules/build/src/main/scala/scala/build/errors/BuildException.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ package scala.build.errors
22

33
import scala.build.Position
44

5+
case class Diagnostic(
6+
message: String,
7+
severity: Severity,
8+
positions: Seq[Position] = Nil
9+
)
10+
511
abstract class BuildException(
612
val message: String,
713
val positions: Seq[Position] = Nil,
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package scala.build.errors
2+
3+
sealed abstract class Severity extends Product with Serializable
4+
5+
object Severity {
6+
case object Error extends Severity
7+
case object Warning extends Severity
8+
}

modules/build/src/main/scala/scala/build/options/BuildOptions.scala

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import java.nio.file.Path
1010
import java.security.MessageDigest
1111

1212
import scala.build.EitherCps.{either, value}
13-
import scala.build.errors.{BuildException, InvalidBinaryScalaVersionError}
13+
import scala.build.errors.{BuildException, Diagnostic, InvalidBinaryScalaVersionError}
1414
import scala.build.internal.Constants._
1515
import scala.build.internal.{Constants, OsLibc, Util}
16-
import scala.build.{Artifacts, Logger, Os, Positioned}
16+
import scala.build.options.validation.BuildOptionsRule
17+
import scala.build.{Artifacts, Logger, Os, Position, Positioned}
1718
import scala.util.Properties
1819

1920
final case class BuildOptions(
@@ -32,11 +33,11 @@ final case class BuildOptions(
3233
replOptions: ReplOptions = ReplOptions()
3334
) {
3435

35-
lazy val platform: Platform =
36-
scalaOptions.platform.getOrElse(Platform.JVM)
36+
lazy val platform: Positioned[Platform] =
37+
scalaOptions.platform.getOrElse(Positioned(List(Position.Custom("DEFAULT")), Platform.JVM))
3738

3839
lazy val projectParams: Either[BuildException, Seq[String]] = either {
39-
val platform0 = platform match {
40+
val platform0 = platform.value match {
4041
case Platform.JVM => "JVM"
4142
case Platform.JS => "Scala.JS"
4243
case Platform.Native => "Scala Native"
@@ -46,7 +47,7 @@ final case class BuildOptions(
4647

4748
def addRunnerDependency: Option[Boolean] =
4849
internalDependencies.addRunnerDependencyOpt
49-
.orElse(if (platform == Platform.JVM) None else Some(false))
50+
.orElse(if (platform.value == Platform.JVM) None else Some(false))
5051

5152
private def scalaLibraryDependencies: Either[BuildException, Seq[AnyDependency]] = either {
5253
if (scalaOptions.addScalaLibrary.getOrElse(true)) {
@@ -62,11 +63,12 @@ final case class BuildOptions(
6263
}
6364

6465
private def maybeJsDependencies: Either[BuildException, Seq[AnyDependency]] = either {
65-
if (platform == Platform.JS) scalaJsOptions.jsDependencies(value(scalaParams).scalaVersion)
66+
if (platform.value == Platform.JS)
67+
scalaJsOptions.jsDependencies(value(scalaParams).scalaVersion)
6668
else Nil
6769
}
6870
private def maybeNativeDependencies: Seq[AnyDependency] =
69-
if (platform == Platform.Native) scalaNativeOptions.nativeDependencies
71+
if (platform.value == Platform.Native) scalaNativeOptions.nativeDependencies
7072
else Nil
7173
private def dependencies: Either[BuildException, Seq[Positioned[AnyDependency]]] = either {
7274
value(maybeJsDependencies).map(Positioned.none(_)) ++
@@ -87,11 +89,12 @@ final case class BuildOptions(
8789
}
8890

8991
private def maybeJsCompilerPlugins: Either[BuildException, Seq[AnyDependency]] = either {
90-
if (platform == Platform.JS) scalaJsOptions.compilerPlugins(value(scalaParams).scalaVersion)
92+
if (platform.value == Platform.JS)
93+
scalaJsOptions.compilerPlugins(value(scalaParams).scalaVersion)
9194
else Nil
9295
}
9396
private def maybeNativeCompilerPlugins: Seq[AnyDependency] =
94-
if (platform == Platform.Native) scalaNativeOptions.compilerPlugins
97+
if (platform.value == Platform.Native) scalaNativeOptions.compilerPlugins
9598
else Nil
9699
def compilerPlugins: Either[BuildException, Seq[Positioned[AnyDependency]]] = either {
97100
value(maybeJsCompilerPlugins).map(Positioned.none(_)) ++
@@ -108,7 +111,7 @@ final case class BuildOptions(
108111
classPathOptions.extraSourceJars.map(_.toNIO)
109112

110113
private def addJvmTestRunner: Boolean =
111-
platform == Platform.JVM &&
114+
platform.value == Platform.JVM &&
112115
internalDependencies.addTestRunnerDependency
113116
private def addJsTestBridge: Option[String] =
114117
if (internalDependencies.addTestRunnerDependency) Some(scalaJsOptions.finalVersion)
@@ -234,7 +237,7 @@ final case class BuildOptions(
234237
lazy val scalaParams: Either[BuildException, ScalaParameters] = either {
235238
val (scalaVersion, scalaBinaryVersion) =
236239
value(computeScalaVersions(scalaOptions.scalaVersion, scalaOptions.scalaBinaryVersion))
237-
val maybePlatformSuffix = platform match {
240+
val maybePlatformSuffix = platform.value match {
238241
case Platform.JVM => None
239242
case Platform.JS => Some(scalaJsOptions.platformSuffix)
240243
case Platform.Native => Some(scalaNativeOptions.platformSuffix)
@@ -265,8 +268,8 @@ final case class BuildOptions(
265268
// FIXME We'll probably need more refined rules if we start to support extra Scala.JS or Scala Native specific types
266269
def packageTypeOpt: Option[PackageType] =
267270
if (packageOptions.isDockerEnabled) Some(PackageType.Docker)
268-
else if (platform == Platform.JS) Some(PackageType.Js)
269-
else if (platform == Platform.Native) Some(PackageType.Native)
271+
else if (platform.value == Platform.JS) Some(PackageType.Js)
272+
else if (platform.value == Platform.Native) Some(PackageType.Native)
270273
else packageOptions.packageTypeOpt
271274

272275
private def allCrossScalaVersionOptions: Seq[BuildOptions] = {
@@ -293,12 +296,11 @@ final case class BuildOptions(
293296
val sortedExtraPlatforms = scalaOptions0
294297
.extraPlatforms
295298
.toVector
296-
.sorted
297-
this +: sortedExtraPlatforms.map { pf =>
299+
this +: sortedExtraPlatforms.map { case (pf, pos) =>
298300
copy(
299301
scalaOptions = scalaOptions0.copy(
300-
platform = Some(pf),
301-
extraPlatforms = Set.empty
302+
platform = Some(Positioned(pos.positions, pf)),
303+
extraPlatforms = Map.empty
302304
)
303305
)
304306
}
@@ -319,9 +321,9 @@ final case class BuildOptions(
319321
private def normalize: BuildOptions = {
320322
var opt = this
321323

322-
if (platform != Platform.JS)
324+
if (platform.value != Platform.JS)
323325
opt = opt.clearJsOptions
324-
if (platform != Platform.Native)
326+
if (platform.value != Platform.Native)
325327
opt = opt.clearNativeOptions
326328

327329
opt.copy(
@@ -357,6 +359,8 @@ final case class BuildOptions(
357359

358360
def orElse(other: BuildOptions): BuildOptions =
359361
BuildOptions.monoid.orElse(this, other)
362+
363+
def validate: Seq[Diagnostic] = BuildOptionsRule.validateAll(this)
360364
}
361365

362366
object BuildOptions {

modules/build/src/main/scala/scala/build/options/ConfigMonoid.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,23 @@ object ConfigMonoid {
8181
main ++ defaults
8282
}
8383

84+
implicit def map[K, V](implicit valueMonoid: ConfigMonoid[V]): ConfigMonoid[Map[K, V]] =
85+
instance(Map.empty[K, V]) {
86+
(main, defaults) =>
87+
(main.keySet ++ defaults.keySet).map {
88+
key =>
89+
val mainVal = main.getOrElse(key, valueMonoid.zero)
90+
val defaultsVal = defaults.getOrElse(key, valueMonoid.zero)
91+
key -> valueMonoid.orElse(mainVal, defaultsVal)
92+
}.toMap
93+
}
94+
8495
implicit val boolean: ConfigMonoid[Boolean] = instance(false) {
8596
(main, defaults) =>
8697
main || defaults
8798
}
8899

100+
implicit val unit: ConfigMonoid[Unit] = instance(()) {
101+
(_, _) => ()
102+
}
89103
}

modules/build/src/main/scala/scala/build/options/HashedField.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ object HashedField extends LowPriorityHashedField {
4141
update(s"$name+=${hasher.value.hashedValue(t)}")
4242
}
4343

44+
implicit def map[K, V](
45+
implicit
46+
hasherK: Lazy[HashedType[K]],
47+
hasherV: Lazy[HashedType[V]],
48+
ordering: Ordering[K]
49+
): HashedField[Map[K, V]] = {
50+
(name, map0, update) =>
51+
for ((k, v) <- map0.toVector.sortBy(_._1)(ordering))
52+
update(s"$name+=${hasherK.value.hashedValue(k)}${hasherV.value.hashedValue(v)}")
53+
}
54+
4455
}
4556

4657
abstract class LowPriorityHashedField {

0 commit comments

Comments
 (0)