From fc9d8a30440d82f4f0021df897c8766e5bc4e1ba Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Thu, 10 Jul 2025 12:58:25 +0200 Subject: [PATCH 1/5] loneElement and loneElementOption: extension methods for Iterable --- .../main/scala/io/shiftleft/Implicits.scala | 92 +++++++++++++------ .../scala/io/shiftleft/ImplicitsTests.scala | 52 +++++++++++ project/build.properties | 2 +- 3 files changed, 119 insertions(+), 27 deletions(-) create mode 100644 codepropertygraph/src/test/scala/io/shiftleft/ImplicitsTests.scala diff --git a/codepropertygraph/src/main/scala/io/shiftleft/Implicits.scala b/codepropertygraph/src/main/scala/io/shiftleft/Implicits.scala index c42894502..243b6c0fc 100644 --- a/codepropertygraph/src/main/scala/io/shiftleft/Implicits.scala +++ b/codepropertygraph/src/main/scala/io/shiftleft/Implicits.scala @@ -3,45 +3,85 @@ package io.shiftleft import org.slf4j.{Logger, LoggerFactory} object Implicits { - private val logger: Logger = LoggerFactory.getLogger(Implicits.getClass) - implicit class IterableOnceDeco[T](val iterable: IterableOnce[T]) extends AnyVal { - def onlyChecked: T = { - if (iterable.iterator.hasNext) { - val res = iterable.iterator.next() - if (iterable.iterator.hasNext) { - logger.warn("iterator was expected to have exactly one element, but it actually has more") + extension [A](iterable: IterableOnce[A]) { + + /** @see {{{loneElement(hint)}}} */ + def loneElement: A = + loneElement(hint = "") + + /** @return + * the one and only element from an Iterable + * @throws NoSuchElementException + * if the Iterable is empty + * @throws AssertionError + * if the Iterable has more than one element + */ + def loneElement(hint: String): A = { + lazy val hintMaybe = + if (hint.isEmpty) "" + else s" Hint: $hint" + + val iter = iterable.iterator + if (iter.isEmpty) { + throw new NoSuchElementException( + s"Iterable was expected to have exactly one element, but it is empty.$hintMaybe" + ) + } else { + val res = iter.next() + if (iter.hasNext) { + val collectionSizeHint = iterable.knownSize match { + case -1 => "it has more than one" // cannot be computed cheaply, i.e. without traversing the collection + case knownSize => s"it has $knownSize" + } + throw new AssertionError( + s"Iterable was expected to have exactly one element, but $collectionSizeHint.$hintMaybe" + ) } res - } else { throw new NoSuchElementException() } + } } - } - /** A wrapper around a Java iterator that throws a proper NoSuchElementException. - * - * Proper in this case means an exception with a stack trace. This is intended to be used as a replacement for next() - * on the iterators returned from TinkerPop since those are missing stack traces. - */ - implicit class JavaIteratorDeco[T](val iterator: java.util.Iterator[T]) extends AnyVal { - def nextChecked: T = { - try { - iterator.next - } catch { - case _: NoSuchElementException => - throw new NoSuchElementException() + /** @see {{{loneElementOption(hint)}}} */ + def loneElementOption: Option[A] = + loneElementOption(hint = None) + + /** @return + * {{{Some(element)}}} if the Iterable has exactly one element, or {{{None}}} if the Iterable has zero or more + * than 1 element. Note: if the lone element is {{{null}}}, this will return {{{Some(null)}}}, which is in + * accordance with how {{{headOption}}} works. + */ + def loneElementOption(hint: String | None.type = None): Option[A] = { + val iter = iterable.iterator + if (iter.isEmpty) { + None + } else { + val result = iter.next() + if (iter.hasNext) { + None + } else { + Some(result) + } } } + } + implicit class IterableOnceDeco[T](val iterable: IterableOnce[T]) extends AnyVal { + @deprecated( + "please use `loneElement` instead, which has a better name and will throw if the iterable has more than one element (rather than just log.warn)", + since = "1.7.42 (July 2025)" + ) def onlyChecked: T = { - if (iterator.hasNext) { - val res = iterator.next - if (iterator.hasNext) { + if (iterable.iterator.hasNext) { + val res = iterable.iterator.next() + if (iterable.iterator.hasNext) { logger.warn("iterator was expected to have exactly one element, but it actually has more") } res - } else { throw new NoSuchElementException() } + } else { + throw new NoSuchElementException() + } } } - } diff --git a/codepropertygraph/src/test/scala/io/shiftleft/ImplicitsTests.scala b/codepropertygraph/src/test/scala/io/shiftleft/ImplicitsTests.scala new file mode 100644 index 000000000..6d9c462b4 --- /dev/null +++ b/codepropertygraph/src/test/scala/io/shiftleft/ImplicitsTests.scala @@ -0,0 +1,52 @@ +package io.shiftleft + +import io.shiftleft.Implicits.* +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import scala.collection.mutable.ArrayBuffer + +class ImplicitsTests extends AnyWordSpec with Matchers { + + "loneElement returns the one and only element from an Iterable, and throws an exception otherwise" in { + Seq(1).loneElement shouldBe 1 + Seq(1).loneElement("some context") shouldBe 1 + Seq(null).loneElement shouldBe null + + intercept[NoSuchElementException] { + Seq.empty.loneElement + }.getMessage should include("it is empty") + + intercept[NoSuchElementException] { + Seq.empty.loneElement("some context") + }.getMessage should include("it is empty. Hint: some context") + + intercept[AssertionError] { + Seq(1, 2).loneElement + }.getMessage should include("it has more than one") + + intercept[AssertionError] { + ArrayBuffer(1, 2).loneElement + }.getMessage should include( + "it has 2" + ) // ArrayBuffer can 'cheaply' compute their size, so we can have it in the exception message + + intercept[AssertionError] { + Seq(1, 2).loneElement("some context") + }.getMessage should include("it has more than one. Hint: some context") + } + + "loneElementOption returns an Option of the one and only element from an Iterable, or else None" in { + Seq(1).loneElementOption shouldBe Some(1) + Seq(1).loneElementOption("some context") shouldBe Some(1) + Seq(null).loneElementOption shouldBe Some(null) + Seq(null).loneElementOption("some context") shouldBe Some(null) + + Seq.empty.loneElementOption shouldBe None + Seq.empty.loneElementOption("some context") shouldBe None + + Seq(1, 2).loneElementOption shouldBe None + Seq(1, 2).loneElementOption("some context") shouldBe None + } + +} diff --git a/project/build.properties b/project/build.properties index cc68b53f1..bbb0b608c 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.11 +sbt.version=1.11.2 From d03cd7f22a7762deab2a5e1f86eb20c9591eb2b4 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Thu, 10 Jul 2025 13:13:44 +0200 Subject: [PATCH 2/5] remove 'hint' from loneElementOption --- .../src/main/scala/io/shiftleft/Implicits.scala | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/codepropertygraph/src/main/scala/io/shiftleft/Implicits.scala b/codepropertygraph/src/main/scala/io/shiftleft/Implicits.scala index 243b6c0fc..1f2ef0b05 100644 --- a/codepropertygraph/src/main/scala/io/shiftleft/Implicits.scala +++ b/codepropertygraph/src/main/scala/io/shiftleft/Implicits.scala @@ -43,16 +43,12 @@ object Implicits { } } - /** @see {{{loneElementOption(hint)}}} */ - def loneElementOption: Option[A] = - loneElementOption(hint = None) - /** @return * {{{Some(element)}}} if the Iterable has exactly one element, or {{{None}}} if the Iterable has zero or more * than 1 element. Note: if the lone element is {{{null}}}, this will return {{{Some(null)}}}, which is in * accordance with how {{{headOption}}} works. */ - def loneElementOption(hint: String | None.type = None): Option[A] = { + def loneElementOption: Option[A] = { val iter = iterable.iterator if (iter.isEmpty) { None From 32b3ac98a63758ce6e45ed9515bc62461238f43c Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Thu, 10 Jul 2025 13:13:44 +0200 Subject: [PATCH 3/5] remove 'hint' from loneElementOption --- .../src/test/scala/io/shiftleft/ImplicitsTests.scala | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/codepropertygraph/src/test/scala/io/shiftleft/ImplicitsTests.scala b/codepropertygraph/src/test/scala/io/shiftleft/ImplicitsTests.scala index 6d9c462b4..3c3127645 100644 --- a/codepropertygraph/src/test/scala/io/shiftleft/ImplicitsTests.scala +++ b/codepropertygraph/src/test/scala/io/shiftleft/ImplicitsTests.scala @@ -38,15 +38,11 @@ class ImplicitsTests extends AnyWordSpec with Matchers { "loneElementOption returns an Option of the one and only element from an Iterable, or else None" in { Seq(1).loneElementOption shouldBe Some(1) - Seq(1).loneElementOption("some context") shouldBe Some(1) Seq(null).loneElementOption shouldBe Some(null) - Seq(null).loneElementOption("some context") shouldBe Some(null) Seq.empty.loneElementOption shouldBe None - Seq.empty.loneElementOption("some context") shouldBe None - + Seq(1, 2).loneElementOption shouldBe None - Seq(1, 2).loneElementOption("some context") shouldBe None } } From 7fdeee0497728ee02e3eb1b4b9fcb9ea2a4a9045 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Thu, 10 Jul 2025 14:45:52 +0200 Subject: [PATCH 4/5] moved to flatgraph. only change left: deprecation `onlyChecked` --- .../main/scala/io/shiftleft/Implicits.scala | 60 +------------------ .../scala/io/shiftleft/ImplicitsTests.scala | 48 --------------- 2 files changed, 1 insertion(+), 107 deletions(-) delete mode 100644 codepropertygraph/src/test/scala/io/shiftleft/ImplicitsTests.scala diff --git a/codepropertygraph/src/main/scala/io/shiftleft/Implicits.scala b/codepropertygraph/src/main/scala/io/shiftleft/Implicits.scala index 1f2ef0b05..7cb5ccb6c 100644 --- a/codepropertygraph/src/main/scala/io/shiftleft/Implicits.scala +++ b/codepropertygraph/src/main/scala/io/shiftleft/Implicits.scala @@ -5,67 +5,9 @@ import org.slf4j.{Logger, LoggerFactory} object Implicits { private val logger: Logger = LoggerFactory.getLogger(Implicits.getClass) - extension [A](iterable: IterableOnce[A]) { - - /** @see {{{loneElement(hint)}}} */ - def loneElement: A = - loneElement(hint = "") - - /** @return - * the one and only element from an Iterable - * @throws NoSuchElementException - * if the Iterable is empty - * @throws AssertionError - * if the Iterable has more than one element - */ - def loneElement(hint: String): A = { - lazy val hintMaybe = - if (hint.isEmpty) "" - else s" Hint: $hint" - - val iter = iterable.iterator - if (iter.isEmpty) { - throw new NoSuchElementException( - s"Iterable was expected to have exactly one element, but it is empty.$hintMaybe" - ) - } else { - val res = iter.next() - if (iter.hasNext) { - val collectionSizeHint = iterable.knownSize match { - case -1 => "it has more than one" // cannot be computed cheaply, i.e. without traversing the collection - case knownSize => s"it has $knownSize" - } - throw new AssertionError( - s"Iterable was expected to have exactly one element, but $collectionSizeHint.$hintMaybe" - ) - } - res - } - } - - /** @return - * {{{Some(element)}}} if the Iterable has exactly one element, or {{{None}}} if the Iterable has zero or more - * than 1 element. Note: if the lone element is {{{null}}}, this will return {{{Some(null)}}}, which is in - * accordance with how {{{headOption}}} works. - */ - def loneElementOption: Option[A] = { - val iter = iterable.iterator - if (iter.isEmpty) { - None - } else { - val result = iter.next() - if (iter.hasNext) { - None - } else { - Some(result) - } - } - } - } - implicit class IterableOnceDeco[T](val iterable: IterableOnce[T]) extends AnyVal { @deprecated( - "please use `loneElement` instead, which has a better name and will throw if the iterable has more than one element (rather than just log.warn)", + "please use `.loneElement` from flatgraph (mixed into the generated `language` packages) instead, which has a better name and will throw if the iterable has more than one element (rather than just log.warn)", since = "1.7.42 (July 2025)" ) def onlyChecked: T = { diff --git a/codepropertygraph/src/test/scala/io/shiftleft/ImplicitsTests.scala b/codepropertygraph/src/test/scala/io/shiftleft/ImplicitsTests.scala deleted file mode 100644 index 3c3127645..000000000 --- a/codepropertygraph/src/test/scala/io/shiftleft/ImplicitsTests.scala +++ /dev/null @@ -1,48 +0,0 @@ -package io.shiftleft - -import io.shiftleft.Implicits.* -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec - -import scala.collection.mutable.ArrayBuffer - -class ImplicitsTests extends AnyWordSpec with Matchers { - - "loneElement returns the one and only element from an Iterable, and throws an exception otherwise" in { - Seq(1).loneElement shouldBe 1 - Seq(1).loneElement("some context") shouldBe 1 - Seq(null).loneElement shouldBe null - - intercept[NoSuchElementException] { - Seq.empty.loneElement - }.getMessage should include("it is empty") - - intercept[NoSuchElementException] { - Seq.empty.loneElement("some context") - }.getMessage should include("it is empty. Hint: some context") - - intercept[AssertionError] { - Seq(1, 2).loneElement - }.getMessage should include("it has more than one") - - intercept[AssertionError] { - ArrayBuffer(1, 2).loneElement - }.getMessage should include( - "it has 2" - ) // ArrayBuffer can 'cheaply' compute their size, so we can have it in the exception message - - intercept[AssertionError] { - Seq(1, 2).loneElement("some context") - }.getMessage should include("it has more than one. Hint: some context") - } - - "loneElementOption returns an Option of the one and only element from an Iterable, or else None" in { - Seq(1).loneElementOption shouldBe Some(1) - Seq(null).loneElementOption shouldBe Some(null) - - Seq.empty.loneElementOption shouldBe None - - Seq(1, 2).loneElementOption shouldBe None - } - -} From 633a742cbb54469921570d1996b25d8b80597e32 Mon Sep 17 00:00:00 2001 From: Michael Pollmeier Date: Fri, 11 Jul 2025 14:50:05 +0200 Subject: [PATCH 5/5] latest fg --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index d41d9e7f1..a389016ca 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ name := "codepropertygraph" // parsed by project/Versions.scala, updated by updateDependencies.sh -val flatgraphVersion = "0.1.22" +val flatgraphVersion = "0.1.23" inThisBuild( List(