Repository: bnorm/kotlin-power-assert (kotlin)

Issue:
Correctly handle `in` and `!in` infix operators
For something like the following:

```kotlin
assert("Hello" in "World")
```

Version `0.12.1` currently throws an exception where version `0.12.0` did not.

```
java.lang.StringIndexOutOfBoundsException: String index out of range: 19
	at java.base/java.lang.StringLatin1.charAt(StringLatin1.java:47)
	at java.base/java.lang.String.charAt(String.java:693)
	at com.bnorm.power.diagram.IrDiagramKt.memberAccessOffset(IrDiagram.kt:198)
	at com.bnorm.power.diagram.IrDiagramKt.findDisplayOffset(IrDiagram.kt:164)
	at com.bnorm.power.diagram.IrDiagramKt.toValueDisplay(IrDiagram.kt:112)
	at com.bnorm.power.diagram.IrDiagramKt.irDiagramString(IrDiagram.kt:46)
```

Pull request description:
Fixes #94

Patch:
diff --git a/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/diagram/ExpressionTree.kt b/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/diagram/ExpressionTree.kt
index b5e735f..f5e9535 100644
--- a/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/diagram/ExpressionTree.kt
+++ b/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/diagram/ExpressionTree.kt
@@ -114,6 +114,11 @@ fun buildTree(expression: IrExpression): Node? {
         if (expression.symbol.owner.name.asString() == "EQEQ" && expression.origin == IrStatementOrigin.EXCLEQ) {
           // Skip the EQEQ part of a EXCLEQ call
           expression.acceptChildren(this, data)
+        } else if (expression.origin == IrStatementOrigin.NOT_IN) {
+          // Exclude the wrapped "contains" call for `!in` operator expressions and only display the final result
+          val node = data as? ExpressionNode ?: ExpressionNode().also { data.addChild(it) }
+          node.add(expression)
+          expression.dispatchReceiver!!.acceptChildren(this, node)
         } else {
           super.visitCall(expression, data)
         }
diff --git a/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/diagram/IrDiagram.kt b/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/diagram/IrDiagram.kt
index 0fd1dfe..96c80ab 100644
--- a/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/diagram/IrDiagram.kt
+++ b/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/diagram/IrDiagram.kt
@@ -172,9 +172,14 @@ private fun memberAccessOffset(
   expression: IrMemberAccessExpression<*>,
   source: String,
 ): Int {
-  if (expression.origin == IrStatementOrigin.EXCLEQ || expression.origin == IrStatementOrigin.EXCLEQEQ) {
+  when (expression.origin) {
     // special case to handle `value != null`
-    return source.indexOf("!=")
+    IrStatementOrigin.EXCLEQ, IrStatementOrigin.EXCLEQEQ -> return source.indexOf("!=")
+    // special case to handle `in` operator
+    IrStatementOrigin.IN -> return source.indexOf(" in ") + 1
+    // special case to handle `in` operator
+    IrStatementOrigin.NOT_IN -> return source.indexOf(" !in ") + 1
+    else -> Unit
   }
 
   val owner = expression.symbol.owner
@@ -193,6 +198,7 @@ private fun memberAccessOffset(
     val expressionInfo = sourceFile.getSourceRangeInfo(expression)
     var offset = receiver.endOffset - expressionInfo.startOffset + 1
     if (receiver is IrConst<*> && receiver.kind == IrConstKind.String) offset++ // String constants don't include the quote
+    if (offset < 0 || offset >= source.length) return 0 // infix function called using non-infix syntax
 
     // Continue until there is a non-whitespace character
     while (source[offset].isWhitespace() || source[offset] == '.') {


Test patch:
diff --git a/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/OperatorTest.kt b/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/OperatorTest.kt
new file mode 100644
index 0000000..dbfb11d
--- /dev/null
+++ b/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/OperatorTest.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2023 Brian Norman
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.bnorm.power
+
+import kotlin.test.Test
+
+class OperatorTest {
+  @Test
+  fun `contains operator is correctly aligned`() {
+    assertMessage(
+      """
+      fun main() {
+        assert("Name" in listOf("Hello", "World"))
+      }""",
+      """
+      Assertion failed
+      assert("Name" in listOf("Hello", "World"))
+                    |  |
+                    |  [Hello, World]
+                    false
+      """.trimIndent(),
+    )
+  }
+
+  @Test
+  fun `contains function is correctly aligned`() {
+    assertMessage(
+      """
+      fun main() {
+        assert(listOf("Hello", "World").contains("Name"))
+      }""",
+      """
+      Assertion failed
+      assert(listOf("Hello", "World").contains("Name"))
+             |                        |
+             |                        false
+             [Hello, World]
+      """.trimIndent(),
+    )
+  }
+
+  @Test
+  fun `negative contains operator is correctly aligned`() {
+    assertMessage(
+      """
+      fun main() {
+        assert("Hello" !in listOf("Hello", "World"))
+      }""",
+      """
+      Assertion failed
+      assert("Hello" !in listOf("Hello", "World"))
+                     |   |
+                     |   [Hello, World]
+                     false
+      """.trimIndent(),
+    )
+  }
+
+  @Test
+  fun `negative contains function is correctly aligned`() {
+    assertMessage(
+      """
+      fun main() {
+        assert(!listOf("Hello", "World").contains("Hello"))
+      }""",
+      """
+      Assertion failed
+      assert(!listOf("Hello", "World").contains("Hello"))
+             ||                        |
+             ||                        true
+             |[Hello, World]
+             false
+      """.trimIndent(),
+    )
+  }
+}