From f8270b4cc997fff8850a1873d68f08ed19e8ddfd Mon Sep 17 00:00:00 2001 From: Brian Norman Date: Sat, 3 Apr 2021 17:20:52 -0500 Subject: [PATCH] Allow functions with a single argument of any type --- .../bnorm/power/PowerAssertCallTransformer.kt | 47 +++++---- .../bnorm/power/diagram/DiagramGenerator.kt | 98 ++++++++++--------- .../com/bnorm/power/diagram/IrDiagram.kt | 2 +- .../com/bnorm/power/AssertBooleanTest.kt | 45 +++++++++ .../com/bnorm/power/DebugFunctionTest.kt | 80 +++++++++++++++ sample/build.gradle.kts | 3 +- .../kotlin/com/bnorm/power/Debug.kt | 10 ++ .../kotlin/com/bnorm/power/PowerAssertTest.kt | 37 +++++++ 8 files changed, 257 insertions(+), 65 deletions(-) create mode 100644 kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/AssertBooleanTest.kt create mode 100644 kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/DebugFunctionTest.kt create mode 100644 sample/src/commonMain/kotlin/com/bnorm/power/Debug.kt diff --git a/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/PowerAssertCallTransformer.kt b/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/PowerAssertCallTransformer.kt index cbfecc8f280..e6d2aca4d7c 100644 --- a/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/PowerAssertCallTransformer.kt +++ b/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/PowerAssertCallTransformer.kt @@ -35,17 +35,18 @@ import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.backend.js.utils.asString import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope import org.jetbrains.kotlin.ir.builders.declarations.buildFun import org.jetbrains.kotlin.ir.builders.irBlockBody import org.jetbrains.kotlin.ir.builders.irCall import org.jetbrains.kotlin.ir.builders.irCallOp -import org.jetbrains.kotlin.ir.builders.irFalse import org.jetbrains.kotlin.ir.builders.irReturn import org.jetbrains.kotlin.ir.builders.irString import org.jetbrains.kotlin.ir.builders.parent import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin import org.jetbrains.kotlin.ir.declarations.IrFile +import org.jetbrains.kotlin.ir.declarations.IrFunction import org.jetbrains.kotlin.ir.declarations.path import org.jetbrains.kotlin.ir.expressions.IrCall import org.jetbrains.kotlin.ir.expressions.IrConst @@ -53,10 +54,12 @@ import org.jetbrains.kotlin.ir.expressions.IrExpression import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin import org.jetbrains.kotlin.ir.expressions.IrStringConcatenation import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionExpressionImpl +import org.jetbrains.kotlin.ir.symbols.IrTypeParameterSymbol import org.jetbrains.kotlin.ir.types.IrSimpleType import org.jetbrains.kotlin.ir.types.IrType import org.jetbrains.kotlin.ir.types.IrTypeArgument import org.jetbrains.kotlin.ir.types.IrTypeProjection +import org.jetbrains.kotlin.ir.types.classifierOrNull import org.jetbrains.kotlin.ir.types.getClass import org.jetbrains.kotlin.ir.types.isBoolean import org.jetbrains.kotlin.ir.types.isSubtypeOf @@ -99,23 +102,25 @@ class PowerAssertCallTransformer( .replace("\r\n", "\n") // https://youtrack.jetbrains.com/issue/KT-41888 irFile.transformChildrenVoid() +// println(irFile.dumpKotlinLike()) } override fun visitCall(expression: IrCall): IrExpression { - val fqName = expression.symbol.owner.kotlinFqName - if (functions.none { fqName == it }) + val function = expression.symbol.owner + val fqName = function.kotlinFqName + if (function.valueParameters.isEmpty() || functions.none { fqName == it }) return super.visitCall(expression) // Find a valid delegate function or do not translate - val delegate = findDelegate(fqName) ?: run { + val delegate = findDelegate(function) ?: run { + val valueType = function.valueParameters[0].type.asString() messageCollector.warn( expression, - "Unable to find overload for function $fqName callable as $fqName(Boolean, String) or $fqName(Boolean, () -> String) for power-assert transformation" + "Unable to find overload for function $fqName callable as $fqName($valueType, String) or $fqName($valueType, () -> String) for power-assert transformation" ) return super.visitCall(expression) } - val function = expression.symbol.owner val assertionArgument = expression.getValueArgument(0)!! val messageArgument = if (function.valueParameters.size == 2) expression.getValueArgument(1) else null @@ -130,7 +135,7 @@ class PowerAssertCallTransformer( at(expression) val generator = object : DiagramGenerator() { - override fun IrBuilderWithScope.buildAssertThrow(variables: List): IrExpression { + override fun IrBuilderWithScope.buildCall(argument: IrExpression, variables: List): IrExpression { val lambda = messageArgument?.asSimpleLambda() val title = when { @@ -142,18 +147,18 @@ class PowerAssertCallTransformer( irCallOp(invoke.symbol, invoke.returnType, messageArgument) } // TODO what should the default message be? - else -> irString("Assertion failed") + assertionArgument.type.isBoolean() -> irString("Assertion failed") + else -> null } - val prefix = title.deepCopyWithSymbols(parent) + val prefix = title?.deepCopyWithSymbols(parent) val diagram = irDiagram(file, fileSource, prefix, expression, variables) - return delegate.buildCall(this, expression, irFalse(), diagram) + return delegate.buildCall(this, expression, argument, diagram) } } // println(root.dump()) - - return generator.buildAssert(this, root) + return generator.buildExpression(this, root) // .also { println(expression.dump()) } // .also { println(it.dump()) } // .also { println(expression.dumpKotlinLike()) } @@ -165,20 +170,22 @@ class PowerAssertCallTransformer( fun buildCall(builder: IrBuilderWithScope, original: IrCall, argument: IrExpression, message: IrExpression): IrExpression } - private fun findDelegate(fqName: FqName): FunctionDelegate? { - return context.referenceFunctions(fqName) + private fun findDelegate(function: IrFunction): FunctionDelegate? { + if (function.valueParameters.isEmpty()) return null + + return context.referenceFunctions(function.kotlinFqName) .mapNotNull { overload -> // TODO allow other signatures than (Boolean, String) and (Boolean, () -> String) val parameters = overload.owner.valueParameters if (parameters.size != 2) return@mapNotNull null - if (!parameters[0].type.isBoolean()) return@mapNotNull null + if (!function.valueParameters[0].type.isAssignableTo(parameters[0].type)) return@mapNotNull null val messageParameter = parameters.last() return@mapNotNull when { isStringSupertype(messageParameter.type) -> { object : FunctionDelegate { override fun buildCall(builder: IrBuilderWithScope, original: IrCall, argument: IrExpression, message: IrExpression): IrExpression = with(builder) { - irCall(overload, type = overload.owner.returnType).apply { + irCall(overload, type = original.type).apply { dispatchReceiver = original.dispatchReceiver?.deepCopyWithSymbols(parent) extensionReceiver = original.extensionReceiver?.deepCopyWithSymbols(parent) for (i in 0 until original.typeArgumentsCount) { @@ -207,7 +214,7 @@ class PowerAssertCallTransformer( parent = scope.parent } val expression = IrFunctionExpressionImpl(original.startOffset, original.endOffset, messageParameter.type, lambda, IrStatementOrigin.LAMBDA) - irCall(overload, type = overload.owner.returnType).apply { + irCall(overload, type = original.type).apply { dispatchReceiver = original.dispatchReceiver?.deepCopyWithSymbols(parent) extensionReceiver = original.extensionReceiver?.deepCopyWithSymbols(parent) for (i in 0 until original.typeArgumentsCount) { @@ -236,6 +243,12 @@ class PowerAssertCallTransformer( private fun isStringSupertype(type: IrType): Boolean = context.irBuiltIns.stringType.isSubtypeOf(type, context.irBuiltIns) + private fun IrType.isAssignableTo(type: IrType): Boolean = + isSubtypeOf(type, context.irBuiltIns) || + ((type.classifierOrNull as? IrTypeParameterSymbol)?.owner?.superTypes?.all { + isSubtypeOf(it, context.irBuiltIns) + } ?: false) + private fun MessageCollector.info(expression: IrElement, message: String) { report(expression, CompilerMessageSeverity.INFO, message) } diff --git a/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/diagram/DiagramGenerator.kt b/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/diagram/DiagramGenerator.kt index 99334d20ab5..f2ddcdda3da 100644 --- a/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/diagram/DiagramGenerator.kt +++ b/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/diagram/DiagramGenerator.kt @@ -16,45 +16,50 @@ package com.bnorm.power.diagram -import org.jetbrains.kotlin.backend.common.lower.irIfThen -import org.jetbrains.kotlin.backend.common.lower.irNot import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope import org.jetbrains.kotlin.ir.builders.IrStatementsBuilder import org.jetbrains.kotlin.ir.builders.irBlock +import org.jetbrains.kotlin.ir.builders.irFalse +import org.jetbrains.kotlin.ir.builders.irIfThenElse +import org.jetbrains.kotlin.ir.builders.irTrue import org.jetbrains.kotlin.ir.expressions.IrExpression import org.jetbrains.kotlin.ir.util.deepCopyWithSymbols abstract class DiagramGenerator { - abstract fun IrBuilderWithScope.buildAssertThrow(variables: List): IrExpression + abstract fun IrBuilderWithScope.buildCall( + argument: IrExpression, + variables: List + ): IrExpression - fun buildAssert( + fun buildExpression( builder: IrBuilderWithScope, root: Node ): IrExpression { return builder.irBlock { - buildAssert(root, listOf()) { subStack -> - buildAssertThrow(subStack) + buildExpression(root, listOf()) { argument, subStack -> + buildCall(argument, subStack) } } } - private fun IrStatementsBuilder<*>.buildAssert( + private fun IrStatementsBuilder<*>.buildExpression( node: Node, variables: List, - thenPart: IrStatementsBuilder<*>.(List) -> IrExpression - ): List = when (node) { - is ExpressionNode -> add(node, variables, thenPart) - is AndNode -> chain(node, variables, thenPart) - is OrNode -> nest(node, 0, variables) - else -> TODO("Unknown node type=$node") + call: IrStatementsBuilder<*>.(IrExpression, List) -> IrExpression + ) { + when (node) { + is ExpressionNode -> add(node, variables, call) + is AndNode -> nest(node, 0, variables, call) + is OrNode -> nest(node, 0, variables, call) + else -> TODO("Unknown node type=$node") + } } /** - * TODO - this is how the following function should behave * ``` * val result = call(1 + 2 + 3) * ``` - * Transformed to + * Transforms to * ``` * val result = run { * val tmp0 = 1 + 2 @@ -66,67 +71,63 @@ abstract class DiagramGenerator { private fun IrStatementsBuilder<*>.add( node: ExpressionNode, variables: List, - thenPart: IrStatementsBuilder<*>.(List) -> IrExpression - ): List { + call: IrStatementsBuilder<*>.(IrExpression, List) -> IrExpression + ) { val head = node.expressions.first().deepCopyWithSymbols(scope.getLocalDeclarationParent()) val expressions = (buildTree(head) as ExpressionNode).expressions val transformer = IrTemporaryExtractionTransformer(this, expressions.toSet()) val transformed = expressions.first().transform(transformer, null) - if (transformed.type == context.irBuiltIns.booleanType) { - // TODO irIfThenElse to support property nesting of ANDs & ORs - +irIfThen(irNot(transformed), thenPart(variables + transformer.variables)) - } else { - +thenPart(variables + transformer.variables) - } - - return transformer.variables + +call(transformed, variables + transformer.variables) } /** - * TODO - this is how the following function should behave * ``` * val result = call(1 == 1 && 2 == 2) * ``` - * Transformed to + * Transforms to * ``` * val result = run { * val tmp0 = 1 == 1 * if (tmp0) { * val tmp1 = 2 == 2 - * if (tmp1) call(true, ) - * else call(false, ) + * call(tmp1, ) * } * else call(false, ) * } * ``` */ - private fun IrStatementsBuilder<*>.chain( + private fun IrStatementsBuilder<*>.nest( node: AndNode, + index: Int, variables: List, - thenPart: IrStatementsBuilder<*>.(List) -> IrExpression - ): List { - val newVariables = mutableListOf() - for (child in node.children) { - newVariables.addAll(buildAssert(child, variables + newVariables, thenPart)) + call: IrStatementsBuilder<*>.(IrExpression, List) -> IrExpression + ) { + val children = node.children + val child = children[index] + buildExpression(child, variables) { argument, newVariables -> + if (index + 1 == children.size) call(argument, newVariables) // last expression, result is false + else irIfThenElse( + context.irBuiltIns.anyType, + argument, + irBlock { nest(node, index + 1, newVariables, call) }, // more expressions, continue nesting + call(irFalse(), newVariables), // short-circuit result to false + ) } - return newVariables } /** - * TODO - this is how the following function should behave * ``` * val result = call(1 == 1 || 2 == 2) * ``` - * Transformed to + * Transforms to * ``` * val result = run { * val tmp0 = 1 == 1 * if (tmp0) call(true, ) * else { * val tmp1 = 2 == 2 - * if (tmp1) call(true, ) - * else call(false, ) + * call(tmp1, ) * } * } * ``` @@ -134,14 +135,19 @@ abstract class DiagramGenerator { private fun IrStatementsBuilder<*>.nest( node: OrNode, index: Int, - variables: List - ): List { + variables: List, + call: IrStatementsBuilder<*>.(IrExpression, List) -> IrExpression + ) { val children = node.children val child = children[index] - buildAssert(child, variables) { newVariables -> - if (index + 1 == children.size) buildAssertThrow(newVariables) - else irBlock { nest(node, index + 1, newVariables) } + buildExpression(child, variables) { argument, newVariables -> + if (index + 1 == children.size) call(argument, newVariables) // last expression, result is false + else irIfThenElse( + context.irBuiltIns.anyType, + argument, + call(irTrue(), newVariables), // short-circuit result to true + irBlock { nest(node, index + 1, newVariables, call) }, // more expressions, continue nesting + ) } - return emptyList() } } 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 6dbef3015d9..0e1dacfa322 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 @@ -59,7 +59,7 @@ fun IrBuilderWithScope.irDiagram( addArgument( irString { - appendLine() + if (row != 0 || prefix != null) appendLine() append(rowSource) if (indentations.isNotEmpty()) { appendLine() diff --git a/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/AssertBooleanTest.kt b/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/AssertBooleanTest.kt new file mode 100644 index 00000000000..786287f45d0 --- /dev/null +++ b/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/AssertBooleanTest.kt @@ -0,0 +1,45 @@ +package com.bnorm.power + +import org.jetbrains.kotlin.name.FqName +import org.junit.Test +import kotlin.test.assertEquals + +class AssertBooleanTest { + @Test + fun `test assertTrue transformation`() { + assertMessage( + """ +import kotlin.test.assertTrue + +fun main() { + assertTrue(1 != 1) +}""", + """ +Assertion failed +assertTrue(1 != 1) + | + false +""".trimIndent(), + PowerAssertComponentRegistrar(setOf(FqName("kotlin.test.assertTrue"))) + ) + } + + @Test + fun `test assertFalse transformation`() { + assertMessage( + """ +import kotlin.test.assertFalse + +fun main() { + assertFalse(1 == 1) +}""", + """ +Assertion failed +assertFalse(1 == 1) + | + true +""".trimIndent(), + PowerAssertComponentRegistrar(setOf(FqName("kotlin.test.assertFalse"))) + ) + } +} diff --git a/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/DebugFunctionTest.kt b/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/DebugFunctionTest.kt new file mode 100644 index 00000000000..1470322fe03 --- /dev/null +++ b/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/DebugFunctionTest.kt @@ -0,0 +1,80 @@ +package com.bnorm.power + +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile +import org.jetbrains.kotlin.name.FqName +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.io.PrintStream +import java.lang.reflect.InvocationTargetException +import kotlin.test.assertEquals + +class DebugFunctionTest { + @Test + fun `debug function transformation`() { + val actual = executeMainDebug(""" + dbg(1 + 2 + 3) + """.trimIndent()) + assertEquals( + """ + dbg(1 + 2 + 3) + | | + | 6 + 3 + """.trimIndent(), + actual.trim() + ) + } + @Test + fun `debug function transformation with message`() { + val actual = executeMainDebug(""" + dbg(1 + 2 + 3, "Message:") + """.trimIndent()) + assertEquals( + """ + Message: + dbg(1 + 2 + 3, "Message:") + | | + | 6 + 3 + """.trimIndent(), + actual.trim() + ) + } +} + +fun executeMainDebug(mainBody: String): String { + val file = SourceFile.kotlin( + name = "main.kt", + contents = """ +fun dbg(value: T): T = value + +fun dbg(value: T, msg: String): T { + println(msg) + return value +} + +fun main() { + $mainBody +} +""", + trimIndent = false + ) + + val result = compile(listOf(file), PowerAssertComponentRegistrar(setOf(FqName("dbg")))) + assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode) + + val kClazz = result.classLoader.loadClass("MainKt") + val main = kClazz.declaredMethods.single { it.name == "main" && it.parameterCount == 0 } + val prevOut = System.out + try { + val out = ByteArrayOutputStream() + System.setOut(PrintStream(out)) + main.invoke(null) + return out.toString(Charsets.UTF_8) + } catch (t: InvocationTargetException) { + throw t.cause!! + } finally { + System.setOut(prevOut) + } +} diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index f76fdb01494..3374efc02a5 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -62,6 +62,7 @@ configure { "kotlin.test.assertTrue", "kotlin.require", "com.bnorm.power.AssertScope.assert", - "com.bnorm.power.assert" + "com.bnorm.power.assert", + "com.bnorm.power.dbg" ) } diff --git a/sample/src/commonMain/kotlin/com/bnorm/power/Debug.kt b/sample/src/commonMain/kotlin/com/bnorm/power/Debug.kt new file mode 100644 index 00000000000..0e0c85d320e --- /dev/null +++ b/sample/src/commonMain/kotlin/com/bnorm/power/Debug.kt @@ -0,0 +1,10 @@ +package com.bnorm.power + +val debugLog = StringBuilder() + +fun dbg(value: T): T = value + +fun dbg(value: T, msg: String): T { + debugLog.appendLine(msg) + return value +} diff --git a/sample/src/commonTest/kotlin/com/bnorm/power/PowerAssertTest.kt b/sample/src/commonTest/kotlin/com/bnorm/power/PowerAssertTest.kt index 0d5d607acb5..8e374f9396e 100644 --- a/sample/src/commonTest/kotlin/com/bnorm/power/PowerAssertTest.kt +++ b/sample/src/commonTest/kotlin/com/bnorm/power/PowerAssertTest.kt @@ -16,6 +16,7 @@ package com.bnorm.power +import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -23,6 +24,11 @@ import kotlin.test.assertTrue class PowerAssertTest { + @AfterTest + fun cleanup() { + debugLog.clear() + } + @Test fun assertTrue() { val error = assertFailsWith { assertTrue(Person.UNKNOWN.size == 1) } @@ -111,4 +117,35 @@ class PowerAssertTest { """.trimIndent(), ) } + + @Test + fun dbgTest() { + val name = "Jane" + val greeting = dbg("Hello, $name") + assert(greeting == "Hello, Jane") + assertEquals( + actual = debugLog.toString().trim(), + expected = """ + dbg("Hello, ${"$"}name") + | | + | Jane + Hello, Jane + """.trimIndent()) + } + + @Test + fun dbgMessageTest() { + val name = "Jane" + val greeting = dbg("Hello, $name", "Greeting:") + assert(greeting == "Hello, Jane") + assertEquals( + actual = debugLog.toString().trim(), + expected = """ + Greeting: + dbg("Hello, ${"$"}name", "Greeting:") + | | + | Jane + Hello, Jane + """.trimIndent()) + } }