Initial support for infix target functions

This commit is contained in:
Brian Norman
2022-07-31 12:39:58 -05:00
parent cd9fb4c089
commit b5d8143f4c
10 changed files with 284 additions and 104 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Run ktlint - name: Run ktlint
uses: ScaCap/action-ktlint@1.3 uses: ScaCap/action-ktlint@1.4
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
reporter: github-pr-check reporter: github-pr-check
@@ -22,15 +22,13 @@ import com.bnorm.power.delegate.SamConversionLambdaFunctionDelegate
import com.bnorm.power.delegate.SimpleFunctionDelegate import com.bnorm.power.delegate.SimpleFunctionDelegate
import com.bnorm.power.diagram.IrTemporaryVariable import com.bnorm.power.diagram.IrTemporaryVariable
import com.bnorm.power.diagram.Node import com.bnorm.power.diagram.Node
import com.bnorm.power.diagram.SourceFile
import com.bnorm.power.diagram.buildDiagramNesting import com.bnorm.power.diagram.buildDiagramNesting
import com.bnorm.power.diagram.buildTree import com.bnorm.power.diagram.buildTree
import com.bnorm.power.diagram.info
import com.bnorm.power.diagram.irDiagramString import com.bnorm.power.diagram.irDiagramString
import com.bnorm.power.diagram.substring
import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.ir.IrElement import org.jetbrains.kotlin.ir.IrElement
@@ -38,10 +36,8 @@ import org.jetbrains.kotlin.ir.backend.js.utils.asString
import org.jetbrains.kotlin.ir.builders.irCall import org.jetbrains.kotlin.ir.builders.irCall
import org.jetbrains.kotlin.ir.builders.irString import org.jetbrains.kotlin.ir.builders.irString
import org.jetbrains.kotlin.ir.builders.parent import org.jetbrains.kotlin.ir.builders.parent
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.declarations.IrFunction import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.IrValueParameter import org.jetbrains.kotlin.ir.declarations.IrValueParameter
import org.jetbrains.kotlin.ir.declarations.path
import org.jetbrains.kotlin.ir.expressions.IrCall import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrConst import org.jetbrains.kotlin.ir.expressions.IrConst
import org.jetbrains.kotlin.ir.expressions.IrExpression import org.jetbrains.kotlin.ir.expressions.IrExpression
@@ -68,8 +64,7 @@ import org.jetbrains.kotlin.ir.util.parentClassOrNull
import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.FqName
class PowerAssertCallTransformer( class PowerAssertCallTransformer(
private val file: IrFile, private val sourceFile: SourceFile,
private val fileSource: String,
private val context: IrPluginContext, private val context: IrPluginContext,
private val messageCollector: MessageCollector, private val messageCollector: MessageCollector,
private val functions: Set<FqName> private val functions: Set<FqName>
@@ -104,6 +99,8 @@ class PowerAssertCallTransformer(
return super.visitCall(expression) return super.visitCall(expression)
} }
val extensionRoot =
if (expression.symbol.owner.isInfix) expression.extensionReceiver?.let { buildTree(it) } else null
val messageArgument: IrExpression? val messageArgument: IrExpression?
val roots: List<Node?> val roots: List<Node?>
if (delegate.function.valueParameters.size == function.valueParameters.size) { if (delegate.function.valueParameters.size == function.valueParameters.size) {
@@ -119,42 +116,55 @@ class PowerAssertCallTransformer(
} }
// If all roots are null, there are no transformable parameters // If all roots are null, there are no transformable parameters
if (roots.all { it == null }) { if (extensionRoot == null && roots.all { it == null }) {
messageCollector.info(expression, "Expression is constant and will not be power-assert transformed") messageCollector.info(expression, "Expression is constant and will not be power-assert transformed")
return super.visitCall(expression) return super.visitCall(expression)
} }
val symbol = currentScope!!.scope.scopeOwnerSymbol val symbol = currentScope!!.scope.scopeOwnerSymbol
val builder = DeclarationIrBuilder(context, symbol, expression.startOffset, expression.endOffset) val builder = DeclarationIrBuilder(context, symbol, expression.startOffset, expression.endOffset)
return builder.diagram(expression, delegate, messageArgument, roots) return builder.diagram(expression, delegate, messageArgument, roots, extensionRoot)
} }
private fun DeclarationIrBuilder.diagram( private fun DeclarationIrBuilder.diagram(
original: IrCall, call: IrCall,
delegate: FunctionDelegate, delegate: FunctionDelegate,
messageArgument: IrExpression?, messageArgument: IrExpression?,
roots: List<Node?>, roots: List<Node?>,
index: Int = 0, extensionRoot: Node? = null
arguments: List<IrExpression?> = listOf(),
variables: List<IrTemporaryVariable> = listOf()
): IrExpression { ): IrExpression {
if (index >= roots.size) { fun recursive(
val prefix = buildMessagePrefix(messageArgument, delegate.messageParameter, roots, original) index: Int,
?.deepCopyWithSymbols(parent) extension: IrExpression? = null,
val diagram = irDiagramString(file, fileSource, prefix, original, variables) arguments: List<IrExpression?>,
return delegate.buildCall(this, original, arguments, diagram) variables: List<IrTemporaryVariable>
} else { ): IrExpression {
val root = roots[index] if (index >= roots.size) {
if (root == null) { val prefix = buildMessagePrefix(messageArgument, delegate.messageParameter, roots, call)
val newArguments = arguments + original.getValueArgument(index) ?.deepCopyWithSymbols(parent)
return diagram(original, delegate, messageArgument, roots, index + 1, newArguments, variables) val diagram = irDiagramString(sourceFile, prefix, call, variables)
return delegate.buildCall(this, call, extension, arguments, diagram)
} else { } else {
return buildDiagramNesting(root) { argument, newVariables -> val root = roots[index]
val newArguments = arguments + argument if (root == null) {
diagram(original, delegate, messageArgument, roots, index + 1, newArguments, variables + newVariables) val newArguments = arguments + call.getValueArgument(index)
return recursive(index + 1, extension, newArguments, variables)
} else {
return buildDiagramNesting(root) { argument, newVariables ->
val newArguments = arguments + argument
recursive(index + 1, extension, newArguments, variables + newVariables)
}
} }
} }
} }
return if (extensionRoot != null) {
buildDiagramNesting(extensionRoot) { extension, newVariables ->
recursive(0, extension, emptyList(), newVariables)
}
} else {
recursive(0, null, emptyList(), emptyList())
}
} }
private fun DeclarationIrBuilder.buildMessagePrefix( private fun DeclarationIrBuilder.buildMessagePrefix(
@@ -204,23 +214,32 @@ class PowerAssertCallTransformer(
val possible = (context.referenceFunctions(function.kotlinFqName) + parentClassFunctions) val possible = (context.referenceFunctions(function.kotlinFqName) + parentClassFunctions)
.distinct() .distinct()
return possible return possible.mapNotNull { overload ->
.mapNotNull { overload -> // Dispatch receivers must always match exactly
val parameters = overload.owner.valueParameters if (function.dispatchReceiverParameter != overload.owner.dispatchReceiverParameter) {
if (parameters.size !in values.size..values.size + 1) return@mapNotNull null 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
}
} }
// 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 = private fun isStringFunction(type: IrType): Boolean =
@@ -238,10 +257,14 @@ class PowerAssertCallTransformer(
private fun isStringSupertype(type: IrType): Boolean = private fun isStringSupertype(type: IrType): Boolean =
context.irBuiltIns.stringType.isSubtypeOf(type, irTypeSystemContext) context.irBuiltIns.stringType.isSubtypeOf(type, irTypeSystemContext)
private fun IrType.isAssignableTo(type: IrType): Boolean { private fun IrType?.isAssignableTo(type: IrType?): Boolean {
if (isSubtypeOf(type, irTypeSystemContext)) return true if (this != null && type != null) {
val superTypes = (type.classifierOrNull as? IrTypeParameterSymbol)?.owner?.superTypes if (isSubtypeOf(type, irTypeSystemContext)) return true
return superTypes != null && superTypes.all { isSubtypeOf(it, irTypeSystemContext) } 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) { private fun MessageCollector.info(expression: IrElement, message: String) {
@@ -253,12 +276,6 @@ class PowerAssertCallTransformer(
} }
private fun MessageCollector.report(expression: IrElement, severity: CompilerMessageSeverity, message: String) { private fun MessageCollector.report(expression: IrElement, severity: CompilerMessageSeverity, message: String) {
report(severity, message, expression.toCompilerMessageLocation()) report(severity, message, sourceFile.getCompilerMessageLocation(expression))
}
private fun IrElement.toCompilerMessageLocation(): CompilerMessageLocation {
val info = file.info(this)
val lineContent = fileSource.substring(this)
return CompilerMessageLocation.create(file.path, info.startLineNumber, info.startColumnNumber, lineContent)!!
} }
} }
@@ -16,13 +16,12 @@
package com.bnorm.power 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.IrGenerationExtension
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.ir.declarations.path
import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.FqName
import java.io.File
class PowerAssertIrGenerationExtension( class PowerAssertIrGenerationExtension(
private val messageCollector: MessageCollector, private val messageCollector: MessageCollector,
@@ -30,10 +29,7 @@ class PowerAssertIrGenerationExtension(
) : IrGenerationExtension { ) : IrGenerationExtension {
override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
for (file in moduleFragment.files) { for (file in moduleFragment.files) {
val fileSource = File(file.path).readText() PowerAssertCallTransformer(SourceFile(file), pluginContext, messageCollector, functions)
.replace("\r\n", "\n") // https://youtrack.jetbrains.com/issue/KT-41888
PowerAssertCallTransformer(file, fileSource, pluginContext, messageCollector, functions)
.visitFile(file) .visitFile(file)
} }
} }
@@ -33,26 +33,28 @@ interface FunctionDelegate {
fun buildCall( fun buildCall(
builder: IrBuilderWithScope, builder: IrBuilderWithScope,
original: IrCall, original: IrCall,
arguments: List<IrExpression?>, extensionReceiver: IrExpression?,
message: IrExpression valueArguments: List<IrExpression?>,
messageArgument: IrExpression
): IrExpression ): IrExpression
fun IrBuilderWithScope.irCallCopy( fun IrBuilderWithScope.irCallCopy(
overload: IrSimpleFunctionSymbol, overload: IrSimpleFunctionSymbol,
original: IrCall, original: IrCall,
arguments: List<IrExpression?>, extensionReceiver: IrExpression?,
expression: IrExpression valueArguments: List<IrExpression?>,
messageArgument: IrExpression
): IrExpression { ): IrExpression {
return irCall(overload, type = original.type).apply { return irCall(overload, type = original.type).apply {
dispatchReceiver = original.dispatchReceiver?.deepCopyWithSymbols(parent) dispatchReceiver = original.dispatchReceiver?.deepCopyWithSymbols(parent)
extensionReceiver = original.extensionReceiver?.deepCopyWithSymbols(parent) this.extensionReceiver = (extensionReceiver ?: original.extensionReceiver)?.deepCopyWithSymbols(parent)
for (i in 0 until original.typeArgumentsCount) { for (i in 0 until original.typeArgumentsCount) {
putTypeArgument(i, original.getTypeArgument(i)) putTypeArgument(i, original.getTypeArgument(i))
} }
for ((i, argument) in arguments.withIndex()) { for ((i, argument) in valueArguments.withIndex()) {
putValueArgument(i, argument?.deepCopyWithSymbols(parent)) putValueArgument(i, argument?.deepCopyWithSymbols(parent))
} }
putValueArgument(arguments.size, expression.deepCopyWithSymbols(parent)) putValueArgument(valueArguments.size, messageArgument.deepCopyWithSymbols(parent))
} }
} }
} }
@@ -33,12 +33,13 @@ class LambdaFunctionDelegate(
override fun buildCall( override fun buildCall(
builder: IrBuilderWithScope, builder: IrBuilderWithScope,
original: IrCall, original: IrCall,
arguments: List<IrExpression?>, extensionReceiver: IrExpression?,
message: IrExpression valueArguments: List<IrExpression?>,
messageArgument: IrExpression
): IrExpression = with(builder) { ): IrExpression = with(builder) {
val expression = irLambda(context.irBuiltIns.stringType, messageParameter.type) { val expression = irLambda(context.irBuiltIns.stringType, messageParameter.type) {
+irReturn(message) +irReturn(messageArgument)
} }
irCallCopy(overload, original, arguments, expression) irCallCopy(overload, original, extensionReceiver, valueArguments, expression)
} }
} }
@@ -19,6 +19,7 @@ package com.bnorm.power.delegate
import com.bnorm.power.irLambda import com.bnorm.power.irLambda
import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope
import org.jetbrains.kotlin.ir.builders.irReturn import org.jetbrains.kotlin.ir.builders.irReturn
import org.jetbrains.kotlin.ir.builders.irSamConversion
import org.jetbrains.kotlin.ir.builders.typeOperator import org.jetbrains.kotlin.ir.builders.typeOperator
import org.jetbrains.kotlin.ir.declarations.IrValueParameter import org.jetbrains.kotlin.ir.declarations.IrValueParameter
import org.jetbrains.kotlin.ir.expressions.IrCall import org.jetbrains.kotlin.ir.expressions.IrCall
@@ -35,13 +36,14 @@ class SamConversionLambdaFunctionDelegate(
override fun buildCall( override fun buildCall(
builder: IrBuilderWithScope, builder: IrBuilderWithScope,
original: IrCall, original: IrCall,
arguments: List<IrExpression?>, extensionReceiver: IrExpression?,
message: IrExpression valueArguments: List<IrExpression?>,
messageArgument: IrExpression
): IrExpression = with(builder) { ): IrExpression = with(builder) {
val lambda = irLambda(context.irBuiltIns.stringType, messageParameter.type) { val lambda = irLambda(context.irBuiltIns.stringType, messageParameter.type) {
+irReturn(message) +irReturn(messageArgument)
} }
val expression = typeOperator(messageParameter.type, lambda, IrTypeOperator.SAM_CONVERSION, messageParameter.type) val expression = irSamConversion(lambda, messageParameter.type)
irCallCopy(overload, original, arguments, expression) irCallCopy(overload, original, extensionReceiver, valueArguments, expression)
} }
} }
@@ -31,7 +31,8 @@ class SimpleFunctionDelegate(
override fun buildCall( override fun buildCall(
builder: IrBuilderWithScope, builder: IrBuilderWithScope,
original: IrCall, original: IrCall,
arguments: List<IrExpression?>, extensionReceiver: IrExpression?,
message: IrExpression valueArguments: List<IrExpression?>,
): IrExpression = builder.irCallCopy(overload, original, arguments, message) messageArgument: IrExpression
): IrExpression = builder.irCallCopy(overload, original, extensionReceiver, valueArguments, messageArgument)
} }
@@ -18,14 +18,13 @@ package com.bnorm.power.diagram
import com.bnorm.power.irString import com.bnorm.power.irString
import org.jetbrains.kotlin.ir.IrBuiltIns import org.jetbrains.kotlin.ir.IrBuiltIns
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.SourceRangeInfo import org.jetbrains.kotlin.ir.SourceRangeInfo
import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope
import org.jetbrains.kotlin.ir.builders.irConcat import org.jetbrains.kotlin.ir.builders.irConcat
import org.jetbrains.kotlin.ir.builders.irGet import org.jetbrains.kotlin.ir.builders.irGet
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.declarations.IrVariable 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.IrConst
import org.jetbrains.kotlin.ir.expressions.IrConstKind import org.jetbrains.kotlin.ir.expressions.IrConstKind
import org.jetbrains.kotlin.ir.expressions.IrExpression import org.jetbrains.kotlin.ir.expressions.IrExpression
@@ -36,20 +35,18 @@ import org.jetbrains.kotlin.ir.expressions.IrTypeOperatorCall
import org.jetbrains.kotlin.ir.expressions.addArgument import org.jetbrains.kotlin.ir.expressions.addArgument
fun IrBuilderWithScope.irDiagramString( fun IrBuilderWithScope.irDiagramString(
file: IrFile, sourceFile: SourceFile,
fileSource: String,
prefix: IrExpression? = null, prefix: IrExpression? = null,
original: IrExpression, call: IrCall,
variables: List<IrTemporaryVariable> variables: List<IrTemporaryVariable>
): IrExpression { ): IrExpression {
val callInfo = sourceFile.getSourceRangeInfo(call)
val callIndent = callInfo.startColumnNumber
val originalInfo = file.info(original) val stackValues = variables.map { it.toValueDisplay(sourceFile, callIndent, callInfo) }
val callIndent = originalInfo.startColumnNumber
val stackValues = variables.map { it.toValueDisplay(fileSource, callIndent, file, originalInfo) }
val valuesByRow = stackValues.groupBy { it.row } val valuesByRow = stackValues.groupBy { it.row }
val rows = fileSource.substring(original) val rows = sourceFile.getText(callInfo)
.replace("\n" + " ".repeat(callIndent), "\n") // Remove additional indentation .replace("\n" + " ".repeat(callIndent), "\n") // Remove additional indentation
.split("\n") .split("\n")
@@ -102,19 +99,17 @@ private data class ValueDisplay(
) )
private fun IrTemporaryVariable.toValueDisplay( private fun IrTemporaryVariable.toValueDisplay(
fileSource: String, fileSource: SourceFile,
callIndent: Int, callIndent: Int,
file: IrFile,
originalInfo: SourceRangeInfo originalInfo: SourceRangeInfo
): ValueDisplay { ): ValueDisplay {
val source = fileSource.substring(original) val info = fileSource.getSourceRangeInfo(original)
.replace("\n" + " ".repeat(callIndent), "\n") // Remove additional indentation
val info = file.info(original)
var indent = info.startColumnNumber - callIndent var indent = info.startColumnNumber - callIndent
var row = info.startLineNumber - originalInfo.startLineNumber var row = info.startLineNumber - originalInfo.startLineNumber
val columnOffset = findDisplayOffset(original, source) 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 prefix = source.substring(0, columnOffset)
val rowShift = prefix.count { it == '\n' } val rowShift = prefix.count { it == '\n' }
@@ -161,17 +156,19 @@ private fun IrTemporaryVariable.toValueDisplay(
* ``` * ```
*/ */
private fun findDisplayOffset( private fun findDisplayOffset(
sourceFile: SourceFile,
expression: IrExpression, expression: IrExpression,
source: String source: String
): Int { ): Int {
return when (expression) { return when (expression) {
is IrMemberAccessExpression<*> -> memberAccessOffset(expression, source) is IrMemberAccessExpression<*> -> memberAccessOffset(sourceFile, expression, source)
is IrTypeOperatorCall -> typeOperatorOffset(expression, source) is IrTypeOperatorCall -> typeOperatorOffset(expression, source)
else -> 0 else -> 0
} }
} }
private fun memberAccessOffset( private fun memberAccessOffset(
sourceFile: SourceFile,
expression: IrMemberAccessExpression<*>, expression: IrMemberAccessExpression<*>,
source: String source: String
): Int { ): Int {
@@ -193,12 +190,12 @@ private fun memberAccessOffset(
?: expression.extensionReceiver ?: expression.extensionReceiver
?: expression.getValueArgument(0).takeIf { owner.origin == IrBuiltIns.BUILTIN_OPERATOR } ?: expression.getValueArgument(0).takeIf { owner.origin == IrBuiltIns.BUILTIN_OPERATOR }
?: return 0 ?: return 0
var offset = receiver.endOffset - expression.startOffset 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 (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 // Continue until there is a non-whitespace character
while (source[offset].isWhitespace()) { while (source[offset].isWhitespace() || source[offset] == '.') {
offset++ offset++
if (offset >= source.length) return 0 if (offset >= source.length) return 0
} }
@@ -219,9 +216,6 @@ private fun typeOperatorOffset(
} }
} }
fun String.substring(expression: IrElement) = substring(expression.startOffset, expression.endOffset)
fun IrFile.info(expression: IrElement) = fileEntry.getSourceRangeInfo(expression.startOffset, expression.endOffset)
fun StringBuilder.indent(indentation: Int): StringBuilder { fun StringBuilder.indent(indentation: Int): StringBuilder {
repeat(indentation) { append(" ") } repeat(indentation) { append(" ") }
return this return this
@@ -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)!!
}
}
@@ -26,7 +26,7 @@ import kotlin.test.fail
class InfixFunctionTest { class InfixFunctionTest {
@Test @Test
fun `infix functions include receiver`() { fun `infix function call includes receiver`() {
val actual = execute( val actual = execute(
""" """
(1 + 1) mustEqual (2 + 4) (1 + 1) mustEqual (2 + 4)
@@ -43,6 +43,122 @@ class InfixFunctionTest {
) )
} }
@Test
fun `infix function call with constant receiver`() {
val actual = execute(
"""
1 mustEqual (2 + 4)
""".trimIndent()
)
assertEquals(
"""
1 mustEqual (2 + 4)
|
6
""".trimIndent(),
actual.trim()
)
}
@Test
fun `infix function call with constant parameter`() {
val actual = execute(
"""
(1 + 1) mustEqual 6
""".trimIndent()
)
assertEquals(
"""
(1 + 1) mustEqual 6
|
2
""".trimIndent(),
actual.trim()
)
}
@Test
fun `infix function call with only constants`() {
val actual = execute(
"""
2 mustEqual 6
""".trimIndent()
)
assertEquals(
"""
Assertion failed
""".trimIndent(),
actual.trim()
)
}
@Test
fun `non-infix function call includes receiver`() {
val actual = execute(
"""
(1 + 1).mustEqual(2 + 4)
""".trimIndent()
)
assertEquals(
"""
(1 + 1).mustEqual(2 + 4)
| |
| 6
2
""".trimIndent(),
actual.trim()
)
}
@Test
fun `non-infix function call with constant receiver`() {
val actual = execute(
"""
1.mustEqual(2 + 4)
""".trimIndent()
)
assertEquals(
"""
1.mustEqual(2 + 4)
|
6
""".trimIndent(),
actual.trim()
)
}
@Test
fun `non-infix function call with constant parameter`() {
val actual = execute(
"""
(1 + 1).mustEqual(6)
""".trimIndent()
)
assertEquals(
"""
(1 + 1).mustEqual(6)
|
2
""".trimIndent(),
actual.trim()
)
}
@Test
fun `non-infix function call with only constants`() {
val actual = execute(
"""
2.mustEqual(6)
""".trimIndent()
)
assertEquals(
"""
Assertion failed
""".trimIndent(),
actual.trim()
)
}
private fun execute(mainBody: String): String { private fun execute(mainBody: String): String {
val file = SourceFile.kotlin( val file = SourceFile.kotlin(
name = "main.kt", name = "main.kt",