[PowerAssert] Find the earliest starting offset for receiver expression

Calls of infix functions have a start offset which doesn't include the
receiver. Property accessors have a start offset at the start of the
property name and do not include their receiver expression. This
combination can cause problems when the entire expression needs to be
displayed, including the entirety of a complex receiver. Make sure to
navigate expressions to find their earliest starting offset and use that
instead.

^KT-65810 Fixed
This commit is contained in:
Brian Norman
2024-02-26 11:59:36 -06:00
committed by Space Team
parent d53d5dddc5
commit 721d02f4f3
10 changed files with 77 additions and 51 deletions
@@ -21,6 +21,7 @@ package org.jetbrains.kotlin.powerassert
import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.builders.* import org.jetbrains.kotlin.ir.builders.*
import org.jetbrains.kotlin.ir.builders.declarations.buildFun import org.jetbrains.kotlin.ir.builders.declarations.buildFun
import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
@@ -28,6 +29,8 @@ import org.jetbrains.kotlin.ir.expressions.IrFunctionExpression
import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin
import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionExpressionImpl import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionExpressionImpl
import org.jetbrains.kotlin.ir.types.IrType import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.name.Name
fun IrBuilderWithScope.irString(builderAction: StringBuilder.() -> Unit) = fun IrBuilderWithScope.irString(builderAction: StringBuilder.() -> Unit) =
@@ -57,3 +60,17 @@ fun IrBuilderWithScope.irLambda(
} }
return IrFunctionExpressionImpl(startOffset, endOffset, lambdaType, lambda, IrStatementOrigin.LAMBDA) return IrFunctionExpressionImpl(startOffset, endOffset, lambdaType, lambda, IrStatementOrigin.LAMBDA)
} }
val IrElement.earliestStartOffset: Int
get() {
var offset = startOffset
this.acceptChildrenVoid(
object : IrElementVisitorVoid {
override fun visitElement(element: IrElement) {
if (element.startOffset < offset) offset = element.startOffset
element.acceptChildrenVoid(this)
}
},
)
return offset
}
@@ -143,7 +143,7 @@ class PowerAssertCallTransformer(
val newArguments = arguments + call.getValueArgument(index) val newArguments = arguments + call.getValueArgument(index)
return recursive(index + 1, dispatch, extension, newArguments, variables) return recursive(index + 1, dispatch, extension, newArguments, variables)
} else { } else {
return buildDiagramNesting(root, variables) { argument, newVariables -> return buildDiagramNesting(sourceFile, root, variables) { argument, newVariables ->
val newArguments = arguments + argument val newArguments = arguments + argument
recursive(index + 1, dispatch, extension, newArguments, newVariables) recursive(index + 1, dispatch, extension, newArguments, newVariables)
} }
@@ -151,8 +151,8 @@ class PowerAssertCallTransformer(
} }
} }
return buildDiagramNestingNullable(dispatchRoot) { dispatch, dispatchNewVariables -> return buildDiagramNestingNullable(sourceFile, dispatchRoot) { dispatch, dispatchNewVariables ->
buildDiagramNestingNullable(extensionRoot, dispatchNewVariables) { extension, extensionNewVariables -> buildDiagramNestingNullable(sourceFile, extensionRoot, dispatchNewVariables) { extension, extensionNewVariables ->
recursive(0, dispatch, extension, emptyList(), extensionNewVariables) recursive(0, dispatch, extension, emptyList(), extensionNewVariables)
} }
} }
@@ -24,31 +24,34 @@ import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.util.deepCopyWithSymbols import org.jetbrains.kotlin.ir.util.deepCopyWithSymbols
fun IrBuilderWithScope.buildDiagramNesting( fun IrBuilderWithScope.buildDiagramNesting(
sourceFile: SourceFile,
root: Node, root: Node,
variables: List<IrTemporaryVariable> = emptyList(), variables: List<IrTemporaryVariable> = emptyList(),
call: IrBuilderWithScope.(IrExpression, List<IrTemporaryVariable>) -> IrExpression, call: IrBuilderWithScope.(IrExpression, List<IrTemporaryVariable>) -> IrExpression,
): IrExpression { ): IrExpression {
return buildExpression(root, variables) { argument, subStack -> return buildExpression(sourceFile, root, variables) { argument, subStack ->
call(argument, subStack) call(argument, subStack)
} }
} }
fun IrBuilderWithScope.buildDiagramNestingNullable( fun IrBuilderWithScope.buildDiagramNestingNullable(
sourceFile: SourceFile,
root: Node?, root: Node?,
variables: List<IrTemporaryVariable> = emptyList(), variables: List<IrTemporaryVariable> = emptyList(),
call: IrBuilderWithScope.(IrExpression?, List<IrTemporaryVariable>) -> IrExpression, call: IrBuilderWithScope.(IrExpression?, List<IrTemporaryVariable>) -> IrExpression,
): IrExpression { ): IrExpression {
return if (root != null) buildDiagramNesting(root, variables, call) else call(null, variables) return if (root != null) buildDiagramNesting(sourceFile, root, variables, call) else call(null, variables)
} }
private fun IrBuilderWithScope.buildExpression( private fun IrBuilderWithScope.buildExpression(
sourceFile: SourceFile,
node: Node, node: Node,
variables: List<IrTemporaryVariable>, variables: List<IrTemporaryVariable>,
call: IrBuilderWithScope.(IrExpression, List<IrTemporaryVariable>) -> IrExpression, call: IrBuilderWithScope.(IrExpression, List<IrTemporaryVariable>) -> IrExpression,
): IrExpression = when (node) { ): IrExpression = when (node) {
is ExpressionNode -> add(node, variables, call) is ExpressionNode -> add(sourceFile, node, variables, call)
is AndNode -> nest(node, 0, variables, call) is AndNode -> nest(sourceFile, node, 0, variables, call)
is OrNode -> nest(node, 0, variables, call) is OrNode -> nest(sourceFile, node, 0, variables, call)
else -> TODO("Unknown node type=$node") else -> TODO("Unknown node type=$node")
} }
@@ -66,6 +69,7 @@ private fun IrBuilderWithScope.buildExpression(
* ``` * ```
*/ */
private fun IrBuilderWithScope.add( private fun IrBuilderWithScope.add(
sourceFile: SourceFile,
node: ExpressionNode, node: ExpressionNode,
variables: List<IrTemporaryVariable>, variables: List<IrTemporaryVariable>,
call: IrBuilderWithScope.(IrExpression, List<IrTemporaryVariable>) -> IrExpression, call: IrBuilderWithScope.(IrExpression, List<IrTemporaryVariable>) -> IrExpression,
@@ -73,7 +77,7 @@ private fun IrBuilderWithScope.add(
return irBlock { return irBlock {
val head = node.expressions.first().deepCopyWithSymbols(scope.getLocalDeclarationParent()) val head = node.expressions.first().deepCopyWithSymbols(scope.getLocalDeclarationParent())
val expressions = (buildTree(head) as ExpressionNode).expressions val expressions = (buildTree(head) as ExpressionNode).expressions
val transformer = IrTemporaryExtractionTransformer(this@irBlock, expressions.toSet()) val transformer = IrTemporaryExtractionTransformer(this@irBlock, expressions.toSet(), sourceFile)
val transformed = expressions.first().transform(transformer, null) val transformed = expressions.first().transform(transformer, null)
+call(transformed, variables + transformer.variables) +call(transformed, variables + transformer.variables)
} }
@@ -96,6 +100,7 @@ private fun IrBuilderWithScope.add(
* ``` * ```
*/ */
private fun IrBuilderWithScope.nest( private fun IrBuilderWithScope.nest(
sourceFile: SourceFile,
node: AndNode, node: AndNode,
index: Int, index: Int,
variables: List<IrTemporaryVariable>, variables: List<IrTemporaryVariable>,
@@ -103,14 +108,14 @@ private fun IrBuilderWithScope.nest(
): IrExpression { ): IrExpression {
val children = node.children val children = node.children
val child = children[index] val child = children[index]
return buildExpression(child, variables) { argument, newVariables -> return buildExpression(sourceFile, child, variables) { argument, newVariables ->
if (index + 1 == children.size) { if (index + 1 == children.size) {
call(argument, newVariables) // last expression, result is false call(argument, newVariables) // last expression, result is false
} else { } else {
irIfThenElse( irIfThenElse(
context.irBuiltIns.anyType, context.irBuiltIns.anyType,
argument, argument,
nest(node, index + 1, newVariables, call), // more expressions, continue nesting nest(sourceFile, node, index + 1, newVariables, call), // more expressions, continue nesting
call(irFalse(), newVariables), // short-circuit result to false call(irFalse(), newVariables), // short-circuit result to false
) )
} }
@@ -134,6 +139,7 @@ private fun IrBuilderWithScope.nest(
* ``` * ```
*/ */
private fun IrBuilderWithScope.nest( private fun IrBuilderWithScope.nest(
sourceFile: SourceFile,
node: OrNode, node: OrNode,
index: Int, index: Int,
variables: List<IrTemporaryVariable>, variables: List<IrTemporaryVariable>,
@@ -141,7 +147,7 @@ private fun IrBuilderWithScope.nest(
): IrExpression { ): IrExpression {
val children = node.children val children = node.children
val child = children[index] val child = children[index]
return buildExpression(child, variables) { argument, newVariables -> return buildExpression(sourceFile, child, variables) { argument, newVariables ->
if (index + 1 == children.size) { if (index + 1 == children.size) {
call(argument, newVariables) // last expression, result is false call(argument, newVariables) // last expression, result is false
} else { } else {
@@ -149,7 +155,7 @@ private fun IrBuilderWithScope.nest(
context.irBuiltIns.anyType, context.irBuiltIns.anyType,
argument, argument,
call(irTrue(), newVariables), // short-circuit result to true call(irTrue(), newVariables), // short-circuit result to true
nest(node, index + 1, newVariables, call), // more expressions, continue nesting nest(sourceFile, node, index + 1, newVariables, call), // more expressions, continue nesting
) )
} }
} }
@@ -38,7 +38,7 @@ fun IrBuilderWithScope.irDiagramString(
val callInfo = sourceFile.getSourceRangeInfo(call) val callInfo = sourceFile.getSourceRangeInfo(call)
val callIndent = callInfo.startColumnNumber val callIndent = callInfo.startColumnNumber
val stackValues = variables.map { it.toValueDisplay(sourceFile, callIndent, callInfo) } val stackValues = variables.map { it.toValueDisplay(callIndent, callInfo) }
val valuesByRow = stackValues.groupBy { it.row } val valuesByRow = stackValues.groupBy { it.row }
val rows = sourceFile.getText(callInfo) val rows = sourceFile.getText(callInfo)
@@ -94,17 +94,14 @@ private data class ValueDisplay(
) )
private fun IrTemporaryVariable.toValueDisplay( private fun IrTemporaryVariable.toValueDisplay(
fileSource: SourceFile,
callIndent: Int, callIndent: Int,
originalInfo: SourceRangeInfo, originalInfo: SourceRangeInfo,
): ValueDisplay { ): ValueDisplay {
val info = fileSource.getSourceRangeInfo(original) var indent = sourceRangeInfo.startColumnNumber - callIndent
var indent = info.startColumnNumber - callIndent var row = sourceRangeInfo.startLineNumber - originalInfo.startLineNumber
var row = info.startLineNumber - originalInfo.startLineNumber
val source = fileSource.getText(info) val source = text.replace("\n" + " ".repeat(callIndent), "\n") // Remove additional indentation
.replace("\n" + " ".repeat(callIndent), "\n") // Remove additional indentation val columnOffset = findDisplayOffset(original, sourceRangeInfo, source)
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' }
@@ -151,20 +148,20 @@ private fun IrTemporaryVariable.toValueDisplay(
* ``` * ```
*/ */
private fun findDisplayOffset( private fun findDisplayOffset(
sourceFile: SourceFile,
expression: IrExpression, expression: IrExpression,
sourceRangeInfo: SourceRangeInfo,
source: String, source: String,
): Int { ): Int {
return when (expression) { return when (expression) {
is IrMemberAccessExpression<*> -> memberAccessOffset(sourceFile, expression, source) is IrMemberAccessExpression<*> -> memberAccessOffset(expression, sourceRangeInfo, 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<*>,
sourceRangeInfo: SourceRangeInfo,
source: String, source: String,
): Int { ): Int {
when (expression.origin) { when (expression.origin) {
@@ -190,8 +187,7 @@ 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
val expressionInfo = sourceFile.getSourceRangeInfo(expression) var offset = receiver.endOffset - sourceRangeInfo.startOffset + 1
var offset = receiver.endOffset - expressionInfo.startOffset + 1
if (offset < 0 || offset >= source.length) return 0 // infix function called using non-infix syntax 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
@@ -19,6 +19,7 @@
package org.jetbrains.kotlin.powerassert.diagram package org.jetbrains.kotlin.powerassert.diagram
import org.jetbrains.kotlin.ir.SourceRangeInfo
import org.jetbrains.kotlin.ir.builders.IrStatementsBuilder import org.jetbrains.kotlin.ir.builders.IrStatementsBuilder
import org.jetbrains.kotlin.ir.builders.irGet import org.jetbrains.kotlin.ir.builders.irGet
import org.jetbrains.kotlin.ir.builders.irTemporary import org.jetbrains.kotlin.ir.builders.irTemporary
@@ -30,11 +31,14 @@ import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
data class IrTemporaryVariable( data class IrTemporaryVariable(
val temporary: IrVariable, val temporary: IrVariable,
val original: IrExpression, val original: IrExpression,
val sourceRangeInfo: SourceRangeInfo,
val text: String,
) )
class IrTemporaryExtractionTransformer( class IrTemporaryExtractionTransformer(
private val builder: IrStatementsBuilder<*>, private val builder: IrStatementsBuilder<*>,
private val transform: Set<IrExpression>, private val transform: Set<IrExpression>,
private val sourceFile: SourceFile,
) : IrElementTransformerVoid() { ) : IrElementTransformerVoid() {
private val _variables = mutableListOf<IrTemporaryVariable>() private val _variables = mutableListOf<IrTemporaryVariable>()
val variables: List<IrTemporaryVariable> = _variables val variables: List<IrTemporaryVariable> = _variables
@@ -43,7 +47,9 @@ class IrTemporaryExtractionTransformer(
return if (expression in transform) { return if (expression in transform) {
val copy = expression.deepCopyWithSymbols(builder.scope.getLocalDeclarationParent()) val copy = expression.deepCopyWithSymbols(builder.scope.getLocalDeclarationParent())
val variable = builder.irTemporary(super.visitExpression(expression)) val variable = builder.irTemporary(super.visitExpression(expression))
_variables.add(IrTemporaryVariable(variable, copy)) val sourceRangeInfo = sourceFile.getSourceRangeInfo(copy)
val text = sourceFile.getText(sourceRangeInfo)
_variables.add(IrTemporaryVariable(variable, copy, sourceRangeInfo, text))
builder.irGet(variable) builder.irGet(variable)
} else { } else {
super.visitExpression(expression) super.visitExpression(expression)
@@ -25,6 +25,7 @@ import org.jetbrains.kotlin.ir.SourceRangeInfo
import org.jetbrains.kotlin.ir.declarations.IrFile import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.declarations.path 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.powerassert.earliestStartOffset
import java.io.File import java.io.File
data class SourceFile( data class SourceFile(
@@ -59,14 +60,14 @@ data class SourceFile(
val receiver = element.extensionReceiver ?: element.dispatchReceiver val receiver = element.extensionReceiver ?: element.dispatchReceiver
if (element.symbol.owner.isInfix && receiver != null) { if (element.symbol.owner.isInfix && receiver != null) {
// When an infix function is called *not* with infix notation, the startOffset will not include the receiver. // 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 // Force the range to include the receiver, so it is always present.
range = receiver.startOffset..element.endOffset range = element.earliestStartOffset..element.endOffset
// The offsets of the receiver will *not* include surrounding parentheses so these need to be checked for // The offsets of the receiver will *not* include surrounding parentheses so these need to be checked for
// manually. // manually.
val substring = safeSubstring(receiver.startOffset - 1, receiver.endOffset + 1) val substring = safeSubstring(range.first - 1, receiver.endOffset + 1)
if (substring.startsWith('(') && substring.endsWith(')')) { if (substring.startsWith('(') && substring.endsWith(')')) {
range = receiver.startOffset - 1..element.endOffset range = (range.first - 1)..range.last
} }
} }
} }
@@ -1,6 +1,6 @@
wrapper mustEqual "world".length Complex.holder.wrapper mustEqual "world".length
| | | | | | |
| | 5 | | | 5
| Wrapper | | Wrapper
Holder(wrapper=Wrapper) | Holder(wrapper=Wrapper)
Complex Complex
@@ -1,6 +1,6 @@
wrapper.mustEqual("world".length) Complex.holder.wrapper.mustEqual("world".length)
| | | | | | |
| | 5 | | | 5
| Wrapper | | Wrapper
Holder(wrapper=Wrapper) | Holder(wrapper=Wrapper)
Complex Complex
@@ -1,5 +1,5 @@
length mustEqual "world".length "hello".substring(1, 4).length mustEqual "world".length
| | | | |
| 5 | | 5
3 | 3
ell ell
@@ -1,5 +1,5 @@
length.mustEqual("world".length) "hello".substring(1, 4).length.mustEqual("world".length)
| | | | |
| 5 | | 5
3 | 3
ell ell