From 479f37276c37c8db6440de7c293db980b54008c1 Mon Sep 17 00:00:00 2001 From: Brian Norman Date: Fri, 26 Nov 2021 14:37:56 -0600 Subject: [PATCH] Support Java Suppliers as message arguments --- .../bnorm/power/PowerAssertCallTransformer.kt | 50 +++-- .../SamConversionLambdaFunctionDelegate.kt | 47 ++++ .../internal/ReturnableBlockTransformer.kt | 98 --------- .../bnorm/power/{test.kt => AssertTest.kt} | 2 +- .../com/bnorm/power/Junit5AssertionsTest.kt | 201 ++++++++++++++++++ ...LibraryTest.kt => KotlinTestAssertTest.kt} | 2 +- 6 files changed, 284 insertions(+), 116 deletions(-) create mode 100644 kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/delegate/SamConversionLambdaFunctionDelegate.kt delete mode 100644 kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/internal/ReturnableBlockTransformer.kt rename kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/{test.kt => AssertTest.kt} (99%) create mode 100644 kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/Junit5AssertionsTest.kt rename kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/{AssertLibraryTest.kt => KotlinTestAssertTest.kt} (99%) 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 f3a838fe779..2216c7d36c3 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 @@ -18,6 +18,7 @@ package com.bnorm.power import com.bnorm.power.delegate.FunctionDelegate import com.bnorm.power.delegate.LambdaFunctionDelegate +import com.bnorm.power.delegate.SamConversionLambdaFunctionDelegate import com.bnorm.power.delegate.SimpleFunctionDelegate import com.bnorm.power.diagram.IrTemporaryVariable import com.bnorm.power.diagram.Node @@ -26,18 +27,15 @@ import com.bnorm.power.diagram.buildTree import com.bnorm.power.diagram.info import com.bnorm.power.diagram.irDiagramString import com.bnorm.power.diagram.substring -import com.bnorm.power.internal.ReturnableBlockTransformer import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext -import org.jetbrains.kotlin.backend.common.ir.asInlinable -import org.jetbrains.kotlin.backend.common.ir.inline import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.ir.IrElement import org.jetbrains.kotlin.ir.backend.js.utils.asString -import org.jetbrains.kotlin.ir.builders.irBlock +import org.jetbrains.kotlin.ir.builders.irCall import org.jetbrains.kotlin.ir.builders.irString import org.jetbrains.kotlin.ir.builders.parent import org.jetbrains.kotlin.ir.declarations.IrFile @@ -46,20 +44,25 @@ import org.jetbrains.kotlin.ir.declarations.path import org.jetbrains.kotlin.ir.expressions.IrCall import org.jetbrains.kotlin.ir.expressions.IrConst import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.expressions.IrFunctionExpression import org.jetbrains.kotlin.ir.expressions.IrStringConcatenation +import org.jetbrains.kotlin.ir.expressions.IrTypeOperatorCall 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.IrTypeSystemContextImpl +import org.jetbrains.kotlin.ir.types.classOrNull import org.jetbrains.kotlin.ir.types.classifierOrNull import org.jetbrains.kotlin.ir.types.isBoolean import org.jetbrains.kotlin.ir.types.isSubtypeOf +import org.jetbrains.kotlin.ir.types.isSubtypeOfClass import org.jetbrains.kotlin.ir.util.deepCopyWithSymbols +import org.jetbrains.kotlin.ir.util.functions import org.jetbrains.kotlin.ir.util.isFunctionOrKFunction import org.jetbrains.kotlin.ir.util.kotlinFqName -import org.jetbrains.kotlin.ir.util.patchDeclarationParents +import org.jetbrains.kotlin.ir.util.parentClassOrNull import org.jetbrains.kotlin.name.FqName class PowerAssertCallTransformer( @@ -122,10 +125,6 @@ class PowerAssertCallTransformer( val symbol = currentScope!!.scope.scopeOwnerSymbol val builder = DeclarationIrBuilder(context, symbol, expression.startOffset, expression.endOffset) return builder.diagram(expression, delegate, messageArgument, roots) -// .also { println(expression.dump()) } -// .also { println(it.dump()) } -// .also { println(expression.dumpKotlinLike()) } -// .also { println(it.dumpKotlinLike()) } } private fun DeclarationIrBuilder.diagram( @@ -163,12 +162,14 @@ class PowerAssertCallTransformer( return when { messageArgument is IrConst<*> -> messageArgument messageArgument is IrStringConcatenation -> messageArgument - messageArgument != null -> irBlock { - +messageArgument.deepCopyWithSymbols(parent) - .asInlinable(this) - .inline(parent) - .patchDeclarationParents(scope.getLocalDeclarationParent()) - }.transform(ReturnableBlockTransformer(context, scope.scopeOwnerSymbol), null) + messageArgument is IrFunctionExpression -> { + val invoke = messageArgument.type.classOrNull!!.functions + .filter { it.owner.name.toString() == "invoke" } + .single() + irCall(invoke).apply { dispatchReceiver = messageArgument } + } + // SAMs conversion can be unwrapped to IrFunctionExpression + messageArgument is IrTypeOperatorCall -> buildMessagePrefix(messageArgument.argument, roots, original) // TODO what should the default message be? roots.size == 1 && original.getValueArgument(0)!!.type.isBoolean() -> irString("Assertion failed") else -> null @@ -179,7 +180,16 @@ class PowerAssertCallTransformer( val values = function.valueParameters if (values.isEmpty()) return emptyList() - return context.referenceFunctions(function.kotlinFqName) + // Java static functions require searching by class + val parentClassFunctions = (function.parentClassOrNull + ?.let { context.referenceClass(it.kotlinFqName) } + ?.functions ?: emptySequence()) + .filter { it.owner.kotlinFqName == function.kotlinFqName } + .toList() + val possible = (context.referenceFunctions(function.kotlinFqName) + parentClassFunctions) + .distinct() + + return possible .mapNotNull { overload -> val parameters = overload.owner.valueParameters if (parameters.size !in values.size..values.size + 1) return@mapNotNull null @@ -191,6 +201,8 @@ class PowerAssertCallTransformer( return@mapNotNull when { isStringSupertype(messageParameter.type) -> SimpleFunctionDelegate(overload) isStringFunction(messageParameter.type) -> LambdaFunctionDelegate(overload, messageParameter) + isStringJavaSupplierFunction(messageParameter.type) -> + SamConversionLambdaFunctionDelegate(overload, messageParameter) else -> null } } @@ -199,6 +211,12 @@ class PowerAssertCallTransformer( private fun isStringFunction(type: IrType): Boolean = type.isFunctionOrKFunction() && type is IrSimpleType && (type.arguments.size == 1 && isStringSupertype(type.arguments.first())) + private fun isStringJavaSupplierFunction(type: IrType): Boolean { + val javaSupplier = context.referenceClass(FqName("java.util.function.Supplier")) + return javaSupplier != null && type.isSubtypeOfClass(javaSupplier) + && type is IrSimpleType && (type.arguments.size == 1 && isStringSupertype(type.arguments.first())) + } + private fun isStringSupertype(argument: IrTypeArgument): Boolean = argument is IrTypeProjection && isStringSupertype(argument.type) diff --git a/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/delegate/SamConversionLambdaFunctionDelegate.kt b/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/delegate/SamConversionLambdaFunctionDelegate.kt new file mode 100644 index 00000000000..5fca3ad1fdf --- /dev/null +++ b/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/delegate/SamConversionLambdaFunctionDelegate.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 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.delegate + +import com.bnorm.power.irLambda +import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope +import org.jetbrains.kotlin.ir.builders.irReturn +import org.jetbrains.kotlin.ir.builders.typeOperator +import org.jetbrains.kotlin.ir.declarations.IrValueParameter +import org.jetbrains.kotlin.ir.expressions.IrCall +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.expressions.IrTypeOperator +import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol + +class SamConversionLambdaFunctionDelegate( + private val overload: IrSimpleFunctionSymbol, + private val messageParameter: IrValueParameter +) : FunctionDelegate { + override val function = overload.owner + + override fun buildCall( + builder: IrBuilderWithScope, + original: IrCall, + arguments: List, + message: IrExpression + ): IrExpression = with(builder) { + val lambda = irLambda(context.irBuiltIns.stringType, messageParameter.type) { + +irReturn(message) + } + val expression = typeOperator(messageParameter.type, lambda, IrTypeOperator.SAM_CONVERSION, messageParameter.type) + irCallCopy(overload, original, arguments, expression) + } +} diff --git a/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/internal/ReturnableBlockTransformer.kt b/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/internal/ReturnableBlockTransformer.kt deleted file mode 100644 index f5df2377e6e..00000000000 --- a/kotlin-power-assert-plugin/src/main/kotlin/com/bnorm/power/internal/ReturnableBlockTransformer.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. - * - * Copied from https://github.com/JetBrains/kotlin/blob/1.4.20/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/lower/ReturnableBlockLowering.kt - */ - -package com.bnorm.power.internal - -import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext -import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder -import org.jetbrains.kotlin.backend.common.lower.irComposite -import org.jetbrains.kotlin.ir.builders.* -import org.jetbrains.kotlin.ir.expressions.* -import org.jetbrains.kotlin.ir.expressions.impl.* -import org.jetbrains.kotlin.ir.symbols.IrReturnableBlockSymbol -import org.jetbrains.kotlin.ir.symbols.IrSymbol -import org.jetbrains.kotlin.ir.transformStatement - -// TODO Remove when inlining works correctly on Kotlin/JS and Kotlin/Native -class ReturnableBlockTransformer(val context: IrGeneratorContext, val containerSymbol: IrSymbol? = null) : IrElementTransformerVoidWithContext() { - private var labelCnt = 0 - private val returnMap = mutableMapOf IrExpression>() - - override fun visitReturn(expression: IrReturn): IrExpression { - expression.transformChildrenVoid() - return returnMap[expression.returnTargetSymbol]?.invoke(expression) ?: expression - } - - override fun visitContainerExpression(expression: IrContainerExpression): IrExpression { - if (expression !is IrReturnableBlock) return super.visitContainerExpression(expression) - - val scopeSymbol = currentScope?.scope?.scopeOwnerSymbol ?: containerSymbol - val builder = DeclarationIrBuilder(context, scopeSymbol!!) - val variable by lazy { - builder.scope.createTmpVariable(expression.type, "tmp\$ret\$${labelCnt++}", true) - } - - val loop by lazy { - IrDoWhileLoopImpl( - expression.startOffset, - expression.endOffset, - context.irBuiltIns.unitType, - expression.origin - ).apply { - label = "l\$ret\$${labelCnt++}" - condition = builder.irBoolean(false) - } - } - - var hasReturned = false - - returnMap[expression.symbol] = { returnExpression -> - hasReturned = true - builder.irComposite(returnExpression) { - +irSet(variable.symbol, returnExpression.value) - +irBreak(loop) - } - } - - val newStatements = expression.statements.mapIndexed { i, s -> - if (i == expression.statements.lastIndex && s is IrReturn && s.returnTargetSymbol == expression.symbol) { - s.transformChildrenVoid() - if (!hasReturned) s.value else { - builder.irSet(variable.symbol, s.value) - } - } else { - s.transformStatement(this) - } - } - - returnMap.remove(expression.symbol) - - if (!hasReturned) { - return IrCompositeImpl( - expression.startOffset, - expression.endOffset, - expression.type, - expression.origin, - newStatements - ) - } else { - loop.body = IrBlockImpl( - expression.startOffset, - expression.endOffset, - context.irBuiltIns.unitType, - expression.origin, - newStatements - ) - - return builder.irComposite(expression, expression.origin) { - +variable - +loop - +irGet(variable) - } - } - } -} diff --git a/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/test.kt b/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/AssertTest.kt similarity index 99% rename from kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/test.kt rename to kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/AssertTest.kt index d8904dcea70..6a04e851d5b 100644 --- a/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/test.kt +++ b/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/AssertTest.kt @@ -19,7 +19,7 @@ package com.bnorm.power import kotlin.test.Test import org.jetbrains.kotlin.name.FqName -class CompilerTest { +class AssertTest { @Test fun memberFunctions() { assertMessage( diff --git a/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/Junit5AssertionsTest.kt b/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/Junit5AssertionsTest.kt new file mode 100644 index 00000000000..f584fd634d5 --- /dev/null +++ b/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/Junit5AssertionsTest.kt @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2021 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 org.jetbrains.kotlin.name.FqName + +class Junit5AssertionsTest { + @Test + fun `test JUnit5 Assertions#assertTrue transformation`() { + assertMessage( + """ + import org.junit.jupiter.api.Assertions.assertTrue + + fun main() { + assertTrue(1 != 1) + }""", + """ + Assertion failed + assertTrue(1 != 1) + | + false ==> expected: but was: + """.trimIndent(), + PowerAssertComponentRegistrar(setOf(FqName("org.junit.jupiter.api.Assertions.assertTrue"))) + ) + } + + @Test + fun `test JUnit5 Assertions#assertTrue transformation with message`() { + assertMessage( + """ + import org.junit.jupiter.api.Assertions.assertTrue + + fun main() { + assertTrue(1 != 1, "Message:") + }""", + """ + Message: + assertTrue(1 != 1, "Message:") + | + false ==> expected: but was: + """.trimIndent(), + PowerAssertComponentRegistrar(setOf(FqName("org.junit.jupiter.api.Assertions.assertTrue"))) + ) + } + + @Test + fun `test JUnit5 Assertions#assertTrue transformation with message supplier`() { + assertMessage( + """ + import org.junit.jupiter.api.Assertions.assertTrue + + fun main() { + assertTrue(1 != 1) { "Message:" } + }""", + """ + Message: + assertTrue(1 != 1) { "Message:" } + | + false ==> expected: but was: + """.trimIndent(), + PowerAssertComponentRegistrar(setOf(FqName("org.junit.jupiter.api.Assertions.assertTrue"))) + ) + } + + @Test + fun `test JUnit5 Assertions#assertFalse transformation`() { + assertMessage( + """ + import org.junit.jupiter.api.Assertions.assertFalse + + fun main() { + assertFalse(1 == 1) + }""", + """ + Assertion failed + assertFalse(1 == 1) + | + true ==> expected: but was: + """.trimIndent(), + PowerAssertComponentRegistrar(setOf(FqName("org.junit.jupiter.api.Assertions.assertFalse"))) + ) + } + + @Test + fun `test JUnit5 Assertions#assertFalse transformation with message`() { + assertMessage( + """ + import org.junit.jupiter.api.Assertions.assertFalse + + fun main() { + assertFalse(1 == 1, "Message:") + }""", + """ + Message: + assertFalse(1 == 1, "Message:") + | + true ==> expected: but was: + """.trimIndent(), + PowerAssertComponentRegistrar(setOf(FqName("org.junit.jupiter.api.Assertions.assertFalse"))) + ) + } + + @Test + fun `test JUnit5 Assertions#assertFalse transformation with message supplier`() { + assertMessage( + """ + import org.junit.jupiter.api.Assertions.assertFalse + + fun main() { + assertFalse(1 == 1) { "Message:" } + }""", + """ + Message: + assertFalse(1 == 1) { "Message:" } + | + true ==> expected: but was: + """.trimIndent(), + PowerAssertComponentRegistrar(setOf(FqName("org.junit.jupiter.api.Assertions.assertFalse"))) + ) + } + + @Test + fun `test JUnit5 Assertions#assertEquals transformation`() { + assertMessage( + """ + import org.junit.jupiter.api.Assertions.assertEquals + + fun main() { + val greeting = "Hello" + val name = "World" + assertEquals(greeting, name) + }""", + """ + assertEquals(greeting, name) + | | + | World + Hello ==> expected: but was: + """.trimIndent(), + PowerAssertComponentRegistrar(setOf(FqName("org.junit.jupiter.api.Assertions.assertEquals"))) + ) + } + + @Test + fun `test JUnit5 Assertions#assertEquals transformation with message`() { + assertMessage( + """ + import org.junit.jupiter.api.Assertions.assertEquals + + fun main() { + val greeting = "Hello" + val name = "World" + assertEquals(greeting, name, "Message:") + }""", + """ + Message: + assertEquals(greeting, name, "Message:") + | | + | World + Hello ==> expected: but was: + """.trimIndent(), + PowerAssertComponentRegistrar(setOf(FqName("org.junit.jupiter.api.Assertions.assertEquals"))) + ) + } + + @Test + fun `test JUnit5 Assertions#assertEquals transformation with message supplier`() { + assertMessage( + """ + import org.junit.jupiter.api.Assertions.assertEquals + + fun main() { + val greeting = "Hello" + val name = "World" + assertEquals(greeting, name) { "Message:" } + }""", + """ + Message: + assertEquals(greeting, name) { "Message:" } + | | + | World + Hello ==> expected: but was: + """.trimIndent(), + PowerAssertComponentRegistrar(setOf(FqName("org.junit.jupiter.api.Assertions.assertEquals"))) + ) + } +} diff --git a/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/AssertLibraryTest.kt b/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/KotlinTestAssertTest.kt similarity index 99% rename from kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/AssertLibraryTest.kt rename to kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/KotlinTestAssertTest.kt index adfd7dec779..ec6b3a0205e 100644 --- a/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/AssertLibraryTest.kt +++ b/kotlin-power-assert-plugin/src/test/kotlin/com/bnorm/power/KotlinTestAssertTest.kt @@ -19,7 +19,7 @@ package com.bnorm.power import kotlin.test.Test import org.jetbrains.kotlin.name.FqName -class AssertLibraryTest { +class KotlinTestAssertTest { @Test fun `test assertTrue transformation`() { assertMessage(