From 98d7bd7f8daadf658ac33ff027c29bdf14ef7ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Raddum=20Berg?= Date: Sun, 25 Aug 2019 22:40:46 +0200 Subject: [PATCH 1/2] Add NullUndefOr --- build.sbt | 4 +- .../scalablytyped/runtime/NullUndefOr.scala | 353 ++++++++++++++++++ .../scalablytyped/runtime/AssertThrows.scala | 59 +++ .../org/scalablytyped/runtime/JSAssert.scala | 10 + .../scalablytyped/runtime/NullOrTest.scala | 202 ++++++++++ .../runtime/NullUndefOrTest.scala | 205 ++++++++++ .../org/scalablytyped/runtime/TypeTests.scala | 43 +++ .../scalablytyped/runtime/UndefOrTest.scala | 197 ++++++++++ 8 files changed, 1071 insertions(+), 2 deletions(-) create mode 100644 src/main/scala/org/scalablytyped/runtime/NullUndefOr.scala create mode 100644 src/test/scala/org/scalablytyped/runtime/AssertThrows.scala create mode 100644 src/test/scala/org/scalablytyped/runtime/JSAssert.scala create mode 100644 src/test/scala/org/scalablytyped/runtime/NullOrTest.scala create mode 100644 src/test/scala/org/scalablytyped/runtime/NullUndefOrTest.scala create mode 100644 src/test/scala/org/scalablytyped/runtime/TypeTests.scala create mode 100644 src/test/scala/org/scalablytyped/runtime/UndefOrTest.scala diff --git a/build.sbt b/build.sbt index be2c29f..0189844 100644 --- a/build.sbt +++ b/build.sbt @@ -1,9 +1,9 @@ -enablePlugins(spray.boilerplate.BoilerplatePlugin, ScalaJSPlugin) +enablePlugins(spray.boilerplate.BoilerplatePlugin, ScalaJSPlugin, ScalaJSJUnitPlugin) crossScalaVersions := List("2.12.8", "2.13.0") scalaVersion := "2.12.8" organization := "com.olvind" -version := "2.1.0" +version := "2.2.0-M1" scalacOptions ++= { if (scalaJSVersion.startsWith("0.6.")) Seq("-P:scalajs:sjsDefinedByDefault") else Nil diff --git a/src/main/scala/org/scalablytyped/runtime/NullUndefOr.scala b/src/main/scala/org/scalablytyped/runtime/NullUndefOr.scala new file mode 100644 index 0000000..d19011b --- /dev/null +++ b/src/main/scala/org/scalablytyped/runtime/NullUndefOr.scala @@ -0,0 +1,353 @@ +package org.scalablytyped.runtime + +import scala.language.{higherKinds, implicitConversions} +import scala.scalajs.js +import scala.scalajs.js.| +import scala.scalajs.js.|.Evidence + +/** + * The value, `null` or `undefined` + */ +sealed trait NullUndefOr[+A] extends js.Any +object NullUndefOr extends Companion { + override type F[+A] = NullUndefOr[A] + + /* Duplicated for intellij. name it `apply` so users can be explicit */ + @inline implicit final def apply[A](a: A): NullUndefOr[A] = + a.asInstanceOf[NullUndefOr[A]] + + /* Integrate with Scala.js built-in type */ + @inline implicit def fromJsUndefOr[A](a: js.UndefOr[A]): NullUndefOr[A] = + a.asInstanceOf[NullUndefOr[A]] + + /* This is pretty arbitrary, but choose `undefined` as the empty value */ + @inline def Empty: NullUndefOr[Nothing] = + UndefOr.Empty + + @inline implicit final class Ops[A](val self: NullUndefOr[A]) extends AnyVal with OpsBase[NullUndefOr, A] { + @inline override def isEmpty: Boolean = + self == null || js.isUndefined(self) + + @inline override protected def Empty: NullUndefOr[Nothing] = + NullUndefOr.Empty + } +} + +/** + * The value or `null` + */ +sealed trait NullOr[+A] extends NullUndefOr[A] +object NullOr extends Companion { + override type F[+A] = NullOr[A] + + /* Duplicated for intellij. name it `apply` so users can be explicit */ + @inline implicit final def apply[A](a: A): NullOr[A] = + a.asInstanceOf[NullOr[A]] + + @inline def Empty: NullOr[Nothing] = + null + + @inline implicit final class Ops[A](val self: NullOr[A]) extends AnyVal with OpsBase[NullOr, A] { + @inline override def isEmpty: Boolean = + self == NullOr.Empty + + @inline override protected def Empty: NullOr[Nothing] = + NullOr.Empty + } +} + +/** + * The value or `undefined` + */ +sealed trait UndefOr[+A] extends NullUndefOr[A] +object UndefOr extends Companion { + override type F[+A] = UndefOr[A] + + /* Duplicated for intellij. name it `apply` so users can be explicit */ + @inline implicit final def apply[A](a: A): UndefOr[A] = + a.asInstanceOf[UndefOr[A]] + + /* Integrate with Scala.js built-in type */ + @inline implicit def fromJsUndefOr[A](a: js.UndefOr[A]): UndefOr[A] = + a.asInstanceOf[UndefOr[A]] + + @inline def Empty: UndefOr[Nothing] = + ().asInstanceOf[UndefOr[Nothing]] + + @inline implicit final class Ops[A](val self: UndefOr[A]) extends AnyVal with OpsBase[UndefOr, A] { + @inline override def isEmpty: Boolean = + js.isUndefined(self) + + @inline override protected def Empty: UndefOr[Nothing] = + UndefOr.Empty + } +} + +/** + * This can represent when a superclass has an optional field and a subclass narrows it to a required field + */ +sealed trait Required[+A] extends NullOr[A] with UndefOr[A] +object Required extends Companion { + override type F[+A] = Required[A] + + /* Duplicated for intellij. name it `apply` so users can be explicit */ + @inline implicit final def apply[A](a: A): Required[A] = + a.asInstanceOf[Required[A]] + + @inline implicit final class Ops[A](val self: Required[A]) extends AnyVal { + @inline def get: A = + self.asInstanceOf[A] + } +} + +trait Companion { + type F[+A] <: NullUndefOr[A] + + /** Upcast `A` to `F[B1 | B2]`. + * + * This needs evidence that `A <: B1 | B2`. + */ + @inline implicit final def upcastUnion[A, B1, B2](a: A)(implicit ev: Evidence[A, B1 | B2]): F[B1 | B2] = + a.asInstanceOf[F[B1 | B2]] + + @inline implicit final def isJsAny[A](value: F[A])(implicit ev: A => js.Any): js.Any = + value.map(ev).asInstanceOf[js.Any] +} + +sealed trait OpsBase[F[+_], A] extends Any { ops => + @inline protected def self: F[A] + + @inline protected def Empty: F[Nothing] + + /** Returns true if the option is empty, false otherwise. */ + @inline def isEmpty: Boolean + + /** Returns true if the option is not empty. + */ + @inline final def isDefined: Boolean = + !isEmpty + + /** Returns the option's value. + * @note The option must be nonEmpty. + * @throws java.util.NoSuchElementException if the option is empty. + */ + @inline final def get: A = + if (isEmpty) throw new NoSuchElementException(s"Value was empty: $self") + else self.asInstanceOf[A] + + @inline final private def forceGet: A = + self.asInstanceOf[A] + + /** Returns the option's value if the option is nonempty, otherwise + * return the result of evaluating `default`. + * + * @param default the default expression. + */ + @inline final def getOrElse[B >: A](default: => B): B = + if (isEmpty) default else forceGet + + /** Returns the option's value if it is nonempty, + * or `null` if it is empty. + * Although the use of null is discouraged, code written to use + * $option must often interface with code that expects and returns nulls. + * @example {{{ + * val initalText: Option[String] = getInitialText + * val textField = new JComponent(initalText.orNull,20) + * }}} + */ + @inline final def orNull[A1 >: A](implicit ev: Null <:< A1): A1 = + this getOrElse ev(null) + + /** Returns the result of applying `f` to this $option's + * value if this $option is nonempty. + * Otherwise return $none. + * + * @note This is similar to `flatMap` except here, + * `f` does not need to wrap its result in an $option. + * + * @param f the function to apply + * @see flatMap + * @see foreach + */ + @inline final def map[B](f: A => B): F[B] = + (if (isEmpty) self else f(forceGet)).asInstanceOf[F[B]] + + /** Returns the result of applying `f` to this $option's + * value if the $option is nonempty. Otherwise, evaluates + * expression `ifEmpty`. + * + * @note This is equivalent to `$option map f getOrElse ifEmpty`. + * + * @param ifEmpty the expression to evaluate if empty. + * @param f the function to apply if nonempty. + */ + @inline final def fold[B](ifEmpty: => B)(f: A => B): B = + if (isEmpty) ifEmpty else f(forceGet) + + /** Returns the result of applying `f` to this $option's value if + * this $option is nonempty. + * Returns $none if this $option is empty. + * Slightly different from `map` in that `f` is expected to + * return an $option (which could be $none). + * + * @param f the function to apply + * @see map + * @see foreach + */ + @inline final def flatMap[B](f: A => F[B]): F[B] = + if (isEmpty) self.asInstanceOf[F[B]] else f(forceGet) + + @inline def flatten[B](implicit ev: A <:< F[B]): F[B] = + if (isEmpty) self.asInstanceOf[F[B]] else ev(forceGet) + + /** Returns this $option if it is nonempty '''and''' applying the predicate `p` to + * this $option's value returns true. Otherwise, return $none. + * + * @param p the predicate used for testing. + */ + @inline final def filter(p: A => Boolean): F[A] = + if (isEmpty || p(forceGet)) self else Empty + + /** Returns this $option if it is nonempty '''and''' applying the predicate `p` to + * this $option's value returns false. Otherwise, return $none. + * + * @param p the predicate used for testing. + */ + @inline final def filterNot(p: A => Boolean): F[A] = + if (isEmpty || !p(forceGet)) self else Empty + + /** Returns false if the option is $none, true otherwise. + * @note Implemented here to avoid the implicit conversion to Iterable. + */ + @inline final def nonEmpty: Boolean = + isDefined + + /** We need a whole WithFilter class to honor the "doesn't create a new + * collection" contract even though it seems unlikely to matter much in a + * collection with max size 1. + */ + @inline final class WithFilter(p: A => Boolean) { + @inline def map[B](f: A => B): F[B] = + if (isEmpty) self.asInstanceOf[F[B]] + else if (!p(ops.forceGet)) Empty.asInstanceOf[F[B]] + else ops.map(f) + + @inline def flatMap[B](f: A => F[B]): F[B] = + if (isEmpty) self.asInstanceOf[F[B]] + else if (!p(ops.forceGet)) Empty.asInstanceOf[F[B]] + else ops.flatMap(f) + + @inline def foreach[U](f: A => U): Unit = + if (isEmpty || !p(ops.forceGet)) () + else ops.foreach(f) + + @inline def withFilter(q: A => Boolean): WithFilter = + new WithFilter(x => p(x) && q(x)) + } + + /** Necessary to keep $option from being implicitly converted to + * [[scala.collection.Iterable]] in `for` comprehensions. + */ + @inline final def withFilter(p: A => Boolean): WithFilter = + new WithFilter(p) + + /** Tests whether the $option contains a given value as an element. + * + * `x.contains(y)` differs from `x == y` only in the fact that it will + * return `false` when `x` and `y` are both `undefined[F]`. + * + * @param elem the element to test. + * @return `true` if the $option has an element that is equal (as + * determined by `==`) to `elem`, `false` otherwise. + */ + @inline final def contains[A1 >: A](elem: A1): Boolean = + !isEmpty && elem == forceGet + + /** Returns true if this option is nonempty '''and''' the predicate + * `p` returns true when applied to this $option's value. + * Otherwise, returns false. + * + * @param p the predicate to test + */ + @inline final def exists(p: A => Boolean): Boolean = + !isEmpty && p(forceGet) + + /** Returns true if this option is empty '''or''' the predicate + * `p` returns true when applied to this $option's value. + * + * @param p the predicate to test + */ + @inline final def forall(p: A => Boolean): Boolean = + isEmpty || p(forceGet) + + /** Apply the given procedure `f` to the option's value, + * if it is nonempty. Otherwise, do nothing. + * + * @param f the procedure to apply. + * @see map + * @see flatMap + */ + @inline final def foreach[U](f: A => U): Unit = + if (!isEmpty) f(forceGet) + + /** Returns the result of applying `pf` to this $option's contained + * value, '''if''' this option is + * nonempty '''and''' `pf` is defined for that value. + * Returns $none otherwise. + * + * @param pf the partial function. + * @return the result of applying `pf` to this $option's + * value (if possible), or $none. + */ + @inline final def collect[B](pf: PartialFunction[A, B]): F[B] = + if (isEmpty) self.asInstanceOf[F[B]] + else pf.applyOrElse(forceGet, (_: A) => Empty).asInstanceOf[F[B]] + + /** Returns this $option if it is nonempty, + * otherwise return the result of evaluating `alternative`. + * @param alternative the alternative expression. + */ + @inline final def orElse[B >: A](alternative: => F[B]): F[B] = + if (isEmpty) alternative else self + + /** Returns a singleton iterator returning the $option's value + * if it is nonempty, or an empty iterator if the option is empty. + */ + def iterator: scala.collection.Iterator[A] = + if (isEmpty) scala.collection.Iterator.empty + else scala.collection.Iterator.single(forceGet) + + /** Returns a singleton list containing the $option's value + * if it is nonempty, or the empty list if the $option is empty. + */ + def toList: List[A] = + if (isEmpty) Nil else forceGet :: Nil + + /** Returns a `Left` containing the given argument `left` if this $option is + * empty, or a `Right` containing this $option's value if this is nonempty. + * + * @param left the expression to evaluate and return if this is empty + * @see toLeft + */ + @inline final def toRight[X](left: => X): Either[X, A] = + if (isEmpty) Left(left) else Right(forceGet) + + /** Returns a `Right` containing the given argument `right` if this is empty, + * or a `Left` containing this $option's value if this $option is nonempty. + * + * @param right the expression to evaluate and return if this is empty + * @see toRight + */ + @inline final def toLeft[X](right: => X): Either[A, X] = + if (isEmpty) Right(right) else Left(forceGet) + + /** Returns a `Some` containing this $option's value + * if this $option is nonempty, [[scala.None None]] otherwise. + */ + @inline final def toOption: Option[A] = + if (isEmpty) None else Some(forceGet) + + /** Interop with Scala.js built-in type */ + @inline def toJsUndefOr: js.UndefOr[A] = + if (isEmpty) js.undefined else self.asInstanceOf[js.UndefOr[A]] +} diff --git a/src/test/scala/org/scalablytyped/runtime/AssertThrows.scala b/src/test/scala/org/scalablytyped/runtime/AssertThrows.scala new file mode 100644 index 0000000..2ec9125 --- /dev/null +++ b/src/test/scala/org/scalablytyped/runtime/AssertThrows.scala @@ -0,0 +1,59 @@ +package org.scalablytyped.runtime + +object AssertThrows { + + /** Backport implementation of Assert.assertThrows to be used until JUnit 4.13 is + * released. See org.junit.Assert.scala in jUnitRuntime. + */ + private def assertThrowsBackport(expectedThrowable: Class[_ <: Throwable], runnable: ThrowingRunnable): Unit = + expectThrowsBackport(expectedThrowable, runnable) + + /** Backport implementation of Assert.expectThrows to be used until JUnit 4.13 is + * released. See org.junit.Assert.scala in jUnitRuntime. + */ + private def expectThrowsBackport[T <: Throwable](expectedThrowable: Class[T], runnable: ThrowingRunnable): T = { + val result = { + try { + runnable.run() + null.asInstanceOf[T] + } catch { + case actualThrown: Throwable => + if (expectedThrowable.isInstance(actualThrown)) { + actualThrown.asInstanceOf[T] + } else { + val mismatchMessage = "unexpected exception type thrown;" + + expectedThrowable.getSimpleName + " " + actualThrown.getClass.getSimpleName + + val assertionError = new AssertionError(mismatchMessage) + assertionError.initCause(actualThrown) + throw assertionError + } + } + } + if (result == null) { + throw new AssertionError( + "expected " + expectedThrowable.getSimpleName + + " to be thrown, but nothing was thrown" + ) + } else { + result + } + } + + /** Backport implementation of Assert.ThrowingRunnable to be used until + * JUnit 4.13 is released. See org.junit.Assert.scala in jUnitRuntime. + */ + private trait ThrowingRunnable { + def run(): Unit + } + + private def throwingRunnable(code: => Unit): ThrowingRunnable = { () => + code + } + + def assertThrows[T <: Throwable, U](expectedThrowable: Class[T], code: => U): Unit = + assertThrowsBackport(expectedThrowable, throwingRunnable { code }) + + def expectThrows[T <: Throwable, U](expectedThrowable: Class[T], code: => U): T = + expectThrowsBackport(expectedThrowable, throwingRunnable { code }) +} diff --git a/src/test/scala/org/scalablytyped/runtime/JSAssert.scala b/src/test/scala/org/scalablytyped/runtime/JSAssert.scala new file mode 100644 index 0000000..7aed8f4 --- /dev/null +++ b/src/test/scala/org/scalablytyped/runtime/JSAssert.scala @@ -0,0 +1,10 @@ +package org.scalablytyped.runtime + +import org.junit.Assert._ + +import scala.scalajs.js + +object JSAssert { + def assertJSUndefined(obj: Any): Unit = + assertTrue(s"Expected <$obj> to be .", js.isUndefined(obj)) +} diff --git a/src/test/scala/org/scalablytyped/runtime/NullOrTest.scala b/src/test/scala/org/scalablytyped/runtime/NullOrTest.scala new file mode 100644 index 0000000..ccda856 --- /dev/null +++ b/src/test/scala/org/scalablytyped/runtime/NullOrTest.scala @@ -0,0 +1,202 @@ +package org.scalablytyped.runtime + +import org.junit.Assert._ +import org.junit.Test +import org.scalablytyped.runtime.AssertThrows._ +import org.scalablytyped.runtime.JSAssert._ + +import scala.scalajs.js + +class NullOrTest { + def some[A](v: A): NullOr[A] = v + def none[A]: NullOr[A] = null + + @Test def convert_A_to_js_UndefOr_A(): Unit = { + val x: NullOr[Int] = 42 + assertFalse(x.isEmpty) + assertTrue(x.isDefined) + assertTrue(x.nonEmpty) + assertEquals(42, x.get) + } + + @Test def convert_undefined_to_js_UndefOr_A(): Unit = { + val x: NullOr[Int] = none + assertTrue(x.isEmpty) + assertFalse(x.isDefined) + assertFalse(x.nonEmpty) + assertThrows(classOf[NoSuchElementException], x.get) + } + + @Test def convert_undefined_to_js_UndefOr_A_null(): Unit = { + val x: NullOr[Int] = null + assertTrue(x.isEmpty) + assertFalse(x.isDefined) + assertFalse(x.nonEmpty) + assertThrows(classOf[NoSuchElementException], x.get) + } + + @Test def explicitly_convert_A_to_js_UndefOr_A(): Unit = { + val x: NullOr[Int] = some(42) + assertFalse(x.isEmpty) + assertEquals(42, x.get) + + val f: NullOr[js.Function1[Int, Int]] = some((x: Int) => x + 1) + assertFalse(f.isEmpty) + assertEquals(6, f.get(5)) + } + + @Test def `convert_to_js_Any_when_A_<%_js_Any`(): Unit = { + val x: NullOr[Int] = 42 + assertEquals(42, x) + + val y: NullOr[String] = none + assertNull(y) + } + + @Test def getOrElse(): Unit = { + assertEquals("hello", some("hello").getOrElse("ko")) + assertEquals("ok", none[String].getOrElse("ok")) + + var defaultComputed = false + assertEquals("test", some("test") getOrElse { + defaultComputed = true + "ko" + }) + assertFalse(defaultComputed) + } + + @Test def orNull(): Unit = { + assertEquals("hello", some("hello").orNull) + assertNull(none[String].orNull) + } + + @Test def map(): Unit = { + assertEquals(62 / 3, some(62).map(_ / 3)) + assertNull(none[Int].map(_ / 3)) + } + + @Test def fold(): Unit = { + assertEquals(6, some(3).fold(10)(_ * 2)) + assertEquals(10, none[Int].fold(10)(_ * 2)) + } + + @Test def flatMap(): Unit = { + def f(x: Int): NullOr[Int] = if (x > 0) x+3 else none + assertEquals(9, some(6).flatMap(f)) + assertNull(some(-6).flatMap(f)) + assertNull(none[Int].flatMap(f)) + } + + @Test def flatten(): Unit = { + assertTrue(some(some(7)).flatten.isDefined) + assertEquals(7, some(some(7)).flatten.get) + assertFalse(some(none[Int]).flatten.isDefined) + assertFalse(none[NullOr[Int]].flatten.isDefined) + } + + @Test def filter(): Unit = { + assertTrue(some(7).filter(_ > 0).isDefined) + assertEquals(7, some(7).filter(_ > 0).get) + assertFalse(some(7).filter(_ < 0).isDefined) + assertFalse(none[Int].filter(_ < 0).isDefined) + } + + @Test def filterNot(): Unit = { + assertTrue(some(7).filterNot(_ < 0).isDefined) + assertEquals(7, some(7).filterNot(_ < 0).get) + assertFalse(some(7).filterNot(_ > 0).isDefined) + assertFalse(none[Int].filterNot(_ > 0).isDefined) + } + + @Test def contains(): Unit = { + assertTrue(some(7).contains(7)) + assertFalse(some(7).contains(8)) + assertFalse(none[Int].contains(7)) + + assertFalse(some(null).contains(())) + } + + @Test def exists(): Unit = { + assertTrue(some(7).exists(_ > 0)) + assertFalse(some(7).exists(_ < 0)) + assertFalse(none[Int].exists(_ > 0)) + } + + @Test def forall(): Unit = { + assertTrue(some(7).forall(_ > 0)) + assertFalse(some(7).forall(_ < 0)) + assertTrue(none[Int].forall(_ > 0)) + } + + @Test def foreach(): Unit = { + var witness1 = 3 + some(42).foreach(witness1 = _) + assertEquals(42, witness1) + + var witness2 = 3 + none[Int].foreach(witness2 = _) + assertEquals(3, witness2) + } + + @Test def collect(): Unit = { + assertEquals("ok", some("hello") collect { + case "hello" => "ok" + }) + assertNull((some("hello") collect { + case "notthis" => "ko" + })) + assertNull((none[String] collect { + case "hello" => "ko" + })) + } + + @Test def collect_should_call_guard_at_most_once(): Unit = { + var witness = 0 + def guard(x: String): Boolean = { + witness += 1 + true + } + assertEquals("ok", some("hello") collect { + case x @ "hello" if guard(x) => "ok" + }) + assertEquals(1, witness) + } + + @Test def orElse(): Unit = { + assertTrue((some(true) orElse some(false)).get) + assertEquals("ok", some("ok") orElse none) + assertEquals("yes", none orElse some("yes")) + assertNull(none orElse none) + + // #2095 + assertEquals("ok", some("ok") orElse "yes") + assertEquals("yes", none orElse "yes") + } + + @Test def toList(): Unit = { + assertEquals(List("hello"), some("hello").toList) + assertEquals(List.empty[String], none[String].toList) + } + + @Test def toLeft_and_toRight(): Unit = { + assertTrue(some("left").toLeft("right").isInstanceOf[Left[_, _]]) + assertTrue(none[String].toLeft("right").isInstanceOf[Right[_, _]]) + assertTrue(some("right").toRight("left").isInstanceOf[Right[_, _]]) + assertTrue(none[String].toRight("left").isInstanceOf[Left[_, _]]) + } + + @Test def toOption(): Unit = { + assertTrue(some("foo").toOption == Some("foo")) + assertTrue(none.toOption == None) + } + + // scala.scalajs.js.JSConverters.JSRichOption + + import js.JSConverters._ + + @Test def should_provide_orUndefined(): Unit = { + assertEquals("asdf", Some("asdf").orUndefined) + assertJSUndefined((None: Option[String]).orUndefined) + assertJSUndefined(None.orUndefined) + } +} diff --git a/src/test/scala/org/scalablytyped/runtime/NullUndefOrTest.scala b/src/test/scala/org/scalablytyped/runtime/NullUndefOrTest.scala new file mode 100644 index 0000000..a8ee0eb --- /dev/null +++ b/src/test/scala/org/scalablytyped/runtime/NullUndefOrTest.scala @@ -0,0 +1,205 @@ +package org.scalablytyped.runtime + +import org.junit.Assert._ +import org.junit.Test +import org.scalablytyped.runtime.AssertThrows._ +import org.scalablytyped.runtime.JSAssert._ + +import scala.scalajs.js + +class NullUndefOrTest { + def some[A](v: A): NullUndefOr[A] = v + def none[A]: NullUndefOr[A] = ().asInstanceOf[NullUndefOr[A]] + + // scala.scalajs.NullUndefOr[A] + + @Test def convert_A_to_js_UndefOr_A(): Unit = { + val x: NullUndefOr[Int] = 42 + assertFalse(x.isEmpty) + assertTrue(x.isDefined) + assertTrue(x.nonEmpty) + assertEquals(42, x.get) + } + + @Test def convert_undefined_to_js_UndefOr_A(): Unit = { + val x: NullUndefOr[Int] = none + assertTrue(x.isEmpty) + assertFalse(x.isDefined) + assertFalse(x.nonEmpty) + assertThrows(classOf[NoSuchElementException], x.get) + } + + @Test def convert_undefined_to_js_UndefOr_A_null(): Unit = { + val x: NullUndefOr[Int] = null + assertTrue(x.isEmpty) + assertFalse(x.isDefined) + assertFalse(x.nonEmpty) + assertThrows(classOf[NoSuchElementException], x.get) + } + + @Test def explicitly_convert_A_to_js_UndefOr_A(): Unit = { + val x: NullUndefOr[Int] = some(42) + assertFalse(x.isEmpty) + assertEquals(42, x.get) + + val f: NullUndefOr[js.Function1[Int, Int]] = some((x: Int) => x + 1) + assertFalse(f.isEmpty) + assertEquals(6, f.get(5)) + } + + @Test def `convert_to_js_Any_when_A_<%_js_Any`(): Unit = { + val x: NullUndefOr[Int] = 42 + assertEquals(42, x) + + val y: NullUndefOr[String] = none + assertJSUndefined(y) + } + + @Test def getOrElse(): Unit = { + assertEquals("hello", some("hello").getOrElse("ko")) + assertEquals("ok", none[String].getOrElse("ok")) + + var defaultComputed = false + assertEquals("test", some("test") getOrElse { + defaultComputed = true + "ko" + }) + assertFalse(defaultComputed) + } + + @Test def orNull(): Unit = { + assertEquals("hello", some("hello").orNull) + assertNull(none[String].orNull) + } + + @Test def map(): Unit = { + assertEquals(62 / 3, some(62).map(_ / 3)) + assertJSUndefined(none[Int].map(_ / 3)) + } + + @Test def fold(): Unit = { + assertEquals(6, some(3).fold(10)(_ * 2)) + assertEquals(10, none[Int].fold(10)(_ * 2)) + } + + @Test def flatMap(): Unit = { + def f(x: Int): NullUndefOr[Int] = if (x > 0) x + 3 else none + assertEquals(9, some(6).flatMap(f)) + assertJSUndefined(some(-6).flatMap(f)) + assertJSUndefined(none[Int].flatMap(f)) + } + + @Test def flatten(): Unit = { + assertTrue(some(some(7)).flatten.isDefined) + assertEquals(7, some(some(7)).flatten.get) + assertFalse(some(none[Int]).flatten.isDefined) + assertFalse(none[NullUndefOr[Int]].flatten.isDefined) + } + + @Test def filter(): Unit = { + assertTrue(some(7).filter(_ > 0).isDefined) + assertEquals(7, some(7).filter(_ > 0).get) + assertFalse(some(7).filter(_ < 0).isDefined) + assertFalse(none[Int].filter(_ < 0).isDefined) + } + + @Test def filterNot(): Unit = { + assertTrue(some(7).filterNot(_ < 0).isDefined) + assertEquals(7, some(7).filterNot(_ < 0).get) + assertFalse(some(7).filterNot(_ > 0).isDefined) + assertFalse(none[Int].filterNot(_ > 0).isDefined) + } + + @Test def contains(): Unit = { + assertTrue(some(7).contains(7)) + assertFalse(some(7).contains(8)) + assertFalse(none[Int].contains(7)) + + assertFalse(some(()).contains(())) + } + + @Test def exists(): Unit = { + assertTrue(some(7).exists(_ > 0)) + assertFalse(some(7).exists(_ < 0)) + assertFalse(none[Int].exists(_ > 0)) + } + + @Test def forall(): Unit = { + assertTrue(some(7).forall(_ > 0)) + assertFalse(some(7).forall(_ < 0)) + assertTrue(none[Int].forall(_ > 0)) + } + + @Test def foreach(): Unit = { + var witness1 = 3 + some(42).foreach(witness1 = _) + assertEquals(42, witness1) + + var witness2 = 3 + none[Int].foreach(witness2 = _) + assertEquals(3, witness2) + } + + @Test def collect(): Unit = { + assertEquals("ok", some("hello") collect { + case "hello" => "ok" + }) + assertTrue(js.isUndefined(some("hello") collect { + case "notthis" => "ko" + })) + assertTrue(js.isUndefined(none[String] collect { + case "hello" => "ko" + })) + } + + @Test def collect_should_call_guard_at_most_once(): Unit = { + var witness = 0 + def guard(x: String): Boolean = { + witness += 1 + true + } + assertEquals("ok", some("hello") collect { + case x @ "hello" if guard(x) => "ok" + }) + assertEquals(1, witness) + } + + @Test def orElse(): Unit = { + assertTrue((some(true) orElse some(false)).get) + assertEquals("ok", some("ok") orElse none) + assertEquals("yes", none orElse some("yes")) + assertJSUndefined(none orElse none) + + // #2095 + assertEquals("ok", some("ok") orElse "yes") + assertEquals("yes", none orElse "yes") + } + + @Test def toList(): Unit = { + assertEquals(List("hello"), some("hello").toList) + assertEquals(List.empty[String], none[String].toList) + } + + @Test def toLeft_and_toRight(): Unit = { + assertTrue(some("left").toLeft("right").isInstanceOf[Left[_, _]]) + assertTrue(none[String].toLeft("right").isInstanceOf[Right[_, _]]) + assertTrue(some("right").toRight("left").isInstanceOf[Right[_, _]]) + assertTrue(none[String].toRight("left").isInstanceOf[Left[_, _]]) + } + + @Test def toOption(): Unit = { + assertTrue(some("foo").toOption == Some("foo")) + assertTrue(none.toOption == None) + } + + // scala.scalajs.js.JSConverters.JSRichOption + + import js.JSConverters._ + + @Test def should_provide_orUndefined(): Unit = { + assertEquals("asdf", Some("asdf").orUndefined) + assertJSUndefined((None: Option[String]).orUndefined) + assertJSUndefined(None.orUndefined) + } + +} diff --git a/src/test/scala/org/scalablytyped/runtime/TypeTests.scala b/src/test/scala/org/scalablytyped/runtime/TypeTests.scala new file mode 100644 index 0000000..02ab30c --- /dev/null +++ b/src/test/scala/org/scalablytyped/runtime/TypeTests.scala @@ -0,0 +1,43 @@ +package org.scalablytyped.runtime + +import scala.scalajs.js + +object TypeTests { + trait Test1 extends js.Object { + val a: NullUndefOr[Int] + } + trait Test2 extends Test1 { + val a: NullOr[Int] + } + trait Test3 extends Test1 { + val a: UndefOr[Int] + } + trait Test4 extends Test2 with Test3 { + val a: Required[Int] + } + + new Test1 { + override val a: NullUndefOr[Int] = null + } + new Test1 { + override val a = 1 + } + new Test2 { + override val a = null + } + new Test2 { + override val a = 1 + } + new Test3 { + override val a = UndefOr.fromJsUndefOr(js.undefined) + } + new Test3 { + override val a = 1 + } +// new Test4 { +// override val a = js.undefined +// } + new Test4 { + override val a = 1 + } +} diff --git a/src/test/scala/org/scalablytyped/runtime/UndefOrTest.scala b/src/test/scala/org/scalablytyped/runtime/UndefOrTest.scala new file mode 100644 index 0000000..1d13936 --- /dev/null +++ b/src/test/scala/org/scalablytyped/runtime/UndefOrTest.scala @@ -0,0 +1,197 @@ +package org.scalablytyped.runtime + +import org.junit.Assert._ +import org.junit.Test +import org.scalablytyped.runtime.AssertThrows._ +import org.scalablytyped.runtime.JSAssert._ + +import scala.scalajs.js + +class UndefOrTest { + def some[A](v: A): UndefOr[A] = v + def none[A]: UndefOr[A] = ().asInstanceOf[UndefOr[A]] + + // scala.scalajs.UndefOr[A] + + @Test def convert_A_to_js_UndefOr_A(): Unit = { + val x: UndefOr[Int] = 42 + assertFalse(x.isEmpty) + assertTrue(x.isDefined) + assertTrue(x.nonEmpty) + assertEquals(42, x.get) + } + + @Test def convert_undefined_to_js_UndefOr_A(): Unit = { + val x: UndefOr[Int] = none + assertTrue(x.isEmpty) + assertFalse(x.isDefined) + assertFalse(x.nonEmpty) + assertThrows(classOf[NoSuchElementException], x.get) + } + + @Test def explicitly_convert_A_to_js_UndefOr_A(): Unit = { + val x: UndefOr[Int] = some(42) + assertFalse(x.isEmpty) + assertEquals(42, x.get) + + val f: UndefOr[js.Function1[Int, Int]] = some((x: Int) => x + 1) + assertFalse(f.isEmpty) + assertEquals(6, f.get(5)) + } + + @Test def `convert_to_js_Any_when_A_<%_js_Any`(): Unit = { + val x: UndefOr[Int] = 42 + assertEquals(42, x) + + val y: UndefOr[String] = none + assertJSUndefined(y) + } + + @Test def getOrElse(): Unit = { + assertEquals("hello", some("hello").getOrElse("ko")) + assertEquals("ok", none[String].getOrElse("ok")) + + var defaultComputed = false + assertEquals("test", some("test") getOrElse { + defaultComputed = true + "ko" + }) + assertFalse(defaultComputed) + } + + @Test def orNull(): Unit = { + assertEquals("hello", some("hello").orNull) + assertNull(none[String].orNull) + } + + @Test def map(): Unit = { + assertEquals(62 / 3, some(62).map(_ / 3)) + assertJSUndefined(none[Int].map(_ / 3)) + } + + @Test def fold(): Unit = { + assertEquals(6, some(3).fold(10)(_ * 2)) + assertEquals(10, none[Int].fold(10)(_ * 2)) + } + + @Test def flatMap(): Unit = { + def f(x: Int): UndefOr[Int] = if (x > 0) x+3 else none + assertEquals(9, some(6).flatMap(f)) + assertJSUndefined(some(-6).flatMap(f)) + assertJSUndefined(none[Int].flatMap(f)) + } + + @Test def flatten(): Unit = { + assertTrue(some(some(7)).flatten.isDefined) + assertEquals(7, some(some(7)).flatten.get) + assertFalse(some(none[Int]).flatten.isDefined) + assertFalse(none[UndefOr[Int]].flatten.isDefined) + } + + @Test def filter(): Unit = { + assertTrue(some(7).filter(_ > 0).isDefined) + assertEquals(7, some(7).filter(_ > 0).get) + assertFalse(some(7).filter(_ < 0).isDefined) + assertFalse(none[Int].filter(_ < 0).isDefined) + } + + @Test def filterNot(): Unit = { + assertTrue(some(7).filterNot(_ < 0).isDefined) + assertEquals(7, some(7).filterNot(_ < 0).get) + assertFalse(some(7).filterNot(_ > 0).isDefined) + assertFalse(none[Int].filterNot(_ > 0).isDefined) + } + + @Test def contains(): Unit = { + assertTrue(some(7).contains(7)) + assertFalse(some(7).contains(8)) + assertFalse(none[Int].contains(7)) + + assertFalse(some(()).contains(())) + } + + @Test def exists(): Unit = { + assertTrue(some(7).exists(_ > 0)) + assertFalse(some(7).exists(_ < 0)) + assertFalse(none[Int].exists(_ > 0)) + } + + @Test def forall(): Unit = { + assertTrue(some(7).forall(_ > 0)) + assertFalse(some(7).forall(_ < 0)) + assertTrue(none[Int].forall(_ > 0)) + } + + @Test def foreach(): Unit = { + var witness1 = 3 + some(42).foreach(witness1 = _) + assertEquals(42, witness1) + + var witness2 = 3 + none[Int].foreach(witness2 = _) + assertEquals(3, witness2) + } + + @Test def collect(): Unit = { + assertEquals("ok", some("hello") collect { + case "hello" => "ok" + }) + assertTrue(js.isUndefined(some("hello") collect { + case "notthis" => "ko" + })) + assertTrue(js.isUndefined(none[String] collect { + case "hello" => "ko" + })) + } + + @Test def collect_should_call_guard_at_most_once(): Unit = { + var witness = 0 + def guard(x: String): Boolean = { + witness += 1 + true + } + assertEquals("ok", some("hello") collect { + case x @ "hello" if guard(x) => "ok" + }) + assertEquals(1, witness) + } + + @Test def orElse(): Unit = { + assertTrue((some(true) orElse some(false)).get) + assertEquals("ok", some("ok") orElse none) + assertEquals("yes", none orElse some("yes")) + assertJSUndefined(none orElse none) + + // #2095 + assertEquals("ok", some("ok") orElse "yes") + assertEquals("yes", none orElse "yes") + } + + @Test def toList(): Unit = { + assertEquals(List("hello"), some("hello").toList) + assertEquals(List.empty[String], none[String].toList) + } + + @Test def toLeft_and_toRight(): Unit = { + assertTrue(some("left").toLeft("right").isInstanceOf[Left[_, _]]) + assertTrue(none[String].toLeft("right").isInstanceOf[Right[_, _]]) + assertTrue(some("right").toRight("left").isInstanceOf[Right[_, _]]) + assertTrue(none[String].toRight("left").isInstanceOf[Left[_, _]]) + } + + @Test def toOption(): Unit = { + assertTrue(some("foo").toOption == Some("foo")) + assertTrue(none.toOption == None) + } + + // scala.scalajs.js.JSConverters.JSRichOption + + import js.JSConverters._ + + @Test def should_provide_orUndefined(): Unit = { + assertEquals("asdf", Some("asdf").orUndefined) + assertJSUndefined((None: Option[String]).orUndefined) + assertJSUndefined(None.orUndefined) + } + +} From ba7420d03e76ea6bbb909c5f055980e1ee248e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Raddum=20Berg?= Date: Fri, 30 Aug 2019 15:28:21 +0200 Subject: [PATCH 2/2] remove a bunch of implicits --- .../scalablytyped/runtime/NullUndefOr.scala | 19 ++++++------ .../org/scalablytyped/runtime/TypeTests.scala | 31 ++++++++++++------- test.ts | 21 +++++++++++++ 3 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 test.ts diff --git a/src/main/scala/org/scalablytyped/runtime/NullUndefOr.scala b/src/main/scala/org/scalablytyped/runtime/NullUndefOr.scala index d19011b..e32eb09 100644 --- a/src/main/scala/org/scalablytyped/runtime/NullUndefOr.scala +++ b/src/main/scala/org/scalablytyped/runtime/NullUndefOr.scala @@ -12,15 +12,17 @@ sealed trait NullUndefOr[+A] extends js.Any object NullUndefOr extends Companion { override type F[+A] = NullUndefOr[A] + @inline implicit final def asResult[A](a: A): Required[A] = + a.asInstanceOf[Required[A]] + /* Duplicated for intellij. name it `apply` so users can be explicit */ - @inline implicit final def apply[A](a: A): NullUndefOr[A] = + @inline final def apply[A](a: A): NullUndefOr[A] = a.asInstanceOf[NullUndefOr[A]] /* Integrate with Scala.js built-in type */ - @inline implicit def fromJsUndefOr[A](a: js.UndefOr[A]): NullUndefOr[A] = + @inline def fromJsUndefOr[A](a: js.UndefOr[A]): NullUndefOr[A] = a.asInstanceOf[NullUndefOr[A]] - /* This is pretty arbitrary, but choose `undefined` as the empty value */ @inline def Empty: NullUndefOr[Nothing] = UndefOr.Empty @@ -40,8 +42,7 @@ sealed trait NullOr[+A] extends NullUndefOr[A] object NullOr extends Companion { override type F[+A] = NullOr[A] - /* Duplicated for intellij. name it `apply` so users can be explicit */ - @inline implicit final def apply[A](a: A): NullOr[A] = + @inline final def apply[A](a: A): NullOr[A] = a.asInstanceOf[NullOr[A]] @inline def Empty: NullOr[Nothing] = @@ -63,12 +64,11 @@ sealed trait UndefOr[+A] extends NullUndefOr[A] object UndefOr extends Companion { override type F[+A] = UndefOr[A] - /* Duplicated for intellij. name it `apply` so users can be explicit */ - @inline implicit final def apply[A](a: A): UndefOr[A] = + @inline final def apply[A](a: A): UndefOr[A] = a.asInstanceOf[UndefOr[A]] /* Integrate with Scala.js built-in type */ - @inline implicit def fromJsUndefOr[A](a: js.UndefOr[A]): UndefOr[A] = + @inline def fromJsUndefOr[A](a: js.UndefOr[A]): UndefOr[A] = a.asInstanceOf[UndefOr[A]] @inline def Empty: UndefOr[Nothing] = @@ -90,8 +90,7 @@ sealed trait Required[+A] extends NullOr[A] with UndefOr[A] object Required extends Companion { override type F[+A] = Required[A] - /* Duplicated for intellij. name it `apply` so users can be explicit */ - @inline implicit final def apply[A](a: A): Required[A] = + @inline final def apply[A](a: A): Required[A] = a.asInstanceOf[Required[A]] @inline implicit final class Ops[A](val self: Required[A]) extends AnyVal { diff --git a/src/test/scala/org/scalablytyped/runtime/TypeTests.scala b/src/test/scala/org/scalablytyped/runtime/TypeTests.scala index 02ab30c..359bf35 100644 --- a/src/test/scala/org/scalablytyped/runtime/TypeTests.scala +++ b/src/test/scala/org/scalablytyped/runtime/TypeTests.scala @@ -4,40 +4,49 @@ import scala.scalajs.js object TypeTests { trait Test1 extends js.Object { - val a: NullUndefOr[Int] + val a: NullUndefOr[Int] = js.undefined.asInstanceOf[NullUndefOr[Int]] // Error:(7, 34) Members of Scala.js-defined JS traits must either be abstract, or their right-hand-side must be `js.undefined`. } trait Test2 extends Test1 { - val a: NullOr[Int] + override val a: NullOr[Int] } trait Test3 extends Test1 { - val a: UndefOr[Int] + override val a: UndefOr[Int] = js.undefined.asInstanceOf[UndefOr[Int]] // Error:(13, 34) Members of Scala.js-defined JS traits must either be abstract, or their right-hand-side must be `js.undefined`. } trait Test4 extends Test2 with Test3 { - val a: Required[Int] + override val a: Required[Int] } + new Test1 {} new Test1 { - override val a: NullUndefOr[Int] = null + override val a = 1 } new Test1 { - override val a = 1 + override val a = NullUndefOr(1) } + new Test2 { override val a = null } new Test2 { override val a = 1 } - new Test3 { - override val a = UndefOr.fromJsUndefOr(js.undefined) + new Test2 { + override val a = NullOr(1) } + + new Test3 {} new Test3 { override val a = 1 } -// new Test4 { -// override val a = js.undefined -// } + new Test3 { + override val a = UndefOr(1) + } + + new Test4 {} new Test4 { override val a = 1 } + new Test4 { + override val a = Required(1) + } } diff --git a/test.ts b/test.ts new file mode 100644 index 0000000..8aa737d --- /dev/null +++ b/test.ts @@ -0,0 +1,21 @@ +interface A { + a: number | null | undefined +} + +interface B extends A { + a: number | null +} + +interface C extends A { + a: number | undefined +} + +interface D extends B, C { + a: number +} + +const foo: D = { + a: 1 +} + +const fooA: A = foo;