fixup! Add PowerAssert annotations and support in compiler-plugin
* Add metadata to transformed functions and containing classes
* Support function overrides and super calls
* Support recursive calls
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallFunctionFactory.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallFunctionFactory.kt
index f83de34..5a05f12 100644
--- a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallFunctionFactory.kt
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallFunctionFactory.kt
@@ -5,70 +5,77 @@
package org.jetbrains.kotlin.powerassert
-import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.ir.builders.declarations.addValueParameter
-import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
+import org.jetbrains.kotlin.ir.declarations.IrClass
+import org.jetbrains.kotlin.ir.declarations.IrDeclaration
+import org.jetbrains.kotlin.ir.declarations.IrMetadataSourceOwner
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
-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.impl.IrConstImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl
-import org.jetbrains.kotlin.ir.expressions.impl.IrGetValueImpl
import org.jetbrains.kotlin.ir.expressions.impl.fromSymbolOwner
import org.jetbrains.kotlin.ir.irAttribute
import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
-import org.jetbrains.kotlin.ir.types.classFqName
-import org.jetbrains.kotlin.ir.types.makeNullable
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
import org.jetbrains.kotlin.name.JvmStandardClassIds
import org.jetbrains.kotlin.name.Name
+var IrSimpleFunction.explainedDispatchSymbol: IrSimpleFunctionSymbol? by irAttribute(followAttributeOwner = false)
+
class ExplainCallFunctionFactory(
- private val module: IrModuleFragment,
private val context: IrPluginContext,
private val builtIns: PowerAssertBuiltIns,
) {
- private var IrSimpleFunction.explainedDispatchFunctionSymbol: IrSimpleFunctionSymbol? by irAttribute(followAttributeOwner = false)
+ private val memorizedClassMetadata = mutableSetOf<IrClass>()
fun find(function: IrSimpleFunction): IrSimpleFunctionSymbol? {
- function.explainedDispatchFunctionSymbol?.let { return it }
+ // If there is an explained symbol:
+ // 1. Function was transformed to generate an explained overload.
+ // 2. Explained overload was already found and saved for faster lookup.
+ function.explainedDispatchSymbol?.let { return it }
- // TODO this never works because... the generated function has no metadata? need to create an FIR function as well?
- val callableId = function.callableId.copy(Name.identifier(function.name.identifier + "\$explained"))
- context.referenceFunctions(callableId).singleOrNull { it.isSyntheticFor(function) }?.let { return it }
-
- // TODO is this the best way to handle compilation unit checks?
-// if (function.fileOrNull !in module.files) return null
+ // Metadata indicates the function was transformed but is not in the current compilation unit.
+ // Generate a stub-function so a symbol exists which can be called.
+ val parentClass = function.parent as? IrClass
+ getPowerAssertMetadata(parentClass ?: function) ?: return null
return generate(function).symbol
}
- fun generate(function: IrSimpleFunction): IrSimpleFunction {
- val newFunction = function.deepCopyWithSymbols(function.parent).apply {
+ fun generate(originalFunction: IrSimpleFunction): IrSimpleFunction {
+ originalFunction.explainedDispatchSymbol?.let { return it.owner }
+
+ val explainedFunction = originalFunction.deepCopyWithSymbols(originalFunction.parent)
+ explainedFunction.apply {
origin = FUNCTION_FOR_EXPLAIN_CALL
- name = Name.identifier(function.name.identifier + "\$explained")
- annotations = annotations.filter { !it.isAnnotationWithEqualFqName(builtIns.explainCallType.classFqName!!) } +
- createJvmSyntheticAnnotation()
- addValueParameter {
+ name = Name.identifier("${originalFunction.name.identifier}\$explained")
+ annotations = annotations.filter { !it.isAnnotation(PowerAssertBuiltIns.explainCallFqName) }
+ annotations += createJvmSyntheticAnnotation() // TODO is this needed?
+ val explanationParameter = addValueParameter {
name = Name.identifier("\$explanation") // TODO what if there's another property with this name?
type = builtIns.callExplanationType
}
+
+ overriddenSymbols = originalFunction.overriddenSymbols.map { generate(it.owner).symbol }
+
+ // Transform the generated function to use the `$explanation` parameter instead of CallExplain.explanation.
+ transformChildrenVoid(ExplainCallGetExplanationTransformer(builtIns, explanationParameter))
+ // Transform the generated function to propagate the `$explanation` parameter during recursive or super-calls.
+ transformChildrenVoid(ExplainedSelfCallTransformer(originalFunction, explanationParameter))
+
}
- // Transform the generated function to use the `$explanation` parameter instead of Explain.explanation.
- val diagramParameter = newFunction.valueParameters.last()
- newFunction.transformChildrenVoid(DiagramDispatchTransformer(diagramParameter, context))
- function.explainedDispatchFunctionSymbol = newFunction.symbol
+ // Save the explained function to the original function to make overload lookup easier.
+ originalFunction.explainedDispatchSymbol = explainedFunction.symbol
- // Transform the original function to use `null` instead of Explain.explanation.
- // This keeps the code from throwing an error when Explain.explanation.
- // This in turn helps make sure the compiler-plugin is applied to functions which use `@Explain`.
- // TODO should this be in the transformer and not here?
- function.transformChildrenVoid(DiagramDispatchTransformer(explanation = null, context))
+ // Write custom metadata to indicate the original function was indeed compiled with the plugin.
+ // This allows callers to be confident the explained function exists, even when calling from a different compilation unit.
+ val parentClass = originalFunction.parent as? IrClass
+ when {
+ parentClass == null -> addPowerAssertMetadata(originalFunction)
+ memorizedClassMetadata.add(parentClass) -> addPowerAssertMetadata(parentClass)
+ }
- return newFunction
+ return explainedFunction
}
private fun createJvmSyntheticAnnotation(): IrConstructorCallImpl {
@@ -81,36 +88,17 @@
)
}
- private class DiagramDispatchTransformer(
- private val explanation: IrValueParameter?,
- private val context: IrPluginContext,
- ) : IrElementTransformerVoidWithContext() {
- override fun visitExpression(expression: IrExpression): IrExpression {
- return when {
- isExplanation(expression) -> when (explanation) {
- null -> IrConstImpl.constNull(expression.startOffset, expression.endOffset, context.irBuiltIns.anyType.makeNullable())
- else -> IrGetValueImpl(expression.startOffset, expression.endOffset, explanation.type, explanation.symbol)
- }
- else -> super.visitExpression(expression)
- }
+ private fun <E> addPowerAssertMetadata(declaration: E)
+ where E : IrDeclaration, E : IrMetadataSourceOwner {
+ if (declaration.metadata != null) {
+ context.metadataDeclarationRegistrar
+ .addCustomMetadataExtension(declaration, PowerAssertBuiltIns.PLUGIN_ID, builtIns.metadata.data)
}
-
- private fun isExplanation(expression: IrExpression): Boolean =
- (expression as? IrCall)?.symbol?.owner?.kotlinFqName == PowerAssertGetDiagram
}
- private fun IrSimpleFunctionSymbol.isSyntheticFor(function: IrSimpleFunction): Boolean {
- // TODO need to consider type parameters and how they differ.
-
- val owner = owner
- if (function.dispatchReceiverParameter?.type != owner.dispatchReceiverParameter?.type) return false
- if (function.extensionReceiverParameter?.type != owner.extensionReceiverParameter?.type) return false
-
- if (function.valueParameters.size != owner.valueParameters.size - 1) return false
- for (index in function.valueParameters.indices) {
- if (function.valueParameters[index].type != owner.valueParameters[index].type) return false
- }
-
- return owner.valueParameters.last().type == builtIns.callExplanationType
+ private fun <E> getPowerAssertMetadata(declaration: E): ByteArray?
+ where E : IrDeclaration, E : IrMetadataSourceOwner {
+ return context.metadataDeclarationRegistrar.getCustomMetadataExtension(declaration, PowerAssertBuiltIns.PLUGIN_ID)
}
}
+
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallFunctionTransformer.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallFunctionTransformer.kt
new file mode 100644
index 0000000..610fe45
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallFunctionTransformer.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2010-2024 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.
+ */
+
+package org.jetbrains.kotlin.powerassert
+
+import org.jetbrains.kotlin.backend.common.DeclarationTransformer
+import org.jetbrains.kotlin.ir.declarations.IrDeclaration
+import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
+import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
+
+class ExplainCallFunctionTransformer(
+ private val builtIns: PowerAssertBuiltIns,
+ private val factory: ExplainCallFunctionFactory,
+) : DeclarationTransformer {
+ private val transformer = ExplainCallGetExplanationTransformer(builtIns, parameter = null)
+
+ override fun transformFlat(declaration: IrDeclaration): List<IrDeclaration>? {
+ if (declaration is IrSimpleFunction) {
+ if (declaration.hasAnnotationOrOverridden(builtIns.explainCallClass)) {
+ return lower(declaration)
+ }
+ }
+
+ return null
+ }
+
+ private fun lower(originalFunction: IrSimpleFunction): List<IrSimpleFunction> {
+ val explainedFunction = factory.generate(originalFunction)
+
+ // Transform the original function to use `null` instead of ExplainCall.explanation.
+ // This keeps the code from throwing an error when ExplainCall.explanation is used.
+ // This in turn helps make sure the compiler-plugin is applied to functions which use `@ExplainCall`.
+ originalFunction.transformChildrenVoid(transformer)
+
+ return listOf(originalFunction, explainedFunction)
+ }
+}
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallGetExplanationTransformer.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallGetExplanationTransformer.kt
new file mode 100644
index 0000000..8d31069
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainCallGetExplanationTransformer.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010-2025 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.
+ */
+
+package org.jetbrains.kotlin.powerassert
+
+import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext
+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.impl.IrConstImpl
+import org.jetbrains.kotlin.ir.expressions.impl.IrGetValueImpl
+import org.jetbrains.kotlin.ir.types.makeNullable
+import org.jetbrains.kotlin.ir.util.kotlinFqName
+
+/**
+ * Replaces all calls to `ExplainCall.explanation` with either `null` or a parameter access.
+ */
+class ExplainCallGetExplanationTransformer(
+ private val builtIns: PowerAssertBuiltIns,
+ private val parameter: IrValueParameter?,
+) : IrElementTransformerVoidWithContext() {
+ override fun visitExpression(expression: IrExpression): IrExpression {
+ return when {
+ isGetExplanation(expression) -> when (parameter) {
+ null -> IrConstImpl.constNull(expression.startOffset, expression.endOffset, builtIns.callExplanationType.makeNullable())
+ else -> IrGetValueImpl(expression.startOffset, expression.endOffset, parameter.type, parameter.symbol)
+ }
+ else -> super.visitExpression(expression)
+ }
+ }
+
+ private fun isGetExplanation(expression: IrExpression): Boolean =
+ (expression as? IrCall)?.symbol?.owner?.kotlinFqName == ExplainCallGetExplanation
+}
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainedSelfCallTransformer.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainedSelfCallTransformer.kt
new file mode 100644
index 0000000..6eb83bb
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/ExplainedSelfCallTransformer.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2010-2025 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.
+ */
+
+package org.jetbrains.kotlin.powerassert
+
+import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext
+import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
+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.impl.IrGetValueImpl
+import org.jetbrains.kotlin.ir.util.irCall
+
+/**
+ * Replaces all calls within a function to itself or super to include explanation parameter,
+ * so explanation is propagated to super function calls and recursive calls.
+ */
+class ExplainedSelfCallTransformer(
+ private val originalFunction: IrSimpleFunction,
+ private val explanation: IrValueParameter,
+) : IrElementTransformerVoidWithContext() {
+ override fun visitCall(expression: IrCall): IrExpression {
+ val call = if (expression.symbol == originalFunction.symbol || expression.symbol in originalFunction.overriddenSymbols) {
+ val explainedDispatchSymbol = expression.symbol.owner.explainedDispatchSymbol!!
+ irCall(expression, explainedDispatchSymbol, newSuperQualifierSymbol = expression.superQualifierSymbol).apply {
+ arguments[explainedDispatchSymbol.owner.parameters.last()] =
+ IrGetValueImpl(expression.startOffset, expression.endOffset, explanation.type, explanation.symbol)
+ }
+ } else {
+ expression
+ }
+
+ call.transformChildren(this, null)
+ return call
+ }
+}
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/IrUtils.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/IrUtils.kt
index 5b30cec..191ab39 100644
--- a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/IrUtils.kt
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/IrUtils.kt
@@ -29,18 +29,21 @@
import org.jetbrains.kotlin.ir.builders.irBlockBody
import org.jetbrains.kotlin.ir.builders.parent
import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
-import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.declarations.IrParameterKind
+import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionExpressionImpl
-import org.jetbrains.kotlin.ir.expressions.isComparisonOperator
import org.jetbrains.kotlin.ir.irAttribute
+import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
import org.jetbrains.kotlin.ir.types.IrType
-import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
+import org.jetbrains.kotlin.ir.util.hasAnnotation
import org.jetbrains.kotlin.ir.visitors.IrVisitorVoid
import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
import org.jetbrains.kotlin.name.Name
+fun IrSimpleFunction.hasAnnotationOrOverridden(annotation: IrClassSymbol): Boolean =
+ hasAnnotation(annotation) || overriddenSymbols.any { it.owner.hasAnnotationOrOverridden(annotation) }
+
fun IrBuilderWithScope.irLambda(
returnType: IrType,
lambdaType: IrType,
@@ -87,7 +90,7 @@
var range = startOffset..endOffset
acceptChildrenVoid(
- object : IrElementVisitorVoid {
+ object : IrVisitorVoid() {
override fun visitElement(element: IrElement) {
val childRange = element.sourceRange
range = minOf(range.start, childRange.start)..maxOf(range.endInclusive, childRange.endInclusive)
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssert.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssert.kt
index 4ad6b74..50f808e 100644
--- a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssert.kt
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssert.kt
@@ -17,7 +17,7 @@
val FUNCTION_FOR_EXPLAIN_CALL by IrDeclarationOriginImpl.Synthetic
val EXPLANATION by IrDeclarationOriginImpl.Synthetic
-val PowerAssertGetDiagram = FqName("kotlin.explain.ExplainCall.Companion.<get-explanation>")
+val ExplainCallGetExplanation = FqName("kotlin.explain.ExplainCall.Companion.<get-explanation>")
fun IrValueDeclaration.isExplained(): Boolean {
val variable = this as? IrVariable ?: return false
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertBuiltIns.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertBuiltIns.kt
index b6372fe..d5e88a6 100644
--- a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertBuiltIns.kt
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertBuiltIns.kt
@@ -24,6 +24,8 @@
private val context: IrPluginContext,
) {
companion object {
+ const val PLUGIN_ID = "org.jetbrains.kotlin.powerassert"
+
private fun dependencyError(): Nothing {
error("Power-Assert plugin runtime dependency was not found.")
}
@@ -55,6 +57,8 @@
private val callExplanationClassId = ClassId.topLevel(callExplanationFqName)
}
+ val metadata = PowerAssertMetadata(context.languageVersionSettings.languageVersion)
+
private fun referenceClass(classId: ClassId): IrClassSymbol =
context.referenceClass(classId) ?: dependencyError()
@@ -69,6 +73,7 @@
val explainCallClass = referenceClass(explainCallClassId)
val explainCallType = explainCallClass.defaultTypeWithoutArguments
+ val explainCallConstructor = explainCallClass.primaryConstructor()
val expressionClass = referenceClass(classId("Expression"))
val expressionType = expressionClass.defaultTypeWithoutArguments
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertCallTransformer.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertCallTransformer.kt
index 5c54907..0b04e09 100644
--- a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertCallTransformer.kt
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertCallTransformer.kt
@@ -37,7 +37,6 @@
import org.jetbrains.kotlin.ir.builders.irSet
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.IrBlockBody
-import org.jetbrains.kotlin.ir.declarations.IrParameterKind
import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.symbols.IrTypeParameterSymbol
@@ -82,8 +81,8 @@
val scope = currentScope as PowerAssertScope
val body = declaration.body
if (body is IrBlockBody) {
- for (variable in scope.variables.values.reversed()) {
- body.statements.add(0, variable)
+ if (scope.variables.values.isNotEmpty()) {
+ body.statements.addAll(0, scope.variables.values)
}
}
@@ -122,11 +121,17 @@
override fun visitCall(expression: IrCall): IrExpression {
expression.transformChildrenVoid()
+ // Never transform calls within functions annotated with ExplainCall.
+ // TODO needs a better checks
+ if ((currentFunction?.irElement as? IrSimpleFunction)?.hasAnnotationOrOverridden(builtIns.explainCallClass) == true) {
+ return expression
+ }
+
val function = expression.symbol.owner
val fqName = function.kotlinFqName
return when {
function.parameters.isEmpty() -> expression
- function.hasAnnotation(builtIns.explainCallClass) -> buildForAnnotated(expression, function)
+ function.hasAnnotationOrOverridden(builtIns.explainCallClass) -> buildForAnnotated(expression, function)
fqName in configuration.functions -> buildForOverride(expression, function)
else -> expression
}
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertFunctionTransformer.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertFunctionTransformer.kt
deleted file mode 100644
index b9d42d3..0000000
--- a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertFunctionTransformer.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright 2010-2024 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.
- */
-
-package org.jetbrains.kotlin.powerassert
-
-import org.jetbrains.kotlin.backend.common.DeclarationTransformer
-import org.jetbrains.kotlin.ir.declarations.IrDeclaration
-import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
-import org.jetbrains.kotlin.ir.util.hasAnnotation
-
-class PowerAssertFunctionTransformer(
- private val builtIns: PowerAssertBuiltIns,
- private val factory: ExplainCallFunctionFactory,
-) : DeclarationTransformer {
- override fun transformFlat(declaration: IrDeclaration): List<IrDeclaration>? {
- if (declaration is IrSimpleFunction) {
- if (declaration.hasAnnotation(builtIns.explainCallClass)) {
- return lower(declaration)
- }
- }
-
- return null
- }
-
- private fun lower(irFunction: IrSimpleFunction): List<IrSimpleFunction> {
- return listOf(irFunction, factory.generate(irFunction))
- }
-}
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertIrGenerationExtension.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertIrGenerationExtension.kt
index 792398b..d26ae9d 100644
--- a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertIrGenerationExtension.kt
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertIrGenerationExtension.kt
@@ -29,9 +29,9 @@
) : IrGenerationExtension {
override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
val builtIns = PowerAssertBuiltIns(pluginContext)
- val factory = ExplainCallFunctionFactory(moduleFragment, pluginContext, builtIns)
+ val factory = ExplainCallFunctionFactory(pluginContext, builtIns)
- val functionTransformer = PowerAssertFunctionTransformer(builtIns, factory)
+ val functionTransformer = ExplainCallFunctionTransformer(builtIns, factory)
moduleFragment.files.forEach(functionTransformer::lower)
for (file in moduleFragment.files) {
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertMetadata.kt b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertMetadata.kt
new file mode 100644
index 0000000..7b97c2f
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/power-assert.backend/src/org/jetbrains/kotlin/powerassert/PowerAssertMetadata.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2010-2025 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.
+ */
+
+package org.jetbrains.kotlin.powerassert
+
+import org.jetbrains.kotlin.config.LanguageVersion
+
+/**
+ * Power-Assert metadata that helps track what version of the compiler was used to generate the
+ * `$explained` function overload of the original function. While the version is not currently used
+ * for anything, the presence of such metadata indicates that the explained function has been
+ * generated. In the future it may indicate what shape the explained function takes, for example,
+ * the parameter type of the explanation variable.
+ */
+@JvmInline
+value class PowerAssertMetadata(val data: ByteArray) {
+ constructor(version: LanguageVersion) : this(byteArrayOf(version.major.toByte(), version.minor.toByte()))
+}
diff --git a/plugins/power-assert/power-assert-compiler/power-assert.cli/src/org/jetbrains/kotlin/powerassert/PowerAssertCommandLineProcessor.kt b/plugins/power-assert/power-assert-compiler/power-assert.cli/src/org/jetbrains/kotlin/powerassert/PowerAssertCommandLineProcessor.kt
index 43fd07c..3997401 100644
--- a/plugins/power-assert/power-assert-compiler/power-assert.cli/src/org/jetbrains/kotlin/powerassert/PowerAssertCommandLineProcessor.kt
+++ b/plugins/power-assert/power-assert-compiler/power-assert.cli/src/org/jetbrains/kotlin/powerassert/PowerAssertCommandLineProcessor.kt
@@ -25,7 +25,7 @@
import org.jetbrains.kotlin.config.CompilerConfiguration
class PowerAssertCommandLineProcessor : CommandLineProcessor {
- override val pluginId: String = "org.jetbrains.kotlin.powerassert"
+ override val pluginId: String = PowerAssertBuiltIns.PLUGIN_ID
override val pluginOptions: Collection<CliOption> = listOf(
CliOption(
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.box.txt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.box.txt
new file mode 100644
index 0000000..bf9680d
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.box.txt
@@ -0,0 +1,21 @@
+callTypeA: ---
+no description
+---
+callTypeB: ---
+type.describe(1 == 2)
+| |
+TypeD Expected <1>, actual <2>.
+
+---
+callTypeC: ---
+type.describe(1 == 2)
+| |
+TypeD Expected <1>, actual <2>.
+
+---
+callTypeD: ---
+type.describe(1 == 2)
+| |
+TypeD Expected <1>, actual <2>.
+
+---
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.fir.kt.txt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.fir.kt.txt
new file mode 100644
index 0000000..0faba5e
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.fir.kt.txt
@@ -0,0 +1,153 @@
+// MODULE: lib
+// FILE: A.kt
+
+abstract class TypeB : TypeA {
+ constructor() /* primary */ {
+ super/*Any*/()
+ /* <init>() */
+
+ }
+
+ @JvmSynthetic
+ override fun describe$explained(value: Any, $explanation: CallExplanation): String? {
+ return { // BLOCK
+ val tmp_0: CallExplanation? = $explanation
+ when {
+ EQEQ(arg0 = tmp_0, arg1 = null) -> null
+ else -> tmp_0.toDefaultMessage()
+ }
+ }
+ }
+
+ @ExplainCall
+ override fun describe(value: Any): String? {
+ return { // BLOCK
+ val tmp_1: CallExplanation? = null
+ when {
+ EQEQ(arg0 = tmp_1, arg1 = null) -> null
+ else -> tmp_1.toDefaultMessage()
+ }
+ }
+ }
+
+}
+
+abstract class TypeC : TypeB {
+ constructor() /* primary */ {
+ super/*TypeB*/()
+ /* <init>() */
+
+ }
+
+ @JvmSynthetic
+ override fun describe$explained(value: Any, $explanation: CallExplanation): String? {
+ return super<TypeB>.describe$explained(value = value, $explanation = $explanation)
+ }
+
+ override fun describe(value: Any): String? {
+ return super<TypeB>.describe(value = value)
+ }
+
+}
+
+interface TypeA {
+ abstract fun describe(value: Any): String?
+
+}
+
+data object TypeD : TypeC {
+ private constructor() /* primary */ {
+ super/*TypeC*/()
+ /* <init>() */
+
+ }
+
+ override operator fun equals(other: Any?): Boolean {
+ when {
+ EQEQEQ(arg0 = <this>, arg1 = other) -> return true
+ }
+ when {
+ other !is TypeD -> return false
+ }
+ val tmp_2: TypeD = other as TypeD
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return 81291306
+ }
+
+ override fun toString(): String {
+ return "TypeD"
+ }
+
+}
+
+// MODULE: main
+// FILE: B.kt
+
+fun box(): String {
+ return runAllOutput(tests = ["callTypeA".to<String, KFunction0<String>>(that = ::callTypeA), "callTypeB".to<String, KFunction0<String>>(that = ::callTypeB), "callTypeC".to<String, KFunction0<String>>(that = ::callTypeC), "callTypeD".to<String, KFunction0<String>>(that = ::callTypeD)])
+}
+
+fun callTypeA(): String {
+ val type: TypeA = TypeD
+ return { // BLOCK
+ val tmp_0: String? = type.describe(value = EQEQ(arg0 = 1, arg1 = 2))
+ when {
+ EQEQ(arg0 = tmp_0, arg1 = null) -> "no description"
+ else -> tmp_0
+ }
+ }
+}
+
+fun callTypeB(): String {
+ val type: TypeB = TypeD
+ return { // BLOCK
+ val tmp_1: String? = { // BLOCK
+ val tmp0_Explain: TypeB = type
+ { // BLOCK
+ val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+ tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 392, source = " type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+ }
+ }
+ when {
+ EQEQ(arg0 = tmp_1, arg1 = null) -> "no description"
+ else -> tmp_1
+ }
+ }
+}
+
+fun callTypeC(): String {
+ val type: TypeC = TypeD
+ return { // BLOCK
+ val tmp_2: String? = { // BLOCK
+ val tmp0_Explain: TypeC = type
+ { // BLOCK
+ val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+ tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 502, source = " type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+ }
+ }
+ when {
+ EQEQ(arg0 = tmp_2, arg1 = null) -> "no description"
+ else -> tmp_2
+ }
+ }
+}
+
+fun callTypeD(): String {
+ val type: TypeD = TypeD
+ return { // BLOCK
+ val tmp_3: String? = { // BLOCK
+ val tmp0_Explain: TypeD = type
+ { // BLOCK
+ val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+ tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 612, source = " type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+ }
+ }
+ when {
+ EQEQ(arg0 = tmp_3, arg1 = null) -> "no description"
+ else -> tmp_3
+ }
+ }
+}
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.kt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.kt
new file mode 100644
index 0000000..5d9313e
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.kt
@@ -0,0 +1,56 @@
+// IGNORE_BACKEND_K1: ANY
+// DUMP_KT_IR
+
+// MODULE: lib
+// FILE: A.kt
+
+import kotlin.explain.*
+
+interface TypeA {
+ fun describe(value: Any): String?
+}
+
+abstract class TypeB : TypeA {
+ @ExplainCall // TODO should we even support this?
+ override fun describe(value: Any): String? {
+ return ExplainCall.explanation?.toDefaultMessage()
+ }
+}
+
+abstract class TypeC : TypeB() {
+ override fun describe(value: Any): String? {
+ return super.describe(value)
+ }
+}
+
+data object TypeD : TypeC()
+
+// MODULE: main(lib)
+// FILE: B.kt
+
+fun box(): String = runAllOutput(
+ "callTypeA" to ::callTypeA,
+ "callTypeB" to ::callTypeB,
+ "callTypeC" to ::callTypeC,
+ "callTypeD" to ::callTypeD,
+)
+
+fun callTypeA(): String {
+ val type: TypeA = TypeD
+ return type.describe(1 == 2) ?: "no description"
+}
+
+fun callTypeB(): String {
+ val type: TypeB = TypeD
+ return type.describe(1 == 2) ?: "no description"
+}
+
+fun callTypeC(): String {
+ val type: TypeC = TypeD
+ return type.describe(1 == 2) ?: "no description"
+}
+
+fun callTypeD(): String {
+ val type: TypeD = TypeD
+ return type.describe(1 == 2) ?: "no description"
+}
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.kt.txt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.kt.txt
new file mode 100644
index 0000000..cb325a4
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.kt.txt
@@ -0,0 +1,135 @@
+// MODULE: lib
+// FILE: A.kt
+
+abstract class TypeB : TypeA {
+ constructor() /* primary */ {
+ super/*Any*/()
+ /* <init>() */
+
+ }
+
+ @JvmSynthetic
+ override fun describe$explained(value: Any, $explanation: CallExplanation): String? {
+ return { // BLOCK
+ val tmp_0: CallExplanation? = $explanation
+ when {
+ EQEQ(arg0 = tmp_0, arg1 = null) -> null
+ else -> tmp_0.toDefaultMessage()
+ }
+ }
+ }
+
+ @ExplainCall
+ override fun describe(value: Any): String? {
+ return { // BLOCK
+ val tmp_1: CallExplanation? = null
+ when {
+ EQEQ(arg0 = tmp_1, arg1 = null) -> null
+ else -> tmp_1.toDefaultMessage()
+ }
+ }
+ }
+
+}
+
+abstract class TypeC : TypeB {
+ constructor() /* primary */ {
+ super/*TypeB*/()
+ /* <init>() */
+
+ }
+
+ @JvmSynthetic
+ override fun describe$explained(value: Any, $explanation: CallExplanation): String? {
+ return super<TypeB>.describe$explained(value = value, $explanation = $explanation)
+ }
+
+ override fun describe(value: Any): String? {
+ return super<TypeB>.describe(value = value)
+ }
+
+}
+
+interface TypeA {
+ abstract fun describe(value: Any): String?
+
+}
+
+data object TypeD : TypeC {
+ private constructor() /* primary */ {
+ super/*TypeC*/()
+ /* <init>() */
+
+ }
+
+ override operator fun equals(other: Any?): Boolean {
+ when {
+ EQEQEQ(arg0 = <this>, arg1 = other) -> return true
+ }
+ when {
+ other !is TypeD -> return false
+ }
+ val tmp_2: TypeD = other as TypeD
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return 81291306
+ }
+
+ override fun toString(): String {
+ return "TypeD"
+ }
+
+}
+
+// MODULE: main
+// FILE: B.kt
+
+fun box(): String {
+ return runAllOutput(tests = ["callTypeA".to<String, KFunction0<String>>(that = ::callTypeA), "callTypeB".to<String, KFunction0<String>>(that = ::callTypeB), "callTypeC".to<String, KFunction0<String>>(that = ::callTypeC), "callTypeD".to<String, KFunction0<String>>(that = ::callTypeD)])
+}
+
+fun callTypeA(): String {
+ val type: TypeA = TypeD
+ return { // BLOCK
+ val tmp_0: String? = type.describe(value = EQEQ(arg0 = 1, arg1 = 2))
+ when {
+ EQEQ(arg0 = tmp_0, arg1 = null) -> "no description"
+ else -> tmp_0
+ }
+ }
+}
+
+fun callTypeB(): String {
+ val type: TypeB = TypeD
+ return { // BLOCK
+ val tmp_1: String? = type.describe(value = EQEQ(arg0 = 1, arg1 = 2))
+ when {
+ EQEQ(arg0 = tmp_1, arg1 = null) -> "no description"
+ else -> tmp_1
+ }
+ }
+}
+
+fun callTypeC(): String {
+ val type: TypeC = TypeD
+ return { // BLOCK
+ val tmp_2: String? = type.describe(value = EQEQ(arg0 = 1, arg1 = 2))
+ when {
+ EQEQ(arg0 = tmp_2, arg1 = null) -> "no description"
+ else -> tmp_2
+ }
+ }
+}
+
+fun callTypeD(): String {
+ val type: TypeD = TypeD
+ return { // BLOCK
+ val tmp_3: String? = type.describe(value = EQEQ(arg0 = 1, arg1 = 2))
+ when {
+ EQEQ(arg0 = tmp_3, arg1 = null) -> "no description"
+ else -> tmp_3
+ }
+ }
+}
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.box.txt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.box.txt
new file mode 100644
index 0000000..bf9680d
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.box.txt
@@ -0,0 +1,21 @@
+callTypeA: ---
+no description
+---
+callTypeB: ---
+type.describe(1 == 2)
+| |
+TypeD Expected <1>, actual <2>.
+
+---
+callTypeC: ---
+type.describe(1 == 2)
+| |
+TypeD Expected <1>, actual <2>.
+
+---
+callTypeD: ---
+type.describe(1 == 2)
+| |
+TypeD Expected <1>, actual <2>.
+
+---
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.fir.kt.txt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.fir.kt.txt
new file mode 100644
index 0000000..cddbb9a
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.fir.kt.txt
@@ -0,0 +1,147 @@
+abstract class TypeB : TypeA {
+ constructor() /* primary */ {
+ super/*Any*/()
+ /* <init>() */
+
+ }
+
+ @JvmSynthetic
+ override fun describe$explained(value: Any, $explanation: CallExplanation): String? {
+ return { // BLOCK
+ val tmp_0: CallExplanation? = $explanation
+ when {
+ EQEQ(arg0 = tmp_0, arg1 = null) -> null
+ else -> tmp_0.toDefaultMessage()
+ }
+ }
+ }
+
+ @ExplainCall
+ override fun describe(value: Any): String? {
+ return { // BLOCK
+ val tmp_1: CallExplanation? = null
+ when {
+ EQEQ(arg0 = tmp_1, arg1 = null) -> null
+ else -> tmp_1.toDefaultMessage()
+ }
+ }
+ }
+
+}
+
+abstract class TypeC : TypeB {
+ constructor() /* primary */ {
+ super/*TypeB*/()
+ /* <init>() */
+
+ }
+
+ @JvmSynthetic
+ override fun describe$explained(value: Any, $explanation: CallExplanation): String? {
+ return super<TypeB>.describe$explained(value = value, $explanation = $explanation)
+ }
+
+ override fun describe(value: Any): String? {
+ return super<TypeB>.describe(value = value)
+ }
+
+}
+
+interface TypeA {
+ abstract fun describe(value: Any): String?
+
+}
+
+data object TypeD : TypeC {
+ private constructor() /* primary */ {
+ super/*TypeC*/()
+ /* <init>() */
+
+ }
+
+ override operator fun equals(other: Any?): Boolean {
+ when {
+ EQEQEQ(arg0 = <this>, arg1 = other) -> return true
+ }
+ when {
+ other !is TypeD -> return false
+ }
+ val tmp_2: TypeD = other as TypeD
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return 81291306
+ }
+
+ override fun toString(): String {
+ return "TypeD"
+ }
+
+}
+
+fun box(): String {
+ return runAllOutput(tests = ["callTypeA".to<String, KFunction0<String>>(that = ::callTypeA), "callTypeB".to<String, KFunction0<String>>(that = ::callTypeB), "callTypeC".to<String, KFunction0<String>>(that = ::callTypeC), "callTypeD".to<String, KFunction0<String>>(that = ::callTypeD)])
+}
+
+fun callTypeA(): String {
+ val type: TypeA = TypeD
+ return { // BLOCK
+ val tmp_3: String? = type.describe(value = EQEQ(arg0 = 1, arg1 = 2))
+ when {
+ EQEQ(arg0 = tmp_3, arg1 = null) -> "no description"
+ else -> tmp_3
+ }
+ }
+}
+
+fun callTypeB(): String {
+ val type: TypeB = TypeD
+ return { // BLOCK
+ val tmp_4: String? = { // BLOCK
+ val tmp0_Explain: TypeB = type
+ { // BLOCK
+ val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+ tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 750, source = " type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+ }
+ }
+ when {
+ EQEQ(arg0 = tmp_4, arg1 = null) -> "no description"
+ else -> tmp_4
+ }
+ }
+}
+
+fun callTypeC(): String {
+ val type: TypeC = TypeD
+ return { // BLOCK
+ val tmp_5: String? = { // BLOCK
+ val tmp0_Explain: TypeC = type
+ { // BLOCK
+ val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+ tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 860, source = " type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+ }
+ }
+ when {
+ EQEQ(arg0 = tmp_5, arg1 = null) -> "no description"
+ else -> tmp_5
+ }
+ }
+}
+
+fun callTypeD(): String {
+ val type: TypeD = TypeD
+ return { // BLOCK
+ val tmp_6: String? = { // BLOCK
+ val tmp0_Explain: TypeD = type
+ { // BLOCK
+ val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+ tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 970, source = " type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+ }
+ }
+ when {
+ EQEQ(arg0 = tmp_6, arg1 = null) -> "no description"
+ else -> tmp_6
+ }
+ }
+}
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.kt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.kt
new file mode 100644
index 0000000..f07eb5d
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.kt
@@ -0,0 +1,49 @@
+// DUMP_KT_IR
+
+import kotlin.explain.*
+
+interface TypeA {
+ fun describe(value: Any): String?
+}
+
+abstract class TypeB : TypeA {
+ @ExplainCall // TODO should we even support this?
+ override fun describe(value: Any): String? {
+ return ExplainCall.explanation?.toDefaultMessage()
+ }
+}
+
+abstract class TypeC : TypeB() {
+ override fun describe(value: Any): String? {
+ return super.describe(value)
+ }
+}
+
+data object TypeD : TypeC()
+
+fun box(): String = runAllOutput(
+ "callTypeA" to ::callTypeA,
+ "callTypeB" to ::callTypeB,
+ "callTypeC" to ::callTypeC,
+ "callTypeD" to ::callTypeD,
+)
+
+fun callTypeA(): String {
+ val type: TypeA = TypeD
+ return type.describe(1 == 2) ?: "no description"
+}
+
+fun callTypeB(): String {
+ val type: TypeB = TypeD
+ return type.describe(1 == 2) ?: "no description"
+}
+
+fun callTypeC(): String {
+ val type: TypeC = TypeD
+ return type.describe(1 == 2) ?: "no description"
+}
+
+fun callTypeD(): String {
+ val type: TypeD = TypeD
+ return type.describe(1 == 2) ?: "no description"
+}
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.kt.txt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.kt.txt
new file mode 100644
index 0000000..cddbb9a
--- /dev/null
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.kt.txt
@@ -0,0 +1,147 @@
+abstract class TypeB : TypeA {
+ constructor() /* primary */ {
+ super/*Any*/()
+ /* <init>() */
+
+ }
+
+ @JvmSynthetic
+ override fun describe$explained(value: Any, $explanation: CallExplanation): String? {
+ return { // BLOCK
+ val tmp_0: CallExplanation? = $explanation
+ when {
+ EQEQ(arg0 = tmp_0, arg1 = null) -> null
+ else -> tmp_0.toDefaultMessage()
+ }
+ }
+ }
+
+ @ExplainCall
+ override fun describe(value: Any): String? {
+ return { // BLOCK
+ val tmp_1: CallExplanation? = null
+ when {
+ EQEQ(arg0 = tmp_1, arg1 = null) -> null
+ else -> tmp_1.toDefaultMessage()
+ }
+ }
+ }
+
+}
+
+abstract class TypeC : TypeB {
+ constructor() /* primary */ {
+ super/*TypeB*/()
+ /* <init>() */
+
+ }
+
+ @JvmSynthetic
+ override fun describe$explained(value: Any, $explanation: CallExplanation): String? {
+ return super<TypeB>.describe$explained(value = value, $explanation = $explanation)
+ }
+
+ override fun describe(value: Any): String? {
+ return super<TypeB>.describe(value = value)
+ }
+
+}
+
+interface TypeA {
+ abstract fun describe(value: Any): String?
+
+}
+
+data object TypeD : TypeC {
+ private constructor() /* primary */ {
+ super/*TypeC*/()
+ /* <init>() */
+
+ }
+
+ override operator fun equals(other: Any?): Boolean {
+ when {
+ EQEQEQ(arg0 = <this>, arg1 = other) -> return true
+ }
+ when {
+ other !is TypeD -> return false
+ }
+ val tmp_2: TypeD = other as TypeD
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return 81291306
+ }
+
+ override fun toString(): String {
+ return "TypeD"
+ }
+
+}
+
+fun box(): String {
+ return runAllOutput(tests = ["callTypeA".to<String, KFunction0<String>>(that = ::callTypeA), "callTypeB".to<String, KFunction0<String>>(that = ::callTypeB), "callTypeC".to<String, KFunction0<String>>(that = ::callTypeC), "callTypeD".to<String, KFunction0<String>>(that = ::callTypeD)])
+}
+
+fun callTypeA(): String {
+ val type: TypeA = TypeD
+ return { // BLOCK
+ val tmp_3: String? = type.describe(value = EQEQ(arg0 = 1, arg1 = 2))
+ when {
+ EQEQ(arg0 = tmp_3, arg1 = null) -> "no description"
+ else -> tmp_3
+ }
+ }
+}
+
+fun callTypeB(): String {
+ val type: TypeB = TypeD
+ return { // BLOCK
+ val tmp_4: String? = { // BLOCK
+ val tmp0_Explain: TypeB = type
+ { // BLOCK
+ val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+ tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 750, source = " type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+ }
+ }
+ when {
+ EQEQ(arg0 = tmp_4, arg1 = null) -> "no description"
+ else -> tmp_4
+ }
+ }
+}
+
+fun callTypeC(): String {
+ val type: TypeC = TypeD
+ return { // BLOCK
+ val tmp_5: String? = { // BLOCK
+ val tmp0_Explain: TypeC = type
+ { // BLOCK
+ val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+ tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 860, source = " type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+ }
+ }
+ when {
+ EQEQ(arg0 = tmp_5, arg1 = null) -> "no description"
+ else -> tmp_5
+ }
+ }
+}
+
+fun callTypeD(): String {
+ val type: TypeD = TypeD
+ return { // BLOCK
+ val tmp_6: String? = { // BLOCK
+ val tmp0_Explain: TypeD = type
+ { // BLOCK
+ val tmp1_Explain: Boolean = EQEQ(arg0 = 1, arg1 = 2)
+ tmp0_Explain.describe$explained(value = tmp1_Explain, $explanation = CallExplanation(offset = 970, source = " type.describe(1 == 2)", dispatchReceiver = Receiver(startOffset = 11, endOffset = 15, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 11, endOffset = 15, displayOffset = 11, value = tmp0_Explain)])), extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 25, endOffset = 31, expressions = listOf</* null */>(elements = [EqualityExpression(startOffset = 25, endOffset = 31, displayOffset = 27, value = tmp1_Explain, lhs = 1, rhs = 2)])))])))
+ }
+ }
+ when {
+ EQEQ(arg0 = tmp_6, arg1 = null) -> "no description"
+ else -> tmp_6
+ }
+ }
+}
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.fir.kt.txt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.fir.kt.txt
index 9a60318..9bef090 100644
--- a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.fir.kt.txt
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.fir.kt.txt
@@ -34,7 +34,7 @@
val tmp1_Explain: List<String> = tmp0_Explain.reversed<String>()
val tmp2_Explain: List<String> = emptyList<String>()
val tmp3_Explain: Boolean = EQEQ(arg0 = tmp1_Explain, arg1 = tmp2_Explain)
- describe$explained(value = tmp3_Explain, $explanation = CallExplanation(offset = 110, source = " describe(reallyLongList.reversed() == emptyList<String>())", dispatchReceiver = null, extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 20, endOffset = 68, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 20, endOffset = 34, displayOffset = 20, value = tmp0_Explain), ValueExpression(startOffset = 20, endOffset = 45, displayOffset = 35, value = tmp1_Explain), ValueExpression(startOffset = 49, endOffset = 68, displayOffset = 49, value = tmp2_Explain), EqualityExpression(startOffset = 20, endOffset = 68, displayOffset = 46, value = tmp3_Explain, lhs = tmp1_Explain, rhs = tmp2_Explain)])))])))
+ describe$explained(value = tmp3_Explain, $explanation = CallExplanation(offset = 111, source = " describe(reallyLongList.reversed() == emptyList<String>())", dispatchReceiver = null, extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 20, endOffset = 68, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 20, endOffset = 34, displayOffset = 20, value = tmp0_Explain), ValueExpression(startOffset = 20, endOffset = 45, displayOffset = 35, value = tmp1_Explain), ValueExpression(startOffset = 49, endOffset = 68, displayOffset = 49, value = tmp2_Explain), EqualityExpression(startOffset = 20, endOffset = 68, displayOffset = 46, value = tmp3_Explain, lhs = tmp1_Explain, rhs = tmp2_Explain)])))])))
}
when {
EQEQ(arg0 = tmp_0, arg1 = null) -> "FAIL"
@@ -42,4 +42,3 @@
}
}
}
-
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt
index 4b310cf..ba4262a 100644
--- a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt
@@ -1,3 +1,4 @@
+// IGNORE_BACKEND_K1: ANY
// DUMP_KT_IR
// MODULE: lib
diff --git a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt.txt b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt.txt
index 9a60318..86ed7f8 100644
--- a/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt.txt
+++ b/plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt.txt
@@ -29,17 +29,10 @@
fun box(): String {
val reallyLongList: List<String> = listOf<String>(elements = ["a", "b"])
return { // BLOCK
- val tmp_0: String? = { // BLOCK
- val tmp0_Explain: List<String> = reallyLongList
- val tmp1_Explain: List<String> = tmp0_Explain.reversed<String>()
- val tmp2_Explain: List<String> = emptyList<String>()
- val tmp3_Explain: Boolean = EQEQ(arg0 = tmp1_Explain, arg1 = tmp2_Explain)
- describe$explained(value = tmp3_Explain, $explanation = CallExplanation(offset = 110, source = " describe(reallyLongList.reversed() == emptyList<String>())", dispatchReceiver = null, extensionReceiver = null, valueArguments = mapOf</* null */, /* null */>(pairs = [Pair</* null */, /* null */>(first = "value", second = ValueArgument(startOffset = 20, endOffset = 68, expressions = listOf</* null */>(elements = [ValueExpression(startOffset = 20, endOffset = 34, displayOffset = 20, value = tmp0_Explain), ValueExpression(startOffset = 20, endOffset = 45, displayOffset = 35, value = tmp1_Explain), ValueExpression(startOffset = 49, endOffset = 68, displayOffset = 49, value = tmp2_Explain), EqualityExpression(startOffset = 20, endOffset = 68, displayOffset = 46, value = tmp3_Explain, lhs = tmp1_Explain, rhs = tmp2_Explain)])))])))
- }
+ val tmp_0: String? = describe(value = EQEQ(arg0 = reallyLongList.reversed<String>(), arg1 = emptyList<String>()))
when {
EQEQ(arg0 = tmp_0, arg1 = null) -> "FAIL"
else -> tmp_0
}
}
}
-
diff --git a/plugins/power-assert/power-assert-compiler/testData/helpers/utils.kt b/plugins/power-assert/power-assert-compiler/testData/helpers/utils.kt
index 193f25a..9161a26 100644
--- a/plugins/power-assert/power-assert-compiler/testData/helpers/utils.kt
+++ b/plugins/power-assert/power-assert-compiler/testData/helpers/utils.kt
@@ -14,3 +14,19 @@
"${name}: $msg"
}
}
+
+fun withThrowableMessage(block: () -> String): String {
+ val msg = try {
+ block()
+ } catch (e: Throwable) {
+ e.message ?: "no message"
+ }
+ return "---\n${msg}\n---\n"
+}
+
+fun runAllOutput(vararg tests: Pair<String, () -> String>): String {
+ return tests.joinToString("") { (name, func) ->
+ val msg = withThrowableMessage { func() }
+ "${name}: $msg"
+ }
+}
diff --git a/plugins/power-assert/power-assert-compiler/tests-gen/org/jetbrains/kotlin/powerassert/FirLightTreeBlackBoxCodegenTestForPowerAssertGenerated.java b/plugins/power-assert/power-assert-compiler/tests-gen/org/jetbrains/kotlin/powerassert/FirLightTreeBlackBoxCodegenTestForPowerAssertGenerated.java
index 853ad11..0f78c44 100644
--- a/plugins/power-assert/power-assert-compiler/tests-gen/org/jetbrains/kotlin/powerassert/FirLightTreeBlackBoxCodegenTestForPowerAssertGenerated.java
+++ b/plugins/power-assert/power-assert-compiler/tests-gen/org/jetbrains/kotlin/powerassert/FirLightTreeBlackBoxCodegenTestForPowerAssertGenerated.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Copyright 2010-2024 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.
*/
@@ -101,10 +101,22 @@
}
@Test
+ @TestMetadata("ModuleOverride.kt")
+ public void testModuleOverride() {
+ runTest("plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.kt");
+ }
+
+ @Test
@TestMetadata("modules.kt")
public void testModules() {
runTest("plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt");
}
+
+ @Test
+ @TestMetadata("Override.kt")
+ public void testOverride() {
+ runTest("plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.kt");
+ }
}
@Nested
diff --git a/plugins/power-assert/power-assert-compiler/tests-gen/org/jetbrains/kotlin/powerassert/IrBlackBoxCodegenTestForPowerAssertGenerated.java b/plugins/power-assert/power-assert-compiler/tests-gen/org/jetbrains/kotlin/powerassert/IrBlackBoxCodegenTestForPowerAssertGenerated.java
index 62edd09..c1a9002 100644
--- a/plugins/power-assert/power-assert-compiler/tests-gen/org/jetbrains/kotlin/powerassert/IrBlackBoxCodegenTestForPowerAssertGenerated.java
+++ b/plugins/power-assert/power-assert-compiler/tests-gen/org/jetbrains/kotlin/powerassert/IrBlackBoxCodegenTestForPowerAssertGenerated.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Copyright 2010-2024 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.
*/
@@ -101,10 +101,22 @@
}
@Test
+ @TestMetadata("ModuleOverride.kt")
+ public void testModuleOverride() {
+ runTest("plugins/power-assert/power-assert-compiler/testData/codegen/annotated/ModuleOverride.kt");
+ }
+
+ @Test
@TestMetadata("modules.kt")
public void testModules() {
runTest("plugins/power-assert/power-assert-compiler/testData/codegen/annotated/modules.kt");
}
+
+ @Test
+ @TestMetadata("Override.kt")
+ public void testOverride() {
+ runTest("plugins/power-assert/power-assert-compiler/testData/codegen/annotated/Override.kt");
+ }
}
@Nested