From b06882760d5b78fde0c0c19a1744f5c9a1630613 Mon Sep 17 00:00:00 2001 From: Tanmay Date: Tue, 16 Jun 2026 17:27:10 +0200 Subject: [PATCH 1/2] Fix phantom argument attachment in Fluent DSL member expressions member() base lambdas evaluated eagerly under the enclosing Call's ArgumentHolder, attaching ref()/member() inside them as phantom arguments before member() itself ran. Clean up the phantom via holder -= memberBase, mirroring the existing memberCall() fix. Removes the makeMagic flag on ref(), now redundant. --- .../fraunhofer/aisec/cpg/testcases/Passes.kt | 52 +++++++---- .../fraunhofer/aisec/cpg/testcases/Query.kt | 61 ++++++------- .../aisec/cpg/graph/builder/Fluent.kt | 25 ++++-- .../cpg/evaluation/ValueEvaluationTests.kt | 16 ++-- .../fraunhofer/aisec/cpg/graph/FluentTest.kt | 49 ++++++++++ .../aisec/cpg/passes/UnresolvedDFGPassTest.kt | 4 +- .../aisec/cpg/test/GraphExamples.kt | 89 ++++++++----------- 7 files changed, 178 insertions(+), 118 deletions(-) diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/Passes.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/Passes.kt index ecadc3468a5..9bf33d45fbc 100644 --- a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/Passes.kt +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/Passes.kt @@ -60,7 +60,9 @@ class Passes { thenStmt { ref("y").inc() } elseStmt { ref("y").dec() } } - memberCall("println", member("out", ref("System"))) { ref("y") } + memberCall("println", member("out") { ref("System") }) { + ref("y") + } returnStmt {} } } @@ -79,7 +81,9 @@ class Passes { thenStmt { ref("y").inc() } elseStmt { ref("y").dec() } } - memberCall("println", member("out", ref("System"))) { ref("y") } + memberCall("println", member("out") { ref("System") }) { + ref("y") + } returnStmt {} } } @@ -98,7 +102,9 @@ class Passes { thenStmt { ref("y").inc() } elseStmt { ref("y").dec() } } - memberCall("println", member("out", ref("System"))) { ref("y") } + memberCall("println", member("out") { ref("System") }) { + ref("y") + } returnStmt {} } } @@ -117,7 +123,9 @@ class Passes { thenStmt { ref("y").inc() } elseStmt { ref("y").dec() } } - memberCall("println", member("out", ref("System"))) { ref("y") } + memberCall("println", member("out") { ref("System") }) { + ref("y") + } returnStmt {} } } @@ -138,7 +146,9 @@ class Passes { elseStmt { ref("y").dec() } } ref("z") assign literal(10, t("int")) - memberCall("println", member("out", ref("System"))) { ref("y") } + memberCall("println", member("out") { ref("System") }) { + ref("y") + } returnStmt {} } } @@ -159,7 +169,9 @@ class Passes { elseStmt { ref("y").dec() } } ref("z") assign literal(3, t("int")) - memberCall("println", member("out", ref("System"))) { ref("y") } + memberCall("println", member("out") { ref("System") }) { + ref("y") + } returnStmt {} } } @@ -178,7 +190,9 @@ class Passes { thenStmt { ref("y").inc() } elseStmt { ref("y").dec() } } - memberCall("println", member("out", ref("System"))) { ref("y") } + memberCall("println", member("out") { ref("System") }) { + ref("y") + } returnStmt {} } } @@ -192,13 +206,13 @@ class Passes { whileStmt { whileCondition { ref("x") } loopBody { - memberCall("println", member("out", ref("System"))) { + memberCall("println", member("out") { ref("System") }) { literal("Cool loop", t("string")) } } } - memberCall("println", member("out", ref("System"))) { + memberCall("println", member("out") { ref("System") }) { literal("After cool loop", t("string")) } returnStmt {} @@ -214,14 +228,14 @@ class Passes { whileStmt { whileCondition { ref("x") } loopBody { - memberCall("println", member("out", ref("System"))) { + memberCall("println", member("out") { ref("System") }) { literal("Cool loop", t("string")) } ref("x") assign literal(false, t("boolean")) } } - memberCall("println", member("out", ref("System"))) { + memberCall("println", member("out") { ref("System") }) { literal("After cool loop", t("string")) } returnStmt {} @@ -233,13 +247,13 @@ class Passes { whileStmt { whileCondition { literal(false, t("boolean")) } loopBody { - memberCall("println", member("out", ref("System"))) { + memberCall("println", member("out") { ref("System") }) { literal("Cool loop", t("string")) } } } - memberCall("println", member("out", ref("System"))) { + memberCall("println", member("out") { ref("System") }) { literal("After cool loop", t("string")) } returnStmt {} @@ -253,13 +267,13 @@ class Passes { whileStmt { whileCondition { ref("x") le literal(2, t("int")) } loopBody { - memberCall("println", member("out", ref("System"))) { + memberCall("println", member("out") { ref("System") }) { literal("Cool loop", t("string")) } } } - memberCall("println", member("out", ref("System"))) { + memberCall("println", member("out") { ref("System") }) { literal("After cool loop", t("string")) } returnStmt {} @@ -273,13 +287,13 @@ class Passes { whileStmt { whileCondition { ref("x") gt literal(3, t("int")) } loopBody { - memberCall("println", member("out", ref("System"))) { + memberCall("println", member("out") { ref("System") }) { literal("Cool loop", t("string")) } } } - memberCall("println", member("out", ref("System"))) { + memberCall("println", member("out") { ref("System") }) { literal("After cool loop", t("string")) } returnStmt {} @@ -297,14 +311,14 @@ class Passes { whileStmt { whileCondition { ref("y") le literal(2, t("int")) } loopBody { - memberCall("println", member("out", ref("System"))) { + memberCall("println", member("out") { ref("System") }) { literal("Cool loop", t("string")) } ref("y") assign memberCall("nextUInt", ref("URandomKt")) } } - memberCall("println", member("out", ref("System"))) { + memberCall("println", member("out") { ref("System") }) { literal("After cool loop", t("string")) } returnStmt {} diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/Query.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/Query.kt index b1e838c46eb..6d23e1be234 100644 --- a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/Query.kt +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/testcases/Query.kt @@ -63,7 +63,9 @@ class Query { method("print", void()) { param("s", t("string")) body { - memberCall("println", member("out", ref("System"))) { ref("s") } + memberCall("println", member("out") { ref("System") }) { + ref("s") + } returnStmt {} } } @@ -84,9 +86,7 @@ class Query { } } memberCall("print", ref("sc")) { ref("s") } - memberCall("print", ref("sc")) { - memberCall("test", ref("sc", makeMagic = false)) - } + memberCall("print", ref("sc")) { memberCall("test", ref("sc")) } } } } @@ -120,7 +120,9 @@ class Query { isStatic = true param("s", t("string")) body { - memberCall("println", member("out", ref("System"))) { ref("s") } + memberCall("println", member("out") { ref("System") }) { + ref("s") + } returnStmt {} } } @@ -135,7 +137,7 @@ class Query { } } - member("a", ref("sc")) assign literal(5, t("int")) + member("a") { ref("sc") } assign literal(5, t("int")) memberCall( "highlyCriticalOperation", @@ -145,21 +147,18 @@ class Query { }, ) { this@memberCall.isStatic = true - memberCall( - "toString", - ref("Integer", t("Integer"), makeMagic = false), - ) { + memberCall("toString", ref("Integer", t("Integer"))) { this.type = t("string") this@memberCall.isStatic = true isStatic = true - member("a", ref("sc", makeMagic = false)) + member("a") { ref("sc") } } } memberCall("log", ref("logger")) { - member("INFO", ref("Level", makeMagic = false)) + member("INFO") { ref("Level") } literal("put ", t("string")) + - member("a", ref("sc", makeMagic = false)) + + member("a") { ref("sc") } + literal(" into highlyCriticalOperation()", t("string")) } returnStmt {} @@ -198,7 +197,9 @@ class Query { isStatic = true param("s", t("string")) body { - memberCall("println", member("out", ref("System"))) { ref("s") } + memberCall("println", member("out") { ref("System") }) { + ref("s") + } returnStmt {} } } @@ -213,12 +214,12 @@ class Query { } } - member("a", ref("sc")) assign literal(5, t("int")) + member("a") { ref("sc") } assign literal(5, t("int")) memberCall("log", ref("logger")) { - member("INFO", ref("Level", makeMagic = false)) + member("INFO") { ref("Level") } literal("put ", t("string")) + - member("a", ref("sc", makeMagic = false)) + + member("a") { ref("sc") } + literal(" into highlyCriticalOperation()", t("string")) } @@ -230,14 +231,11 @@ class Query { }, ) { this@memberCall.isStatic = true - memberCall( - "toString", - ref("Integer", t("Integer"), makeMagic = false), - ) { + memberCall("toString", ref("Integer", t("Integer"))) { this.type = t("string") this@memberCall.isStatic = true isStatic = true - member("a", ref("sc", makeMagic = false)) + member("a") { ref("sc") } } } returnStmt {} @@ -273,7 +271,9 @@ class Query { isStatic = true param("s", t("string")) body { - memberCall("println", member("out", ref("System"))) { ref("s") } + memberCall("println", member("out") { ref("System") }) { + ref("s") + } returnStmt {} } } @@ -288,16 +288,16 @@ class Query { } } - member("a", ref("sc")) assign literal(5, t("int")) + member("a") { ref("sc") } assign literal(5, t("int")) memberCall("log", ref("logger")) { - member("INFO", ref("Level", makeMagic = false)) + member("INFO") { ref("Level") } literal("put ", t("string")) + - member("a", ref("a", makeMagic = false)) + + member("a") { ref("a") } + literal(" into highlyCriticalOperation()", t("string")) } - member("a", ref("sc")) assign literal(3, t("int")) + member("a") { ref("sc") } assign literal(3, t("int")) memberCall( "highlyCriticalOperation", @@ -307,14 +307,11 @@ class Query { }, ) { this@memberCall.isStatic = true - memberCall( - "toString", - ref("Integer", t("Integer"), makeMagic = false), - ) { + memberCall("toString", ref("Integer", t("Integer"))) { this.type = t("string") this@memberCall.isStatic = true isStatic = true - member("a", ref("sc", makeMagic = false)) + member("a") { ref("sc") } } } returnStmt {} diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt index aa6e36e7063..88a2b69f1fa 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt @@ -547,6 +547,7 @@ fun LanguageFrontend<*, *>.memberCall( holder += node } else if (holder is ArgumentHolder) { holder += node + holder -= base } return node @@ -1066,7 +1067,6 @@ context(holder: Holder) fun LanguageFrontend<*, *>.ref( name: CharSequence, type: Type = unknownType(), - makeMagic: Boolean = true, init: (Reference.() -> Unit)? = null, ): Reference { val node = newReference(name).apply { this.location = getCallerFileAndLine() } @@ -1077,11 +1077,9 @@ fun LanguageFrontend<*, *>.ref( init(node) } - if (makeMagic) { - // Only add this to an argument holder if the nearest holder is an argument holder - if (holder is ArgumentHolder) { - holder += node - } + // Only add this to an argument holder if the nearest holder is an argument holder + if (holder is ArgumentHolder) { + holder += node } return node @@ -1109,12 +1107,16 @@ fun Expression.line(i: Int): Expression { * Creates a new [MemberAccess] in the Fluent Node DSL and invokes [ArgumentHolder.addArgument] of * the nearest enclosing [Holder], but only if it is an [ArgumentHolder]. If the [name] doesn't * already contain a fqn, we add an implicit "this" as base. + * + * The [base] is an optional lambda that produces the base expression. Using a lambda defers + * evaluation so the base expression is produced after [member] has started executing, preventing it + * from eagerly attaching to the wrong [ArgumentHolder]. */ context(holder: Holder) fun LanguageFrontend<*, *>.member( name: CharSequence, - base: Expression? = null, operatorCode: String = ".", + base: (() -> Expression)? = null, ): MemberAccess { val parsedName = parseName(name) val type = @@ -1128,16 +1130,21 @@ fun LanguageFrontend<*, *>.member( val scopeType = scope?.name?.let { this.t(it) } ?: unknownType() scopeType } - val memberBase = base ?: this.memberOrRef(parsedName.parent ?: this.parseName("this"), type) + val memberBase = + base?.invoke() ?: this.memberOrRef(parsedName.parent ?: this.parseName("this"), type) val node = newMemberAccess(name, memberBase, operatorCode = operatorCode).apply { this.location = getCallerFileAndLine() } - // Only add this to an argument holder if the nearest holder is an argument holder if (holder is ArgumentHolder) { holder += node + // base?.invoke() runs with the outer holder in context, so ref() / member() inside + // the lambda eagerly attach memberBase to holder. Remove that phantom attachment. + if (base != null) { + holder -= memberBase + } } return node diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/evaluation/ValueEvaluationTests.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/evaluation/ValueEvaluationTests.kt index 528d4a3c9da..ccdf5e363d7 100644 --- a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/evaluation/ValueEvaluationTests.kt +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/evaluation/ValueEvaluationTests.kt @@ -69,10 +69,12 @@ class ValueEvaluationTests { variable("i", t("int")) { literal(0, t("int")) } } } - forCondition { ref("i") lt member("length", ref("array")) } + forCondition { + ref("i") lt member("length") { ref("array") } + } forIteration { ref("i").inc() } } - memberCall("println", member("out", ref("System"))) { + memberCall("println", member("out") { ref("System") }) { subscriptExpr { ref("array") literal(1, t("int")) @@ -85,7 +87,7 @@ class ValueEvaluationTests { } } - memberCall("println", member("out", ref("System"))) { + memberCall("println", member("out") { ref("System") }) { ref("str") } returnStmt {} @@ -126,9 +128,13 @@ class ValueEvaluationTests { ref("i").inc() - memberCall("println", member("out", ref("System"))) { ref("s") } + memberCall("println", member("out") { ref("System") }) { + ref("s") + } - memberCall("println", member("out", ref("System"))) { ref("i") } + memberCall("println", member("out") { ref("System") }) { + ref("i") + } returnStmt {} } } diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/FluentTest.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/FluentTest.kt index 7880e69e266..3177fd344af 100644 --- a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/FluentTest.kt +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/FluentTest.kt @@ -39,6 +39,7 @@ import de.fraunhofer.aisec.cpg.graph.expressions.Comprehension import de.fraunhofer.aisec.cpg.graph.expressions.DeclarationStatement import de.fraunhofer.aisec.cpg.graph.expressions.IfElse import de.fraunhofer.aisec.cpg.graph.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.expressions.MemberAccess import de.fraunhofer.aisec.cpg.graph.expressions.MemberCall import de.fraunhofer.aisec.cpg.graph.expressions.Reference import de.fraunhofer.aisec.cpg.graph.expressions.Return @@ -361,6 +362,54 @@ class FluentTest { assertNotNull(compExpr.predicate) } + @Test + fun testMemberWithBaseAttachesExactlyOnce() { + val result = + testFrontend { it.registerLanguage() } + .build { + translationResult { + translationUnit("member_test.c") { + function("main", t("void")) { + body { + call("doSomething") { member("field1") { ref("s1") } } + + call("useNested") { + member("field") { member("in") { ref("o") } } + } + } + } + } + } + } + + val main = result.functions["main"] + assertNotNull(main) + + val doSomething = (main.body as? Block)?.statements?.filterIsInstance()?.get(0) + assertNotNull(doSomething) + assertEquals( + 1, + doSomething.arguments.size, + "doSomething must have exactly 1 argument (the MemberAccess, no phantom ref)", + ) + val fieldAccess = doSomething.arguments.single() + assertIs(fieldAccess) + assertLocalName("field1", fieldAccess) + val s1Ref = fieldAccess.base + assertIs(s1Ref) + assertLocalName("s1", s1Ref) + + val useNested = (main.body as? Block)?.statements?.filterIsInstance()?.get(1) + assertNotNull(useNested) + assertEquals(1, useNested.arguments.size, "useNested must have exactly 1 argument") + val outerMember = useNested.arguments.single() + assertIs(outerMember) + assertLocalName("field", outerMember) + val innerMember = outerMember.base + assertIs(innerMember) + assertLocalName("in", innerMember) + } + @Test fun testLocationIntegrity() { val results = diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/UnresolvedDFGPassTest.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/UnresolvedDFGPassTest.kt index 4896e09c6d9..52268d196cf 100644 --- a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/UnresolvedDFGPassTest.kt +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/UnresolvedDFGPassTest.kt @@ -160,14 +160,14 @@ class UnresolvedDFGPassTest { receiver = newVariable("this", t("DfgUnresolvedCalls")) param("i", t("int")) body { - member("i", ref("this")) assign { ref("i") } + member("i") { ref("this") } assign { ref("i") } returnStmt { isImplicit = true } } } method("knownFunction", t("int")) { receiver = newVariable("this", t("DfgUnresolvedCalls")) param("arg", t("int")) - body { returnStmt { member("i", ref("this")) + ref("arg") } } + body { returnStmt { member("i") { ref("this") } + ref("arg") } } } // The main method diff --git a/cpg-core/src/testFixtures/kotlin/de/fraunhofer/aisec/cpg/test/GraphExamples.kt b/cpg-core/src/testFixtures/kotlin/de/fraunhofer/aisec/cpg/test/GraphExamples.kt index c7b0bc9ec59..e299931a861 100644 --- a/cpg-core/src/testFixtures/kotlin/de/fraunhofer/aisec/cpg/test/GraphExamples.kt +++ b/cpg-core/src/testFixtures/kotlin/de/fraunhofer/aisec/cpg/test/GraphExamples.kt @@ -395,8 +395,8 @@ class GraphExamples { t("T").reference(PointerType.PointerOrigin.POINTER), ) } - member("value", ref("node"), "->") assign literal(42, t("int")) - member("next", ref("node"), "->") assign ref("node") + member("value", "->") { ref("node") } assign literal(42, t("int")) + member("next", "->") { ref("node") } assign ref("node") memberCall( "dump", ref("node"), @@ -425,8 +425,8 @@ class GraphExamples { function("main", t("int")) { body { declare { variable("node", t("T")) } - member("value", ref("node")) assign literal(42, t("int")) - member("next", ref("node")) assign { reference(ref("node")) } + member("value") { ref("node") } assign literal(42, t("int")) + member("next") { ref("node") } assign { reference(ref("node")) } returnStmt { isImplicit = true } } } @@ -577,7 +577,7 @@ class GraphExamples { declare { variable("field", t("int")) { literal(43, t("int")) } } - returnStmt { member("field", ref("this")) } + returnStmt { member("field") { ref("this") } } } } } @@ -823,16 +823,15 @@ class GraphExamples { declare { variable("a", t("int")) { literal(1, t("int")) } } ifStmt { condition { - member("length", ref("args")) gt literal(3, t("int")) + member("length") { ref("args") } gt literal(3, t("int")) } thenStmt { ref("a") assign literal(2, t("int")) } elseStmt { memberCall( "println", - member( - "out", - ref("System") { isStaticAccess = true }, - ), + member("out") { + ref("System") { isStaticAccess = true } + }, ) { ref("a") } @@ -854,7 +853,7 @@ class GraphExamples { new { construct("ControlFlowSensitiveDFGIfMerge") } } } - member("bla", ref("obj")) assign literal(3, t("int")) + member("bla") { ref("obj") } assign literal(3, t("int")) returnStmt { isImplicit = true } } } @@ -913,10 +912,9 @@ class GraphExamples { default() memberCall( "println", - member( - "out", - ref("System") { isStaticAccess = true }, - ), + member("out") { + ref("System") { isStaticAccess = true } + }, ) { ref("a") } @@ -952,7 +950,7 @@ class GraphExamples { declare { variable("a", t("int")) { literal(1, t("int")) } } ifStmt { condition { - member("length", ref("args")) gt literal(3, t("int")) + member("length") { ref("args") } gt literal(3, t("int")) } thenStmt { ref("a") assign literal(2, t("int")) } elseStmt { @@ -1005,12 +1003,11 @@ class GraphExamples { elseStmt { memberCall( "println", - member( - "out", + member("out") { ref("System") { isStaticAccess = true - }, - ), + } + }, ) { ref("a") } @@ -1023,10 +1020,9 @@ class GraphExamples { } memberCall( "println", - member( - "out", - ref("System") { isStaticAccess = true }, - ), + member("out") { + ref("System") { isStaticAccess = true } + }, ) { ref("a") } @@ -1037,7 +1033,7 @@ class GraphExamples { memberCall( "println", - member("out", ref("System") { isStaticAccess = true }), + member("out") { ref("System") { isStaticAccess = true } }, ) { ref("a") } @@ -1078,10 +1074,9 @@ class GraphExamples { elseStmt { memberCall( "println", - member( - "out", - ref("System") { isStaticAccess = true }, - ), + member("out") { + ref("System") { isStaticAccess = true } + }, ) { ref("a") } @@ -1194,7 +1189,9 @@ class GraphExamples { body { memberCall( "println", - member("out", ref("System") { isStaticAccess = true }), + member("out") { + ref("System") { isStaticAccess = true } + }, ) { literal("Hello world") } @@ -1253,7 +1250,7 @@ class GraphExamples { body { memberCall( "println", - member("out", ref("System") { isStaticAccess = true }), + member("out") { ref("System") { isStaticAccess = true } }, ) { ref("s") } @@ -1310,7 +1307,7 @@ class GraphExamples { body { memberCall( "println", - member("out", ref("System") { isStaticAccess = true }), + member("out") { ref("System") { isStaticAccess = true } }, ) { call("this.toString") } @@ -1477,26 +1474,21 @@ class GraphExamples { // Call doSomething on field1 of s1 call("doSomething") { - member("field1", ref("s1", makeMagic = false).line(11)) - .line(11) + member("field1") { ref("s1").line(11) }.line(11) } .line(11) // Set field1 of both s1 and s2, to literal 1 and 2 respectively - member("field1", ref("s1", makeMagic = false).line(13)) - .line(13) assign literal(1) - member("field1", ref("s2", makeMagic = false).line(14)) - .line(14) assign literal(2) + member("field1") { ref("s1").line(13) }.line(13) assign literal(1) + member("field1") { ref("s2").line(14) }.line(14) assign literal(2) // Call doSomething on field1 of s1 and s2 call("doSomething") { - member("field1", ref("s1", makeMagic = false).line(15)) - .line(15) + member("field1") { ref("s1").line(15) }.line(15) } .line(15) call("doSomething") { - member("field1", ref("s2", makeMagic = false).line(16)) - .line(16) + member("field1") { ref("s2").line(16) }.line(16) } .line(16) } @@ -1543,18 +1535,13 @@ class GraphExamples { body { declare { variable("o", t("outer")) } - member( - "field", - member("in", ref("o", makeMagic = false).line(13)).line(13), - ) + member("field") { member("in") { ref("o").line(13) }.line(13) } .line(13) assign literal(1) call("doSomething") { - member( - "field", - member("in", ref("o", makeMagic = false).line(15)) - .line(15), - ) + member("field") { + member("in") { ref("o").line(15) }.line(15) + } .line(15) } .line(15) From 7beddf7d2ee2e986db6d5e77343f6e6f66db46e3 Mon Sep 17 00:00:00 2001 From: Tanmay Date: Thu, 18 Jun 2026 11:05:38 +0200 Subject: [PATCH 2/2] Make member() base lambda context-bound instead of post-hoc cleanup base now declares its own Holder context parameter, resolved at invocation rather than capture. Calling it under a no-op DiscardHolder means ref()/member() inside it never reach the real outer holder, removing the need for the holder -= memberBase cleanup. --- .../aisec/cpg/graph/builder/Fluent.kt | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt index 88a2b69f1fa..947e96245f9 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt @@ -1103,20 +1103,25 @@ fun Expression.line(i: Int): Expression { return this } +private object DiscardHolder : Holder { + override fun replace(old: Expression, new: Expression) = false + + override fun plusAssign(node: Expression) {} +} + /** * Creates a new [MemberAccess] in the Fluent Node DSL and invokes [ArgumentHolder.addArgument] of * the nearest enclosing [Holder], but only if it is an [ArgumentHolder]. If the [name] doesn't * already contain a fqn, we add an implicit "this" as base. - * - * The [base] is an optional lambda that produces the base expression. Using a lambda defers - * evaluation so the base expression is produced after [member] has started executing, preventing it - * from eagerly attaching to the wrong [ArgumentHolder]. */ context(holder: Holder) fun LanguageFrontend<*, *>.member( name: CharSequence, operatorCode: String = ".", - base: (() -> Expression)? = null, + base: + (context(Holder) + () -> Expression)? = + null, ): MemberAccess { val parsedName = parseName(name) val type = @@ -1131,7 +1136,8 @@ fun LanguageFrontend<*, *>.member( scopeType } val memberBase = - base?.invoke() ?: this.memberOrRef(parsedName.parent ?: this.parseName("this"), type) + base?.let { with(DiscardHolder) { it() } } + ?: this.memberOrRef(parsedName.parent ?: this.parseName("this"), type) val node = newMemberAccess(name, memberBase, operatorCode = operatorCode).apply { @@ -1140,11 +1146,6 @@ fun LanguageFrontend<*, *>.member( if (holder is ArgumentHolder) { holder += node - // base?.invoke() runs with the outer holder in context, so ref() / member() inside - // the lambda eagerly attach memberBase to holder. Remove that phantom attachment. - if (base != null) { - holder -= memberBase - } } return node