Prepare kotlin-power-assert files for git merge with Kotlin

This commit is contained in:
Brian Norman
2023-11-22 15:53:49 -06:00
parent 9d2ff1f2de
commit f9bcd697da
62 changed files with 1 additions and 5346 deletions
@@ -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("<anonymous>")
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)
}
@@ -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<FqName>,
) : 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<Node?>
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<Node?>,
dispatchRoot: Node? = null,
extensionRoot: Node? = null,
): IrExpression {
fun recursive(
index: Int,
dispatch: IrExpression?,
extension: IrExpression?,
arguments: List<IrExpression?>,
variables: List<IrTemporaryVariable>,
): 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<Node?>,
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<FunctionDelegate> {
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)
}
}
@@ -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<FqName>,
) : IrGenerationExtension {
override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
for (file in moduleFragment.files) {
PowerAssertCallTransformer(SourceFile(file), pluginContext, messageCollector, functions)
.visitFile(file)
}
}
}
@@ -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<IrExpression?>,
messageArgument: IrExpression,
): IrExpression
fun IrBuilderWithScope.irCallCopy(
overload: IrSimpleFunctionSymbol,
original: IrCall,
dispatchReceiver: IrExpression?,
extensionReceiver: IrExpression?,
valueArguments: List<IrExpression?>,
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))
}
}
}
@@ -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<IrExpression?>,
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,
)
}
}
@@ -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<IrExpression?>,
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,
)
}
}
@@ -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<IrExpression?>,
messageArgument: IrExpression,
): IrExpression = builder.irCallCopy(
overload = overload,
original = original,
dispatchReceiver = dispatchReceiver,
extensionReceiver = extensionReceiver,
valueArguments = valueArguments,
messageArgument = messageArgument,
)
}
@@ -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<IrTemporaryVariable> = emptyList(),
call: IrBuilderWithScope.(IrExpression, List<IrTemporaryVariable>) -> IrExpression,
): IrExpression {
return buildExpression(root, variables) { argument, subStack ->
call(argument, subStack)
}
}
fun IrBuilderWithScope.buildDiagramNestingNullable(
root: Node?,
variables: List<IrTemporaryVariable> = emptyList(),
call: IrBuilderWithScope.(IrExpression?, List<IrTemporaryVariable>) -> IrExpression,
): IrExpression {
return if (root != null) buildDiagramNesting(root, variables, call) else call(null, variables)
}
private fun IrBuilderWithScope.buildExpression(
node: Node,
variables: List<IrTemporaryVariable>,
call: IrBuilderWithScope.(IrExpression, List<IrTemporaryVariable>) -> 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, <diagram>)
* }
* ```
*/
private fun IrBuilderWithScope.add(
node: ExpressionNode,
variables: List<IrTemporaryVariable>,
call: IrBuilderWithScope.(IrExpression, List<IrTemporaryVariable>) -> 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, <diagram>)
* }
* else call(false, <diagram>)
* }
* ```
*/
private fun IrBuilderWithScope.nest(
node: AndNode,
index: Int,
variables: List<IrTemporaryVariable>,
call: IrBuilderWithScope.(IrExpression, List<IrTemporaryVariable>) -> 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, <diagram>)
* else {
* val tmp1 = 2 == 2
* call(tmp1, <diagram>)
* }
* }
* ```
*/
private fun IrBuilderWithScope.nest(
node: OrNode,
index: Int,
variables: List<IrTemporaryVariable>,
call: IrBuilderWithScope.(IrExpression, List<IrTemporaryVariable>) -> 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
)
}
}
}
@@ -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<Node>()
val children: List<Node> 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<IrExpression>()
val expressions: List<IrExpression> = _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<Unit, Node> {
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()
}
@@ -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<IrTemporaryVariable>,
): 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
}
@@ -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<IrExpression>,
) : IrElementTransformerVoid() {
private val _variables = mutableListOf<IrTemporaryVariable>()
val variables: List<IrTemporaryVariable> = _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)
}
}
}
@@ -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)!!
}
}
@@ -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<CliOption> = 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}")
}
}
}
@@ -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<List<String>>("fully-qualified function names")
@AutoService(CompilerPluginRegistrar::class)
class PowerAssertCompilerPluginRegistrar(
private val functions: Set<FqName>,
) : 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()))
}
}
@@ -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,
)
}
}
@@ -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,
)
}
}
@@ -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<SourceFile>,
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)
}
@@ -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 <T> dbg(value: T): T = value
fun <T> 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!!
}
}
@@ -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> V.mustEqual(expected: V): Unit = assert(this == expected)
fun <V> 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<V>(
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<FqName>, 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 ?: ""
}
}
}
@@ -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(),
)
}
}
@@ -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,
)
}
}
@@ -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(),
)
}
}