[PowerAssert] Correctly align infix calls for built-in operators
Instead of searching for the operator in the string representation of
the whole expression, consider the operator's start to be the
first non-whitespace non-dot character _after_ the LHS of the infix
expression.
This fixes cases like this:
```
assert("Name in " in listOf("Hello", "World"))
| |
| [Hello, World]
false
```
^KT-66208 Fixed
This commit is contained in:
committed by
Space Team
parent
7d22825176
commit
54c58671fb
+7
-2
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
package org.jetbrains.kotlin.powerassert.diagram
|
package org.jetbrains.kotlin.powerassert.diagram
|
||||||
|
|
||||||
|
import org.jetbrains.kotlin.ir.BuiltInOperatorNames
|
||||||
import org.jetbrains.kotlin.ir.IrElement
|
import org.jetbrains.kotlin.ir.IrElement
|
||||||
import org.jetbrains.kotlin.ir.expressions.*
|
import org.jetbrains.kotlin.ir.expressions.*
|
||||||
import org.jetbrains.kotlin.ir.util.dumpKotlinLike
|
import org.jetbrains.kotlin.ir.util.dumpKotlinLike
|
||||||
@@ -105,8 +106,12 @@ fun buildTree(expression: IrExpression): Node? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun visitCall(expression: IrCall, data: Node) {
|
override fun visitCall(expression: IrCall, data: Node) {
|
||||||
if (expression.symbol.owner.name.asString() == "EQEQ" && expression.origin == IrStatementOrigin.EXCLEQ) {
|
val isExcleq = expression.symbol.owner.name.asString() == BuiltInOperatorNames.EQEQ
|
||||||
// Skip the EQEQ part of a EXCLEQ call
|
&& expression.origin == IrStatementOrigin.EXCLEQ
|
||||||
|
val isExcleqeq = expression.symbol.owner.name.asString() == BuiltInOperatorNames.EQEQEQ
|
||||||
|
&& expression.origin == IrStatementOrigin.EXCLEQEQ
|
||||||
|
if (isExcleq || isExcleqeq) {
|
||||||
|
// Skip the EQEQ/EQEQEQ part of a EXCLEQ/EXCLEQEQ call
|
||||||
expression.acceptChildren(this, data)
|
expression.acceptChildren(this, data)
|
||||||
} else if (expression.origin == IrStatementOrigin.NOT_IN) {
|
} else if (expression.origin == IrStatementOrigin.NOT_IN) {
|
||||||
// Exclude the wrapped "contains" call for `!in` operator expressions and only display the final result
|
// Exclude the wrapped "contains" call for `!in` operator expressions and only display the final result
|
||||||
|
|||||||
+63
-31
@@ -154,7 +154,7 @@ private fun findDisplayOffset(
|
|||||||
): Int {
|
): Int {
|
||||||
return when (expression) {
|
return when (expression) {
|
||||||
is IrMemberAccessExpression<*> -> memberAccessOffset(expression, sourceRangeInfo, source)
|
is IrMemberAccessExpression<*> -> memberAccessOffset(expression, sourceRangeInfo, source)
|
||||||
is IrTypeOperatorCall -> typeOperatorOffset(expression, source)
|
is IrTypeOperatorCall -> typeOperatorOffset(expression, sourceRangeInfo, source)
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,38 +164,12 @@ private fun memberAccessOffset(
|
|||||||
sourceRangeInfo: SourceRangeInfo,
|
sourceRangeInfo: SourceRangeInfo,
|
||||||
source: String,
|
source: String,
|
||||||
): Int {
|
): 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
|
val owner = expression.symbol.owner
|
||||||
if (owner !is IrSimpleFunction) return 0
|
if (owner !is IrSimpleFunction) return 0
|
||||||
|
|
||||||
if (owner.isInfix || owner.isOperator || owner.origin == IrBuiltIns.BUILTIN_OPERATOR) {
|
if (owner.isInfix || owner.isOperator || owner.origin == IrBuiltIns.BUILTIN_OPERATOR) {
|
||||||
// Ignore single value operators
|
val lhs = expression.binaryOperatorLhs() ?: return 0
|
||||||
val singleReceiver = (expression.dispatchReceiver != null) xor (expression.extensionReceiver != null)
|
return binaryOperatorOffset(lhs, sourceRangeInfo, source)
|
||||||
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
|
|
||||||
var offset = receiver.endOffset - sourceRangeInfo.startOffset + 1
|
|
||||||
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
|
return 0
|
||||||
@@ -203,15 +177,73 @@ private fun memberAccessOffset(
|
|||||||
|
|
||||||
private fun typeOperatorOffset(
|
private fun typeOperatorOffset(
|
||||||
expression: IrTypeOperatorCall,
|
expression: IrTypeOperatorCall,
|
||||||
|
sourceRangeInfo: SourceRangeInfo,
|
||||||
source: String,
|
source: String,
|
||||||
): Int {
|
): Int {
|
||||||
return when (expression.operator) {
|
return when (expression.operator) {
|
||||||
IrTypeOperator.INSTANCEOF -> source.indexOf(" is ") + 1
|
IrTypeOperator.INSTANCEOF, IrTypeOperator.NOT_INSTANCEOF -> binaryOperatorOffset(expression.argument, sourceRangeInfo, source)
|
||||||
IrTypeOperator.NOT_INSTANCEOF -> source.indexOf(" !is ") + 1
|
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The offset of the infix operator/function itself.
|
||||||
|
*
|
||||||
|
* @param lhs The left-hand side expression of the operator.
|
||||||
|
* @param wholeOperatorSourceRangeInfo The source range of the whole operator expression.
|
||||||
|
* @param wholeOperatorSource The source text of the whole operator expression.
|
||||||
|
*/
|
||||||
|
private fun binaryOperatorOffset(lhs: IrExpression, wholeOperatorSourceRangeInfo: SourceRangeInfo, wholeOperatorSource: String): Int {
|
||||||
|
var offset = lhs.endOffset - wholeOperatorSourceRangeInfo.startOffset + 1
|
||||||
|
if (offset < 0 || offset >= wholeOperatorSource.length) return 0 // infix function called using non-infix syntax
|
||||||
|
|
||||||
|
// Continue until there is a non-whitespace character
|
||||||
|
while (wholeOperatorSource[offset].isWhitespace() || wholeOperatorSource[offset] == '.') {
|
||||||
|
offset++
|
||||||
|
if (offset >= wholeOperatorSource.length) return 0
|
||||||
|
}
|
||||||
|
return offset
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The left-hand side expression of an infix operator/function that takes into account special cases like `in`, `!in` and `!=` operators
|
||||||
|
* that have a more complex structure than just a single call with two arguments.
|
||||||
|
*/
|
||||||
|
private fun IrMemberAccessExpression<*>.binaryOperatorLhs(): IrExpression? = when (origin) {
|
||||||
|
IrStatementOrigin.EXCLEQ -> {
|
||||||
|
// The `!=` operator call is actually a sugar for `lhs.equals(rhs).not()`.
|
||||||
|
(dispatchReceiver as? IrCall)?.simpleBinaryOperatorLhs()
|
||||||
|
}
|
||||||
|
IrStatementOrigin.EXCLEQEQ -> {
|
||||||
|
// The `!==` operator call is actually a sugar for `(lhs === rhs).not()`.
|
||||||
|
(dispatchReceiver as? IrCall)?.simpleBinaryOperatorLhs()
|
||||||
|
}
|
||||||
|
IrStatementOrigin.IN -> {
|
||||||
|
// The `in` operator call is actually a sugar for `rhs.contains(lhs)`.
|
||||||
|
getValueArgument(0)
|
||||||
|
}
|
||||||
|
IrStatementOrigin.NOT_IN -> {
|
||||||
|
// The `!in` operator call is actually a sugar for `rhs.contains(lhs).not()`.
|
||||||
|
(dispatchReceiver as? IrCall)?.getValueArgument(0)
|
||||||
|
}
|
||||||
|
else -> simpleBinaryOperatorLhs()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The left-hand side expression of an infix operator/function.
|
||||||
|
* For single-value operators returns `null`, for all other infix operators/functions, returns the receiver or the first value argument.
|
||||||
|
*/
|
||||||
|
private fun IrMemberAccessExpression<*>.simpleBinaryOperatorLhs(): IrExpression? {
|
||||||
|
val singleReceiver = (dispatchReceiver != null) xor (extensionReceiver != null)
|
||||||
|
return if (singleReceiver && valueArgumentsCount == 0) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
dispatchReceiver
|
||||||
|
?: extensionReceiver
|
||||||
|
?: getValueArgument(0).takeIf { (symbol.owner as? IrSimpleFunction)?.origin == IrBuiltIns.BUILTIN_OPERATOR }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun StringBuilder.indent(indentation: Int): StringBuilder {
|
fun StringBuilder.indent(indentation: Int): StringBuilder {
|
||||||
repeat(indentation) { append(" ") }
|
repeat(indentation) { append(" ") }
|
||||||
return this
|
return this
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ assert(null is String)
|
|||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(!(" is " is String))
|
assert(!(" is " is String))
|
||||||
| |
|
| |
|
||||||
| true
|
| true
|
||||||
false
|
false
|
||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
@@ -14,22 +14,22 @@ assert(!(
|
|||||||
|
|
|
|
||||||
false
|
false
|
||||||
" is "
|
" is "
|
||||||
|
|
|
||||||
true
|
|
||||||
|
|
||||||
is
|
is
|
||||||
|
|
|
||||||
|
true
|
||||||
|
|
||||||
String
|
String
|
||||||
))
|
))
|
||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(null/*is*/is/*is*/String)
|
assert(null/*is*/is/*is*/String)
|
||||||
|
|
|
|
||||||
false
|
false
|
||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(!((null is String) is Boolean))
|
assert(!((null is String) is Boolean))
|
||||||
| |
|
| | |
|
||||||
| true
|
| | true
|
||||||
| false
|
| false
|
||||||
false
|
false
|
||||||
@@ -5,27 +5,27 @@ assert("Hello, world!" !is String)
|
|||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(" !is " !is String)
|
assert(" !is " !is String)
|
||||||
|
|
|
|
||||||
false
|
false
|
||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(
|
assert(
|
||||||
" !is "
|
" !is "
|
||||||
|
|
|
||||||
false
|
|
||||||
|
|
||||||
!is
|
!is
|
||||||
|
|
|
||||||
|
false
|
||||||
|
|
||||||
String
|
String
|
||||||
)
|
)
|
||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert("Hello, world!"/*!is*/!is/*!is*/String)
|
assert("Hello, world!"/*!is*/!is/*!is*/String)
|
||||||
|
|
|
|
||||||
false
|
false
|
||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(("Hello, world!" !is String) !is Boolean)
|
assert(("Hello, world!" !is String) !is Boolean)
|
||||||
|
|
| |
|
||||||
false
|
| false
|
||||||
false
|
false
|
||||||
@@ -6,17 +6,17 @@ assert("Name" in listOf("Hello", "World"))
|
|||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(" in " in listOf("Hello", "World"))
|
assert(" in " in listOf("Hello", "World"))
|
||||||
| |
|
| |
|
||||||
| [Hello, World]
|
| [Hello, World]
|
||||||
false
|
false
|
||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(
|
assert(
|
||||||
" in "
|
" in "
|
||||||
|
|
|
||||||
false
|
|
||||||
|
|
||||||
in
|
in
|
||||||
|
|
|
||||||
|
false
|
||||||
|
|
||||||
listOf("Hello", "World")
|
listOf("Hello", "World")
|
||||||
|
|
|
|
||||||
@@ -25,14 +25,14 @@ assert(
|
|||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert("Name"/*in*/in/*in*/listOf("Hello", "World"))
|
assert("Name"/*in*/in/*in*/listOf("Hello", "World"))
|
||||||
| |
|
| |
|
||||||
| [Hello, World]
|
| [Hello, World]
|
||||||
false
|
false
|
||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(("Name" in listOf("Hello", "World")) in listOf(true))
|
assert(("Name" in listOf("Hello", "World")) in listOf(true))
|
||||||
| | |
|
| | | |
|
||||||
| | [true]
|
| | | [true]
|
||||||
|
| | false
|
||||||
| [Hello, World]
|
| [Hello, World]
|
||||||
false
|
|
||||||
false
|
false
|
||||||
@@ -5,27 +5,27 @@ assert(1 != 1)
|
|||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(" != " != " != ")
|
assert(" != " != " != ")
|
||||||
|
|
|
|
||||||
false
|
false
|
||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(
|
assert(
|
||||||
" != "
|
" != "
|
||||||
|
|
|
||||||
false
|
|
||||||
|
|
||||||
!=
|
!=
|
||||||
|
|
|
||||||
|
false
|
||||||
|
|
||||||
" != "
|
" != "
|
||||||
)
|
)
|
||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(1/*!=*/!=/*!=*/1)
|
assert(1/*!=*/!=/*!=*/1)
|
||||||
|
|
|
|
||||||
false
|
false
|
||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert((1 != 1) != false)
|
assert((1 != 1) != false)
|
||||||
|
|
| |
|
||||||
false
|
| false
|
||||||
false
|
false
|
||||||
@@ -2,36 +2,30 @@ Assertion failed
|
|||||||
assert(1 !== 1)
|
assert(1 !== 1)
|
||||||
|
|
|
|
||||||
false
|
false
|
||||||
true
|
|
||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(" !== " !== " !== ")
|
assert(" !== " !== " !== ")
|
||||||
|
|
|
|
||||||
false
|
false
|
||||||
true
|
|
||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(
|
assert(
|
||||||
" !== "
|
" !== "
|
||||||
|
|
|
||||||
false
|
|
||||||
true
|
|
||||||
|
|
||||||
!==
|
!==
|
||||||
|
|
|
||||||
|
false
|
||||||
|
|
||||||
" !== "
|
" !== "
|
||||||
)
|
)
|
||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(1/*!==*/!==/*!==*/1)
|
assert(1/*!==*/!==/*!==*/1)
|
||||||
|
|
|
|
||||||
false
|
false
|
||||||
true
|
|
||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert((1 !== 1) !== false)
|
assert((1 !== 1) !== false)
|
||||||
|
|
| |
|
||||||
false
|
| false
|
||||||
true
|
false
|
||||||
false
|
|
||||||
true
|
|
||||||
+11
-11
@@ -6,17 +6,17 @@ assert("Hello" !in listOf("Hello", "World"))
|
|||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(" !in " !in listOf(" !in "))
|
assert(" !in " !in listOf(" !in "))
|
||||||
| |
|
| |
|
||||||
| [ !in ]
|
| [ !in ]
|
||||||
false
|
false
|
||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(
|
assert(
|
||||||
" !in "
|
" !in "
|
||||||
|
|
|
||||||
false
|
|
||||||
|
|
||||||
!in
|
!in
|
||||||
|
|
|
||||||
|
false
|
||||||
|
|
||||||
listOf(" !in ")
|
listOf(" !in ")
|
||||||
|
|
|
|
||||||
@@ -25,14 +25,14 @@ assert(
|
|||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert("Hello"/*!in*/!in/*!in*/listOf("Hello", "World"))
|
assert("Hello"/*!in*/!in/*!in*/listOf("Hello", "World"))
|
||||||
| |
|
| |
|
||||||
| [Hello, World]
|
| [Hello, World]
|
||||||
false
|
false
|
||||||
|
|
||||||
Assertion failed
|
Assertion failed
|
||||||
assert(("Hello" !in listOf("Hello", "World")) !in listOf(false))
|
assert(("Hello" !in listOf("Hello", "World")) !in listOf(false))
|
||||||
| | |
|
| | | |
|
||||||
| | [false]
|
| | | [false]
|
||||||
|
| | false
|
||||||
| [Hello, World]
|
| [Hello, World]
|
||||||
false
|
|
||||||
false
|
false
|
||||||
Reference in New Issue
Block a user