Repository: scalameta/scalameta (scala)

Issue:
Tokens lacks bounds checking
An empty `Tokens` should throw on `head` and `last` and `apply` but does not when it avoids a bounds check on the underlying array. (`last` started throwing on 2.13.12.; `head` was also improved, so the override should use `super.head`.)
```
@data class Tokens private (
    private val tokens: Array[Token],
    private val start: Int,
    private val end: Int
) extends immutable.IndexedSeq[Token] with IndexedSeqOptimized[Token] {
  def apply(idx: Int): Token = tokens(start + idx)
  def length: Int = end - start
  override def slice(from: Int, until: Int): Tokens = {
    val lo = start + from.max(0).min(length)
    val hi = lo.max(start + until.min(length))
    Tokens(tokens, lo, hi)
  }

  /* Both head and headOption need to be implemented here due to
   * binary incompatibility caused by
   * https://github.com/scala/scala/commit/b20dd00b11f06c14c823d277cdfb58043a2586fc
   */
  override def head: Token = apply(0)

```

Noticed at https://github.com/scala/community-build/issues/1680#issuecomment-1625131437

Pull request:
Fixes #3235.

Patch:
diff --git a/scalameta/tokens/shared/src/main/scala/scala/meta/tokens/Tokens.scala b/scalameta/tokens/shared/src/main/scala/scala/meta/tokens/Tokens.scala
index 08fd84d5a0..9010a1d952 100644
--- a/scalameta/tokens/shared/src/main/scala/scala/meta/tokens/Tokens.scala
+++ b/scalameta/tokens/shared/src/main/scala/scala/meta/tokens/Tokens.scala
@@ -31,7 +31,10 @@ import scala.meta.internal.prettyprinters._
     private val start: Int,
     val length: Int
 ) extends immutable.IndexedSeq[Token] with IndexedSeqOptimized[Token] {
-  def apply(idx: Int): Token = tokens(start + idx)
+  @inline private def get(idx: Int): Token = tokens(start + idx)
+  def apply(idx: Int): Token =
+    if (idx >= 0 && idx < length) get(idx)
+    else throw new NoSuchElementException(s"token $idx out of $length")
   def end: Int = start + length // for MIMA compat
   override def slice(from: Int, until: Int): Tokens = {
     val lo = start + from.max(0).min(length)
@@ -44,8 +47,10 @@ import scala.meta.internal.prettyprinters._
    * https://github.com/scala/scala/commit/b20dd00b11f06c14c823d277cdfb58043a2586fc
    */
   override def head: Token = apply(0)
+  override def headOption: Option[Token] = if (isEmpty) None else Some(get(0))
 
-  override def headOption: Option[Token] = if (isEmpty) None else Some(head)
+  override def last: Token = apply(length - 1)
+  override def lastOption: Option[Token] = if (isEmpty) None else Some(get(length - 1))
 
   override def toString = scala.meta.internal.prettyprinters.TokensToString(this)
 


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/TemplateSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/TemplateSuite.scala
index 3ebf03e34b..cb154ddae7 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/TemplateSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/TemplateSuite.scala
@@ -6,9 +6,18 @@ import scala.meta.dialects.Scala211
 
 class TemplateSuite extends ParseSuite {
   test("trait T") {
-    assertTree(templStat("trait T")) {
+    val tree = templStat("trait T")
+    assertTree(tree) {
       Trait(Nil, Type.Name("T"), Type.ParamClause(Nil), EmptyCtor(), EmptyTemplate())
     }
+    def testTokens(t: Tree): Unit = {
+      interceptMessage[NoSuchElementException]("token 0 out of 0")(t.tokens.head)
+      interceptMessage[NoSuchElementException]("token -1 out of 0")(t.tokens.last)
+    }
+    val traitTree = tree.asInstanceOf[Trait]
+    testTokens(traitTree.tparamClause)
+    testTokens(traitTree.ctor)
+    testTokens(traitTree.templ)
   }
 
   test("trait T {}") {