Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Convert.kt
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ class EtsMethodBuilder(
callee = method.toEtsMethodSignature(),
args = args.map { ensureLocal(it.toEtsEntity()) },
type = type.toEtsType(),
receiver = receiver?.let { ensureLocal(it.toEtsEntity()) },
)

is ThisRefDto -> EtsThis(
Expand Down
1 change: 1 addition & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Values.kt
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ data class PtrCallExprDto(
val ptr: ValueDto, // Local or FieldRef
override val method: MethodSignatureDto,
override val args: List<ValueDto>,
val receiver: ValueDto? = null,
) : CallExprDto

@Serializable
Expand Down
1 change: 1 addition & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/model/Expr.kt
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,7 @@ data class EtsPtrCallExpr(
override val callee: EtsMethodSignature,
override val args: List<EtsLocal>,
override val type: EtsType,
val receiver: EtsLocal? = null,
) : EtsCallExpr {
override fun toString(): String {
return "ptr_call ${ptr}(${args.joinToString()})"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ private object EntityGetOperands : EtsEntity.Visitor<Sequence<EtsEntity>> {
expr.args.asSequence()

override fun visit(expr: EtsPtrCallExpr): Sequence<EtsEntity> =
sequenceOf(expr.ptr) + expr.args.asSequence()
sequenceOf(expr.ptr) + listOfNotNull(expr.receiver).asSequence() + expr.args.asSequence()

override fun visit(value: EtsRawEntity): Sequence<EtsEntity> =
emptySequence()
Expand Down
1 change: 1 addition & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/Handler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ abstract class AbstractHandler : EtsEntity.Visitor<Unit>, EtsStmt.Visitor<Unit>
final override fun visit(expr: EtsPtrCallExpr) {
handle(expr)
expr.ptr.accept(this)
expr.receiver?.accept(this)
expr.args.forEach { it.accept(this) }
}
final override fun visit(expr: EtsSpreadExpansionExpr) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 2022 UnitTestBot contributors (utbot.org)
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 org.jacodb.ets.test

import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.jacodb.ets.dto.EtsFileDto
import org.jacodb.ets.dto.dtoModule
import org.jacodb.ets.dto.toEtsFile
import org.jacodb.ets.model.EtsAssignStmt
import org.jacodb.ets.model.EtsEntity
import org.jacodb.ets.model.EtsLocal
import org.jacodb.ets.model.EtsPtrCallExpr
import org.jacodb.ets.model.EtsStmt
import org.jacodb.ets.utils.AbstractHandler
import org.jacodb.ets.utils.getOperands
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull

class EtsPtrCallReceiverTest {
private fun call(includeReceiver: Boolean): EtsPtrCallExpr {
val fileSignature = """{"projectName":"test","fileName":"receiver.ts"}"""
val classSignature = """{"name":"Container","declaringFile":$fileSignature}"""
val receiver = if (includeReceiver) """, "receiver":{"_":"Local","name":"object","type":{"_":"UnknownType"}}""" else ""
val source = """
{
"signature":$fileSignature,"namespaces":[],"importInfos":[],"exportInfos":[],
"classes":[{
"signature":$classSignature,"modifiers":0,"decorators":[],
"superClassName":"","implementedInterfaceNames":[],"fields":[],
"methods":[{
"signature":{"name":"run","declaringClass":$classSignature,"parameters":[],"returnType":{"_":"UnknownType"}},
"modifiers":0,"decorators":[],
"body":{"locals":[],"cfg":{"blocks":[{"id":0,"successors":[],"stmts":[{
"_":"AssignStmt","left":{"_":"Local","name":"result","type":{"_":"UnknownType"}},
"right":{"_":"PtrCallExpr","ptr":{"_":"Local","name":"fn","type":{"_":"UnknownType"}},
"method":{"name":"invoke","declaringClass":$classSignature,"parameters":[],"returnType":{"_":"UnknownType"}},
"args":[]$receiver}
},{"_":"ReturnVoidStmt"}]}]}}
}]
}]
}
""".trimIndent()
val dto = EtsFileDto.loadFromJson(source)
val roundTripped = EtsFileDto.loadFromJson(Json { serializersModule = dtoModule }.encodeToString(dto))

return roundTripped.toEtsFile().classes.single().methods.single().cfg.stmts
.filterIsInstance<EtsAssignStmt>().single().rhv as EtsPtrCallExpr
}

@Test
fun `receiver survives JSON conversion and is traversed once`() {
val call = call(includeReceiver = true)
val visited = mutableListOf<String>()
val handler = object : AbstractHandler() {
override fun handle(value: EtsEntity) {
if (value is EtsLocal) visited += value.name
}

override fun handle(stmt: EtsStmt) = Unit
}

call.accept(handler)

assertEquals("object", call.receiver?.name)
assertEquals(listOf("fn", "object"), call.getOperands().filterIsInstance<EtsLocal>().map { it.name }.toList())
assertEquals(listOf("fn", "object"), visited)
}

@Test
fun `old payload without receiver remains readable`() {
val call = call(includeReceiver = false)

assertNull(call.receiver)
assertEquals(listOf("fn"), call.getOperands().filterIsInstance<EtsLocal>().map { it.name }.toList())
}
}
1 change: 1 addition & 0 deletions jacodb-ets/ts-frontend/src/dto/values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ export interface PtrCallExprDto {
ptr: ValueDto; // Local or FieldRef (must be a value, not an expr)
method: MethodSignatureDto;
args: ValueDto[];
receiver?: LocalDto; // Receiver of a captured property callable.
}

export interface ThisRefDto {
Expand Down
5 changes: 4 additions & 1 deletion jacodb-ets/ts-frontend/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,9 @@ function validateValue(
}
break;
case "PtrCallExpr":
if (value.receiver !== undefined && value.receiver._ !== "Local") {
err(`${ctx}: PtrCallExpr.receiver must be Local`);
}
// Kotlin Convert casts ptr to EtsValue — expr kinds would throw a ClassCastException.
if (EXPR_KINDS.has(value.ptr._)) {
err(`${ctx}: PtrCallExpr.ptr has expr kind '${value.ptr._}', must be an immediate or ref`);
Expand Down Expand Up @@ -409,7 +412,7 @@ export function valueOperands(value: ValueDto): ValueDto[] {
case "StaticCallExpr":
return [...value.args];
case "PtrCallExpr":
return [value.ptr, ...value.args];
return [value.ptr, ...(value.receiver === undefined ? [] : [value.receiver]), ...value.args];
case "GlobalRef":
return value.ref !== null ? [value.ref] : [];
case "ClosureFieldRef":
Expand Down
22 changes: 22 additions & 0 deletions jacodb-ets/ts-frontend/test/pointer-receiver.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { expect, it } from "vitest";
import { UNKNOWN_CLASS_SIGNATURE } from "../src/dto/signatures";
import { UNKNOWN_TYPE } from "../src/dto/types";
import { LocalDto, PtrCallExprDto } from "../src/dto/values";
import { valueOperands } from "../src/validate";

it("preserves and traverses a captured pointer-call receiver exactly once", () => {
const ptr: LocalDto = { _: "Local", name: "fn", type: UNKNOWN_TYPE };
const receiver: LocalDto = { _: "Local", name: "object", type: UNKNOWN_TYPE };
const argument: LocalDto = { _: "Local", name: "arg", type: UNKNOWN_TYPE };
const call: PtrCallExprDto = {
_: "PtrCallExpr", ptr, receiver,
method: { declaringClass: UNKNOWN_CLASS_SIGNATURE, name: "call", parameters: [], returnType: UNKNOWN_TYPE },
args: [argument],
};

const roundTripped: PtrCallExprDto = JSON.parse(JSON.stringify(call));

expect(roundTripped.receiver).toEqual(receiver);
expect(valueOperands(roundTripped)).toEqual([ptr, receiver, argument]);
expect(valueOperands({ ...call, receiver: undefined })).toEqual([ptr, argument]);
});