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 b8715719ba5..27e79c00e76 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 @@ -23,6 +23,8 @@ import org.jetbrains.kotlin.ir.expressions.IrContainerExpression import org.jetbrains.kotlin.ir.expressions.IrExpression import org.jetbrains.kotlin.ir.expressions.IrFunctionExpression import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin +import org.jetbrains.kotlin.ir.expressions.IrTypeOperator +import org.jetbrains.kotlin.ir.expressions.IrTypeOperatorCall import org.jetbrains.kotlin.ir.expressions.IrWhen import org.jetbrains.kotlin.ir.util.dumpKotlinLike import org.jetbrains.kotlin.ir.visitors.IrElementVisitor @@ -102,6 +104,16 @@ fun buildTree(expression: IrExpression): Node? { super.visitContainerExpression(expression, data) } + override fun visitTypeOperator(expression: IrTypeOperatorCall, data: Node) { + val node = data as? ExpressionNode ?: ExpressionNode().also { data.addChild(it) } + if (expression.operator in setOf(IrTypeOperator.INSTANCEOF, IrTypeOperator.NOT_INSTANCEOF)) { + // Only include `is` and `!is` checks + node.add(expression) + } + + expression.acceptChildren(this, node) + } + override fun visitCall(expression: IrCall, data: Node) { if (expression.symbol.owner.name.asString() == "EQEQ" && expression.origin == IrStatementOrigin.EXCLEQ) { // Skip the EQEQ part of a EXCLEQ call 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 87db4016fbf..d3cf40be54d 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 @@ -31,6 +31,8 @@ import org.jetbrains.kotlin.ir.expressions.IrConstKind import org.jetbrains.kotlin.ir.expressions.IrExpression import org.jetbrains.kotlin.ir.expressions.IrMemberAccessExpression import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin +import org.jetbrains.kotlin.ir.expressions.IrTypeOperator +import org.jetbrains.kotlin.ir.expressions.IrTypeOperatorCall fun IrBuilderWithScope.irDiagramString( file: IrFile, @@ -161,8 +163,17 @@ private fun findDisplayOffset( expression: IrExpression, source: String ): Int { - if (expression !is IrMemberAccessExpression<*>) return 0 + return when (expression) { + is IrMemberAccessExpression<*> -> memberAccessOffset(expression, source) + is IrTypeOperatorCall -> typeOperatorOffset(expression, source) + else -> 0 + } +} +private fun memberAccessOffset( + expression: IrMemberAccessExpression<*>, + source: String +): Int { if (expression.origin == IrStatementOrigin.EXCLEQ || expression.origin == IrStatementOrigin.EXCLEQEQ) { // special case to handle `value != null` return source.indexOf("!=") @@ -196,6 +207,17 @@ private fun findDisplayOffset( return 0 } +private fun typeOperatorOffset( + expression: IrTypeOperatorCall, + source: String +): Int { + return when (expression.operator) { + IrTypeOperator.INSTANCEOF -> source.indexOf(" is ") + 1 + IrTypeOperator.NOT_INSTANCEOF -> source.indexOf(" !is ") + 1 + else -> 0 + } +} + fun String.substring(expression: IrElement) = substring(expression.startOffset, expression.endOffset) fun IrFile.info(expression: IrElement) = fileEntry.getSourceRangeInfo(expression.startOffset, expression.endOffset) diff --git a/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/CastExpressionTest.kt b/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/CastExpressionTest.kt new file mode 100644 index 00000000000..a871d45f9d3 --- /dev/null +++ b/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/CastExpressionTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 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 +import kotlin.test.assertEquals + +class CastExpressionTest { + @Test + fun `instance check is correctly aligned`() { + val actual = executeMainAssertion("""assert(null is String)""") + assertEquals( + """ + Assertion failed + assert(null is String) + | + false + """.trimIndent(), + actual + ) + } + + @Test + fun `negative instance check is correctly aligned`() { + val actual = executeMainAssertion("""assert("Hello, world!" !is String)""") + assertEquals( + """ + Assertion failed + assert("Hello, world!" !is String) + | + false + """.trimIndent(), + actual + ) + } + + @Test + fun `smart casts do not duplicate output`() { + val actual = executeMainAssertion( + """ + val greeting: Any = "hello" + assert(greeting is String && greeting.length == 2) + """.trimIndent() + ) + assertEquals( + """ + Assertion failed + assert(greeting is String && greeting.length == 2) + | | | | | + | | | | false + | | | 5 + | | hello + | true + hello + """.trimIndent(), + actual + ) + } +} diff --git a/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/compiler.kt b/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/compiler.kt index 01570df154a..c73106a3537 100644 --- a/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/compiler.kt +++ b/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/compiler.kt @@ -21,6 +21,7 @@ import com.tschuchort.compiletesting.SourceFile import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar import org.jetbrains.kotlin.name.FqName +import java.io.OutputStream import java.lang.reflect.InvocationTargetException import kotlin.test.assertEquals import kotlin.test.fail @@ -36,7 +37,15 @@ fun compile( return KotlinCompilation().apply { sources = list useIR = true - messageOutputStream = System.out + messageOutputStream = object : OutputStream() { + override fun write(b: Int) { + // black hole all writes + } + + override fun write(b: ByteArray, off: Int, len: Int) { + // black hole all writes + } + } compilerPlugins = plugins.toList() inheritClassPath = true }.compile()