From 3692db906832bbc7e00c4b05e966a8b9f3fbe7c8 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Tue, 27 Mar 2018 01:36:41 +0100 Subject: [PATCH] Give Scoped a default equals/hashCode implementation --- build.sbt | 4 + .../src/main/scala/sbt/Structure.scala | 19 ++- .../scala/sbt/BuildSettingsInstances.scala | 10 +- .../src/test/scala/sbt/ScopedSpec.scala | 145 ++++++++++++++++++ project/plugins.sbt | 1 + 5 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 main-settings/src/test/scala/sbt/ScopedSpec.scala diff --git a/build.sbt b/build.sbt index c5e115e31..eeb77984a 100644 --- a/build.sbt +++ b/build.sbt @@ -432,6 +432,10 @@ lazy val mainSettingsProj = (project in file("main-settings")) mimaSettings, mimaBinaryIssueFilters ++= Seq( exclude[DirectMissingMethodProblem]("sbt.Scope.display012StyleMasked"), + + // added a method to a sealed trait + exclude[InheritedNewAbstractMethodProblem]("sbt.Scoped.canEqual"), + exclude[InheritedNewAbstractMethodProblem]("sbt.ScopedTaskable.canEqual"), ), ) .configure( diff --git a/main-settings/src/main/scala/sbt/Structure.scala b/main-settings/src/main/scala/sbt/Structure.scala index 36955839d..9bb02bc02 100644 --- a/main-settings/src/main/scala/sbt/Structure.scala +++ b/main-settings/src/main/scala/sbt/Structure.scala @@ -17,7 +17,18 @@ import sbt.Def.{ Initialize, KeyedInitialize, ScopedKey, Setting, setting } import std.TaskExtra.{ task => mktask, _ } /** An abstraction on top of Settings for build configuration and task definition. */ -sealed trait Scoped { def scope: Scope; val key: AttributeKey[_] } +sealed trait Scoped extends Equals { + def scope: Scope + val key: AttributeKey[_] + + override def equals(that: Any) = + (this eq that.asInstanceOf[AnyRef]) || (that match { + case that: Scoped => scope == that.scope && key == that.key && canEqual(that) + case _ => false + }) + + override def hashCode() = (scope, key).## +} /** A common type for SettingKey and TaskKey so that both can be used as inputs to tasks.*/ sealed trait ScopedTaskable[T] extends Scoped { @@ -95,6 +106,8 @@ sealed abstract class SettingKey[T] final def withRank(rank: Int): SettingKey[T] = SettingKey(AttributeKey.copyWithRank(key, rank)) + + def canEqual(that: Any): Boolean = that.isInstanceOf[SettingKey[_]] } /** @@ -163,6 +176,8 @@ sealed abstract class TaskKey[T] final def withRank(rank: Int): TaskKey[T] = TaskKey(AttributeKey.copyWithRank(key, rank)) + + def canEqual(that: Any): Boolean = that.isInstanceOf[TaskKey[_]] } /** @@ -195,6 +210,8 @@ sealed trait InputKey[T] final def withRank(rank: Int): InputKey[T] = InputKey(AttributeKey.copyWithRank(key, rank)) + + def canEqual(that: Any): Boolean = that.isInstanceOf[InputKey[_]] } /** Methods and types related to constructing settings, including keys, scopes, and initializations. */ diff --git a/main-settings/src/test/scala/sbt/BuildSettingsInstances.scala b/main-settings/src/test/scala/sbt/BuildSettingsInstances.scala index 6ca193ccf..3618fb841 100644 --- a/main-settings/src/test/scala/sbt/BuildSettingsInstances.scala +++ b/main-settings/src/test/scala/sbt/BuildSettingsInstances.scala @@ -92,9 +92,13 @@ object BuildSettingsInstances { type Key = K forSome { type K <: Scoped.ScopingSetting[K] with Scoped } - def genInputKey[A: Manifest]: Gen[InputKey[A]] = Gen.identifier map (InputKey[A](_)) - def genSettingKey[A: Manifest]: Gen[SettingKey[A]] = Gen.identifier map (SettingKey[A](_)) - def genTaskKey[A: Manifest]: Gen[TaskKey[A]] = Gen.identifier map (TaskKey[A](_)) + final case class Label(value: String) + val genLabel: Gen[Label] = Gen.identifier map Label + implicit def arbLabel: Arbitrary[Label] = Arbitrary(genLabel) + + def genInputKey[A: Manifest]: Gen[InputKey[A]] = genLabel map (x => InputKey[A](x.value)) + def genSettingKey[A: Manifest]: Gen[SettingKey[A]] = genLabel map (x => SettingKey[A](x.value)) + def genTaskKey[A: Manifest]: Gen[TaskKey[A]] = genLabel map (x => TaskKey[A](x.value)) def withScope[K <: Scoped.ScopingSetting[K]](keyGen: Gen[K]): Arbitrary[K] = Arbitrary { Gen.frequency( diff --git a/main-settings/src/test/scala/sbt/ScopedSpec.scala b/main-settings/src/test/scala/sbt/ScopedSpec.scala new file mode 100644 index 000000000..5992403b0 --- /dev/null +++ b/main-settings/src/test/scala/sbt/ScopedSpec.scala @@ -0,0 +1,145 @@ +/* + * sbt + * Copyright 2011 - 2017, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under BSD-3-Clause license (see LICENSE) + */ + +package sbt.test + +import org.scalacheck._, Prop._, util.Pretty + +import sbt.internal.util.AttributeKey +import sbt.util.NoJsonWriter +import sbt.{ InputTask, Scope, Task } +import sbt.{ InputKey, Scoped, SettingKey, TaskKey } + +import BuildSettingsInstances._ + +object ScopedSpec extends Properties("Scoped") { + val intManifest = manifest[Int] + val stringManifest = manifest[String] + + implicit val arbManifest: Arbitrary[Manifest[_]] = + Arbitrary(Gen.oneOf(intManifest, stringManifest)) + + property("setting keys are structurally equal") = { + forAll { (label: Label, manifest: Manifest[_], scope: Scope) => + val k1 = settingKey(label, manifest, scope) + val k2 = settingKey(label, manifest, scope) + expectEq(k1, k2) + } + } + + property("task keys are structurally equal") = { + forAll { (label: Label, manifest: Manifest[_], scope: Scope) => + val k1 = taskKey(label, manifest, scope) + val k2 = taskKey(label, manifest, scope) + expectEq(k1, k2) + } + } + + property("input keys are structurally equal") = { + forAll { (label: Label, manifest: Manifest[_], scope: Scope) => + val k1 = inputKey(label, manifest, scope) + val k2 = inputKey(label, manifest, scope) + expectEq(k1, k2) + } + } + + property("different key types are not equal") = { + forAll { (label: Label, manifest: Manifest[_], scope: Scope) => + val settingKey1 = settingKey(label, manifest, scope) + val taskKey1 = taskKey(label, manifest, scope) + val inputKey1 = inputKey(label, manifest, scope) + + all( + expectNe(settingKey1, taskKey1), + expectNe(settingKey1, inputKey1), + expectNe(taskKey1, inputKey1), + ) + } + } + + property("different key types, with the same manifest, are not equal") = { + forAll { (label: Label, scope: Scope) => + val prop1 = { + val manifest1 = manifest[Task[String]] + val attrKey = attributeKey(label, manifest1) + val k1 = SettingKey(attrKey) in scope + val k2 = TaskKey(attrKey) in scope + expectNeSameManifest(k1, k2) + } + + val prop2 = { + val manifest1 = manifest[InputTask[String]] + val attrKey = attributeKey(label, manifest1) + val k1 = SettingKey(attrKey) in scope + val k2 = InputKey(attrKey) in scope + expectNeSameManifest(k1, k2) + } + + all(prop1, prop2) + } + } + + /// + + def settingKey[A](label: Label, manifest: Manifest[A], scope: Scope): SettingKey[A] = { + val noJsonWriter = NoJsonWriter[A]() + SettingKey[A](label.value)(manifest, noJsonWriter) in scope + } + + def taskKey[A](label: Label, manifest: Manifest[A], s: Scope): TaskKey[A] = + TaskKey[A](label.value)(manifest) in s + + def inputKey[A](label: Label, manifest: Manifest[A], scope: Scope): InputKey[A] = + InputKey[A](label.value)(manifest) in scope + + def attributeKey[A](label: Label, manifest: Manifest[A]): AttributeKey[A] = { + val jsonWriter = NoJsonWriter[A]() + AttributeKey[A](label.value)(manifest, jsonWriter) + } + + /// + + def expectEq(k1: Scoped, k2: Scoped): Prop = + ?=(k1, k2) && ?=(k2, k1) map eqLabels(k1, k2) + + def expectNe(k1: Scoped, k2: Scoped): Prop = + !=(k1, k2) && !=(k2, k1) map eqLabels(k1, k2) + + def expectNeSameManifest(k1: Scoped, k2: Scoped) = { + all( + ?=(k1.key.manifest, k2.key.manifest), // sanity check the manifests are the same + expectNe(k1, k2), + ) + } + + def eqLabels(k1: Scoped, k2: Scoped): Prop.Result => Prop.Result = r => { + val eqLabel = k1.key.label == k2.key.label + val eqManifest = k1.key.manifest == k2.key.manifest + val eqScope = k1.scope == k2.scope + r.label(s"label equality: ${k1.key.label} == ${k2.key.label} : $eqLabel") + .label(s"manifest equality: ${k1.key.manifest} == ${k2.key.manifest} : $eqManifest") + .label(s"scope equality: ${k1.scope} == ${k2.scope} : $eqScope") + } + + def ?=[T](x: T, y: T)(implicit pp: T => Pretty): Prop = + if (x == y) proved + else + falsified :| { + val act = Pretty.pretty[T](x, Pretty.Params(0)) + val exp = Pretty.pretty[T](y, Pretty.Params(0)) + s"Expected $act to be equal to $exp" + } + + def !=[T](x: T, y: T)(implicit pp: T => Pretty): Prop = + if (x == y) falsified + else + proved :| { + val act = Pretty.pretty[T](x, Pretty.Params(0)) + val exp = Pretty.pretty[T](y, Pretty.Params(0)) + s"Expected $act to NOT be equal to $exp" + } +} diff --git a/project/plugins.sbt b/project/plugins.sbt index f2509a98e..0352032f5 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,7 @@ scalaVersion := "2.12.4" scalacOptions ++= Seq("-feature", "-language:postfixOps") +addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.2.0") addSbtPlugin("org.scala-sbt" % "sbt-houserules" % "0.3.5") addSbtPlugin("org.scala-sbt" % "sbt-contraband" % "0.3.2") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "3.0.2")