diff --git a/libraries/tools/kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertGradleExtension.kt b/libraries/tools/kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertGradleExtension.kt new file mode 100644 index 00000000000..151f9cfa1c5 --- /dev/null +++ b/libraries/tools/kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertGradleExtension.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2020 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 + +open class PowerAssertGradleExtension { + var functions: List = listOf("kotlin.assert") + var excludedSourceSets: List = listOf() +} diff --git a/libraries/tools/kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertGradlePlugin.kt b/libraries/tools/kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertGradlePlugin.kt new file mode 100644 index 00000000000..aa061a55b3c --- /dev/null +++ b/libraries/tools/kotlin-power-assert/src/main/kotlin/com/bnorm/power/PowerAssertGradlePlugin.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2020 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 org.gradle.api.Project +import org.gradle.api.provider.Provider +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin +import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact +import org.jetbrains.kotlin.gradle.plugin.SubpluginOption + +class PowerAssertGradlePlugin : KotlinCompilerPluginSupportPlugin { + override fun apply(target: Project): Unit = with(target) { + extensions.create("kotlinPowerAssert", PowerAssertGradleExtension::class.java) + } + + override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean { + val project = kotlinCompilation.target.project + val extension = project.extensions.getByType(PowerAssertGradleExtension::class.java) + return extension.excludedSourceSets.none { it == kotlinCompilation.defaultSourceSet.name } + } + + override fun getCompilerPluginId(): String = "com.bnorm.kotlin-power-assert" + + override fun getPluginArtifact(): SubpluginArtifact = SubpluginArtifact( + groupId = BuildConfig.PLUGIN_GROUP_ID, + artifactId = BuildConfig.PLUGIN_ARTIFACT_ID, + version = BuildConfig.PLUGIN_VERSION, + ) + + override fun applyToCompilation( + kotlinCompilation: KotlinCompilation<*>, + ): Provider> { + val project = kotlinCompilation.target.project + val extension = project.extensions.getByType(PowerAssertGradleExtension::class.java) + return project.provider { + extension.functions.map { + SubpluginOption(key = "function", value = it) + } + } + } +} diff --git a/plugins/power-assert/power-assert.backend/src/com/bnorm/power/IrUtils.kt b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/IrUtils.kt new file mode 100644 index 00000000000..6e1f201962d --- /dev/null +++ b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/IrUtils.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2020 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 org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities +import org.jetbrains.kotlin.ir.builders.IrBlockBodyBuilder +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.irString +import org.jetbrains.kotlin.ir.builders.parent +import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin +import org.jetbrains.kotlin.ir.expressions.IrFunctionExpression +import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin +import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionExpressionImpl +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.name.Name + +fun IrBuilderWithScope.irString(builderAction: StringBuilder.() -> Unit) = + irString(buildString { builderAction() }) + +fun IrBuilderWithScope.irLambda( + returnType: IrType, + lambdaType: IrType, + startOffset: Int = this.startOffset, + endOffset: Int = this.endOffset, + block: IrBlockBodyBuilder.() -> Unit, +): IrFunctionExpression { + val scope = this + val lambda = context.irFactory.buildFun { + this.startOffset = startOffset + this.endOffset = endOffset + name = Name.special("") + this.returnType = returnType + visibility = DescriptorVisibilities.LOCAL + origin = IrDeclarationOrigin.LOCAL_FUNCTION_FOR_LAMBDA + }.apply { + val bodyBuilder = DeclarationIrBuilder(context, symbol, startOffset, endOffset) + body = bodyBuilder.irBlockBody { + block() + } + parent = scope.parent + } + return IrFunctionExpressionImpl(startOffset, endOffset, lambdaType, lambda, IrStatementOrigin.LAMBDA) +} diff --git a/plugins/power-assert/power-assert.backend/src/com/bnorm/power/PowerAssertCallTransformer.kt b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/PowerAssertCallTransformer.kt new file mode 100644 index 00000000000..58e20cbb87d --- /dev/null +++ b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/PowerAssertCallTransformer.kt @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2020 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 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 +import com.bnorm.power.diagram.SourceFile +import com.bnorm.power.diagram.buildDiagramNesting +import com.bnorm.power.diagram.buildDiagramNestingNullable +import com.bnorm.power.diagram.buildTree +import com.bnorm.power.diagram.irDiagramString +import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder +import org.jetbrains.kotlin.backend.jvm.ir.parentClassId +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.irCall +import org.jetbrains.kotlin.ir.builders.irString +import org.jetbrains.kotlin.ir.builders.parent +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.IrValueParameter +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.IrGetValue +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.classId +import org.jetbrains.kotlin.ir.util.deepCopyWithSymbols +import org.jetbrains.kotlin.ir.util.functions +import org.jetbrains.kotlin.ir.util.isFileClass +import org.jetbrains.kotlin.ir.util.isFunctionOrKFunction +import org.jetbrains.kotlin.ir.util.kotlinFqName +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName + +class PowerAssertCallTransformer( + private val sourceFile: SourceFile, + private val context: IrPluginContext, + private val messageCollector: MessageCollector, + private val functions: Set, +) : IrElementTransformerVoidWithContext() { + private val irTypeSystemContext = IrTypeSystemContextImpl(context.irBuiltIns) + + override fun visitCall(expression: IrCall): IrExpression { + 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 + // TODO better way to determine which delegate to actually use + val delegates = findDelegates(function) + val delegate = delegates.maxByOrNull { it.function.valueParameters.size } + if (delegate == null) { + val valueTypesTruncated = function.valueParameters.subList(0, function.valueParameters.size - 1) + .joinToString("") { it.type.asString() + ", " } + val valueTypesAll = function.valueParameters.joinToString("") { it.type.asString() + ", " } + messageCollector.warn( + expression = expression, + message = """ + |Unable to find overload of function $fqName for power-assert transformation callable as: + | - $fqName(${valueTypesTruncated}String) + | - $fqName($valueTypesTruncated() -> String) + | - $fqName(${valueTypesAll}String) + | - $fqName($valueTypesAll() -> String) + """.trimMargin(), + ) + return super.visitCall(expression) + } + + val dispatchRoot = + if (expression.symbol.owner.isInfix) expression.dispatchReceiver?.let { buildTree(it) } else null + val extensionRoot = + if (expression.symbol.owner.isInfix) expression.extensionReceiver?.let { buildTree(it) } else null + val messageArgument: IrExpression? + val roots: List + if (delegate.function.valueParameters.size == function.valueParameters.size) { + messageArgument = expression.getValueArgument(expression.valueArgumentsCount - 1) + roots = (0 until expression.valueArgumentsCount - 1) + .map { index -> expression.getValueArgument(index) } + .map { arg -> arg?.let { buildTree(it) } } + } else { + messageArgument = null + roots = (0 until expression.valueArgumentsCount) + .map { index -> expression.getValueArgument(index) } + .map { arg -> arg?.let { buildTree(it) } } + } + + // If all roots are null, there are no transformable parameters + if (dispatchRoot == null && extensionRoot == null && roots.all { it == null }) { + messageCollector.info(expression, "Expression is constant and will not be power-assert transformed") + return super.visitCall(expression) + } + + val symbol = currentScope!!.scope.scopeOwnerSymbol + val builder = DeclarationIrBuilder(context, symbol, expression.startOffset, expression.endOffset) + return builder.diagram( + call = expression, + delegate = delegate, + messageArgument = messageArgument, + roots = roots, + dispatchRoot = dispatchRoot, + extensionRoot = extensionRoot, + ) + } + + private fun DeclarationIrBuilder.diagram( + call: IrCall, + delegate: FunctionDelegate, + messageArgument: IrExpression?, + roots: List, + dispatchRoot: Node? = null, + extensionRoot: Node? = null, + ): IrExpression { + fun recursive( + index: Int, + dispatch: IrExpression?, + extension: IrExpression?, + arguments: List, + variables: List, + ): IrExpression { + if (index >= roots.size) { + val prefix = buildMessagePrefix(messageArgument, delegate.messageParameter, roots, call) + ?.deepCopyWithSymbols(parent) + val diagram = irDiagramString(sourceFile, prefix, call, variables) + return delegate.buildCall(this, call, dispatch, extension, arguments, diagram) + } else { + val root = roots[index] + if (root == null) { + val newArguments = arguments + call.getValueArgument(index) + return recursive(index + 1, dispatch, extension, newArguments, variables) + } else { + return buildDiagramNesting(root, variables) { argument, newVariables -> + val newArguments = arguments + argument + recursive(index + 1, dispatch, extension, newArguments, newVariables) + } + } + } + } + + return buildDiagramNestingNullable(dispatchRoot) { dispatch, dispatchNewVariables -> + buildDiagramNestingNullable(extensionRoot, dispatchNewVariables) { extension, extensionNewVariables -> + recursive(0, dispatch, extension, emptyList(), extensionNewVariables) + } + } + } + + private fun DeclarationIrBuilder.buildMessagePrefix( + messageArgument: IrExpression?, + messageParameter: IrValueParameter, + roots: List, + original: IrCall, + ): IrExpression? { + return when { + messageArgument is IrConst<*> -> messageArgument + messageArgument is IrStringConcatenation -> messageArgument + messageArgument is IrGetValue -> { + if (messageArgument.type.isAssignableTo(context.irBuiltIns.stringType)) { + return messageArgument + } else { + val invoke = messageParameter.type.classOrNull!!.functions + .filter { !it.owner.isFakeOverride } // TODO best way to find single access method? + .single() + irCall(invoke).apply { dispatchReceiver = messageArgument } + } + } + // Kotlin Lambda or SAMs conversion lambda + messageArgument is IrFunctionExpression || messageArgument is IrTypeOperatorCall -> { + val invoke = messageParameter.type.classOrNull!!.functions + .filter { !it.owner.isFakeOverride } // TODO best way to find single access method? + .single() + irCall(invoke).apply { dispatchReceiver = messageArgument } + } + // TODO what should the default message be? + roots.size == 1 && original.getValueArgument(0)!!.type.isBoolean() -> irString("Assertion failed") + else -> null + } + } + + private fun findDelegates(function: IrFunction): List { + val values = function.valueParameters + if (values.isEmpty()) return emptyList() + + // Java static functions require searching by class + val parentClassFunctions = ( + function.parentClassId + ?.let { context.referenceClass(it) } + ?.functions ?: emptySequence() + ) + .filter { it.owner.kotlinFqName == function.kotlinFqName } + .toList() + val possible = (context.referenceFunctions(function.callableId) + parentClassFunctions) + .distinct() + + return possible.mapNotNull { overload -> + // Dispatch receivers must always match exactly + if (function.dispatchReceiverParameter?.type != overload.owner.dispatchReceiverParameter?.type) { + return@mapNotNull null + } + + // Extension receiver may only be assignable + if (!function.extensionReceiverParameter?.type.isAssignableTo(overload.owner.extensionReceiverParameter?.type)) { + return@mapNotNull null + } + + val parameters = overload.owner.valueParameters + if (parameters.size !in values.size..values.size + 1) return@mapNotNull null + if (!parameters.zip(values).all { (param, value) -> value.type.isAssignableTo(param.type) }) { + return@mapNotNull null + } + + val messageParameter = parameters.last() + return@mapNotNull when { + isStringSupertype(messageParameter.type) -> SimpleFunctionDelegate(overload, messageParameter) + isStringFunction(messageParameter.type) -> LambdaFunctionDelegate(overload, messageParameter) + isStringJavaSupplierFunction(messageParameter.type) -> + SamConversionLambdaFunctionDelegate(overload, messageParameter) + else -> null + } + } + } + + 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(ClassId.topLevel(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) + + private fun isStringSupertype(type: IrType): Boolean = + context.irBuiltIns.stringType.isSubtypeOf(type, irTypeSystemContext) + + private fun IrType?.isAssignableTo(type: IrType?): Boolean { + if (this != null && type != null) { + if (isSubtypeOf(type, irTypeSystemContext)) return true + val superTypes = (type.classifierOrNull as? IrTypeParameterSymbol)?.owner?.superTypes + return superTypes != null && superTypes.all { isSubtypeOf(it, irTypeSystemContext) } + } else { + return this == null && type == null + } + } + + private fun MessageCollector.info(expression: IrElement, message: String) { + report(expression, CompilerMessageSeverity.INFO, message) + } + + private fun MessageCollector.warn(expression: IrElement, message: String) { + report(expression, CompilerMessageSeverity.WARNING, message) + } + + private fun MessageCollector.report(expression: IrElement, severity: CompilerMessageSeverity, message: String) { + report(severity, message, sourceFile.getCompilerMessageLocation(expression)) + } +} + +val IrFunction.callableId: CallableId + get() { + val parentClass = parent as? IrClass + val classId = parentClass?.classId + return if (classId != null && !parentClass.isFileClass) { + CallableId(classId, name) + } else { + CallableId(parent.kotlinFqName, name) + } + } diff --git a/plugins/power-assert/power-assert.backend/src/com/bnorm/power/PowerAssertIrGenerationExtension.kt b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/PowerAssertIrGenerationExtension.kt new file mode 100644 index 00000000000..f194d2120d9 --- /dev/null +++ b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/PowerAssertIrGenerationExtension.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020 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 com.bnorm.power.diagram.SourceFile +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.ir.declarations.IrModuleFragment +import org.jetbrains.kotlin.name.FqName + +class PowerAssertIrGenerationExtension( + private val messageCollector: MessageCollector, + private val functions: Set, +) : IrGenerationExtension { + override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { + for (file in moduleFragment.files) { + PowerAssertCallTransformer(SourceFile(file), pluginContext, messageCollector, functions) + .visitFile(file) + } + } +} diff --git a/plugins/power-assert/power-assert.backend/src/com/bnorm/power/delegate/FunctionDelegate.kt b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/delegate/FunctionDelegate.kt new file mode 100644 index 00000000000..d9980d3a551 --- /dev/null +++ b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/delegate/FunctionDelegate.kt @@ -0,0 +1,62 @@ +/* + * 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 org.jetbrains.kotlin.ir.builders.IrBuilderWithScope +import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.builders.parent +import org.jetbrains.kotlin.ir.declarations.IrFunction +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.symbols.IrSimpleFunctionSymbol +import org.jetbrains.kotlin.ir.util.deepCopyWithSymbols + +interface FunctionDelegate { + val function: IrFunction + val messageParameter: IrValueParameter + + fun buildCall( + builder: IrBuilderWithScope, + original: IrCall, + dispatchReceiver: IrExpression?, + extensionReceiver: IrExpression?, + valueArguments: List, + messageArgument: IrExpression, + ): IrExpression + + fun IrBuilderWithScope.irCallCopy( + overload: IrSimpleFunctionSymbol, + original: IrCall, + dispatchReceiver: IrExpression?, + extensionReceiver: IrExpression?, + valueArguments: List, + messageArgument: IrExpression, + ): IrExpression { + return irCall(overload, type = original.type).apply { + this.dispatchReceiver = original.dispatchReceiver?.deepCopyWithSymbols(parent) + this.extensionReceiver = (extensionReceiver ?: original.extensionReceiver)?.deepCopyWithSymbols(parent) + for (i in 0 until original.typeArgumentsCount) { + putTypeArgument(i, original.getTypeArgument(i)) + } + for ((i, argument) in valueArguments.withIndex()) { + putValueArgument(i, argument?.deepCopyWithSymbols(parent)) + } + putValueArgument(valueArguments.size, messageArgument.deepCopyWithSymbols(parent)) + } + } +} diff --git a/plugins/power-assert/power-assert.backend/src/com/bnorm/power/delegate/LambdaFunctionDelegate.kt b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/delegate/LambdaFunctionDelegate.kt new file mode 100644 index 00000000000..d59f1bf2ff4 --- /dev/null +++ b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/delegate/LambdaFunctionDelegate.kt @@ -0,0 +1,53 @@ +/* + * 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.declarations.IrValueParameter +import org.jetbrains.kotlin.ir.expressions.IrCall +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol + +class LambdaFunctionDelegate( + private val overload: IrSimpleFunctionSymbol, + override val messageParameter: IrValueParameter, +) : FunctionDelegate { + override val function = overload.owner + + override fun buildCall( + builder: IrBuilderWithScope, + original: IrCall, + dispatchReceiver: IrExpression?, + extensionReceiver: IrExpression?, + valueArguments: List, + messageArgument: IrExpression, + ): IrExpression = with(builder) { + val expression = irLambda(context.irBuiltIns.stringType, messageParameter.type) { + +irReturn(messageArgument) + } + irCallCopy( + overload = overload, + original = original, + dispatchReceiver = dispatchReceiver, + extensionReceiver = extensionReceiver, + valueArguments = valueArguments, + messageArgument = expression, + ) + } +} diff --git a/plugins/power-assert/power-assert.backend/src/com/bnorm/power/delegate/SamConversionLambdaFunctionDelegate.kt b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/delegate/SamConversionLambdaFunctionDelegate.kt new file mode 100644 index 00000000000..d3ecc484cff --- /dev/null +++ b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/delegate/SamConversionLambdaFunctionDelegate.kt @@ -0,0 +1,55 @@ +/* + * 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.irSamConversion +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.symbols.IrSimpleFunctionSymbol + +class SamConversionLambdaFunctionDelegate( + private val overload: IrSimpleFunctionSymbol, + override val messageParameter: IrValueParameter, +) : FunctionDelegate { + override val function = overload.owner + + override fun buildCall( + builder: IrBuilderWithScope, + original: IrCall, + dispatchReceiver: IrExpression?, + extensionReceiver: IrExpression?, + valueArguments: List, + messageArgument: IrExpression, + ): IrExpression = with(builder) { + val lambda = irLambda(context.irBuiltIns.stringType, messageParameter.type) { + +irReturn(messageArgument) + } + val expression = irSamConversion(lambda, messageParameter.type) + irCallCopy( + overload = overload, + original = original, + dispatchReceiver = dispatchReceiver, + extensionReceiver = extensionReceiver, + valueArguments = valueArguments, + messageArgument = expression, + ) + } +} diff --git a/plugins/power-assert/power-assert.backend/src/com/bnorm/power/delegate/SimpleFunctionDelegate.kt b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/delegate/SimpleFunctionDelegate.kt new file mode 100644 index 00000000000..e78447696e9 --- /dev/null +++ b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/delegate/SimpleFunctionDelegate.kt @@ -0,0 +1,46 @@ +/* + * 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 org.jetbrains.kotlin.ir.builders.IrBuilderWithScope +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.symbols.IrSimpleFunctionSymbol + +class SimpleFunctionDelegate( + private val overload: IrSimpleFunctionSymbol, + override val messageParameter: IrValueParameter, +) : FunctionDelegate { + override val function = overload.owner + + override fun buildCall( + builder: IrBuilderWithScope, + original: IrCall, + dispatchReceiver: IrExpression?, + extensionReceiver: IrExpression?, + valueArguments: List, + messageArgument: IrExpression, + ): IrExpression = builder.irCallCopy( + overload = overload, + original = original, + dispatchReceiver = dispatchReceiver, + extensionReceiver = extensionReceiver, + valueArguments = valueArguments, + messageArgument = messageArgument, + ) +} diff --git a/plugins/power-assert/power-assert.backend/src/com/bnorm/power/diagram/DiagramBuilder.kt b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/diagram/DiagramBuilder.kt new file mode 100644 index 00000000000..875ef9d6d06 --- /dev/null +++ b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/diagram/DiagramBuilder.kt @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2020 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.diagram + +import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope +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 + +fun IrBuilderWithScope.buildDiagramNesting( + root: Node, + variables: List = emptyList(), + call: IrBuilderWithScope.(IrExpression, List) -> IrExpression, +): IrExpression { + return buildExpression(root, variables) { argument, subStack -> + call(argument, subStack) + } +} + +fun IrBuilderWithScope.buildDiagramNestingNullable( + root: Node?, + variables: List = emptyList(), + call: IrBuilderWithScope.(IrExpression?, List) -> IrExpression, +): IrExpression { + return if (root != null) buildDiagramNesting(root, variables, call) else call(null, variables) +} + +private fun IrBuilderWithScope.buildExpression( + node: Node, + variables: List, + call: IrBuilderWithScope.(IrExpression, List) -> IrExpression, +): 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") +} + +/** + * ``` + * val result = call(1 + 2 + 3) + * ``` + * Transforms to + * ``` + * val result = run { + * val tmp0 = 1 + 2 + * val tmp1 = tmp0 + 3 + * call(tmp1, ) + * } + * ``` + */ +private fun IrBuilderWithScope.add( + node: ExpressionNode, + variables: List, + call: IrBuilderWithScope.(IrExpression, List) -> IrExpression, +): IrExpression { + return irBlock { + val head = node.expressions.first().deepCopyWithSymbols(scope.getLocalDeclarationParent()) + val expressions = (buildTree(head) as ExpressionNode).expressions + val transformer = IrTemporaryExtractionTransformer(this@irBlock, expressions.toSet()) + val transformed = expressions.first().transform(transformer, null) + +call(transformed, variables + transformer.variables) + } +} + +/** + * ``` + * val result = call(1 == 1 && 2 == 2) + * ``` + * Transforms to + * ``` + * val result = run { + * val tmp0 = 1 == 1 + * if (tmp0) { + * val tmp1 = 2 == 2 + * call(tmp1, ) + * } + * else call(false, ) + * } + * ``` + */ +private fun IrBuilderWithScope.nest( + node: AndNode, + index: Int, + variables: List, + call: IrBuilderWithScope.(IrExpression, List) -> IrExpression, +): IrExpression { + val children = node.children + val child = children[index] + return buildExpression(child, variables) { argument, newVariables -> + if (index + 1 == children.size) { + call(argument, newVariables) // last expression, result is false + } else { + irIfThenElse( + context.irBuiltIns.anyType, + argument, + nest(node, index + 1, newVariables, call), // more expressions, continue nesting + call(irFalse(), newVariables), // short-circuit result to false + ) + } + } +} + +/** + * ``` + * val result = call(1 == 1 || 2 == 2) + * ``` + * Transforms to + * ``` + * val result = run { + * val tmp0 = 1 == 1 + * if (tmp0) call(true, ) + * else { + * val tmp1 = 2 == 2 + * call(tmp1, ) + * } + * } + * ``` + */ +private fun IrBuilderWithScope.nest( + node: OrNode, + index: Int, + variables: List, + call: IrBuilderWithScope.(IrExpression, List) -> IrExpression, +): IrExpression { + val children = node.children + val child = children[index] + return 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 + nest(node, index + 1, newVariables, call), // more expressions, continue nesting + ) + } + } +} diff --git a/plugins/power-assert/power-assert.backend/src/com/bnorm/power/diagram/ExpressionTree.kt b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/diagram/ExpressionTree.kt new file mode 100644 index 00000000000..f5e95357130 --- /dev/null +++ b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/diagram/ExpressionTree.kt @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2020 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.diagram + +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.expressions.IrCall +import org.jetbrains.kotlin.ir.expressions.IrConst +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.IrVararg +import org.jetbrains.kotlin.ir.expressions.IrWhen +import org.jetbrains.kotlin.ir.util.dumpKotlinLike +import org.jetbrains.kotlin.ir.visitors.IrElementVisitor + +abstract class Node { + private val _children = mutableListOf() + val children: List get() = _children + + fun addChild(node: Node) { + _children.add(node) + } + + fun dump(): String = buildString { + dump(this, 0) + } + + private fun dump(builder: StringBuilder, indent: Int) { + builder.append(" ".repeat(indent)).append(this).appendLine() + for (child in children) { + child.dump(builder, indent + 1) + } + } +} + +class AndNode : Node() { + override fun toString() = "AndNode" +} + +class OrNode : Node() { + override fun toString() = "OrNode" +} + +class ExpressionNode : Node() { + private val _expressions = mutableListOf() + val expressions: List = _expressions + + fun add(expression: IrExpression) { + _expressions.add(expression) + } + + override fun toString() = "ExpressionNode(${_expressions.map { it.dumpKotlinLike() }})" +} + +fun buildTree(expression: IrExpression): Node? { + class RootNode : Node() { + override fun toString() = "RootNode" + } + + val tree = RootNode() + expression.accept( + object : IrElementVisitor { + override fun visitElement(element: IrElement, data: Node) { + element.acceptChildren(this, data) + } + + override fun visitExpression(expression: IrExpression, data: Node) { + if (expression is IrFunctionExpression) return // Do not transform lambda expressions, especially their body + + val node = data as? ExpressionNode ?: ExpressionNode().also { data.addChild(it) } + node.add(expression) + expression.acceptChildren(this, node) + } + + override fun visitContainerExpression(expression: IrContainerExpression, data: Node) { + if (expression.origin is IrStatementOrigin.SAFE_CALL) { + // Null safe expressions can be correctly navigated + super.visitContainerExpression(expression, data) + } else { + // Everything else is considered unsafe and terminates the expression tree + val node = data as? ExpressionNode ?: ExpressionNode().also { data.addChild(it) } + node.add(expression) + } + } + + 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 + 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) + } + } + + override fun visitVararg(expression: IrVararg, data: Node) { + // Skip processing of vararg array + expression.acceptChildren(this, data) + } + + override fun visitConst(expression: IrConst<*>, data: Node) { + // Do not include constants + } + + override fun visitWhen(expression: IrWhen, data: Node) { + when (expression.origin) { + IrStatementOrigin.ANDAND -> { + // flatten `&&` expressions to be at the same level + val node = data as? AndNode ?: AndNode().also { data.addChild(it) } + + require(expression.branches.size == 2) + val thenBranch = expression.branches[0] + + thenBranch.condition.accept(this, node) + thenBranch.result.accept(this, node) + + val elseBranchCondition = expression.branches[1].condition + val elseBranchResult = expression.branches[1].result + + if (elseBranchCondition !is IrConst<*> || elseBranchCondition.value != true) { + elseBranchCondition.accept(this, node) + } + + if (elseBranchResult !is IrConst<*> || elseBranchResult.value != false) { + elseBranchResult.accept(this, node) + } + } + IrStatementOrigin.OROR -> { + // flatten `||` expressions to be at the same level + val node = data as? OrNode ?: OrNode().also { data.addChild(it) } + + require(expression.branches.size == 2) + val thenBranchCondition = expression.branches[0].condition + val thenBranchResult = expression.branches[0].result + val elseBranchCondition = expression.branches[1].condition + val elseBranchResult = expression.branches[1].result + + thenBranchCondition.accept(this, node) + + if (thenBranchResult !is IrConst<*> || thenBranchResult.value != true) { + thenBranchResult.accept(this, node) + } + + if (elseBranchCondition !is IrConst<*> || elseBranchCondition.value != true) { + elseBranchCondition.accept(this, node) + } + + if (elseBranchResult !is IrConst<*> || elseBranchResult.value != false) { + elseBranchResult.accept(this, node) + } + } + else -> { + // Add as basic expression and terminate + // TODO this has to be broken and not work in all cases... + ExpressionNode().also { data.addChild(it) } + } + } + } + }, + tree, + ) + + return tree.children.singleOrNull() +} diff --git a/plugins/power-assert/power-assert.backend/src/com/bnorm/power/diagram/IrDiagram.kt b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/diagram/IrDiagram.kt new file mode 100644 index 00000000000..96c80ab28c2 --- /dev/null +++ b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/diagram/IrDiagram.kt @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2020 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.diagram + +import com.bnorm.power.irString +import org.jetbrains.kotlin.ir.IrBuiltIns +import org.jetbrains.kotlin.ir.SourceRangeInfo +import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope +import org.jetbrains.kotlin.ir.builders.irConcat +import org.jetbrains.kotlin.ir.builders.irGet +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.declarations.IrVariable +import org.jetbrains.kotlin.ir.expressions.IrCall +import org.jetbrains.kotlin.ir.expressions.IrConst +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 +import org.jetbrains.kotlin.ir.expressions.addArgument + +fun IrBuilderWithScope.irDiagramString( + sourceFile: SourceFile, + prefix: IrExpression? = null, + call: IrCall, + variables: List, +): IrExpression { + val callInfo = sourceFile.getSourceRangeInfo(call) + val callIndent = callInfo.startColumnNumber + + val stackValues = variables.map { it.toValueDisplay(sourceFile, callIndent, callInfo) } + + val valuesByRow = stackValues.groupBy { it.row } + val rows = sourceFile.getText(callInfo) + .replace("\n" + " ".repeat(callIndent), "\n") // Remove additional indentation + .split("\n") + + return irConcat().apply { + if (prefix != null) addArgument(prefix) + + for ((row, rowSource) in rows.withIndex()) { + val rowValues = valuesByRow[row]?.let { values -> values.sortedBy { it.indent } } ?: emptyList() + val indentations = rowValues.map { it.indent } + + addArgument( + irString { + if (row != 0 || prefix != null) appendLine() + append(rowSource) + if (indentations.isNotEmpty()) { + appendLine() + var last = -1 + for (i in indentations) { + if (i > last) indent(i - last - 1).append("|") + last = i + } + } + }, + ) + + for (tmp in rowValues.asReversed()) { + addArgument( + irString { + appendLine() + var last = -1 + for (i in indentations) { + if (i == tmp.indent) break + if (i > last) indent(i - last - 1).append("|") + last = i + } + indent(tmp.indent - last - 1) + }, + ) + addArgument(irGet(tmp.value)) + } + } + } +} + +private data class ValueDisplay( + val value: IrVariable, + val indent: Int, + val row: Int, + val source: String, +) + +private fun IrTemporaryVariable.toValueDisplay( + fileSource: SourceFile, + callIndent: Int, + originalInfo: SourceRangeInfo, +): ValueDisplay { + val info = fileSource.getSourceRangeInfo(original) + var indent = info.startColumnNumber - callIndent + var row = info.startLineNumber - originalInfo.startLineNumber + + val source = fileSource.getText(info) + .replace("\n" + " ".repeat(callIndent), "\n") // Remove additional indentation + val columnOffset = findDisplayOffset(fileSource, original, source) + + val prefix = source.substring(0, columnOffset) + val rowShift = prefix.count { it == '\n' } + if (rowShift == 0) { + indent += columnOffset + } else { + row += rowShift + indent = columnOffset - (prefix.lastIndexOf('\n') + 1) + } + + return ValueDisplay(temporary, indent, row, source) +} + +/** + * Responsible for determining the diagram display offset of the expression + * beginning from the startOffset of the expression. + * + * Equality: + * ``` + * number == 42 + * | <- startOffset + * | <- display offset: 7 + * ``` + * + * Arithmetic: + * ``` + * i + 2 + * | <- startOffset + * | <- display offset: 2 + * ``` + * + * Infix: + * ``` + * 1 shl 2 + * | <- startOffset + * | <- display offset: 2 + * ``` + * + * Standard: + * ``` + * 1.shl(2) + * | <- startOffset + * | <- display offset: 0 + * ``` + */ +private fun findDisplayOffset( + sourceFile: SourceFile, + expression: IrExpression, + source: String, +): Int { + return when (expression) { + is IrMemberAccessExpression<*> -> memberAccessOffset(sourceFile, expression, source) + is IrTypeOperatorCall -> typeOperatorOffset(expression, source) + else -> 0 + } +} + +private fun memberAccessOffset( + sourceFile: SourceFile, + expression: IrMemberAccessExpression<*>, + source: String, +): Int { + when (expression.origin) { + // special case to handle `value != null` + 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 + if (owner !is IrSimpleFunction) return 0 + + if (owner.isInfix || owner.isOperator || owner.origin == IrBuiltIns.BUILTIN_OPERATOR) { + // Ignore single value operators + val singleReceiver = (expression.dispatchReceiver != null) xor (expression.extensionReceiver != null) + if (singleReceiver && expression.valueArgumentsCount == 0) return 0 + + // Start after the dispatcher or first argument + val receiver = expression.dispatchReceiver + ?: expression.extensionReceiver + ?: expression.getValueArgument(0).takeIf { owner.origin == IrBuiltIns.BUILTIN_OPERATOR } + ?: return 0 + 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] == '.') { + offset++ + if (offset >= source.length) return 0 + } + return offset + } + + 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 StringBuilder.indent(indentation: Int): StringBuilder { + repeat(indentation) { append(" ") } + return this +} diff --git a/plugins/power-assert/power-assert.backend/src/com/bnorm/power/diagram/IrTemporaryVariable.kt b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/diagram/IrTemporaryVariable.kt new file mode 100644 index 00000000000..710195c9f2a --- /dev/null +++ b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/diagram/IrTemporaryVariable.kt @@ -0,0 +1,49 @@ +/* + * 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.diagram + +import org.jetbrains.kotlin.ir.builders.IrStatementsBuilder +import org.jetbrains.kotlin.ir.builders.irGet +import org.jetbrains.kotlin.ir.builders.irTemporary +import org.jetbrains.kotlin.ir.declarations.IrVariable +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.util.deepCopyWithSymbols +import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid + +data class IrTemporaryVariable( + val temporary: IrVariable, + val original: IrExpression, +) + +class IrTemporaryExtractionTransformer( + private val builder: IrStatementsBuilder<*>, + private val transform: Set, +) : IrElementTransformerVoid() { + private val _variables = mutableListOf() + val variables: List = _variables + + override fun visitExpression(expression: IrExpression): IrExpression { + return if (expression in transform) { + val copy = expression.deepCopyWithSymbols(builder.scope.getLocalDeclarationParent()) + val variable = builder.irTemporary(super.visitExpression(expression)) + _variables.add(IrTemporaryVariable(variable, copy)) + builder.irGet(variable) + } else { + super.visitExpression(expression) + } + } +} diff --git a/plugins/power-assert/power-assert.backend/src/com/bnorm/power/diagram/SourceFile.kt b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/diagram/SourceFile.kt new file mode 100644 index 00000000000..45387004d66 --- /dev/null +++ b/plugins/power-assert/power-assert.backend/src/com/bnorm/power/diagram/SourceFile.kt @@ -0,0 +1,51 @@ +package com.bnorm.power.diagram + +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.SourceRangeInfo +import org.jetbrains.kotlin.ir.declarations.IrFile +import org.jetbrains.kotlin.ir.declarations.path +import org.jetbrains.kotlin.ir.expressions.IrCall +import java.io.File + +data class SourceFile( + private val irFile: IrFile, +) { + private val source: String = File(irFile.path).readText() + .replace("\r\n", "\n") // https://youtrack.jetbrains.com/issue/KT-41888 + + fun getSourceRangeInfo(element: IrElement): SourceRangeInfo { + var range = element.startOffset..element.endOffset + when (element) { + is IrCall -> { + val receiver = element.extensionReceiver ?: element.dispatchReceiver + if (element.symbol.owner.isInfix && receiver != null) { + // When an infix function is called *not* with infix notation, the startOffset will not include the receiver. + // Force the range to include the receiver, so it is always present + range = receiver.startOffset..element.endOffset + + // The offsets of the receiver will *not* include surrounding parentheses so these need to be checked for + // manually. + val substring = safeSubstring(receiver.startOffset - 1, receiver.endOffset + 1) + if (substring.startsWith('(') && substring.endsWith(')')) { + range = receiver.startOffset - 1..element.endOffset + } + } + } + } + return irFile.fileEntry.getSourceRangeInfo(range.first, range.last) + } + + fun getText(info: SourceRangeInfo): String { + return safeSubstring(info.startOffset, info.endOffset) + } + + private fun safeSubstring(start: Int, end: Int): String = + source.substring(maxOf(start, 0), minOf(end, source.length)) + + fun getCompilerMessageLocation(element: IrElement): CompilerMessageLocation { + val info = getSourceRangeInfo(element) + val lineContent = getText(info) + return CompilerMessageLocation.create(irFile.path, info.startLineNumber, info.startColumnNumber, lineContent)!! + } +} diff --git a/plugins/power-assert/power-assert.cli/src/com/bnorm/power/PowerAssertCommandLineProcessor.kt b/plugins/power-assert/power-assert.cli/src/com/bnorm/power/PowerAssertCommandLineProcessor.kt new file mode 100644 index 00000000000..599d69a9111 --- /dev/null +++ b/plugins/power-assert/power-assert.cli/src/com/bnorm/power/PowerAssertCommandLineProcessor.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2020 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 com.google.auto.service.AutoService +import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption +import org.jetbrains.kotlin.compiler.plugin.CliOption +import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor +import org.jetbrains.kotlin.config.CompilerConfiguration + +@AutoService(CommandLineProcessor::class) +class PowerAssertCommandLineProcessor : CommandLineProcessor { + override val pluginId: String = "com.bnorm.kotlin-power-assert" + + override val pluginOptions: Collection = listOf( + CliOption( + optionName = "function", + valueDescription = "function full-qualified name", + description = "fully qualified path of function to intercept", + required = false, // TODO required for Kotlin/JS + allowMultipleOccurrences = true, + ), + ) + + override fun processOption( + option: AbstractCliOption, + value: String, + configuration: CompilerConfiguration, + ) { + return when (option.optionName) { + "function" -> configuration.add(KEY_FUNCTIONS, value) + else -> error("Unexpected config option ${option.optionName}") + } + } +} diff --git a/plugins/power-assert/power-assert.cli/src/com/bnorm/power/PowerAssertCompilerPluginRegistrar.kt b/plugins/power-assert/power-assert.cli/src/com/bnorm/power/PowerAssertCompilerPluginRegistrar.kt new file mode 100644 index 00000000000..50148950958 --- /dev/null +++ b/plugins/power-assert/power-assert.cli/src/com/bnorm/power/PowerAssertCompilerPluginRegistrar.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2020 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 com.google.auto.service.AutoService +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.config.CompilerConfigurationKey +import org.jetbrains.kotlin.name.FqName + +val KEY_FUNCTIONS = CompilerConfigurationKey>("fully-qualified function names") + +@AutoService(CompilerPluginRegistrar::class) +class PowerAssertCompilerPluginRegistrar( + private val functions: Set, +) : CompilerPluginRegistrar() { + @Suppress("unused") + constructor() : this(emptySet()) // Used by service loader + + override val supportsK2: Boolean = true + + override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { + val functions = configuration[KEY_FUNCTIONS]?.map { FqName(it) } ?: functions + if (functions.isEmpty()) return + + val messageCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE) + IrGenerationExtension.registerExtension(PowerAssertIrGenerationExtension(messageCollector, functions.toSet())) + } +} diff --git a/plugins/power-assert/test/com/bnorm/power/ArithmeticExpressionTest.kt b/plugins/power-assert/test/com/bnorm/power/ArithmeticExpressionTest.kt new file mode 100644 index 00000000000..1ec3a311f20 --- /dev/null +++ b/plugins/power-assert/test/com/bnorm/power/ArithmeticExpressionTest.kt @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2020 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 ArithmeticExpressionTest { + @Test + fun `assertion with inline addition`() { + val actual = executeMainAssertion("assert(1 + 1 == 4)") + assertEquals( + """ + Assertion failed + assert(1 + 1 == 4) + | | + | false + 2 + """.trimIndent(), + actual, + ) + } + + @Test + fun `assertion with inline subtraction`() { + val actual = executeMainAssertion("assert(3 - 1 == 4)") + assertEquals( + """ + Assertion failed + assert(3 - 1 == 4) + | | + | false + 2 + """.trimIndent(), + actual, + ) + } + + @Test + fun `assertion with inline multiplication`() { + val actual = executeMainAssertion("assert(1 * 2 == 4)") + assertEquals( + """ + Assertion failed + assert(1 * 2 == 4) + | | + | false + 2 + """.trimIndent(), + actual, + ) + } + + @Test + fun `assertion with inline division`() { + val actual = executeMainAssertion("assert(2 / 1 == 4)") + assertEquals( + """ + Assertion failed + assert(2 / 1 == 4) + | | + | false + 2 + """.trimIndent(), + actual, + ) + } + + @Test + fun `assertion with inline prefix increment`() { + val actual = executeMainAssertion( + """ + var i = 1 + assert(++i == 4) + """.trimIndent(), + ) + assertEquals( + """ + Assertion failed + assert(++i == 4) + | | + | false + 2 + """.trimIndent(), + actual, + ) + } + + @Test + fun `assertion with inline postfix increment`() { + val actual = executeMainAssertion( + """ + var i = 1 + assert(i++ == 4) + """.trimIndent(), + ) + assertEquals( + """ + Assertion failed + assert(i++ == 4) + | | + | false + 1 + """.trimIndent(), + actual, + ) + } + + @Test + fun `assertion with inline prefix decrement`() { + val actual = executeMainAssertion( + """ + var i = 3 + assert(--i == 4) + """.trimIndent(), + ) + assertEquals( + """ + Assertion failed + assert(--i == 4) + | | + | false + 2 + """.trimIndent(), + actual, + ) + } + + @Test + fun `assertion with inline postfix decrement`() { + val actual = executeMainAssertion( + """ + var i = 3 + assert(i-- == 4) + """.trimIndent(), + ) + assertEquals( + """ + Assertion failed + assert(i-- == 4) + | | + | false + 3 + """.trimIndent(), + actual, + ) + } +} diff --git a/plugins/power-assert/test/com/bnorm/power/CastExpressionTest.kt b/plugins/power-assert/test/com/bnorm/power/CastExpressionTest.kt new file mode 100644 index 00000000000..546adbc739a --- /dev/null +++ b/plugins/power-assert/test/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/plugins/power-assert/test/com/bnorm/power/Compiler.kt b/plugins/power-assert/test/com/bnorm/power/Compiler.kt new file mode 100644 index 00000000000..126d67ead98 --- /dev/null +++ b/plugins/power-assert/test/com/bnorm/power/Compiler.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2020 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 com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile +import org.intellij.lang.annotations.Language +import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +import org.jetbrains.kotlin.name.FqName +import java.io.OutputStream +import java.lang.reflect.InvocationTargetException +import kotlin.test.assertEquals +import kotlin.test.fail + +private val DEFAULT_COMPILER_PLUGIN_REGISTRARS = arrayOf( + PowerAssertCompilerPluginRegistrar(setOf(FqName("kotlin.assert"))), +) + +fun compile( + list: List, + vararg compilerPluginRegistrars: CompilerPluginRegistrar = DEFAULT_COMPILER_PLUGIN_REGISTRARS, +): KotlinCompilation.Result { + return KotlinCompilation().apply { + sources = list + 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 + } + } + this.compilerPluginRegistrars = compilerPluginRegistrars.toList() + inheritClassPath = true + }.compile() +} + +fun executeAssertion( + @Language("kotlin") source: String, + vararg compilerPluginRegistrars: CompilerPluginRegistrar = DEFAULT_COMPILER_PLUGIN_REGISTRARS, +): String { + val result = compile( + listOf(SourceFile.kotlin("main.kt", source, trimIndent = false)), + *compilerPluginRegistrars, + ) + assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode, result.messages) + + val kClazz = result.classLoader.loadClass("MainKt") + val main = kClazz.declaredMethods.single { it.name == "main" && it.parameterCount == 0 } + try { + try { + main.invoke(null) + } catch (t: InvocationTargetException) { + throw t.cause!! + } + fail("should have thrown assertion") + } catch (t: Throwable) { + return t.message ?: "" + } +} + +fun executeMainAssertion(mainBody: String) = executeAssertion( + """ +fun main() { + $mainBody +} +""", +) + +fun assertMessage( + @Language("kotlin") source: String, + message: String, + vararg compilerPluginRegistrars: CompilerPluginRegistrar = DEFAULT_COMPILER_PLUGIN_REGISTRARS, +) { + val actual = executeAssertion(source, *compilerPluginRegistrars) + assertEquals(message, actual) +} diff --git a/plugins/power-assert/test/com/bnorm/power/DebugFunctionTest.kt b/plugins/power-assert/test/com/bnorm/power/DebugFunctionTest.kt new file mode 100644 index 00000000000..cc4ca96b46e --- /dev/null +++ b/plugins/power-assert/test/com/bnorm/power/DebugFunctionTest.kt @@ -0,0 +1,105 @@ +/* + * 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 com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile +import org.jetbrains.kotlin.name.FqName +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +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(), + ) + } +} + +private 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 { + throw RuntimeException("result:"+msg) + return value +} + +fun main() { + $mainBody +} +""", + trimIndent = false, + ) + + val result = compile(listOf(file), PowerAssertCompilerPluginRegistrar(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 } + return getMainResult(main) +} + +fun getMainResult(main: Method): String { + try { + main.invoke(null) + fail("main did not throw expected exception") + } catch (t: InvocationTargetException) { + with(t.cause) { + if (this is RuntimeException && message != null && message!!.startsWith("result:")) { + return message!!.substringAfter("result:") + } + } + throw t.cause!! + } +} diff --git a/plugins/power-assert/test/com/bnorm/power/InfixFunctionTest.kt b/plugins/power-assert/test/com/bnorm/power/InfixFunctionTest.kt new file mode 100644 index 00000000000..26368fb8511 --- /dev/null +++ b/plugins/power-assert/test/com/bnorm/power/InfixFunctionTest.kt @@ -0,0 +1,369 @@ +/* + * 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 com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile +import org.jetbrains.kotlin.name.FqName +import java.lang.reflect.InvocationTargetException +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +class InfixFunctionTest { + @Test + fun `extension infix function call includes receiver`() { + val actual = runExtensionInfix( + """ + (1 + 1) mustEqual (2 + 4) + """.trimIndent(), + ) + assertEquals( + """ + (1 + 1) mustEqual (2 + 4) + | | + | 6 + 2 + """.trimIndent(), + actual.trim(), + ) + } + + @Test + fun `extension infix function call with constant receiver`() { + val actual = runExtensionInfix( + """ + 1 mustEqual (2 + 4) + """.trimIndent(), + ) + assertEquals( + """ + 1 mustEqual (2 + 4) + | + 6 + """.trimIndent(), + actual.trim(), + ) + } + + @Test + fun `extension infix function call with constant parameter`() { + val actual = runExtensionInfix( + """ + (1 + 1) mustEqual 6 + """.trimIndent(), + ) + assertEquals( + """ + (1 + 1) mustEqual 6 + | + 2 + """.trimIndent(), + actual.trim(), + ) + } + + @Test + fun `extension infix function call with only constants`() { + val actual = runExtensionInfix( + """ + 2 mustEqual 6 + """.trimIndent(), + ) + assertEquals( + """ + Assertion failed + """.trimIndent(), + actual.trim(), + ) + } + + @Test + fun `extension non-infix function call includes receiver`() { + val actual = runExtensionInfix( + """ + (1 + 1).mustEqual(2 + 4) + """.trimIndent(), + ) + assertEquals( + """ + (1 + 1).mustEqual(2 + 4) + | | + | 6 + 2 + """.trimIndent(), + actual.trim(), + ) + } + + @Test + fun `extension non-infix function call with constant receiver`() { + val actual = runExtensionInfix( + """ + 1.mustEqual(2 + 4) + """.trimIndent(), + ) + assertEquals( + """ + 1.mustEqual(2 + 4) + | + 6 + """.trimIndent(), + actual.trim(), + ) + } + + @Test + fun `extension non-infix function call with constant parameter`() { + val actual = runExtensionInfix( + """ + (1 + 1).mustEqual(6) + """.trimIndent(), + ) + assertEquals( + """ + (1 + 1).mustEqual(6) + | + 2 + """.trimIndent(), + actual.trim(), + ) + } + + @Test + fun `extension non-infix function call with only constants`() { + val actual = runExtensionInfix( + """ + 2.mustEqual(6) + """.trimIndent(), + ) + assertEquals( + """ + Assertion failed + """.trimIndent(), + actual.trim(), + ) + } + + @Test + fun `dispatch infix function call includes receiver`() { + val actual = runDispatchInfix( + """ + Wrapper(1 + 1) mustEqual (2 + 4) + """.trimIndent(), + ) + assertEquals( + """ + Wrapper(1 + 1) mustEqual (2 + 4) + | | | + | | 6 + | 2 + Wrapper + """.trimIndent(), + actual.trim(), + ) + } + + @Test + fun `dispatch infix function call with constant receiver`() { + val actual = runDispatchInfix( + """ + Wrapper(1) mustEqual (2 + 4) + """.trimIndent(), + ) + assertEquals( + """ + Wrapper(1) mustEqual (2 + 4) + | | + | 6 + Wrapper + """.trimIndent(), + actual.trim(), + ) + } + + @Test + fun `dispatch infix function call with constant parameter`() { + val actual = runDispatchInfix( + """ + Wrapper(1 + 1) mustEqual 6 + """.trimIndent(), + ) + assertEquals( + """ + Wrapper(1 + 1) mustEqual 6 + | | + | 2 + Wrapper + """.trimIndent(), + actual.trim(), + ) + } + + @Test + fun `dispatch infix function call with only constants`() { + val actual = runDispatchInfix( + """ + Wrapper(2) mustEqual 6 + """.trimIndent(), + ) + assertEquals( + """ + Wrapper(2) mustEqual 6 + | + Wrapper + """.trimIndent(), + actual.trim(), + ) + } + + @Test + fun `dispatch non-infix function call includes receiver`() { + val actual = runDispatchInfix( + """ + Wrapper(1 + 1).mustEqual(2 + 4) + """.trimIndent(), + ) + assertEquals( + """ + Wrapper(1 + 1).mustEqual(2 + 4) + | | | + | | 6 + | 2 + Wrapper + """.trimIndent(), + actual.trim(), + ) + } + + @Test + fun `dispatch non-infix function call with constant receiver`() { + val actual = runDispatchInfix( + """ + Wrapper(1).mustEqual(2 + 4) + """.trimIndent(), + ) + assertEquals( + """ + Wrapper(1).mustEqual(2 + 4) + | | + | 6 + Wrapper + """.trimIndent(), + actual.trim(), + ) + } + + @Test + fun `dispatch non-infix function call with constant parameter`() { + val actual = runDispatchInfix( + """ + Wrapper(1 + 1).mustEqual(6) + """.trimIndent(), + ) + assertEquals( + """ + Wrapper(1 + 1).mustEqual(6) + | | + | 2 + Wrapper + """.trimIndent(), + actual.trim(), + ) + } + + @Test + fun `dispatch non-infix function call with only constants`() { + val actual = runDispatchInfix( + """ + Wrapper(2).mustEqual(6) + """.trimIndent(), + ) + assertEquals( + """ + Wrapper(2).mustEqual(6) + | + Wrapper + """.trimIndent(), + actual.trim(), + ) + } + + private fun runExtensionInfix(mainBody: String): String { + return run( + SourceFile.kotlin( + name = "main.kt", + contents = """ + infix fun V.mustEqual(expected: V): Unit = assert(this == expected) + + fun V.mustEqual(expected: V, message: () -> String): Unit = + assert(this == expected, message) + + fun main() { + $mainBody + } + """.trimIndent(), + trimIndent = false, + ), + setOf(FqName("mustEqual")), + ) + } + + private fun runDispatchInfix(mainBody: String): String { + return run( + SourceFile.kotlin( + name = "main.kt", + contents = """ + class Wrapper( + private val value: V + ) { + infix fun mustEqual(expected: V): Unit = assert(value == expected) + + fun mustEqual(expected: V, message: () -> String): Unit = + assert(value == expected, message) + + override fun toString() = "Wrapper" + } + + fun main() { + $mainBody + } + """.trimIndent(), + trimIndent = false, + ), + setOf(FqName("Wrapper.mustEqual")), + ) + } + + private fun run(file: SourceFile, fqNames: Set, main: String = "MainKt"): String { + val result = compile(listOf(file), PowerAssertCompilerPluginRegistrar(fqNames)) + assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode, "Failed with messages: " + result.messages) + + val kClazz = result.classLoader.loadClass(main) + val mainMethod = kClazz.declaredMethods.single { it.name == "main" && it.parameterCount == 0 } + try { + try { + mainMethod.invoke(null) + } catch (t: InvocationTargetException) { + throw t.cause!! + } + fail("should have thrown assertion") + } catch (t: Throwable) { + return t.message ?: "" + } + } +} diff --git a/plugins/power-assert/test/com/bnorm/power/ObjectLiteralTest.kt b/plugins/power-assert/test/com/bnorm/power/ObjectLiteralTest.kt new file mode 100644 index 00000000000..c7d04fbdae7 --- /dev/null +++ b/plugins/power-assert/test/com/bnorm/power/ObjectLiteralTest.kt @@ -0,0 +1,39 @@ +/* + * 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 ObjectLiteralTest { + @Test + fun `internals of object literal should not be separated`() { + assertMessage( + """ + fun main() { + assert(object { override fun toString() = "ANONYMOUS" }.toString() == "toString()") + }""", + """ + Assertion failed + assert(object { override fun toString() = "ANONYMOUS" }.toString() == "toString()") + | | | + | | false + | ANONYMOUS + ANONYMOUS + """.trimIndent(), + ) + } +} diff --git a/plugins/power-assert/test/com/bnorm/power/RegexMatchTest.kt b/plugins/power-assert/test/com/bnorm/power/RegexMatchTest.kt new file mode 100644 index 00000000000..fd806cfaf90 --- /dev/null +++ b/plugins/power-assert/test/com/bnorm/power/RegexMatchTest.kt @@ -0,0 +1,52 @@ +/* + * 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 kotlin.test.assertEquals + +class RegexMatchTest { + @Test + fun `regex matches`() { + val actual = executeMainAssertion("""assert("Hello, World".matches("[A-Za-z]+".toRegex()))""") + assertEquals( + """ + Assertion failed + assert("Hello, World".matches("[A-Za-z]+".toRegex())) + | | + | [A-Za-z]+ + false + """.trimIndent(), + actual, + ) + } + + @Test + fun `infix regex matches`() { + val actual = executeMainAssertion("""assert("Hello, World" matches "[A-Za-z]+".toRegex())""") + assertEquals( + """ + Assertion failed + assert("Hello, World" matches "[A-Za-z]+".toRegex()) + | | + | [A-Za-z]+ + false + """.trimIndent(), + actual, + ) + } +} diff --git a/plugins/power-assert/test/com/bnorm/power/VarargTest.kt b/plugins/power-assert/test/com/bnorm/power/VarargTest.kt new file mode 100644 index 00000000000..77062d55942 --- /dev/null +++ b/plugins/power-assert/test/com/bnorm/power/VarargTest.kt @@ -0,0 +1,43 @@ +/* + * 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 VarargTest { + @Test + fun `implicit array of vararg parameters is excluded from diagram`() { + assertMessage( + """ + fun main() { + var i = 0 + assert(listOf("a", "b", "c") == listOf(i++, i++, i++)) + }""", + """ + Assertion failed + assert(listOf("a", "b", "c") == listOf(i++, i++, i++)) + | | | | | | + | | | | | 2 + | | | | 1 + | | | 0 + | | [0, 1, 2] + | false + [a, b, c] + """.trimIndent(), + ) + } +}