diff --git a/build.sbt b/build.sbt index b9e606583..ef93194c9 100644 --- a/build.sbt +++ b/build.sbt @@ -24,6 +24,7 @@ val scalaCheckVersion = "1.19.0" val scalazVersion = "7.3.9" val scodecVersion = "1.11.11" val scoptVersion = "4.1.0" +val hearthVersion = "0.4.1" def macroParadise(configuration: Configuration): Def.Initialize[Seq[ModuleID]] = Def.setting { @@ -191,7 +192,10 @@ lazy val core = myCrossProject("core") libraryDependencies ++= macroParadise(Compile).value ++ ( if (isScala3Setting.value) - Seq() + Seq( + "com.kubuszok" %%% "hearth" % hearthVersion, + "com.kubuszok" % "hearth-cross-quotes_3" % hearthVersion % Provided + ) else Seq( scalaOrganization.value % "scala-reflect" % scalaVersion.value, @@ -205,6 +209,21 @@ lazy val core = myCrossProject("core") ) ++ Seq( "org.scalacheck" %%% "scalacheck" % scalaCheckVersion % Test ), + // On Scala 3 the macros use Hearth's cross-quotes DSL (`Expr.quote`/`Expr.splice`/`Expr.upcast`), + // which is desugared by the `hearth-cross-quotes` *compiler plugin*. The plugin is published for + // JVM only and reused across all platforms, so it is pulled in as a `Provided` dependency (see + // above) and wired into the compiler here via `-Xplugin`, resolved from the compile classpath. + scalacOptions ++= { + if (isScala3Setting.value) + Seq( + "-Xplugin:" + (Compile / dependencyClasspath).value + .map(_.data.getAbsolutePath) + .find(_.contains("hearth-cross-quotes")) + .getOrElse(sys.error("hearth-cross-quotes jar not found on classpath")) + ) + else + Seq.empty + }, buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion), buildInfoPackage := s"$rootPkg.internal" ) diff --git a/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/api/RefType.scala b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/api/RefType.scala index e45f1de17..508c9d442 100644 --- a/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/api/RefType.scala +++ b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/api/RefType.scala @@ -40,6 +40,23 @@ trait RefType[F[_, _]] extends Serializable { def refine[P]: RefinePartiallyApplied[F, P] = new RefinePartiallyApplied(this) + /** + * Macro that returns a value of type `T` refined as `F[T, P]` if it + * satisfies the predicate `P`, or fails to compile otherwise. + * + * Example: {{{ + * scala> import eu.timepit.refined.api.{ Refined, RefType } + * | import eu.timepit.refined.numeric.Positive + * + * scala> RefType[Refined].refineM[Positive](10) + * res0: Refined[Int, Positive] = 10 + * }}} + * + * Note: `M` stands for '''m'''acro. + */ + def refineM[P]: RefineMPartiallyApplied[F, P] = + new RefineMPartiallyApplied + def mapRefine[T, P, U]( tp: F[T, P] )(f: T => U)(implicit v: Validate[U, P]): Either[String, F[U, P]] = @@ -77,6 +94,24 @@ object RefType { def applyRef[FTP]: ApplyRefPartiallyApplied[FTP] = new ApplyRefPartiallyApplied + /** + * Macro that returns a value of type `T` refined as `FTP` if it + * satisfies the predicate in `FTP`, or fails to compile otherwise. + * + * Example: {{{ + * scala> import eu.timepit.refined.api.{ Refined, RefType } + * | import eu.timepit.refined.numeric.Positive + * + * scala> type PosInt = Int Refined Positive + * scala> RefType.applyRefM[PosInt](10) + * res0: PosInt = 10 + * }}} + * + * Note: `M` stands for '''m'''acro. + */ + def applyRefM[FTP]: ApplyRefMPartiallyApplied[FTP] = + new ApplyRefMPartiallyApplied + implicit val refinedRefType: RefType[Refined] = new RefType[Refined] { override def unsafeWrap[T, P](t: T): Refined[T, P] = diff --git a/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/api/RefinedTypeOps.scala b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/api/RefinedTypeOps.scala index a53ba7c4d..a6df8a23f 100644 --- a/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/api/RefinedTypeOps.scala +++ b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/api/RefinedTypeOps.scala @@ -1,5 +1,7 @@ package eu.timepit.refined.api +import eu.timepit.refined.macros.RefinedTypeOpsM + /** * Provides functions to create values of the refined type `FTP` from * values of the base type `T`. It is intended to simplify the definition @@ -20,7 +22,9 @@ package eu.timepit.refined.api * res1: PosInt = 2 * }}} */ -class RefinedTypeOps[FTP, T](implicit rt: RefinedType.AuxT[FTP, T]) extends Serializable { +class RefinedTypeOps[FTP, T](implicit rt: RefinedType.AuxT[FTP, T]) + extends RefinedTypeOpsM[FTP, T] + with Serializable { def from(t: T): Either[String, FTP] = rt.refine(t) diff --git a/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/auto.scala b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/auto.scala index 9dd8c13d7..ee5b5a0a9 100644 --- a/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/auto.scala +++ b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/auto.scala @@ -1,33 +1,25 @@ package eu.timepit.refined -import eu.timepit.refined.api.RefType +import eu.timepit.refined.api.{Inference, RefType, Refined, Validate} +import eu.timepit.refined.macros.Macros + +import scala.language.implicitConversions +import scala.quoted.* -/** - * Module that provides automatic refinements and automatic conversions - * between refined types (refinement subtyping) at compile-time. - */ object auto { - /** - * Implicitly unwraps the `T` from a value of type `F[T, P]` using the - * `[[api.RefType]]` instance of `F`. This allows a `F[T, P]` to be - * used as it were a subtype of `T`. - * - * Example: {{{ - * scala> import eu.timepit.refined.auto.autoUnwrap - * | import eu.timepit.refined.types.numeric.PosInt - * - * scala> def plusOne(i: Int): Int = i + 1 - * | val x = PosInt.unsafeFrom(42) - * - * // converts x implicitly to an Int: - * scala> plusOne(x) - * res0: Int = 43 - * }}} - * - * Note: This conversion is not needed if `F[T, _] <: T` holds (which - * is the case for `shapeless.tag.@@`, for example). - */ + implicit inline def autoRefineV[T, P](inline t: T)(implicit + inline v: Validate[T, P] + ): Refined[T, P] = ${ + Macros.autoRefineV[T, P]('t, 'v) + } + + implicit inline def autoInfer[T, A, B](inline ta: Refined[T, A])(implicit + inline ir: Inference[A, B] + ): Refined[T, B] = ${ + Macros.autoInfer[T, A, B]('ta, 'ir) + } + implicit def autoUnwrap[F[_, _], T, P](tp: F[T, P])(implicit rt: RefType[F]): T = rt.unwrap(tp) } diff --git a/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/boolean.scala b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/boolean.scala index 2f3529fbc..a6b5697f6 100644 --- a/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/boolean.scala +++ b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/boolean.scala @@ -286,7 +286,16 @@ private[refined] trait BooleanInference2 extends BooleanInference3 { implicit def conjunctionEliminationL[A, B, C](implicit p1: A ==> C): (A And B) ==> C = p1.adapt("conjunctionEliminationL(%s)") - implicit def hypotheticalSyllogism[A, B, C](implicit p1: A ==> B, p2: B ==> C): A ==> C = + // NOTE: `hypotheticalSyllogism` (transitivity: `A ==> B`, `B ==> C` ⟹ `A ==> C`) from the Scala 2 + // sources is intentionally omitted here. Its intermediate `B` appears only in the premises, never + // the conclusion, so resolving a goal through it spawns a free-RHS subgoal `A ==> ?B` that unifies + // with several always-valid rules at once (`minimalTautology`, `disjunctionIntroduction{L,R}`, ...), + // which Scala 3's implicit search reports as an ambiguity that aborts the whole search — including + // otherwise-derivable goals such as `Size[Interval.Closed[1, n]] ==> NonEmpty`. Dropping it keeps the + // common single-step and conjunction-elimination inferences working reliably; the price is that + // purely transitive two-hop chains (e.g. `Last[P] ==> NonEmpty`, via `Exists[P]`) are not derived. + // kept non implicit version for bin-compat + def hypotheticalSyllogism[A, B, C](implicit p1: A ==> B, p2: B ==> C): A ==> C = Inference.combine(p1, p2, "hypotheticalSyllogism(%s, %s)") } diff --git a/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/internal/ApplyRefMPartiallyApplied.scala b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/internal/ApplyRefMPartiallyApplied.scala new file mode 100644 index 000000000..69b02c617 --- /dev/null +++ b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/internal/ApplyRefMPartiallyApplied.scala @@ -0,0 +1,20 @@ +package eu.timepit.refined.internal + +import eu.timepit.refined.api.{Refined, Validate} +import eu.timepit.refined.macros.Macros + +/** + * Helper class that allows the types `T` and `P` to be inferred from calls + * like `[[api.RefType.applyRefM]][F[T, P]](t)`. + * + * See [[http://tpolecat.github.io/2015/07/30/infer.html]] for a detailed + * explanation of this trick. + */ +final class ApplyRefMPartiallyApplied[FTP] { + + inline def apply[T, P](inline t: T)(implicit + inline ev: Refined[T, P] =:= FTP, + inline v: Validate[T, P] + ): FTP = + ${ Macros.applyRef[FTP, T, P]('t, 'v) } +} diff --git a/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/internal/RefineMPartiallyApplied.scala b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/internal/RefineMPartiallyApplied.scala new file mode 100644 index 000000000..b62e88db3 --- /dev/null +++ b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/internal/RefineMPartiallyApplied.scala @@ -0,0 +1,24 @@ +package eu.timepit.refined.internal + +import eu.timepit.refined.api.{RefType, Validate} +import eu.timepit.refined.macros.Macros + +/** + * Helper class that allows the type `T` to be inferred from calls like + * `[[api.RefType.refineM]][P](t)`. + * + * See [[http://tpolecat.github.io/2015/07/30/infer.html]] for a detailed + * explanation of this trick. + */ +final class RefineMPartiallyApplied[F[_, _], P] { + + // The macro only validates `t` against `P` at compile time (returning `t`); wrapping into `F[T, P]` + // is the zero-cost runtime `unsafeWrap`. This keeps the macro carrier-agnostic, so no higher-kinded + // macro over `F` is needed. `apply` is inline (not itself a macro) so it may combine the macro call + // with `unsafeWrap` — a macro's splice must be the entire right-hand side, which `validated` is. + inline def apply[T](inline t: T)(implicit rt: RefType[F], inline v: Validate[T, P]): F[T, P] = + rt.unsafeWrap[T, P](validated[T](t)) + + private inline def validated[T](inline t: T)(implicit inline v: Validate[T, P]): T = + ${ Macros.refineM[T, P]('t, 'v) } +} diff --git a/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/internal/WitnessAs.scala b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/internal/WitnessAs.scala index c493042d6..c29b9e6d4 100644 --- a/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/internal/WitnessAs.scala +++ b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/internal/WitnessAs.scala @@ -1,6 +1,6 @@ package eu.timepit.refined.internal -import scala.compiletime.{constValue, error} +import scala.compiletime.{constValue, error, summonFrom, summonInline} /** * `WitnessAs[A, B]` provides the singleton value of type `A` in `fst` @@ -35,9 +35,18 @@ object WitnessAs extends WitnessAs1 { ): WitnessAs[A, B] = WitnessAs(wa.value, nb.fromInt(ta.apply())) - inline given singletonWitnessAs[B, A <: B]: WitnessAs[A, B] = { - inline val a = constValue[A] - WitnessAs(a, a) + // Route by whether the base type `B` is a singleton (i.e. has a `ValueOf`): + // - object singletons (`B = Foo.type`) can't be witnessed by `constValue` ("not a constant type"), + // so use `ValueOf` — exercised only at runtime (`isValid`), matching Scala 2's `Equal[Foo.type]`; + // - everything else (`Int`, `Char`, `String`, ... literal witnesses) uses `constValue`, which the + // compile-time macros' `semiEval` reduces natively as a plain literal. + inline given singletonWitnessAs[B, A <: B]: WitnessAs[A, B] = summonFrom { + case _: ValueOf[B] => + val v = summonInline[ValueOf[A]] + WitnessAs[A, B](v.value, v.value) + case _ => + inline val a = constValue[A] + WitnessAs(a, a) } } diff --git a/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/macros/Macros.scala b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/macros/Macros.scala new file mode 100644 index 000000000..fec79afac --- /dev/null +++ b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/macros/Macros.scala @@ -0,0 +1,41 @@ +package eu.timepit.refined.macros + +import hearth.* +import eu.timepit.refined.api.{Inference, Refined, Validate} + +import scala.quoted.* + +private[refined] class RefinedMacros(q: Quotes) extends MacroCommonsScala3(using q), RefinedMacro + +private[refined] object Macros { + + def autoRefineV[T: Type, P: Type]( + t: Expr[T], + v: Expr[Validate[T, P]] + )(using q: Quotes): Expr[Refined[T, P]] = + new RefinedMacros(q).autoRefineImpl[T, P](t, v) + + def autoInfer[T: Type, A: Type, B: Type]( + ta: Expr[Refined[T, A]], + ir: Expr[Inference[A, B]] + )(using q: Quotes): Expr[Refined[T, B]] = + new RefinedMacros(q).autoInferImpl[T, A, B](ta, ir) + + def refineMV[T: Type, P: Type]( + t: Expr[T], + v: Expr[Validate[T, P]] + )(using q: Quotes): Expr[Refined[T, P]] = + new RefinedMacros(q).refineMVImpl[T, P](t, v) + + def refineM[T: Type, P: Type]( + t: Expr[T], + v: Expr[Validate[T, P]] + )(using q: Quotes): Expr[T] = + new RefinedMacros(q).refineMImpl[T, P](t, v) + + def applyRef[FTP: Type, T: Type, P: Type]( + t: Expr[T], + v: Expr[Validate[T, P]] + )(using q: Quotes): Expr[FTP] = + new RefinedMacros(q).applyRefImpl[FTP, T, P](t, v) +} diff --git a/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/macros/RefinedMacro.scala b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/macros/RefinedMacro.scala new file mode 100644 index 000000000..41d9ac7a4 --- /dev/null +++ b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/macros/RefinedMacro.scala @@ -0,0 +1,84 @@ +package eu.timepit.refined.macros + +import hearth.* +import eu.timepit.refined.api.{Inference, RefType, Refined, Validate} + +trait RefinedMacro { this: MacroCommons => + + def autoRefineImpl[T: Type, P: Type]( + t: Expr[T], + v: Expr[Validate[T, P]] + ): Expr[Refined[T, P]] = { + validateAtCompileTime(t, v) + Expr.quote(Refined.unsafeApply[T, P](Expr.splice(t))) + } + + def autoInferImpl[T: Type, A: Type, B: Type]( + ta: Expr[Refined[T, A]], + ir: Expr[Inference[A, B]] + ): Expr[Refined[T, B]] = { + val inference = ir.semiEval match { + case Right(value) => value + case Left(errors) => + Environment.reportErrorAndAbort( + s"Cannot evaluate Inference[${Type[A].plainPrint}, ${Type[B].plainPrint}] at compile time: ${errors.mkString(", ")}. " + ) + } + if (!inference.isValid) + Environment.reportErrorAndAbort( + s"Inference failed: ${inference.show}" + ) + Expr.quote(Refined.unsafeApply[T, B](Expr.splice(ta).value)) + } + + def refineMVImpl[T: Type, P: Type]( + t: Expr[T], + v: Expr[Validate[T, P]] + ): Expr[Refined[T, P]] = autoRefineImpl[T, P](t, v) + + /** + * Validates `t` against `P` at compile time and returns `t` unchanged. Used by the carrier-generic + * `RefType.refineM`, whose wrapping into `F[T, P]` is done by the (zero-cost) runtime `unsafeWrap`, + * so no higher-kinded macro over `F` is needed. + */ + def refineMImpl[T: Type, P: Type]( + t: Expr[T], + v: Expr[Validate[T, P]] + ): Expr[T] = { + validateAtCompileTime(t, v) + t + } + + def applyRefImpl[FTP: Type, T: Type, P: Type]( + t: Expr[T], + v: Expr[Validate[T, P]] + ): Expr[FTP] = { + validateAtCompileTime(t, v) + val refined: Expr[Refined[T, P]] = Expr.quote(Refined.unsafeApply[T, P](Expr.splice(t))) + Expr.upcast[Refined[T, P], FTP](refined)(using Type.of[Refined[T, P]], Type[FTP]) + } + + private def validateAtCompileTime[T: Type, P: Type]( + t: Expr[T], + v: Expr[Validate[T, P]] + ): Unit = { + val tValue = t.semiEval match { + case Right(value) => value + case Left(errors) => + Environment.reportErrorAndAbort( + s"Cannot evaluate expression at compile time: ${errors.mkString(", ")}" + ) + } + val validate = v.semiEval match { + case Right(value) => value + case Left(errors) => + Environment.reportErrorAndAbort( + s"Cannot evaluate Validate[${Type[T].plainPrint}, ${Type[P].plainPrint}] at compile time: ${errors.mkString(", ")}. " + + s"Use refineV for runtime validation instead." + ) + } + val result = validate.validate(tValue) + if (!result.isPassed) + Environment.reportErrorAndAbort(s"Predicate failed: ${validate.showResult(tValue, result)}") + } +} diff --git a/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/macros/RefinedTypeOpsM.scala b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/macros/RefinedTypeOpsM.scala new file mode 100644 index 000000000..74715705d --- /dev/null +++ b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/macros/RefinedTypeOpsM.scala @@ -0,0 +1,14 @@ +package eu.timepit.refined.macros + +import hearth.* +import eu.timepit.refined.api.{Refined, Validate} + +import scala.quoted.* + +trait RefinedTypeOpsM[FTP, T] { + + inline def apply[P]( + inline t: T + )(implicit inline ev: Refined[T, P] =:= FTP, inline v: Validate[T, P]): FTP = + ${ Macros.applyRef[FTP, T, P]('t, 'v) } +} diff --git a/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/package.scala b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/package.scala index 4ac4be621..24ebe9e7a 100644 --- a/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/package.scala +++ b/modules/core/shared/src/main/scala-3.0+/eu/timepit/refined/package.scala @@ -1,7 +1,8 @@ package eu.timepit -import eu.timepit.refined.api.{Refined, RefType} +import eu.timepit.refined.api.{RefType, Refined, Validate} import eu.timepit.refined.internal._ +import eu.timepit.refined.macros.Macros package object refined { @@ -12,4 +13,13 @@ package object refined { * Note: `V` stands for '''v'''alue class. */ def refineV[P]: RefinePartiallyApplied[Refined, P] = RefType.refinedRefType.refine[P] + + inline def refineMV[P]: RefineMVBuilder[P] = new RefineMVBuilder[P] + + final class RefineMVBuilder[P] { + + inline def apply[T](inline t: T)(implicit inline v: Validate[T, P]): Refined[T, P] = + ${ Macros.refineMV[T, P]('t, 'v) } + } + } diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/AutoMacrosSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/AutoMacrosSpec.scala new file mode 100644 index 000000000..f792de331 --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/AutoMacrosSpec.scala @@ -0,0 +1,38 @@ +package eu.timepit.refined + +import eu.timepit.refined.api.Refined +import eu.timepit.refined.auto._ +import eu.timepit.refined.char.{Digit, Letter} +import eu.timepit.refined.generic._ +import eu.timepit.refined.types.numeric.PosInt +import org.scalacheck.Prop._ +import org.scalacheck.Properties +import eu.timepit.refined.test.ScalaVersionSpecific.illTyped + +class AutoMacrosSpec extends Properties("auto.macros") { + + property("autoInfer") = secure { + val a: Char Refined Equal['0'] = '0' + val b: Char Refined Digit = a + // Self-contained snippet that stays in auto mode: the inner `val` is built by `autoRefineV`, and + // the missing `Inference[Equal['0'], Letter]` makes the `autoInfer` conversion on the outer `val` + // inapplicable, surfacing as a type mismatch against the required type. (The source value is + // defined inside the snippet block because `typeCheckErrors` cannot see the enclosing `a`.) + illTyped( + "{ val a0: Char Refined Equal['0'] = '0'; val c: Char Refined Letter = a0 }", + "Required: Char Refined .*Letter" + ) + a == b + } + + property("autoRefineV") = secure { + val a: Char Refined Equal['0'] = '0' + illTyped("val b: Char Refined Equal['0'] = '1'", "Required: Char Refined .*Equal") + a.value == '0' + } + + property("#260") = secure { + val somePosInt: Option[PosInt] = Some(5) + somePosInt.isDefined + } +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/BooleanInferenceSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/BooleanInferenceSpec.scala new file mode 100644 index 000000000..06b0bbbf9 --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/BooleanInferenceSpec.scala @@ -0,0 +1,174 @@ +package eu.timepit.refined + +import eu.timepit.refined.TestUtils.wellTyped +import eu.timepit.refined.api.Inference +import eu.timepit.refined.boolean._ +import eu.timepit.refined.char.{Digit, Letter, UpperCase, Whitespace} +import eu.timepit.refined.numeric._ +import eu.timepit.refined.string._ +import eu.timepit.refined.collection._ +import eu.timepit.refined.generic.Equal +import eu.timepit.refined.char.LetterOrDigit +import org.scalacheck.Prop._ +import org.scalacheck.Properties +import eu.timepit.refined.test.ScalaVersionSpecific.illTyped + +class BooleanInferenceSpec extends Properties("BooleanInference") { + + property("double negation elimination with Greater") = secure { + // Scala 3 names the numeric rule `greaterInferenceInt` (Scala 2 uses `greaterInference`). + Inference[Not[Not[Greater[5]]], Greater[4]] ?= + Inference(5 > 4, "doubleNegationElimination(greaterInferenceInt(5, 4))") + } + + property("double negation elimination") = secure { + Inference[Not[Not[UpperCase]], UpperCase].isValid + } + + property("substitution in conjunction") = secure { + Inference[NonEmpty And ValidLong, NonEmpty And (ValidLong Or ValidDouble)].isValid + } + + property("elimination of tautology in disjunction") = secure { + Inference[(NonEmpty And ValidLong) Or (NonEmpty And ValidDouble), NonEmpty].isValid + } + + property("double negation elimination 2x") = secure { + Inference[Not[Not[Not[Not[UpperCase]]]], UpperCase].isValid + } + + property("double negation elimination 3x") = secure { + Inference[Not[Not[Not[Not[Not[Not[UpperCase]]]]]], UpperCase].isValid + } + + property("double negation elimination 4x") = secure { + Inference[Not[Not[Not[Not[Not[Not[Not[Not[UpperCase]]]]]]]], UpperCase].isValid + } + + property("double negation introduction with Greater") = secure { + Inference[Greater[5], Not[Not[Greater[4]]]].isValid + } + + property("double negation introduction") = secure { + Inference[UpperCase, Not[Not[UpperCase]]].isValid + } + + property("double negation introduction 2x") = secure { + Inference[UpperCase, Not[Not[Not[Not[UpperCase]]]]].isValid + } + + property("conjunction associativity") = secure { + Inference[ + (UpperCase And Letter) And Not[Whitespace], + UpperCase And + (Letter And + Not[ + Whitespace + ]) + ].isValid + } + + property("conjunction commutativity") = secure { + Inference[UpperCase And Letter, Letter And UpperCase].isValid + } + + property("conjunction elimination left") = secure { + Inference[UpperCase And Letter, UpperCase].isValid + } + + property("conjunction elimination right") = secure { + Inference[Letter And UpperCase, UpperCase].isValid + } + + property("complex conjunction elimination") = secure { + type BaseRefinement = And[Size[Equal[10]], Forall[LetterOrDigit]] + type ConcreteRefinement = And[StartsWith["001"], BaseRefinement] + + Inference[ConcreteRefinement, BaseRefinement].isValid + } + + property("conjunction introduction") = wellTyped { + illTyped("Inference[UpperCase, UpperCase And Digit]", "No given instance") + } + + property("disjunction associativity") = secure { + Inference[(UpperCase Or Letter) Or Digit, UpperCase Or (Letter Or Digit)].isValid + } + + property("disjunction commutativity") = secure { + Inference[UpperCase Or Letter, Letter Or UpperCase].isValid + } + + property("disjunction introduction left") = secure { + Inference[Digit, Digit Or Letter].isValid + } + + property("disjunction introduction right") = secure { + Inference[Digit, Letter Or Digit].isValid + } + + property("disjunction elimination") = wellTyped { + illTyped("Inference[UpperCase Or Digit, Digit]", "No given instance") + } + + property("De Morgan's law 1") = secure { + Inference[Not[UpperCase And Letter], Not[UpperCase] Or Not[Letter]].isValid + } + + /* + property("De Morgan's law 1 (reversed)") = secure { + Inference[Not[UpperCase] Or Not[Letter], Not[UpperCase And Letter]].isValid + } + */ + + property("De Morgan's law 2") = secure { + Inference[Not[UpperCase Or Letter], Not[UpperCase] And Not[Letter]].isValid + } + + /* + property("De Morgan's law 2 (reversed)") = secure { + Inference[Not[UpperCase] And Not[Letter], Not[UpperCase Or Letter]].isValid + } + */ + + /* + property("De Morgan's law 1 (substitution form)") = secure { + Inference[Not[Not[UpperCase] Or Not[Letter]], UpperCase And Letter].isValid + } + */ + + /* + property("De Morgan's law 1 (substitution form, reversed)") = secure { + Inference[UpperCase And Letter, Not[Not[UpperCase] Or Not[Letter]]].isValid + } + */ + + /* + property("De Morgan's law 2 (substitution form)") = secure { + Inference[Not[Not[UpperCase] And Not[Letter]], UpperCase Or Letter].isValid + } + */ + + /* + property("De Morgan's law 2 (substitution form, reversed)") = secure { + Inference[UpperCase Or Letter, Not[Not[UpperCase] And Not[Letter]]].isValid + } + */ + + property("Xor commutativity") = secure { + Inference[Letter Xor Digit, Digit Xor Letter].isValid + } + + property("Nand commutativity") = secure { + Inference[Letter Nand Digit, Digit Nand Letter].isValid + } + + property("Nor commutativity") = secure { + Inference[Letter Nor Digit, Digit Nor Letter].isValid + } + + property("modus tollens") = secure { + Inference[Not[Digit Xor Letter], Not[Letter Xor Digit]] ?= + Inference.alwaysValid("modusTollens(xorCommutativity)") + } +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/BooleanValidateSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/BooleanValidateSpec.scala new file mode 100644 index 000000000..b0a3b30b0 --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/BooleanValidateSpec.scala @@ -0,0 +1,162 @@ +package eu.timepit.refined + +import eu.timepit.refined.TestUtils._ +import eu.timepit.refined.api.Validate +import eu.timepit.refined.boolean._ +import eu.timepit.refined.char._ +import eu.timepit.refined.numeric.{Greater, Less} +import org.scalacheck.Prop._ +import org.scalacheck.Properties + +class BooleanValidateSpec extends Properties("BooleanValidate") { + + type FF[Op[_, _]] = False Op False + type FT[Op[_, _]] = False Op True + type TF[Op[_, _]] = True Op False + type TT[Op[_, _]] = True Op True + + property("True.isValid") = secure { + isValid[True](()) + } + + property("True.showExpr") = secure { + showExpr[True](()) ?= "true" + } + + property("False.isValid") = secure { + notValid[False](()) + } + + property("False.showExpr") = secure { + showExpr[False](()) ?= "false" + } + + property("Not.isValid") = secure { + isValid[Not[False]](()) + } + + property("Not.showExpr") = secure { + showExpr[Not[True]](()) ?= "!true" + } + + property("Not.showResult") = secure { + showResult[Not[False]](()) ?= "Predicate false did not pass." + } + + property("And.isValid") = secure { + notValid[FF[And]](()) && + notValid[FT[And]](()) && + notValid[TF[And]](()) && + isValid[TT[And]](()) + } + + property("And.showExpr") = secure { + showExpr[TF[And]](()) ?= "(true && false)" + } + + property("And.showResult") = secure { + (showResult[TT[And]](()) ?= "Both predicates of (true && true) passed.") && + (showResult[FT[And]](()) ?= + "Left predicate of (false && true) failed: Predicate failed: false.") && + (showResult[TF[And]](()) + ?= "Right predicate of (true && false) failed: Predicate failed: false.") && + (showResult[FF[And]](()) ?= "Both predicates of (false && false) failed. " + + "Left: Predicate failed: false. Right: Predicate failed: false.") + } + + property("Or.isValid") = secure { + notValid[FF[Or]](()) && + isValid[FT[Or]](()) && + isValid[TF[Or]](()) && + isValid[TT[Or]](()) + } + + property("Or.showExpr") = secure { + showExpr[TF[Or]](()) ?= "(true || false)" + } + + property("Or.showResult") = secure { + (showResult[TT[Or]](()) ?= "Both predicates of (true || true) passed.") && + (showResult[FT[Or]](()) ?= "Right predicate of (false || true) passed.") && + (showResult[TF[Or]](()) ?= "Left predicate of (true || false) passed.") && + (showResult[FF[Or]](()) ?= "Both predicates of (false || false) failed. " + + "Left: Predicate failed: false. Right: Predicate failed: false.") + } + + property("Xor.isValid") = secure { + notValid[FF[Xor]](()) && + isValid[FT[Xor]](()) && + isValid[TF[Xor]](()) && + notValid[TT[Xor]](()) + } + + property("Xor.showExpr") = secure { + showExpr[TF[Xor]](()) ?= "(true ^ false)" + } + + property("Xor.showResult") = secure { + (showResult[TT[Xor]](()) ?= "Both predicates of (true ^ true) passed.") && + (showResult[FT[Xor]](()) ?= "Right predicate of (false ^ true) passed.") && + (showResult[TF[Xor]](()) ?= "Left predicate of (true ^ false) passed.") && + (showResult[FF[Xor]](()) ?= "Both predicates of (false ^ false) failed. " + + "Left: Predicate failed: false. Right: Predicate failed: false.") + } + + property("Nand.isValid") = secure { + isValid[FF[Nand]](()) && + isValid[FT[Nand]](()) && + isValid[TF[Nand]](()) && + notValid[TT[Nand]](()) + } + + property("Nand.showExpr") = secure { + showExpr[TF[Nand]](()) ?= "!(true && false)" + } + + property("Nand.showResult") = secure { + (showResult[TT[Nand]](()) ?= "Predicate (true && true) did not fail.") && + (showResult[FT[Nand]](()) ?= "Predicate (false && true) did not pass.") && + (showResult[TF[Nand]](()) ?= "Predicate (true && false) did not pass.") && + (showResult[FF[Nand]](()) ?= "Predicate (false && false) did not pass.") + } + + property("Nor.isValid") = secure { + isValid[FF[Nor]](()) && + notValid[FT[Nor]](()) && + notValid[TF[Nor]](()) && + notValid[TT[Nor]](()) + } + + property("Nor.showExpr") = secure { + showExpr[TF[Nor]](()) ?= "!(true || false)" + } + + property("Nor.showResult") = secure { + (showResult[TT[Nor]](()) ?= "Predicate (true || true) did not fail.") && + (showResult[FT[Nor]](()) ?= "Predicate (false || true) did not fail.") && + (showResult[TF[Nor]](()) ?= "Predicate (true || false) did not fail.") && + (showResult[FF[Nor]](()) ?= "Predicate (false || false) did not pass.") + } + + property("AllOf.isValid") = forAll { (i: Int) => + isValid[AllOf[(Greater[0], Less[10])]](i) ?= (i > 0 && i < 10) + } + + property("AllOf.showExpr") = secure { + showExpr[AllOf[(Greater[0], Less[10])]](5) ?= + "((5 > 0) && (5 < 10) && true)" + } + + property("AnyOf.isValid") = forAll { (c: Char) => + isValid[AnyOf[(Digit, LowerCase, Whitespace)]](c) ?= + (c.isDigit || c.isLower || c.isWhitespace) + } + + property("AnyOf.showExpr") = secure { + showExpr[AnyOf[(Digit, LowerCase, Whitespace)]]('c') ?= + "(isDigit('c') || isLower('c') || isWhitespace('c') || false)" + } + + // `OneOf` has no `Validate` instance on Scala 3 (only its case class is defined), so the + // Scala 2 `OneOf.*` properties are not ported. +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/CollectionInferenceSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/CollectionInferenceSpec.scala new file mode 100644 index 000000000..7304a0ae5 --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/CollectionInferenceSpec.scala @@ -0,0 +1,73 @@ +package eu.timepit.refined + +import eu.timepit.refined.TestUtils.wellTyped +import eu.timepit.refined.api.Inference +import eu.timepit.refined.char._ +import eu.timepit.refined.collection._ +import eu.timepit.refined.numeric.{Greater, Interval} +import org.scalacheck.Prop._ +import org.scalacheck.Properties +import eu.timepit.refined.test.ScalaVersionSpecific.illTyped + +class CollectionInferenceSpec extends Properties("CollectionInference") { + + property("Exists[A] ==> Exists[B]") = secure { + Inference[Contains['5'], Exists[Digit]].isValid + } + + property("Exists ==> NonEmpty") = secure { + Inference[Exists[Digit], NonEmpty].isValid + } + + property("NonEmpty =!> Exists") = wellTyped { + illTyped("Inference[NonEmpty, Exists[Digit]]", "No given instance") + } + + property("Head[A] ==> Head[B]") = secure { + Inference[Head[Digit], Head[LetterOrDigit]].isValid + } + + property("Head[A] ==> Exists[A]") = secure { + Inference[Head[Digit], Exists[Digit]].isValid + } + + property("Exists[A] =!> Head[A]") = wellTyped { + illTyped("Inference[Exists[Digit], Head[Digit]]") + } + + property("Index[N, A] ==> Index[N, B]") = secure { + Inference[Index[1, Letter], Index[1, LetterOrDigit]].isValid + } + + property("Index ==> Exists") = secure { + Inference[Index[1, LowerCase], Exists[LowerCase]].isValid + } + + property("Last[A] ==> Last[B]") = secure { + Inference[Last[Letter], Last[LetterOrDigit]].isValid + } + + property("Last ==> Exists") = secure { + Inference[Last[Whitespace], Exists[Whitespace]].isValid + } + + // `Last ==> NonEmpty` (Scala 2) is not ported: it holds only transitively via `Last ==> Exists` and + // `Exists ==> NonEmpty`, and the transitivity rule (`hypotheticalSyllogism`) is omitted on Scala 3 + // because its free intermediate type makes implicit search ambiguous. See boolean.scala. + + property("NonEmpty =!> Last") = wellTyped { + illTyped("Inference[NonEmpty, Last[Whitespace]]", "No given instance") + } + + property("Size[A] ==> Size[B]") = secure { + Inference[Size[Greater[5]], Size[Greater[4]]].isValid + } + + property("Size[Greater[1]] ==> NonEmpty") = secure { + Inference[Size[Greater[1]], NonEmpty].isValid + } + + property("Size[Interval.Closed[2, 5]] ==> NonEmpty") = secure { + Inference[Size[Interval.Closed[2, 5]], NonEmpty].isValid + } +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/CollectionValidateSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/CollectionValidateSpec.scala new file mode 100644 index 000000000..0339a8237 --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/CollectionValidateSpec.scala @@ -0,0 +1,190 @@ +package eu.timepit.refined + +import eu.timepit.refined.TestUtils._ +import eu.timepit.refined.boolean.And +import eu.timepit.refined.char.{Digit, LowerCase} +import eu.timepit.refined.collection._ +import eu.timepit.refined.numeric._ +import org.scalacheck.Prop._ +import org.scalacheck.Properties + +class CollectionValidateSpec extends Properties("CollectionValidate") { + + property("Contains.isValid") = forAll { (l: List[Int]) => + isValid[Contains[0]](l) ?= l.contains(0) + } + + property("Contains.showExpr") = secure { + showExpr[Contains[0]](List(1, 2, 3)) ?= "!(!(1 == 0) && !(2 == 0) && !(3 == 0))" + } + + property("Contains.String.isValid") = forAll { (s: String) => + isValid[Contains['0']](s) ?= s.contains('0') + } + + property("Contains.String.showExpr") = secure { + showExpr[Contains['0']]("012") ?= "!(!(0 == 0) && !(1 == 0) && !(2 == 0))" + } + + property("Count.isValid") = forAll { (l: List[Char]) => + isValid[Count[LowerCase, Greater[2]]](l) ?= (l.count(_.isLower) > 2) + } + + property("Count.showExpr") = secure { + showExpr[Count[LowerCase, Greater[2]]](List('a', 'B')) ?= "(1 > 2)" + } + + property("Count.showResult") = secure { + showResult[Count[LowerCase, Greater[2]]](List('a', 'B')) ?= + "Predicate taking count(isLower('a'), isLower('B')) = 1 failed: Predicate failed: (1 > 2)." + } + + property("Count.String.isValid") = forAll { (s: String) => + isValid[Count[LowerCase, Greater[2]]](s) ?= (s.count(_.isLower) > 2) + } + + property("Empty.isValid") = forAll((l: List[Int]) => isValid[Empty](l) ?= l.isEmpty) + + property("Empty.showExpr") = secure { + showExpr[Empty](List(1, 2)) ?= "isEmpty(List(1, 2))" + } + + property("Empty.String.isValid") = forAll((s: String) => isValid[Empty](s) ?= s.isEmpty) + + property("Empty.String.showExpr") = secure { + showExpr[Empty]("test") ?= "isEmpty(test)" + } + + property("Exists.isValid") = forAll { (l: List[Int]) => + isValid[Exists[Less[1]]](l) ?= l.exists(_ < 1) + } + + property("Exists.showExpr") = secure { + showExpr[Exists[Less[1]]](List(1, 2, 3)) ?= "!(!(1 < 1) && !(2 < 1) && !(3 < 1))" + } + + property("Forall.String.isValid") = forAll { (s: String) => + isValid[Forall[LowerCase]](s) ?= s.forall(_.isLower) + } + + property("Forall.String.showExpr") = secure { + showExpr[Forall[LowerCase]]("abc") ?= "(isLower('a') && isLower('b') && isLower('c'))" + } + + property("Forall.String.showResult") = secure { + showResult[Forall[LowerCase]]("ab") ?= "Predicate passed: (isLower('a') && isLower('b'))." + } + + property("Head.isValid") = forAll { (l: List[Char]) => + isValid[Head[Digit]](l) ?= l.headOption.fold(false)(_.isDigit) + } + + property("Head.showExpr.empty") = secure { + showExpr[Head[Digit]](List.empty[Char]) ?= "" + } + + property("Head.showExpr.nonEmpty") = secure { + showExpr[Head[Digit]](List('a', 'b')) ?= "isDigit('a')" + } + + property("Head.showResult") = secure { + showResult[Head[Digit]](List('a', '1')) ?= + "Predicate taking head(List(a, 1)) = a failed: Predicate failed: isDigit('a')." + } + + property("Head.String.isValid") = forAll { (s: String) => + isValid[Head[Digit]](s) ?= s.headOption.fold(false)(_.isDigit) + } + + property("Head.String.showExpr") = secure { + showExpr[Head[Digit]]("ab") ?= "isDigit('a')" + } + + property("Index.isValid") = forAll { (l: List[Char]) => + isValid[Index[2, Digit]](l) ?= l.lift(2).fold(false)(_.isDigit) + } + + property("Index.showExpr") = secure { + showExpr[Index[1, Digit]](List('a', 'b')) ?= "isDigit('b')" + } + + property("Index.showResult.empty") = secure { + showResult[Index[2, Digit]](List.empty[Char]) ?= "Predicate failed: empty collection." + } + + property("Index.showResult.nonEmpty") = secure { + showResult[Index[2, Digit]](List('a', 'b', 'c')) ?= + "Predicate taking index(List(a, b, c), 2) = c failed: Predicate failed: isDigit('c')." + } + + property("Last.isValid") = forAll { (l: List[Int]) => + isValid[Last[Greater[5]]](l) ?= l.lastOption.fold(false)(_ > 5) + } + + property("Last.showExpr") = secure { + showExpr[Last[Greater[5]]](List(1, 2, 3)) ?= "(3 > 5)" + } + + property("Last.showResult") = secure { + showResult[Last[Greater[5]]](List(1, 2, 3)) ?= + "Predicate taking last(List(1, 2, 3)) = 3 failed: Predicate failed: (3 > 5)." + } + + property("Last.String.isValid") = forAll { (s: String) => + isValid[Last[Digit]](s) ?= s.lastOption.fold(false)(_.isDigit) + } + + property("Last.String.showExpr") = secure { + showExpr[Last[Digit]]("abc0") ?= "isDigit('0')" + } + + property("Init.String.isValid") = forAll { (s: String) => + isValid[Init[LowerCase]](s) ?= s.toList.dropRight(1).forall(_.isLower) + } + + property("Init.String.showExpr") = secure { + showExpr[Init[LowerCase]]("abcd") ?= "(isLower('a') && isLower('b') && isLower('c'))" + } + + property("Init.String.showResult") = secure { + showResult[Init[LowerCase]]("abc") ?= "Predicate passed: (isLower('a') && isLower('b'))." + } + + property("Tail.String.isValid") = forAll { (s: String) => + isValid[Tail[LowerCase]](s) ?= s.toList.drop(1).forall(_.isLower) + } + + property("Tail.String.showExpr") = secure { + showExpr[Tail[LowerCase]]("abcd") ?= "(isLower('b') && isLower('c') && isLower('d'))" + } + + property("Tail.String.showResult") = secure { + showResult[Tail[LowerCase]]("abc") ?= "Predicate passed: (isLower('b') && isLower('c'))." + } + + property("MinSize.String.isValid") = forAll { (s: String) => + isValid[MinSize[5]](s) ?= (s.length >= 5) + } + + property("NonEmpty.String.isValid") = forAll((s: String) => isValid[NonEmpty](s) ?= s.nonEmpty) + + property("NonEmpty.String.showExpr") = secure { + showExpr[NonEmpty]("test") ?= "!isEmpty(test)" + } + + property("Size.isValid") = forAll { (l: List[Int]) => + isValid[Size[Greater[5]]](l) ?= (l.size > 5) + } + + property("Size.showExpr") = secure { + showExpr[Size[Greater[5]]](List(1, 2, 3)) ?= "(3 > 5)" + } + + property("Size.String.isValid") = forAll { (s: String) => + isValid[Size[LessEqual[10]]](s) ?= (s.length <= 10) + } + + property("Size.String.showExpr") = secure { + showExpr[Size[Greater[5] And LessEqual[10]]]("test") ?= "((4 > 5) && !(4 > 10))" + } +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/GenericInferenceSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/GenericInferenceSpec.scala new file mode 100644 index 000000000..f5a4d5a6c --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/GenericInferenceSpec.scala @@ -0,0 +1,27 @@ +package eu.timepit.refined + +import eu.timepit.refined.api.Inference +import eu.timepit.refined.generic.Equal +import eu.timepit.refined.numeric.Greater +import eu.timepit.refined.string.StartsWith +import org.scalacheck.Prop._ +import org.scalacheck.Properties + +class GenericInferenceSpec extends Properties("GenericInference") { + + property("""Equal["abcd"] ==> StartsWith["ab"]""") = secure { + Inference[Equal["abcd"], StartsWith["ab"]].isValid + } + + property("""Equal["abcd"] =!> StartsWith["cd"]""") = secure { + Inference[Equal["abcd"], StartsWith["cd"]].notValid + } + + property("Equal[10] ==> Greater[5]") = secure { + Inference[Equal[10], Greater[5]].isValid + } + + property("Equal[5] =!> Greater[10]") = secure { + Inference[Equal[5], Greater[10]].notValid + } +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/GenericValidateSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/GenericValidateSpec.scala new file mode 100644 index 000000000..36cb51614 --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/GenericValidateSpec.scala @@ -0,0 +1,42 @@ +package eu.timepit.refined + +import eu.timepit.refined.TestUtils._ +import eu.timepit.refined.generic._ +import org.scalacheck.Prop._ +import org.scalacheck.Properties + +class GenericValidateSpec extends Properties("GenericValidate") { + + property("isValid[Equal[1.4]](1.4)") = secure { + isValid[Equal[1.4]](1.4) + } + + property("notValid[Equal[1.4]](2.4)") = secure { + notValid[Equal[1.4]](2.4) + } + + property("showExpr[Equal[1.4]](0.4)") = secure { + showExpr[Equal[1.4]](0.4) ?= "(0.4 == 1.4)" + } + + property("isValid[Equal[Foo.type]](Foo)") = secure { + object Foo + isValid[Equal[Foo.type]](Foo) + } + + property("isValid[Equal[0]](i: Int)") = forAll { (i: Int) => + isValid[Equal[0]](i) ?= (i == 0) + } + + property("isValid[Equal[0]](l: Long)") = forAll { (l: Long) => + isValid[Equal[0]](l) ?= (l == 0L) + } + + property("isValid[Equal[0]](d: Double)") = forAll { (d: Double) => + isValid[Equal[0]](d) ?= (d == 0.0) + } + + property("showExpr[Equal[5]](0)") = secure { + showExpr[Equal[5]](0) ?= "(0 == 5)" + } +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/ImplicitScopeSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/ImplicitScopeSpec.scala new file mode 100644 index 000000000..0879b32e3 --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/ImplicitScopeSpec.scala @@ -0,0 +1,37 @@ +package eu.timepit.refined + +import eu.timepit.refined.TestUtils.wellTyped +import eu.timepit.refined.api.{Inference, Validate} +import org.scalacheck.Properties + +/** + * Tests that ensure that `Validate` and `Inference` instances of + * predicates are in their implicit scope and do not need to be imported + * explicitly. + */ +class ImplicitScopeSpec extends Properties("implicit scope") { + + property("Validate[Char, LetterOrDigit]") = wellTyped { + Validate[Char, char.LetterOrDigit] + } + + property("Validate[Int, Positive]") = wellTyped { + Validate[Int, numeric.Positive] + } + + property("Validate[Int, NonPositive]") = wellTyped { + Validate[Int, numeric.NonPositive] + } + + property("Validate[Int, Interval.Closed[0, 10]]") = wellTyped { + Validate[Int, numeric.Interval.Closed[0, 10]] + } + + property("Inference[And[UpperCase, Letter], And[Letter, UpperCase]]") = wellTyped { + Inference[boolean.And[char.UpperCase, char.Letter], boolean.And[char.Letter, char.UpperCase]] + } + + property("Inference[Greater[1], Greater[0]]") = wellTyped { + Inference[numeric.Greater[1], numeric.Greater[0]] + } +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/MaxSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/MaxSpec.scala new file mode 100644 index 000000000..00a8f6f85 --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/MaxSpec.scala @@ -0,0 +1,111 @@ +package eu.timepit.refined + +import eu.timepit.refined.api.{Max, Refined} +import eu.timepit.refined.boolean._ +import eu.timepit.refined.numeric._ +import eu.timepit.refined.types.numeric._ +import org.scalacheck.Prop._ +import org.scalacheck.Properties + +class MaxSpec extends Properties("Max") { + + property("Max[Int Refined Less[1]]") = secure { + Max[Int Refined Less[1]].max.value ?= 0 + } + + property("Max[Int Refined Less[0]]") = secure { + Max[Int Refined Less[0]].max.value ?= -1 + } + + property("Max[Long Refined Less[5]]") = secure { + Max[Long Refined Less[5]].max.value ?= 4L + } + + property("Max[Float Refined Less[5]]") = secure { + Max[Float Refined Less[5]].max.value ?= 4.9999995f + } + + property("Max[Byte Refined Greater[0]]") = secure { + Max[Byte Refined Greater[0]].max.value ?= Byte.MaxValue + } + + property("Max[Short Refined Greater[0]]") = secure { + Max[Short Refined Greater[0]].max.value ?= Short.MaxValue + } + + property("Max[Int Refined Greater[0]]") = secure { + Max[Int Refined Greater[0]].max.value ?= Int.MaxValue + } + + property("Max[Long Refined Greater[0]]") = secure { + Max[Long Refined Greater[0]].max.value ?= Long.MaxValue + } + + property("Max[Float Refined Greater[0]]") = secure { + Max[Float Refined Greater[0]].max.value ?= Float.MaxValue + } + + property("Max[Double Refined Greater[0]]") = secure { + Max[Double Refined Greater[0]].max.value ?= Double.MaxValue + } + + property("Max[Int Refined NonNegative]") = secure { + Max[Int Refined NonNegative].max.value ?= Int.MaxValue + } + + property("Max[Int Refined NonPositive]") = secure { + Max[Int Refined NonPositive].max.value ?= 0 + } + + property("Max[Float Refined NonPositive]") = secure { + Max[Float Refined NonPositive].max.value ?= 0f + } + + property("Max[Double Refined NonPositive]") = secure { + Max[Double Refined NonPositive].max.value ?= 0d + } + + property("Max[Int Refined Not[Greater[-5]]]") = secure { + Max[Int Refined Not[Greater[-5]]].max.value ?= -5 + } + + property("Max[Int Refined Interval.Open[1, 4]]") = secure { + Max[Int Refined Interval.Open[1, 4]].max.value ?= 3 + } + + property("Max[Double Refined Interval.Open[1, 4]]") = secure { + Max[Double Refined Interval.Open[1, 4]].max.value ?= 3.9999999999999996 + } + + property("Max[Int Refined Interval.Closed[-20, 10]]") = secure { + Max[Int Refined Interval.Closed[-20, 10]].max.value ?= 10 + } + + property("Max[Double Refined Interval.Closed[-20d, 10.99991d]]") = secure { + Max[Double Refined Interval.Closed[-20d, 10.99991d]].max.value ?= 10.99991d + } + + property("Max[Char Refined Interval.Closed['A', 'Z']]") = secure { + Max[Char Refined Interval.Closed['A', 'Z']].max.value ?= 'Z' + } + + property("Max[Int Refined Even]") = secure { + Max[Int Refined Even].max.value ?= 2147483646 + } + + property("Max[Int Refined Divisible[5]]") = secure { + Max[Int Refined Divisible[5]].max.value ?= 2147483645 + } + + property("Max[Int Refined (Negative And Even)]") = secure { + Max[Int Refined (Negative And Even)].max.value ?= -2 + } + + property("NegLong.MaxValue") = secure { + NegLong.MaxValue ?= NegLong.unsafeFrom(-1) + } + + property("PosFloat.MaxValue") = secure { + PosFloat.MaxValue ?= PosFloat.unsafeFrom(Float.MaxValue) + } +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/MinSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/MinSpec.scala new file mode 100644 index 000000000..5bac96ead --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/MinSpec.scala @@ -0,0 +1,111 @@ +package eu.timepit.refined + +import eu.timepit.refined.api.{Min, Refined} +import eu.timepit.refined.boolean._ +import eu.timepit.refined.numeric._ +import eu.timepit.refined.types.numeric._ +import org.scalacheck.Prop._ +import org.scalacheck.Properties + +class MinSpec extends Properties("Min") { + + property("Min[Int Refined Greater[1]]") = secure { + Min[Int Refined Greater[1]].min.value ?= 2 + } + + property("Min[Int Refined Greater[0]]") = secure { + Min[Int Refined Greater[0]].min.value ?= 1 + } + + property("Min[Long Refined Greater[0]]") = secure { + Min[Long Refined Greater[0]].min.value ?= 1L + } + + property("Min[Float Refined Greater[0]]") = secure { + Min[Float Refined Greater[0f]].min.value ?= "1.4E-45".toFloat + } + + property("Min[Byte Refined Less[0]]") = secure { + Min[Byte Refined Less[0]].min.value ?= Byte.MinValue + } + + property("Min[Short Refined Less[0]]") = secure { + Min[Short Refined Less[0]].min.value ?= Short.MinValue + } + + property("Min[Int Refined Less[0]]") = secure { + Min[Int Refined Less[0]].min.value ?= Int.MinValue + } + + property("Min[Long Refined Less[0]]") = secure { + Min[Long Refined Less[0]].min.value ?= Long.MinValue + } + + property("Min[Float Refined Less[0]]") = secure { + Min[Float Refined Less[0]].min.value ?= Float.MinValue + } + + property("Min[Double Refined Less[0]]") = secure { + Min[Double Refined Less[0]].min.value ?= Double.MinValue + } + + property("Min[Int Refined NonPositive]") = secure { + Min[Int Refined NonPositive].min.value ?= Int.MinValue + } + + property("Min[Int Refined NonNegative]") = secure { + Min[Int Refined NonNegative].min.value ?= 0 + } + + property("Min[Float Refined NonNegative]") = secure { + Min[Float Refined NonNegative].min.value ?= 0f + } + + property("Min[Double Refined NonNegative]") = secure { + Min[Double Refined NonNegative].min.value ?= 0f + } + + property("Min[Int Refined Not[Less[-5]]]") = secure { + Min[Int Refined Not[Less[-5]]].min.value ?= -5 + } + + property("Min[Int Refined Interval.Open[1, 4]]") = secure { + Min[Int Refined Interval.Open[1, 4]].min.value ?= 2 + } + + property("Min[Double Refined Interval.Open[1, 4]]") = secure { + Min[Double Refined Interval.Open[1, 4]].min.value ?= 1.0000000000000002 + } + + property("Min[Int Refined Interval.Closed[-20, 10]]") = secure { + Min[Int Refined Interval.Closed[-20, 10]].min.value ?= -20 + } + + property("Min[Double Refined Interval.Closed[-20.001d, 0d]]") = secure { + Min[Double Refined Interval.Closed[-20.001d, 0d]].min.value ?= -20.001d + } + + property("Min[Char Refined Interval.Closed['A', 'Z']]") = secure { + Min[Char Refined Interval.Closed['A', 'Z']].min.value ?= 'A' + } + + property("Min[Int Refined Even]") = secure { + Min[Int Refined Even].min.value ?= Int.MinValue + } + + property("Min[Int Refined Divisible[5]]") = secure { + Min[Int Refined Divisible[5]].min.value ?= -2147483645 + } + + property("Min[Int Refined (Positive And Even)]") = secure { + Min[Int Refined (Positive And Even)].min.value ?= 2 + } + + property("PosLong.MinValue") = secure { + PosLong.MinValue ?= PosLong.unsafeFrom(1) + } + + property("NegFloat.MinValue") = secure { + NegFloat.MinValue ?= NegFloat.unsafeFrom(Float.MinValue) + } +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/NumericInferenceSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/NumericInferenceSpec.scala new file mode 100644 index 000000000..2f19644a9 --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/NumericInferenceSpec.scala @@ -0,0 +1,78 @@ +package eu.timepit.refined + +import eu.timepit.refined.api.Inference +import eu.timepit.refined.boolean._ +import eu.timepit.refined.numeric._ +import org.scalacheck.Prop._ +import org.scalacheck.Properties + +class NumericInferenceSpec extends Properties("NumericInference") { + + property("Less[5] ==> Less[10]") = secure { + Inference[Less[5], Less[10]].isValid + } + + property("Less[10] =!> Less[5]") = secure { + Inference[Less[10], Less[5]].notValid + } + + property("Less[7.2] ==> Less[7.5]") = secure { + Inference[Less[7.2], Less[7.5]].isValid + } + + property("Less[7.5] =!> Less[7.2]") = secure { + Inference[Less[7.5], Less[7.2]].notValid + } + + property("LessEqual[1] ==> LessEqual[1]") = secure { + Inference[LessEqual[1], LessEqual[1]].isValid + } + + property("LessEqual[7.2] ==> LessEqual[7.5]") = secure { + Inference[LessEqual[7.2], LessEqual[7.5]].isValid + } + + property("LessEqual[7.5] =!> LessEqual[7.2]") = secure { + Inference[LessEqual[7.5], LessEqual[7.2]].notValid + } + + property("Greater[10] ==> Greater[5]") = secure { + Inference[Greater[10], Greater[5]].isValid + } + + property("Greater[5] =!> Greater[10]") = secure { + Inference[Greater[5], Greater[10]].notValid + } + + property("Greater[7.5] ==> Greater[7.2]") = secure { + Inference[Greater[7.5], Greater[7.2]].isValid + } + + property("Greater[7.2] =!> Greater[7.5]") = secure { + Inference[Greater[7.2], Greater[7.5]].notValid + } + + property("GreaterEqual[1] ==> GreaterEqual[1]") = secure { + Inference[GreaterEqual[1], GreaterEqual[1]].isValid + } + + property("GreaterEqual[7.5] ==> GreaterEqual[7.2]") = secure { + Inference[GreaterEqual[7.5], GreaterEqual[7.2]].isValid + } + + property("GreaterEqual[7.2] =!> GreaterEqual[7.5]") = secure { + Inference[GreaterEqual[7.2], GreaterEqual[7.5]].notValid + } + + property("Greater[0] ==> GreaterEqual[0]") = secure { + Inference[Greater[0], GreaterEqual[0]].isValid + } + + property("Less[0] ==> LessEqual[0]") = secure { + Inference[Less[0], LessEqual[0]].isValid + } + + property("Interval.Closed[5, 10] ==> LessEqual[11]") = secure { + Inference[Interval.Closed[5, 10], LessEqual[11]].isValid + } +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/RefineMSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/RefineMSpec.scala new file mode 100644 index 000000000..f371a744f --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/RefineMSpec.scala @@ -0,0 +1,66 @@ +package eu.timepit.refined + +import eu.timepit.refined.TestUtils.wellTyped +import eu.timepit.refined.api.Refined +import eu.timepit.refined.char._ +import eu.timepit.refined.collection._ +import eu.timepit.refined.numeric._ +import eu.timepit.refined.string.MatchesRegex +import org.scalacheck.Prop._ +import org.scalacheck.Properties +import eu.timepit.refined.test.ScalaVersionSpecific.illTyped + +// Ported from the Scala 2 `RefineMSpec`. The `refineMT` (shapeless `@@`) variants are dropped +// because the tag encoding is not available on Scala 3; only the `refineMV` (`Refined`) cases remain. +class RefineMSpec extends Properties("refineM") { + + property("RefineMVBuilder instance") = secure { + val rv = refineMV[Digit] + rv('0') == Refined.unsafeApply('0') + } + + property("refineM with Forall") = wellTyped { + def ignore1: String Refined Forall[LowerCase] = refineMV[Forall[LowerCase]]("hello") + illTyped("""refineMV[Forall[UpperCase]]("hello")""", "Predicate.*fail.*") + } + + property("refineM with Greater") = wellTyped { + def ignore1: Int Refined Greater[10] = refineMV[Greater[10]](15) + illTyped("""refineMV[Greater[10]](5)""", "Predicate.*fail.*") + } + + property("refineM with Size") = wellTyped { + type ShortString = Size[LessEqual[10]] + def ignore1: String Refined ShortString = refineMV[ShortString]("abc") + illTyped("""refineMV[Size[LessEqual[10]]]("abcdefghijklmnopqrstuvwxyz")""", "Predicate.*fail.*") + } + + property("refineM with LowerCase") = wellTyped { + def ignore1: Char Refined LowerCase = refineMV[LowerCase]('c') + illTyped("refineMV[LowerCase]('C')", "Predicate.*failed.*") + } + + property("refineM with MatchesRegex") = wellTyped { + def ignore1: String Refined MatchesRegex["[0-9]+"] = refineMV("123") + illTyped("""refineMV[MatchesRegex["[0-9]+"]]("abc")""", "Predicate.*fail.*") + } + + property("refineM with Contains") = wellTyped { + def ignore1: String Refined Contains['c'] = refineMV("abcd") + illTyped("""refineMV[Contains['c']]("abde")""", "Predicate.*fail.*") + } + + property("refineM with Double Witness") = wellTyped { + def ignore1: Double Refined Greater[2.3] = refineMV(2.4) + illTyped("refineMV[Greater[2.3]](2.2)", "Predicate.*fail.*") + } + + property("refineM failure with non-literals") = wellTyped { + // hearth's `semiEval` can evaluate more than plain literals at compile time (e.g. `List(1, 2, 3)`), + // so the failure case must use a genuinely non-compile-time-constant value — here a `def`. + illTyped( + "{ def xs: List[Int] = List(1, 2, 3); refineMV[NonEmpty](xs) }", + "Cannot evaluate expression at compile time" + ) + } +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/RefineSyntaxSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/RefineSyntaxSpec.scala new file mode 100644 index 000000000..046ff8156 --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/RefineSyntaxSpec.scala @@ -0,0 +1,48 @@ +package eu.timepit.refined + +import eu.timepit.refined.TestUtils.wellTyped +import eu.timepit.refined.api.Refined +import eu.timepit.refined.auto._ +import eu.timepit.refined.numeric.Positive +import org.scalacheck.Prop._ +import org.scalacheck.Properties +import eu.timepit.refined.test.ScalaVersionSpecific.illTyped + +// Ported from the Scala 2 `RefineSyntaxSpec`. Only the `refineT`/`refineMT` (shapeless `@@`) variants +// are dropped. The point-free/curried `refineV`/`refineMV` forms work just like Scala 2 — the +// predicate `P` is inferred from the expected type (here the `testRefineV`/`testRefineMV` parameter). +class RefineSyntaxSpec extends Properties("refine syntax") { + + def testRefineV(arg: Either[String, Int Refined Positive]): Boolean = true + def testRefineMV(arg: Int Refined Positive): Boolean = true + + property("refineV success") = secure { + testRefineV(refineV(1)) + testRefineV(refineV[Positive](1)) + testRefineV(refineV[Positive][Int](1)) + } + + property("refineV failure") = secure { + // `refineV` is a runtime refinement (returns `Either`), so these compile and yield `Left` at runtime. + testRefineV(refineV(-1)) + testRefineV(refineV[Positive](-1)) + testRefineV(refineV[Positive][Int](-1)) + } + + property("refineMV success") = secure { + testRefineMV(1) // via autoRefineV + testRefineMV(refineMV(1)) + testRefineMV(refineMV[Positive](1)) + testRefineMV(refineMV[Positive][Int](1)) + } + + property("refineMV failure") = wellTyped { + // A rejected `autoRefineV` conversion surfaces as a type mismatch against the required refined + // type; a direct `refineMV[...]` macro call surfaces the "Predicate failed" abort. (`typeCheckErrors` + // exposes the macro message only for the direct call — see ScalaVersionSpecific.illTyped.) + illTyped("testRefineMV(-1)", "Required: Int Refined .*Positive") + illTyped("testRefineMV(refineMV(-1))") + illTyped("testRefineMV(refineMV[Positive](-1))", "Predicate.*fail.*") + illTyped("testRefineMV(refineMV[Positive][Int](-1))", "Predicate.*fail.*") + } +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/RefinedSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/RefinedSpec.scala new file mode 100644 index 000000000..4f639c33e --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/RefinedSpec.scala @@ -0,0 +1,49 @@ +package eu.timepit.refined + +import eu.timepit.refined.TestUtils.wellTyped +import eu.timepit.refined.api.Refined +import eu.timepit.refined.collection.NonEmpty +import eu.timepit.refined.types.string.NonEmptyString +import org.scalacheck.Prop._ +import org.scalacheck.Properties +import eu.timepit.refined.test.ScalaVersionSpecific.illTyped + +class RefinedSpec extends Properties("Refined") { + + property("apply") = wellTyped { + illTyped( + """ val x: NonEmptyString = Refined("") """, + "does not take parameters" + ) + } + + property("copy") = wellTyped { + // Self-contained snippet: `typeCheckErrors` does not see block-local vals, so inline the value. + illTyped( + """ refineMV[NonEmpty]("abc").copy("") """, + "copy is not a member" + ) + } + + property("equals") = secure { + // Note: unlike the Scala 2 value class, `Refined` is an opaque type on Scala 3 and erases to its + // base type, so `Refined.unsafeApply(1).equals(1)` is `true` — only reflexive equality is checked. + Refined.unsafeApply(1) ?= Refined.unsafeApply(1) + } + + property("hashCode") = forAll((i: Int) => Refined.unsafeApply(i).hashCode() ?= i.hashCode) + + property("unapply") = secure { + val x: NonEmptyString = refineMV("Hi") + val Refined(s) = x + s ?= x.value + } + + property("unapply in pattern matching") = secure { + val x: NonEmptyString = refineMV("abc") + x match { + case Refined("abc") => true + case _ => false + } + } +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/StringInferenceSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/StringInferenceSpec.scala new file mode 100644 index 000000000..4bd877bd6 --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/StringInferenceSpec.scala @@ -0,0 +1,74 @@ +package eu.timepit.refined + +import eu.timepit.refined.api.Inference +import eu.timepit.refined.collection.NonEmpty +import eu.timepit.refined.string._ +import org.scalacheck.Prop._ +import org.scalacheck.Properties + +class StringInferenceSpec extends Properties("StringInference") { + + property("EndsWith ==> EndsWith") = secure { + Inference[EndsWith["cde"], EndsWith["de"]].isValid + } + + property("EndsWith =!> EndsWith") = secure { + Inference[EndsWith["de"], EndsWith["cde"]].notValid + } + + property("StartsWith ==> StartsWith") = secure { + Inference[StartsWith["cde"], StartsWith["cd"]].isValid + } + + property("StartsWith =!> StartsWith") = secure { + Inference[StartsWith["cde"], StartsWith["de"]].notValid + } + + property("MatchesRegex ==> NonEmpty") = secure { + Inference[MatchesRegex[".+"], NonEmpty].isValid + } + + property("MatchesRegex =!> NonEmpty") = secure { + Inference[MatchesRegex[".*"], NonEmpty].notValid + } + + property("UUID ==> NonEmpty ") = secure { + Inference[Uuid, NonEmpty].isValid + } + + property("Url ==> NonEmpty ") = secure { + Inference[Url, NonEmpty].isValid + } + + property("ValidByte ==> NonEmpty ") = secure { + Inference[ValidByte, NonEmpty].isValid + } + + property("ValidShort ==> NonEmpty ") = secure { + Inference[ValidShort, NonEmpty].isValid + } + + property("ValidInt ==> NonEmpty ") = secure { + Inference[ValidInt, NonEmpty].isValid + } + property("ValidLong ==> NonEmpty ") = secure { + Inference[ValidLong, NonEmpty].isValid + } + + property("ValidFloat ==> NonEmpty ") = secure { + Inference[ValidFloat, NonEmpty].isValid + } + + property("ValidDouble ==> NonEmpty ") = secure { + Inference[ValidDouble, NonEmpty].isValid + } + + property("ValidBigInt ==> NonEmpty ") = secure { + Inference[ValidBigInt, NonEmpty].isValid + } + + property("ValidBigDecimal ==> NonEmpty ") = secure { + Inference[ValidBigDecimal, NonEmpty].isValid + } + +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/StringValidateSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/StringValidateSpec.scala new file mode 100644 index 000000000..525edaa10 --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/StringValidateSpec.scala @@ -0,0 +1,134 @@ +package eu.timepit.refined + +import eu.timepit.refined.TestUtils._ +import eu.timepit.refined.api.Validate +import eu.timepit.refined.string._ +import org.scalacheck.{Arbitrary, Properties} +import org.scalacheck.Prop._ + +class StringValidateSpec extends Properties("StringValidate") { + + property("EndsWith.isValid") = secure { + val s = "abcd" + isValid[EndsWith["cd"]](s) ?= s.endsWith("cd") + } + + property("EndsWith.showExpr") = secure { + showExpr[EndsWith["cd"]]("abcd") ?= """"abcd".endsWith("cd")""" + } + + property("MatchesRegex.isValid") = forAll { (s: String) => + isValid[MatchesRegex[".{2,10}"]](s) ?= s.matches(".{2,10}") + } + + property("MatchesRegex.showExpr") = secure { + showExpr[MatchesRegex[".{2,10}"]]("Hello") ?= """"Hello".matches(".{2,10}")""" + } + + property("Regex.isValid") = secure { + isValid[Regex](".*") + } + + property("Regex.showExpr") = secure { + showExpr[Regex]("(a|b)") ?= """isValidRegex("(a|b)")""" + } + + property("StartsWith.isValid") = secure { + val s = "abcd" + isValid[StartsWith["ab"]](s) ?= s.startsWith("ab") + } + + property("StartsWith.showExpr") = secure { + showExpr[StartsWith["ab"]]("abcd") ?= """"abcd".startsWith("ab")""" + } + + property("Uri.isValid") = secure { + isValid[Uri]("/a/b/c") + } + + property("Uri.showResult") = secure { + showResult[Uri](" /a/b/c").startsWith("Uri predicate failed") + } + + property("Uuid.isValid") = secure { + isValid[Uuid]("9ecce884-47fe-4ba4-a1bb-1a3d71ed6530") + } + + property("Uuid.showResult.Passed") = secure { + showResult[Uuid]("9ecce884-47fe-4ba4-a1bb-1a3d71ed6530") ?= "Uuid predicate passed." + } + + property("Uuid.showResult.Failed") = secure { + showResult[Uuid]("whops") ?= "Uuid predicate failed: Invalid UUID string: whops" + } + + property("IPv4.isValid") = secure { + isValid[IPv4]("10.0.0.1") + } + + property("IPv4.showResult.InvalidOctet") = secure { + showResult[IPv4]("10.0.256.1") ?= "Predicate failed: 10.0.256.1 is a valid IPv4." + } + + property("IPv4.showResult.Failed") = secure { + showResult[IPv4]("::1") ?= "Predicate failed: ::1 is a valid IPv4." + } + property("IPv6.isValid.full") = secure { + isValid[IPv6]("2001:0db8:85a3:0000:0000:8a2e:0370:7334") + } + + property("IPv6.isValid.noLeadingZeros") = secure { + isValid[IPv6]("2001:db8:85a3:0:0:8a2e:370:7334") + } + + property("IPv6.isValid.compact") = secure { + isValid[IPv6]("2001:db8:85a3::8a2e:370:7334") + } + + property("IPv6.isValid.local") = secure { + isValid[IPv6]("::1") + } + + property("IPv6.isValid.linkLocal") = secure { + isValid[IPv6]("fe80::7:8%eth0") + } + + property("IPv6.isValid.mapped") = secure { + isValid[IPv6]("::ffff:255.255.255.255") + } + + property("IPv6.isValid.embedded") = secure { + isValid[IPv6]("2001:db8:122:344::192.0.2.33") + } + + property("IPv6.showResult.Failed.Random") = secure { + showResult[IPv6]("foo") ?= "Predicate failed: foo is a valid IPv6." + } + + property("IPv6.showResult.Failed.DoubleCompact") = secure { + showResult[IPv6]("2001::0::1234") ?= "Predicate failed: 2001::0::1234 is a valid IPv6." + } + + private def validNumber[N: Arbitrary, P](name: String, invalidValue: String)(implicit + v: Validate[String, P] + ) = { + property(name) = secure { + forAll { (n: N) => + isValid[P](n.toString) && + (showResult[P](n.toString) ?= s"$name predicate passed.") + } + } + property(s"$name.showResult.Failed") = secure { + showResult[P](invalidValue).startsWith(s"$name predicate failed") + } + } + + validNumber[Byte, ValidByte]("ValidByte", Short.MaxValue.toString) + validNumber[Short, ValidShort]("ValidShort", Int.MaxValue.toString) + validNumber[Int, ValidInt]("ValidInt", Long.MaxValue.toString) + validNumber[Long, ValidLong]("ValidLong", "1.0") + validNumber[Float, ValidFloat]("ValidFloat", "a") + validNumber[Double, ValidDouble]("ValidDouble", "a") + validNumber[BigInt, ValidBigInt]("ValidBigInt", "1.0") + validNumber[BigDecimal, ValidBigDecimal]("ValidBigDecimal", "a") +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/api/RefTypeSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/api/RefTypeSpec.scala new file mode 100644 index 000000000..680af11e5 --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/api/RefTypeSpec.scala @@ -0,0 +1,116 @@ +package eu.timepit.refined.api + +import eu.timepit.refined.TestUtils._ +import eu.timepit.refined.api.RefType.ops._ +import eu.timepit.refined.auto._ +import eu.timepit.refined.char.{Digit, LowerCase} +import eu.timepit.refined.collection.Forall +import eu.timepit.refined.numeric._ +import eu.timepit.refined.string.MatchesRegex +import eu.timepit.refined.test.ScalaVersionSpecific.illTyped +import org.scalacheck.Prop._ +import org.scalacheck.Properties + +// Ported from the Scala 2 `RefTypeSpec`. Only the `Refined` carrier is exercised: the shapeless `@@` +// tag has no `RefType` instance on Scala 3, so the `RefTypeSpecTag` subclass is dropped, as is the +// `<:!<` (shapeless) subtyping test. +abstract class RefTypeSpec[F[_, _]](name: String)(implicit rt: RefType[F]) + extends Properties(s"RefType[$name]") { + + property("unsafeWrap.unwrap ~= id") = forAll((s: String) => rt.unsafeWrap(s).unwrap == s) + + property("unsafeRewrap.unsafeRewrap ~= id") = forAll { (c: Char) => + trait A + trait B + val c1: F[Char, A] = rt.unsafeWrap(c) + val c2: F[Char, B] = rt.unsafeRewrap(c1) + val c3: F[Char, A] = rt.unsafeRewrap(c2) + c1 == c3 + } + + property("RefinePartiallyApplied instance") = secure { + val pa = rt.refine[Digit] + pa('0').isRight + } + + property("refine success with Less") = secure { + rt.refine[Less[100]](-100).isRight + } + + property("refine failure with Interval.Closed") = secure { + rt.refine[Interval.Closed[-0.5, 0.5]](0.6).isLeft + } + + property("refine failure with Forall") = secure { + rt.refine[Forall[LowerCase]]("Hallo").isLeft + } + + property("refine success with MatchesRegex") = secure { + type DigitsOnly = MatchesRegex["[0-9]+"] + rt.refine[DigitsOnly]("123").isRight + } + + property("refine.unsafeFrom success") = secure { + rt.refine[Positive].unsafeFrom(5) ?= rt.unsafeWrap[Int, Positive](5) + } + + property("refine.unsafeFrom failure") = secure { + throws(classOf[IllegalArgumentException])(rt.refine[Positive].unsafeFrom(-5)) + } + + property("mapRefine success with Positive") = secure { + rt.refine[Positive](5).flatMap(_.mapRefine(_.toDouble)).isRight + } + + property("mapRefine failure with Positive") = secure { + rt.refine[Positive](5).flatMap(_.mapRefine(_ - 10)).isLeft + } + + property("coflatMapRefine success with Positive") = secure { + rt.refine[Positive](5).flatMap(_.coflatMapRefine(_.unwrap)).isRight + } + + property("implicit unwrap") = secure { + rt.refine[Positive](5).map(_ + 1) == Right(6) + } + + property("refine ~= RefType.applyRef") = forAll { (i: Int) => + type PosInt = F[Int, Positive] + rt.refine[Positive](i) ?= RefType.applyRef[PosInt](i) + } + + property("RefType.applyRef.unsafeFrom success") = secure { + RefType.applyRef[F[Int, Positive]].unsafeFrom(5) ?= rt.unsafeWrap[Int, Positive](5) + } + + property("RefType.applyRef.unsafeFrom failure") = secure { + throws(classOf[IllegalArgumentException])(RefType.applyRef[F[Int, Positive]].unsafeFrom(-5)) + } +} + +class RefTypeSpecRefined extends RefTypeSpec[Refined]("Refined") { + + property("refineM alias") = secure { + type PositiveInt = Int Refined Positive + + val x: PositiveInt = RefType[Refined].refineM(5) + val y: PositiveInt = 5 + val z = 5: PositiveInt + // Self-contained snippet (no block-local `PositiveInt`); the failed `autoRefineV` conversion + // surfaces as a type mismatch against the required refined type rather than "Predicate failed". + illTyped("val a: Int Refined Positive = -5", "Required: Int Refined .*Positive") + x == y && y == z + } + + property("applyRefM alias") = secure { + type Natural = Long Refined NonNegative + val Natural = RefType.applyRefM[Natural] + + val x: Natural = Natural(1L) + val y: Natural = 1L + val z = 1L: Natural + illTyped("RefType.applyRefM[Long Refined NonNegative](-1L)", "Predicate.*fail.*") + illTyped("RefType.applyRefM[Long Refined NonNegative](1.3)", "Cannot prove that") + x == y && y == z + } +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/issues/BigLiteralsSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/issues/BigLiteralsSpec.scala new file mode 100644 index 000000000..3fffa09b4 --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/issues/BigLiteralsSpec.scala @@ -0,0 +1,71 @@ +package eu.timepit.refined.issues + +import eu.timepit.refined.api.Refined +import eu.timepit.refined.auto._ +import eu.timepit.refined.numeric.Positive +import org.scalacheck.Prop._ +import org.scalacheck.Properties +import eu.timepit.refined.test.ScalaVersionSpecific.illTyped + +class BigLiteralsSpec extends Properties("BigLiterals") { + + property("autoRefineV") = secure { + val ii: BigInt Refined Positive = BigInt(1) + val il: BigInt Refined Positive = BigInt(0x7fffffffffffffffL) + val is: BigInt Refined Positive = BigInt("1") + + val dd: BigDecimal Refined Positive = BigDecimal(1.0) + val di: BigDecimal Refined Positive = BigDecimal(1) + val dl: BigDecimal Refined Positive = BigDecimal(1L) + val ds: BigDecimal Refined Positive = BigDecimal("1.0") + + val ded: BigDecimal Refined Positive = BigDecimal.exact(0.1) + val dei: BigDecimal Refined Positive = BigDecimal.exact(1) + val del: BigDecimal Refined Positive = BigDecimal.exact(1L) + val des: BigDecimal Refined Positive = BigDecimal.exact("0.1") + + val dvd: BigDecimal Refined Positive = BigDecimal.valueOf(0.3) + val dvl: BigDecimal Refined Positive = BigDecimal.valueOf(1L) + + illTyped("val err: BigInt Refined Equal[0] = BigInt(\"0\")") + illTyped("val err: BigInt Refined Equal[1] = BigInt(1)") + illTyped("val err: BigDecimal Refined Equal[0.0] = BigDecimal(0.0)") + illTyped("val err: BigDecimal Refined Equal[0.0] = BigDecimal.exact(\"0.0\")") + + // These fail via the rejected `autoRefineV` conversion, which surfaces as a type mismatch against + // the required refined type (rather than the Scala 2 "Predicate failed" / "compile-time refinement" + // macro messages). The non-constant case uses a self-contained `def` (no block-local `ii`). + illTyped("val err: BigInt Refined Positive = BigInt(0)", "Required: BigInt Refined .*Positive") + illTyped( + "{ def n: Int = 0; val err: BigInt Refined Positive = BigInt(n) }", + "Required: BigInt Refined .*Positive" + ) + illTyped( + "val err: BigInt Refined Positive = BigInt(\"0.1\")", + "Required: BigInt Refined .*Positive" + ) + illTyped( + "val err: BigInt Refined Positive = BigInt(java.math.BigInteger.ZERO)", + "Required: BigInt Refined .*Positive" + ) + illTyped( + "val err: BigDecimal Refined Positive = BigDecimal(java.math.BigDecimal.ZERO)", + "Required: BigDecimal Refined .*Positive" + ) + + (ii.value ?= BigInt(1)) && + (il.value ?= BigInt(Long.MaxValue)) && + (is.value ?= BigInt(1)) && + (dd.value ?= BigDecimal(1.0)) && + (di.value ?= BigDecimal(1)) && + (dl.value ?= BigDecimal(1L)) && + (ds.value ?= BigDecimal("1.0")) && + (ded.value ?= BigDecimal.exact(0.1)) && + (dei.value ?= BigDecimal.exact(1)) && + (del.value ?= BigDecimal.exact(1L)) && + (des.value ?= BigDecimal.exact("0.1")) && + (dvd.value ?= BigDecimal.valueOf(0.3)) && + (dvl.value ?= BigDecimal.valueOf(1L)) + } + +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/test/ScalaVersionSpecific.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/test/ScalaVersionSpecific.scala new file mode 100644 index 000000000..6092450b4 --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/test/ScalaVersionSpecific.scala @@ -0,0 +1,38 @@ +package eu.timepit.refined.test + +import scala.compiletime.testing.{Error, typeCheckErrors} + +object ScalaVersionSpecific { + + /** + * Scala 3 stand-in for `shapeless.test.illTyped`, used by the ported test + * suites. It asserts that `code` does not compile and, when a pattern is + * given, that at least one reported error message matches it. + * + * The pattern is matched against the Scala 3 compiler's diagnostics, which + * differ from Scala 2's — so the ported call sites use Scala-3-accurate + * patterns rather than the originals. + */ + object illTyped { + + inline def apply(inline code: String): Unit = + check(typeCheckErrors(code), code, None) + + inline def apply(inline code: String, inline expected: String): Unit = + check(typeCheckErrors(code), code, Some(expected)) + + // Deliberately a plain method with an explicit `throw` (not `assert`, which is elidable) so the + // check can never be silently compiled out. + private def check(errors: List[Error], code: String, expected: Option[String]): Unit = { + if (errors.isEmpty) + throw new AssertionError(s"Expected a compile error, but the code type-checked:\n$code") + expected.foreach { pattern => + val message = errors.iterator.map(_.message).mkString("\n") + if (pattern.r.findFirstMatchIn(message).isEmpty) + throw new AssertionError( + s"Expected a compile error matching /$pattern/, but got:\n$message" + ) + } + } + } +} diff --git a/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/types/StringTypesSpec.scala b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/types/StringTypesSpec.scala new file mode 100644 index 000000000..f108273ce --- /dev/null +++ b/modules/core/shared/src/test/scala-3.0+/eu/timepit/refined/types/StringTypesSpec.scala @@ -0,0 +1,135 @@ +package eu.timepit.refined.types + +import eu.timepit.refined.TestUtils.wellTyped +import eu.timepit.refined.types.all._ +import eu.timepit.refined.types.string.NonEmptyFiniteString +import org.scalacheck.{Prop, Properties} +import org.scalacheck.Prop._ + +class StringTypesSpec extends Properties("StringTypes") { + + final val FString3 = FiniteString[3] + + property("FString3.from(str)") = forAll { (str: String) => + FString3.from(str).isRight ?= (str.length <= FString3.maxLength) + } + + property("""FString3.from("")""") = secure { + val str = "" + FString3.from(str).map(_.value) ?= Right(str) + } + + property("""FString3.from("abc")""") = secure { + val str = "abc" + FString3.from(str).map(_.value) ?= Right(str) + } + + property("""FString3.from("abcd")""") = secure { + val str = "abcd" + FString3.from(str) ?= Left( + "Predicate taking size(abcd) = 4 failed: Right predicate of (!(4 < 0) && !(4 > 3)) failed: Predicate (4 > 3) did not fail." + ) + } + + property("""FString3.truncate(str)""") = forAll { (str: String) => + val truncated = FString3.truncate(str) + truncated.value.length <= FString3.maxLength && + (truncated.value ?= str.take(FString3.maxLength)) + } + + property("""TrimmedString.trim(str)""") = forAll { (str: String) => + val trimmed = TrimmedString.trim(str) + TrimmedString.from(trimmed.value) ?= Right(trimmed) + } + + final val NEFString3 = NonEmptyFiniteString[3] + + property("NEFString3.from(str)") = forAll { (str: String) => + NEFString3.from(str).isRight ?= (!str.isEmpty && str.length <= NEFString3.maxLength) + } + + property("""NEFString3.from("")""") = secure { + val str = "" + NEFString3.from(str) ?= Left( + "Predicate taking size() = 0 failed: Left predicate of (!(0 < 1) && !(0 > 3)) failed: Predicate (0 < 1) did not fail." + ) + } + + property("""NEFString3.from("abc")""") = secure { + val str = "abc" + NEFString3.from(str).map(_.value) ?= Right(str) + } + + property("""NEFString3.from("abcd")""") = secure { + val str = "abcd" + NEFString3.from(str) ?= Left( + "Predicate taking size(abcd) = 4 failed: Right predicate of (!(4 < 1) && !(4 > 3)) failed: Predicate (4 > 3) did not fail." + ) + } + + property("""NEFString3.truncate(str)""") = forAll { (str: String) => + val truncated = NEFString3.truncate(str) + truncated.fold(Prop(str.isEmpty))(nefs => + nefs.value.length <= NEFString3.maxLength && (nefs.value ?= str.take(NEFString3.maxLength)) + ) + } + + property("NEFString implies NEString") = wellTyped { + import eu.timepit.refined.auto._ + val s1 = NEFString3.unsafeFrom("abc") + val s2: NonEmptyString = s1 + } + + // Hashes for "" + object EmptyString { + val md5 = "d41d8cd98f00b204e9800998ecf8427e" + val sha1 = "da39a3ee5e6b4b0d3255bfef95601890afd80709" + val sha224 = "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f" + val sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + val sha384 = + "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b" + val sha512 = + "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" + } + + property("lower-case hexadecimal number") = forAll { (i: Int) => + val hex = Integer.toHexString(i) + HexString.from(hex).isRight + } + + property("upper-case hexadecimal number") = forAll { (i: Int) => + val hex = Integer.toHexString(i).toUpperCase + HexString.from(hex).isRight + } + + property("mixed-case hexadecimal") = forAll { (i: Int, j: Int) => + val hex1 = Integer.toHexString(i).toUpperCase + "A" + val hex2 = Integer.toHexString(j) + "a" + + HexString.from(hex1 + hex2).isLeft + } + + property(s"MD5.from(${EmptyString.md5})") = secure { + MD5.from(EmptyString.md5).isRight + } + + property(s"SHA1.from(${EmptyString.sha1})") = secure { + SHA1.from(EmptyString.sha1).isRight + } + + property(s"SHA224.from(${EmptyString.sha224})") = secure { + SHA224.from(EmptyString.sha224).isRight + } + + property(s"SHA256.from(${EmptyString.sha256})") = secure { + SHA256.from(EmptyString.sha256).isRight + } + + property(s"SHA384.from(${EmptyString.sha384})") = secure { + SHA384.from(EmptyString.sha384).isRight + } + + property(s"SHA512.from(${EmptyString.sha512})") = secure { + SHA512.from(EmptyString.sha512).isRight + } +}