From bff240e106f679ed85694157a2ad2c9377645034 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Fri, 13 Feb 2026 16:06:08 +0100 Subject: [PATCH 01/55] Initial commit; using JNA and roslyn c# parser --- cpg-language-csharp/build.gradle.kts | 27 +++ .../src/main/csharp/NativeParser/.gitignore | 2 + .../src/main/csharp/NativeParser/Library.cs | 91 +++++++++ .../csharp/NativeParser/NativeParser.csproj | 10 + .../cpg/frontends/csharp/CSharpLanguage.kt | 43 +++++ .../csharp/CSharpLanguageFrontend.kt | 175 ++++++++++++++++++ .../csharp/CSharpLanguageFrontendTest.kt | 55 ++++++ .../src/test/resources/csharp/helloworld.cs | 4 + gradle.properties.example | 1 + settings.gradle.kts | 6 + 10 files changed, 414 insertions(+) create mode 100644 cpg-language-csharp/build.gradle.kts create mode 100644 cpg-language-csharp/src/main/csharp/NativeParser/.gitignore create mode 100644 cpg-language-csharp/src/main/csharp/NativeParser/Library.cs create mode 100644 cpg-language-csharp/src/main/csharp/NativeParser/NativeParser.csproj create mode 100644 cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt create mode 100644 cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt create mode 100644 cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt create mode 100644 cpg-language-csharp/src/test/resources/csharp/helloworld.cs diff --git a/cpg-language-csharp/build.gradle.kts b/cpg-language-csharp/build.gradle.kts new file mode 100644 index 00000000000..23886e0d319 --- /dev/null +++ b/cpg-language-csharp/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { id("cpg.frontend-conventions") } + +dependencies { implementation("net.java.dev.jna:jna:5.18.1") } + +val publishNativeParser by + tasks.registering(Exec::class) { + workingDir = file("src/main/csharp/NativeParser") + val dotnet = + listOf( + "/opt/homebrew/opt/dotnet@8/bin/dotnet", + "/usr/local/bin/dotnet", + "/usr/bin/dotnet", + ) + .map { file(it) } + .firstOrNull { it.exists() } + ?.absolutePath ?: "dotnet" + commandLine(dotnet, "publish", "-c", "Release", "-r", "osx-arm64") + } + +tasks.named("compileKotlin") { dependsOn(publishNativeParser) } + +mavenPublishing { + pom { + name.set("Code Property Graph - C# Frontend") + description.set("A C# language frontend for the CPG") + } +} diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/.gitignore b/cpg-language-csharp/src/main/csharp/NativeParser/.gitignore new file mode 100644 index 00000000000..cbbd0b5c02f --- /dev/null +++ b/cpg-language-csharp/src/main/csharp/NativeParser/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ \ No newline at end of file diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs new file mode 100644 index 00000000000..d728bcecdff --- /dev/null +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -0,0 +1,91 @@ +using System.Runtime.InteropServices; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace NativeParser; + +public static class Library +{ + private static readonly Dictionary Handles = new(); + private static int _nextId = 1; + + private static IntPtr Save(object obj) + { + var id = _nextId++; + Handles[id] = obj; + return (IntPtr)id; + } + + private static T Restore(IntPtr handle) + { + return (T)Handles[(int)handle]; + } + + [UnmanagedCallersOnly(EntryPoint = "parseCsharp")] + public static IntPtr ParseCSharp(IntPtr sourcePtr) + { + var source = Marshal.PtrToStringUTF8(sourcePtr); + var tree = CSharpSyntaxTree.ParseText(source); + var root = tree.GetRoot(); + return Save(root); + } + + [UnmanagedCallersOnly(EntryPoint = "getKind")] + public static IntPtr GetKind(IntPtr handle) + { + var node = Restore(handle); + return Marshal.StringToCoTaskMemUTF8(node.Kind().ToString()); + } + + [UnmanagedCallersOnly(EntryPoint = "getCode")] + public static IntPtr GetCode(IntPtr handle) + { + var node = Restore(handle); + return Marshal.StringToCoTaskMemUTF8(node.ToFullString().Trim()); + } + + [UnmanagedCallersOnly(EntryPoint = "getNumChildren")] + public static int GetNumChildren(IntPtr handle) + { + var node = Restore(handle); + return node.ChildNodes().Count(); + } + + [UnmanagedCallersOnly(EntryPoint = "getChild")] + public static IntPtr GetChild(IntPtr handle, int index) + { + var node = Restore(handle); + return Save(node.ChildNodes().ElementAt(index)); + } + + [UnmanagedCallersOnly(EntryPoint = "getIdentifier")] + public static IntPtr GetIdentifier(IntPtr handle) + { + var node = Restore(handle); + var text = node switch + { + ClassDeclarationSyntax cls => cls.Identifier.Text, + _ => "UNKNOWN" + }; + return Marshal.StringToCoTaskMemUTF8(text); + } + + [UnmanagedCallersOnly(EntryPoint = "getName")] + public static IntPtr GetName(IntPtr handle) + { + var node = Restore(handle); + var text = node switch + { + BaseNamespaceDeclarationSyntax ns => ns.Name.ToString(), + _ => "UNKNOWN" + }; + return Marshal.StringToCoTaskMemUTF8(text); + } + + [UnmanagedCallersOnly(EntryPoint = "freeString")] + public static void FreeString(IntPtr ptr) + { + Marshal.FreeCoTaskMem(ptr); + } +} \ No newline at end of file diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/NativeParser.csproj b/cpg-language-csharp/src/main/csharp/NativeParser/NativeParser.csproj new file mode 100644 index 00000000000..940211b2640 --- /dev/null +++ b/cpg-language-csharp/src/main/csharp/NativeParser/NativeParser.csproj @@ -0,0 +1,10 @@ + + + net8.0 + true + enable + + + + + diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt new file mode 100644 index 00000000000..d852d4cc6a0 --- /dev/null +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 de.fraunhofer.aisec.cpg.frontends.csharp + +import de.fraunhofer.aisec.cpg.frontends.Language +import de.fraunhofer.aisec.cpg.graph.types.Type +import kotlin.reflect.KClass +import org.neo4j.ogm.annotation.Transient + +class CSharpLanguage : Language() { + override val fileExtensions = listOf("cs") + override val namespaceDelimiter = "." + + @Transient + override val frontend: KClass = CSharpLanguageFrontend::class + + override val compoundAssignmentOperators = setOf() + + @Transient override val builtInTypes = mapOf() +} diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt new file mode 100644 index 00000000000..5974458a0df --- /dev/null +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 de.fraunhofer.aisec.cpg.frontends.csharp + +import com.sun.jna.Library +import com.sun.jna.Native +import com.sun.jna.Pointer +import de.fraunhofer.aisec.cpg.TranslationContext +import de.fraunhofer.aisec.cpg.frontends.Language +import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend +import de.fraunhofer.aisec.cpg.frontends.TranslationException +import de.fraunhofer.aisec.cpg.graph.DeclarationHolder +import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration +import de.fraunhofer.aisec.cpg.graph.newNamespaceDeclaration +import de.fraunhofer.aisec.cpg.graph.newRecordDeclaration +import de.fraunhofer.aisec.cpg.graph.newTranslationUnitDeclaration +import de.fraunhofer.aisec.cpg.graph.types.Type +import de.fraunhofer.aisec.cpg.graph.unknownType +import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation +import java.io.File + +class CSharpLanguageFrontend(ctx: TranslationContext, language: Language) : + LanguageFrontend(ctx, language) { + + companion object { + internal val nativeLib: CSharpNativeParser by lazy { + val libFile = + File( + "src/main/csharp/NativeParser/bin/Release/net8.0/osx-arm64/publish/NativeParser.dylib" + ) + if (!libFile.exists()) { + throw TranslationException("nothing found") + } + Native.load(libFile.absolutePath, CSharpNativeParser::class.java) + } + + internal fun nativeString(ptr: Pointer): String { + return try { + ptr.getString(0, "UTF-8") + } finally { + nativeLib.freeString(ptr) + } + } + + fun wrapHandle(handle: Pointer): CSharpSyntaxNode { + val kind = nativeString(nativeLib.getKind(handle)) + return when (kind) { + "CompilationUnit" -> CompilationUnitSyntax(handle) + "NamespaceDeclaration" -> NamespaceDeclarationSyntax(handle) + "ClassDeclaration" -> ClassDeclarationSyntax(handle) + else -> CSharpSyntaxNode(handle) + } + } + } + + override fun parse(file: File): TranslationUnitDeclaration { + val source = file.readText() + val rootHandle = nativeLib.parseCsharp(source) + val root = wrapHandle(rootHandle) + + val tu = newTranslationUnitDeclaration(file.name, rawNode = root) + scopeManager.resetToGlobal(tu) + scopeManager.enterScope(tu) + + for (child in root.children) { + handleNode(child, tu) + } + + scopeManager.leaveScope(tu) + return tu + } + + private fun handleNode(node: CSharpSyntaxNode, parent: DeclarationHolder) { + when (node) { + is NamespaceDeclarationSyntax -> { + val ns = newNamespaceDeclaration(node.name, rawNode = node) + scopeManager.enterScope(ns) + + for (child in node.children) { + handleNode(child, ns) + } + + scopeManager.leaveScope(ns) + scopeManager.addDeclaration(ns) + parent.addDeclaration(ns) + } + is ClassDeclarationSyntax -> { + val record = newRecordDeclaration(node.identifier, "class", rawNode = node) + scopeManager.enterScope(record) + scopeManager.leaveScope(record) + scopeManager.addDeclaration(record) + parent.addDeclaration(record) + } + } + } + + override fun typeOf(type: CSharpSyntaxNode): Type = unknownType() + + override fun codeOf(astNode: CSharpSyntaxNode): String = astNode.code + + override fun locationOf(astNode: CSharpSyntaxNode): PhysicalLocation? = null + + override fun setComment(node: Node, astNode: CSharpSyntaxNode) {} +} + +interface CSharpNativeParser : Library { + fun parseCsharp(source: String): Pointer + + fun getKind(handle: Pointer): Pointer + + fun getCode(handle: Pointer): Pointer + + fun getNumChildren(handle: Pointer): Int + + fun getChild(handle: Pointer, index: Int): Pointer + + fun getIdentifier(handle: Pointer): Pointer + + fun getName(handle: Pointer): Pointer + + fun freeString(ptr: Pointer) +} + +// https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.csharpsyntaxnode?view=roslyn-dotnet-5.0.0 +open class CSharpSyntaxNode(val handle: Pointer) { + val code: String by lazy { + CSharpLanguageFrontend.nativeString(CSharpLanguageFrontend.nativeLib.getCode(handle)) + } + val children: List by lazy { + val n = CSharpLanguageFrontend.nativeLib.getNumChildren(handle) + (0 until n).map { + val childHandle = CSharpLanguageFrontend.nativeLib.getChild(handle, it) + CSharpLanguageFrontend.wrapHandle(childHandle) + } + } +} + +// https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.compilationunitsyntax?view=roslyn-dotnet-5.0.0 +class CompilationUnitSyntax(handle: Pointer) : CSharpSyntaxNode(handle) + +class NamespaceDeclarationSyntax(handle: Pointer) : CSharpSyntaxNode(handle) { + val name: String by lazy { + CSharpLanguageFrontend.nativeString(CSharpLanguageFrontend.nativeLib.getName(handle)) + } +} + +class ClassDeclarationSyntax(handle: Pointer) : CSharpSyntaxNode(handle) { + val identifier: String by lazy { + CSharpLanguageFrontend.nativeString(CSharpLanguageFrontend.nativeLib.getIdentifier(handle)) + } +} diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt new file mode 100644 index 00000000000..3df804fd1f5 --- /dev/null +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 de.fraunhofer.aisec.cpg.frontends.csharp + +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.test.* +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertNotNull + +class CSharpLanguageFrontendTest : BaseTest() { + + @Test + fun testHelloWorld() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("helloworld.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val ns = tu.namespaces["HelloWorld"] + assertNotNull(ns, "Expected namespace HelloWorld") + + val greeter = ns.records["Greeter"] + assertNotNull(greeter, "Expected class Greeter") + } +} diff --git a/cpg-language-csharp/src/test/resources/csharp/helloworld.cs b/cpg-language-csharp/src/test/resources/csharp/helloworld.cs new file mode 100644 index 00000000000..387e9347407 --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/helloworld.cs @@ -0,0 +1,4 @@ +namespace HelloWorld +{ + class Greeter { } +} diff --git a/gradle.properties.example b/gradle.properties.example index 9250e5fe481..43f2c191af0 100644 --- a/gradle.properties.example +++ b/gradle.properties.example @@ -10,6 +10,7 @@ enableTypeScriptFrontend=true enableRubyFrontend=true enableJVMFrontend=true enableINIFrontend=true +enableCSharpFrontend=true enableMCPModule=true org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled diff --git a/settings.gradle.kts b/settings.gradle.kts index 0c7ec80365d..c98cf01f1a9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -49,6 +49,10 @@ val enableJVMFrontend: Boolean by extra { val enableJVMFrontend: String? by settings enableJVMFrontend.toBoolean() } +val enableCSharpFrontend: Boolean by extra { + val enableCSharpFrontend: String? by settings + enableCSharpFrontend.toBoolean() +} val enableINIFrontend: Boolean by extra { val enableINIFrontend: String? by settings enableINIFrontend.toBoolean() @@ -67,9 +71,11 @@ if (enableTypeScriptFrontend) include(":cpg-language-typescript") if (enableRubyFrontend) include(":cpg-language-ruby") if (enableJVMFrontend) include(":cpg-language-jvm") if (enableINIFrontend) include(":cpg-language-ini") +if (enableCSharpFrontend) include(":cpg-language-csharp") if (enableMCPModule) include(":cpg-mcp") + kover { enableCoverage() } From 00ac486a9eaeebc66c94ae5fe5ec112af8f0a932 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Wed, 18 Feb 2026 17:13:44 +0100 Subject: [PATCH 02/55] Add todos --- .../aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt | 2 ++ .../cpg/frontends/csharp/CSharpLanguageFrontendTest.kt | 6 +++--- cpg-language-csharp/src/test/resources/csharp/helloworld.cs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt index 5974458a0df..3d2edbfd3f5 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt @@ -58,6 +58,7 @@ class CSharpLanguageFrontend(ctx: TranslationContext, language: Language Date: Mon, 23 Feb 2026 21:38:26 +0100 Subject: [PATCH 03/55] Using defined kotlin classes instead of pointer --- .../csharp/CSharpLanguageFrontend.kt | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt index 3d2edbfd3f5..f61f59842ea 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt @@ -88,9 +88,9 @@ class CSharpLanguageFrontend(ctx: TranslationContext, language: Language by lazy { - val n = CSharpLanguageFrontend.nativeLib.getNumChildren(handle) - (0 until n).map { - val childHandle = CSharpLanguageFrontend.nativeLib.getChild(handle, it) - CSharpLanguageFrontend.wrapHandle(childHandle) - } - } } // https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.compilationunitsyntax?view=roslyn-dotnet-5.0.0 From 73a788b0cc912900acdce8ec8576031146fd559c Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Thu, 26 Feb 2026 00:18:39 +0100 Subject: [PATCH 04/55] Linux support --- cpg-language-csharp/build.gradle.kts | 19 +++++++++---------- .../csharp/CSharpLanguageFrontend.kt | 13 +++++++++++-- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/cpg-language-csharp/build.gradle.kts b/cpg-language-csharp/build.gradle.kts index 23886e0d319..fe39a9c7300 100644 --- a/cpg-language-csharp/build.gradle.kts +++ b/cpg-language-csharp/build.gradle.kts @@ -4,17 +4,16 @@ dependencies { implementation("net.java.dev.jna:jna:5.18.1") } val publishNativeParser by tasks.registering(Exec::class) { + val os = System.getProperty("os.name").lowercase() + val arch = System.getProperty("os.arch").lowercase() + val rid = + when { + os.contains("mac") && arch.contains("aarch64") -> "osx-arm64" + os.contains("linux") && arch.contains("aarch64") -> "linux-arm64" + else -> error("Unsupported OS/arch: $os / $arch") + } workingDir = file("src/main/csharp/NativeParser") - val dotnet = - listOf( - "/opt/homebrew/opt/dotnet@8/bin/dotnet", - "/usr/local/bin/dotnet", - "/usr/bin/dotnet", - ) - .map { file(it) } - .firstOrNull { it.exists() } - ?.absolutePath ?: "dotnet" - commandLine(dotnet, "publish", "-c", "Release", "-r", "osx-arm64") + commandLine("dotnet", "publish", "-c", "Release", "-r", rid) } tasks.named("compileKotlin") { dependsOn(publishNativeParser) } diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt index f61f59842ea..aefbac4d082 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt @@ -48,12 +48,21 @@ class CSharpLanguageFrontend(ctx: TranslationContext, language: Language "osx-arm64" + os.contains("linux") && arch.contains("aarch64") -> "linux-arm64" + else -> throw TranslationException("Unsupported OS/arch: $os / $arch") + } + val ext = if (os.contains("mac")) "dylib" else "so" val libFile = File( - "src/main/csharp/NativeParser/bin/Release/net8.0/osx-arm64/publish/NativeParser.dylib" + "cpg-language-csharp/src/main/csharp/NativeParser/bin/Release/net8.0/$rid/publish/NativeParser.$ext" ) if (!libFile.exists()) { - throw TranslationException("nothing found") + throw TranslationException("No library found: ${libFile.absolutePath}") } Native.load(libFile.absolutePath, CSharpNativeParser::class.java) } From e61d9adc71344377ffba72944666102abc7d38b9 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Thu, 26 Feb 2026 11:51:50 +0100 Subject: [PATCH 05/55] Support linux-x64 --- cpg-language-csharp/build.gradle.kts | 5 +++-- .../aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cpg-language-csharp/build.gradle.kts b/cpg-language-csharp/build.gradle.kts index fe39a9c7300..088c2a3da26 100644 --- a/cpg-language-csharp/build.gradle.kts +++ b/cpg-language-csharp/build.gradle.kts @@ -6,14 +6,15 @@ val publishNativeParser by tasks.registering(Exec::class) { val os = System.getProperty("os.name").lowercase() val arch = System.getProperty("os.arch").lowercase() - val rid = + val runtimeIdentifier = when { os.contains("mac") && arch.contains("aarch64") -> "osx-arm64" os.contains("linux") && arch.contains("aarch64") -> "linux-arm64" + os.contains("linux") && arch.contains("amd64") -> "linux-x64" else -> error("Unsupported OS/arch: $os / $arch") } workingDir = file("src/main/csharp/NativeParser") - commandLine("dotnet", "publish", "-c", "Release", "-r", rid) + commandLine("dotnet", "publish", "-c", "Release", "-r", runtimeIdentifier) } tasks.named("compileKotlin") { dependsOn(publishNativeParser) } diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt index aefbac4d082..9f46936986a 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt @@ -50,16 +50,17 @@ class CSharpLanguageFrontend(ctx: TranslationContext, language: Language "osx-arm64" os.contains("linux") && arch.contains("aarch64") -> "linux-arm64" + os.contains("linux") && arch.contains("amd64") -> "linux-x64" else -> throw TranslationException("Unsupported OS/arch: $os / $arch") } val ext = if (os.contains("mac")) "dylib" else "so" val libFile = File( - "cpg-language-csharp/src/main/csharp/NativeParser/bin/Release/net8.0/$rid/publish/NativeParser.$ext" + "cpg-language-csharp/src/main/csharp/NativeParser/bin/Release/net8.0/$runtimeIdentifier/publish/NativeParser.$ext" ) if (!libFile.exists()) { throw TranslationException("No library found: ${libFile.absolutePath}") From 7cbb92240e77882070edd1c6f3434088e14e8f3c Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Thu, 26 Feb 2026 16:07:41 +0100 Subject: [PATCH 06/55] Refactor --- cpg-language-csharp/build.gradle.kts | 25 +-- .../src/main/csharp/NativeParser/Library.cs | 77 +--------- .../aisec/cpg/frontends/csharp/CSharp.kt | 72 +++++++++ .../csharp/CSharpLanguageFrontend.kt | 142 ++---------------- .../src/test/resources/csharp/helloworld.cs | 2 +- 5 files changed, 101 insertions(+), 217 deletions(-) create mode 100644 cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt diff --git a/cpg-language-csharp/build.gradle.kts b/cpg-language-csharp/build.gradle.kts index 088c2a3da26..cb550b8131f 100644 --- a/cpg-language-csharp/build.gradle.kts +++ b/cpg-language-csharp/build.gradle.kts @@ -4,17 +4,22 @@ dependencies { implementation("net.java.dev.jna:jna:5.18.1") } val publishNativeParser by tasks.registering(Exec::class) { - val os = System.getProperty("os.name").lowercase() - val arch = System.getProperty("os.arch").lowercase() - val runtimeIdentifier = - when { - os.contains("mac") && arch.contains("aarch64") -> "osx-arm64" - os.contains("linux") && arch.contains("aarch64") -> "linux-arm64" - os.contains("linux") && arch.contains("amd64") -> "linux-x64" - else -> error("Unsupported OS/arch: $os / $arch") - } + val osName = System.getProperty("os.name") + val os = if (osName.startsWith("Mac")) "osx" else "linux" + val arch = + System.getProperty("os.arch").replace("aarch64", "arm64").replace("x86_64", "x64") + val rid = "$os-$arch" workingDir = file("src/main/csharp/NativeParser") - commandLine("dotnet", "publish", "-c", "Release", "-r", runtimeIdentifier) + val dotnet = + listOf( + "/opt/homebrew/opt/dotnet@8/bin/dotnet", + "/usr/local/bin/dotnet", + "/usr/bin/dotnet", + ) + .map { file(it) } + .firstOrNull { it.exists() } + ?.absolutePath ?: "dotnet" + commandLine(dotnet, "publish", "-c", "Release", "-r", rid) } tasks.named("compileKotlin") { dependsOn(publishNativeParser) } diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index d728bcecdff..fc2066e3eaf 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -1,91 +1,16 @@ using System.Runtime.InteropServices; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; namespace NativeParser; public static class Library { - private static readonly Dictionary Handles = new(); - private static int _nextId = 1; - - private static IntPtr Save(object obj) - { - var id = _nextId++; - Handles[id] = obj; - return (IntPtr)id; - } - - private static T Restore(IntPtr handle) - { - return (T)Handles[(int)handle]; - } - [UnmanagedCallersOnly(EntryPoint = "parseCsharp")] public static IntPtr ParseCSharp(IntPtr sourcePtr) { var source = Marshal.PtrToStringUTF8(sourcePtr); var tree = CSharpSyntaxTree.ParseText(source); var root = tree.GetRoot(); - return Save(root); - } - - [UnmanagedCallersOnly(EntryPoint = "getKind")] - public static IntPtr GetKind(IntPtr handle) - { - var node = Restore(handle); - return Marshal.StringToCoTaskMemUTF8(node.Kind().ToString()); - } - - [UnmanagedCallersOnly(EntryPoint = "getCode")] - public static IntPtr GetCode(IntPtr handle) - { - var node = Restore(handle); - return Marshal.StringToCoTaskMemUTF8(node.ToFullString().Trim()); - } - - [UnmanagedCallersOnly(EntryPoint = "getNumChildren")] - public static int GetNumChildren(IntPtr handle) - { - var node = Restore(handle); - return node.ChildNodes().Count(); - } - - [UnmanagedCallersOnly(EntryPoint = "getChild")] - public static IntPtr GetChild(IntPtr handle, int index) - { - var node = Restore(handle); - return Save(node.ChildNodes().ElementAt(index)); - } - - [UnmanagedCallersOnly(EntryPoint = "getIdentifier")] - public static IntPtr GetIdentifier(IntPtr handle) - { - var node = Restore(handle); - var text = node switch - { - ClassDeclarationSyntax cls => cls.Identifier.Text, - _ => "UNKNOWN" - }; - return Marshal.StringToCoTaskMemUTF8(text); - } - - [UnmanagedCallersOnly(EntryPoint = "getName")] - public static IntPtr GetName(IntPtr handle) - { - var node = Restore(handle); - var text = node switch - { - BaseNamespaceDeclarationSyntax ns => ns.Name.ToString(), - _ => "UNKNOWN" - }; - return Marshal.StringToCoTaskMemUTF8(text); - } - - [UnmanagedCallersOnly(EntryPoint = "freeString")] - public static void FreeString(IntPtr ptr) - { - Marshal.FreeCoTaskMem(ptr); + return Marshal.StringToCoTaskMemUTF8(root.Kind().ToString()); } } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt new file mode 100644 index 00000000000..6300ca2ce68 --- /dev/null +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 de.fraunhofer.aisec.cpg.frontends.csharp + +import com.sun.jna.Library +import com.sun.jna.Native +import com.sun.jna.Pointer +import com.sun.jna.PointerType +import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend +import de.fraunhofer.aisec.cpg.frontends.TranslationException + +interface Csharp : Library { + open class CsharpObject(p: Pointer? = Pointer.NULL) : PointerType(p) { + val csharpType: String + get() { + return "INSTANCE.GetType(this.pointer)" + } + } + + fun parseCsharp(source: String): Pointer + + fun freeString(ptr: Pointer) + + companion object { + val INSTANCE: Csharp by lazy { + try { + val osName = System.getProperty("os.name") + val os = if (osName.startsWith("Mac")) "osx" else "linux" + val arch = + System.getProperty("os.arch") + .replace("aarch64", "arm64") + .replace("x86_64", "x64") + val rid = "$os-$arch" + val ext = if (osName.startsWith("Mac")) ".dylib" else ".so" + + val dylib = + java.io.File( + "src/main/csharp/NativeParser/bin/Release/net8.0/$rid/publish/NativeParser$ext" + ) + + LanguageFrontend.log.info("Loading NativeParser from ${dylib.absolutePath}") + + Native.load(dylib.absolutePath, Csharp::class.java) + } catch (ex: Exception) { + throw TranslationException("Error loading C# native library: $ex") + } + } + } +} diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt index 9f46936986a..2b152f126f8 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt @@ -25,18 +25,11 @@ */ package de.fraunhofer.aisec.cpg.frontends.csharp -import com.sun.jna.Library -import com.sun.jna.Native -import com.sun.jna.Pointer import de.fraunhofer.aisec.cpg.TranslationContext import de.fraunhofer.aisec.cpg.frontends.Language import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend -import de.fraunhofer.aisec.cpg.frontends.TranslationException -import de.fraunhofer.aisec.cpg.graph.DeclarationHolder import de.fraunhofer.aisec.cpg.graph.Node import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration -import de.fraunhofer.aisec.cpg.graph.newNamespaceDeclaration -import de.fraunhofer.aisec.cpg.graph.newRecordDeclaration import de.fraunhofer.aisec.cpg.graph.newTranslationUnitDeclaration import de.fraunhofer.aisec.cpg.graph.types.Type import de.fraunhofer.aisec.cpg.graph.unknownType @@ -44,137 +37,26 @@ import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation import java.io.File class CSharpLanguageFrontend(ctx: TranslationContext, language: Language) : - LanguageFrontend(ctx, language) { - - companion object { - internal val nativeLib: CSharpNativeParser by lazy { - val os = System.getProperty("os.name").lowercase() - val arch = System.getProperty("os.arch").lowercase() - val runtimeIdentifier = - when { - os.contains("mac") && arch.contains("aarch64") -> "osx-arm64" - os.contains("linux") && arch.contains("aarch64") -> "linux-arm64" - os.contains("linux") && arch.contains("amd64") -> "linux-x64" - else -> throw TranslationException("Unsupported OS/arch: $os / $arch") - } - val ext = if (os.contains("mac")) "dylib" else "so" - val libFile = - File( - "cpg-language-csharp/src/main/csharp/NativeParser/bin/Release/net8.0/$runtimeIdentifier/publish/NativeParser.$ext" - ) - if (!libFile.exists()) { - throw TranslationException("No library found: ${libFile.absolutePath}") - } - Native.load(libFile.absolutePath, CSharpNativeParser::class.java) - } - - // TODO: Do we need the free call? - internal fun nativeString(ptr: Pointer): String { - return try { - ptr.getString(0, "UTF-8") - } finally { - nativeLib.freeString(ptr) - } - } - - // TODO: Change the type of the native parser functions like in the go frontend - fun wrapHandle(handle: Pointer): CSharpSyntaxNode { - val kind = nativeString(nativeLib.getKind(handle)) - return when (kind) { - "CompilationUnit" -> CompilationUnitSyntax(handle) - "NamespaceDeclaration" -> NamespaceDeclarationSyntax(handle) - "ClassDeclaration" -> ClassDeclarationSyntax(handle) - else -> CSharpSyntaxNode(handle) - } - } - } + LanguageFrontend(ctx, language) { override fun parse(file: File): TranslationUnitDeclaration { val source = file.readText() - val rootHandle = nativeLib.parseCsharp(source) - val root = wrapHandle(rootHandle) - - val tu = newTranslationUnitDeclaration(file.name, rawNode = root) - scopeManager.resetToGlobal(tu) - scopeManager.enterScope(tu) - - // for (child in root.children) { - // handleNode(child, tu) - // } + val ptr = Csharp.INSTANCE.parseCsharp(source) + val rootKind = ptr.getString(0, "UTF-8") + if (rootKind == "CompilationUnit") { + newTranslationUnitDeclaration(file.name) + } + log.info("C# root node kind: $rootKind") - scopeManager.leaveScope(tu) + val tu = newTranslationUnitDeclaration(file.name) return tu } - private fun handleNode(node: CSharpSyntaxNode, parent: DeclarationHolder) { - when (node) { - is NamespaceDeclarationSyntax -> { - val ns = newNamespaceDeclaration(node.name, rawNode = node) - scopeManager.enterScope(ns) - - // for (child in node.children) { - // handleNode(child, ns) - // } - - scopeManager.leaveScope(ns) - scopeManager.addDeclaration(ns) - parent.addDeclaration(ns) - } - is ClassDeclarationSyntax -> { - val record = newRecordDeclaration(node.identifier, "class", rawNode = node) - scopeManager.enterScope(record) - scopeManager.leaveScope(record) - scopeManager.addDeclaration(record) - parent.addDeclaration(record) - } - } - } - - override fun typeOf(type: CSharpSyntaxNode): Type = unknownType() - - override fun codeOf(astNode: CSharpSyntaxNode): String = astNode.code - - override fun locationOf(astNode: CSharpSyntaxNode): PhysicalLocation? = null + override fun typeOf(type: String): Type = unknownType() - override fun setComment(node: Node, astNode: CSharpSyntaxNode) {} -} - -interface CSharpNativeParser : Library { - fun parseCsharp(source: String): Pointer - - fun getKind(handle: Pointer): Pointer - - fun getCode(handle: Pointer): Pointer - - fun getNumChildren(handle: Pointer): Int + override fun codeOf(astNode: String): String = astNode - fun getChild(handle: CSharpSyntaxNode, index: Int): CSharpSyntaxNode - - fun getIdentifier(handle: Pointer): Pointer - - fun getName(handle: Pointer): Pointer - - fun freeString(ptr: Pointer) -} + override fun locationOf(astNode: String): PhysicalLocation? = null -// https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.csharpsyntaxnode?view=roslyn-dotnet-5.0.0 -open class CSharpSyntaxNode(val handle: Pointer) { - val code: String by lazy { - CSharpLanguageFrontend.nativeString(CSharpLanguageFrontend.nativeLib.getCode(handle)) - } -} - -// https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.compilationunitsyntax?view=roslyn-dotnet-5.0.0 -class CompilationUnitSyntax(handle: Pointer) : CSharpSyntaxNode(handle) - -class NamespaceDeclarationSyntax(handle: Pointer) : CSharpSyntaxNode(handle) { - val name: String by lazy { - CSharpLanguageFrontend.nativeString(CSharpLanguageFrontend.nativeLib.getName(handle)) - } -} - -class ClassDeclarationSyntax(handle: Pointer) : CSharpSyntaxNode(handle) { - val identifier: String by lazy { - CSharpLanguageFrontend.nativeString(CSharpLanguageFrontend.nativeLib.getIdentifier(handle)) - } + override fun setComment(node: Node, astNode: String) {} } diff --git a/cpg-language-csharp/src/test/resources/csharp/helloworld.cs b/cpg-language-csharp/src/test/resources/csharp/helloworld.cs index f7f7cf7de1b..6530d7e862e 100644 --- a/cpg-language-csharp/src/test/resources/csharp/helloworld.cs +++ b/cpg-language-csharp/src/test/resources/csharp/helloworld.cs @@ -1,4 +1,4 @@ namespace HelloWorld { - class Foo { } + class Foo {} } From edca954f09ffcb7c2619187720a6c57f776dab02 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Thu, 26 Feb 2026 16:18:06 +0100 Subject: [PATCH 07/55] Support amd64 --- cpg-language-csharp/build.gradle.kts | 2 +- .../kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cpg-language-csharp/build.gradle.kts b/cpg-language-csharp/build.gradle.kts index cb550b8131f..49ba3bbe268 100644 --- a/cpg-language-csharp/build.gradle.kts +++ b/cpg-language-csharp/build.gradle.kts @@ -7,7 +7,7 @@ val publishNativeParser by val osName = System.getProperty("os.name") val os = if (osName.startsWith("Mac")) "osx" else "linux" val arch = - System.getProperty("os.arch").replace("aarch64", "arm64").replace("x86_64", "x64") + System.getProperty("os.arch").replace("aarch64", "arm64").replace("amd64", "amd64") val rid = "$os-$arch" workingDir = file("src/main/csharp/NativeParser") val dotnet = diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 6300ca2ce68..e1a847e83a1 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -52,7 +52,7 @@ interface Csharp : Library { val arch = System.getProperty("os.arch") .replace("aarch64", "arm64") - .replace("x86_64", "x64") + .replace("amd64", "amd64") val rid = "$os-$arch" val ext = if (osName.startsWith("Mac")) ".dylib" else ".so" From 5776bad7f9f2117740645657b7730f28e11cf906 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Thu, 26 Feb 2026 16:21:33 +0100 Subject: [PATCH 08/55] Change rid --- cpg-language-csharp/build.gradle.kts | 3 +-- .../kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cpg-language-csharp/build.gradle.kts b/cpg-language-csharp/build.gradle.kts index 49ba3bbe268..3ac4ec158d8 100644 --- a/cpg-language-csharp/build.gradle.kts +++ b/cpg-language-csharp/build.gradle.kts @@ -6,8 +6,7 @@ val publishNativeParser by tasks.registering(Exec::class) { val osName = System.getProperty("os.name") val os = if (osName.startsWith("Mac")) "osx" else "linux" - val arch = - System.getProperty("os.arch").replace("aarch64", "arm64").replace("amd64", "amd64") + val arch = System.getProperty("os.arch").replace("aarch64", "arm64").replace("amd64", "x64") val rid = "$os-$arch" workingDir = file("src/main/csharp/NativeParser") val dotnet = diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index e1a847e83a1..fb3873d54c5 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -52,7 +52,7 @@ interface Csharp : Library { val arch = System.getProperty("os.arch") .replace("aarch64", "arm64") - .replace("amd64", "amd64") + .replace("amd64", "x64") val rid = "$os-$arch" val ext = if (osName.startsWith("Mac")) ".dylib" else ".so" From 08474562f06f107b6b188c88a0350c96986158a0 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Fri, 27 Feb 2026 20:54:29 +0100 Subject: [PATCH 09/55] Fix kdoc links --- .../src/main/csharp/NativeParser/Library.cs | 31 +++++++-- .../aisec/cpg/frontends/csharp/CSharp.kt | 66 ++++++++++++++++++- .../csharp/CSharpLanguageFrontend.kt | 20 ++---- 3 files changed, 96 insertions(+), 21 deletions(-) diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index fc2066e3eaf..2dcc0a4cba6 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -1,16 +1,37 @@ using System.Runtime.InteropServices; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace NativeParser; public static class Library { - [UnmanagedCallersOnly(EntryPoint = "parseCsharp")] - public static IntPtr ParseCSharp(IntPtr sourcePtr) + private static readonly Dictionary Nodes = new(); + private static int _nextId = 1; + + private static IntPtr Register(CSharpSyntaxNode node) + { + var ptr = new IntPtr(_nextId++); + Nodes[ptr] = node; + return ptr; + } + + [UnmanagedCallersOnly(EntryPoint = "CSharpRoslynSyntaxTreeParseText")] + public static IntPtr CSharpRoslynSyntaxTreeParseText(IntPtr sourcePtr) { + // Kotlin sends the source code as string. JNA converts it into a C char*-pointer. + // Marshal gets the pointer and converts it to an .NET string var source = Marshal.PtrToStringUTF8(sourcePtr); - var tree = CSharpSyntaxTree.ParseText(source); - var root = tree.GetRoot(); - return Marshal.StringToCoTaskMemUTF8(root.Kind().ToString()); + // Roslyn parses the source code -> AST + var root = CSharpSyntaxTree.ParseText(source).GetRoot(); + return Register(root); + } + + [UnmanagedCallersOnly(EntryPoint = "GetKind")] + public static IntPtr GetKind(IntPtr handlePtr) + { + var kind = Nodes[handlePtr].Kind().ToString(); + return Marshal.StringToCoTaskMemUTF8(kind); } } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index fb3873d54c5..4eda074c88f 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -32,17 +32,77 @@ import com.sun.jna.PointerType import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend import de.fraunhofer.aisec.cpg.frontends.TranslationException +/** This interface encapsulates C# <-> Kotlin translation objects. */ interface Csharp : Library { + /** Base class for all JNA pointer wrappers in this interface. */ open class CsharpObject(p: Pointer? = Pointer.NULL) : PointerType(p) { val csharpType: String get() { - return "INSTANCE.GetType(this.pointer)" + return INSTANCE.GetKind(this.pointer) } } - fun parseCsharp(source: String): Pointer + /** + * This interface represents the `Microsoft.CodeAnalysis.CSharp.Syntax` namespace in Roslyn. It + * contains classes representing the syntax nodes of the C# AST as returned by Roslyn's parser. + */ + interface Ast { + /** + * Base class for all C# syntax nodes. Represents Roslyn's + * `Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode`. + * + * See + * [CSharpSyntaxNode](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.csharpsyntaxnode?view=roslyn-dotnet-5.0.0) + */ + open class Node(p: Pointer? = Pointer.NULL) : CsharpObject(p) - fun freeString(ptr: Pointer) + /** + * Represents the Roslyn `CompilationUnitSyntax` class. + * + * See + * [CompilationUnitSyntax](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.compilationunitsyntax?view=roslyn-dotnet-5.0.0) + */ + class CompilationUnitSyntax(p: Pointer? = Pointer.NULL) : Node(p) + + /** + * Represents the Roslyn `NamespaceDeclarationSyntax` class. + * + * See + * [NamespaceDeclarationSyntax](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.namespacedeclarationsyntax?view=roslyn-dotnet-5.0.0) + */ + class NamespaceDeclarationSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val name: String by lazy { INSTANCE.GetNamespaceDeclarationName(this) } + } + + /** + * Represents the Roslyn `ClassDeclarationSyntax` class. + * + * See + * [ClassDeclarationSyntax](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.classdeclarationsyntax?view=roslyn-dotnet-5.0.0) + */ + class ClassDeclarationSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val identifier: String by lazy { INSTANCE.GetClassDeclarationIdentifier(this) } + } + } + + /** + * Represents the Roslyn `CSharpSyntaxTree` class. + * + * See: TODO(link) + */ + object CSharpSyntaxTree { + fun parseText(source: String): Ast.CompilationUnitSyntax { + return Ast.CompilationUnitSyntax(INSTANCE.CSharpRoslynSyntaxTreeParseText(source)) + } + } + + fun CSharpRoslynSyntaxTreeParseText(source: String): Pointer + + fun GetKind(handle: Pointer): String + + fun GetNamespaceDeclarationName(handle: Ast.NamespaceDeclarationSyntax): String + + fun GetClassDeclarationIdentifier(handle: Ast.ClassDeclarationSyntax): String companion object { val INSTANCE: Csharp by lazy { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt index 2b152f126f8..5bf888a381b 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt @@ -37,26 +37,20 @@ import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation import java.io.File class CSharpLanguageFrontend(ctx: TranslationContext, language: Language) : - LanguageFrontend(ctx, language) { + LanguageFrontend(ctx, language) { override fun parse(file: File): TranslationUnitDeclaration { val source = file.readText() - val ptr = Csharp.INSTANCE.parseCsharp(source) - val rootKind = ptr.getString(0, "UTF-8") - if (rootKind == "CompilationUnit") { - newTranslationUnitDeclaration(file.name) - } - log.info("C# root node kind: $rootKind") - - val tu = newTranslationUnitDeclaration(file.name) + val root = Csharp.CSharpSyntaxTree.parseText(source) + val tu = newTranslationUnitDeclaration(file.name, rawNode = root) return tu } - override fun typeOf(type: String): Type = unknownType() + override fun typeOf(type: Csharp.Ast.Node): Type = unknownType() - override fun codeOf(astNode: String): String = astNode + override fun codeOf(astNode: Csharp.Ast.Node): String? = null - override fun locationOf(astNode: String): PhysicalLocation? = null + override fun locationOf(astNode: Csharp.Ast.Node): PhysicalLocation? = null - override fun setComment(node: Node, astNode: String) {} + override fun setComment(node: Node, astNode: Csharp.Ast.Node) {} } From 3d36f1b3f3f03a13d3760963500747909f37cf46 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Fri, 27 Feb 2026 20:58:50 +0100 Subject: [PATCH 10/55] Add dotnet version in build.yml --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 959a4a6c1f9..2d344fabc0e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,6 +40,9 @@ jobs: - uses: actions/setup-go@v6 with: go-version-file: cpg-language-go/src/test/resources/golang/integration/go.mod + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0" - name: Setup neo4j run: | docker run -d --env NEO4J_AUTH=neo4j/password -p7474:7474 -p7687:7687 -e NEO4JLABS_PLUGINS='["apoc"]' neo4j:5 || true From 70ea6f3adff4ebbc3cfdfb94fa8b82d6e6de1847 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Fri, 27 Feb 2026 21:03:53 +0100 Subject: [PATCH 11/55] Put dotnet to another folder where root is not needed --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2d344fabc0e..738be815b10 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,6 +43,8 @@ jobs: - uses: actions/setup-dotnet@v4 with: dotnet-version: "8.0" + env: + DOTNET_INSTALL_DIR: ~/.dotnet - name: Setup neo4j run: | docker run -d --env NEO4J_AUTH=neo4j/password -p7474:7474 -p7687:7687 -e NEO4JLABS_PLUGINS='["apoc"]' neo4j:5 || true From 1bf34b3603629c6791f7e411a7186f9b35833224 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Fri, 27 Feb 2026 21:06:30 +0100 Subject: [PATCH 12/55] Cast syntaxNode to CSharpSyntaxNode --- cpg-language-csharp/src/main/csharp/NativeParser/Library.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 2dcc0a4cba6..e38f10e7575 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -24,7 +24,7 @@ public static IntPtr CSharpRoslynSyntaxTreeParseText(IntPtr sourcePtr) // Marshal gets the pointer and converts it to an .NET string var source = Marshal.PtrToStringUTF8(sourcePtr); // Roslyn parses the source code -> AST - var root = CSharpSyntaxTree.ParseText(source).GetRoot(); + var root = (CSharpSyntaxNode)CSharpSyntaxTree.ParseText(source).GetRoot(); return Register(root); } From 4acd95a78e366fa7fdf85e2671b92120d0f871cf Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Fri, 27 Feb 2026 21:16:11 +0100 Subject: [PATCH 13/55] Add csharp config in configure_frontends --- configure_frontends.sh | 2 ++ .../csharp/CSharpLanguageFrontendTest.kt | 15 +++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/configure_frontends.sh b/configure_frontends.sh index 8ff8627683e..4e7a298242a 100755 --- a/configure_frontends.sh +++ b/configure_frontends.sh @@ -62,5 +62,7 @@ answerJVM=$(ask "Do you want to enable the JVM frontend? (currently $(getPropert setProperty "enableJVMFrontend" $answerJVM answerINI=$(ask "Do you want to enable the INI frontend? (currently $(getProperty "enableINIFrontend"))") setProperty "enableINIFrontend" $answerINI +answerCsharp=$(ask "Do you want to enable the C# frontend? (currently $(getProperty "enableCSharpFrontend"))") +setProperty "enableCSharpFrontend" answerCsharp answerMCP=$(ask "Do you want to enable the MCP module? (currently $(getProperty "enableMCPModule"))") setProperty "enableMCPModule" $answerMCP diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt index ef04df4d575..b1259027f91 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt @@ -29,7 +29,6 @@ import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.test.* import java.nio.file.Path import kotlin.test.Test -import kotlin.test.assertNotNull class CSharpLanguageFrontendTest : BaseTest() { @@ -44,12 +43,12 @@ class CSharpLanguageFrontendTest : BaseTest() { ) { it.registerLanguage() } - assertNotNull(tu) - - val ns = tu.namespaces["HelloWorld"] - assertNotNull(ns) - - val foo = ns.records["Foo"] - assertNotNull(foo) + // assertNotNull(tu) + // + // val ns = tu.namespaces["HelloWorld"] + // assertNotNull(ns) + // + // val foo = ns.records["Foo"] + // assertNotNull(foo) } } From 23632f4a01b395a5eaccd690a93fe80c28ea2ce0 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Fri, 27 Feb 2026 21:20:58 +0100 Subject: [PATCH 14/55] Fix build --- .../aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt index 5bf888a381b..947bbdb2ae0 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt @@ -29,8 +29,8 @@ import de.fraunhofer.aisec.cpg.TranslationContext import de.fraunhofer.aisec.cpg.frontends.Language import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend import de.fraunhofer.aisec.cpg.graph.Node -import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration -import de.fraunhofer.aisec.cpg.graph.newTranslationUnitDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnit +import de.fraunhofer.aisec.cpg.graph.newTranslationUnit import de.fraunhofer.aisec.cpg.graph.types.Type import de.fraunhofer.aisec.cpg.graph.unknownType import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation @@ -39,10 +39,10 @@ import java.io.File class CSharpLanguageFrontend(ctx: TranslationContext, language: Language) : LanguageFrontend(ctx, language) { - override fun parse(file: File): TranslationUnitDeclaration { + override fun parse(file: File): TranslationUnit { val source = file.readText() val root = Csharp.CSharpSyntaxTree.parseText(source) - val tu = newTranslationUnitDeclaration(file.name, rawNode = root) + val tu = newTranslationUnit(file.name, rawNode = root) return tu } From 7ce3e86369872cfa56d9e6590f14474259b7185e Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Sat, 28 Feb 2026 23:11:50 +0100 Subject: [PATCH 15/55] Add DeclarationHandler handling namespace and class declarations --- cpg-language-csharp/build.gradle.kts | 1 + .../src/main/csharp/NativeParser/Library.cs | 40 +++++++++ .../aisec/cpg/frontends/csharp/CSharp.kt | 84 +++++++++++++------ .../cpg/frontends/csharp/CSharpHandler.kt | 49 +++++++++++ .../csharp/CSharpLanguageFrontend.kt | 24 ++++-- .../frontends/csharp/DeclarationHandler.kt | 63 ++++++++++++++ .../csharp/CSharpLanguageFrontendTest.kt | 15 ++-- 7 files changed, 240 insertions(+), 36 deletions(-) create mode 100644 cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpHandler.kt create mode 100644 cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt diff --git a/cpg-language-csharp/build.gradle.kts b/cpg-language-csharp/build.gradle.kts index 3ac4ec158d8..da38ea3689b 100644 --- a/cpg-language-csharp/build.gradle.kts +++ b/cpg-language-csharp/build.gradle.kts @@ -4,6 +4,7 @@ dependencies { implementation("net.java.dev.jna:jna:5.18.1") } val publishNativeParser by tasks.registering(Exec::class) { + // TODO: Check again https://learn.microsoft.com/en-us/dotnet/core/rid-catalog val osName = System.getProperty("os.name") val os = if (osName.startsWith("Mac")) "osx" else "linux" val arch = System.getProperty("os.arch").replace("aarch64", "arm64").replace("amd64", "x64") diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index e38f10e7575..db2d10a0156 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -34,4 +34,44 @@ public static IntPtr GetKind(IntPtr handlePtr) var kind = Nodes[handlePtr].Kind().ToString(); return Marshal.StringToCoTaskMemUTF8(kind); } + + [UnmanagedCallersOnly(EntryPoint = "GetCompilationUnitMembersCount")] + public static int GetCompilationUnitMembersCount(IntPtr handlePtr) + { + return ((CompilationUnitSyntax)Nodes[handlePtr]).Members.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetCompilationUnitMember")] + public static IntPtr GetCompilationUnitMember(IntPtr handlePtr, int index) + { + return Register(((CompilationUnitSyntax)Nodes[handlePtr]).Members[index]); + } + + [UnmanagedCallersOnly(EntryPoint = "GetNamespaceDeclarationName")] + public static IntPtr GetNamespaceDeclarationName(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8( + ((NamespaceDeclarationSyntax)Nodes[handlePtr]).Name.ToString() + ); + } + + [UnmanagedCallersOnly(EntryPoint = "GetNamespaceDeclarationMembersCount")] + public static int GetNamespaceDeclarationMembersCount(IntPtr handlePtr) + { + return ((NamespaceDeclarationSyntax)Nodes[handlePtr]).Members.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetNamespaceDeclarationMember")] + public static IntPtr GetNamespaceDeclarationMember(IntPtr handlePtr, int index) + { + return Register(((NamespaceDeclarationSyntax)Nodes[handlePtr]).Members[index]); + } + + [UnmanagedCallersOnly(EntryPoint = "GetClassDeclarationIdentifier")] + public static IntPtr GetClassDeclarationIdentifier(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8( + ((ClassDeclarationSyntax)Nodes[handlePtr]).Identifier.ToString() + ); + } } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 4eda074c88f..158d355d2d5 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -25,6 +25,7 @@ */ package de.fraunhofer.aisec.cpg.frontends.csharp +import com.sun.jna.FromNativeContext import com.sun.jna.Library import com.sun.jna.Native import com.sun.jna.Pointer @@ -46,41 +47,62 @@ interface Csharp : Library { * This interface represents the `Microsoft.CodeAnalysis.CSharp.Syntax` namespace in Roslyn. It * contains classes representing the syntax nodes of the C# AST as returned by Roslyn's parser. */ - interface Ast { + interface AST { /** * Base class for all C# syntax nodes. Represents Roslyn's - * `Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode`. - * - * See - * [CSharpSyntaxNode](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.csharpsyntaxnode?view=roslyn-dotnet-5.0.0) + * [`CSharpSyntaxNode`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.csharpsyntaxnode?view=roslyn-dotnet-5.0.0) */ open class Node(p: Pointer? = Pointer.NULL) : CsharpObject(p) /** - * Represents the Roslyn `CompilationUnitSyntax` class. - * - * See - * [CompilationUnitSyntax](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.compilationunitsyntax?view=roslyn-dotnet-5.0.0) + * Represents the Roslyn + * [`CompilationUnitSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.compilationunitsyntax?view=roslyn-dotnet-5.0.0) + * class. */ - class CompilationUnitSyntax(p: Pointer? = Pointer.NULL) : Node(p) + class CompilationUnitSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val members: List by lazy { + val count = INSTANCE.GetCompilationUnitMembersCount(this) + (0 until count).map { i -> INSTANCE.GetCompilationUnitMember(this, i) } + } + } + + /** + * Represents the Roslyn + * [MemberDeclarationSyntax](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.memberdeclarationsyntax?view=roslyn-dotnet-5.0.0) + * class. + */ + open class MemberDeclarationSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + override fun fromNative(nativeValue: Any?, context: FromNativeContext?): Any { + if (nativeValue !is Pointer) { + return super.fromNative(nativeValue, context) + } + return when (INSTANCE.GetKind(nativeValue)) { + "NamespaceDeclaration" -> NamespaceDeclarationSyntax(nativeValue) + "ClassDeclaration" -> ClassDeclarationSyntax(nativeValue) + else -> super.fromNative(nativeValue, context) + } + } + } /** - * Represents the Roslyn `NamespaceDeclarationSyntax` class. - * - * See - * [NamespaceDeclarationSyntax](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.namespacedeclarationsyntax?view=roslyn-dotnet-5.0.0) + * Represents the Roslyn + * [`NamespaceDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.namespacedeclarationsyntax?view=roslyn-dotnet-5.0.0) + * class. */ - class NamespaceDeclarationSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + class NamespaceDeclarationSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { val name: String by lazy { INSTANCE.GetNamespaceDeclarationName(this) } + val members: List by lazy { + val count = INSTANCE.GetNamespaceDeclarationMembersCount(this) + (0 until count).map { i -> INSTANCE.GetNamespaceDeclarationMember(this, i) } + } } /** - * Represents the Roslyn `ClassDeclarationSyntax` class. - * - * See - * [ClassDeclarationSyntax](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.classdeclarationsyntax?view=roslyn-dotnet-5.0.0) + * Represents the Roslyn + * [`ClassDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.classdeclarationsyntax?view=roslyn-dotnet-5.0.0) + * class. */ - class ClassDeclarationSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + class ClassDeclarationSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { val identifier: String by lazy { INSTANCE.GetClassDeclarationIdentifier(this) } } } @@ -91,8 +113,8 @@ interface Csharp : Library { * See: TODO(link) */ object CSharpSyntaxTree { - fun parseText(source: String): Ast.CompilationUnitSyntax { - return Ast.CompilationUnitSyntax(INSTANCE.CSharpRoslynSyntaxTreeParseText(source)) + fun parseText(source: String): AST.CompilationUnitSyntax { + return AST.CompilationUnitSyntax(INSTANCE.CSharpRoslynSyntaxTreeParseText(source)) } } @@ -100,9 +122,23 @@ interface Csharp : Library { fun GetKind(handle: Pointer): String - fun GetNamespaceDeclarationName(handle: Ast.NamespaceDeclarationSyntax): String + fun GetCompilationUnitMembersCount(handle: AST.CompilationUnitSyntax): Int + + fun GetCompilationUnitMember( + handle: AST.CompilationUnitSyntax, + index: Int, + ): AST.MemberDeclarationSyntax + + fun GetNamespaceDeclarationName(handle: AST.NamespaceDeclarationSyntax): String + + fun GetNamespaceDeclarationMembersCount(handle: AST.NamespaceDeclarationSyntax): Int + + fun GetNamespaceDeclarationMember( + handle: AST.NamespaceDeclarationSyntax, + index: Int, + ): AST.MemberDeclarationSyntax - fun GetClassDeclarationIdentifier(handle: Ast.ClassDeclarationSyntax): String + fun GetClassDeclarationIdentifier(handle: AST.ClassDeclarationSyntax): String companion object { val INSTANCE: Csharp by lazy { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpHandler.kt new file mode 100644 index 00000000000..b113588ceee --- /dev/null +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpHandler.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 de.fraunhofer.aisec.cpg.frontends.csharp + +import de.fraunhofer.aisec.cpg.frontends.Handler +import de.fraunhofer.aisec.cpg.graph.Node +import java.util.function.Supplier + +/** Base class for all C# handlers. */ +abstract class CSharpHandler( + configConstructor: Supplier, + frontend: CSharpLanguageFrontend, +) : Handler(configConstructor, frontend) { + + override fun handle(ctx: Csharp.AST.Node): ResultNode { + val node = handleNode(ctx) + + frontend.setComment(node, ctx) + frontend.process(ctx, node) + + lastNode = node + return node + } + + abstract fun handleNode(node: Csharp.AST.Node): ResultNode +} diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt index 947bbdb2ae0..057279c8855 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt @@ -37,20 +37,34 @@ import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation import java.io.File class CSharpLanguageFrontend(ctx: TranslationContext, language: Language) : - LanguageFrontend(ctx, language) { + LanguageFrontend(ctx, language) { + + val declarationHandler = DeclarationHandler(this) override fun parse(file: File): TranslationUnit { val source = file.readText() val root = Csharp.CSharpSyntaxTree.parseText(source) val tu = newTranslationUnit(file.name, rawNode = root) + + scopeManager.resetToGlobal(tu) + currentTU = tu + scopeManager.enterScope(tu) + + for (member in root.members) { + val decl = declarationHandler.handle(member) + scopeManager.addDeclaration(decl) + tu.addDeclaration(decl) + } + + scopeManager.leaveScope(tu) return tu } - override fun typeOf(type: Csharp.Ast.Node): Type = unknownType() + override fun typeOf(type: Csharp.AST.Node): Type = unknownType() - override fun codeOf(astNode: Csharp.Ast.Node): String? = null + override fun codeOf(astNode: Csharp.AST.Node): String? = null - override fun locationOf(astNode: Csharp.Ast.Node): PhysicalLocation? = null + override fun locationOf(astNode: Csharp.AST.Node): PhysicalLocation? = null - override fun setComment(node: Node, astNode: Csharp.Ast.Node) {} + override fun setComment(node: Node, astNode: Csharp.AST.Node) {} } diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt new file mode 100644 index 00000000000..e095f6c5a03 --- /dev/null +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 de.fraunhofer.aisec.cpg.frontends.csharp + +import de.fraunhofer.aisec.cpg.graph.declarations.Declaration +import de.fraunhofer.aisec.cpg.graph.declarations.ProblemDeclaration +import de.fraunhofer.aisec.cpg.graph.newNamespace +import de.fraunhofer.aisec.cpg.graph.newRecord + +class DeclarationHandler(frontend: CSharpLanguageFrontend) : + CSharpHandler(::ProblemDeclaration, frontend) { + + override fun handleNode(node: Csharp.AST.Node): Declaration { + return when (node) { + is Csharp.AST.NamespaceDeclarationSyntax -> handleNamespaceDeclaration(node) + is Csharp.AST.ClassDeclarationSyntax -> handleClassDeclaration(node) + else -> ProblemDeclaration("Not supported: ${node.csharpType}") + } + } + + private fun handleNamespaceDeclaration( + node: Csharp.AST.NamespaceDeclarationSyntax + ): Declaration { + val namespace = newNamespace(node.name, rawNode = node) + frontend.scopeManager.enterScope(namespace) + + for (member in node.members) { + val decl = handle(member) + frontend.scopeManager.addDeclaration(decl) + namespace.addDeclaration(decl) + } + + frontend.scopeManager.leaveScope(namespace) + return namespace + } + + private fun handleClassDeclaration(node: Csharp.AST.ClassDeclarationSyntax): Declaration { + return newRecord(node.identifier, "class", rawNode = node) + } +} diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt index b1259027f91..ef04df4d575 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt @@ -29,6 +29,7 @@ import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.test.* import java.nio.file.Path import kotlin.test.Test +import kotlin.test.assertNotNull class CSharpLanguageFrontendTest : BaseTest() { @@ -43,12 +44,12 @@ class CSharpLanguageFrontendTest : BaseTest() { ) { it.registerLanguage() } - // assertNotNull(tu) - // - // val ns = tu.namespaces["HelloWorld"] - // assertNotNull(ns) - // - // val foo = ns.records["Foo"] - // assertNotNull(foo) + assertNotNull(tu) + + val ns = tu.namespaces["HelloWorld"] + assertNotNull(ns) + + val foo = ns.records["Foo"] + assertNotNull(foo) } } From 6495ee4c3e1c4ec01d5b84f08322e3a44b2372e3 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Mon, 2 Mar 2026 14:20:01 +0100 Subject: [PATCH 16/55] Add class and method declaration handling --- .../src/main/csharp/NativeParser/Library.cs | 20 ++++++++ .../aisec/cpg/frontends/csharp/CSharp.kt | 46 +++++++++++++++++++ .../cpg/frontends/csharp/CSharpHandler.kt | 1 + .../frontends/csharp/DeclarationHandler.kt | 18 +++++++- .../csharp/CSharpLanguageFrontendTest.kt | 3 ++ .../src/test/resources/csharp/helloworld.cs | 5 +- 6 files changed, 91 insertions(+), 2 deletions(-) diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index db2d10a0156..446d1b1c138 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -74,4 +74,24 @@ public static IntPtr GetClassDeclarationIdentifier(IntPtr handlePtr) ((ClassDeclarationSyntax)Nodes[handlePtr]).Identifier.ToString() ); } + + [UnmanagedCallersOnly(EntryPoint = "GetClassDeclarationMembersCount")] + public static int GetClassDeclarationMembersCount(IntPtr handlePtr) + { + return ((ClassDeclarationSyntax)Nodes[handlePtr]).Members.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetClassDeclarationMember")] + public static IntPtr GetClassDeclarationMember(IntPtr handlePtr, int index) + { + return Register(((ClassDeclarationSyntax)Nodes[handlePtr]).Members[index]); + } + + [UnmanagedCallersOnly(EntryPoint = "GetMethodDeclarationIdentifier")] + public static IntPtr GetMethodDeclarationIdentifier(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8( + ((MethodDeclarationSyntax)Nodes[handlePtr]).Identifier.ToString() + ); + } } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 158d355d2d5..3727a42d739 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -72,6 +72,12 @@ interface Csharp : Library { * class. */ open class MemberDeclarationSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + /** + * JNA calls this method automatically when converting a native pointer which is + * returned by a JNA function into a [MemberDeclarationSyntax] object. We override it to + * return the concrete subtype (e.g. [NamespaceDeclarationSyntax]) based on the Roslyn + * kind string. + */ override fun fromNative(nativeValue: Any?, context: FromNativeContext?): Any { if (nativeValue !is Pointer) { return super.fromNative(nativeValue, context) @@ -104,6 +110,37 @@ interface Csharp : Library { */ class ClassDeclarationSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { val identifier: String by lazy { INSTANCE.GetClassDeclarationIdentifier(this) } + val members: List by lazy { + val count = INSTANCE.GetClassDeclarationMembersCount(this) + (0 until count).map { i -> INSTANCE.GetClassDeclarationMember(this, i) } + } + } + + /** + * Represents the Roslyn + * [`BaseMethodDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.basemethoddeclarationsyntax?view=roslyn-dotnet-5.0.0) + * class. + */ + open class BaseMethodDeclarationSyntax(p: Pointer? = Pointer.NULL) : + MemberDeclarationSyntax(p) { + override fun fromNative(nativeValue: Any?, context: FromNativeContext?): Any { + if (nativeValue !is Pointer) { + return super.fromNative(nativeValue, context) + } + return when (INSTANCE.GetKind(nativeValue)) { + "MethodDeclaration" -> MethodDeclarationSyntax(nativeValue) + else -> super.fromNative(nativeValue, context) + } + } + } + + /** + * Represents the Roslyn + * [`MethodDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.methoddeclarationsyntax?view=roslyn-dotnet-5.0.0) + * class. + */ + class MethodDeclarationSyntax(p: Pointer? = Pointer.NULL) : BaseMethodDeclarationSyntax(p) { + val identifier: String by lazy { INSTANCE.GetMethodDeclarationIdentifier(this) } } } @@ -140,6 +177,15 @@ interface Csharp : Library { fun GetClassDeclarationIdentifier(handle: AST.ClassDeclarationSyntax): String + fun GetClassDeclarationMembersCount(handle: AST.ClassDeclarationSyntax): Int + + fun GetClassDeclarationMember( + handle: AST.ClassDeclarationSyntax, + index: Int, + ): AST.BaseMethodDeclarationSyntax + + fun GetMethodDeclarationIdentifier(handle: AST.MethodDeclarationSyntax): String + companion object { val INSTANCE: Csharp by lazy { try { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpHandler.kt index b113588ceee..b096ad3363a 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpHandler.kt @@ -38,6 +38,7 @@ abstract class CSharpHandler( override fun handle(ctx: Csharp.AST.Node): ResultNode { val node = handleNode(ctx) + // TODO: implement setComment frontend.setComment(node, ctx) frontend.process(ctx, node) diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt index e095f6c5a03..b76b05f13d4 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -27,6 +27,7 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.graph.declarations.Declaration import de.fraunhofer.aisec.cpg.graph.declarations.ProblemDeclaration +import de.fraunhofer.aisec.cpg.graph.newMethod import de.fraunhofer.aisec.cpg.graph.newNamespace import de.fraunhofer.aisec.cpg.graph.newRecord @@ -37,6 +38,7 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : return when (node) { is Csharp.AST.NamespaceDeclarationSyntax -> handleNamespaceDeclaration(node) is Csharp.AST.ClassDeclarationSyntax -> handleClassDeclaration(node) + is Csharp.AST.MethodDeclarationSyntax -> handleMethodDeclaration(node) else -> ProblemDeclaration("Not supported: ${node.csharpType}") } } @@ -58,6 +60,20 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : } private fun handleClassDeclaration(node: Csharp.AST.ClassDeclarationSyntax): Declaration { - return newRecord(node.identifier, "class", rawNode = node) + val record = newRecord(node.identifier, "class", rawNode = node) + frontend.scopeManager.enterScope(record) + + for (member in node.members) { + val decl = handle(member) + frontend.scopeManager.addDeclaration(decl) + record.addDeclaration(decl) + } + + frontend.scopeManager.leaveScope(record) + return record + } + + private fun handleMethodDeclaration(node: Csharp.AST.MethodDeclarationSyntax): Declaration { + return newMethod(node.identifier, rawNode = node) } } diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt index ef04df4d575..fb0b4e3cf5e 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt @@ -51,5 +51,8 @@ class CSharpLanguageFrontendTest : BaseTest() { val foo = ns.records["Foo"] assertNotNull(foo) + + val bar = foo.methods["Bar"] + assertNotNull(bar) } } diff --git a/cpg-language-csharp/src/test/resources/csharp/helloworld.cs b/cpg-language-csharp/src/test/resources/csharp/helloworld.cs index 6530d7e862e..32ed21e849f 100644 --- a/cpg-language-csharp/src/test/resources/csharp/helloworld.cs +++ b/cpg-language-csharp/src/test/resources/csharp/helloworld.cs @@ -1,4 +1,7 @@ namespace HelloWorld { - class Foo {} + class Foo + { + void Bar() {} + } } From 6eacfabfbc8ee56ac603bfd3aad74346c3d1ed54 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Mon, 2 Mar 2026 19:38:57 +0100 Subject: [PATCH 17/55] Add a custom print ast method for debugging --- .../src/main/csharp/NativeParser/Library.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 446d1b1c138..e128c2e8574 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -17,6 +17,13 @@ private static IntPtr Register(CSharpSyntaxNode node) return ptr; } + private static void PrintASTDump(SyntaxNode node, int indent = 0) + { + Console.Error.WriteLine(new string(' ', indent * 2) + node.GetType().Name + ": " + node.ToString().Split('\n')[0].Trim()); + foreach (var child in node.ChildNodes()) + PrintASTDump(child, indent + 1); + } + [UnmanagedCallersOnly(EntryPoint = "CSharpRoslynSyntaxTreeParseText")] public static IntPtr CSharpRoslynSyntaxTreeParseText(IntPtr sourcePtr) { @@ -25,6 +32,7 @@ public static IntPtr CSharpRoslynSyntaxTreeParseText(IntPtr sourcePtr) var source = Marshal.PtrToStringUTF8(sourcePtr); // Roslyn parses the source code -> AST var root = (CSharpSyntaxNode)CSharpSyntaxTree.ParseText(source).GetRoot(); + PrintASTDump(root); return Register(root); } From 3d13dd6f518223fe1f0d0ec8a7968450fdcbafcd Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Mon, 2 Mar 2026 21:57:36 +0100 Subject: [PATCH 18/55] Implement locationOf --- .../src/main/csharp/NativeParser/Library.cs | 24 +++++++++++++++++++ .../aisec/cpg/frontends/csharp/CSharp.kt | 15 +++++++++++- .../csharp/CSharpLanguageFrontend.kt | 17 ++++++++++++- .../csharp/CSharpLanguageFrontendTest.kt | 4 ++++ 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index e128c2e8574..88873446a5f 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -102,4 +102,28 @@ public static IntPtr GetMethodDeclarationIdentifier(IntPtr handlePtr) ((MethodDeclarationSyntax)Nodes[handlePtr]).Identifier.ToString() ); } + + [UnmanagedCallersOnly(EntryPoint = "GetNodeStartLine")] + public static int GetNodeStartLine(IntPtr handlePtr) + { + return Nodes[handlePtr].GetLocation().GetLineSpan().StartLinePosition.Line + 1; + } + + [UnmanagedCallersOnly(EntryPoint = "GetNodeStartColumn")] + public static int GetNodeStartColumn(IntPtr handlePtr) + { + return Nodes[handlePtr].GetLocation().GetLineSpan().StartLinePosition.Character + 1; + } + + [UnmanagedCallersOnly(EntryPoint = "GetNodeEndLine")] + public static int GetNodeEndLine(IntPtr handlePtr) + { + return Nodes[handlePtr].GetLocation().GetLineSpan().EndLinePosition.Line + 1; + } + + [UnmanagedCallersOnly(EntryPoint = "GetNodeEndColumn")] + public static int GetNodeEndColumn(IntPtr handlePtr) + { + return Nodes[handlePtr].GetLocation().GetLineSpan().EndLinePosition.Character + 1; + } } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 3727a42d739..09f5b1d24cb 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -52,7 +52,12 @@ interface Csharp : Library { * Base class for all C# syntax nodes. Represents Roslyn's * [`CSharpSyntaxNode`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.csharpsyntaxnode?view=roslyn-dotnet-5.0.0) */ - open class Node(p: Pointer? = Pointer.NULL) : CsharpObject(p) + open class Node(p: Pointer? = Pointer.NULL) : CsharpObject(p) { + val startLine: Int by lazy { INSTANCE.GetNodeStartLine(this) } + val startColumn: Int by lazy { INSTANCE.GetNodeStartColumn(this) } + val endLine: Int by lazy { INSTANCE.GetNodeEndLine(this) } + val endColumn: Int by lazy { INSTANCE.GetNodeEndColumn(this) } + } /** * Represents the Roslyn @@ -186,6 +191,14 @@ interface Csharp : Library { fun GetMethodDeclarationIdentifier(handle: AST.MethodDeclarationSyntax): String + fun GetNodeStartLine(handle: AST.Node): Int + + fun GetNodeStartColumn(handle: AST.Node): Int + + fun GetNodeEndLine(handle: AST.Node): Int + + fun GetNodeEndColumn(handle: AST.Node): Int + companion object { val INSTANCE: Csharp by lazy { try { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt index 057279c8855..d6c6dadcce4 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt @@ -34,6 +34,7 @@ import de.fraunhofer.aisec.cpg.graph.newTranslationUnit import de.fraunhofer.aisec.cpg.graph.types.Type import de.fraunhofer.aisec.cpg.graph.unknownType import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation +import de.fraunhofer.aisec.cpg.sarif.Region import java.io.File class CSharpLanguageFrontend(ctx: TranslationContext, language: Language) : @@ -41,7 +42,10 @@ class CSharpLanguageFrontend(ctx: TranslationContext, language: Language Date: Mon, 2 Mar 2026 23:20:25 +0100 Subject: [PATCH 19/55] Implement codeOf in csharp language frontend --- cpg-language-csharp/src/main/csharp/NativeParser/Library.cs | 6 ++++++ .../de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt | 2 ++ .../aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 88873446a5f..8c42215a5e4 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -103,6 +103,12 @@ public static IntPtr GetMethodDeclarationIdentifier(IntPtr handlePtr) ); } + [UnmanagedCallersOnly(EntryPoint = "GetCode")] + public static IntPtr GetCode(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8(Nodes[handlePtr].ToString()); + } + [UnmanagedCallersOnly(EntryPoint = "GetNodeStartLine")] public static int GetNodeStartLine(IntPtr handlePtr) { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 09f5b1d24cb..e7408df7c37 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -191,6 +191,8 @@ interface Csharp : Library { fun GetMethodDeclarationIdentifier(handle: AST.MethodDeclarationSyntax): String + fun GetCode(handle: AST.Node): String + fun GetNodeStartLine(handle: AST.Node): Int fun GetNodeStartColumn(handle: AST.Node): Int diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt index d6c6dadcce4..c1e6babd5c8 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt @@ -66,7 +66,7 @@ class CSharpLanguageFrontend(ctx: TranslationContext, language: Language Date: Tue, 3 Mar 2026 11:52:00 +0100 Subject: [PATCH 20/55] Add indent in ast print --- cpg-language-csharp/src/main/csharp/NativeParser/Library.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 8c42215a5e4..6dc2da926fd 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -17,9 +17,9 @@ private static IntPtr Register(CSharpSyntaxNode node) return ptr; } - private static void PrintASTDump(SyntaxNode node, int indent = 0) + private static void PrintASTDump(SyntaxNode node, int indent = 2) { - Console.Error.WriteLine(new string(' ', indent * 2) + node.GetType().Name + ": " + node.ToString().Split('\n')[0].Trim()); + Console.Error.WriteLine(new string(' ', indent) + node.GetType().Name + ": " + node.ToString().Split('\n')[0].Trim()); foreach (var child in node.ChildNodes()) PrintASTDump(child, indent + 1); } From 529f049eb3f3edd3989f13fa6c5ef66df5d01e35 Mon Sep 17 00:00:00 2001 From: Yannick Date: Fri, 13 Mar 2026 15:52:00 +0100 Subject: [PATCH 21/55] Add FieldDeclaration and VariableDeclarator --- .../src/main/csharp/NativeParser/Library.cs | 29 +++++++++++++++++++ .../aisec/cpg/frontends/csharp/CSharp.kt | 28 ++++++++++++++++-- .../frontends/csharp/DeclarationHandler.kt | 13 ++++++++- .../csharp/CSharpLanguageFrontendTest.kt | 25 ++++++++++++++++ .../src/test/resources/csharp/helloworld.cs | 27 +++++++++++++++-- 5 files changed, 116 insertions(+), 6 deletions(-) diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 8c42215a5e4..bf0005bf6ff 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -132,4 +132,33 @@ public static int GetNodeEndColumn(IntPtr handlePtr) { return Nodes[handlePtr].GetLocation().GetLineSpan().EndLinePosition.Character + 1; } + [UnmanagedCallersOnly(EntryPoint = "GetFieldDeclarationSyntax")] + public static IntPtr GetFieldDeclarationSyntax(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8(((FieldDeclarationSyntax)Nodes[handlePtr]).ToString()); + } + + [UnmanagedCallersOnly(EntryPoint = "GetFieldDeclarationSyntaxVariableCount")] + public static int GetFieldDeclarationSyntaxVariableCount(IntPtr handlePtr) + { + return ((FieldDeclarationSyntax)Nodes[handlePtr]).Declaration.Variables.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetVariableDeclaratorSyntax")] + public static IntPtr GetVariableDeclaratorSyntax(IntPtr handlePtr, int index) + { + return Register(((FieldDeclarationSyntax)Nodes[handlePtr]).Declaration.Variables[index]); + } + + [UnmanagedCallersOnly(EntryPoint = "GetVariableDeclarationSyntax")] + public static IntPtr GetVariableDeclarationSyntax(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8(((VariableDeclarationSyntax)Nodes[handlePtr]).ToString()); + } + + [UnmanagedCallersOnly(EntryPoint = "GetVariableDeclaratorSyntaxString")] + public static IntPtr GetVariableDeclaratorSyntaxString(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8(((VariableDeclaratorSyntax)Nodes[handlePtr]).Identifier.ToString()); + } } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index e7408df7c37..cc6343240c1 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -90,6 +90,9 @@ interface Csharp : Library { return when (INSTANCE.GetKind(nativeValue)) { "NamespaceDeclaration" -> NamespaceDeclarationSyntax(nativeValue) "ClassDeclaration" -> ClassDeclarationSyntax(nativeValue) + "FieldDeclaration" -> FieldDeclarationSyntax(nativeValue) + "MethodDeclaration" -> MethodDeclarationSyntax(nativeValue) + "VariableDeclarator" -> VariableDeclaratorSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } } @@ -115,7 +118,7 @@ interface Csharp : Library { */ class ClassDeclarationSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { val identifier: String by lazy { INSTANCE.GetClassDeclarationIdentifier(this) } - val members: List by lazy { + val members: List by lazy { val count = INSTANCE.GetClassDeclarationMembersCount(this) (0 until count).map { i -> INSTANCE.GetClassDeclarationMember(this, i) } } @@ -133,7 +136,6 @@ interface Csharp : Library { return super.fromNative(nativeValue, context) } return when (INSTANCE.GetKind(nativeValue)) { - "MethodDeclaration" -> MethodDeclarationSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } } @@ -147,6 +149,17 @@ interface Csharp : Library { class MethodDeclarationSyntax(p: Pointer? = Pointer.NULL) : BaseMethodDeclarationSyntax(p) { val identifier: String by lazy { INSTANCE.GetMethodDeclarationIdentifier(this) } } + + class FieldDeclarationSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { + val variables: List by lazy { + val count = INSTANCE.GetFieldDeclarationSyntaxVariableCount(this) + (0 until count).map { i -> INSTANCE.GetVariableDeclaratorSyntax(this, i) } + } + } + + class VariableDeclaratorSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { + val identifier: String by lazy { INSTANCE.GetVariableDeclaratorSyntaxString(this) } + } } /** @@ -187,10 +200,19 @@ interface Csharp : Library { fun GetClassDeclarationMember( handle: AST.ClassDeclarationSyntax, index: Int, - ): AST.BaseMethodDeclarationSyntax + ): AST.MemberDeclarationSyntax + + fun GetFieldDeclarationSyntaxVariableCount(handle: AST.FieldDeclarationSyntax): Int + + fun GetVariableDeclaratorSyntax( + handle: AST.FieldDeclarationSyntax, + index: Int, + ): AST.VariableDeclaratorSyntax fun GetMethodDeclarationIdentifier(handle: AST.MethodDeclarationSyntax): String + fun GetVariableDeclaratorSyntaxString(handle: AST.VariableDeclaratorSyntax): String + fun GetCode(handle: AST.Node): String fun GetNodeStartLine(handle: AST.Node): Int diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt index b76b05f13d4..ee36ac878d2 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -27,6 +27,7 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.graph.declarations.Declaration import de.fraunhofer.aisec.cpg.graph.declarations.ProblemDeclaration +import de.fraunhofer.aisec.cpg.graph.newField import de.fraunhofer.aisec.cpg.graph.newMethod import de.fraunhofer.aisec.cpg.graph.newNamespace import de.fraunhofer.aisec.cpg.graph.newRecord @@ -39,6 +40,10 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : is Csharp.AST.NamespaceDeclarationSyntax -> handleNamespaceDeclaration(node) is Csharp.AST.ClassDeclarationSyntax -> handleClassDeclaration(node) is Csharp.AST.MethodDeclarationSyntax -> handleMethodDeclaration(node) + is Csharp.AST.FieldDeclarationSyntax -> handleFieldDeclaration(node) + // is Csharp.AST.LocalDeclarationStatementSyntax -> handleLocalDeclaration(node) + // is Csharp.AST.VariableDeclaratorSyntax -> handleVariableDeclarator(node) + // is Csharp.AST.VariableDeclarationSyntax -> handleVariableDeclaration(node) else -> ProblemDeclaration("Not supported: ${node.csharpType}") } } @@ -74,6 +79,12 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : } private fun handleMethodDeclaration(node: Csharp.AST.MethodDeclarationSyntax): Declaration { - return newMethod(node.identifier, rawNode = node) + + val method = newMethod(node.identifier, rawNode = node) + return method + } + + private fun handleFieldDeclaration(node: Csharp.AST.FieldDeclarationSyntax): Declaration { + return newField(node.variables[0].identifier, rawNode = node) } } diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt index 6ffca81cc21..92be8a87820 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt @@ -31,6 +31,7 @@ import java.nio.file.Path import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue class CSharpLanguageFrontendTest : BaseTest() { @@ -59,4 +60,28 @@ class CSharpLanguageFrontendTest : BaseTest() { assertNotNull(bar) assertEquals(5, bar.location?.region?.startLine) } + + @Test + fun testFieldDeclaration() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("helloworld.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + assertEquals(8, tu.fields?.size) + + assertTrue(tu.fields[0].name.localName == "name") + assertTrue(tu.fields[1].name.localName == "alter") + assertTrue(tu.fields[2].name.localName == "toll") + assertTrue(tu.fields[3].name.localName == "test") + assertTrue(tu.fields[4].name.localName == "xx") + assertTrue(tu.fields[5].name.localName == "exptest") + assertTrue(tu.fields[6].name.localName == "i") + assertTrue(tu.fields[7].name.localName == "x") + } } diff --git a/cpg-language-csharp/src/test/resources/csharp/helloworld.cs b/cpg-language-csharp/src/test/resources/csharp/helloworld.cs index 32ed21e849f..342e1507ce9 100644 --- a/cpg-language-csharp/src/test/resources/csharp/helloworld.cs +++ b/cpg-language-csharp/src/test/resources/csharp/helloworld.cs @@ -1,7 +1,30 @@ namespace HelloWorld { + public class User + { + // FieldDec + public string name; + public int alter; + } + class Foo { - void Bar() {} + public string toll; + int test = 3; + int xx = 1 * 2; + int exptest = test + xx; + private int i; + + void Bar() + { + int txx = 10; + int txxx= 10 + 1; + public int txxxx = 10 + 1; + xx = xx + i; + private int x, xxx; + + + //User u = new User(); + } } -} +} \ No newline at end of file From 12fed4f090d0e356be022c08a4e61c68dddfc01c Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Mon, 16 Mar 2026 14:21:31 +0100 Subject: [PATCH 22/55] gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index ea84ea33338..6b4b68f071e 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,10 @@ logs *.dylib *.so *.log +*.dll + +# .NET build artifacts +obj/ gradle.properties LibrariesTest.kt From 62657c73c864c79ae18c44274754b2d4293db922 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Mon, 16 Mar 2026 16:52:36 +0100 Subject: [PATCH 23/55] Refactor field declaration handling --- codyze-console/src/main/resources/.gitignore | 1 + .../src/main/csharp/NativeParser/Library.cs | 28 ++++++------------- .../aisec/cpg/frontends/csharp/CSharp.kt | 17 ++++++----- .../frontends/csharp/DeclarationHandler.kt | 24 ++++++++-------- .../csharp/CSharpLanguageFrontendTest.kt | 17 ++--------- .../src/test/resources/csharp/helloworld.cs | 24 +++------------- 6 files changed, 37 insertions(+), 74 deletions(-) diff --git a/codyze-console/src/main/resources/.gitignore b/codyze-console/src/main/resources/.gitignore index 6499bf4a1bb..fbeccbc6634 100644 --- a/codyze-console/src/main/resources/.gitignore +++ b/codyze-console/src/main/resources/.gitignore @@ -2,4 +2,5 @@ static node_modules .vite.deps .vite +application.conf diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 810960f0dd3..4159cbaa6af 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -132,33 +132,23 @@ public static int GetNodeEndColumn(IntPtr handlePtr) { return Nodes[handlePtr].GetLocation().GetLineSpan().EndLinePosition.Character + 1; } - [UnmanagedCallersOnly(EntryPoint = "GetFieldDeclarationSyntax")] - public static IntPtr GetFieldDeclarationSyntax(IntPtr handlePtr) - { - return Marshal.StringToCoTaskMemUTF8(((FieldDeclarationSyntax)Nodes[handlePtr]).ToString()); - } - - [UnmanagedCallersOnly(EntryPoint = "GetFieldDeclarationSyntaxVariableCount")] - public static int GetFieldDeclarationSyntaxVariableCount(IntPtr handlePtr) + [UnmanagedCallersOnly(EntryPoint = "GetFieldVariableCount")] + public static int GetFieldVariableCount(IntPtr handlePtr) { return ((FieldDeclarationSyntax)Nodes[handlePtr]).Declaration.Variables.Count; } - [UnmanagedCallersOnly(EntryPoint = "GetVariableDeclaratorSyntax")] - public static IntPtr GetVariableDeclaratorSyntax(IntPtr handlePtr, int index) + [UnmanagedCallersOnly(EntryPoint = "GetFieldVariable")] + public static IntPtr GetFieldVariable(IntPtr handlePtr, int index) { return Register(((FieldDeclarationSyntax)Nodes[handlePtr]).Declaration.Variables[index]); } - [UnmanagedCallersOnly(EntryPoint = "GetVariableDeclarationSyntax")] - public static IntPtr GetVariableDeclarationSyntax(IntPtr handlePtr) - { - return Marshal.StringToCoTaskMemUTF8(((VariableDeclarationSyntax)Nodes[handlePtr]).ToString()); - } - - [UnmanagedCallersOnly(EntryPoint = "GetVariableDeclaratorSyntaxString")] - public static IntPtr GetVariableDeclaratorSyntaxString(IntPtr handlePtr) + [UnmanagedCallersOnly(EntryPoint = "GetVariableDeclaratorIdentifier")] + public static IntPtr GetVariableDeclaratorIdentifier(IntPtr handlePtr) { - return Marshal.StringToCoTaskMemUTF8(((VariableDeclaratorSyntax)Nodes[handlePtr]).Identifier.ToString()); + return Marshal.StringToCoTaskMemUTF8( + ((VariableDeclaratorSyntax)Nodes[handlePtr]).Identifier.ToString() + ); } } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index cc6343240c1..8077fccc215 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -92,7 +92,6 @@ interface Csharp : Library { "ClassDeclaration" -> ClassDeclarationSyntax(nativeValue) "FieldDeclaration" -> FieldDeclarationSyntax(nativeValue) "MethodDeclaration" -> MethodDeclarationSyntax(nativeValue) - "VariableDeclarator" -> VariableDeclaratorSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } } @@ -152,13 +151,13 @@ interface Csharp : Library { class FieldDeclarationSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { val variables: List by lazy { - val count = INSTANCE.GetFieldDeclarationSyntaxVariableCount(this) - (0 until count).map { i -> INSTANCE.GetVariableDeclaratorSyntax(this, i) } + val count = INSTANCE.GetFieldVariableCount(this) + (0 until count).map { i -> INSTANCE.GetFieldVariable(this, i) } } } - class VariableDeclaratorSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { - val identifier: String by lazy { INSTANCE.GetVariableDeclaratorSyntaxString(this) } + class VariableDeclaratorSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val identifier: String by lazy { INSTANCE.GetVariableDeclaratorIdentifier(this) } } } @@ -202,16 +201,16 @@ interface Csharp : Library { index: Int, ): AST.MemberDeclarationSyntax - fun GetFieldDeclarationSyntaxVariableCount(handle: AST.FieldDeclarationSyntax): Int + fun GetFieldVariableCount(handle: AST.FieldDeclarationSyntax): Int - fun GetVariableDeclaratorSyntax( + fun GetFieldVariable( handle: AST.FieldDeclarationSyntax, index: Int, ): AST.VariableDeclaratorSyntax - fun GetMethodDeclarationIdentifier(handle: AST.MethodDeclarationSyntax): String + fun GetVariableDeclaratorIdentifier(handle: AST.VariableDeclaratorSyntax): String - fun GetVariableDeclaratorSyntaxString(handle: AST.VariableDeclaratorSyntax): String + fun GetMethodDeclarationIdentifier(handle: AST.MethodDeclarationSyntax): String fun GetCode(handle: AST.Node): String diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt index ee36ac878d2..bf9d841601b 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -40,10 +40,6 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : is Csharp.AST.NamespaceDeclarationSyntax -> handleNamespaceDeclaration(node) is Csharp.AST.ClassDeclarationSyntax -> handleClassDeclaration(node) is Csharp.AST.MethodDeclarationSyntax -> handleMethodDeclaration(node) - is Csharp.AST.FieldDeclarationSyntax -> handleFieldDeclaration(node) - // is Csharp.AST.LocalDeclarationStatementSyntax -> handleLocalDeclaration(node) - // is Csharp.AST.VariableDeclaratorSyntax -> handleVariableDeclarator(node) - // is Csharp.AST.VariableDeclarationSyntax -> handleVariableDeclaration(node) else -> ProblemDeclaration("Not supported: ${node.csharpType}") } } @@ -69,9 +65,18 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : frontend.scopeManager.enterScope(record) for (member in node.members) { - val decl = handle(member) - frontend.scopeManager.addDeclaration(decl) - record.addDeclaration(decl) + when (member) { + is Csharp.AST.FieldDeclarationSyntax -> { + for (variable in member.variables) { + val field = newField(variable.identifier, rawNode = member) + frontend.scopeManager.addDeclaration(field) + } + } + else -> { + val decl = handle(member) + frontend.scopeManager.addDeclaration(decl) + } + } } frontend.scopeManager.leaveScope(record) @@ -79,12 +84,7 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : } private fun handleMethodDeclaration(node: Csharp.AST.MethodDeclarationSyntax): Declaration { - val method = newMethod(node.identifier, rawNode = node) return method } - - private fun handleFieldDeclaration(node: Csharp.AST.FieldDeclarationSyntax): Declaration { - return newField(node.variables[0].identifier, rawNode = node) - } } diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt index 92be8a87820..8342e6f983e 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt @@ -31,7 +31,6 @@ import java.nio.file.Path import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull -import org.junit.jupiter.api.Assertions.assertTrue class CSharpLanguageFrontendTest : BaseTest() { @@ -54,15 +53,15 @@ class CSharpLanguageFrontendTest : BaseTest() { val foo = ns.records["Foo"] assertNotNull(foo) - assertEquals(3, foo.location?.region?.startLine) + assertEquals(11, foo.location?.region?.startLine) val bar = foo.methods["Bar"] assertNotNull(bar) - assertEquals(5, bar.location?.region?.startLine) + assertEquals(13, bar.location?.region?.startLine) } @Test - fun testFieldDeclaration() { + fun testFieldDeclarations() { val topLevel = Path.of("src", "test", "resources", "csharp") val tu = analyzeAndGetFirstTU( @@ -73,15 +72,5 @@ class CSharpLanguageFrontendTest : BaseTest() { it.registerLanguage() } assertNotNull(tu) - assertEquals(8, tu.fields?.size) - - assertTrue(tu.fields[0].name.localName == "name") - assertTrue(tu.fields[1].name.localName == "alter") - assertTrue(tu.fields[2].name.localName == "toll") - assertTrue(tu.fields[3].name.localName == "test") - assertTrue(tu.fields[4].name.localName == "xx") - assertTrue(tu.fields[5].name.localName == "exptest") - assertTrue(tu.fields[6].name.localName == "i") - assertTrue(tu.fields[7].name.localName == "x") } } diff --git a/cpg-language-csharp/src/test/resources/csharp/helloworld.cs b/cpg-language-csharp/src/test/resources/csharp/helloworld.cs index 342e1507ce9..da356a50659 100644 --- a/cpg-language-csharp/src/test/resources/csharp/helloworld.cs +++ b/cpg-language-csharp/src/test/resources/csharp/helloworld.cs @@ -1,30 +1,14 @@ namespace HelloWorld { - public class User - { - // FieldDec - public string name; - public int alter; - } - class Foo { - public string toll; - int test = 3; - int xx = 1 * 2; - int exptest = test + xx; - private int i; + int bar = 3; + private int baz; + private int first, second; void Bar() { - int txx = 10; - int txxx= 10 + 1; - public int txxxx = 10 + 1; - xx = xx + i; - private int x, xxx; - - - //User u = new User(); + int localVar = 10; } } } \ No newline at end of file From a049e3e071ae9885de1964ebfb7aae3a8602e3c2 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Mon, 16 Mar 2026 17:19:30 +0100 Subject: [PATCH 24/55] Add support for file scoped namespaces --- .../src/main/csharp/NativeParser/Library.cs | 10 +++-- .../aisec/cpg/frontends/csharp/CSharp.kt | 3 +- .../frontends/csharp/DeclarationHandler.kt | 2 + .../csharp/CSharpLanguageFrontendTest.kt | 42 +++++++++++++------ .../csharp/{helloworld.cs => fields.cs} | 5 --- .../resources/csharp/fileScoped_namespace.cs | 6 +++ .../src/test/resources/csharp/namespace.cs | 9 ++++ 7 files changed, 55 insertions(+), 22 deletions(-) rename cpg-language-csharp/src/test/resources/csharp/{helloworld.cs => fields.cs} (64%) create mode 100644 cpg-language-csharp/src/test/resources/csharp/fileScoped_namespace.cs create mode 100644 cpg-language-csharp/src/test/resources/csharp/namespace.cs diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 4159cbaa6af..8d460892e8d 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -32,7 +32,7 @@ public static IntPtr CSharpRoslynSyntaxTreeParseText(IntPtr sourcePtr) var source = Marshal.PtrToStringUTF8(sourcePtr); // Roslyn parses the source code -> AST var root = (CSharpSyntaxNode)CSharpSyntaxTree.ParseText(source).GetRoot(); - PrintASTDump(root); +// PrintASTDump(root); return Register(root); } @@ -55,24 +55,26 @@ public static IntPtr GetCompilationUnitMember(IntPtr handlePtr, int index) return Register(((CompilationUnitSyntax)Nodes[handlePtr]).Members[index]); } + // Cast to BaseNamespaceDeclarationSyntax to support both block-scoped (NamespaceDeclarationSyntax) + // and file-scoped (FileScopedNamespaceDeclarationSyntax) namespaces. [UnmanagedCallersOnly(EntryPoint = "GetNamespaceDeclarationName")] public static IntPtr GetNamespaceDeclarationName(IntPtr handlePtr) { return Marshal.StringToCoTaskMemUTF8( - ((NamespaceDeclarationSyntax)Nodes[handlePtr]).Name.ToString() + ((BaseNamespaceDeclarationSyntax)Nodes[handlePtr]).Name.ToString() ); } [UnmanagedCallersOnly(EntryPoint = "GetNamespaceDeclarationMembersCount")] public static int GetNamespaceDeclarationMembersCount(IntPtr handlePtr) { - return ((NamespaceDeclarationSyntax)Nodes[handlePtr]).Members.Count; + return ((BaseNamespaceDeclarationSyntax)Nodes[handlePtr]).Members.Count; } [UnmanagedCallersOnly(EntryPoint = "GetNamespaceDeclarationMember")] public static IntPtr GetNamespaceDeclarationMember(IntPtr handlePtr, int index) { - return Register(((NamespaceDeclarationSyntax)Nodes[handlePtr]).Members[index]); + return Register(((BaseNamespaceDeclarationSyntax)Nodes[handlePtr]).Members[index]); } [UnmanagedCallersOnly(EntryPoint = "GetClassDeclarationIdentifier")] diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 8077fccc215..1608a846218 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -88,7 +88,8 @@ interface Csharp : Library { return super.fromNative(nativeValue, context) } return when (INSTANCE.GetKind(nativeValue)) { - "NamespaceDeclaration" -> NamespaceDeclarationSyntax(nativeValue) + "NamespaceDeclaration", + "FileScopedNamespaceDeclaration" -> NamespaceDeclarationSyntax(nativeValue) "ClassDeclaration" -> ClassDeclarationSyntax(nativeValue) "FieldDeclaration" -> FieldDeclarationSyntax(nativeValue) "MethodDeclaration" -> MethodDeclarationSyntax(nativeValue) diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt index bf9d841601b..c47bc0782f8 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -70,11 +70,13 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : for (variable in member.variables) { val field = newField(variable.identifier, rawNode = member) frontend.scopeManager.addDeclaration(field) + record.addDeclaration(field) } } else -> { val decl = handle(member) frontend.scopeManager.addDeclaration(decl) + record.addDeclaration(decl) } } } diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt index 8342e6f983e..aeb68cd5231 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt @@ -35,11 +35,11 @@ import kotlin.test.assertNotNull class CSharpLanguageFrontendTest : BaseTest() { @Test - fun testHelloWorld() { + fun testNamespaces() { val topLevel = Path.of("src", "test", "resources", "csharp") val tu = analyzeAndGetFirstTU( - listOf(topLevel.resolve("helloworld.cs").toFile()), + listOf(topLevel.resolve("namespace.cs").toFile()), topLevel, true, ) { @@ -47,30 +47,48 @@ class CSharpLanguageFrontendTest : BaseTest() { } assertNotNull(tu) - val ns = tu.namespaces["HelloWorld"] - assertNotNull(ns) - assertEquals(1, ns.location?.region?.startLine) - - val foo = ns.records["Foo"] + val foo = tu.namespaces["Foo"] assertNotNull(foo) - assertEquals(11, foo.location?.region?.startLine) - val bar = foo.methods["Bar"] + val bar = foo.namespaces["Bar"] assertNotNull(bar) - assertEquals(13, bar.location?.region?.startLine) + + val baz = bar.records["Baz"] + assertNotNull(baz) + + val dottedNameSpace = tu.namespaces["Dotted.NameSpace"] + assertNotNull(dottedNameSpace) } @Test - fun testFieldDeclarations() { + fun testFileScopedNamespace() { val topLevel = Path.of("src", "test", "resources", "csharp") val tu = analyzeAndGetFirstTU( - listOf(topLevel.resolve("helloworld.cs").toFile()), + listOf(topLevel.resolve("fileScoped_namespace.cs").toFile()), topLevel, true, ) { it.registerLanguage() } assertNotNull(tu) + + val ns = tu.namespaces["HelloWorld"] + assertNotNull(ns) + + val foo = ns.records["Foo"] + assertNotNull(foo) + + assertEquals("bar", foo.fields["bar"]?.name?.localName) + } + + @Test + fun testFieldDeclarations() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("fields.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + assertNotNull(tu) } } diff --git a/cpg-language-csharp/src/test/resources/csharp/helloworld.cs b/cpg-language-csharp/src/test/resources/csharp/fields.cs similarity index 64% rename from cpg-language-csharp/src/test/resources/csharp/helloworld.cs rename to cpg-language-csharp/src/test/resources/csharp/fields.cs index da356a50659..f2961797acd 100644 --- a/cpg-language-csharp/src/test/resources/csharp/helloworld.cs +++ b/cpg-language-csharp/src/test/resources/csharp/fields.cs @@ -5,10 +5,5 @@ class Foo int bar = 3; private int baz; private int first, second; - - void Bar() - { - int localVar = 10; - } } } \ No newline at end of file diff --git a/cpg-language-csharp/src/test/resources/csharp/fileScoped_namespace.cs b/cpg-language-csharp/src/test/resources/csharp/fileScoped_namespace.cs new file mode 100644 index 00000000000..8a8411fe217 --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/fileScoped_namespace.cs @@ -0,0 +1,6 @@ +namespace HelloWorld; + +class Foo +{ + int bar; +} diff --git a/cpg-language-csharp/src/test/resources/csharp/namespace.cs b/cpg-language-csharp/src/test/resources/csharp/namespace.cs new file mode 100644 index 00000000000..4325d1c0f3d --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/namespace.cs @@ -0,0 +1,9 @@ +namespace Foo +{ + namespace Bar + { + class Baz { } + } +} + +namespace Dotted.NameSpace { } \ No newline at end of file From 69ec2d30f55ba53f14001ee20d8e801b1bb561d3 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Tue, 17 Mar 2026 14:22:27 +0100 Subject: [PATCH 25/55] Add builtInTypes --- .../cpg/frontends/csharp/CSharpLanguage.kt | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt index d852d4cc6a0..a8ad6eccc74 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt @@ -26,6 +26,12 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.frontends.Language +import de.fraunhofer.aisec.cpg.graph.types.BooleanType +import de.fraunhofer.aisec.cpg.graph.types.FloatingPointType +import de.fraunhofer.aisec.cpg.graph.types.IntegerType +import de.fraunhofer.aisec.cpg.graph.types.NumericType +import de.fraunhofer.aisec.cpg.graph.types.ObjectType +import de.fraunhofer.aisec.cpg.graph.types.StringType import de.fraunhofer.aisec.cpg.graph.types.Type import kotlin.reflect.KClass import org.neo4j.ogm.annotation.Transient @@ -39,5 +45,32 @@ class CSharpLanguage : Language() { override val compoundAssignmentOperators = setOf() - @Transient override val builtInTypes = mapOf() + /** + * See + * [Documentation](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/built-in-types). + */ + @Transient + override val builtInTypes = + mapOf( + // Boolean type + "bool" to BooleanType(typeName = "bool", language = this), + // Integral Types: + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/integral-numeric-types + "int" to IntegerType("int", Integer.MAX_VALUE, this, NumericType.Modifier.SIGNED), + "short" to IntegerType("short", 16, this, NumericType.Modifier.SIGNED), + "long" to IntegerType("long", 64, this, NumericType.Modifier.SIGNED), + "ulong" to IntegerType("ulong", 64, this, NumericType.Modifier.UNSIGNED), + "byte" to IntegerType("byte", 8, this, NumericType.Modifier.UNSIGNED), + "sbyte" to IntegerType("sbyte", 8, this, NumericType.Modifier.SIGNED), + // Floating-Point types: + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/floating-point-numeric-types + "float" to FloatingPointType("float", 32, this, NumericType.Modifier.SIGNED), + "double" to FloatingPointType("double", 64, this, NumericType.Modifier.SIGNED), + "decimal" to FloatingPointType("decimal", 128, this, NumericType.Modifier.SIGNED), + // Char Type + "char" to IntegerType("char", 16, this, NumericType.Modifier.UNSIGNED), + // String Type + "string" to StringType("string", this), + "object" to ObjectType("object", listOf(), false, true, this), + ) } From fb20bc0edb4ce1dd1221b97bc380e22a56cb9f88 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Tue, 17 Mar 2026 14:23:32 +0100 Subject: [PATCH 26/55] Handle method parameters --- .../src/main/csharp/NativeParser/Library.cs | 37 +++++++++++++++ .../aisec/cpg/frontends/csharp/CSharp.kt | 46 +++++++++++++++++++ .../csharp/CSharpLanguageFrontend.kt | 8 +++- .../frontends/csharp/DeclarationHandler.kt | 15 ++++++ .../csharp/CSharpLanguageFrontendTest.kt | 23 ++++++++++ .../src/test/resources/csharp/method.cs | 8 ++++ 6 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 cpg-language-csharp/src/test/resources/csharp/method.cs diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 8d460892e8d..717fec0b6ab 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -153,4 +153,41 @@ public static IntPtr GetVariableDeclaratorIdentifier(IntPtr handlePtr) ((VariableDeclaratorSyntax)Nodes[handlePtr]).Identifier.ToString() ); } + + [UnmanagedCallersOnly(EntryPoint = "GetMethodDeclarationParameterCount")] + public static int GetMethodDeclarationParameterCount(IntPtr handlePtr) + { + return ((MethodDeclarationSyntax)Nodes[handlePtr]).ParameterList.Parameters.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetMethodDeclarationParameter")] + public static IntPtr GetMethodDeclarationParameter(IntPtr handlePtr, int index) + { + return Register(((MethodDeclarationSyntax)Nodes[handlePtr]).ParameterList.Parameters[index]); + } + + [UnmanagedCallersOnly(EntryPoint = "GetParameterIdentifier")] + public static IntPtr GetParameterIdentifier(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8( + ((ParameterSyntax)Nodes[handlePtr]).Identifier.ToString() + ); + } + + [UnmanagedCallersOnly(EntryPoint = "GetParameterType")] + public static IntPtr GetParameterType(IntPtr handlePtr) + { + return Register(((ParameterSyntax)Nodes[handlePtr]).Type); + } + + // We need to call ToString() on the TypeSyntax node to get the actual type name + // (e.g. "int", "string"). Otherwise, it would return the syntax node category + // (e.g. "PredefinedType", "IdentifierName"). + [UnmanagedCallersOnly(EntryPoint = "GetTypeName")] + public static IntPtr GetTypeName(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8( + ((TypeSyntax)Nodes[handlePtr]).ToString() + ); + } } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 1608a846218..befd92b6e8f 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -59,6 +59,15 @@ interface Csharp : Library { val endColumn: Int by lazy { INSTANCE.GetNodeEndColumn(this) } } + /** + * Represents the Roslyn + * [`TypeSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.typesyntax?view=roslyn-dotnet-5.0.0) + * class. + */ + class TypeSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val name: String by lazy { INSTANCE.GetTypeName(this) } + } + /** * Represents the Roslyn * [`CompilationUnitSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.compilationunitsyntax?view=roslyn-dotnet-5.0.0) @@ -148,8 +157,17 @@ interface Csharp : Library { */ class MethodDeclarationSyntax(p: Pointer? = Pointer.NULL) : BaseMethodDeclarationSyntax(p) { val identifier: String by lazy { INSTANCE.GetMethodDeclarationIdentifier(this) } + val parameters: List by lazy { + val count = INSTANCE.GetMethodDeclarationParameterCount(this) + (0 until count).map { i -> INSTANCE.GetMethodDeclarationParameter(this, i) } + } } + /** + * Represents the Roslyn + * [`FieldDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.fielddeclarationsyntax?view=roslyn-dotnet-5.0.0) + * class. + */ class FieldDeclarationSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { val variables: List by lazy { val count = INSTANCE.GetFieldVariableCount(this) @@ -157,9 +175,24 @@ interface Csharp : Library { } } + /** + * Represents the Roslyn + * [`VariableDeclaratorSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.variabledeclaratorsyntax?view=roslyn-dotnet-5.0.0) + * class. + */ class VariableDeclaratorSyntax(p: Pointer? = Pointer.NULL) : Node(p) { val identifier: String by lazy { INSTANCE.GetVariableDeclaratorIdentifier(this) } } + + /** + * Represents the Roslyn + * [`ParameterSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.parametersyntax?view=roslyn-dotnet-5.0.0) + * class. + */ + class ParameterSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val identifier: String by lazy { INSTANCE.GetParameterIdentifier(this) } + val type: TypeSyntax by lazy { INSTANCE.GetParameterType(this) } + } } /** @@ -213,6 +246,19 @@ interface Csharp : Library { fun GetMethodDeclarationIdentifier(handle: AST.MethodDeclarationSyntax): String + fun GetMethodDeclarationParameterCount(handle: AST.MethodDeclarationSyntax): Int + + fun GetMethodDeclarationParameter( + handle: AST.MethodDeclarationSyntax, + index: Int, + ): AST.ParameterSyntax + + fun GetParameterIdentifier(handle: AST.ParameterSyntax): String + + fun GetParameterType(handle: AST.ParameterSyntax): AST.TypeSyntax + + fun GetTypeName(handle: AST.TypeSyntax): String + fun GetCode(handle: AST.Node): String fun GetNodeStartLine(handle: AST.Node): Int diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt index c1e6babd5c8..34c71814490 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt @@ -31,6 +31,7 @@ import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend import de.fraunhofer.aisec.cpg.graph.Node import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnit import de.fraunhofer.aisec.cpg.graph.newTranslationUnit +import de.fraunhofer.aisec.cpg.graph.objectType import de.fraunhofer.aisec.cpg.graph.types.Type import de.fraunhofer.aisec.cpg.graph.unknownType import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation @@ -64,7 +65,12 @@ class CSharpLanguageFrontend(ctx: TranslationContext, language: Language objectType(type.name) + else -> unknownType() + } + } override fun codeOf(astNode: Csharp.AST.Node): String = Csharp.INSTANCE.GetCode(astNode) diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt index c47bc0782f8..5747723a8b4 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -30,6 +30,7 @@ import de.fraunhofer.aisec.cpg.graph.declarations.ProblemDeclaration import de.fraunhofer.aisec.cpg.graph.newField import de.fraunhofer.aisec.cpg.graph.newMethod import de.fraunhofer.aisec.cpg.graph.newNamespace +import de.fraunhofer.aisec.cpg.graph.newParameter import de.fraunhofer.aisec.cpg.graph.newRecord class DeclarationHandler(frontend: CSharpLanguageFrontend) : @@ -87,6 +88,20 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : private fun handleMethodDeclaration(node: Csharp.AST.MethodDeclarationSyntax): Declaration { val method = newMethod(node.identifier, rawNode = node) + frontend.scopeManager.enterScope(method) + + for (parameter in node.parameters) { + val param = + newParameter( + name = parameter.identifier, + type = frontend.typeOf(parameter.type), + rawNode = parameter, + ) + frontend.scopeManager.addDeclaration(param) + method.parameters += param + } + + frontend.scopeManager.leaveScope(method) return method } } diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt index aeb68cd5231..ce10e116f57 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt @@ -91,4 +91,27 @@ class CSharpLanguageFrontendTest : BaseTest() { } assertNotNull(tu) } + + @Test + fun testMethodDeclarationsTest() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("method.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + assertNotNull(tu) + + val foo = tu.namespaces["HelloWorld"]?.records["Foo"] + assertNotNull(foo) + + val bar = foo.methods["Bar"] + assertNotNull(bar) + assertEquals(0, bar.parameters.size) + + val baz = foo.methods["Baz"] + assertNotNull(baz) + assertEquals(2, baz.parameters.size) + assertEquals("a", baz.parameters[0].name.localName) + assertEquals("b", baz.parameters[1].name.localName) + } } diff --git a/cpg-language-csharp/src/test/resources/csharp/method.cs b/cpg-language-csharp/src/test/resources/csharp/method.cs new file mode 100644 index 00000000000..ebd578c6f89 --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/method.cs @@ -0,0 +1,8 @@ +namespace HelloWorld; + +class Foo +{ + void Bar() { } + + void Baz(int a, string b) { } +} \ No newline at end of file From 428d59a527117aebc576d6a599f997b8c8179a5f Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Tue, 17 Mar 2026 14:37:03 +0100 Subject: [PATCH 27/55] Add docstrings --- .gitignore | 1 + .../cpg/frontends/csharp/CSharpHandler.kt | 1 - .../cpg/frontends/csharp/DeclarationHandler.kt | 18 ++++++++++++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 6b4b68f071e..6568e93b550 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ logs # .NET build artifacts obj/ +bin/ gradle.properties LibrariesTest.kt diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpHandler.kt index b096ad3363a..b113588ceee 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpHandler.kt @@ -38,7 +38,6 @@ abstract class CSharpHandler( override fun handle(ctx: Csharp.AST.Node): ResultNode { val node = handleNode(ctx) - // TODO: implement setComment frontend.setComment(node, ctx) frontend.process(ctx, node) diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt index 5747723a8b4..0bc1ea950c4 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -25,8 +25,7 @@ */ package de.fraunhofer.aisec.cpg.frontends.csharp -import de.fraunhofer.aisec.cpg.graph.declarations.Declaration -import de.fraunhofer.aisec.cpg.graph.declarations.ProblemDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.* import de.fraunhofer.aisec.cpg.graph.newField import de.fraunhofer.aisec.cpg.graph.newMethod import de.fraunhofer.aisec.cpg.graph.newNamespace @@ -45,6 +44,11 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : } } + /** + * Translates a C# + * [`NamespaceDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.namespacedeclarationsyntax?view=roslyn-dotnet-5.0.0) + * into a [Namespace]. + */ private fun handleNamespaceDeclaration( node: Csharp.AST.NamespaceDeclarationSyntax ): Declaration { @@ -61,6 +65,11 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : return namespace } + /** + * Translates a C# + * [`ClassDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.classdeclarationsyntax?view=roslyn-dotnet-5.0.0) + * into a [Record]. + */ private fun handleClassDeclaration(node: Csharp.AST.ClassDeclarationSyntax): Declaration { val record = newRecord(node.identifier, "class", rawNode = node) frontend.scopeManager.enterScope(record) @@ -86,6 +95,11 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : return record } + /** + * Translates a C# + * [`MethodDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.methoddeclarationsyntax?view=roslyn-dotnet-5.0.0) + * into a [Method]. + */ private fun handleMethodDeclaration(node: Csharp.AST.MethodDeclarationSyntax): Declaration { val method = newMethod(node.identifier, rawNode = node) frontend.scopeManager.enterScope(method) From 189c58a84afd719d1fb8c2e4549e32edeec2dd8d Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Fri, 20 Mar 2026 15:14:39 +0100 Subject: [PATCH 28/55] Add constructor declaration handling; start implementing support for block statements --- .../src/main/csharp/NativeParser/Library.cs | 39 +++++++++++++++ .../aisec/cpg/frontends/csharp/CSharp.kt | 49 +++++++++++++++++++ .../cpg/frontends/csharp/CSharpHandler.kt | 12 +++-- .../frontends/csharp/DeclarationHandler.kt | 34 +++++++++++-- .../cpg/frontends/csharp/StatementHandler.kt | 41 ++++++++++++++++ .../csharp/CSharpLanguageFrontendTest.kt | 34 ++++++++++++- .../src/test/resources/csharp/constructor.cs | 8 +++ .../src/test/resources/csharp/method.cs | 4 +- 8 files changed, 212 insertions(+), 9 deletions(-) create mode 100644 cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt create mode 100644 cpg-language-csharp/src/test/resources/csharp/constructor.cs diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 717fec0b6ab..5effaaaee8a 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -180,6 +180,26 @@ public static IntPtr GetParameterType(IntPtr handlePtr) return Register(((ParameterSyntax)Nodes[handlePtr]).Type); } + [UnmanagedCallersOnly(EntryPoint = "GetConstructorDeclarationIdentifier")] + public static IntPtr GetConstructorDeclarationIdentifier(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8( + ((ConstructorDeclarationSyntax)Nodes[handlePtr]).Identifier.ToString() + ); + } + + [UnmanagedCallersOnly(EntryPoint = "GetConstructorDeclarationParameterCount")] + public static int GetConstructorDeclarationParameterCount(IntPtr handlePtr) + { + return ((ConstructorDeclarationSyntax)Nodes[handlePtr]).ParameterList.Parameters.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetConstructorDeclarationParameter")] + public static IntPtr GetConstructorDeclarationParameter(IntPtr handlePtr, int index) + { + return Register(((ConstructorDeclarationSyntax)Nodes[handlePtr]).ParameterList.Parameters[index]); + } + // We need to call ToString() on the TypeSyntax node to get the actual type name // (e.g. "int", "string"). Otherwise, it would return the syntax node category // (e.g. "PredefinedType", "IdentifierName"). @@ -190,4 +210,23 @@ public static IntPtr GetTypeName(IntPtr handlePtr) ((TypeSyntax)Nodes[handlePtr]).ToString() ); } + + // `BaseMethodDeclarationSyntax` to get the body for Methods and Constructors. + [UnmanagedCallersOnly(EntryPoint = "GetBaseMethodDeclarationBody")] + public static IntPtr GetBaseMethodDeclarationBody(IntPtr handlePtr) + { + return Register(((BaseMethodDeclarationSyntax)Nodes[handlePtr]).Body); + } + + [UnmanagedCallersOnly(EntryPoint = "GetBlockStatementCount")] + public static int GetBlockStatementCount(IntPtr handlePtr) + { + return ((BlockSyntax)Nodes[handlePtr]).Statements.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetBlockStatement")] + public static IntPtr GetBlockStatement(IntPtr handlePtr, int index) + { + return Register(((BlockSyntax)Nodes[handlePtr]).Statements[index]); + } } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index befd92b6e8f..485c0f3c64a 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -102,6 +102,7 @@ interface Csharp : Library { "ClassDeclaration" -> ClassDeclarationSyntax(nativeValue) "FieldDeclaration" -> FieldDeclarationSyntax(nativeValue) "MethodDeclaration" -> MethodDeclarationSyntax(nativeValue) + "ConstructorDeclaration" -> ConstructorDeclarationSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } } @@ -161,6 +162,39 @@ interface Csharp : Library { val count = INSTANCE.GetMethodDeclarationParameterCount(this) (0 until count).map { i -> INSTANCE.GetMethodDeclarationParameter(this, i) } } + val body: BlockSyntax by lazy { INSTANCE.GetBaseMethodDeclarationBody(this) } + } + + class BlockSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) { + val statements: List by lazy { + val count = INSTANCE.GetBlockStatementCount(this) + (0 until count).map { i -> INSTANCE.GetBlockStatement(this, i) } + } + } + + open class StatementSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + override fun fromNative(nativeValue: Any?, context: FromNativeContext?): Any { + if (nativeValue !is Pointer) { + return super.fromNative(nativeValue, context) + } + return when (INSTANCE.GetKind(nativeValue)) { + else -> super.fromNative(nativeValue, context) + } + } + } + + /** + * Represents the Roslyn + * [`ConstructorDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.constructordeclarationsyntax?view=roslyn-dotnet-5.0.0) + * class. + */ + class ConstructorDeclarationSyntax(p: Pointer? = Pointer.NULL) : + BaseMethodDeclarationSyntax(p) { + val identifier: String by lazy { INSTANCE.GetConstructorDeclarationIdentifier(this) } + val parameters: List by lazy { + val count = INSTANCE.GetConstructorDeclarationParameterCount(this) + (0 until count).map { i -> INSTANCE.GetConstructorDeclarationParameter(this, i) } + } } /** @@ -259,6 +293,21 @@ interface Csharp : Library { fun GetTypeName(handle: AST.TypeSyntax): String + fun GetConstructorDeclarationIdentifier(handle: AST.ConstructorDeclarationSyntax): String + + fun GetConstructorDeclarationParameterCount(handle: AST.ConstructorDeclarationSyntax): Int + + fun GetConstructorDeclarationParameter( + handle: AST.ConstructorDeclarationSyntax, + index: Int, + ): AST.ParameterSyntax + + fun GetBaseMethodDeclarationBody(handle: AST.MethodDeclarationSyntax): AST.BlockSyntax + + fun GetBlockStatementCount(handle: AST.StatementSyntax): Int + + fun GetBlockStatement(handle: AST.StatementSyntax, index: Int): AST.StatementSyntax + fun GetCode(handle: AST.Node): String fun GetNodeStartLine(handle: AST.Node): Int diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpHandler.kt index b113588ceee..7427fc7e7b5 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpHandler.kt @@ -30,12 +30,16 @@ import de.fraunhofer.aisec.cpg.graph.Node import java.util.function.Supplier /** Base class for all C# handlers. */ -abstract class CSharpHandler( +abstract class CSharpHandler( configConstructor: Supplier, frontend: CSharpLanguageFrontend, -) : Handler(configConstructor, frontend) { +) : + Handler( + configConstructor = configConstructor, + frontend = frontend, + ) { - override fun handle(ctx: Csharp.AST.Node): ResultNode { + override fun handle(ctx: HandlerNode): ResultNode { val node = handleNode(ctx) frontend.setComment(node, ctx) @@ -45,5 +49,5 @@ abstract class CSharpHandler( return node } - abstract fun handleNode(node: Csharp.AST.Node): ResultNode + abstract fun handleNode(node: HandlerNode): ResultNode } diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt index 0bc1ea950c4..60df0116576 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -26,6 +26,7 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.graph.declarations.* +import de.fraunhofer.aisec.cpg.graph.newConstructor import de.fraunhofer.aisec.cpg.graph.newField import de.fraunhofer.aisec.cpg.graph.newMethod import de.fraunhofer.aisec.cpg.graph.newNamespace @@ -33,13 +34,13 @@ import de.fraunhofer.aisec.cpg.graph.newParameter import de.fraunhofer.aisec.cpg.graph.newRecord class DeclarationHandler(frontend: CSharpLanguageFrontend) : - CSharpHandler(::ProblemDeclaration, frontend) { - - override fun handleNode(node: Csharp.AST.Node): Declaration { + CSharpHandler(::ProblemDeclaration, frontend) { + override fun handleNode(node: Csharp.AST.MemberDeclarationSyntax): Declaration { return when (node) { is Csharp.AST.NamespaceDeclarationSyntax -> handleNamespaceDeclaration(node) is Csharp.AST.ClassDeclarationSyntax -> handleClassDeclaration(node) is Csharp.AST.MethodDeclarationSyntax -> handleMethodDeclaration(node) + is Csharp.AST.ConstructorDeclarationSyntax -> handleConstructorDeclaration(node) else -> ProblemDeclaration("Not supported: ${node.csharpType}") } } @@ -118,4 +119,31 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : frontend.scopeManager.leaveScope(method) return method } + + /** + * Translates a C# + * [`ConstructorDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.constructordeclarationsyntax?view=roslyn-dotnet-5.0.0) + * into a [Constructor]. + */ + private fun handleConstructorDeclaration( + node: Csharp.AST.ConstructorDeclarationSyntax + ): Declaration { + val record = frontend.scopeManager.currentRecord + val constructor = newConstructor(node.identifier, record, rawNode = node) + frontend.scopeManager.enterScope(constructor) + + for (parameter in node.parameters) { + val param = + newParameter( + name = parameter.identifier, + type = frontend.typeOf(parameter.type), + rawNode = parameter, + ) + frontend.scopeManager.addDeclaration(param) + constructor.parameters += param + } + + frontend.scopeManager.leaveScope(constructor) + return constructor + } } diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt new file mode 100644 index 00000000000..b2766530581 --- /dev/null +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 de.fraunhofer.aisec.cpg.frontends.csharp + +// class StatementHandler(frontend: CSharpLanguageFrontend) : CSharpHandler( +// configConstructor = ::ProblemExpression, +// frontend = frontend +// ) { +// override fun handleNode(node: Csharp.AST.StatementSyntax): Expression { +//// return when (node) { +//// is Csharp.AST.ReturnSyntax -> handleReturn(node) +//// } +//// } +//// +//// private fun handleReturn(node: Csharp.AST.StatementSyntax): Expression { +//// } +// } diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt index ce10e116f57..f7bf48c5c40 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt @@ -26,10 +26,12 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.declarations.Constructor import de.fraunhofer.aisec.cpg.test.* import java.nio.file.Path import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertIs import kotlin.test.assertNotNull class CSharpLanguageFrontendTest : BaseTest() { @@ -93,7 +95,7 @@ class CSharpLanguageFrontendTest : BaseTest() { } @Test - fun testMethodDeclarationsTest() { + fun testMethodDeclarations() { val topLevel = Path.of("src", "test", "resources", "csharp") val tu = analyzeAndGetFirstTU(listOf(topLevel.resolve("method.cs").toFile()), topLevel, true) { @@ -114,4 +116,34 @@ class CSharpLanguageFrontendTest : BaseTest() { assertEquals("a", baz.parameters[0].name.localName) assertEquals("b", baz.parameters[1].name.localName) } + + @Test + fun testConstructorDeclarations() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("constructor.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val foo = tu.namespaces["HelloWorld"]?.records["Foo"] + assertNotNull(foo) + + val constructors = foo.constructors + assertEquals(2, constructors.size) + + val noParameter = constructors.single { it.parameters.isEmpty() } + assertNotNull(noParameter) + assertIs(noParameter) + + val twoParameters = constructors.single { it.parameters.size == 2 } + assertNotNull(twoParameters) + assertIs(twoParameters) + assertEquals("x", twoParameters.parameters[0].name.localName) + assertEquals("y", twoParameters.parameters[1].name.localName) + } } diff --git a/cpg-language-csharp/src/test/resources/csharp/constructor.cs b/cpg-language-csharp/src/test/resources/csharp/constructor.cs new file mode 100644 index 00000000000..c7d4ce26bf9 --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/constructor.cs @@ -0,0 +1,8 @@ +namespace HelloWorld; + +class Foo +{ + Foo() { } + + Foo(int x, string y) { } +} \ No newline at end of file diff --git a/cpg-language-csharp/src/test/resources/csharp/method.cs b/cpg-language-csharp/src/test/resources/csharp/method.cs index ebd578c6f89..a4adaa53048 100644 --- a/cpg-language-csharp/src/test/resources/csharp/method.cs +++ b/cpg-language-csharp/src/test/resources/csharp/method.cs @@ -4,5 +4,7 @@ class Foo { void Bar() { } - void Baz(int a, string b) { } + void Baz(int a, string b) { + var a = 0; + } } \ No newline at end of file From 6eecc5af962dc66337d0cdd3302281e5f1b52aa6 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Mon, 23 Mar 2026 09:30:07 +0100 Subject: [PATCH 29/55] Start implementing statement handler and handling blocks --- .../aisec/cpg/frontends/csharp/CSharp.kt | 3 ++ .../csharp/CSharpLanguageFrontend.kt | 1 + .../frontends/csharp/DeclarationHandler.kt | 3 +- .../cpg/frontends/csharp/StatementHandler.kt | 45 +++++++++++++------ .../src/test/resources/csharp/method.cs | 7 ++- 5 files changed, 42 insertions(+), 17 deletions(-) diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 485c0f3c64a..705d8598547 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -183,6 +183,8 @@ interface Csharp : Library { } } + class ReturnStatementSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) {} + /** * Represents the Roslyn * [`ConstructorDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.constructordeclarationsyntax?view=roslyn-dotnet-5.0.0) @@ -195,6 +197,7 @@ interface Csharp : Library { val count = INSTANCE.GetConstructorDeclarationParameterCount(this) (0 until count).map { i -> INSTANCE.GetConstructorDeclarationParameter(this, i) } } + // val body: StatementSyntax by lazy { StatementSyntax(p) } } /** diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt index 34c71814490..3b39ddafbf0 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt @@ -42,6 +42,7 @@ class CSharpLanguageFrontend(ctx: TranslationContext, language: Language(ctx, language) { val declarationHandler = DeclarationHandler(this) + val statementHandler = StatementHandler(this) private var currentFile: File? = null diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt index 60df0116576..9f1a1260090 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -115,7 +115,7 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : frontend.scopeManager.addDeclaration(param) method.parameters += param } - + method.body = frontend.statementHandler.handle(node.body) frontend.scopeManager.leaveScope(method) return method } @@ -142,6 +142,7 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : frontend.scopeManager.addDeclaration(param) constructor.parameters += param } + // constructor.body = frontend.statementHandler.handle(node.body) frontend.scopeManager.leaveScope(constructor) return constructor diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt index b2766530581..584796087ca 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt @@ -25,17 +25,34 @@ */ package de.fraunhofer.aisec.cpg.frontends.csharp -// class StatementHandler(frontend: CSharpLanguageFrontend) : CSharpHandler( -// configConstructor = ::ProblemExpression, -// frontend = frontend -// ) { -// override fun handleNode(node: Csharp.AST.StatementSyntax): Expression { -//// return when (node) { -//// is Csharp.AST.ReturnSyntax -> handleReturn(node) -//// } -//// } -//// -//// private fun handleReturn(node: Csharp.AST.StatementSyntax): Expression { -//// } -// } +import de.fraunhofer.aisec.cpg.graph.expressions.Block +import de.fraunhofer.aisec.cpg.graph.expressions.Expression +import de.fraunhofer.aisec.cpg.graph.expressions.ProblemExpression +import de.fraunhofer.aisec.cpg.graph.newBlock + +class StatementHandler(frontend: CSharpLanguageFrontend) : + CSharpHandler( + configConstructor = ::ProblemExpression, + frontend = frontend, + ) { + override fun handleNode(node: Csharp.AST.StatementSyntax): Expression { + return when (node) { + is Csharp.AST.BlockSyntax -> handleBlock(node) + else -> ProblemExpression("Not supported: ${node.csharpType}") + } + } + + /** + * Translates a C# + * [`BlockSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.blocksyntax?view=roslyn-dotnet-5.0.0) + * into a [Block]. + */ + private fun handleBlock(node: Csharp.AST.BlockSyntax): Block { + val block = newBlock(rawNode = node) + for (stmt in node.statements) { + val statement = handle(stmt) + statement.let { block.statements += it } + } + return block + } +} diff --git a/cpg-language-csharp/src/test/resources/csharp/method.cs b/cpg-language-csharp/src/test/resources/csharp/method.cs index a4adaa53048..14c1d86561d 100644 --- a/cpg-language-csharp/src/test/resources/csharp/method.cs +++ b/cpg-language-csharp/src/test/resources/csharp/method.cs @@ -4,7 +4,10 @@ class Foo { void Bar() { } - void Baz(int a, string b) { - var a = 0; + void Baz(int a, string b) { } + + int returnSomething() + { + return 1; } } \ No newline at end of file From ac2f1804ad35fc75f1662f5935774a48e110cd68 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Mon, 23 Mar 2026 22:08:03 +0100 Subject: [PATCH 30/55] Add return statement and literal support --- .../src/main/csharp/NativeParser/Library.cs | 22 +++- .../aisec/cpg/frontends/csharp/CSharp.kt | 33 +++++- .../csharp/CSharpLanguageFrontend.kt | 1 + .../cpg/frontends/csharp/ExpressionHandler.kt | 64 +++++++++++ .../cpg/frontends/csharp/StatementHandler.kt | 9 ++ .../csharp/CSharpLanguageFrontendTest.kt | 104 ++++++++++++++++++ .../src/test/resources/csharp/literals.cs | 12 ++ .../src/test/resources/csharp/method.cs | 5 + 8 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt create mode 100644 cpg-language-csharp/src/test/resources/csharp/literals.cs diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 5effaaaee8a..6d6b19a9df3 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -211,7 +211,7 @@ public static IntPtr GetTypeName(IntPtr handlePtr) ); } - // `BaseMethodDeclarationSyntax` to get the body for Methods and Constructors. + // We use `BaseMethodDeclarationSyntax` to get the body for Methods and Constructors. [UnmanagedCallersOnly(EntryPoint = "GetBaseMethodDeclarationBody")] public static IntPtr GetBaseMethodDeclarationBody(IntPtr handlePtr) { @@ -229,4 +229,24 @@ public static IntPtr GetBlockStatement(IntPtr handlePtr, int index) { return Register(((BlockSyntax)Nodes[handlePtr]).Statements[index]); } + + [UnmanagedCallersOnly(EntryPoint = "GetReturnStatementExpression")] + public static IntPtr GetReturnStatementExpression(IntPtr handlePtr) + { + var expression = ((ReturnStatementSyntax)Nodes[handlePtr]).Expression; + if(expression != null) + { + return Register(expression); + } + else + { + return IntPtr.Zero; + }; + } + + [UnmanagedCallersOnly(EntryPoint = "GetLiteralExpressionValue")] + public static IntPtr GetLiteralExpressionValue(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8(((LiteralExpressionSyntax)Nodes[handlePtr]).Token.Value.ToString()); + } } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 705d8598547..f08e860f90a 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -178,12 +178,18 @@ interface Csharp : Library { return super.fromNative(nativeValue, context) } return when (INSTANCE.GetKind(nativeValue)) { + "ReturnStatement" -> ReturnStatementSyntax(nativeValue) + "Block" -> BlockSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } } } - class ReturnStatementSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) {} + class ReturnStatementSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) { + val expression: ExpressionSyntax? by lazy { + INSTANCE.GetReturnStatementExpression(this) + } + } /** * Represents the Roslyn @@ -230,6 +236,27 @@ interface Csharp : Library { val identifier: String by lazy { INSTANCE.GetParameterIdentifier(this) } val type: TypeSyntax by lazy { INSTANCE.GetParameterType(this) } } + + open class ExpressionSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + override fun fromNative(nativeValue: Any?, context: FromNativeContext?): Any? { + if (nativeValue !is Pointer) { + return super.fromNative(nativeValue, context) + } + return when (INSTANCE.GetKind(nativeValue)) { + "NumericLiteralExpression", + "StringLiteralExpression", + "TrueLiteralExpression", + "FalseLiteralExpression", + "NullLiteralExpression", + "CharacterLiteralExpression" -> LiteralExpressionSyntax(nativeValue) + else -> super.fromNative(nativeValue, context) + } + } + } + + class LiteralExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { + val value: String by lazy { INSTANCE.GetLiteralExpressionValue(this) } + } } /** @@ -311,6 +338,10 @@ interface Csharp : Library { fun GetBlockStatement(handle: AST.StatementSyntax, index: Int): AST.StatementSyntax + fun GetLiteralExpressionValue(handle: AST.ExpressionSyntax): String + + fun GetReturnStatementExpression(handle: AST.StatementSyntax): AST.ExpressionSyntax? + fun GetCode(handle: AST.Node): String fun GetNodeStartLine(handle: AST.Node): Int diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt index 3b39ddafbf0..85bc5f54262 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt @@ -43,6 +43,7 @@ class CSharpLanguageFrontend(ctx: TranslationContext, language: Language(::ProblemExpression, frontend) { + override fun handleNode(node: Csharp.AST.ExpressionSyntax): Expression { + return when (node) { + is Csharp.AST.LiteralExpressionSyntax -> handleLiteral(node) + else -> + newProblemExpression( + "The expression of class ${node.javaClass} is not supported yet", + rawNode = node, + ) + } + } + + private fun handleLiteral(node: Csharp.AST.LiteralExpressionSyntax): Expression { + return when (node.csharpType) { + "NumericLiteralExpression" -> + newLiteral(node.value.toInt(), primitiveType("int"), rawNode = node) + "StringLiteralExpression" -> + newLiteral(node.value, primitiveType("string"), rawNode = node) + "TrueLiteralExpression" -> newLiteral(true, primitiveType("bool"), rawNode = node) + "FalseLiteralExpression" -> newLiteral(false, primitiveType("bool"), rawNode = node) + "NullLiteralExpression" -> newLiteral(null, objectType("null"), rawNode = node) + "CharacterLiteralExpression" -> + newLiteral(node.value.single(), primitiveType("char"), rawNode = node) + else -> + // TODO: Return unknowntype() instead? + newProblemExpression("Unknown type: ${node.csharpType}", rawNode = node) + } + } +} diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt index 584796087ca..1a4bf3fdd82 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt @@ -28,7 +28,9 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.graph.expressions.Block import de.fraunhofer.aisec.cpg.graph.expressions.Expression import de.fraunhofer.aisec.cpg.graph.expressions.ProblemExpression +import de.fraunhofer.aisec.cpg.graph.expressions.Return import de.fraunhofer.aisec.cpg.graph.newBlock +import de.fraunhofer.aisec.cpg.graph.newReturn class StatementHandler(frontend: CSharpLanguageFrontend) : CSharpHandler( @@ -38,10 +40,17 @@ class StatementHandler(frontend: CSharpLanguageFrontend) : override fun handleNode(node: Csharp.AST.StatementSyntax): Expression { return when (node) { is Csharp.AST.BlockSyntax -> handleBlock(node) + is Csharp.AST.ReturnStatementSyntax -> handleReturn(node) else -> ProblemExpression("Not supported: ${node.csharpType}") } } + private fun handleReturn(node: Csharp.AST.ReturnStatementSyntax): Return { + val ret = newReturn(rawNode = node) + node.expression?.let { ret.returnValue = frontend.expressionHandler.handle(it) } + return ret + } + /** * Translates a C# * [`BlockSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.blocksyntax?view=roslyn-dotnet-5.0.0) diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt index f7bf48c5c40..d494f718231 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt @@ -27,12 +27,16 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.declarations.Constructor +import de.fraunhofer.aisec.cpg.graph.expressions.Block +import de.fraunhofer.aisec.cpg.graph.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.expressions.Return import de.fraunhofer.aisec.cpg.test.* import java.nio.file.Path import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs import kotlin.test.assertNotNull +import kotlin.test.assertNull class CSharpLanguageFrontendTest : BaseTest() { @@ -146,4 +150,104 @@ class CSharpLanguageFrontendTest : BaseTest() { assertEquals("x", twoParameters.parameters[0].name.localName) assertEquals("y", twoParameters.parameters[1].name.localName) } + + @Test + fun testReturnStatement() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("method.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + assertNotNull(tu) + + val foo = tu.namespaces["HelloWorld"]?.records["Foo"] + assertNotNull(foo) + + // return 1; + val returnSomething = foo.methods["returnSomething"] + assertNotNull(returnSomething) + val body = returnSomething.body + assertIs(body) + assertEquals(1, body.statements.size) + + val returnStmt = body.statements.first() + assertIs(returnStmt) + + val literal = returnStmt.returnValue + assertIs>(literal) + assertEquals(1, literal.value) + + // return; + val returnWithout = foo.methods["returnWithoutExpression"] + assertNotNull(returnWithout) + val body2 = returnWithout.body + assertIs(body2) + + val returnStmt2 = body2.statements.first() + assertIs(returnStmt2) + assertNull(returnStmt2.returnValue) + } + + @Test + fun testLiteralExpressionsTypes() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("literals.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + assertNotNull(tu) + + val foo = tu.records["Foo"] + assertNotNull(foo) + + // int + val returnInt = foo.methods["returnInt"] + assertNotNull(returnInt) + val intReturn = (returnInt.body as Block).statements.first() + assertIs(intReturn) + val intLiteral = intReturn.returnValue + assertIs>(intLiteral) + assertEquals(42, intLiteral.value) + assertEquals("int", intLiteral.type.name.localName) + + // string + val returnString = foo.methods["returnString"] + assertNotNull(returnString) + val stringReturn = (returnString.body as Block).statements.first() + assertIs(stringReturn) + val stringLiteral = stringReturn.returnValue + assertIs>(stringLiteral) + assertEquals("hello", stringLiteral.value) + assertEquals("string", stringLiteral.type.name.localName) + + // bool true + val returnTrue = foo.methods["returnTrue"] + assertNotNull(returnTrue) + val trueReturn = (returnTrue.body as Block).statements.first() + assertIs(trueReturn) + val trueLiteral = trueReturn.returnValue + assertIs>(trueLiteral) + assertEquals(true, trueLiteral.value) + assertEquals("bool", trueLiteral.type.name.localName) + + // bool false + val returnFalse = foo.methods["returnFalse"] + assertNotNull(returnFalse) + val falseReturn = (returnFalse.body as Block).statements.first() + assertIs(falseReturn) + val falseLiteral = falseReturn.returnValue + assertIs>(falseLiteral) + assertEquals(false, falseLiteral.value) + assertEquals("bool", falseLiteral.type.name.localName) + + // char + val returnChar = foo.methods["returnChar"] + assertNotNull(returnChar) + val charReturn = (returnChar.body as Block).statements.first() + assertIs(charReturn) + val charLiteral = charReturn.returnValue + assertIs>(charLiteral) + assertEquals('a', charLiteral.value) + assertEquals("char", charLiteral.type.name.localName) + } } diff --git a/cpg-language-csharp/src/test/resources/csharp/literals.cs b/cpg-language-csharp/src/test/resources/csharp/literals.cs new file mode 100644 index 00000000000..3a23ac9ee56 --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/literals.cs @@ -0,0 +1,12 @@ +class Foo +{ + int returnInt() { return 42; } + + string returnString() { return "hello"; } + + bool returnTrue() { return true; } + + bool returnFalse() { return false; } + + char returnChar() { return 'a'; } +} \ No newline at end of file diff --git a/cpg-language-csharp/src/test/resources/csharp/method.cs b/cpg-language-csharp/src/test/resources/csharp/method.cs index 14c1d86561d..124a316f17e 100644 --- a/cpg-language-csharp/src/test/resources/csharp/method.cs +++ b/cpg-language-csharp/src/test/resources/csharp/method.cs @@ -10,4 +10,9 @@ int returnSomething() { return 1; } + + void returnWithoutExpression() + { + return; + } } \ No newline at end of file From dac2bbad9d14ad350ccb42ffdb75e98343da2f10 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Mon, 23 Mar 2026 22:53:26 +0100 Subject: [PATCH 31/55] Start implementing support for IfStatements --- .../src/main/csharp/NativeParser/Library.cs | 6 +++++ .../aisec/cpg/frontends/csharp/CSharp.kt | 7 ++++++ .../cpg/frontends/csharp/ExpressionHandler.kt | 22 +++++++++++++------ .../cpg/frontends/csharp/StatementHandler.kt | 14 ++++++++++++ .../csharp/CSharpLanguageFrontendTest.kt | 14 ++++++------ .../csharp/{constructor.cs => Constructor.cs} | 0 .../resources/csharp/{fields.cs => Fields.cs} | 0 ...ed_namespace.cs => FileScopedNamespace.cs} | 0 .../src/test/resources/csharp/IfStatements.cs | 10 +++++++++ .../csharp/{literals.cs => Literals.cs} | 0 .../csharp/{method.cs => Methods.cs} | 0 .../csharp/{namespace.cs => Namespaces.cs} | 0 12 files changed, 59 insertions(+), 14 deletions(-) rename cpg-language-csharp/src/test/resources/csharp/{constructor.cs => Constructor.cs} (100%) rename cpg-language-csharp/src/test/resources/csharp/{fields.cs => Fields.cs} (100%) rename cpg-language-csharp/src/test/resources/csharp/{fileScoped_namespace.cs => FileScopedNamespace.cs} (100%) create mode 100644 cpg-language-csharp/src/test/resources/csharp/IfStatements.cs rename cpg-language-csharp/src/test/resources/csharp/{literals.cs => Literals.cs} (100%) rename cpg-language-csharp/src/test/resources/csharp/{method.cs => Methods.cs} (100%) rename cpg-language-csharp/src/test/resources/csharp/{namespace.cs => Namespaces.cs} (100%) diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 6d6b19a9df3..01ebc590569 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -249,4 +249,10 @@ public static IntPtr GetLiteralExpressionValue(IntPtr handlePtr) { return Marshal.StringToCoTaskMemUTF8(((LiteralExpressionSyntax)Nodes[handlePtr]).Token.Value.ToString()); } + + [UnmanagedCallersOnly(EntryPoint = "GetIfStatementSyntaxCondition")] + public static IntPtr GetIfStatementSyntaxCondition(IntPtr handlePtr) + { + return Register(((IfStatementSyntax)Nodes[handlePtr]).Condition); + } } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index f08e860f90a..3e4eea15ba3 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -180,6 +180,7 @@ interface Csharp : Library { return when (INSTANCE.GetKind(nativeValue)) { "ReturnStatement" -> ReturnStatementSyntax(nativeValue) "Block" -> BlockSyntax(nativeValue) + "IfStatementSyntax" -> IfStatementSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } } @@ -191,6 +192,10 @@ interface Csharp : Library { } } + class IfStatementSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) { + val condition: ExpressionSyntax by lazy { INSTANCE.GetIfStatementSyntaxCondition(this) } + } + /** * Represents the Roslyn * [`ConstructorDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.constructordeclarationsyntax?view=roslyn-dotnet-5.0.0) @@ -342,6 +347,8 @@ interface Csharp : Library { fun GetReturnStatementExpression(handle: AST.StatementSyntax): AST.ExpressionSyntax? + fun GetIfStatementSyntaxCondition(handle: AST.StatementSyntax): AST.ExpressionSyntax + fun GetCode(handle: AST.Node): String fun GetNodeStartLine(handle: AST.Node): Int diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt index fb8d04893dd..c9d219840b2 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt @@ -26,11 +26,11 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.graph.expressions.Expression +import de.fraunhofer.aisec.cpg.graph.expressions.Literal import de.fraunhofer.aisec.cpg.graph.expressions.ProblemExpression import de.fraunhofer.aisec.cpg.graph.newLiteral import de.fraunhofer.aisec.cpg.graph.newProblemExpression import de.fraunhofer.aisec.cpg.graph.objectType -import de.fraunhofer.aisec.cpg.graph.primitiveType class ExpressionHandler(frontend: CSharpLanguageFrontend) : CSharpHandler(::ProblemExpression, frontend) { @@ -45,19 +45,27 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : } } + /** + * Translates a C# + * [`LiteralExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.literalexpressionsyntax?view=roslyn-dotnet-5.0.0) + * into a [Literal]. + */ private fun handleLiteral(node: Csharp.AST.LiteralExpressionSyntax): Expression { + val builtInTypes = frontend.language.builtInTypes return when (node.csharpType) { "NumericLiteralExpression" -> - newLiteral(node.value.toInt(), primitiveType("int"), rawNode = node) + newLiteral(node.value.toInt(), builtInTypes.getValue("int"), rawNode = node) "StringLiteralExpression" -> - newLiteral(node.value, primitiveType("string"), rawNode = node) - "TrueLiteralExpression" -> newLiteral(true, primitiveType("bool"), rawNode = node) - "FalseLiteralExpression" -> newLiteral(false, primitiveType("bool"), rawNode = node) + newLiteral(node.value, builtInTypes.getValue("string"), rawNode = node) + "TrueLiteralExpression" -> + newLiteral(true, builtInTypes.getValue("bool"), rawNode = node) + "FalseLiteralExpression" -> + newLiteral(false, builtInTypes.getValue("bool"), rawNode = node) "NullLiteralExpression" -> newLiteral(null, objectType("null"), rawNode = node) "CharacterLiteralExpression" -> - newLiteral(node.value.single(), primitiveType("char"), rawNode = node) + newLiteral(node.value.single(), builtInTypes.getValue("char"), rawNode = node) else -> - // TODO: Return unknowntype() instead? + // TODO: Return unknownType() instead? newProblemExpression("Unknown type: ${node.csharpType}", rawNode = node) } } diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt index 1a4bf3fdd82..50c12b75c6e 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt @@ -27,9 +27,11 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.graph.expressions.Block import de.fraunhofer.aisec.cpg.graph.expressions.Expression +import de.fraunhofer.aisec.cpg.graph.expressions.IfElse import de.fraunhofer.aisec.cpg.graph.expressions.ProblemExpression import de.fraunhofer.aisec.cpg.graph.expressions.Return import de.fraunhofer.aisec.cpg.graph.newBlock +import de.fraunhofer.aisec.cpg.graph.newIfElse import de.fraunhofer.aisec.cpg.graph.newReturn class StatementHandler(frontend: CSharpLanguageFrontend) : @@ -41,10 +43,22 @@ class StatementHandler(frontend: CSharpLanguageFrontend) : return when (node) { is Csharp.AST.BlockSyntax -> handleBlock(node) is Csharp.AST.ReturnStatementSyntax -> handleReturn(node) + is Csharp.AST.IfStatementSyntax -> handleIf(node) else -> ProblemExpression("Not supported: ${node.csharpType}") } } + private fun handleIf(node: Csharp.AST.IfStatementSyntax): IfElse { + return newIfElse(rawNode = node).apply { + this.condition = frontend.expressionHandler.handle(node.condition) + } + } + + /** + * Translates a C# + * [`ReturnStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.returnstatementsyntax?view=roslyn-dotnet-5.0.0) + * into a [Return]. The return value expression is optional (e.g. `return;` in void methods). + */ private fun handleReturn(node: Csharp.AST.ReturnStatementSyntax): Return { val ret = newReturn(rawNode = node) node.expression?.let { ret.returnValue = frontend.expressionHandler.handle(it) } diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt index d494f718231..652ea269183 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt @@ -45,7 +45,7 @@ class CSharpLanguageFrontendTest : BaseTest() { val topLevel = Path.of("src", "test", "resources", "csharp") val tu = analyzeAndGetFirstTU( - listOf(topLevel.resolve("namespace.cs").toFile()), + listOf(topLevel.resolve("Namespaces.cs").toFile()), topLevel, true, ) { @@ -71,7 +71,7 @@ class CSharpLanguageFrontendTest : BaseTest() { val topLevel = Path.of("src", "test", "resources", "csharp") val tu = analyzeAndGetFirstTU( - listOf(topLevel.resolve("fileScoped_namespace.cs").toFile()), + listOf(topLevel.resolve("FileScopedNamespace.cs").toFile()), topLevel, true, ) { @@ -92,7 +92,7 @@ class CSharpLanguageFrontendTest : BaseTest() { fun testFieldDeclarations() { val topLevel = Path.of("src", "test", "resources", "csharp") val tu = - analyzeAndGetFirstTU(listOf(topLevel.resolve("fields.cs").toFile()), topLevel, true) { + analyzeAndGetFirstTU(listOf(topLevel.resolve("Fields.cs").toFile()), topLevel, true) { it.registerLanguage() } assertNotNull(tu) @@ -102,7 +102,7 @@ class CSharpLanguageFrontendTest : BaseTest() { fun testMethodDeclarations() { val topLevel = Path.of("src", "test", "resources", "csharp") val tu = - analyzeAndGetFirstTU(listOf(topLevel.resolve("method.cs").toFile()), topLevel, true) { + analyzeAndGetFirstTU(listOf(topLevel.resolve("Methods.cs").toFile()), topLevel, true) { it.registerLanguage() } assertNotNull(tu) @@ -126,7 +126,7 @@ class CSharpLanguageFrontendTest : BaseTest() { val topLevel = Path.of("src", "test", "resources", "csharp") val tu = analyzeAndGetFirstTU( - listOf(topLevel.resolve("constructor.cs").toFile()), + listOf(topLevel.resolve("Constructor.cs").toFile()), topLevel, true, ) { @@ -155,7 +155,7 @@ class CSharpLanguageFrontendTest : BaseTest() { fun testReturnStatement() { val topLevel = Path.of("src", "test", "resources", "csharp") val tu = - analyzeAndGetFirstTU(listOf(topLevel.resolve("method.cs").toFile()), topLevel, true) { + analyzeAndGetFirstTU(listOf(topLevel.resolve("Methods.cs").toFile()), topLevel, true) { it.registerLanguage() } assertNotNull(tu) @@ -192,7 +192,7 @@ class CSharpLanguageFrontendTest : BaseTest() { fun testLiteralExpressionsTypes() { val topLevel = Path.of("src", "test", "resources", "csharp") val tu = - analyzeAndGetFirstTU(listOf(topLevel.resolve("literals.cs").toFile()), topLevel, true) { + analyzeAndGetFirstTU(listOf(topLevel.resolve("Literals.cs").toFile()), topLevel, true) { it.registerLanguage() } assertNotNull(tu) diff --git a/cpg-language-csharp/src/test/resources/csharp/constructor.cs b/cpg-language-csharp/src/test/resources/csharp/Constructor.cs similarity index 100% rename from cpg-language-csharp/src/test/resources/csharp/constructor.cs rename to cpg-language-csharp/src/test/resources/csharp/Constructor.cs diff --git a/cpg-language-csharp/src/test/resources/csharp/fields.cs b/cpg-language-csharp/src/test/resources/csharp/Fields.cs similarity index 100% rename from cpg-language-csharp/src/test/resources/csharp/fields.cs rename to cpg-language-csharp/src/test/resources/csharp/Fields.cs diff --git a/cpg-language-csharp/src/test/resources/csharp/fileScoped_namespace.cs b/cpg-language-csharp/src/test/resources/csharp/FileScopedNamespace.cs similarity index 100% rename from cpg-language-csharp/src/test/resources/csharp/fileScoped_namespace.cs rename to cpg-language-csharp/src/test/resources/csharp/FileScopedNamespace.cs diff --git a/cpg-language-csharp/src/test/resources/csharp/IfStatements.cs b/cpg-language-csharp/src/test/resources/csharp/IfStatements.cs new file mode 100644 index 00000000000..df5755f38c3 --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/IfStatements.cs @@ -0,0 +1,10 @@ +class Bar +{ + void doIf(int a) + { + if(a < 10) + { + new String("smaller than 10"); + } + } +} \ No newline at end of file diff --git a/cpg-language-csharp/src/test/resources/csharp/literals.cs b/cpg-language-csharp/src/test/resources/csharp/Literals.cs similarity index 100% rename from cpg-language-csharp/src/test/resources/csharp/literals.cs rename to cpg-language-csharp/src/test/resources/csharp/Literals.cs diff --git a/cpg-language-csharp/src/test/resources/csharp/method.cs b/cpg-language-csharp/src/test/resources/csharp/Methods.cs similarity index 100% rename from cpg-language-csharp/src/test/resources/csharp/method.cs rename to cpg-language-csharp/src/test/resources/csharp/Methods.cs diff --git a/cpg-language-csharp/src/test/resources/csharp/namespace.cs b/cpg-language-csharp/src/test/resources/csharp/Namespaces.cs similarity index 100% rename from cpg-language-csharp/src/test/resources/csharp/namespace.cs rename to cpg-language-csharp/src/test/resources/csharp/Namespaces.cs From 6f73461a87b454e95ce0aa2e35b1fc9ddea0d719 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Tue, 24 Mar 2026 15:41:08 +0100 Subject: [PATCH 32/55] Change getKind to getType; Add BinaryExpressionSyntax --- .../src/main/csharp/NativeParser/Library.cs | 70 ++++++++++++++++++- .../aisec/cpg/frontends/csharp/CSharp.kt | 61 ++++++++++------ .../cpg/frontends/csharp/ExpressionHandler.kt | 44 +++++++----- .../csharp/CSharpLanguageFrontendTest.kt | 17 +++++ 4 files changed, 154 insertions(+), 38 deletions(-) diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 01ebc590569..45d80627af3 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -19,7 +19,7 @@ private static IntPtr Register(CSharpSyntaxNode node) private static void PrintASTDump(SyntaxNode node, int indent = 2) { - Console.Error.WriteLine(new string(' ', indent) + node.GetType().Name + ": " + node.ToString().Split('\n')[0].Trim()); + Console.Error.WriteLine(new string(' ', indent) + node.GetType().Name + " (Kind: " + ((CSharpSyntaxNode)node).Kind() + "): " + node.ToString().Split('\n')[0].Trim()); foreach (var child in node.ChildNodes()) PrintASTDump(child, indent + 1); } @@ -43,6 +43,12 @@ public static IntPtr GetKind(IntPtr handlePtr) return Marshal.StringToCoTaskMemUTF8(kind); } + [UnmanagedCallersOnly(EntryPoint = "GetType")] + public static IntPtr GetType(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8(Nodes[handlePtr].GetType().Name); + } + [UnmanagedCallersOnly(EntryPoint = "GetCompilationUnitMembersCount")] public static int GetCompilationUnitMembersCount(IntPtr handlePtr) { @@ -250,9 +256,71 @@ public static IntPtr GetLiteralExpressionValue(IntPtr handlePtr) return Marshal.StringToCoTaskMemUTF8(((LiteralExpressionSyntax)Nodes[handlePtr]).Token.Value.ToString()); } + // Maps .NET runtime types (e.g. System.Int32) to C# keywords (e.g. "int"). + // Roslyn's Token.Value returns .NET runtime types, but we need the C# keywords to match + // the built-in type definitions on the Kotlin side. + + // The alternative would be to use Roslyn's SyntaxKind (e.g. "NumericLiteralExpression", + // "StringLiteralExpression") via GetKind() and do the mapping on the Kotlin side. + private static readonly Dictionary DotNetTypeToCSharpKeyword = new() + { + { typeof(int), "int" }, + { typeof(long), "long" }, + { typeof(float), "float" }, + { typeof(double), "double" }, + { typeof(decimal), "decimal" }, + { typeof(string), "string" }, + { typeof(bool), "bool" }, + { typeof(char), "char" }, + { typeof(byte), "byte" }, + { typeof(sbyte), "sbyte" }, + { typeof(short), "short" }, + { typeof(ushort), "ushort" }, + { typeof(uint), "uint" }, + { typeof(ulong), "ulong" }, + }; + + [UnmanagedCallersOnly(EntryPoint = "GetLiteralExpressionKind")] + public static IntPtr GetLiteralExpressionKind(IntPtr handlePtr) + { + var value = ((LiteralExpressionSyntax)Nodes[handlePtr]).Token.Value; + if (value == null) + { + return Marshal.StringToCoTaskMemUTF8("null"); + } + else + { + return Marshal.StringToCoTaskMemUTF8(DotNetTypeToCSharpKeyword.TryGetValue(value.GetType(), out var keyword) ? keyword : value.GetType().Name); + } + } + [UnmanagedCallersOnly(EntryPoint = "GetIfStatementSyntaxCondition")] public static IntPtr GetIfStatementSyntaxCondition(IntPtr handlePtr) { return Register(((IfStatementSyntax)Nodes[handlePtr]).Condition); } + + [UnmanagedCallersOnly(EntryPoint = "GetBinaryExpressionLeft")] + public static IntPtr GetBinaryExpressionLeft(IntPtr handlePtr) + { + return Register(((BinaryExpressionSyntax)Nodes[handlePtr]).Left); + } + + [UnmanagedCallersOnly(EntryPoint = "GetBinaryExpressionRight")] + public static IntPtr GetBinaryExpressionRight(IntPtr handlePtr) + { + return Register(((BinaryExpressionSyntax)Nodes[handlePtr]).Right); + } + + [UnmanagedCallersOnly(EntryPoint = "GetBinaryExpressionOperator")] + public static IntPtr GetBinaryExpressionOperator(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8(((BinaryExpressionSyntax)Nodes[handlePtr]).OperatorToken.Text); + } + + [UnmanagedCallersOnly(EntryPoint = "GetIdentifierNameSyntaxIdentifier")] + public static IntPtr GetIdentifierNameSyntaxIdentifier(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8(((IdentifierNameSyntax)Nodes[handlePtr]).Identifier.ToString()); + } } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 3e4eea15ba3..9cacc2d6a01 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -39,7 +39,7 @@ interface Csharp : Library { open class CsharpObject(p: Pointer? = Pointer.NULL) : PointerType(p) { val csharpType: String get() { - return INSTANCE.GetKind(this.pointer) + return INSTANCE.GetType(this.pointer) } } @@ -96,13 +96,14 @@ interface Csharp : Library { if (nativeValue !is Pointer) { return super.fromNative(nativeValue, context) } - return when (INSTANCE.GetKind(nativeValue)) { - "NamespaceDeclaration", - "FileScopedNamespaceDeclaration" -> NamespaceDeclarationSyntax(nativeValue) - "ClassDeclaration" -> ClassDeclarationSyntax(nativeValue) - "FieldDeclaration" -> FieldDeclarationSyntax(nativeValue) - "MethodDeclaration" -> MethodDeclarationSyntax(nativeValue) - "ConstructorDeclaration" -> ConstructorDeclarationSyntax(nativeValue) + return when (INSTANCE.GetType(nativeValue)) { + "NamespaceDeclarationSyntax", + "FileScopedNamespaceDeclarationSyntax" -> + NamespaceDeclarationSyntax(nativeValue) + "ClassDeclarationSyntax" -> ClassDeclarationSyntax(nativeValue) + "FieldDeclarationSyntax" -> FieldDeclarationSyntax(nativeValue) + "MethodDeclarationSyntax" -> MethodDeclarationSyntax(nativeValue) + "ConstructorDeclarationSyntax" -> ConstructorDeclarationSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } } @@ -145,7 +146,7 @@ interface Csharp : Library { if (nativeValue !is Pointer) { return super.fromNative(nativeValue, context) } - return when (INSTANCE.GetKind(nativeValue)) { + return when (INSTANCE.GetType(nativeValue)) { else -> super.fromNative(nativeValue, context) } } @@ -177,9 +178,9 @@ interface Csharp : Library { if (nativeValue !is Pointer) { return super.fromNative(nativeValue, context) } - return when (INSTANCE.GetKind(nativeValue)) { - "ReturnStatement" -> ReturnStatementSyntax(nativeValue) - "Block" -> BlockSyntax(nativeValue) + return when (INSTANCE.GetType(nativeValue)) { + "ReturnStatementSyntax" -> ReturnStatementSyntax(nativeValue) + "BlockSyntax" -> BlockSyntax(nativeValue) "IfStatementSyntax" -> IfStatementSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } @@ -247,13 +248,10 @@ interface Csharp : Library { if (nativeValue !is Pointer) { return super.fromNative(nativeValue, context) } - return when (INSTANCE.GetKind(nativeValue)) { - "NumericLiteralExpression", - "StringLiteralExpression", - "TrueLiteralExpression", - "FalseLiteralExpression", - "NullLiteralExpression", - "CharacterLiteralExpression" -> LiteralExpressionSyntax(nativeValue) + return when (INSTANCE.GetType(nativeValue)) { + "LiteralExpressionSyntax" -> LiteralExpressionSyntax(nativeValue) + "BinaryExpressionSyntax" -> BinaryExpressionSyntax(nativeValue) + "IdentifierNameSyntax" -> IdentifierNameSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } } @@ -261,6 +259,17 @@ interface Csharp : Library { class LiteralExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { val value: String by lazy { INSTANCE.GetLiteralExpressionValue(this) } + val kind: String by lazy { INSTANCE.GetLiteralExpressionKind(this) } + } + + class BinaryExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { + val left: ExpressionSyntax by lazy { INSTANCE.GetBinaryExpressionLeft(this) } + val operatorToken: String by lazy { INSTANCE.GetBinaryExpressionOperator(this) } + val right: ExpressionSyntax by lazy { INSTANCE.GetBinaryExpressionRight(this) } + } + + class IdentifierNameSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { + val identifier: String by lazy { INSTANCE.GetIdentifierNameSyntaxIdentifier(this) } } } @@ -279,6 +288,8 @@ interface Csharp : Library { fun GetKind(handle: Pointer): String + fun GetType(handle: Pointer): String + fun GetCompilationUnitMembersCount(handle: AST.CompilationUnitSyntax): Int fun GetCompilationUnitMember( @@ -343,12 +354,20 @@ interface Csharp : Library { fun GetBlockStatement(handle: AST.StatementSyntax, index: Int): AST.StatementSyntax - fun GetLiteralExpressionValue(handle: AST.ExpressionSyntax): String + fun GetLiteralExpressionValue(handle: AST.LiteralExpressionSyntax): String + + fun GetLiteralExpressionKind(handle: AST.LiteralExpressionSyntax): String fun GetReturnStatementExpression(handle: AST.StatementSyntax): AST.ExpressionSyntax? fun GetIfStatementSyntaxCondition(handle: AST.StatementSyntax): AST.ExpressionSyntax + fun GetBinaryExpressionLeft(handle: AST.BinaryExpressionSyntax): AST.ExpressionSyntax + + fun GetBinaryExpressionOperator(handle: AST.BinaryExpressionSyntax): String + + fun GetBinaryExpressionRight(handle: AST.BinaryExpressionSyntax): AST.ExpressionSyntax + fun GetCode(handle: AST.Node): String fun GetNodeStartLine(handle: AST.Node): Int @@ -359,6 +378,8 @@ interface Csharp : Library { fun GetNodeEndColumn(handle: AST.Node): Int + fun GetIdentifierNameSyntaxIdentifier(handle: AST.IdentifierNameSyntax): String + companion object { val INSTANCE: Csharp by lazy { try { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt index c9d219840b2..aaa8a640b21 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt @@ -25,18 +25,24 @@ */ package de.fraunhofer.aisec.cpg.frontends.csharp +import de.fraunhofer.aisec.cpg.graph.expressions.BinaryOperator import de.fraunhofer.aisec.cpg.graph.expressions.Expression import de.fraunhofer.aisec.cpg.graph.expressions.Literal import de.fraunhofer.aisec.cpg.graph.expressions.ProblemExpression +import de.fraunhofer.aisec.cpg.graph.expressions.Reference +import de.fraunhofer.aisec.cpg.graph.newBinaryOperator import de.fraunhofer.aisec.cpg.graph.newLiteral import de.fraunhofer.aisec.cpg.graph.newProblemExpression +import de.fraunhofer.aisec.cpg.graph.newReference import de.fraunhofer.aisec.cpg.graph.objectType class ExpressionHandler(frontend: CSharpLanguageFrontend) : CSharpHandler(::ProblemExpression, frontend) { override fun handleNode(node: Csharp.AST.ExpressionSyntax): Expression { return when (node) { - is Csharp.AST.LiteralExpressionSyntax -> handleLiteral(node) + is Csharp.AST.IdentifierNameSyntax -> handleIdentifierName(node) + is Csharp.AST.LiteralExpressionSyntax -> handleLiteralExpression(node) + is Csharp.AST.BinaryExpressionSyntax -> handleBinaryExpression(node) else -> newProblemExpression( "The expression of class ${node.javaClass} is not supported yet", @@ -45,28 +51,32 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : } } + private fun handleIdentifierName(node: Csharp.AST.IdentifierNameSyntax): Reference { + return newReference(name = node.identifier, rawNode = node) + } + + private fun handleBinaryExpression(node: Csharp.AST.BinaryExpressionSyntax): BinaryOperator { + val binOp = newBinaryOperator(operatorCode = node.operatorToken, rawNode = node) + binOp.lhs = handle(node.left) + binOp.rhs = handle(node.right) + return binOp + } + /** * Translates a C# * [`LiteralExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.literalexpressionsyntax?view=roslyn-dotnet-5.0.0) * into a [Literal]. */ - private fun handleLiteral(node: Csharp.AST.LiteralExpressionSyntax): Expression { + private fun handleLiteralExpression(node: Csharp.AST.LiteralExpressionSyntax): Expression { val builtInTypes = frontend.language.builtInTypes - return when (node.csharpType) { - "NumericLiteralExpression" -> - newLiteral(node.value.toInt(), builtInTypes.getValue("int"), rawNode = node) - "StringLiteralExpression" -> - newLiteral(node.value, builtInTypes.getValue("string"), rawNode = node) - "TrueLiteralExpression" -> - newLiteral(true, builtInTypes.getValue("bool"), rawNode = node) - "FalseLiteralExpression" -> - newLiteral(false, builtInTypes.getValue("bool"), rawNode = node) - "NullLiteralExpression" -> newLiteral(null, objectType("null"), rawNode = node) - "CharacterLiteralExpression" -> - newLiteral(node.value.single(), builtInTypes.getValue("char"), rawNode = node) - else -> - // TODO: Return unknownType() instead? - newProblemExpression("Unknown type: ${node.csharpType}", rawNode = node) + return when (node.kind) { + "int" -> newLiteral(node.value.toInt(), builtInTypes.getValue("int"), rawNode = node) + "string" -> newLiteral(node.value, builtInTypes.getValue("string"), rawNode = node) + "bool" -> + newLiteral(node.value.toBoolean(), builtInTypes.getValue("bool"), rawNode = node) + "char" -> newLiteral(node.value.single(), builtInTypes.getValue("char"), rawNode = node) + "null" -> newLiteral(null, objectType("null"), rawNode = node) + else -> newProblemExpression("Unknown literal kind: ${node.kind}", rawNode = node) } } } diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt index 652ea269183..d587fca99e8 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt @@ -250,4 +250,21 @@ class CSharpLanguageFrontendTest : BaseTest() { assertEquals('a', charLiteral.value) assertEquals("char", charLiteral.type.name.localName) } + + @Test + fun testIfStatement() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("IfStatements.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val bar = tu.records["Bar"] + assertNotNull(bar) + } } From 6c56f236d6ec456c669192c51372db6d726a5008 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Wed, 25 Mar 2026 10:31:07 +0100 Subject: [PATCH 33/55] IfStatements --- .../src/main/csharp/NativeParser/Library.cs | 66 ++++++------- .../aisec/cpg/frontends/csharp/CSharp.kt | 93 ++++++++++++++++++- .../cpg/frontends/csharp/ExpressionHandler.kt | 43 ++++++--- .../cpg/frontends/csharp/StatementHandler.kt | 7 ++ .../csharp/CSharpLanguageFrontendTest.kt | 54 +++++++++++ .../src/test/resources/csharp/IfStatements.cs | 33 ++++++- 6 files changed, 238 insertions(+), 58 deletions(-) diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 45d80627af3..0fc59a4934b 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -18,11 +18,11 @@ private static IntPtr Register(CSharpSyntaxNode node) } private static void PrintASTDump(SyntaxNode node, int indent = 2) - { - Console.Error.WriteLine(new string(' ', indent) + node.GetType().Name + " (Kind: " + ((CSharpSyntaxNode)node).Kind() + "): " + node.ToString().Split('\n')[0].Trim()); - foreach (var child in node.ChildNodes()) - PrintASTDump(child, indent + 1); - } + { + Console.Error.WriteLine(new string(' ', indent) + node.GetType().Name + " (Kind: " + ((CSharpSyntaxNode)node).Kind() + "): " + node.ToString().Split('\n')[0].Trim()); + foreach (var child in node.ChildNodes()) + PrintASTDump(child, indent + 1); + } [UnmanagedCallersOnly(EntryPoint = "CSharpRoslynSyntaxTreeParseText")] public static IntPtr CSharpRoslynSyntaxTreeParseText(IntPtr sourcePtr) @@ -256,48 +256,36 @@ public static IntPtr GetLiteralExpressionValue(IntPtr handlePtr) return Marshal.StringToCoTaskMemUTF8(((LiteralExpressionSyntax)Nodes[handlePtr]).Token.Value.ToString()); } - // Maps .NET runtime types (e.g. System.Int32) to C# keywords (e.g. "int"). - // Roslyn's Token.Value returns .NET runtime types, but we need the C# keywords to match - // the built-in type definitions on the Kotlin side. - - // The alternative would be to use Roslyn's SyntaxKind (e.g. "NumericLiteralExpression", - // "StringLiteralExpression") via GetKind() and do the mapping on the Kotlin side. - private static readonly Dictionary DotNetTypeToCSharpKeyword = new() - { - { typeof(int), "int" }, - { typeof(long), "long" }, - { typeof(float), "float" }, - { typeof(double), "double" }, - { typeof(decimal), "decimal" }, - { typeof(string), "string" }, - { typeof(bool), "bool" }, - { typeof(char), "char" }, - { typeof(byte), "byte" }, - { typeof(sbyte), "sbyte" }, - { typeof(short), "short" }, - { typeof(ushort), "ushort" }, - { typeof(uint), "uint" }, - { typeof(ulong), "ulong" }, - }; - - [UnmanagedCallersOnly(EntryPoint = "GetLiteralExpressionKind")] - public static IntPtr GetLiteralExpressionKind(IntPtr handlePtr) - { - var value = ((LiteralExpressionSyntax)Nodes[handlePtr]).Token.Value; - if (value == null) + [UnmanagedCallersOnly(EntryPoint = "GetIfStatementSyntaxCondition")] + public static IntPtr GetIfStatementSyntaxCondition(IntPtr handlePtr) + { + return Register(((IfStatementSyntax)Nodes[handlePtr]).Condition); + } + + [UnmanagedCallersOnly(EntryPoint = "GetIfStatementSyntaxStatement")] + public static IntPtr GetIfStatementSyntaxStatement(IntPtr handlePtr) + { + return Register(((IfStatementSyntax)Nodes[handlePtr]).Statement); + } + + [UnmanagedCallersOnly(EntryPoint = "GetElseClauseSyntax")] + public static IntPtr GetElseClauseSyntax(IntPtr handlePtr) + { + var elseClause = ((IfStatementSyntax)Nodes[handlePtr]).Else; + if (elseClause != null) { - return Marshal.StringToCoTaskMemUTF8("null"); + return Register(elseClause); } else { - return Marshal.StringToCoTaskMemUTF8(DotNetTypeToCSharpKeyword.TryGetValue(value.GetType(), out var keyword) ? keyword : value.GetType().Name); + return IntPtr.Zero; } } - [UnmanagedCallersOnly(EntryPoint = "GetIfStatementSyntaxCondition")] - public static IntPtr GetIfStatementSyntaxCondition(IntPtr handlePtr) + [UnmanagedCallersOnly(EntryPoint = "GetElseClauseSyntaxStatement")] + public static IntPtr GetElseClauseSyntaxStatement(IntPtr handlePtr) { - return Register(((IfStatementSyntax)Nodes[handlePtr]).Condition); + return Register(((ElseClauseSyntax)Nodes[handlePtr]).Statement); } [UnmanagedCallersOnly(EntryPoint = "GetBinaryExpressionLeft")] diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 9cacc2d6a01..41bd7b55db1 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -166,6 +166,11 @@ interface Csharp : Library { val body: BlockSyntax by lazy { INSTANCE.GetBaseMethodDeclarationBody(this) } } + /** + * Represents the Roslyn + * [`BlockSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.blocksyntax?view=roslyn-dotnet-5.0.0) + * class. + */ class BlockSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) { val statements: List by lazy { val count = INSTANCE.GetBlockStatementCount(this) @@ -173,6 +178,11 @@ interface Csharp : Library { } } + /** + * Represents the Roslyn + * [`StatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.statementsyntax?view=roslyn-dotnet-5.0.0) + * class. + */ open class StatementSyntax(p: Pointer? = Pointer.NULL) : Node(p) { override fun fromNative(nativeValue: Any?, context: FromNativeContext?): Any { if (nativeValue !is Pointer) { @@ -187,14 +197,37 @@ interface Csharp : Library { } } + /** + * Represents the Roslyn + * [`ReturnStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.returnstatementsyntax?view=roslyn-dotnet-5.0.0) + * class. + */ class ReturnStatementSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) { val expression: ExpressionSyntax? by lazy { INSTANCE.GetReturnStatementExpression(this) } } + /** + * Represents the Roslyn + * [`IfStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.ifstatementsyntax?view=roslyn-dotnet-5.0.0) + * class. + */ class IfStatementSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) { val condition: ExpressionSyntax by lazy { INSTANCE.GetIfStatementSyntaxCondition(this) } + /** The statement that is executed when the condition is true. */ + val statement: StatementSyntax by lazy { INSTANCE.GetIfStatementSyntaxStatement(this) } + val elseClause: ElseClauseSyntax? by lazy { INSTANCE.GetElseClauseSyntax(this) } + } + + /** + * Represents the Roslyn + * [`ElseClauseSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.elseclausesyntax?view=roslyn-dotnet-5.0.0) + * class. Note: not a [StatementSyntax] because it cannot appear on its own. It's always + * attached to an [IfStatementSyntax]. + */ + class ElseClauseSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val statement: StatementSyntax by lazy { INSTANCE.GetElseClauseSyntaxStatement(this) } } /** @@ -243,13 +276,31 @@ interface Csharp : Library { val type: TypeSyntax by lazy { INSTANCE.GetParameterType(this) } } + /** + * Represents the Roslyn + * [`ExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.expressionsyntax?view=roslyn-dotnet-5.0.0) + * class. + */ open class ExpressionSyntax(p: Pointer? = Pointer.NULL) : Node(p) { override fun fromNative(nativeValue: Any?, context: FromNativeContext?): Any? { if (nativeValue !is Pointer) { return super.fromNative(nativeValue, context) } return when (INSTANCE.GetType(nativeValue)) { - "LiteralExpressionSyntax" -> LiteralExpressionSyntax(nativeValue) + // All literals share the same Roslyn type (LiteralExpressionSyntax), + // so we need to use 'GetKind' to get the underlying type. + "LiteralExpressionSyntax" -> + when (INSTANCE.GetKind(nativeValue)) { + "NumericLiteralExpression" -> + NumericLiteralExpressionSyntax(nativeValue) + "StringLiteralExpression" -> StringLiteralExpressionSyntax(nativeValue) + "TrueLiteralExpression", + "FalseLiteralExpression" -> BooleanLiteralExpressionSyntax(nativeValue) + "CharacterLiteralExpression" -> + CharacterLiteralExpressionSyntax(nativeValue) + "NullLiteralExpression" -> NullLiteralExpressionSyntax(nativeValue) + else -> LiteralExpressionSyntax(nativeValue) + } "BinaryExpressionSyntax" -> BinaryExpressionSyntax(nativeValue) "IdentifierNameSyntax" -> IdentifierNameSyntax(nativeValue) else -> super.fromNative(nativeValue, context) @@ -257,17 +308,45 @@ interface Csharp : Library { } } - class LiteralExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { + /** + * Represents the Roslyn + * [`LiteralExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.literalexpressionsyntax?view=roslyn-dotnet-5.0.0) + * class. + */ + open class LiteralExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { val value: String by lazy { INSTANCE.GetLiteralExpressionValue(this) } - val kind: String by lazy { INSTANCE.GetLiteralExpressionKind(this) } } + class NumericLiteralExpressionSyntax(p: Pointer? = Pointer.NULL) : + LiteralExpressionSyntax(p) + + class StringLiteralExpressionSyntax(p: Pointer? = Pointer.NULL) : + LiteralExpressionSyntax(p) + + class BooleanLiteralExpressionSyntax(p: Pointer? = Pointer.NULL) : + LiteralExpressionSyntax(p) + + class CharacterLiteralExpressionSyntax(p: Pointer? = Pointer.NULL) : + LiteralExpressionSyntax(p) + + class NullLiteralExpressionSyntax(p: Pointer? = Pointer.NULL) : LiteralExpressionSyntax(p) + + /** + * Represents the Roslyn + * [`BinaryExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.binaryexpressionsyntax?view=roslyn-dotnet-5.0.0) + * class. + */ class BinaryExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { val left: ExpressionSyntax by lazy { INSTANCE.GetBinaryExpressionLeft(this) } val operatorToken: String by lazy { INSTANCE.GetBinaryExpressionOperator(this) } val right: ExpressionSyntax by lazy { INSTANCE.GetBinaryExpressionRight(this) } } + /** + * Represents the Roslyn + * [`IdentifierNameSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.identifiernamesyntax?view=roslyn-dotnet-5.0.0) + * class. + */ class IdentifierNameSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { val identifier: String by lazy { INSTANCE.GetIdentifierNameSyntaxIdentifier(this) } } @@ -356,8 +435,6 @@ interface Csharp : Library { fun GetLiteralExpressionValue(handle: AST.LiteralExpressionSyntax): String - fun GetLiteralExpressionKind(handle: AST.LiteralExpressionSyntax): String - fun GetReturnStatementExpression(handle: AST.StatementSyntax): AST.ExpressionSyntax? fun GetIfStatementSyntaxCondition(handle: AST.StatementSyntax): AST.ExpressionSyntax @@ -380,6 +457,12 @@ interface Csharp : Library { fun GetIdentifierNameSyntaxIdentifier(handle: AST.IdentifierNameSyntax): String + fun GetIfStatementSyntaxStatement(handle: AST.IfStatementSyntax): AST.StatementSyntax + + fun GetElseClauseSyntax(handle: AST.IfStatementSyntax): AST.ElseClauseSyntax? + + fun GetElseClauseSyntaxStatement(handle: AST.ElseClauseSyntax): AST.StatementSyntax + companion object { val INSTANCE: Csharp by lazy { try { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt index aaa8a640b21..d47c9e069d9 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt @@ -51,32 +51,51 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : } } + /** + * Translates a C# + * [`IdentifierNameSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.identifiernamesyntax?view=roslyn-dotnet-5.0.0) + * into a [Reference]. + */ private fun handleIdentifierName(node: Csharp.AST.IdentifierNameSyntax): Reference { return newReference(name = node.identifier, rawNode = node) } + /** + * Translates a C# + * [`BinaryExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.binaryexpressionsyntax?view=roslyn-dotnet-5.0.0) + * into a [BinaryOperator]. + */ private fun handleBinaryExpression(node: Csharp.AST.BinaryExpressionSyntax): BinaryOperator { - val binOp = newBinaryOperator(operatorCode = node.operatorToken, rawNode = node) - binOp.lhs = handle(node.left) - binOp.rhs = handle(node.right) - return binOp + return newBinaryOperator(operatorCode = node.operatorToken, rawNode = node).apply { + this.lhs = handle(node.left) + this.rhs = handle(node.right) + } } /** * Translates a C# * [`LiteralExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.literalexpressionsyntax?view=roslyn-dotnet-5.0.0) - * into a [Literal]. + * into a [Literal]. The concrete literal subclass is determined by the Roslyn SyntaxKind. + * + * Note: [Csharp.AST.NumericLiteralExpressionSyntax] does not distinguish between numeric types + * (e.g. int vs long). Instead, the .NET runtime type of `Token.Value` (e.g. `System.Int32` vs + * `System.Int64`) can be used, but this requires an additional mapping from .NET types to C# + * keywords. */ private fun handleLiteralExpression(node: Csharp.AST.LiteralExpressionSyntax): Expression { val builtInTypes = frontend.language.builtInTypes - return when (node.kind) { - "int" -> newLiteral(node.value.toInt(), builtInTypes.getValue("int"), rawNode = node) - "string" -> newLiteral(node.value, builtInTypes.getValue("string"), rawNode = node) - "bool" -> + return when (node) { + is Csharp.AST.NumericLiteralExpressionSyntax -> + newLiteral(node.value.toInt(), builtInTypes.getValue("int"), rawNode = node) + is Csharp.AST.StringLiteralExpressionSyntax -> + newLiteral(node.value, builtInTypes.getValue("string"), rawNode = node) + is Csharp.AST.BooleanLiteralExpressionSyntax -> newLiteral(node.value.toBoolean(), builtInTypes.getValue("bool"), rawNode = node) - "char" -> newLiteral(node.value.single(), builtInTypes.getValue("char"), rawNode = node) - "null" -> newLiteral(null, objectType("null"), rawNode = node) - else -> newProblemExpression("Unknown literal kind: ${node.kind}", rawNode = node) + is Csharp.AST.CharacterLiteralExpressionSyntax -> + newLiteral(node.value.single(), builtInTypes.getValue("char"), rawNode = node) + is Csharp.AST.NullLiteralExpressionSyntax -> + newLiteral(null, objectType("null"), rawNode = node) + else -> newProblemExpression("Unknown literal type: ${node.csharpType}", rawNode = node) } } } diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt index 50c12b75c6e..1529974576a 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt @@ -48,9 +48,16 @@ class StatementHandler(frontend: CSharpLanguageFrontend) : } } + /** + * Translates a C# + * [`IfStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.ifstatementsyntax?view=roslyn-dotnet-5.0.0) + * into an [IfElse]. + */ private fun handleIf(node: Csharp.AST.IfStatementSyntax): IfElse { return newIfElse(rawNode = node).apply { this.condition = frontend.expressionHandler.handle(node.condition) + this.thenStatement = handle(node.statement) + node.elseClause?.let { this.elseStatement = handle(it.statement) } } } diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt index d587fca99e8..4ca8380ad93 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt @@ -27,8 +27,11 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.declarations.Constructor +import de.fraunhofer.aisec.cpg.graph.expressions.BinaryOperator import de.fraunhofer.aisec.cpg.graph.expressions.Block +import de.fraunhofer.aisec.cpg.graph.expressions.IfElse import de.fraunhofer.aisec.cpg.graph.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.expressions.Reference import de.fraunhofer.aisec.cpg.graph.expressions.Return import de.fraunhofer.aisec.cpg.test.* import java.nio.file.Path @@ -266,5 +269,56 @@ class CSharpLanguageFrontendTest : BaseTest() { val bar = tu.records["Bar"] assertNotNull(bar) + + // if without else + val doIf = bar.methods["doIf"] + assertNotNull(doIf) + val doIfBody = doIf.body + assertIs(doIfBody) + + val ifStmt = doIfBody.statements[0] + assertIs(ifStmt) + + val condition = ifStmt.condition + assertIs(condition) + assertEquals("<", condition.operatorCode) + assertIs(condition.lhs) + assertEquals("a", condition.lhs.name.localName) + assertIs>(condition.rhs) + assertEquals(10, (condition.rhs as Literal<*>).value) + + val thenBlock = ifStmt.thenStatement + assertIs(thenBlock) + assertIs(thenBlock.statements.firstOrNull()) + + assertNull(ifStmt.elseStatement) + + // if-else + val doIfElse = bar.methods["doIfElse"] + assertNotNull(doIfElse) + val doIfElseBody = doIfElse.body + assertIs(doIfElseBody) + + val ifElseStmt = doIfElseBody.statements.firstOrNull() + assertIs(ifElseStmt) + assertNotNull(ifElseStmt.thenStatement) + assertNotNull(ifElseStmt.elseStatement) + assertIs(ifElseStmt.elseStatement) + + // if-else if-else + val doIfElseIf = bar.methods["doIfElseIf"] + assertNotNull(doIfElseIf) + val doIfElseIfBody = doIfElseIf.body + assertIs(doIfElseIfBody) + + val outerIf = doIfElseIfBody.statements.firstOrNull() + assertIs(outerIf) + assertNotNull(outerIf.thenStatement) + + // else if is modeled as a nested IfElse in the elseStatement + val innerIf = outerIf.elseStatement + assertIs(innerIf) + assertNotNull(innerIf.thenStatement) + assertNotNull(innerIf.elseStatement) } } diff --git a/cpg-language-csharp/src/test/resources/csharp/IfStatements.cs b/cpg-language-csharp/src/test/resources/csharp/IfStatements.cs index df5755f38c3..233a039eb5e 100644 --- a/cpg-language-csharp/src/test/resources/csharp/IfStatements.cs +++ b/cpg-language-csharp/src/test/resources/csharp/IfStatements.cs @@ -1,10 +1,39 @@ class Bar { - void doIf(int a) + int doIf(int a) { if(a < 10) { - new String("smaller than 10"); + return 1; + } + return 0; + } + + int doIfElse(int a) + { + if(a < 10) + { + return 1; + } + else + { + return 2; + } + } + + int doIfElseIf(int a) + { + if(a < 10) + { + return 1; + } + else if(a < 20) + { + return 2; + } + else + { + return 3; } } } \ No newline at end of file From 2516c32b623f928ddf3247e521a69a4364f203bc Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Thu, 26 Mar 2026 22:36:10 +0100 Subject: [PATCH 34/55] Local variables --- .../src/main/csharp/NativeParser/Library.cs | 39 +++ .../aisec/cpg/frontends/csharp/CSharp.kt | 88 +++++-- .../cpg/frontends/csharp/ExpressionHandler.kt | 31 ++- .../cpg/frontends/csharp/StatementHandler.kt | 53 +++- .../VariableDeclarationsTest.kt | 226 ++++++++++++++++++ .../src/test/resources/csharp/Variables.cs | 32 +++ 6 files changed, 426 insertions(+), 43 deletions(-) create mode 100644 cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/statementhandler/VariableDeclarationsTest.kt create mode 100644 cpg-language-csharp/src/test/resources/csharp/Variables.cs diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 0fc59a4934b..72bf0f6f583 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -311,4 +311,43 @@ public static IntPtr GetIdentifierNameSyntaxIdentifier(IntPtr handlePtr) { return Marshal.StringToCoTaskMemUTF8(((IdentifierNameSyntax)Nodes[handlePtr]).Identifier.ToString()); } + + [UnmanagedCallersOnly(EntryPoint = "GetVariableDeclarationSyntax")] + public static IntPtr GetVariableDeclarationSyntax(IntPtr handlePtr) + { + return Register(((LocalDeclarationStatementSyntax)Nodes[handlePtr]).Declaration); + } + + [UnmanagedCallersOnly(EntryPoint = "GetVariableDeclarationType")] + public static IntPtr GetVariableDeclarationType(IntPtr handlePtr) + { + return Register(((VariableDeclarationSyntax)Nodes[handlePtr]).Type); + } + + [UnmanagedCallersOnly(EntryPoint = "GetLocalVariableCount")] + public static int GetLocalVariableCount(IntPtr handlePtr) + { + return ((VariableDeclarationSyntax)Nodes[handlePtr]).Variables.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetLocalVariable")] + public static IntPtr GetLocalVariable(IntPtr handlePtr, int index) + { + return Register(((VariableDeclarationSyntax)Nodes[handlePtr]).Variables[index]); + } + + [UnmanagedCallersOnly(EntryPoint = "GetVariableDeclaratorInitializer")] + public static IntPtr GetVariableDeclaratorInitializer(IntPtr handlePtr) + { + var initializer = ((VariableDeclaratorSyntax)Nodes[handlePtr]).Initializer; + if (initializer != null) + { + return Register(initializer.Value); + } + else + { + return IntPtr.Zero; + } + } + } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 41bd7b55db1..29e7c831a29 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -50,7 +50,7 @@ interface Csharp : Library { interface AST { /** * Base class for all C# syntax nodes. Represents Roslyn's - * [`CSharpSyntaxNode`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.csharpsyntaxnode?view=roslyn-dotnet-5.0.0) + * [`CSharpSyntaxNode`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.csharpsyntaxnode) */ open class Node(p: Pointer? = Pointer.NULL) : CsharpObject(p) { val startLine: Int by lazy { INSTANCE.GetNodeStartLine(this) } @@ -61,7 +61,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`TypeSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.typesyntax?view=roslyn-dotnet-5.0.0) + * [`TypeSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.typesyntax) * class. */ class TypeSyntax(p: Pointer? = Pointer.NULL) : Node(p) { @@ -70,7 +70,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`CompilationUnitSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.compilationunitsyntax?view=roslyn-dotnet-5.0.0) + * [`CompilationUnitSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.compilationunitsyntax) * class. */ class CompilationUnitSyntax(p: Pointer? = Pointer.NULL) : Node(p) { @@ -82,7 +82,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [MemberDeclarationSyntax](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.memberdeclarationsyntax?view=roslyn-dotnet-5.0.0) + * [MemberDeclarationSyntax](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.memberdeclarationsyntax) * class. */ open class MemberDeclarationSyntax(p: Pointer? = Pointer.NULL) : Node(p) { @@ -111,7 +111,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`NamespaceDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.namespacedeclarationsyntax?view=roslyn-dotnet-5.0.0) + * [`NamespaceDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.namespacedeclarationsyntax) * class. */ class NamespaceDeclarationSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { @@ -124,7 +124,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`ClassDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.classdeclarationsyntax?view=roslyn-dotnet-5.0.0) + * [`ClassDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.classdeclarationsyntax) * class. */ class ClassDeclarationSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { @@ -137,7 +137,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`BaseMethodDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.basemethoddeclarationsyntax?view=roslyn-dotnet-5.0.0) + * [`BaseMethodDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.basemethoddeclarationsyntax) * class. */ open class BaseMethodDeclarationSyntax(p: Pointer? = Pointer.NULL) : @@ -154,7 +154,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`MethodDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.methoddeclarationsyntax?view=roslyn-dotnet-5.0.0) + * [`MethodDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.methoddeclarationsyntax) * class. */ class MethodDeclarationSyntax(p: Pointer? = Pointer.NULL) : BaseMethodDeclarationSyntax(p) { @@ -168,7 +168,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`BlockSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.blocksyntax?view=roslyn-dotnet-5.0.0) + * [`BlockSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.blocksyntax) * class. */ class BlockSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) { @@ -180,7 +180,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`StatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.statementsyntax?view=roslyn-dotnet-5.0.0) + * [`StatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.statementsyntax) * class. */ open class StatementSyntax(p: Pointer? = Pointer.NULL) : Node(p) { @@ -192,6 +192,8 @@ interface Csharp : Library { "ReturnStatementSyntax" -> ReturnStatementSyntax(nativeValue) "BlockSyntax" -> BlockSyntax(nativeValue) "IfStatementSyntax" -> IfStatementSyntax(nativeValue) + "LocalDeclarationStatementSyntax" -> + LocalDeclarationStatementSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } } @@ -199,7 +201,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`ReturnStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.returnstatementsyntax?view=roslyn-dotnet-5.0.0) + * [`ReturnStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.returnstatementsyntax) * class. */ class ReturnStatementSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) { @@ -210,7 +212,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`IfStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.ifstatementsyntax?view=roslyn-dotnet-5.0.0) + * [`IfStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.ifstatementsyntax) * class. */ class IfStatementSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) { @@ -222,7 +224,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`ElseClauseSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.elseclausesyntax?view=roslyn-dotnet-5.0.0) + * [`ElseClauseSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.elseclausesyntax) * class. Note: not a [StatementSyntax] because it cannot appear on its own. It's always * attached to an [IfStatementSyntax]. */ @@ -232,7 +234,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`ConstructorDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.constructordeclarationsyntax?view=roslyn-dotnet-5.0.0) + * [`ConstructorDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.constructordeclarationsyntax) * class. */ class ConstructorDeclarationSyntax(p: Pointer? = Pointer.NULL) : @@ -247,7 +249,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`FieldDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.fielddeclarationsyntax?view=roslyn-dotnet-5.0.0) + * [`FieldDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.fielddeclarationsyntax) * class. */ class FieldDeclarationSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { @@ -259,16 +261,43 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`VariableDeclaratorSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.variabledeclaratorsyntax?view=roslyn-dotnet-5.0.0) + * [`LocalDeclarationStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.localdeclarationstatementsyntax) + * class. + */ + class LocalDeclarationStatementSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) { + val declaration: VariableDeclarationSyntax by lazy { + INSTANCE.GetVariableDeclarationSyntax(this) + } + } + + /** + * Represents the Roslyn + * [`VariableDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.variabledeclarationsyntax) + * class. + */ + class VariableDeclarationSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val type: TypeSyntax by lazy { INSTANCE.GetVariableDeclarationType(this) } + val variables: List by lazy { + val count = INSTANCE.GetLocalVariableCount(this) + (0 until count).map { i -> INSTANCE.GetLocalVariable(this, i) } + } + } + + /** + * Represents the Roslyn + * [`VariableDeclaratorSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.variabledeclaratorsyntax) * class. */ class VariableDeclaratorSyntax(p: Pointer? = Pointer.NULL) : Node(p) { val identifier: String by lazy { INSTANCE.GetVariableDeclaratorIdentifier(this) } + val initializer: ExpressionSyntax? by lazy { + INSTANCE.GetVariableDeclaratorInitializer(this) + } } /** * Represents the Roslyn - * [`ParameterSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.parametersyntax?view=roslyn-dotnet-5.0.0) + * [`ParameterSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.parametersyntax) * class. */ class ParameterSyntax(p: Pointer? = Pointer.NULL) : Node(p) { @@ -278,7 +307,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`ExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.expressionsyntax?view=roslyn-dotnet-5.0.0) + * [`ExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.expressionsyntax) * class. */ open class ExpressionSyntax(p: Pointer? = Pointer.NULL) : Node(p) { @@ -310,7 +339,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`LiteralExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.literalexpressionsyntax?view=roslyn-dotnet-5.0.0) + * [`LiteralExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.literalexpressionsyntax) * class. */ open class LiteralExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { @@ -333,7 +362,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`BinaryExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.binaryexpressionsyntax?view=roslyn-dotnet-5.0.0) + * [`BinaryExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.binaryexpressionsyntax) * class. */ class BinaryExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { @@ -344,7 +373,7 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`IdentifierNameSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.identifiernamesyntax?view=roslyn-dotnet-5.0.0) + * [`IdentifierNameSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.identifiernamesyntax) * class. */ class IdentifierNameSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { @@ -463,6 +492,23 @@ interface Csharp : Library { fun GetElseClauseSyntaxStatement(handle: AST.ElseClauseSyntax): AST.StatementSyntax + fun GetLocalVariableCount(handle: AST.VariableDeclarationSyntax): Int + + fun GetLocalVariable( + handle: AST.VariableDeclarationSyntax, + index: Int, + ): AST.VariableDeclaratorSyntax + + fun GetVariableDeclarationSyntax( + handle: AST.LocalDeclarationStatementSyntax + ): AST.VariableDeclarationSyntax + + fun GetVariableDeclarationType(handle: AST.VariableDeclarationSyntax): AST.TypeSyntax + + fun GetVariableDeclaratorInitializer( + handle: AST.VariableDeclaratorSyntax + ): AST.ExpressionSyntax? + companion object { val INSTANCE: Csharp by lazy { try { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt index d47c9e069d9..6cab76c2787 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt @@ -52,18 +52,21 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : } /** - * Translates a C# - * [`IdentifierNameSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.identifiernamesyntax?view=roslyn-dotnet-5.0.0) - * into a [Reference]. + * Translates an [IdentifierNameSyntax][Csharp.AST.IdentifierNameSyntax] into a [Reference]. + * + * C# spec: + * [SimpleNames](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#12842-simple-names) */ private fun handleIdentifierName(node: Csharp.AST.IdentifierNameSyntax): Reference { return newReference(name = node.identifier, rawNode = node) } /** - * Translates a C# - * [`BinaryExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.binaryexpressionsyntax?view=roslyn-dotnet-5.0.0) - * into a [BinaryOperator]. + * Translates a [BinaryExpressionSyntax][Csharp.AST.BinaryExpressionSyntax] into a + * [BinaryOperator]. + * + * C# spec: + * [BinaryOperator](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#12104-binary-operators) */ private fun handleBinaryExpression(node: Csharp.AST.BinaryExpressionSyntax): BinaryOperator { return newBinaryOperator(operatorCode = node.operatorToken, rawNode = node).apply { @@ -73,14 +76,16 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : } /** - * Translates a C# - * [`LiteralExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.literalexpressionsyntax?view=roslyn-dotnet-5.0.0) - * into a [Literal]. The concrete literal subclass is determined by the Roslyn SyntaxKind. + * Translates a [LiteralExpressionSyntax][Csharp.AST.LiteralExpressionSyntax] into a [Literal]. + * The concrete literal subclass is determined by the Roslyn SyntaxKind. + * + * Note: [NumericLiteralExpressionSyntax][Csharp.AST.NumericLiteralExpressionSyntax] does not + * distinguish between numeric types (e.g. int vs long). Instead, the .NET runtime type of + * `Token.Value` (e.g. `System.Int32` vs `System.Int64`) can be used, but this requires an + * additional mapping from .NET types to C# keywords. * - * Note: [Csharp.AST.NumericLiteralExpressionSyntax] does not distinguish between numeric types - * (e.g. int vs long). Instead, the .NET runtime type of `Token.Value` (e.g. `System.Int32` vs - * `System.Int64`) can be used, but this requires an additional mapping from .NET types to C# - * keywords. + * C# spec: + * [Literals](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#645-literals) */ private fun handleLiteralExpression(node: Csharp.AST.LiteralExpressionSyntax): Expression { val builtInTypes = frontend.language.builtInTypes diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt index 1529974576a..c66f401caeb 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt @@ -25,14 +25,18 @@ */ package de.fraunhofer.aisec.cpg.frontends.csharp +import de.fraunhofer.aisec.cpg.graph.declarations.* import de.fraunhofer.aisec.cpg.graph.expressions.Block +import de.fraunhofer.aisec.cpg.graph.expressions.DeclarationStatement import de.fraunhofer.aisec.cpg.graph.expressions.Expression import de.fraunhofer.aisec.cpg.graph.expressions.IfElse import de.fraunhofer.aisec.cpg.graph.expressions.ProblemExpression import de.fraunhofer.aisec.cpg.graph.expressions.Return import de.fraunhofer.aisec.cpg.graph.newBlock +import de.fraunhofer.aisec.cpg.graph.newDeclarationStatement import de.fraunhofer.aisec.cpg.graph.newIfElse import de.fraunhofer.aisec.cpg.graph.newReturn +import de.fraunhofer.aisec.cpg.graph.newVariable class StatementHandler(frontend: CSharpLanguageFrontend) : CSharpHandler( @@ -44,14 +48,16 @@ class StatementHandler(frontend: CSharpLanguageFrontend) : is Csharp.AST.BlockSyntax -> handleBlock(node) is Csharp.AST.ReturnStatementSyntax -> handleReturn(node) is Csharp.AST.IfStatementSyntax -> handleIf(node) + is Csharp.AST.LocalDeclarationStatementSyntax -> handleLocalDeclaration(node) else -> ProblemExpression("Not supported: ${node.csharpType}") } } /** - * Translates a C# - * [`IfStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.ifstatementsyntax?view=roslyn-dotnet-5.0.0) - * into an [IfElse]. + * Translates an [IfStatementSyntax][Csharp.AST.IfStatementSyntax] into an [IfElse]. + * + * C# spec: + * [IfStatement](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#1382-the-if-statement) */ private fun handleIf(node: Csharp.AST.IfStatementSyntax): IfElse { return newIfElse(rawNode = node).apply { @@ -62,9 +68,11 @@ class StatementHandler(frontend: CSharpLanguageFrontend) : } /** - * Translates a C# - * [`ReturnStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.returnstatementsyntax?view=roslyn-dotnet-5.0.0) - * into a [Return]. The return value expression is optional (e.g. `return;` in void methods). + * Translates a [ReturnStatementSyntax][Csharp.AST.ReturnStatementSyntax] into a [Return]. The + * return value expression is optional (e.g. `return;` in void methods). + * + * C# spec: + * [ReturnStatement](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#13105-the-return-statement) */ private fun handleReturn(node: Csharp.AST.ReturnStatementSyntax): Return { val ret = newReturn(rawNode = node) @@ -73,9 +81,36 @@ class StatementHandler(frontend: CSharpLanguageFrontend) : } /** - * Translates a C# - * [`BlockSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.blocksyntax?view=roslyn-dotnet-5.0.0) - * into a [Block]. + * Translates a [LocalDeclarationStatementSyntax][Csharp.AST.LocalDeclarationStatementSyntax] + * into a [DeclarationStatement]. Each + * [VariableDeclaratorSyntax][Csharp.AST.VariableDeclaratorSyntax] is translated into a + * [Variable] and added to the current scope. + * + * C# spec: + * [LocalVariableDeclaration](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#1364-local-variable-declarations) + */ + private fun handleLocalDeclaration( + node: Csharp.AST.LocalDeclarationStatementSyntax + ): DeclarationStatement { + val declStmt = newDeclarationStatement(rawNode = node) + val declaration = node.declaration + val type = frontend.typeOf(declaration.type) + + for (variable in declaration.variables) { + val v = newVariable(name = variable.identifier, type = type, rawNode = variable) + variable.initializer?.let { v.initializer = frontend.expressionHandler.handle(it) } + frontend.scopeManager.addDeclaration(v) + declStmt.declarations += v + } + + return declStmt + } + + /** + * Translates a [BlockSyntax][Csharp.AST.BlockSyntax] into a [Block]. + * + * C# spec: + * [Block](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#133-blocks) */ private fun handleBlock(node: Csharp.AST.BlockSyntax): Block { val block = newBlock(rawNode = node) diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/statementhandler/VariableDeclarationsTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/statementhandler/VariableDeclarationsTest.kt new file mode 100644 index 00000000000..938058fdf42 --- /dev/null +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/statementhandler/VariableDeclarationsTest.kt @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 de.fraunhofer.aisec.cpg.frontends.csharp.statementhandler + +import de.fraunhofer.aisec.cpg.frontends.csharp.CSharpLanguage +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.declarations.Variable +import de.fraunhofer.aisec.cpg.graph.expressions.BinaryOperator +import de.fraunhofer.aisec.cpg.graph.expressions.Block +import de.fraunhofer.aisec.cpg.graph.expressions.DeclarationStatement +import de.fraunhofer.aisec.cpg.graph.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.types.IntegerType +import de.fraunhofer.aisec.cpg.graph.types.StringType +import de.fraunhofer.aisec.cpg.test.* +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class VariableDeclarationsTest : BaseTest() { + + @Test + fun testExplicitlyAndImplicitlyTypedVariables() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("Variables.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val foo = tu.records["Foo"] + assertNotNull(foo) + + val bar = foo.methods["bar"] + assertNotNull(bar) + val barBody = bar.body + assertIs(barBody) + + // int a = 1; + val intDecl = barBody.statements[0] + assertIs(intDecl) + val a = intDecl.singleDeclaration + assertIs(a) + assertEquals("a", a.name.localName) + assertEquals("int", a.type.name.localName) + val aInit = a.initializer + assertIs>(aInit) + assertEquals(1, aInit.value) + + // string b = "2"; + val stringDecl = barBody.statements[1] + assertIs(stringDecl) + val b = stringDecl.singleDeclaration + assertIs(b) + assertEquals("b", b.name.localName) + assertEquals("string", b.type.name.localName) + val bInit = b.initializer + assertIs>(bInit) + assertEquals("2", bInit.value) + + // var c = 5; + val varIntDecl = barBody.statements[2] + assertIs(varIntDecl) + val c = varIntDecl.singleDeclaration + assertIs(c) + assertEquals("c", c.name.localName) + assertEquals("var", c.type.name.localName) + assertIs(c.assignedTypes.elementAt(1)) + + // var d = "Hello"; + val varStringDecl = barBody.statements[3] + assertIs(varStringDecl) + val d = varStringDecl.singleDeclaration + assertIs(d) + assertEquals("d", d.name.localName) + assertEquals("var", d.type.name.localName) + assertIs(d.assignedTypes.elementAt(1)) + } + + @Test + fun testMultipleDeclarators() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("Variables.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val foo = tu.records["Foo"] + assertNotNull(foo) + + // int a = 1, b = 2, c = 3; + val multipleDeclarations = foo.methods["multipleDeclarations"] + assertNotNull(multipleDeclarations) + val multiBody = multipleDeclarations.body + assertIs(multiBody) + assertEquals(1, multiBody.statements.size) + + val multiStmt = multiBody.statements[0] + assertIs(multiStmt) + assertEquals(3, multiStmt.declarations.size) + + val a = multiStmt.declarations[0] + assertIs(a) + assertEquals("a", a.name.localName) + assertIs(a.type) + + val b = multiStmt.declarations[1] + assertIs(b) + assertEquals("b", b.name.localName) + assertIs(a.type) + + val c = multiStmt.declarations[2] + assertIs(c) + assertEquals("c", c.name.localName) + assertIs(a.type) + } + + @Test + fun testWithoutInitializer() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("Variables.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val foo = tu.records["Foo"] + assertNotNull(foo) + + val withoutInit = foo.methods["withoutInitializer"] + assertNotNull(withoutInit) + val body = withoutInit.body + assertIs(body) + assertEquals(2, body.statements.size) + + // int a; + val intDecl = body.statements[0] + assertIs(intDecl) + val a = intDecl.singleDeclaration + assertIs(a) + assertEquals("a", a.name.localName) + assertIs(a.type) + assertNull(a.initializer) + + // string b; + val stringDecl = body.statements[1] + assertIs(stringDecl) + val b = stringDecl.singleDeclaration + assertIs(b) + assertEquals("b", b.name.localName) + assertIs(b.type) + assertNull(b.initializer) + } + + @Test + fun testExpressionInitializer() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("Variables.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val foo = tu.records["Foo"] + assertNotNull(foo) + + val withInit = foo.methods["withInitializer"] + assertNotNull(withInit) + val body = withInit.body + assertIs(body) + assertEquals(2, body.statements.size) + + // int b = a + 2; + val exprStmt = body.statements[1] + assertIs(exprStmt) + val b = exprStmt.singleDeclaration + assertIs(b) + assertEquals("b", b.name.localName) + val bInit = b.initializer + assertIs(bInit) + assertEquals("+", bInit.operatorCode) + assertIs(b.type) + } +} diff --git a/cpg-language-csharp/src/test/resources/csharp/Variables.cs b/cpg-language-csharp/src/test/resources/csharp/Variables.cs new file mode 100644 index 00000000000..5bfa25a5703 --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/Variables.cs @@ -0,0 +1,32 @@ +class Foo +{ + void bar() + { + // explicitly typed + int a = 1; + string b = "2"; + // implicitly typed + var c = 5; + var d = "Hello"; + } + + void multipleDeclarations() + { + // multiple variables in one statement + int a = 1, b = 2, c = 3; + } + + void withoutInitializer() + { + // declaration without initializer + int a; + string b; + } + + void withInitializer() + { + int a = 1; + int b = a + 2; + } + +} \ No newline at end of file From 18cc0ea00fcc4cdb098dfa55ac6546b2575e5081 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Fri, 27 Mar 2026 08:54:30 +0100 Subject: [PATCH 35/55] Assign expressions --- .../src/main/csharp/NativeParser/Library.cs | 23 ++++ .../aisec/cpg/frontends/csharp/CSharp.kt | 34 ++++++ .../cpg/frontends/csharp/CSharpLanguage.kt | 7 +- .../cpg/frontends/csharp/ExpressionHandler.kt | 19 +++ .../cpg/frontends/csharp/StatementHandler.kt | 12 ++ .../expressionhandler/AssignmentsTest.kt | 113 ++++++++++++++++++ .../src/test/resources/csharp/Assignments.cs | 14 +++ 7 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/AssignmentsTest.kt create mode 100644 cpg-language-csharp/src/test/resources/csharp/Assignments.cs diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 72bf0f6f583..65c73dc636f 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -350,4 +350,27 @@ public static IntPtr GetVariableDeclaratorInitializer(IntPtr handlePtr) } } + [UnmanagedCallersOnly(EntryPoint = "GetExpressionStatementExpression")] + public static IntPtr GetExpressionStatementExpression(IntPtr handlePtr) + { + return Register(((ExpressionStatementSyntax)Nodes[handlePtr]).Expression); + } + + [UnmanagedCallersOnly(EntryPoint = "GetAssignmentExpressionLeft")] + public static IntPtr GetAssignmentExpressionLeft(IntPtr handlePtr) + { + return Register(((AssignmentExpressionSyntax)Nodes[handlePtr]).Left); + } + + [UnmanagedCallersOnly(EntryPoint = "GetAssignmentExpressionRight")] + public static IntPtr GetAssignmentExpressionRight(IntPtr handlePtr) + { + return Register(((AssignmentExpressionSyntax)Nodes[handlePtr]).Right); + } + + [UnmanagedCallersOnly(EntryPoint = "GetAssignmentExpressionOperator")] + public static IntPtr GetAssignmentExpressionOperator(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8(((AssignmentExpressionSyntax)Nodes[handlePtr]).OperatorToken.Text); + } } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 29e7c831a29..3f119414a50 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -194,6 +194,7 @@ interface Csharp : Library { "IfStatementSyntax" -> IfStatementSyntax(nativeValue) "LocalDeclarationStatementSyntax" -> LocalDeclarationStatementSyntax(nativeValue) + "ExpressionStatementSyntax" -> ExpressionStatementSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } } @@ -270,6 +271,17 @@ interface Csharp : Library { } } + /** + * Represents the Roslyn + * [`ExpressionStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.expressionstatementsyntax) + * class. + */ + class ExpressionStatementSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) { + val expression: ExpressionSyntax by lazy { + INSTANCE.GetExpressionStatementExpression(this) + } + } + /** * Represents the Roslyn * [`VariableDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.variabledeclarationsyntax) @@ -332,6 +344,7 @@ interface Csharp : Library { } "BinaryExpressionSyntax" -> BinaryExpressionSyntax(nativeValue) "IdentifierNameSyntax" -> IdentifierNameSyntax(nativeValue) + "AssignmentExpressionSyntax" -> AssignmentExpressionSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } } @@ -371,6 +384,17 @@ interface Csharp : Library { val right: ExpressionSyntax by lazy { INSTANCE.GetBinaryExpressionRight(this) } } + /** + * Represents the Roslyn + * [`AssignmentExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.assignmentexpressionsyntax) + * class. + */ + class AssignmentExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { + val left: ExpressionSyntax by lazy { INSTANCE.GetAssignmentExpressionLeft(this) } + val operatorToken: String by lazy { INSTANCE.GetAssignmentExpressionOperator(this) } + val right: ExpressionSyntax by lazy { INSTANCE.GetAssignmentExpressionRight(this) } + } + /** * Represents the Roslyn * [`IdentifierNameSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.identifiernamesyntax) @@ -509,6 +533,16 @@ interface Csharp : Library { handle: AST.VariableDeclaratorSyntax ): AST.ExpressionSyntax? + fun GetExpressionStatementExpression( + handle: AST.ExpressionStatementSyntax + ): AST.ExpressionSyntax + + fun GetAssignmentExpressionLeft(handle: AST.AssignmentExpressionSyntax): AST.ExpressionSyntax + + fun GetAssignmentExpressionRight(handle: AST.AssignmentExpressionSyntax): AST.ExpressionSyntax + + fun GetAssignmentExpressionOperator(handle: AST.AssignmentExpressionSyntax): String + companion object { val INSTANCE: Csharp by lazy { try { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt index a8ad6eccc74..3037627dd75 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt @@ -43,7 +43,12 @@ class CSharpLanguage : Language() { @Transient override val frontend: KClass = CSharpLanguageFrontend::class - override val compoundAssignmentOperators = setOf() + /** + * See + * [Documentation](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#12214-assignment-operators). + */ + override val compoundAssignmentOperators = + setOf("+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "&=", "|=", "^=", "??=") /** * See diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt index 6cab76c2787..895ec50d6a0 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt @@ -25,11 +25,13 @@ */ package de.fraunhofer.aisec.cpg.frontends.csharp +import de.fraunhofer.aisec.cpg.graph.expressions.Assign import de.fraunhofer.aisec.cpg.graph.expressions.BinaryOperator import de.fraunhofer.aisec.cpg.graph.expressions.Expression import de.fraunhofer.aisec.cpg.graph.expressions.Literal import de.fraunhofer.aisec.cpg.graph.expressions.ProblemExpression import de.fraunhofer.aisec.cpg.graph.expressions.Reference +import de.fraunhofer.aisec.cpg.graph.newAssign import de.fraunhofer.aisec.cpg.graph.newBinaryOperator import de.fraunhofer.aisec.cpg.graph.newLiteral import de.fraunhofer.aisec.cpg.graph.newProblemExpression @@ -43,6 +45,7 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : is Csharp.AST.IdentifierNameSyntax -> handleIdentifierName(node) is Csharp.AST.LiteralExpressionSyntax -> handleLiteralExpression(node) is Csharp.AST.BinaryExpressionSyntax -> handleBinaryExpression(node) + is Csharp.AST.AssignmentExpressionSyntax -> handleAssignmentExpression(node) else -> newProblemExpression( "The expression of class ${node.javaClass} is not supported yet", @@ -75,6 +78,22 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : } } + /** + * Translates an [AssignmentExpressionSyntax][Csharp.AST.AssignmentExpressionSyntax] into an + * [Assign]. + * + * C# spec: + * [AssignmentOperators](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#12214-assignment-operators) + */ + private fun handleAssignmentExpression(node: Csharp.AST.AssignmentExpressionSyntax): Assign { + return newAssign( + operatorCode = node.operatorToken, + lhs = listOf(handle(node.left)), + rhs = listOf(handle(node.right)), + rawNode = node, + ) + } + /** * Translates a [LiteralExpressionSyntax][Csharp.AST.LiteralExpressionSyntax] into a [Literal]. * The concrete literal subclass is determined by the Roslyn SyntaxKind. diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt index c66f401caeb..4f45bb8d234 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt @@ -49,6 +49,7 @@ class StatementHandler(frontend: CSharpLanguageFrontend) : is Csharp.AST.ReturnStatementSyntax -> handleReturn(node) is Csharp.AST.IfStatementSyntax -> handleIf(node) is Csharp.AST.LocalDeclarationStatementSyntax -> handleLocalDeclaration(node) + is Csharp.AST.ExpressionStatementSyntax -> handleExpressionStatement(node) else -> ProblemExpression("Not supported: ${node.csharpType}") } } @@ -106,6 +107,17 @@ class StatementHandler(frontend: CSharpLanguageFrontend) : return declStmt } + /** + * Translates an [ExpressionStatementSyntax][Csharp.AST.ExpressionStatementSyntax] into an + * [Expression]. + * + * C# spec: + * [ExpressionStatement](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#1372-expression-statements) + */ + private fun handleExpressionStatement(node: Csharp.AST.ExpressionStatementSyntax): Expression { + return frontend.expressionHandler.handle(node.expression) + } + /** * Translates a [BlockSyntax][Csharp.AST.BlockSyntax] into a [Block]. * diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/AssignmentsTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/AssignmentsTest.kt new file mode 100644 index 00000000000..ccd52433bbe --- /dev/null +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/AssignmentsTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 de.fraunhofer.aisec.cpg.frontends.csharp.expressionhandler + +import de.fraunhofer.aisec.cpg.frontends.csharp.CSharpLanguage +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.expressions.Assign +import de.fraunhofer.aisec.cpg.graph.expressions.Block +import de.fraunhofer.aisec.cpg.graph.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.expressions.Reference +import de.fraunhofer.aisec.cpg.test.* +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +class AssignmentsTest : BaseTest() { + + @Test + fun testSimpleAssignment() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("Assignments.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val foo = tu.records["Foo"] + assertNotNull(foo) + + val method = foo.methods["simpleAssignment"] + assertNotNull(method) + val body = method.body + assertIs(body) + + // a = 5; + val assign = body.statements.getOrNull(1) + assertIs(assign) + assertEquals("=", assign.operatorCode) + + val lhs = assign.lhs.singleOrNull() + assertIs(lhs) + assertEquals("a", lhs.name.localName) + + val rhs = assign.rhs.singleOrNull() + assertIs>(rhs) + assertEquals(5, rhs.value) + } + + @Test + fun testCompoundAssignment() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("Assignments.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val foo = tu.records["Foo"] + assertNotNull(foo) + + val method = foo.methods["compoundAssignment"] + assertNotNull(method) + val body = method.body + assertIs(body) + + // a += 5; + val assign = body.statements.getOrNull(1) + assertIs(assign) + assertEquals("+=", assign.operatorCode) + assertEquals(true, assign.isCompoundAssignment) + + val lhs = assign.lhs.singleOrNull() + assertIs(lhs) + assertEquals("a", lhs.name.localName) + + val rhs = assign.rhs.singleOrNull() + assertIs>(rhs) + assertEquals(5, rhs.value) + } +} diff --git a/cpg-language-csharp/src/test/resources/csharp/Assignments.cs b/cpg-language-csharp/src/test/resources/csharp/Assignments.cs new file mode 100644 index 00000000000..9d4d3eabdae --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/Assignments.cs @@ -0,0 +1,14 @@ +class Foo +{ + void simpleAssignment() + { + int a = 0; + a = 5; + } + + void compoundAssignment() + { + int a = 10; + a += 5; + } +} \ No newline at end of file From e85acaa924e354fe923b65b85e8e7fd5747afd86 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Fri, 27 Mar 2026 16:36:08 +0100 Subject: [PATCH 36/55] For loop --- .../src/main/csharp/NativeParser/Library.cs | 98 ++++++++ .../aisec/cpg/frontends/csharp/CSharp.kt | 118 ++++++++- .../csharp/CSharpLanguageFrontend.kt | 2 + .../cpg/frontends/csharp/StatementHandler.kt | 115 +++++++++ .../csharp/statementhandler/LoopsTest.kt | 226 ++++++++++++++++++ .../src/test/resources/csharp/Loops.cs | 47 ++++ 6 files changed, 605 insertions(+), 1 deletion(-) create mode 100644 cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/statementhandler/LoopsTest.kt create mode 100644 cpg-language-csharp/src/test/resources/csharp/Loops.cs diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 65c73dc636f..f1a2ffcb842 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -217,6 +217,12 @@ public static IntPtr GetTypeName(IntPtr handlePtr) ); } + [UnmanagedCallersOnly(EntryPoint = "GetArrayTypeElementType")] + public static IntPtr GetArrayTypeElementType(IntPtr handlePtr) + { + return Register(((ArrayTypeSyntax)Nodes[handlePtr]).ElementType); + } + // We use `BaseMethodDeclarationSyntax` to get the body for Methods and Constructors. [UnmanagedCallersOnly(EntryPoint = "GetBaseMethodDeclarationBody")] public static IntPtr GetBaseMethodDeclarationBody(IntPtr handlePtr) @@ -373,4 +379,96 @@ public static IntPtr GetAssignmentExpressionOperator(IntPtr handlePtr) { return Marshal.StringToCoTaskMemUTF8(((AssignmentExpressionSyntax)Nodes[handlePtr]).OperatorToken.Text); } + + [UnmanagedCallersOnly(EntryPoint = "GetWhileStatementCondition")] + public static IntPtr GetWhileStatementCondition(IntPtr handlePtr) + { + return Register(((WhileStatementSyntax)Nodes[handlePtr]).Condition); + } + + [UnmanagedCallersOnly(EntryPoint = "GetWhileStatementStatement")] + public static IntPtr GetWhileStatementStatement(IntPtr handlePtr) + { + return Register(((WhileStatementSyntax)Nodes[handlePtr]).Statement); + } + + [UnmanagedCallersOnly(EntryPoint = "GetDoStatementCondition")] + public static IntPtr GetDoStatementCondition(IntPtr handlePtr) + { + return Register(((DoStatementSyntax)Nodes[handlePtr]).Condition); + } + + [UnmanagedCallersOnly(EntryPoint = "GetDoStatementStatement")] + public static IntPtr GetDoStatementStatement(IntPtr handlePtr) + { + return Register(((DoStatementSyntax)Nodes[handlePtr]).Statement); + } + + [UnmanagedCallersOnly(EntryPoint = "GetForStatementDeclaration")] + public static IntPtr GetForStatementDeclaration(IntPtr handlePtr) + { + var declaration = ((ForStatementSyntax)Nodes[handlePtr]).Declaration; + return declaration != null ? Register(declaration) : IntPtr.Zero; + } + + [UnmanagedCallersOnly(EntryPoint = "GetForStatementInitializerExpressionCount")] + public static int GetForStatementInitializerExpressionCount(IntPtr handlePtr) + { + return ((ForStatementSyntax)Nodes[handlePtr]).Initializers.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetForStatementInitializerExpression")] + public static IntPtr GetForStatementInitializerExpression(IntPtr handlePtr, int index) + { + return Register(((ForStatementSyntax)Nodes[handlePtr]).Initializers[index]); + } + + [UnmanagedCallersOnly(EntryPoint = "GetForStatementCondition")] + public static IntPtr GetForStatementCondition(IntPtr handlePtr) + { + var condition = ((ForStatementSyntax)Nodes[handlePtr]).Condition; + return condition != null ? Register(condition) : IntPtr.Zero; + } + + [UnmanagedCallersOnly(EntryPoint = "GetForStatementIncrementorCount")] + public static int GetForStatementIncrementorCount(IntPtr handlePtr) + { + return ((ForStatementSyntax)Nodes[handlePtr]).Incrementors.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetForStatementIncrementor")] + public static IntPtr GetForStatementIncrementor(IntPtr handlePtr, int index) + { + return Register(((ForStatementSyntax)Nodes[handlePtr]).Incrementors[index]); + } + + [UnmanagedCallersOnly(EntryPoint = "GetForStatementStatement")] + public static IntPtr GetForStatementStatement(IntPtr handlePtr) + { + return Register(((ForStatementSyntax)Nodes[handlePtr]).Statement); + } + + [UnmanagedCallersOnly(EntryPoint = "GetForEachStatementIdentifier")] + public static IntPtr GetForEachStatementIdentifier(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8(((ForEachStatementSyntax)Nodes[handlePtr]).Identifier.ToString()); + } + + [UnmanagedCallersOnly(EntryPoint = "GetForEachStatementType")] + public static IntPtr GetForEachStatementType(IntPtr handlePtr) + { + return Register(((ForEachStatementSyntax)Nodes[handlePtr]).Type); + } + + [UnmanagedCallersOnly(EntryPoint = "GetForEachStatementExpression")] + public static IntPtr GetForEachStatementExpression(IntPtr handlePtr) + { + return Register(((ForEachStatementSyntax)Nodes[handlePtr]).Expression); + } + + [UnmanagedCallersOnly(EntryPoint = "GetForEachStatementStatement")] + public static IntPtr GetForEachStatementStatement(IntPtr handlePtr) + { + return Register(((ForEachStatementSyntax)Nodes[handlePtr]).Statement); + } } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 3f119414a50..8904176fd48 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -64,8 +64,27 @@ interface Csharp : Library { * [`TypeSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.typesyntax) * class. */ - class TypeSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + open class TypeSyntax(p: Pointer? = Pointer.NULL) : Node(p) { val name: String by lazy { INSTANCE.GetTypeName(this) } + + override fun fromNative(nativeValue: Any?, context: FromNativeContext?): Any { + if (nativeValue !is Pointer) { + return super.fromNative(nativeValue, context) + } + return when (INSTANCE.GetType(nativeValue)) { + "ArrayTypeSyntax" -> ArrayTypeSyntax(nativeValue) + else -> TypeSyntax(nativeValue) + } + } + } + + /** + * Represents the Roslyn + * [`ArrayTypeSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.arraytypesyntax) + * class. + */ + class ArrayTypeSyntax(p: Pointer? = Pointer.NULL) : TypeSyntax(p) { + val elementType: TypeSyntax by lazy { INSTANCE.GetArrayTypeElementType(this) } } /** @@ -195,6 +214,10 @@ interface Csharp : Library { "LocalDeclarationStatementSyntax" -> LocalDeclarationStatementSyntax(nativeValue) "ExpressionStatementSyntax" -> ExpressionStatementSyntax(nativeValue) + "WhileStatementSyntax" -> WhileStatementSyntax(nativeValue) + "DoStatementSyntax" -> DoStatementSyntax(nativeValue) + "ForStatementSyntax" -> ForStatementSyntax(nativeValue) + "ForEachStatementSyntax" -> ForEachStatementSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } } @@ -233,6 +256,64 @@ interface Csharp : Library { val statement: StatementSyntax by lazy { INSTANCE.GetElseClauseSyntaxStatement(this) } } + /** + * Represents the Roslyn + * [`WhileStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.whilestatementsyntax) + * class. + */ + class WhileStatementSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) { + val condition: ExpressionSyntax by lazy { INSTANCE.GetWhileStatementCondition(this) } + val statement: StatementSyntax by lazy { INSTANCE.GetWhileStatementStatement(this) } + } + + /** + * Represents the Roslyn + * [`DoStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.dostatementsyntax) + * class. + */ + class DoStatementSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) { + val condition: ExpressionSyntax by lazy { INSTANCE.GetDoStatementCondition(this) } + val statement: StatementSyntax by lazy { INSTANCE.GetDoStatementStatement(this) } + } + + /** + * Represents the Roslyn + * [`ForStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.forstatementsyntax) + * class. + */ + class ForStatementSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) { + /** Variable declaration in the initializer (e.g. `int i = 0`) */ + val declaration: VariableDeclarationSyntax? by lazy { + INSTANCE.GetForStatementDeclaration(this) + } + /** Expression initializers (e.g. `i = 0`), used when there is no declaration */ + val initializerExpressions: List by lazy { + val count = INSTANCE.GetForStatementInitializerExpressionCount(this) + (0 until count).map { i -> INSTANCE.GetForStatementInitializerExpression(this, i) } + } + val condition: ExpressionSyntax? by lazy { INSTANCE.GetForStatementCondition(this) } + /** The incrementors (e.g. `i++, j--`) */ + val incrementors: List by lazy { + val count = INSTANCE.GetForStatementIncrementorCount(this) + (0 until count).map { i -> INSTANCE.GetForStatementIncrementor(this, i) } + } + val statement: StatementSyntax by lazy { INSTANCE.GetForStatementStatement(this) } + } + + /** + * Represents the Roslyn + * [`ForEachStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.foreachstatementsyntax) + * class. + */ + class ForEachStatementSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) { + val identifier: String by lazy { INSTANCE.GetForEachStatementIdentifier(this) } + val type: TypeSyntax by lazy { INSTANCE.GetForEachStatementType(this) } + val expression: ExpressionSyntax by lazy { + INSTANCE.GetForEachStatementExpression(this) + } + val statement: StatementSyntax by lazy { INSTANCE.GetForEachStatementStatement(this) } + } + /** * Represents the Roslyn * [`ConstructorDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.constructordeclarationsyntax) @@ -471,6 +552,8 @@ interface Csharp : Library { fun GetTypeName(handle: AST.TypeSyntax): String + fun GetArrayTypeElementType(handle: AST.ArrayTypeSyntax): AST.TypeSyntax + fun GetConstructorDeclarationIdentifier(handle: AST.ConstructorDeclarationSyntax): String fun GetConstructorDeclarationParameterCount(handle: AST.ConstructorDeclarationSyntax): Int @@ -543,6 +626,39 @@ interface Csharp : Library { fun GetAssignmentExpressionOperator(handle: AST.AssignmentExpressionSyntax): String + fun GetWhileStatementCondition(handle: AST.WhileStatementSyntax): AST.ExpressionSyntax + + fun GetWhileStatementStatement(handle: AST.WhileStatementSyntax): AST.StatementSyntax + + fun GetDoStatementCondition(handle: AST.DoStatementSyntax): AST.ExpressionSyntax + + fun GetDoStatementStatement(handle: AST.DoStatementSyntax): AST.StatementSyntax + + fun GetForStatementDeclaration(handle: AST.ForStatementSyntax): AST.VariableDeclarationSyntax? + + fun GetForStatementInitializerExpressionCount(handle: AST.ForStatementSyntax): Int + + fun GetForStatementInitializerExpression( + handle: AST.ForStatementSyntax, + index: Int, + ): AST.ExpressionSyntax + + fun GetForStatementCondition(handle: AST.ForStatementSyntax): AST.ExpressionSyntax? + + fun GetForStatementIncrementorCount(handle: AST.ForStatementSyntax): Int + + fun GetForStatementIncrementor(handle: AST.ForStatementSyntax, index: Int): AST.ExpressionSyntax + + fun GetForStatementStatement(handle: AST.ForStatementSyntax): AST.StatementSyntax + + fun GetForEachStatementIdentifier(handle: AST.ForEachStatementSyntax): String + + fun GetForEachStatementType(handle: AST.ForEachStatementSyntax): AST.TypeSyntax + + fun GetForEachStatementExpression(handle: AST.ForEachStatementSyntax): AST.ExpressionSyntax + + fun GetForEachStatementStatement(handle: AST.ForEachStatementSyntax): AST.StatementSyntax + companion object { val INSTANCE: Csharp by lazy { try { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt index 85bc5f54262..e922d10eb49 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontend.kt @@ -29,6 +29,7 @@ import de.fraunhofer.aisec.cpg.TranslationContext import de.fraunhofer.aisec.cpg.frontends.Language import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.graph.array import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnit import de.fraunhofer.aisec.cpg.graph.newTranslationUnit import de.fraunhofer.aisec.cpg.graph.objectType @@ -69,6 +70,7 @@ class CSharpLanguageFrontend(ctx: TranslationContext, language: Language typeOf(type.elementType).array() is Csharp.AST.TypeSyntax -> objectType(type.name) else -> unknownType() } diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt index 4f45bb8d234..957ca76cc15 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt @@ -28,15 +28,24 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.graph.declarations.* import de.fraunhofer.aisec.cpg.graph.expressions.Block import de.fraunhofer.aisec.cpg.graph.expressions.DeclarationStatement +import de.fraunhofer.aisec.cpg.graph.expressions.DoWhile import de.fraunhofer.aisec.cpg.graph.expressions.Expression +import de.fraunhofer.aisec.cpg.graph.expressions.For +import de.fraunhofer.aisec.cpg.graph.expressions.ForEach import de.fraunhofer.aisec.cpg.graph.expressions.IfElse import de.fraunhofer.aisec.cpg.graph.expressions.ProblemExpression import de.fraunhofer.aisec.cpg.graph.expressions.Return +import de.fraunhofer.aisec.cpg.graph.expressions.While import de.fraunhofer.aisec.cpg.graph.newBlock import de.fraunhofer.aisec.cpg.graph.newDeclarationStatement +import de.fraunhofer.aisec.cpg.graph.newDoWhile +import de.fraunhofer.aisec.cpg.graph.newExpressionList +import de.fraunhofer.aisec.cpg.graph.newFor +import de.fraunhofer.aisec.cpg.graph.newForEach import de.fraunhofer.aisec.cpg.graph.newIfElse import de.fraunhofer.aisec.cpg.graph.newReturn import de.fraunhofer.aisec.cpg.graph.newVariable +import de.fraunhofer.aisec.cpg.graph.newWhile class StatementHandler(frontend: CSharpLanguageFrontend) : CSharpHandler( @@ -50,6 +59,10 @@ class StatementHandler(frontend: CSharpLanguageFrontend) : is Csharp.AST.IfStatementSyntax -> handleIf(node) is Csharp.AST.LocalDeclarationStatementSyntax -> handleLocalDeclaration(node) is Csharp.AST.ExpressionStatementSyntax -> handleExpressionStatement(node) + is Csharp.AST.WhileStatementSyntax -> handleWhile(node) + is Csharp.AST.DoStatementSyntax -> handleDoWhile(node) + is Csharp.AST.ForStatementSyntax -> handleFor(node) + is Csharp.AST.ForEachStatementSyntax -> handleForEach(node) else -> ProblemExpression("Not supported: ${node.csharpType}") } } @@ -118,6 +131,108 @@ class StatementHandler(frontend: CSharpLanguageFrontend) : return frontend.expressionHandler.handle(node.expression) } + /** + * Translates a [WhileStatementSyntax][Csharp.AST.WhileStatementSyntax] into a [While]. + * + * C# spec: + * [WhileStatement](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#1392-the-while-statement) + */ + private fun handleWhile(node: Csharp.AST.WhileStatementSyntax): While { + val whileStmt = newWhile(rawNode = node) + whileStmt.condition = frontend.expressionHandler.handle(node.condition) + whileStmt.statement = handle(node.statement) + return whileStmt + } + + /** + * Translates a [DoStatementSyntax][Csharp.AST.DoStatementSyntax] into a [DoWhile]. + * + * C# spec: + * [DoStatement](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#1393-the-do-statement) + */ + private fun handleDoWhile(node: Csharp.AST.DoStatementSyntax): DoWhile { + val doStmt = newDoWhile(rawNode = node) + doStmt.condition = frontend.expressionHandler.handle(node.condition) + doStmt.statement = handle(node.statement) + return doStmt + } + + /** + * Translates a [ForStatementSyntax][Csharp.AST.ForStatementSyntax] into a [For]. + * + * C# spec: + * [ForStatement](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#1394-the-for-statement) + */ + private fun handleFor(node: Csharp.AST.ForStatementSyntax): For { + val forStmt = newFor(rawNode = node) + frontend.scopeManager.enterScope(forStmt) + + // The initializer can be either a variable declaration (`for (int i = 0; ...)`) + // or one/more expressions (`for (i = 0, j = 0; ...)`), so we check for the declaration + // first and fall back to the expression form. + val declaration = node.declaration + if (declaration != null) { + val declStmt = newDeclarationStatement(rawNode = declaration) + val type = frontend.typeOf(declaration.type) + for (variable in declaration.variables) { + val v = newVariable(name = variable.identifier, type = type, rawNode = variable) + variable.initializer?.let { v.initializer = frontend.expressionHandler.handle(it) } + frontend.scopeManager.addDeclaration(v) + declStmt.declarations += v + } + forStmt.initializerStatement = declStmt + } else if (node.initializerExpressions.size == 1) { + // Single expression initializer (e.g. `for (i = 0; ...)`) + forStmt.initializerStatement = + frontend.expressionHandler.handle(node.initializerExpressions[0]) + } else if (node.initializerExpressions.size > 1) { + // Multiple expression initializers (e.g. `for (i = 0, j = 0; ...)`) + val list = newExpressionList() + for (expr in node.initializerExpressions) { + list.expressions += frontend.expressionHandler.handle(expr) + } + forStmt.initializerStatement = list + } + + // Condition is optional in C#. + node.condition?.let { forStmt.condition = frontend.expressionHandler.handle(it) } + + // It can be multiple Incrementors (e.g. 'i++, j--'). + if (node.incrementors.size == 1) { + forStmt.iterationStatement = frontend.expressionHandler.handle(node.incrementors[0]) + } else if (node.incrementors.size > 1) { + val list = newExpressionList() + for (incr in node.incrementors) { + list.expressions += frontend.expressionHandler.handle(incr) + } + forStmt.iterationStatement = list + } + forStmt.statement = handle(node.statement) + frontend.scopeManager.leaveScope(forStmt) + return forStmt + } + + /** + * Translates a [ForEachStatementSyntax][Csharp.AST.ForEachStatementSyntax] into a [ForEach]. + * + * C# spec: + * [ForeachStatement](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#1395-the-foreach-statement) + */ + private fun handleForEach(node: Csharp.AST.ForEachStatementSyntax): ForEach { + val forEachStmt = newForEach(rawNode = node) + frontend.scopeManager.enterScope(forEachStmt) + val type = frontend.typeOf(node.type) + val variable = newVariable(name = node.identifier, type = type, rawNode = node) + frontend.scopeManager.addDeclaration(variable) + val declStmt = newDeclarationStatement(rawNode = node) + declStmt.declarations += variable + forEachStmt.variable = declStmt + forEachStmt.iterable = frontend.expressionHandler.handle(node.expression) + forEachStmt.statement = handle(node.statement) + frontend.scopeManager.leaveScope(forEachStmt) + return forEachStmt + } + /** * Translates a [BlockSyntax][Csharp.AST.BlockSyntax] into a [Block]. * diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/statementhandler/LoopsTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/statementhandler/LoopsTest.kt new file mode 100644 index 00000000000..6c778cb5143 --- /dev/null +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/statementhandler/LoopsTest.kt @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 de.fraunhofer.aisec.cpg.frontends.csharp.statementhandler + +import de.fraunhofer.aisec.cpg.frontends.csharp.CSharpLanguage +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.declarations.Variable +import de.fraunhofer.aisec.cpg.graph.expressions.* +import de.fraunhofer.aisec.cpg.graph.expressions.ExpressionList +import de.fraunhofer.aisec.cpg.graph.types.IntegerType +import de.fraunhofer.aisec.cpg.graph.types.PointerType +import de.fraunhofer.aisec.cpg.test.* +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +class LoopsTest : BaseTest() { + + @Test + fun testWhileLoop() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("Loops.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + assertNotNull(tu) + + val method = tu.records["Foo"]?.methods["whileLoop"] + assertNotNull(method) + val body = method.body + assertIs(body) + + // int i = 0; + val decl = body.statements.getOrNull(0) + assertIs(decl) + val iVar = decl.declarations.firstOrNull() + assertIs(iVar) + + // while (i < 10) { ... } + val whileStmt = body.statements.getOrNull(1) + assertIs(whileStmt) + assertIs(whileStmt.statement) + + val condition = whileStmt.condition + assertIs(condition) + + val lhsRef = condition.lhs + assertIs(lhsRef) + assertNotNull(lhsRef.refersTo) + assertEquals(iVar, lhsRef.refersTo) + } + + @Test + fun testDoWhileLoop() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("Loops.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + assertNotNull(tu) + + val method = tu.records["Foo"]?.methods["doWhileLoop"] + assertNotNull(method) + val body = method.body + assertIs(body) + + // do {..} while (i < 10); + val doStmt = body.statements.getOrNull(1) + assertIs(doStmt) + + val condition = doStmt.condition + assertIs(condition) + + assertIs(doStmt.statement) + } + + @Test + fun testForLoop() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("Loops.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + assertNotNull(tu) + + val method = tu.records["Foo"]?.methods["forLoop"] + assertNotNull(method) + val body = method.body + assertIs(body) + + // for (int i = 0; i < 10; i += 1) + val forStmt = body.statements.getOrNull(0) + assertNotNull(forStmt) + assertIs(forStmt) + + // initializer: int i = 0 + val initializer = forStmt.initializerStatement + assertIs(initializer) + val iVar = initializer.declarations.firstOrNull() + assertIs(iVar) + assertIs(iVar.type) + + // condition: i < 10 + val condition = forStmt.condition + assertIs(condition) + + // iterator: i += 1 + val iterator = forStmt.iterationStatement + assertIs(iterator) + + assertIs(forStmt.statement) + } + + @Test + fun testForLoopMultipleIncrementors() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("Loops.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + assertNotNull(tu) + + val method = tu.records["Foo"]?.methods["forLoopMultipleIncrementors"] + assertNotNull(method) + val body = method.body + assertIs(body) + + // for (int i = 0, j = 10; i < j; i += 1, j -= 1) + val forStmt = body.statements.getOrNull(0) + assertIs(forStmt) + + // initializer: int i = 0, j = 10 + val initializer = forStmt.initializerStatement + assertIs(initializer) + assertEquals(2, initializer.declarations.size) + + assertIs(forStmt.statement) + + // iterator: i += 1, j -= 1 + val iterator = forStmt.iterationStatement + assertIs(iterator) + assertEquals(2, iterator.expressions.size) + + val firstIncr = iterator.expressions.getOrNull(0) + assertNotNull(firstIncr) + assertIs(firstIncr) + + val secondIncr = iterator.expressions.getOrNull(1) + assertNotNull(secondIncr) + assertIs(secondIncr) + } + + @Test + fun testForEachLoop() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("Loops.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + assertNotNull(tu) + + val method = tu.records["Foo"]?.methods["forEachLoop"] + assertNotNull(method) + val body = method.body + assertIs(body) + + // foreach (int n in numbers) + val forEachStmt = body.statements.getOrNull(1) + assertIs(forEachStmt) + assertIs(forEachStmt.statement) + + val variable = forEachStmt.variable + assertIs(variable) + val nVar = variable.declarations.firstOrNull() + assertIs(nVar) + assertIs(nVar.type) + + val iterable = forEachStmt.iterable + assertIs(iterable) + assertEquals("numbers", iterable.name.localName) + val numbersVar = iterable.refersTo + assertIs(numbersVar) + val numbersType = numbersVar.type + assertIs(numbersType) + assertEquals(PointerType.PointerOrigin.ARRAY, numbersType.pointerOrigin) + assertIs(numbersType.elementType) + + val loopBody = forEachStmt.statement + assertIs(loopBody) + val xDecl = loopBody.statements.getOrNull(0) + assertNotNull(xDecl) + assertIs(xDecl) + val xVar = xDecl.declarations.firstOrNull() + assertNotNull(xVar) + assertIs(xVar) + val nRef = xVar.initializer + assertNotNull(nRef) + assertIs(nRef) + assertEquals(nVar, nRef.refersTo) + } +} diff --git a/cpg-language-csharp/src/test/resources/csharp/Loops.cs b/cpg-language-csharp/src/test/resources/csharp/Loops.cs new file mode 100644 index 00000000000..426e720fcad --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/Loops.cs @@ -0,0 +1,47 @@ +namespace Loops; + +class Foo +{ + void whileLoop() + { + int i = 0; + while (i < 10) + { + i += 1; + } + } + + void doWhileLoop() + { + int i = 0; + do + { + i += 1; + } while (i < 10); + } + + void forLoop() + { + for (int i = 0; i < 10; i += 1) + { + int x = i; + } + } + + void forLoopMultipleIncrementors() + { + for (int i = 0, j = 10; i < j; i += 1, j -= 1) + { + int x = i; + } + } + + void forEachLoop() + { + int[] numbers = null; + foreach (int n in numbers) + { + int x = n; + } + } +} \ No newline at end of file From 963135380d583d7e1f8e5d9e9cc4313beebde1c9 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Mon, 30 Mar 2026 22:13:28 +0200 Subject: [PATCH 37/55] MemberAccess --- .../src/main/csharp/NativeParser/Library.cs | 18 +++++ .../aisec/cpg/frontends/csharp/CSharp.kt | 24 ++++++ .../cpg/frontends/csharp/ExpressionHandler.kt | 22 +++++ .../expressionhandler/MemberAccessTest.kt | 80 +++++++++++++++++++ .../src/test/resources/csharp/MemberAccess.cs | 9 +++ 5 files changed, 153 insertions(+) create mode 100644 cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/MemberAccessTest.kt create mode 100644 cpg-language-csharp/src/test/resources/csharp/MemberAccess.cs diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index f1a2ffcb842..b9da37f5283 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -471,4 +471,22 @@ public static IntPtr GetForEachStatementStatement(IntPtr handlePtr) { return Register(((ForEachStatementSyntax)Nodes[handlePtr]).Statement); } + + [UnmanagedCallersOnly(EntryPoint = "GetMemberAccessExpressionExpression")] + public static IntPtr GetMemberAccessExpressionExpression(IntPtr handlePtr) + { + return Register(((MemberAccessExpressionSyntax)Nodes[handlePtr]).Expression); + } + + [UnmanagedCallersOnly(EntryPoint = "GetMemberAccessExpressionName")] + public static IntPtr GetMemberAccessExpressionName(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8(((MemberAccessExpressionSyntax)Nodes[handlePtr]).Name.Identifier.ToString()); + } + + [UnmanagedCallersOnly(EntryPoint = "GetMemberAccessExpressionOperatorToken")] + public static IntPtr GetMemberAccessExpressionOperatorToken(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8(((MemberAccessExpressionSyntax)Nodes[handlePtr]).OperatorToken.Text); + } } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 8904176fd48..896b55732cd 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -426,6 +426,7 @@ interface Csharp : Library { "BinaryExpressionSyntax" -> BinaryExpressionSyntax(nativeValue) "IdentifierNameSyntax" -> IdentifierNameSyntax(nativeValue) "AssignmentExpressionSyntax" -> AssignmentExpressionSyntax(nativeValue) + "MemberAccessExpressionSyntax" -> MemberAccessExpressionSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } } @@ -484,6 +485,21 @@ interface Csharp : Library { class IdentifierNameSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { val identifier: String by lazy { INSTANCE.GetIdentifierNameSyntaxIdentifier(this) } } + + /** + * Represents the Roslyn + * [`MemberAccessExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.memberaccessexpressionsyntax) + * class. + */ + class MemberAccessExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { + val expression: ExpressionSyntax by lazy { + INSTANCE.GetMemberAccessExpressionExpression(this) + } + val name: String by lazy { INSTANCE.GetMemberAccessExpressionName(this) } + val operatorToken: String by lazy { + INSTANCE.GetMemberAccessExpressionOperatorToken(this) + } + } } /** @@ -659,6 +675,14 @@ interface Csharp : Library { fun GetForEachStatementStatement(handle: AST.ForEachStatementSyntax): AST.StatementSyntax + fun GetMemberAccessExpressionExpression( + handle: AST.MemberAccessExpressionSyntax + ): AST.ExpressionSyntax + + fun GetMemberAccessExpressionName(handle: AST.MemberAccessExpressionSyntax): String + + fun GetMemberAccessExpressionOperatorToken(handle: AST.MemberAccessExpressionSyntax): String + companion object { val INSTANCE: Csharp by lazy { try { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt index 895ec50d6a0..2f0bdbc15aa 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt @@ -29,11 +29,13 @@ import de.fraunhofer.aisec.cpg.graph.expressions.Assign import de.fraunhofer.aisec.cpg.graph.expressions.BinaryOperator import de.fraunhofer.aisec.cpg.graph.expressions.Expression import de.fraunhofer.aisec.cpg.graph.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.expressions.MemberAccess import de.fraunhofer.aisec.cpg.graph.expressions.ProblemExpression import de.fraunhofer.aisec.cpg.graph.expressions.Reference import de.fraunhofer.aisec.cpg.graph.newAssign import de.fraunhofer.aisec.cpg.graph.newBinaryOperator import de.fraunhofer.aisec.cpg.graph.newLiteral +import de.fraunhofer.aisec.cpg.graph.newMemberAccess import de.fraunhofer.aisec.cpg.graph.newProblemExpression import de.fraunhofer.aisec.cpg.graph.newReference import de.fraunhofer.aisec.cpg.graph.objectType @@ -46,6 +48,7 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : is Csharp.AST.LiteralExpressionSyntax -> handleLiteralExpression(node) is Csharp.AST.BinaryExpressionSyntax -> handleBinaryExpression(node) is Csharp.AST.AssignmentExpressionSyntax -> handleAssignmentExpression(node) + is Csharp.AST.MemberAccessExpressionSyntax -> handleMemberAccessExpression(node) else -> newProblemExpression( "The expression of class ${node.javaClass} is not supported yet", @@ -122,4 +125,23 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : else -> newProblemExpression("Unknown literal type: ${node.csharpType}", rawNode = node) } } + + /** + * Translates a [MemberAccessExpressionSyntax][Csharp.AST.MemberAccessExpressionSyntax] into a + * [MemberAccess]. + * + * C# spec: + * [MemberAccess](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#12813-member-access) + */ + private fun handleMemberAccessExpression( + node: Csharp.AST.MemberAccessExpressionSyntax + ): MemberAccess { + val base = handle(node.expression) + return newMemberAccess( + name = node.name, + base = base, + operatorCode = node.operatorToken, + rawNode = node, + ) + } } diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/MemberAccessTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/MemberAccessTest.kt new file mode 100644 index 00000000000..a74b2f682f4 --- /dev/null +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/MemberAccessTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 de.fraunhofer.aisec.cpg.frontends.csharp.expressionhandler + +import de.fraunhofer.aisec.cpg.frontends.csharp.CSharpLanguage +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.declarations.Variable +import de.fraunhofer.aisec.cpg.graph.expressions.Block +import de.fraunhofer.aisec.cpg.graph.expressions.DeclarationStatement +import de.fraunhofer.aisec.cpg.graph.expressions.MemberAccess +import de.fraunhofer.aisec.cpg.graph.expressions.Reference +import de.fraunhofer.aisec.cpg.test.* +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +class MemberAccessTest : BaseTest() { + + @Test + fun simpleMemberAccessTest() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("MemberAccess.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val foo = tu.records["Foo"] + assertNotNull(foo) + + val method = foo.methods["simpleMemberAccess"] + assertNotNull(method) + val body = method.body + assertIs(body) + + // int a = obj.x; + val declStmt = body.statements.getOrNull(0) + assertIs(declStmt) + val decl = declStmt.singleDeclaration + assertIs(decl) + + val memberAccess = decl.initializer + assertIs(memberAccess) + assertEquals("x", memberAccess.name.localName) + assertEquals(".", memberAccess.operatorCode) + + val base = memberAccess.base + assertIs(base) + assertEquals("obj", base.name.localName) + } +} diff --git a/cpg-language-csharp/src/test/resources/csharp/MemberAccess.cs b/cpg-language-csharp/src/test/resources/csharp/MemberAccess.cs new file mode 100644 index 00000000000..a2b8b40751e --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/MemberAccess.cs @@ -0,0 +1,9 @@ +class Foo +{ + int x; + + void simpleMemberAccess(Foo obj) + { + int a = obj.x; + } +} \ No newline at end of file From 33bde9b98d58a4802c262e9b0f2402c1a098a1b3 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Tue, 31 Mar 2026 10:26:07 +0200 Subject: [PATCH 38/55] Add access handling --- .../aisec/cpg/frontends/csharp/CSharp.kt | 8 ++ .../frontends/csharp/DeclarationHandler.kt | 13 +++ .../cpg/frontends/csharp/ExpressionHandler.kt | 14 +++ .../expressionhandler/MemberAccessTest.kt | 94 ++++++++++++++++++- .../src/test/resources/csharp/MemberAccess.cs | 18 +++- 5 files changed, 142 insertions(+), 5 deletions(-) diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 896b55732cd..791ef581668 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -427,6 +427,7 @@ interface Csharp : Library { "IdentifierNameSyntax" -> IdentifierNameSyntax(nativeValue) "AssignmentExpressionSyntax" -> AssignmentExpressionSyntax(nativeValue) "MemberAccessExpressionSyntax" -> MemberAccessExpressionSyntax(nativeValue) + "ThisExpressionSyntax" -> ThisExpressionSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } } @@ -486,6 +487,13 @@ interface Csharp : Library { val identifier: String by lazy { INSTANCE.GetIdentifierNameSyntaxIdentifier(this) } } + /** + * Represents the Roslyn + * [`ThisExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.thisexpressionsyntax) + * class. + */ + class ThisExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) + /** * Represents the Roslyn * [`MemberAccessExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.memberaccessexpressionsyntax) diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt index 9f1a1260090..ba4ce64fd84 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -26,12 +26,15 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.graph.declarations.* +import de.fraunhofer.aisec.cpg.graph.implicit import de.fraunhofer.aisec.cpg.graph.newConstructor import de.fraunhofer.aisec.cpg.graph.newField import de.fraunhofer.aisec.cpg.graph.newMethod import de.fraunhofer.aisec.cpg.graph.newNamespace import de.fraunhofer.aisec.cpg.graph.newParameter import de.fraunhofer.aisec.cpg.graph.newRecord +import de.fraunhofer.aisec.cpg.graph.newVariable +import de.fraunhofer.aisec.cpg.graph.unknownType class DeclarationHandler(frontend: CSharpLanguageFrontend) : CSharpHandler(::ProblemDeclaration, frontend) { @@ -105,6 +108,8 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : val method = newMethod(node.identifier, rawNode = node) frontend.scopeManager.enterScope(method) + createMethodReceiver(method) + for (parameter in node.parameters) { val param = newParameter( @@ -147,4 +152,12 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : frontend.scopeManager.leaveScope(constructor) return constructor } + + /** Creates an implicit `this` receiver variable for the given [method]. */ + private fun createMethodReceiver(method: Method) { + val record = frontend.scopeManager.currentRecord + val receiver = newVariable("this", record?.toType() ?: unknownType()).implicit("this") + frontend.scopeManager.addDeclaration(receiver) + method.receiver = receiver + } } diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt index 2f0bdbc15aa..9634d16f55e 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt @@ -39,6 +39,7 @@ import de.fraunhofer.aisec.cpg.graph.newMemberAccess import de.fraunhofer.aisec.cpg.graph.newProblemExpression import de.fraunhofer.aisec.cpg.graph.newReference import de.fraunhofer.aisec.cpg.graph.objectType +import de.fraunhofer.aisec.cpg.graph.unknownType class ExpressionHandler(frontend: CSharpLanguageFrontend) : CSharpHandler(::ProblemExpression, frontend) { @@ -49,6 +50,7 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : is Csharp.AST.BinaryExpressionSyntax -> handleBinaryExpression(node) is Csharp.AST.AssignmentExpressionSyntax -> handleAssignmentExpression(node) is Csharp.AST.MemberAccessExpressionSyntax -> handleMemberAccessExpression(node) + is Csharp.AST.ThisExpressionSyntax -> handleThisExpression(node) else -> newProblemExpression( "The expression of class ${node.javaClass} is not supported yet", @@ -144,4 +146,16 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : rawNode = node, ) } + + /** + * Translates a [ThisExpressionSyntax][Csharp.AST.ThisExpressionSyntax] into a [Reference] with + * the name `this`. + * + * C# spec: + * [ThisAccess](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#12822-this-access) + */ + private fun handleThisExpression(node: Csharp.AST.ThisExpressionSyntax): Reference { + val type = frontend.scopeManager.currentRecord?.toType() ?: unknownType() + return newReference(name = "this", type = type, rawNode = node) + } } diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/MemberAccessTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/MemberAccessTest.kt index a74b2f682f4..611c20dc035 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/MemberAccessTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/MemberAccessTest.kt @@ -28,6 +28,7 @@ package de.fraunhofer.aisec.cpg.frontends.csharp.expressionhandler import de.fraunhofer.aisec.cpg.frontends.csharp.CSharpLanguage import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.declarations.Variable +import de.fraunhofer.aisec.cpg.graph.expressions.Assign import de.fraunhofer.aisec.cpg.graph.expressions.Block import de.fraunhofer.aisec.cpg.graph.expressions.DeclarationStatement import de.fraunhofer.aisec.cpg.graph.expressions.MemberAccess @@ -59,11 +60,56 @@ class MemberAccessTest : BaseTest() { val method = foo.methods["simpleMemberAccess"] assertNotNull(method) + val objParam = method.parameters["obj"] + assertNotNull(objParam) val body = method.body assertIs(body) - // int a = obj.x; - val declStmt = body.statements.getOrNull(0) + // int a = obj.b; + val declStmt = body.statements.firstOrNull() + assertIs(declStmt) + val decl = declStmt.singleDeclaration + assertIs(decl) + + val memberAccess = decl.initializer + assertIs(memberAccess) + assertEquals("b", memberAccess.name.localName) + assertEquals(".", memberAccess.operatorCode) + + val bar = tu.records["Bar"] + assertNotNull(bar) + val fieldB = bar.fields["b"] + assertNotNull(fieldB) + assertEquals(fieldB, memberAccess.refersTo) + + val base = memberAccess.base + assertIs(base) + assertEquals(objParam, base.refersTo) + } + + @Test + fun thisMemberAccessTest() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("MemberAccess.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val foo = tu.records["Foo"] + assertNotNull(foo) + + val method = foo.methods["thisMemberAccess"] + assertNotNull(method) + val body = method.body + assertIs(body) + + // int a = this.x; + val declStmt = body.statements.firstOrNull() assertIs(declStmt) val decl = declStmt.singleDeclaration assertIs(decl) @@ -75,6 +121,48 @@ class MemberAccessTest : BaseTest() { val base = memberAccess.base assertIs(base) - assertEquals("obj", base.name.localName) + assertEquals("this", base.name.localName) + assertEquals(foo.toType(), base.type) + assertNotNull(method.receiver) + assertEquals(method.receiver, base.refersTo) + } + + @Test + fun thisFieldAccessTest() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("MemberAccess.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val foo = tu.records["Foo"] + assertNotNull(foo) + + val fieldX = foo.fields["x"] + assertNotNull(fieldX) + + val method = foo.methods["thisFieldAccess"] + assertNotNull(method) + val body = method.body + assertIs(body) + + // this.x = 42; + val assign = body.statements.firstOrNull() + assertIs(assign) + + val lhs = assign.lhs.singleOrNull() + assertIs(lhs) + assertEquals("x", lhs.name.localName) + assertEquals(fieldX, lhs.refersTo) + + val base = lhs.base + assertIs(base) + assertEquals("this", base.name.localName) + assertEquals(method.receiver, base.refersTo) } } diff --git a/cpg-language-csharp/src/test/resources/csharp/MemberAccess.cs b/cpg-language-csharp/src/test/resources/csharp/MemberAccess.cs index a2b8b40751e..5948b4e6372 100644 --- a/cpg-language-csharp/src/test/resources/csharp/MemberAccess.cs +++ b/cpg-language-csharp/src/test/resources/csharp/MemberAccess.cs @@ -2,8 +2,22 @@ class Foo { int x; - void simpleMemberAccess(Foo obj) + void simpleMemberAccess(Bar obj) { - int a = obj.x; + int a = obj.b; } + + void thisMemberAccess() + { + int a = this.x; + } + + void thisFieldAccess() + { + this.x = 42; + } +} + +class Bar { + string b; } \ No newline at end of file From 7c33426d7941d5165877c449c7f6a3426e5ccd20 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Tue, 31 Mar 2026 16:02:51 +0200 Subject: [PATCH 39/55] Add Call and MemberCall handling --- .../src/main/csharp/NativeParser/Library.cs | 24 +++ .../aisec/cpg/frontends/csharp/CSharp.kt | 38 +++++ .../frontends/csharp/DeclarationHandler.kt | 30 ++-- .../cpg/frontends/csharp/ExpressionHandler.kt | 43 ++++-- .../cpg/frontends/csharp/StatementHandler.kt | 6 +- .../csharp/expressionhandler/CallTest.kt | 139 ++++++++++++++++++ .../src/test/resources/csharp/Calls.cs | 19 +++ 7 files changed, 274 insertions(+), 25 deletions(-) create mode 100644 cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/CallTest.kt create mode 100644 cpg-language-csharp/src/test/resources/csharp/Calls.cs diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index b9da37f5283..f2abca412e7 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -472,6 +472,30 @@ public static IntPtr GetForEachStatementStatement(IntPtr handlePtr) return Register(((ForEachStatementSyntax)Nodes[handlePtr]).Statement); } + [UnmanagedCallersOnly(EntryPoint = "GetInvocationExpressionExpression")] + public static IntPtr GetInvocationExpressionExpression(IntPtr handlePtr) + { + return Register(((InvocationExpressionSyntax)Nodes[handlePtr]).Expression); + } + + [UnmanagedCallersOnly(EntryPoint = "GetInvocationExpressionArgumentCount")] + public static int GetInvocationExpressionArgumentCount(IntPtr handlePtr) + { + return ((InvocationExpressionSyntax)Nodes[handlePtr]).ArgumentList.Arguments.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetInvocationExpressionArgument")] + public static IntPtr GetInvocationExpressionArgument(IntPtr handlePtr, int index) + { + return Register(((InvocationExpressionSyntax)Nodes[handlePtr]).ArgumentList.Arguments[index]); + } + + [UnmanagedCallersOnly(EntryPoint = "GetArgumentExpression")] + public static IntPtr GetArgumentExpression(IntPtr handlePtr) + { + return Register(((ArgumentSyntax)Nodes[handlePtr]).Expression); + } + [UnmanagedCallersOnly(EntryPoint = "GetMemberAccessExpressionExpression")] public static IntPtr GetMemberAccessExpressionExpression(IntPtr handlePtr) { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 791ef581668..c30a225dfcc 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -427,6 +427,7 @@ interface Csharp : Library { "IdentifierNameSyntax" -> IdentifierNameSyntax(nativeValue) "AssignmentExpressionSyntax" -> AssignmentExpressionSyntax(nativeValue) "MemberAccessExpressionSyntax" -> MemberAccessExpressionSyntax(nativeValue) + "InvocationExpressionSyntax" -> InvocationExpressionSyntax(nativeValue) "ThisExpressionSyntax" -> ThisExpressionSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } @@ -494,6 +495,30 @@ interface Csharp : Library { */ class ThisExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) + /** + * Represents the Roslyn + * [`InvocationExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.invocationexpressionsyntax) + * class. + */ + class InvocationExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { + val expression: ExpressionSyntax by lazy { + INSTANCE.GetInvocationExpressionExpression(this) + } + val arguments: List by lazy { + val count = INSTANCE.GetInvocationExpressionArgumentCount(this) + (0 until count).map { i -> INSTANCE.GetInvocationExpressionArgument(this, i) } + } + } + + /** + * Represents the Roslyn + * [`ArgumentSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.argumentsyntax) + * class. + */ + class ArgumentSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val expression: ExpressionSyntax by lazy { INSTANCE.GetArgumentExpression(this) } + } + /** * Represents the Roslyn * [`MemberAccessExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.memberaccessexpressionsyntax) @@ -683,6 +708,19 @@ interface Csharp : Library { fun GetForEachStatementStatement(handle: AST.ForEachStatementSyntax): AST.StatementSyntax + fun GetInvocationExpressionExpression( + handle: AST.InvocationExpressionSyntax + ): AST.ExpressionSyntax + + fun GetInvocationExpressionArgumentCount(handle: AST.InvocationExpressionSyntax): Int + + fun GetInvocationExpressionArgument( + handle: AST.InvocationExpressionSyntax, + index: Int, + ): AST.ArgumentSyntax + + fun GetArgumentExpression(handle: AST.ArgumentSyntax): AST.ExpressionSyntax + fun GetMemberAccessExpressionExpression( handle: AST.MemberAccessExpressionSyntax ): AST.ExpressionSyntax diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt index ba4ce64fd84..3491d0ca40f 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -49,9 +49,11 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : } /** - * Translates a C# - * [`NamespaceDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.namespacedeclarationsyntax?view=roslyn-dotnet-5.0.0) - * into a [Namespace]. + * Translates a [NamespaceDeclarationSyntax][Csharp.AST.NamespaceDeclarationSyntax] into a + * [Namespace]. + * + * C# spec: + * [Namespace declarations](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/namespaces#143-namespace-declarations) */ private fun handleNamespaceDeclaration( node: Csharp.AST.NamespaceDeclarationSyntax @@ -70,9 +72,10 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : } /** - * Translates a C# - * [`ClassDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.classdeclarationsyntax?view=roslyn-dotnet-5.0.0) - * into a [Record]. + * Translates a [ClassDeclarationSyntax][Csharp.AST.ClassDeclarationSyntax] into a [Record]. + * + * C# spec: + * [Class declarations](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#1522-class-declarations) */ private fun handleClassDeclaration(node: Csharp.AST.ClassDeclarationSyntax): Declaration { val record = newRecord(node.identifier, "class", rawNode = node) @@ -100,9 +103,10 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : } /** - * Translates a C# - * [`MethodDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.methoddeclarationsyntax?view=roslyn-dotnet-5.0.0) - * into a [Method]. + * Translates a [MethodDeclarationSyntax][Csharp.AST.MethodDeclarationSyntax] into a [Method]. + * + * C# spec: + * [Methods](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#156-methods) */ private fun handleMethodDeclaration(node: Csharp.AST.MethodDeclarationSyntax): Declaration { val method = newMethod(node.identifier, rawNode = node) @@ -126,9 +130,11 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : } /** - * Translates a C# - * [`ConstructorDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.constructordeclarationsyntax?view=roslyn-dotnet-5.0.0) - * into a [Constructor]. + * Translates a [ConstructorDeclarationSyntax][Csharp.AST.ConstructorDeclarationSyntax] into a + * [Constructor]. + * + * C# spec: + * [Instance constructors](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#1511-instance-constructors) */ private fun handleConstructorDeclaration( node: Csharp.AST.ConstructorDeclarationSyntax diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt index 9634d16f55e..a3419f6402f 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt @@ -27,6 +27,7 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.graph.expressions.Assign import de.fraunhofer.aisec.cpg.graph.expressions.BinaryOperator +import de.fraunhofer.aisec.cpg.graph.expressions.Call import de.fraunhofer.aisec.cpg.graph.expressions.Expression import de.fraunhofer.aisec.cpg.graph.expressions.Literal import de.fraunhofer.aisec.cpg.graph.expressions.MemberAccess @@ -34,8 +35,10 @@ import de.fraunhofer.aisec.cpg.graph.expressions.ProblemExpression import de.fraunhofer.aisec.cpg.graph.expressions.Reference import de.fraunhofer.aisec.cpg.graph.newAssign import de.fraunhofer.aisec.cpg.graph.newBinaryOperator +import de.fraunhofer.aisec.cpg.graph.newCall import de.fraunhofer.aisec.cpg.graph.newLiteral import de.fraunhofer.aisec.cpg.graph.newMemberAccess +import de.fraunhofer.aisec.cpg.graph.newMemberCall import de.fraunhofer.aisec.cpg.graph.newProblemExpression import de.fraunhofer.aisec.cpg.graph.newReference import de.fraunhofer.aisec.cpg.graph.objectType @@ -49,13 +52,10 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : is Csharp.AST.LiteralExpressionSyntax -> handleLiteralExpression(node) is Csharp.AST.BinaryExpressionSyntax -> handleBinaryExpression(node) is Csharp.AST.AssignmentExpressionSyntax -> handleAssignmentExpression(node) + is Csharp.AST.InvocationExpressionSyntax -> handleInvocationExpression(node) is Csharp.AST.MemberAccessExpressionSyntax -> handleMemberAccessExpression(node) is Csharp.AST.ThisExpressionSyntax -> handleThisExpression(node) - else -> - newProblemExpression( - "The expression of class ${node.javaClass} is not supported yet", - rawNode = node, - ) + else -> ProblemExpression("Not supported: ${node.csharpType}") } } @@ -63,7 +63,7 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : * Translates an [IdentifierNameSyntax][Csharp.AST.IdentifierNameSyntax] into a [Reference]. * * C# spec: - * [SimpleNames](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#12842-simple-names) + * [Simple names](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#1284-simple-names) */ private fun handleIdentifierName(node: Csharp.AST.IdentifierNameSyntax): Reference { return newReference(name = node.identifier, rawNode = node) @@ -74,7 +74,7 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : * [BinaryOperator]. * * C# spec: - * [BinaryOperator](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#12104-binary-operators) + * [Binary operator](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#1245-binary-operator-overload-resolution) */ private fun handleBinaryExpression(node: Csharp.AST.BinaryExpressionSyntax): BinaryOperator { return newBinaryOperator(operatorCode = node.operatorToken, rawNode = node).apply { @@ -88,7 +88,7 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : * [Assign]. * * C# spec: - * [AssignmentOperators](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#12214-assignment-operators) + * [Assignment operators](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#1223-assignment-operators) */ private fun handleAssignmentExpression(node: Csharp.AST.AssignmentExpressionSyntax): Assign { return newAssign( @@ -124,16 +124,39 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : newLiteral(node.value.single(), builtInTypes.getValue("char"), rawNode = node) is Csharp.AST.NullLiteralExpressionSyntax -> newLiteral(null, objectType("null"), rawNode = node) + // TODO: Return unknownType()? else -> newProblemExpression("Unknown literal type: ${node.csharpType}", rawNode = node) } } + /** + * Translates an [InvocationExpressionSyntax][Csharp.AST.InvocationExpressionSyntax] into a + * [Call]. If the callee is a [MemberAccess] (e.g. `obj.Method()`), a `MemberCall` is created. + * Otherwise (e.g. `Foo()`), a plain [Call] is created. + * + * C# spec: + * [Invocation expressions](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#12810-invocation-expressions) + */ + private fun handleInvocationExpression(node: Csharp.AST.InvocationExpressionSyntax): Call { + val callee = handle(node.expression) + val call = + if (callee is MemberAccess) { + newMemberCall(callee, rawNode = node) + } else { + newCall(callee, rawNode = node) + } + for (arg in node.arguments) { + call.addArgument(handle(arg.expression)) + } + return call + } + /** * Translates a [MemberAccessExpressionSyntax][Csharp.AST.MemberAccessExpressionSyntax] into a * [MemberAccess]. * * C# spec: - * [MemberAccess](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#12813-member-access) + * [Member access](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#1287-member-access) */ private fun handleMemberAccessExpression( node: Csharp.AST.MemberAccessExpressionSyntax @@ -152,7 +175,7 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : * the name `this`. * * C# spec: - * [ThisAccess](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#12822-this-access) + * [This access](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#12814-this-access) */ private fun handleThisExpression(node: Csharp.AST.ThisExpressionSyntax): Reference { val type = frontend.scopeManager.currentRecord?.toType() ?: unknownType() diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt index 957ca76cc15..414901c0929 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt @@ -86,7 +86,7 @@ class StatementHandler(frontend: CSharpLanguageFrontend) : * return value expression is optional (e.g. `return;` in void methods). * * C# spec: - * [ReturnStatement](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#13105-the-return-statement) + * [Return statement](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#13105-the-return-statement) */ private fun handleReturn(node: Csharp.AST.ReturnStatementSyntax): Return { val ret = newReturn(rawNode = node) @@ -101,7 +101,7 @@ class StatementHandler(frontend: CSharpLanguageFrontend) : * [Variable] and added to the current scope. * * C# spec: - * [LocalVariableDeclaration](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#1364-local-variable-declarations) + * [Local variable declarations](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#1362-local-variable-declarations) */ private fun handleLocalDeclaration( node: Csharp.AST.LocalDeclarationStatementSyntax @@ -125,7 +125,7 @@ class StatementHandler(frontend: CSharpLanguageFrontend) : * [Expression]. * * C# spec: - * [ExpressionStatement](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#1372-expression-statements) + * [Expression statements](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#137-expression-statements) */ private fun handleExpressionStatement(node: Csharp.AST.ExpressionStatementSyntax): Expression { return frontend.expressionHandler.handle(node.expression) diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/CallTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/CallTest.kt new file mode 100644 index 00000000000..da69891889c --- /dev/null +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/CallTest.kt @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 de.fraunhofer.aisec.cpg.frontends.csharp.expressionhandler + +import de.fraunhofer.aisec.cpg.frontends.csharp.CSharpLanguage +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.declarations.Variable +import de.fraunhofer.aisec.cpg.graph.expressions.Block +import de.fraunhofer.aisec.cpg.graph.expressions.Call +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.types.IntegerType +import de.fraunhofer.aisec.cpg.test.* +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +class CallTest : BaseTest() { + + @Test + fun memberCallTest() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("Calls.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + assertNotNull(tu) + + val bar = tu.records["Bar"] + assertNotNull(bar) + + val addMethod = tu.methods["Add"] + assertNotNull(addMethod) + + val memberCallmethod = bar.methods["memberCall"] + assertNotNull(memberCallmethod) + val body = memberCallmethod.body + assertIs(body) + + // this.Add(1, 2); + val memberCall = body.statements.firstOrNull() + assertNotNull(memberCall) + assertIs(memberCall) + val invokesEdge = memberCall.invokes.firstOrNull() + assertNotNull(invokesEdge) + assertEquals(addMethod, invokesEdge) + + val callee = memberCall.callee + assertIs(callee) + + val base = callee.base + assertIs(base) + assertIs(base.refersTo) + assertEquals(base.type.name, bar.name) + + val memberCallArgs = memberCall.arguments + assertEquals(2, memberCallArgs.size) + val arg0 = memberCallArgs.firstOrNull() + assertNotNull(arg0) + assertIs>(arg0) + assertIs(arg0.type) + assertEquals(1, arg0.value) + val arg1 = memberCallArgs.getOrNull(1) + assertNotNull(arg1) + assertIs>(arg1) + assertIs(arg0.type) + assertEquals(2, arg1.value) + } + + @Test + fun simpleCallTest() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("Calls.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + assertNotNull(tu) + + val bar = tu.records["Bar"] + assertNotNull(bar) + + val addMethod = tu.methods["Add"] + assertNotNull(addMethod) + + val simpleCallMethod = bar.methods["simpleCall"] + assertNotNull(simpleCallMethod) + val body = simpleCallMethod.body + assertIs(body) + + // Add(3, 4); + val call = body.statements.firstOrNull() + assertNotNull(call) + assertIs(call) + assertEquals("Add", call.name.localName) + val invokesEdge = call.invokes.firstOrNull() + assertNotNull(invokesEdge) + assertEquals(addMethod, invokesEdge) + + val callArgs = call.arguments + assertEquals(2, callArgs.size) + val arg0 = callArgs.firstOrNull() + assertNotNull(arg0) + assertIs>(arg0) + assertIs(arg0.type) + assertEquals(3, arg0.value) + val arg1 = callArgs.getOrNull(1) + assertNotNull(arg1) + assertIs>(arg1) + assertIs(arg1.type) + assertEquals(4, arg1.value) + } +} diff --git a/cpg-language-csharp/src/test/resources/csharp/Calls.cs b/cpg-language-csharp/src/test/resources/csharp/Calls.cs new file mode 100644 index 00000000000..d88ba519694 --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/Calls.cs @@ -0,0 +1,19 @@ +class Bar +{ + int x; + + int Add(int a, int b) + { + return a + b; + } + + void memberCall() + { + this.Add(1, 2); + } + + void simpleCall() + { + Add(3, 4); + } +} \ No newline at end of file From 614bca2b37f0e790cd1e7525478b8c1c4eee4197 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Tue, 31 Mar 2026 22:29:52 +0200 Subject: [PATCH 40/55] Add HasImplicitReceiver trait --- .../fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt index 3037627dd75..46a83b230f7 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt @@ -25,6 +25,7 @@ */ package de.fraunhofer.aisec.cpg.frontends.csharp +import de.fraunhofer.aisec.cpg.frontends.HasImplicitReceiver import de.fraunhofer.aisec.cpg.frontends.Language import de.fraunhofer.aisec.cpg.graph.types.BooleanType import de.fraunhofer.aisec.cpg.graph.types.FloatingPointType @@ -36,7 +37,8 @@ import de.fraunhofer.aisec.cpg.graph.types.Type import kotlin.reflect.KClass import org.neo4j.ogm.annotation.Transient -class CSharpLanguage : Language() { +class CSharpLanguage : Language(), HasImplicitReceiver { + override val receiverName = "this" override val fileExtensions = listOf("cs") override val namespaceDelimiter = "." From fb2d8a24a3f673d84505f5561f9f8cd55eea60d2 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Tue, 31 Mar 2026 23:16:58 +0200 Subject: [PATCH 41/55] Add ObjectCreationExpression --- .../src/main/csharp/NativeParser/Library.cs | 61 +++++---- .../aisec/cpg/frontends/csharp/CSharp.kt | 120 ++++++++++++------ .../frontends/csharp/DeclarationHandler.kt | 4 +- .../cpg/frontends/csharp/ExpressionHandler.kt | 31 ++++- .../expressionhandler/ObjectCreationTest.kt | 90 +++++++++++++ .../test/resources/csharp/ObjectCreation.cs | 17 +++ 6 files changed, 258 insertions(+), 65 deletions(-) create mode 100644 cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/ObjectCreationTest.kt create mode 100644 cpg-language-csharp/src/test/resources/csharp/ObjectCreation.cs diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index f2abca412e7..87df96bb16a 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -160,16 +160,10 @@ public static IntPtr GetVariableDeclaratorIdentifier(IntPtr handlePtr) ); } - [UnmanagedCallersOnly(EntryPoint = "GetMethodDeclarationParameterCount")] - public static int GetMethodDeclarationParameterCount(IntPtr handlePtr) + [UnmanagedCallersOnly(EntryPoint = "GetBaseMethodDeclarationParameterList")] + public static IntPtr GetBaseMethodDeclarationParameterList(IntPtr handlePtr) { - return ((MethodDeclarationSyntax)Nodes[handlePtr]).ParameterList.Parameters.Count; - } - - [UnmanagedCallersOnly(EntryPoint = "GetMethodDeclarationParameter")] - public static IntPtr GetMethodDeclarationParameter(IntPtr handlePtr, int index) - { - return Register(((MethodDeclarationSyntax)Nodes[handlePtr]).ParameterList.Parameters[index]); + return Register(((BaseMethodDeclarationSyntax)Nodes[handlePtr]).ParameterList); } [UnmanagedCallersOnly(EntryPoint = "GetParameterIdentifier")] @@ -194,16 +188,28 @@ public static IntPtr GetConstructorDeclarationIdentifier(IntPtr handlePtr) ); } - [UnmanagedCallersOnly(EntryPoint = "GetConstructorDeclarationParameterCount")] - public static int GetConstructorDeclarationParameterCount(IntPtr handlePtr) + [UnmanagedCallersOnly(EntryPoint = "GetParameterListCount")] + public static int GetParameterListCount(IntPtr handlePtr) + { + return ((ParameterListSyntax)Nodes[handlePtr]).Parameters.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetParameterListParameter")] + public static IntPtr GetParameterListParameter(IntPtr handlePtr, int index) + { + return Register(((ParameterListSyntax)Nodes[handlePtr]).Parameters[index]); + } + + [UnmanagedCallersOnly(EntryPoint = "GetArgumentListCount")] + public static int GetArgumentListCount(IntPtr handlePtr) { - return ((ConstructorDeclarationSyntax)Nodes[handlePtr]).ParameterList.Parameters.Count; + return ((ArgumentListSyntax)Nodes[handlePtr]).Arguments.Count; } - [UnmanagedCallersOnly(EntryPoint = "GetConstructorDeclarationParameter")] - public static IntPtr GetConstructorDeclarationParameter(IntPtr handlePtr, int index) + [UnmanagedCallersOnly(EntryPoint = "GetArgumentListArgument")] + public static IntPtr GetArgumentListArgument(IntPtr handlePtr, int index) { - return Register(((ConstructorDeclarationSyntax)Nodes[handlePtr]).ParameterList.Parameters[index]); + return Register(((ArgumentListSyntax)Nodes[handlePtr]).Arguments[index]); } // We need to call ToString() on the TypeSyntax node to get the actual type name @@ -478,16 +484,10 @@ public static IntPtr GetInvocationExpressionExpression(IntPtr handlePtr) return Register(((InvocationExpressionSyntax)Nodes[handlePtr]).Expression); } - [UnmanagedCallersOnly(EntryPoint = "GetInvocationExpressionArgumentCount")] - public static int GetInvocationExpressionArgumentCount(IntPtr handlePtr) + [UnmanagedCallersOnly(EntryPoint = "GetInvocationExpressionArgumentList")] + public static IntPtr GetInvocationExpressionArgumentList(IntPtr handlePtr) { - return ((InvocationExpressionSyntax)Nodes[handlePtr]).ArgumentList.Arguments.Count; - } - - [UnmanagedCallersOnly(EntryPoint = "GetInvocationExpressionArgument")] - public static IntPtr GetInvocationExpressionArgument(IntPtr handlePtr, int index) - { - return Register(((InvocationExpressionSyntax)Nodes[handlePtr]).ArgumentList.Arguments[index]); + return Register(((InvocationExpressionSyntax)Nodes[handlePtr]).ArgumentList); } [UnmanagedCallersOnly(EntryPoint = "GetArgumentExpression")] @@ -513,4 +513,17 @@ public static IntPtr GetMemberAccessExpressionOperatorToken(IntPtr handlePtr) { return Marshal.StringToCoTaskMemUTF8(((MemberAccessExpressionSyntax)Nodes[handlePtr]).OperatorToken.Text); } + + [UnmanagedCallersOnly(EntryPoint = "GetObjectCreationExpressionType")] + public static IntPtr GetObjectCreationExpressionType(IntPtr handlePtr) + { + return Register(((ObjectCreationExpressionSyntax)Nodes[handlePtr]).Type); + } + + [UnmanagedCallersOnly(EntryPoint = "GetBaseObjectCreationExpressionArgumentList")] + public static IntPtr GetBaseObjectCreationExpressionArgumentList(IntPtr handlePtr) + { + var argumentList = ((BaseObjectCreationExpressionSyntax)Nodes[handlePtr]).ArgumentList; + return argumentList != null ? Register(argumentList) : IntPtr.Zero; + } } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index c30a225dfcc..0ca34a9eba5 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -161,6 +161,10 @@ interface Csharp : Library { */ open class BaseMethodDeclarationSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { + val parameterList: ParameterListSyntax by lazy { + INSTANCE.GetBaseMethodDeclarationParameterList(this) + } + override fun fromNative(nativeValue: Any?, context: FromNativeContext?): Any { if (nativeValue !is Pointer) { return super.fromNative(nativeValue, context) @@ -178,10 +182,6 @@ interface Csharp : Library { */ class MethodDeclarationSyntax(p: Pointer? = Pointer.NULL) : BaseMethodDeclarationSyntax(p) { val identifier: String by lazy { INSTANCE.GetMethodDeclarationIdentifier(this) } - val parameters: List by lazy { - val count = INSTANCE.GetMethodDeclarationParameterCount(this) - (0 until count).map { i -> INSTANCE.GetMethodDeclarationParameter(this, i) } - } val body: BlockSyntax by lazy { INSTANCE.GetBaseMethodDeclarationBody(this) } } @@ -322,11 +322,6 @@ interface Csharp : Library { class ConstructorDeclarationSyntax(p: Pointer? = Pointer.NULL) : BaseMethodDeclarationSyntax(p) { val identifier: String by lazy { INSTANCE.GetConstructorDeclarationIdentifier(this) } - val parameters: List by lazy { - val count = INSTANCE.GetConstructorDeclarationParameterCount(this) - (0 until count).map { i -> INSTANCE.GetConstructorDeclarationParameter(this, i) } - } - // val body: StatementSyntax by lazy { StatementSyntax(p) } } /** @@ -388,16 +383,6 @@ interface Csharp : Library { } } - /** - * Represents the Roslyn - * [`ParameterSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.parametersyntax) - * class. - */ - class ParameterSyntax(p: Pointer? = Pointer.NULL) : Node(p) { - val identifier: String by lazy { INSTANCE.GetParameterIdentifier(this) } - val type: TypeSyntax by lazy { INSTANCE.GetParameterType(this) } - } - /** * Represents the Roslyn * [`ExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.expressionsyntax) @@ -429,6 +414,7 @@ interface Csharp : Library { "MemberAccessExpressionSyntax" -> MemberAccessExpressionSyntax(nativeValue) "InvocationExpressionSyntax" -> InvocationExpressionSyntax(nativeValue) "ThisExpressionSyntax" -> ThisExpressionSyntax(nativeValue) + "ObjectCreationExpressionSyntax" -> ObjectCreationExpressionSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } } @@ -504,9 +490,20 @@ interface Csharp : Library { val expression: ExpressionSyntax by lazy { INSTANCE.GetInvocationExpressionExpression(this) } + val argumentList: ArgumentListSyntax by lazy { + INSTANCE.GetInvocationExpressionArgumentList(this) + } + } + + /** + * Represents the Roslyn + * [`ArgumentListSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.argumentlistsyntax) + * class. + */ + class ArgumentListSyntax(p: Pointer? = Pointer.NULL) : Node(p) { val arguments: List by lazy { - val count = INSTANCE.GetInvocationExpressionArgumentCount(this) - (0 until count).map { i -> INSTANCE.GetInvocationExpressionArgument(this, i) } + val count = INSTANCE.GetArgumentListCount(this) + (0 until count).map { i -> INSTANCE.GetArgumentListArgument(this, i) } } } @@ -519,6 +516,28 @@ interface Csharp : Library { val expression: ExpressionSyntax by lazy { INSTANCE.GetArgumentExpression(this) } } + /** + * Represents the Roslyn + * [`ParameterListSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.parameterlistsyntax) + * class. + */ + class ParameterListSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val parameters: List by lazy { + val count = INSTANCE.GetParameterListCount(this) + (0 until count).map { i -> INSTANCE.GetParameterListParameter(this, i) } + } + } + + /** + * Represents the Roslyn + * [`ParameterSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.parametersyntax) + * class. + */ + class ParameterSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val identifier: String by lazy { INSTANCE.GetParameterIdentifier(this) } + val type: TypeSyntax by lazy { INSTANCE.GetParameterType(this) } + } + /** * Represents the Roslyn * [`MemberAccessExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.memberaccessexpressionsyntax) @@ -533,6 +552,30 @@ interface Csharp : Library { INSTANCE.GetMemberAccessExpressionOperatorToken(this) } } + + /** + * Represents the Roslyn + * [`BaseObjectCreationExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.baseobjectcreationexpressionsyntax) + * class. This is the common base for [ObjectCreationExpressionSyntax] (explicit type, e.g. + * `new Foo(42)`) and `ImplicitObjectCreationExpressionSyntax` (target-typed, e.g. + * `new(42)`). + */ + open class BaseObjectCreationExpressionSyntax(p: Pointer? = Pointer.NULL) : + ExpressionSyntax(p) { + val argumentList: ArgumentListSyntax? by lazy { + INSTANCE.GetBaseObjectCreationExpressionArgumentList(this) + } + } + + /** + * Represents the Roslyn + * [`ObjectCreationExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.objectcreationexpressionsyntax) + * class. + */ + class ObjectCreationExpressionSyntax(p: Pointer? = Pointer.NULL) : + BaseObjectCreationExpressionSyntax(p) { + val type: TypeSyntax by lazy { INSTANCE.GetObjectCreationExpressionType(this) } + } } /** @@ -588,12 +631,13 @@ interface Csharp : Library { fun GetMethodDeclarationIdentifier(handle: AST.MethodDeclarationSyntax): String - fun GetMethodDeclarationParameterCount(handle: AST.MethodDeclarationSyntax): Int + fun GetBaseMethodDeclarationParameterList( + handle: AST.BaseMethodDeclarationSyntax + ): AST.ParameterListSyntax - fun GetMethodDeclarationParameter( - handle: AST.MethodDeclarationSyntax, - index: Int, - ): AST.ParameterSyntax + fun GetParameterListCount(handle: AST.ParameterListSyntax): Int + + fun GetParameterListParameter(handle: AST.ParameterListSyntax, index: Int): AST.ParameterSyntax fun GetParameterIdentifier(handle: AST.ParameterSyntax): String @@ -605,13 +649,6 @@ interface Csharp : Library { fun GetConstructorDeclarationIdentifier(handle: AST.ConstructorDeclarationSyntax): String - fun GetConstructorDeclarationParameterCount(handle: AST.ConstructorDeclarationSyntax): Int - - fun GetConstructorDeclarationParameter( - handle: AST.ConstructorDeclarationSyntax, - index: Int, - ): AST.ParameterSyntax - fun GetBaseMethodDeclarationBody(handle: AST.MethodDeclarationSyntax): AST.BlockSyntax fun GetBlockStatementCount(handle: AST.StatementSyntax): Int @@ -712,12 +749,13 @@ interface Csharp : Library { handle: AST.InvocationExpressionSyntax ): AST.ExpressionSyntax - fun GetInvocationExpressionArgumentCount(handle: AST.InvocationExpressionSyntax): Int + fun GetInvocationExpressionArgumentList( + handle: AST.InvocationExpressionSyntax + ): AST.ArgumentListSyntax - fun GetInvocationExpressionArgument( - handle: AST.InvocationExpressionSyntax, - index: Int, - ): AST.ArgumentSyntax + fun GetArgumentListCount(handle: AST.ArgumentListSyntax): Int + + fun GetArgumentListArgument(handle: AST.ArgumentListSyntax, index: Int): AST.ArgumentSyntax fun GetArgumentExpression(handle: AST.ArgumentSyntax): AST.ExpressionSyntax @@ -729,6 +767,12 @@ interface Csharp : Library { fun GetMemberAccessExpressionOperatorToken(handle: AST.MemberAccessExpressionSyntax): String + fun GetObjectCreationExpressionType(handle: AST.ObjectCreationExpressionSyntax): AST.TypeSyntax + + fun GetBaseObjectCreationExpressionArgumentList( + handle: AST.BaseObjectCreationExpressionSyntax + ): AST.ArgumentListSyntax? + companion object { val INSTANCE: Csharp by lazy { try { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt index 3491d0ca40f..9fc02fc6c5d 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -114,7 +114,7 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : createMethodReceiver(method) - for (parameter in node.parameters) { + for (parameter in node.parameterList.parameters) { val param = newParameter( name = parameter.identifier, @@ -143,7 +143,7 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : val constructor = newConstructor(node.identifier, record, rawNode = node) frontend.scopeManager.enterScope(constructor) - for (parameter in node.parameters) { + for (parameter in node.parameterList.parameters) { val param = newParameter( name = parameter.identifier, diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt index a3419f6402f..7138cfc21d0 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt @@ -31,14 +31,17 @@ import de.fraunhofer.aisec.cpg.graph.expressions.Call import de.fraunhofer.aisec.cpg.graph.expressions.Expression import de.fraunhofer.aisec.cpg.graph.expressions.Literal import de.fraunhofer.aisec.cpg.graph.expressions.MemberAccess +import de.fraunhofer.aisec.cpg.graph.expressions.New import de.fraunhofer.aisec.cpg.graph.expressions.ProblemExpression import de.fraunhofer.aisec.cpg.graph.expressions.Reference import de.fraunhofer.aisec.cpg.graph.newAssign import de.fraunhofer.aisec.cpg.graph.newBinaryOperator import de.fraunhofer.aisec.cpg.graph.newCall +import de.fraunhofer.aisec.cpg.graph.newConstruction import de.fraunhofer.aisec.cpg.graph.newLiteral import de.fraunhofer.aisec.cpg.graph.newMemberAccess import de.fraunhofer.aisec.cpg.graph.newMemberCall +import de.fraunhofer.aisec.cpg.graph.newNew import de.fraunhofer.aisec.cpg.graph.newProblemExpression import de.fraunhofer.aisec.cpg.graph.newReference import de.fraunhofer.aisec.cpg.graph.objectType @@ -55,6 +58,7 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : is Csharp.AST.InvocationExpressionSyntax -> handleInvocationExpression(node) is Csharp.AST.MemberAccessExpressionSyntax -> handleMemberAccessExpression(node) is Csharp.AST.ThisExpressionSyntax -> handleThisExpression(node) + is Csharp.AST.ObjectCreationExpressionSyntax -> handleObjectCreationExpression(node) else -> ProblemExpression("Not supported: ${node.csharpType}") } } @@ -145,7 +149,7 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : } else { newCall(callee, rawNode = node) } - for (arg in node.arguments) { + for (arg in node.argumentList.arguments) { call.addArgument(handle(arg.expression)) } return call @@ -181,4 +185,29 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : val type = frontend.scopeManager.currentRecord?.toType() ?: unknownType() return newReference(name = "this", type = type, rawNode = node) } + + /** + * Translates an [ObjectCreationExpressionSyntax][Csharp.AST.ObjectCreationExpressionSyntax] + * into a [New] with a [Construction][de.fraunhofer.aisec.cpg.graph.expressions.Construction] + * initializer. + * + * C# spec: + * [Object creation expressions](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#128172-object-creation-expressions) + */ + private fun handleObjectCreationExpression( + node: Csharp.AST.ObjectCreationExpressionSyntax + ): New { + val type = frontend.typeOf(node.type) + val newExpression = newNew(type, rawNode = node) + val ctor = newConstruction(type.name.localName, rawNode = node) + ctor.type = type + val argumentList = node.argumentList + argumentList?.let { + for (arg in it.arguments) { + ctor.addArgument(handle(arg.expression)) + } + } + newExpression.initializer = ctor + return newExpression + } } diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/ObjectCreationTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/ObjectCreationTest.kt new file mode 100644 index 00000000000..17dc3823230 --- /dev/null +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/ObjectCreationTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 de.fraunhofer.aisec.cpg.frontends.csharp.expressionhandler + +import de.fraunhofer.aisec.cpg.frontends.csharp.CSharpLanguage +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.expressions.Block +import de.fraunhofer.aisec.cpg.graph.expressions.Construction +import de.fraunhofer.aisec.cpg.graph.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.expressions.New +import de.fraunhofer.aisec.cpg.graph.types.IntegerType +import de.fraunhofer.aisec.cpg.test.* +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +class ObjectCreationTest : BaseTest() { + + @Test + fun objectCreationTest() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("ObjectCreation.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val bar = tu.records["Bar"] + assertNotNull(bar) + + val createFooMethod = bar.methods["createFoo"] + assertNotNull(createFooMethod) + val body = createFooMethod.body + assertIs(body) + + // Foo f = new Foo(42); + val fVariable = body.variables["f"] + assertNotNull(fVariable) + assertEquals("Foo", fVariable.type.name.localName) + + val initializer = fVariable.initializer + assertNotNull(initializer) + assertIs(initializer) + assertEquals("Foo", initializer.type.name.localName) + + val constructCall = initializer.initializer + assertNotNull(constructCall) + assertIs(constructCall) + val constructor = constructCall.constructor + assertNotNull(constructor) + assertEquals("Foo", constructor.name.localName) + + val args = constructCall.arguments + assertEquals(1, args.size) + val firstArg = args.firstOrNull() + assertNotNull(firstArg) + assertIs(firstArg.type) + assertIs>(firstArg) + assertEquals(42, firstArg.value) + } +} diff --git a/cpg-language-csharp/src/test/resources/csharp/ObjectCreation.cs b/cpg-language-csharp/src/test/resources/csharp/ObjectCreation.cs new file mode 100644 index 00000000000..41a84f64cb5 --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/ObjectCreation.cs @@ -0,0 +1,17 @@ +class Foo +{ + int x; + + Foo(int x) + { + this.x = x; + } +} + +class Bar +{ + void createFoo() + { + Foo f = new Foo(42); + } +} \ No newline at end of file From 77a34d399634cb167595927145f6ce762a7cf339 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Tue, 7 Apr 2026 09:18:42 +0200 Subject: [PATCH 42/55] Refactor JNA callings of field and variable declaration extraction --- .../src/main/csharp/NativeParser/Library.cs | 31 +++++-- .../aisec/cpg/frontends/csharp/CSharp.kt | 47 ++++++++--- .../frontends/csharp/DeclarationHandler.kt | 23 ++++- .../cpg/frontends/csharp/ExpressionHandler.kt | 65 +++++++++----- .../expressionhandler/ObjectCreationTest.kt | 84 ++++++++++++++++++- .../test/resources/csharp/ObjectCreation.cs | 13 ++- 6 files changed, 218 insertions(+), 45 deletions(-) diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 87df96bb16a..463104856bd 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -140,16 +140,10 @@ public static int GetNodeEndColumn(IntPtr handlePtr) { return Nodes[handlePtr].GetLocation().GetLineSpan().EndLinePosition.Character + 1; } - [UnmanagedCallersOnly(EntryPoint = "GetFieldVariableCount")] - public static int GetFieldVariableCount(IntPtr handlePtr) + [UnmanagedCallersOnly(EntryPoint = "GetFieldDeclaration")] + public static IntPtr GetFieldDeclaration(IntPtr handlePtr) { - return ((FieldDeclarationSyntax)Nodes[handlePtr]).Declaration.Variables.Count; - } - - [UnmanagedCallersOnly(EntryPoint = "GetFieldVariable")] - public static IntPtr GetFieldVariable(IntPtr handlePtr, int index) - { - return Register(((FieldDeclarationSyntax)Nodes[handlePtr]).Declaration.Variables[index]); + return Register(((FieldDeclarationSyntax)Nodes[handlePtr]).Declaration); } [UnmanagedCallersOnly(EntryPoint = "GetVariableDeclaratorIdentifier")] @@ -526,4 +520,23 @@ public static IntPtr GetBaseObjectCreationExpressionArgumentList(IntPtr handlePt var argumentList = ((BaseObjectCreationExpressionSyntax)Nodes[handlePtr]).ArgumentList; return argumentList != null ? Register(argumentList) : IntPtr.Zero; } + + [UnmanagedCallersOnly(EntryPoint = "GetBaseObjectCreationExpressionInitializer")] + public static IntPtr GetBaseObjectCreationExpressionInitializer(IntPtr handlePtr) + { + var initializer = ((BaseObjectCreationExpressionSyntax)Nodes[handlePtr]).Initializer; + return initializer != null ? Register(initializer) : IntPtr.Zero; + } + + [UnmanagedCallersOnly(EntryPoint = "GetInitializerExpressionExpressionsCount")] + public static int GetInitializerExpressionExpressionsCount(IntPtr handlePtr) + { + return ((InitializerExpressionSyntax)Nodes[handlePtr]).Expressions.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetInitializerExpressionExpression")] + public static IntPtr GetInitializerExpressionExpression(IntPtr handlePtr, int index) + { + return Register(((InitializerExpressionSyntax)Nodes[handlePtr]).Expressions[index]); + } } \ No newline at end of file diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 0ca34a9eba5..2debbdd8012 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -330,9 +330,8 @@ interface Csharp : Library { * class. */ class FieldDeclarationSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { - val variables: List by lazy { - val count = INSTANCE.GetFieldVariableCount(this) - (0 until count).map { i -> INSTANCE.GetFieldVariable(this, i) } + val declaration: VariableDeclarationSyntax by lazy { + INSTANCE.GetFieldDeclaration(this) } } @@ -415,6 +414,9 @@ interface Csharp : Library { "InvocationExpressionSyntax" -> InvocationExpressionSyntax(nativeValue) "ThisExpressionSyntax" -> ThisExpressionSyntax(nativeValue) "ObjectCreationExpressionSyntax" -> ObjectCreationExpressionSyntax(nativeValue) + "ImplicitObjectCreationExpressionSyntax" -> + ImplicitObjectCreationExpressionSyntax(nativeValue) + "InitializerExpressionSyntax" -> InitializerExpressionSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } } @@ -557,14 +559,16 @@ interface Csharp : Library { * Represents the Roslyn * [`BaseObjectCreationExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.baseobjectcreationexpressionsyntax) * class. This is the common base for [ObjectCreationExpressionSyntax] (explicit type, e.g. - * `new Foo(42)`) and `ImplicitObjectCreationExpressionSyntax` (target-typed, e.g. - * `new(42)`). + * `new Foo()`) and [ImplicitObjectCreationExpressionSyntax] (target-typed, e.g. `new()`). */ open class BaseObjectCreationExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { val argumentList: ArgumentListSyntax? by lazy { INSTANCE.GetBaseObjectCreationExpressionArgumentList(this) } + val initializer: InitializerExpressionSyntax? by lazy { + INSTANCE.GetBaseObjectCreationExpressionInitializer(this) + } } /** @@ -576,6 +580,21 @@ interface Csharp : Library { BaseObjectCreationExpressionSyntax(p) { val type: TypeSyntax by lazy { INSTANCE.GetObjectCreationExpressionType(this) } } + + class ImplicitObjectCreationExpressionSyntax(p: Pointer? = Pointer.NULL) : + BaseObjectCreationExpressionSyntax(p) + + /** + * Represents the Roslyn + * [`InitializerExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.initializerexpressionsyntax) + * class. + */ + class InitializerExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { + val expressions: List by lazy { + val count = INSTANCE.GetInitializerExpressionExpressionsCount(this) + (0 until count).map { i -> INSTANCE.GetInitializerExpressionExpression(this, i) } + } + } } /** @@ -620,12 +639,7 @@ interface Csharp : Library { index: Int, ): AST.MemberDeclarationSyntax - fun GetFieldVariableCount(handle: AST.FieldDeclarationSyntax): Int - - fun GetFieldVariable( - handle: AST.FieldDeclarationSyntax, - index: Int, - ): AST.VariableDeclaratorSyntax + fun GetFieldDeclaration(handle: AST.FieldDeclarationSyntax): AST.VariableDeclarationSyntax fun GetVariableDeclaratorIdentifier(handle: AST.VariableDeclaratorSyntax): String @@ -773,6 +787,17 @@ interface Csharp : Library { handle: AST.BaseObjectCreationExpressionSyntax ): AST.ArgumentListSyntax? + fun GetBaseObjectCreationExpressionInitializer( + handle: AST.BaseObjectCreationExpressionSyntax + ): AST.InitializerExpressionSyntax? + + fun GetInitializerExpressionExpressionsCount(handle: AST.InitializerExpressionSyntax): Int + + fun GetInitializerExpressionExpression( + handle: AST.InitializerExpressionSyntax, + index: Int, + ): AST.ExpressionSyntax + companion object { val INSTANCE: Csharp by lazy { try { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt index 9fc02fc6c5d..66c82265d42 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -26,6 +26,8 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.graph.declarations.* +import de.fraunhofer.aisec.cpg.graph.expressions.Construction +import de.fraunhofer.aisec.cpg.graph.expressions.New import de.fraunhofer.aisec.cpg.graph.implicit import de.fraunhofer.aisec.cpg.graph.newConstructor import de.fraunhofer.aisec.cpg.graph.newField @@ -34,6 +36,7 @@ import de.fraunhofer.aisec.cpg.graph.newNamespace import de.fraunhofer.aisec.cpg.graph.newParameter import de.fraunhofer.aisec.cpg.graph.newRecord import de.fraunhofer.aisec.cpg.graph.newVariable +import de.fraunhofer.aisec.cpg.graph.parseName import de.fraunhofer.aisec.cpg.graph.unknownType class DeclarationHandler(frontend: CSharpLanguageFrontend) : @@ -84,8 +87,26 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : for (member in node.members) { when (member) { is Csharp.AST.FieldDeclarationSyntax -> { - for (variable in member.variables) { + val declaration = member.declaration + val fieldType = frontend.typeOf(declaration.type) + for (variable in declaration.variables) { val field = newField(variable.identifier, rawNode = member) + field.type = fieldType + variable.initializer?.let { + field.initializer = frontend.expressionHandler.handle(it) + } + // TODO: Not sure if this is correct: For implicit constructor calls (e.g. + // new()), we need to + // propagate the field type to the "New" and "Construction" nodes + val newExpr = field.initializer as? New + if (newExpr != null) { + newExpr.type = fieldType + val construction = newExpr.initializer as? Construction + if (construction != null) { + construction.type = fieldType + construction.name = parseName(fieldType.name.localName) + } + } frontend.scopeManager.addDeclaration(field) record.addDeclaration(field) } diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt index 7138cfc21d0..3b4e5bd9230 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt @@ -25,19 +25,12 @@ */ package de.fraunhofer.aisec.cpg.frontends.csharp -import de.fraunhofer.aisec.cpg.graph.expressions.Assign -import de.fraunhofer.aisec.cpg.graph.expressions.BinaryOperator -import de.fraunhofer.aisec.cpg.graph.expressions.Call -import de.fraunhofer.aisec.cpg.graph.expressions.Expression -import de.fraunhofer.aisec.cpg.graph.expressions.Literal -import de.fraunhofer.aisec.cpg.graph.expressions.MemberAccess -import de.fraunhofer.aisec.cpg.graph.expressions.New -import de.fraunhofer.aisec.cpg.graph.expressions.ProblemExpression -import de.fraunhofer.aisec.cpg.graph.expressions.Reference +import de.fraunhofer.aisec.cpg.graph.expressions.* import de.fraunhofer.aisec.cpg.graph.newAssign import de.fraunhofer.aisec.cpg.graph.newBinaryOperator import de.fraunhofer.aisec.cpg.graph.newCall import de.fraunhofer.aisec.cpg.graph.newConstruction +import de.fraunhofer.aisec.cpg.graph.newInitializerList import de.fraunhofer.aisec.cpg.graph.newLiteral import de.fraunhofer.aisec.cpg.graph.newMemberAccess import de.fraunhofer.aisec.cpg.graph.newMemberCall @@ -58,7 +51,8 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : is Csharp.AST.InvocationExpressionSyntax -> handleInvocationExpression(node) is Csharp.AST.MemberAccessExpressionSyntax -> handleMemberAccessExpression(node) is Csharp.AST.ThisExpressionSyntax -> handleThisExpression(node) - is Csharp.AST.ObjectCreationExpressionSyntax -> handleObjectCreationExpression(node) + is Csharp.AST.BaseObjectCreationExpressionSyntax -> handleObjectCreationExpression(node) + is Csharp.AST.InitializerExpressionSyntax -> handleInitializerExpression(node) else -> ProblemExpression("Not supported: ${node.csharpType}") } } @@ -187,27 +181,56 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : } /** - * Translates an [ObjectCreationExpressionSyntax][Csharp.AST.ObjectCreationExpressionSyntax] - * into a [New] with a [Construction][de.fraunhofer.aisec.cpg.graph.expressions.Construction] - * initializer. + * Translates a + * [BaseObjectCreationExpressionSyntax][Csharp.AST.BaseObjectCreationExpressionSyntax] into a + * [New] with a [Construction][Construction] initializer. Handles both explicit (`new Foo()`) + * and implicit (`new()`) object creations. For implicit cases, the type is [unknownType] and + * will be resolved later. * * C# spec: * [Object creation expressions](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#128172-object-creation-expressions) */ private fun handleObjectCreationExpression( - node: Csharp.AST.ObjectCreationExpressionSyntax + node: Csharp.AST.BaseObjectCreationExpressionSyntax ): New { - val type = frontend.typeOf(node.type) + val type = + if (node is Csharp.AST.ObjectCreationExpressionSyntax) { + frontend.typeOf(node.type) + } else { + unknownType() + } val newExpression = newNew(type, rawNode = node) - val ctor = newConstruction(type.name.localName, rawNode = node) - ctor.type = type + val construction = newConstruction(type.name.localName, rawNode = node) + construction.type = type val argumentList = node.argumentList - argumentList?.let { - for (arg in it.arguments) { - ctor.addArgument(handle(arg.expression)) + if (argumentList != null) { + for (arg in argumentList.arguments) { + construction.addArgument(handle(arg.expression)) } } - newExpression.initializer = ctor + newExpression.initializer = construction + val objectInitializer = node.initializer + if (objectInitializer != null) { + handle(objectInitializer) + } return newExpression } + + /** + * Translates an [InitializerExpressionSyntax][Csharp.AST.InitializerExpressionSyntax] into an + * [InitializerList]. + * + * C# spec: + * [Object initializers](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#1281722-object-initializers) + */ + // For arrays + private fun handleInitializerExpression( + node: Csharp.AST.InitializerExpressionSyntax + ): InitializerList { + val initializerList = newInitializerList(rawNode = node) + for (expr in node.expressions) { + initializerList.addArgument(handle(expr)) + } + return initializerList + } } diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/ObjectCreationTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/ObjectCreationTest.kt index 17dc3823230..980677cf5fb 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/ObjectCreationTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/ObjectCreationTest.kt @@ -62,7 +62,7 @@ class ObjectCreationTest : BaseTest() { val body = createFooMethod.body assertIs(body) - // Foo f = new Foo(42); + // Foo f = new Foo(1); val fVariable = body.variables["f"] assertNotNull(fVariable) assertEquals("Foo", fVariable.type.name.localName) @@ -85,6 +85,86 @@ class ObjectCreationTest : BaseTest() { assertNotNull(firstArg) assertIs(firstArg.type) assertIs>(firstArg) - assertEquals(42, firstArg.value) + assertEquals(1, firstArg.value) + } + + @Test + fun implicitObjectCreationTest() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("ObjectCreation.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + // class Baz { Foo foo = new(1); } + val baz = tu.records["Baz"] + assertNotNull(baz) + + val fooClass = tu.records["Foo"] + assertNotNull(fooClass) + + val fooField = baz.fields["foo"] + assertNotNull(fooField) + + val newNode = fooField.initializer + assertNotNull(newNode) + assertIs(newNode) + val constructCall = newNode.initializer + assertNotNull(constructCall) + assertIs(constructCall) + val constructor = constructCall.constructor + assertNotNull(constructor) + assertEquals("Foo", constructor.name.localName) + val instantiates = constructCall.instantiates + assertNotNull(instantiates) + assertEquals(fooClass, instantiates) + + val args = constructCall.arguments + assertEquals(1, args.size) + val firstArg = args.firstOrNull() + assertNotNull(firstArg) + assertIs>(firstArg) + assertEquals(1, firstArg.value) + } + + @Test + fun objectCreationWithInitializerTest() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("ObjectCreation.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + // Foo f = new Foo(1) { x = 2 }; + val fooBarClass = tu.records["FooBar"] + assertNotNull(fooBarClass) + + val fVariable = tu.variables["f"] + assertNotNull(fVariable) + + val newNode = fVariable.initializer + assertNotNull(newNode) + assertIs(newNode) + + val constructCall = newNode.initializer + assertNotNull(constructCall) + assertIs(constructCall) + + val args = constructCall.arguments + assertEquals(1, args.size) + val firstArg = args.firstOrNull() + assertNotNull(firstArg) + assertIs>(firstArg) + assertEquals(1, firstArg.value) } } diff --git a/cpg-language-csharp/src/test/resources/csharp/ObjectCreation.cs b/cpg-language-csharp/src/test/resources/csharp/ObjectCreation.cs index 41a84f64cb5..fb5951b349e 100644 --- a/cpg-language-csharp/src/test/resources/csharp/ObjectCreation.cs +++ b/cpg-language-csharp/src/test/resources/csharp/ObjectCreation.cs @@ -12,6 +12,17 @@ class Bar { void createFoo() { - Foo f = new Foo(42); + Foo f = new Foo(1); } +} + +class Baz +{ + // implicit constructor call + Foo foo = new(1); +} + +class FooBar +{ + Foo f = new Foo(1) { x = 2 }; } \ No newline at end of file From e8babf93b0ba71b3d7a2fe8025f0283a2a498212 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Tue, 7 Apr 2026 15:42:23 +0200 Subject: [PATCH 43/55] Add object initializer workaround handling --- .../cpg/frontends/csharp/ExpressionHandler.kt | 107 +++++++++++++++--- .../expressionhandler/ObjectCreationTest.kt | 64 +++++++++-- 2 files changed, 146 insertions(+), 25 deletions(-) diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt index 3b4e5bd9230..6cc6c73d44f 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt @@ -25,18 +25,22 @@ */ package de.fraunhofer.aisec.cpg.frontends.csharp +import de.fraunhofer.aisec.cpg.graph.Name import de.fraunhofer.aisec.cpg.graph.expressions.* +import de.fraunhofer.aisec.cpg.graph.implicit import de.fraunhofer.aisec.cpg.graph.newAssign import de.fraunhofer.aisec.cpg.graph.newBinaryOperator import de.fraunhofer.aisec.cpg.graph.newCall import de.fraunhofer.aisec.cpg.graph.newConstruction -import de.fraunhofer.aisec.cpg.graph.newInitializerList +import de.fraunhofer.aisec.cpg.graph.newDeclarationStatement +import de.fraunhofer.aisec.cpg.graph.newExpressionList import de.fraunhofer.aisec.cpg.graph.newLiteral import de.fraunhofer.aisec.cpg.graph.newMemberAccess import de.fraunhofer.aisec.cpg.graph.newMemberCall import de.fraunhofer.aisec.cpg.graph.newNew import de.fraunhofer.aisec.cpg.graph.newProblemExpression import de.fraunhofer.aisec.cpg.graph.newReference +import de.fraunhofer.aisec.cpg.graph.newVariable import de.fraunhofer.aisec.cpg.graph.objectType import de.fraunhofer.aisec.cpg.graph.unknownType @@ -52,7 +56,6 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : is Csharp.AST.MemberAccessExpressionSyntax -> handleMemberAccessExpression(node) is Csharp.AST.ThisExpressionSyntax -> handleThisExpression(node) is Csharp.AST.BaseObjectCreationExpressionSyntax -> handleObjectCreationExpression(node) - is Csharp.AST.InitializerExpressionSyntax -> handleInitializerExpression(node) else -> ProblemExpression("Not supported: ${node.csharpType}") } } @@ -187,12 +190,15 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : * and implicit (`new()`) object creations. For implicit cases, the type is [unknownType] and * will be resolved later. * + * If the expression has an object initializer (e.g. `new Foo(1) { X = 2, Y = 3 }`), the result + * is wrapped in an [ExpressionList] via [handleObjectInitializer]. + * * C# spec: * [Object creation expressions](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#128172-object-creation-expressions) */ private fun handleObjectCreationExpression( node: Csharp.AST.BaseObjectCreationExpressionSyntax - ): New { + ): Expression { val type = if (node is Csharp.AST.ObjectCreationExpressionSyntax) { frontend.typeOf(node.type) @@ -209,28 +215,97 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : } } newExpression.initializer = construction - val objectInitializer = node.initializer - if (objectInitializer != null) { - handle(objectInitializer) + + val initializer = node.initializer + if (initializer != null) { + return handleObjectInitializer(initializer, newExpression, type) } return newExpression } /** - * Translates an [InitializerExpressionSyntax][Csharp.AST.InitializerExpressionSyntax] into an - * [InitializerList]. + * Handles an object initializer by wrapping the expressions in an [ExpressionList]. Since we do + * not have a direct representation for object initializers, we destructure them into an + * equivalent form. For example: + * ```csharp + * var p = new Point(1) { X = 0, Y = 1 }; + * ``` + * + * is equivalent to: + * ```csharp + * Point __tmp = new Point(1); + * __tmp.X = 0; + * __tmp.Y = 1; + * var p = __tmp; + * ``` + * + * The [ExpressionList] contains: + * 1. A [DeclarationStatement] with an implicit temporary variable: `__tmp = new Point(1)` + * 2. Implicit [Assign] statements for each member: `__tmp.X = 0`, `__tmp.Y = 1` + * 3. A [Reference] to the temporary variable * * C# spec: * [Object initializers](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#1281722-object-initializers) */ - // For arrays - private fun handleInitializerExpression( - node: Csharp.AST.InitializerExpressionSyntax - ): InitializerList { - val initializerList = newInitializerList(rawNode = node) - for (expr in node.expressions) { - initializerList.addArgument(handle(expr)) + private fun handleObjectInitializer( + initializer: Csharp.AST.InitializerExpressionSyntax, + newExpression: New, + type: de.fraunhofer.aisec.cpg.graph.types.Type, + ): ExpressionList { + val exprList = newExpressionList() + // Create an implicit temporary variable to hold the new object + val tmpName = Name.temporary(prefix = type.name.localName, separatorChar = '_', exprList) + val tmpVar = newVariable(name = tmpName, type = type).implicit() + tmpVar.initializer = newExpression + frontend.scopeManager.addDeclaration(tmpVar) + val declStmt = newDeclarationStatement().implicit() + declStmt.addDeclaration(tmpVar) + exprList.expressions += declStmt + + // Each assignment in the initializer is translated to a member access on the tmp variable + for (expr in initializer.expressions) { + if (expr is Csharp.AST.AssignmentExpressionSyntax) { + val memberAccess = + newMemberAccess( + name = + (expr.left as? Csharp.AST.IdentifierNameSyntax)?.identifier + ?: "obj", + base = newReference(name = tmpName).implicit(), + ) + .implicit(code = newExpression.code, location = newExpression.location) + val assign = + newAssign( + operatorCode = "=", + lhs = listOf(memberAccess), + rhs = listOf(handle(expr.right)), + ) + .implicit(code = newExpression.code, location = newExpression.location) + exprList.expressions += assign + } } - return initializerList + + // Add a reference to the temporary variable + exprList.expressions += newReference(name = tmpName).implicit() + return exprList } + + // /** + // * Translates an [InitializerExpressionSyntax][Csharp.AST.InitializerExpressionSyntax] + // into an + // * [InitializerList]. + // * + // * C# spec: + // * [Object + // initializers](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#1281722-object-initializers) + // */ + // // For arrays + // private fun handleInitializerExpression( + // node: Csharp.AST.InitializerExpressionSyntax + // ): InitializerList { + // val initializerList = newInitializerList(rawNode = node) + // for (expr in node.expressions) { + // initializerList.addArgument(handle(expr)) + // } + // return initializerList + // } } diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/ObjectCreationTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/ObjectCreationTest.kt index 980677cf5fb..e29742aedba 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/ObjectCreationTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/ObjectCreationTest.kt @@ -27,10 +27,16 @@ package de.fraunhofer.aisec.cpg.frontends.csharp.expressionhandler import de.fraunhofer.aisec.cpg.frontends.csharp.CSharpLanguage import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.declarations.Variable +import de.fraunhofer.aisec.cpg.graph.expressions.Assign import de.fraunhofer.aisec.cpg.graph.expressions.Block import de.fraunhofer.aisec.cpg.graph.expressions.Construction +import de.fraunhofer.aisec.cpg.graph.expressions.DeclarationStatement +import de.fraunhofer.aisec.cpg.graph.expressions.ExpressionList import de.fraunhofer.aisec.cpg.graph.expressions.Literal +import de.fraunhofer.aisec.cpg.graph.expressions.MemberAccess import de.fraunhofer.aisec.cpg.graph.expressions.New +import de.fraunhofer.aisec.cpg.graph.expressions.Reference import de.fraunhofer.aisec.cpg.graph.types.IntegerType import de.fraunhofer.aisec.cpg.test.* import java.nio.file.Path @@ -38,6 +44,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs import kotlin.test.assertNotNull +import kotlin.test.assertTrue class ObjectCreationTest : BaseTest() { @@ -149,22 +156,61 @@ class ObjectCreationTest : BaseTest() { val fooBarClass = tu.records["FooBar"] assertNotNull(fooBarClass) - val fVariable = tu.variables["f"] + val fVariable = fooBarClass.fields["f"] assertNotNull(fVariable) - val newNode = fVariable.initializer + // The initializer as an ExpressionList + val exprList = fVariable.initializer + assertNotNull(exprList) + assertIs(exprList) + assertEquals(3, exprList.expressions.size) + + // 1. DeclarationStatement with implicit tmp variable: __tmp = new Foo(1) + val declStmt = exprList.expressions[0] + assertIs(declStmt) + assertTrue(declStmt.isImplicit) + + val tmpVar = declStmt.declarations.firstOrNull() + assertNotNull(tmpVar) + assertTrue(tmpVar.isImplicit) + assertIs(tmpVar) + + val newNode = tmpVar.initializer assertNotNull(newNode) assertIs(newNode) val constructCall = newNode.initializer assertNotNull(constructCall) assertIs(constructCall) - - val args = constructCall.arguments - assertEquals(1, args.size) - val firstArg = args.firstOrNull() - assertNotNull(firstArg) - assertIs>(firstArg) - assertEquals(1, firstArg.value) + assertEquals(1, constructCall.arguments.size) + val constructArg = constructCall.arguments.firstOrNull() + assertNotNull(constructArg) + assertIs>(constructArg) + assertEquals(1, constructArg.value) + + // 2. Implicit assign: __tmp.x = 2 + val assign = exprList.expressions[1] + assertIs(assign) + assertTrue(assign.isImplicit) + + val memberAccess = assign.lhs.firstOrNull() + assertIs(memberAccess) + assertTrue(memberAccess.isImplicit) + assertEquals("x", memberAccess.name.localName) + + val base = memberAccess.base + assertIs(base) + assertTrue(base.isImplicit) + assertRefersTo(base, tmpVar) + + val rhs = assign.rhs.firstOrNull() + assertIs>(rhs) + assertEquals(2, rhs.value) + + // 3. Reference to the tmp variable + val finalRef = exprList.expressions[2] + assertIs(finalRef) + assertTrue(finalRef.isImplicit) + assertRefersTo(finalRef, tmpVar) } } From ff146d7c20718a460bf35f2bbf1e6a3ab1ee8022 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Tue, 7 Apr 2026 16:02:21 +0200 Subject: [PATCH 44/55] Replace neo4j transient annotation with DoNotPersist --- .../fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt index 46a83b230f7..dfe07fbc485 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguage.kt @@ -34,15 +34,15 @@ import de.fraunhofer.aisec.cpg.graph.types.NumericType import de.fraunhofer.aisec.cpg.graph.types.ObjectType import de.fraunhofer.aisec.cpg.graph.types.StringType import de.fraunhofer.aisec.cpg.graph.types.Type +import de.fraunhofer.aisec.cpg.persistence.DoNotPersist import kotlin.reflect.KClass -import org.neo4j.ogm.annotation.Transient class CSharpLanguage : Language(), HasImplicitReceiver { override val receiverName = "this" override val fileExtensions = listOf("cs") override val namespaceDelimiter = "." - @Transient + @DoNotPersist override val frontend: KClass = CSharpLanguageFrontend::class /** From dca05a5c345f4cf351d8f38e3cdd454b89fa7ea7 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Tue, 7 Apr 2026 16:04:50 +0200 Subject: [PATCH 45/55] Add log4j2.xml file --- cpg-language-csharp/src/test/resources/log4j2.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 cpg-language-csharp/src/test/resources/log4j2.xml diff --git a/cpg-language-csharp/src/test/resources/log4j2.xml b/cpg-language-csharp/src/test/resources/log4j2.xml new file mode 100644 index 00000000000..5b73082e2c0 --- /dev/null +++ b/cpg-language-csharp/src/test/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file From 4123ff83dc82398d471f5f21e24c286f638bf55c Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Thu, 9 Apr 2026 10:33:25 +0200 Subject: [PATCH 46/55] Add ObjectInitializer handling --- .../aisec/cpg/frontends/csharp/CSharp.kt | 25 +- .../cpg/frontends/csharp/ExpressionHandler.kt | 234 +++++++++++++++--- .../CollectionInitializerTest.kt | 192 ++++++++++++++ .../expressionhandler/ObjectCreationTest.kt | 215 +++++++++++++++- .../resources/csharp/CollectionInitializer.cs | 9 + .../test/resources/csharp/ObjectCreation.cs | 37 +++ 6 files changed, 668 insertions(+), 44 deletions(-) create mode 100644 cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/CollectionInitializerTest.kt create mode 100644 cpg-language-csharp/src/test/resources/csharp/CollectionInitializer.cs diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 2debbdd8012..e5bece0508d 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -416,7 +416,16 @@ interface Csharp : Library { "ObjectCreationExpressionSyntax" -> ObjectCreationExpressionSyntax(nativeValue) "ImplicitObjectCreationExpressionSyntax" -> ImplicitObjectCreationExpressionSyntax(nativeValue) - "InitializerExpressionSyntax" -> InitializerExpressionSyntax(nativeValue) + "InitializerExpressionSyntax" -> + when (INSTANCE.GetKind(nativeValue)) { + "ObjectInitializerExpression" -> + ObjectInitializerExpressionSyntax(nativeValue) + "CollectionInitializerExpression" -> + CollectionInitializerExpressionSyntax(nativeValue) + "ComplexElementInitializerExpression" -> + ComplexElementInitializerExpressionSyntax(nativeValue) + else -> InitializerExpressionSyntax(nativeValue) + } else -> super.fromNative(nativeValue, context) } } @@ -589,12 +598,24 @@ interface Csharp : Library { * [`InitializerExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.initializerexpressionsyntax) * class. */ - class InitializerExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { + open class InitializerExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionSyntax(p) { val expressions: List by lazy { val count = INSTANCE.GetInitializerExpressionExpressionsCount(this) (0 until count).map { i -> INSTANCE.GetInitializerExpressionExpression(this, i) } } } + + /** An object initializer, e.g. `{ X = 0, Y = 1 }`. */ + class ObjectInitializerExpressionSyntax(p: Pointer? = Pointer.NULL) : + InitializerExpressionSyntax(p) + + /** A collection initializer, e.g. `{ 0, 1, 2 }`. */ + class CollectionInitializerExpressionSyntax(p: Pointer? = Pointer.NULL) : + InitializerExpressionSyntax(p) + + /** A complex element initializer, e.g. `{ "A", 1 }`. */ + class ComplexElementInitializerExpressionSyntax(p: Pointer? = Pointer.NULL) : + InitializerExpressionSyntax(p) } /** diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt index 6cc6c73d44f..d97b68cf51d 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/ExpressionHandler.kt @@ -117,12 +117,16 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : return when (node) { is Csharp.AST.NumericLiteralExpressionSyntax -> newLiteral(node.value.toInt(), builtInTypes.getValue("int"), rawNode = node) + is Csharp.AST.StringLiteralExpressionSyntax -> newLiteral(node.value, builtInTypes.getValue("string"), rawNode = node) + is Csharp.AST.BooleanLiteralExpressionSyntax -> newLiteral(node.value.toBoolean(), builtInTypes.getValue("bool"), rawNode = node) + is Csharp.AST.CharacterLiteralExpressionSyntax -> newLiteral(node.value.single(), builtInTypes.getValue("char"), rawNode = node) + is Csharp.AST.NullLiteralExpressionSyntax -> newLiteral(null, objectType("null"), rawNode = node) // TODO: Return unknownType()? @@ -132,8 +136,8 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : /** * Translates an [InvocationExpressionSyntax][Csharp.AST.InvocationExpressionSyntax] into a - * [Call]. If the callee is a [MemberAccess] (e.g. `obj.Method()`), a `MemberCall` is created. - * Otherwise (e.g. `Foo()`), a plain [Call] is created. + * [Call]. If the callee is a [MemberAccess] (e.g. `member.Method()`), a `MemberCall` is + * created. Otherwise (e.g. `Foo()`), a [Call] is created. * * C# spec: * [Invocation expressions](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#12810-invocation-expressions) @@ -218,7 +222,13 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : val initializer = node.initializer if (initializer != null) { - return handleObjectInitializer(initializer, newExpression, type) + return when (initializer) { + is Csharp.AST.ObjectInitializerExpressionSyntax -> + handleObjectInitializer(initializer, newExpression, type) + is Csharp.AST.CollectionInitializerExpressionSyntax -> + handleCollectionInitializer(initializer, newExpression, type) + else -> newExpression + } } return newExpression } @@ -257,55 +267,211 @@ class ExpressionHandler(frontend: CSharpLanguageFrontend) : val tmpName = Name.temporary(prefix = type.name.localName, separatorChar = '_', exprList) val tmpVar = newVariable(name = tmpName, type = type).implicit() tmpVar.initializer = newExpression - frontend.scopeManager.addDeclaration(tmpVar) val declStmt = newDeclarationStatement().implicit() declStmt.addDeclaration(tmpVar) exprList.expressions += declStmt - // Each assignment in the initializer is translated to a member access on the tmp variable for (expr in initializer.expressions) { if (expr is Csharp.AST.AssignmentExpressionSyntax) { + val memberName = (expr.left as? Csharp.AST.IdentifierNameSyntax)?.identifier + val baseRef = newReference(name = tmpName).implicit() + baseRef.refersTo = tmpVar val memberAccess = - newMemberAccess( - name = - (expr.left as? Csharp.AST.IdentifierNameSyntax)?.identifier - ?: "obj", - base = newReference(name = tmpName).implicit(), + newMemberAccess(name = memberName, base = baseRef) + .implicit(code = newExpression.code, location = newExpression.location) + + if (expr.right is Csharp.AST.ObjectInitializerExpressionSyntax) { + // Nested object initializer: member = { X = 0, Y = 1 } + // -> __tmp.member.X = 0, __tmp.member.Y = 1 + exprList.expressions += + handleNestedObjectInitializer( + expr.right as Csharp.AST.ObjectInitializerExpressionSyntax, + memberAccess, + newExpression, + ) + } else if (expr.right is Csharp.AST.CollectionInitializerExpressionSyntax) { + // Nested collection initializer: member = { "a", "b" } + // -> __tmp.member.Add("a"), __tmp.member.Add("b") + exprList.expressions += + handleNestedCollectionInitializer( + expr.right as Csharp.AST.CollectionInitializerExpressionSyntax, + memberAccess, + newExpression, ) + } else { + val assign = + newAssign( + operatorCode = "=", + lhs = listOf(memberAccess), + rhs = listOf(handle(expr.right)), + ) + .implicit(code = newExpression.code, location = newExpression.location) + exprList.expressions += assign + } + } + } + + // Add a reference to the temporary variable + exprList.expressions += + newReference(name = tmpName).implicit().apply { this.refersTo = tmpVar } + return exprList + } + + /** + * Handles a nested object initializer where no new object is created. Instead, the members are + * only initialized. For example: + * ```csharp + * Rectangle r = new Rectangle { P1 = { X = 0, Y = 1 } }; + * ``` + * + * is equivalent to: + * ```csharp + * Rectangle __tmp = new Rectangle(); + * __tmp.P1.X = 0; + * __tmp.P1.Y = 1; + * Rectangle r = __tmp; + * ``` + * + * C# spec: + * [Object initializers](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#1281722-object-initializers) + */ + private fun handleNestedObjectInitializer( + initializer: Csharp.AST.ObjectInitializerExpressionSyntax, + memberAccess: MemberAccess, + newExpression: New, + ): List { + val assigns = mutableListOf() + for (innerExpr in initializer.expressions) { + if (innerExpr is Csharp.AST.AssignmentExpressionSyntax) { + val innerName = (innerExpr.left as? Csharp.AST.IdentifierNameSyntax)?.identifier + val innerAccess = + newMemberAccess(name = innerName, base = memberAccess) .implicit(code = newExpression.code, location = newExpression.location) val assign = newAssign( operatorCode = "=", - lhs = listOf(memberAccess), - rhs = listOf(handle(expr.right)), + lhs = listOf(innerAccess), + rhs = listOf(handle(innerExpr.right)), ) .implicit(code = newExpression.code, location = newExpression.location) - exprList.expressions += assign + assigns += assign } } + return assigns + } - // Add a reference to the temporary variable - exprList.expressions += newReference(name = tmpName).implicit() + /** + * Handles a [Csharp.AST.CollectionInitializerExpressionSyntax] by wrapping the expressions in + * an [ExpressionList]. Each element in the initializer is translated into an implicit `Add` + * call on a temporary variable. For example: + * ```csharp + * var list = new List { 0, 1, 2 }; + * ``` + * + * is equivalent to: + * ```csharp + * List __tmp = new List(); + * __tmp.Add(0); + * __tmp.Add(1); + * __tmp.Add(2); + * var list = __tmp; + * ``` + * + * For [Csharp.AST.ComplexElementInitializerExpressionSyntax], each `{ key, value }` element is + * translated into an `Add(key, value)` call: + * ```csharp + * var map = new Map { { "a", 1 }, { "b", 2 } }; + * ``` + * + * is equivalent to: + * ```csharp + * Map __tmp = new Map(); + * __tmp.Add("a", 1); + * __tmp.Add("b", 2); + * var map = __tmp; + * ``` + * + * C# spec: + * [Collection initializers](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#1281723-collection-initializers) + */ + private fun handleCollectionInitializer( + initializer: Csharp.AST.CollectionInitializerExpressionSyntax, + newExpression: New, + type: de.fraunhofer.aisec.cpg.graph.types.Type, + ): ExpressionList { + val exprList = newExpressionList() + val tmpName = Name.temporary(prefix = type.name.localName, separatorChar = '_', exprList) + val tmpVar = newVariable(name = tmpName, type = type).implicit() + tmpVar.initializer = newExpression + val declStmt = newDeclarationStatement().implicit() + declStmt.addDeclaration(tmpVar) + exprList.expressions += declStmt + + for (expr in initializer.expressions) { + val baseRef = newReference(name = tmpName).implicit() + baseRef.refersTo = tmpVar + val addCall = + newMemberCall( + newMemberAccess(name = "Add", base = baseRef) + .implicit(code = newExpression.code, location = newExpression.location) + ) + .implicit(code = newExpression.code, location = newExpression.location) + if (expr is Csharp.AST.ComplexElementInitializerExpressionSyntax) { + // For example { "a", 1 } -> Add("a", 1) + for (arg in expr.expressions) { + addCall.addArgument(handle(arg)) + } + } else { + // Simple element, e.g. 0 -> Add(0) + addCall.addArgument(handle(expr)) + } + exprList.expressions += addCall + } + + exprList.expressions += + newReference(name = tmpName).implicit().apply { this.refersTo = tmpVar } return exprList } - // /** - // * Translates an [InitializerExpressionSyntax][Csharp.AST.InitializerExpressionSyntax] - // into an - // * [InitializerList]. - // * - // * C# spec: - // * [Object - // initializers](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#1281722-object-initializers) - // */ - // // For arrays - // private fun handleInitializerExpression( - // node: Csharp.AST.InitializerExpressionSyntax - // ): InitializerList { - // val initializerList = newInitializerList(rawNode = node) - // for (expr in node.expressions) { - // initializerList.addArgument(handle(expr)) - // } - // return initializerList - // } + /** + * Handles a nested collection initializer where elements are added to an already existing + * member via implicit `Add` calls. For example: + * ```csharp + * new Foo { Items = { 0, 1, 2 } } + * ``` + * + * is equivalent to: + * ```csharp + * __tmp.Items.Add(0); + * __tmp.Items.Add(1); + * __tmp.Items.Add(2); + * ``` + * + * C# spec: + * [Collection initializers](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#1281723-collection-initializers) + */ + private fun handleNestedCollectionInitializer( + initializer: Csharp.AST.CollectionInitializerExpressionSyntax, + memberAccess: MemberAccess, + newExpression: New, + ): List { + val calls = mutableListOf() + for (expr in initializer.expressions) { + val addCall = + newMemberCall( + newMemberAccess(name = "Add", base = memberAccess) + .implicit(code = newExpression.code, location = newExpression.location) + ) + .implicit(code = newExpression.code, location = newExpression.location) + if (expr is Csharp.AST.ComplexElementInitializerExpressionSyntax) { + for (arg in expr.expressions) { + addCall.addArgument(handle(arg)) + } + } else { + addCall.addArgument(handle(expr)) + } + calls += addCall + } + return calls + } } diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/CollectionInitializerTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/CollectionInitializerTest.kt new file mode 100644 index 00000000000..2234cc7c588 --- /dev/null +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/CollectionInitializerTest.kt @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 de.fraunhofer.aisec.cpg.frontends.csharp.expressionhandler + +import de.fraunhofer.aisec.cpg.frontends.csharp.CSharpLanguage +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.declarations.Variable +import de.fraunhofer.aisec.cpg.graph.expressions.DeclarationStatement +import de.fraunhofer.aisec.cpg.graph.expressions.ExpressionList +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.New +import de.fraunhofer.aisec.cpg.graph.expressions.Reference +import de.fraunhofer.aisec.cpg.test.* +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class CollectionInitializerTest : BaseTest() { + + @Test + fun collectionInitializerTest() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("CollectionInitializer.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + /** + * List numbers = new List { 0, 1, 2 }; + * + * translated into: List __tmp = new List(); __tmp.Add(0); __tmp.Add(1); + * __tmp.Add(2); List numbers = __tmp; + */ + val simpleClass = tu.records["CollectionInitializer,"] + assertNotNull(simpleClass) + + val numbersField = simpleClass.fields["numbers"] + assertNotNull(numbersField) + + val exprList = numbersField.initializer + assertNotNull(exprList) + assertIs(exprList) + assertEquals(5, exprList.expressions.size) + + // DeclarationStatement: __tmp = new List() + val declStmt = exprList.expressions[0] + assertIs(declStmt) + assertTrue(declStmt.isImplicit) + val tmpVar = declStmt.declarations.firstOrNull() + assertNotNull(tmpVar) + assertIs(tmpVar) + val newNode = tmpVar.initializer + assertNotNull(newNode) + assertIs(newNode) + + // __tmp.Add(0) + val add0 = exprList.expressions[1] + assertIs(add0) + assertTrue(add0.isImplicit) + assertEquals("Add", add0.name.localName) + val add0Base = (add0.callee as MemberAccess).base + assertIs(add0Base) + assertRefersTo(add0Base, tmpVar) + assertEquals(1, add0.arguments.size) + val arg0 = add0.arguments[0] + assertIs>(arg0) + assertEquals(0, arg0.value) + + // __tmp.Add(1) + val add1 = exprList.expressions[2] + assertIs(add1) + assertEquals("Add", add1.name.localName) + assertEquals(1, add1.arguments.size) + val arg1 = add1.arguments[0] + assertIs>(arg1) + assertEquals(1, arg1.value) + + // __tmp.Add(2) + val add2 = exprList.expressions[3] + assertIs(add2) + assertEquals("Add", add2.name.localName) + assertEquals(1, add2.arguments.size) + val arg2 = add2.arguments[0] + assertIs>(arg2) + assertEquals(2, arg2.value) + + // Reference to __tmp + val ref = exprList.expressions[4] + assertIs(ref) + assertRefersTo(ref, tmpVar) + } + + @Test + fun complexCollectionInitializerTest() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("CollectionInitializer.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + /** + * Dictionary map = new Dictionary { { "a", 1 }, { "b", 2 } }; + * + * translated into: Dictionary __tmp = new Dictionary(); + * __tmp.Add("a", 1); __tmp.Add("b", 2); Dictionary map = __tmp; + */ + val dictClass = tu.records["ComplexCollectionInitializer"] + assertNotNull(dictClass) + + val mapField = dictClass.fields["map"] + assertNotNull(mapField) + + val exprList = mapField.initializer + assertNotNull(exprList) + assertIs(exprList) + assertEquals(4, exprList.expressions.size) + + // DeclarationStatement: __tmp = new Dictionary() + val declStmt = exprList.expressions[0] + assertIs(declStmt) + val tmpVar = declStmt.declarations.firstOrNull() + assertNotNull(tmpVar) + assertIs(tmpVar) + + // __tmp.Add("a", 1) + val addA = exprList.expressions[1] + assertIs(addA) + assertEquals("Add", addA.name.localName) + assertEquals(2, addA.arguments.size) + val keyA = addA.arguments[0] + assertIs>(keyA) + assertEquals("a", keyA.value) + val valA = addA.arguments[1] + assertIs>(valA) + assertEquals(1, valA.value) + + // __tmp.Add("b", 2) + val addB = exprList.expressions[2] + assertIs(addB) + assertEquals("Add", addB.name.localName) + assertEquals(2, addB.arguments.size) + val keyB = addB.arguments[0] + assertIs>(keyB) + assertEquals("b", keyB.value) + val valB = addB.arguments[1] + assertIs>(valB) + assertEquals(2, valB.value) + + // Reference to __tmp + val ref = exprList.expressions[3] + assertIs(ref) + assertRefersTo(ref, tmpVar) + } +} diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/ObjectCreationTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/ObjectCreationTest.kt index e29742aedba..9bc81e484f1 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/ObjectCreationTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/ObjectCreationTest.kt @@ -159,13 +159,12 @@ class ObjectCreationTest : BaseTest() { val fVariable = fooBarClass.fields["f"] assertNotNull(fVariable) - // The initializer as an ExpressionList val exprList = fVariable.initializer assertNotNull(exprList) assertIs(exprList) assertEquals(3, exprList.expressions.size) - // 1. DeclarationStatement with implicit tmp variable: __tmp = new Foo(1) + // DeclarationStatement with implicit tmp variable: __tmp = new Foo(1) val declStmt = exprList.expressions[0] assertIs(declStmt) assertTrue(declStmt.isImplicit) @@ -188,7 +187,7 @@ class ObjectCreationTest : BaseTest() { assertIs>(constructArg) assertEquals(1, constructArg.value) - // 2. Implicit assign: __tmp.x = 2 + // Implicit assign: __tmp.x = 2 val assign = exprList.expressions[1] assertIs(assign) assertTrue(assign.isImplicit) @@ -207,10 +206,210 @@ class ObjectCreationTest : BaseTest() { assertIs>(rhs) assertEquals(2, rhs.value) - // 3. Reference to the tmp variable - val finalRef = exprList.expressions[2] - assertIs(finalRef) - assertTrue(finalRef.isImplicit) - assertRefersTo(finalRef, tmpVar) + // Reference to tmp variable + val ref = exprList.expressions[2] + assertIs(ref) + assertTrue(ref.isImplicit) + assertRefersTo(ref, tmpVar) + } + + @Test + fun nestedObjectCreationWithInitializerTest() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("ObjectCreation.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + /** + * Rectangle r = new Rectangle { P1 = new Point { X = 0, Y = 1 }, P2 = new Point { X = 2, Y + * = 3 } } }; + */ + val rectangleClass = tu.records["Rectangle"] + assertNotNull(rectangleClass) + + val rVariable = tu.variables["r"] + assertNotNull(rVariable) + + val exprList = rVariable.initializer + assertNotNull(exprList) + assertIs(exprList) + assertEquals(4, exprList.expressions.size) + + // DeclarationStatement: tmpRectangle = new Rectangle() + val outerDeclStmt = exprList.expressions[0] + assertIs(outerDeclStmt) + val outerTmpVar = outerDeclStmt.declarations.firstOrNull() + assertNotNull(outerTmpVar) + assertIs(outerTmpVar) + val outerNew = outerTmpVar.initializer + assertNotNull(outerNew) + assertIs(outerNew) + + // Assign: tmpRectangle.P1 = ExpressionList(tmpPoint = new Point(), tmpPoint.X = 0, + // tmpPoint.Y = 1, tmpPoint) + val p1Assign = exprList.expressions[1] + assertIs(p1Assign) + val p1MemberAccess = p1Assign.lhs.firstOrNull() + assertIs(p1MemberAccess) + assertRefersTo(p1MemberAccess.base, outerTmpVar) + + val p1Rhs = p1Assign.rhs.firstOrNull() + assertNotNull(p1Rhs) + assertIs(p1Rhs) + assertEquals(4, p1Rhs.expressions.size) + + // Nested: tmpPoint = new Point() + val p1DeclStmt = p1Rhs.expressions[0] + assertIs(p1DeclStmt) + val p1TmpVar = p1DeclStmt.declarations.firstOrNull() + assertNotNull(p1TmpVar) + assertIs(p1TmpVar) + val p1New = p1TmpVar.initializer + assertNotNull(p1New) + assertIs(p1New) + + // Nested: tmpPoint.X = 0 + val xAssign = p1Rhs.expressions[1] + assertIs(xAssign) + val xMemberAccess = xAssign.lhs.firstOrNull() + assertIs(xMemberAccess) + assertEquals("X", xMemberAccess.name.localName) + val xRhs = xAssign.rhs.firstOrNull() + assertIs>(xRhs) + assertEquals(0, xRhs.value) + + // Nested: tmpPoint.Y = 1 + val yAssign = p1Rhs.expressions[2] + assertIs(yAssign) + val yMemberAccess = yAssign.lhs.firstOrNull() + assertIs(yMemberAccess) + assertEquals("Y", yMemberAccess.name.localName) + val yRhs = yAssign.rhs.firstOrNull() + assertIs>(yRhs) + assertEquals(1, yRhs.value) + + val p1Ref = p1Rhs.expressions[3] + assertIs(p1Ref) + assertRefersTo(p1Ref, p1TmpVar) + + // Assign: tmpRectangle.P2 = ExpressionList() + val p2Assign = exprList.expressions[2] + assertIs(p2Assign) + val p2MemberAccess = p2Assign.lhs.firstOrNull() + assertIs(p2MemberAccess) + assertEquals("P2", p2MemberAccess.name.localName) + assertRefersTo(p2MemberAccess.base, outerTmpVar) + + val p2Rhs = p2Assign.rhs.firstOrNull() + assertNotNull(p2Rhs) + assertIs(p2Rhs) + assertEquals(4, p2Rhs.expressions.size) + + // Reference to tmpRectangle + val outerRef = exprList.expressions[3] + assertIs(outerRef) + assertRefersTo(outerRef, outerTmpVar) + } + + @Test + fun nestedObjectInitializerWithoutNewTest() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("ObjectCreation.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + /** + * Rectangle2 r = new Rectangle2 { P1 = { X = 0, Y = 1 }, P2 = { X = 2, Y = 3 } }; + * + * translated into: Rectangle2 __tmp = new Rectangle2(); __tmp.P1.X = 0; __tmp.P1.Y = 1; + * __tmp.P2.X = 2; __tmp.P2.Y = 3; Rectangle2 r = __tmp; + */ + val rectangle2Class = tu.records["Rectangle2"] + assertNotNull(rectangle2Class) + + val r2Variable = tu.variables["r2"] + assertNotNull(r2Variable) + + val exprList = r2Variable.initializer + assertNotNull(exprList) + assertIs(exprList) + assertEquals(6, exprList.expressions.size) + + // DeclarationStatement: __tmp = new Rectangle2() + val declStmt = exprList.expressions[0] + assertIs(declStmt) + assertTrue(declStmt.isImplicit) + val tmpVar = declStmt.declarations.firstOrNull() + assertNotNull(tmpVar) + assertIs(tmpVar) + val newNode = tmpVar.initializer + assertNotNull(newNode) + assertIs(newNode) + + // __tmp.P1.X = 0 + val p1xAssign = exprList.expressions[1] + assertIs(p1xAssign) + assertTrue(p1xAssign.isImplicit) + val p1xAccess = p1xAssign.lhs.firstOrNull() + assertIs(p1xAccess) + assertEquals("X", p1xAccess.name.localName) + val p1Access = p1xAccess.base + assertIs(p1Access) + assertEquals("P1", p1Access.name.localName) + assertRefersTo(p1Access.base, tmpVar) + val p1xRhs = p1xAssign.rhs.firstOrNull() + assertIs>(p1xRhs) + assertEquals(0, p1xRhs.value) + + // __tmp.P1.Y = 1 + val p1yAssign = exprList.expressions[2] + assertIs(p1yAssign) + val p1yAccess = p1yAssign.lhs.firstOrNull() + assertIs(p1yAccess) + assertEquals("Y", p1yAccess.name.localName) + val p1yRhs = p1yAssign.rhs.firstOrNull() + assertIs>(p1yRhs) + assertEquals(1, p1yRhs.value) + + // __tmp.P2.X = 2 + val p2xAssign = exprList.expressions[3] + assertIs(p2xAssign) + val p2xAccess = p2xAssign.lhs.firstOrNull() + assertIs(p2xAccess) + assertEquals("X", p2xAccess.name.localName) + val p2Access = p2xAccess.base + assertIs(p2Access) + assertEquals("P2", p2Access.name.localName) + assertRefersTo(p2Access.base, tmpVar) + val p2xRhs = p2xAssign.rhs.firstOrNull() + assertIs>(p2xRhs) + assertEquals(2, p2xRhs.value) + + // __tmp.P2.Y = 3 + val p2yAssign = exprList.expressions[4] + assertIs(p2yAssign) + val p2yAccess = p2yAssign.lhs.firstOrNull() + assertIs(p2yAccess) + assertEquals("Y", p2yAccess.name.localName) + val p2yRhs = p2yAssign.rhs.firstOrNull() + assertIs>(p2yRhs) + assertEquals(3, p2yRhs.value) + + // Reference to __tmp + val ref = exprList.expressions[5] + assertIs(ref) + assertRefersTo(ref, tmpVar) } } diff --git a/cpg-language-csharp/src/test/resources/csharp/CollectionInitializer.cs b/cpg-language-csharp/src/test/resources/csharp/CollectionInitializer.cs new file mode 100644 index 00000000000..eaccbfe34cb --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/CollectionInitializer.cs @@ -0,0 +1,9 @@ +class CollectionInitializer +{ + List numbers = new List { 0, 1, 2 }; +} + +class ComplexCollectionInitializer +{ + Dictionary map = new Dictionary { { "a", 1 }, { "b", 2 } }; +} \ No newline at end of file diff --git a/cpg-language-csharp/src/test/resources/csharp/ObjectCreation.cs b/cpg-language-csharp/src/test/resources/csharp/ObjectCreation.cs index fb5951b349e..26d3201bbf6 100644 --- a/cpg-language-csharp/src/test/resources/csharp/ObjectCreation.cs +++ b/cpg-language-csharp/src/test/resources/csharp/ObjectCreation.cs @@ -24,5 +24,42 @@ class Baz class FooBar { + Foo f = new Foo(1) { x = 2 }; +} + +class Point +{ + int X; + int Y; +} + +class Rectangle +{ + Point P1; + Point P2; + + void create() + { + Rectangle r = new Rectangle + { + P1 = new Point { X = 0, Y = 1 }, + P2 = new Point { X = 2, Y = 3 } + }; + } +} + +class Rectangle2 +{ + Point P1 = new Point(); + Point P2 = new Point(); + + void create() + { + Rectangle2 r2 = new Rectangle2 + { + P1 = { X = 0, Y = 1 }, + P2 = { X = 2, Y = 3 } + }; + } } \ No newline at end of file From f58717b6ec8425b51eb60abb69b0b1703b094014 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Thu, 9 Apr 2026 10:36:21 +0200 Subject: [PATCH 47/55] Fix test --- .../csharp/expressionhandler/CollectionInitializerTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/CollectionInitializerTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/CollectionInitializerTest.kt index 2234cc7c588..1c6920787d5 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/CollectionInitializerTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/expressionhandler/CollectionInitializerTest.kt @@ -64,7 +64,7 @@ class CollectionInitializerTest : BaseTest() { * translated into: List __tmp = new List(); __tmp.Add(0); __tmp.Add(1); * __tmp.Add(2); List numbers = __tmp; */ - val simpleClass = tu.records["CollectionInitializer,"] + val simpleClass = tu.records["CollectionInitializer"] assertNotNull(simpleClass) val numbersField = simpleClass.fields["numbers"] From bb84ca450bf8e21d211b36dc0cc5534bf200f638 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Thu, 9 Apr 2026 14:58:12 +0200 Subject: [PATCH 48/55] Add missing constructor body support --- .../aisec/cpg/frontends/csharp/CSharp.kt | 4 +- .../frontends/csharp/DeclarationHandler.kt | 2 +- .../csharp/CSharpLanguageFrontendTest.kt | 46 +++++++++++++++---- .../src/test/resources/csharp/Constructor.cs | 7 ++- 4 files changed, 47 insertions(+), 12 deletions(-) diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index e5bece0508d..17384465a27 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -164,6 +164,7 @@ interface Csharp : Library { val parameterList: ParameterListSyntax by lazy { INSTANCE.GetBaseMethodDeclarationParameterList(this) } + val body: BlockSyntax by lazy { INSTANCE.GetBaseMethodDeclarationBody(this) } override fun fromNative(nativeValue: Any?, context: FromNativeContext?): Any { if (nativeValue !is Pointer) { @@ -182,7 +183,6 @@ interface Csharp : Library { */ class MethodDeclarationSyntax(p: Pointer? = Pointer.NULL) : BaseMethodDeclarationSyntax(p) { val identifier: String by lazy { INSTANCE.GetMethodDeclarationIdentifier(this) } - val body: BlockSyntax by lazy { INSTANCE.GetBaseMethodDeclarationBody(this) } } /** @@ -684,7 +684,7 @@ interface Csharp : Library { fun GetConstructorDeclarationIdentifier(handle: AST.ConstructorDeclarationSyntax): String - fun GetBaseMethodDeclarationBody(handle: AST.MethodDeclarationSyntax): AST.BlockSyntax + fun GetBaseMethodDeclarationBody(handle: AST.BaseMethodDeclarationSyntax): AST.BlockSyntax fun GetBlockStatementCount(handle: AST.StatementSyntax): Int diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt index 66c82265d42..d46577d4709 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -174,7 +174,7 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : frontend.scopeManager.addDeclaration(param) constructor.parameters += param } - // constructor.body = frontend.statementHandler.handle(node.body) + constructor.body = frontend.statementHandler.handle(node.body) frontend.scopeManager.leaveScope(constructor) return constructor diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt index 4ca8380ad93..6909637d074 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt @@ -27,10 +27,14 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.declarations.Constructor +import de.fraunhofer.aisec.cpg.graph.declarations.Field +import de.fraunhofer.aisec.cpg.graph.declarations.Parameter +import de.fraunhofer.aisec.cpg.graph.expressions.Assign import de.fraunhofer.aisec.cpg.graph.expressions.BinaryOperator import de.fraunhofer.aisec.cpg.graph.expressions.Block 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.Reference import de.fraunhofer.aisec.cpg.graph.expressions.Return import de.fraunhofer.aisec.cpg.test.* @@ -143,15 +147,41 @@ class CSharpLanguageFrontendTest : BaseTest() { val constructors = foo.constructors assertEquals(2, constructors.size) - val noParameter = constructors.single { it.parameters.isEmpty() } - assertNotNull(noParameter) - assertIs(noParameter) + val defaultConstructor = constructors.single { it.parameters.isEmpty() } + assertNotNull(defaultConstructor) + assertIs(defaultConstructor) - val twoParameters = constructors.single { it.parameters.size == 2 } - assertNotNull(twoParameters) - assertIs(twoParameters) - assertEquals("x", twoParameters.parameters[0].name.localName) - assertEquals("y", twoParameters.parameters[1].name.localName) + val emptyBody = defaultConstructor.body + assertIs(emptyBody) + assertEquals(0, emptyBody.statements.size) + + val constructorWithParams = constructors.single { it.parameters.size == 2 } + assertNotNull(constructorWithParams) + assertIs(constructorWithParams) + + val body = constructorWithParams.body + assertIs(body) + assertEquals(1, body.statements.size) + + val assignment = body.statements[0] + assertIs(assignment) + assertEquals("=", assignment.operatorCode) + + // lhs: this.x + val lhs = assignment.lhs.firstOrNull() + assertIs(lhs) + val fieldX = foo.fields["x"] + assertNotNull(fieldX) + assertIs(lhs.refersTo) + assertEquals(fieldX, lhs.refersTo) + + // rhs: x + val rhs = assignment.rhs.firstOrNull() + assertIs(rhs) + assertIs(rhs.refersTo) + val refersTo = rhs.refersTo + assertNotNull(refersTo) + assertEquals(constructorWithParams.parameters.first(), refersTo) } @Test diff --git a/cpg-language-csharp/src/test/resources/csharp/Constructor.cs b/cpg-language-csharp/src/test/resources/csharp/Constructor.cs index c7d4ce26bf9..8d753f7a1ad 100644 --- a/cpg-language-csharp/src/test/resources/csharp/Constructor.cs +++ b/cpg-language-csharp/src/test/resources/csharp/Constructor.cs @@ -2,7 +2,12 @@ namespace HelloWorld; class Foo { + int x; + Foo() { } - Foo(int x, string y) { } + Foo(int x, string y) + { + this.x = x; + } } \ No newline at end of file From b175e0906661d3887fa30afdd682b84ca01fdbc6 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Fri, 10 Apr 2026 13:09:57 +0200 Subject: [PATCH 49/55] Add base types/super classes --- .../src/main/csharp/NativeParser/Library.cs | 168 +++++++++++++++- .../aisec/cpg/frontends/csharp/CSharp.kt | 180 ++++++++++++++++-- .../frontends/csharp/DeclarationHandler.kt | 63 +++++- .../csharp/CSharpLanguageFrontendTest.kt | 85 +++++++++ .../src/test/resources/csharp/Inheritance.cs | 32 ++++ 5 files changed, 496 insertions(+), 32 deletions(-) create mode 100644 cpg-language-csharp/src/test/resources/csharp/Inheritance.cs diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index 463104856bd..a856f2c556d 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -49,6 +49,18 @@ public static IntPtr GetType(IntPtr handlePtr) return Marshal.StringToCoTaskMemUTF8(Nodes[handlePtr].GetType().Name); } + [UnmanagedCallersOnly(EntryPoint = "GetCompilationUnitUsingsCount")] + public static int GetCompilationUnitUsingsCount(IntPtr handlePtr) + { + return ((CompilationUnitSyntax)Nodes[handlePtr]).Usings.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetCompilationUnitUsing")] + public static IntPtr GetCompilationUnitUsing(IntPtr handlePtr, int index) + { + return Register(((CompilationUnitSyntax)Nodes[handlePtr]).Usings[index]); + } + [UnmanagedCallersOnly(EntryPoint = "GetCompilationUnitMembersCount")] public static int GetCompilationUnitMembersCount(IntPtr handlePtr) { @@ -83,26 +95,164 @@ public static IntPtr GetNamespaceDeclarationMember(IntPtr handlePtr, int index) return Register(((BaseNamespaceDeclarationSyntax)Nodes[handlePtr]).Members[index]); } - [UnmanagedCallersOnly(EntryPoint = "GetClassDeclarationIdentifier")] - public static IntPtr GetClassDeclarationIdentifier(IntPtr handlePtr) + [UnmanagedCallersOnly(EntryPoint = "GetNamespaceDeclarationUsingsCount")] + public static int GetNamespaceDeclarationUsingsCount(IntPtr handlePtr) + { + return ((BaseNamespaceDeclarationSyntax)Nodes[handlePtr]).Usings.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetNamespaceDeclarationUsing")] + public static IntPtr GetNamespaceDeclarationUsing(IntPtr handlePtr, int index) + { + return Register(((BaseNamespaceDeclarationSyntax)Nodes[handlePtr]).Usings[index]); + } + + [UnmanagedCallersOnly(EntryPoint = "GetUsingDirectiveName")] + public static IntPtr GetUsingDirectiveName(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8( + ((UsingDirectiveSyntax)Nodes[handlePtr]).Name.ToString() + ); + } + + [UnmanagedCallersOnly(EntryPoint = "GetUsingDirectiveAlias")] + public static IntPtr GetUsingDirectiveAlias(IntPtr handlePtr) + { + var alias = ((UsingDirectiveSyntax)Nodes[handlePtr]).Alias; + return alias != null ? Register(alias) : IntPtr.Zero; + } + + [UnmanagedCallersOnly(EntryPoint = "GetUsingDirectiveStaticKeyword")] + public static IntPtr GetUsingDirectiveStaticKeyword(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8( + ((UsingDirectiveSyntax)Nodes[handlePtr]).StaticKeyword.Text + ); + } + + [UnmanagedCallersOnly(EntryPoint = "GetUsingDirectiveGlobalKeyword")] + public static IntPtr GetUsingDirectiveGlobalKeyword(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8( + ((UsingDirectiveSyntax)Nodes[handlePtr]).GlobalKeyword.Text + ); + } + + [UnmanagedCallersOnly(EntryPoint = "GetNameEqualsSyntaxName")] + public static IntPtr GetNameEqualsSyntaxName(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8( + ((NameEqualsSyntax)Nodes[handlePtr]).Name.Identifier.Text + ); + } + + [UnmanagedCallersOnly(EntryPoint = "GetEnumDeclarationIdentifier")] + public static IntPtr GetEnumDeclarationIdentifier(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8( + ((EnumDeclarationSyntax)Nodes[handlePtr]).Identifier.ToString() + ); + } + + [UnmanagedCallersOnly(EntryPoint = "GetEnumDeclarationMembersCount")] + public static int GetEnumDeclarationMembersCount(IntPtr handlePtr) + { + return ((EnumDeclarationSyntax)Nodes[handlePtr]).Members.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetEnumDeclarationMember")] + public static IntPtr GetEnumDeclarationMember(IntPtr handlePtr, int index) + { + return Register(((EnumDeclarationSyntax)Nodes[handlePtr]).Members[index]); + } + + [UnmanagedCallersOnly(EntryPoint = "GetEnumMemberDeclarationIdentifier")] + public static IntPtr GetEnumMemberDeclarationIdentifier(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8( + ((EnumMemberDeclarationSyntax)Nodes[handlePtr]).Identifier.ToString() + ); + } + + // Identifier for Class, Interface, Struct, Record, Enum. + [UnmanagedCallersOnly(EntryPoint = "GetBaseTypeDeclarationIdentifier")] + public static IntPtr GetBaseTypeDeclarationIdentifier(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8( + ((BaseTypeDeclarationSyntax)Nodes[handlePtr]).Identifier.ToString() + ); + } + + // Members for Class, Interface, Struct, Record. + [UnmanagedCallersOnly(EntryPoint = "GetTypeDeclarationMembersCount")] + public static int GetTypeDeclarationMembersCount(IntPtr handlePtr) + { + return ((TypeDeclarationSyntax)Nodes[handlePtr]).Members.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetTypeDeclarationMember")] + public static IntPtr GetTypeDeclarationMember(IntPtr handlePtr, int index) + { + return Register(((TypeDeclarationSyntax)Nodes[handlePtr]).Members[index]); + } + + // BaseList can be null if the class has no base types (e.g. `class Foo { }`). + // We use TypeDeclarationSyntax to support both ClassDeclarationSyntax and InterfaceDeclarationSyntax. + [UnmanagedCallersOnly(EntryPoint = "GetTypeDeclarationBaseList")] + public static IntPtr GetTypeDeclarationBaseList(IntPtr handlePtr) + { + var baseList = ((TypeDeclarationSyntax)Nodes[handlePtr]).BaseList; + return baseList != null ? Register(baseList) : IntPtr.Zero; + } + + [UnmanagedCallersOnly(EntryPoint = "GetBaseListTypeCount")] + public static int GetBaseListTypeCount(IntPtr handlePtr) + { + return ((BaseListSyntax)Nodes[handlePtr]).Types.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetBaseListType")] + public static IntPtr GetBaseListType(IntPtr handlePtr, int index) + { + return Register(((BaseListSyntax)Nodes[handlePtr]).Types[index].Type); + } + + [UnmanagedCallersOnly(EntryPoint = "GetMemberDeclarationModifierCount")] + public static int GetMemberDeclarationModifierCount(IntPtr handlePtr) + { + return ((MemberDeclarationSyntax)Nodes[handlePtr]).Modifiers.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetMemberDeclarationModifier")] + public static IntPtr GetMemberDeclarationModifier(IntPtr handlePtr, int index) { return Marshal.StringToCoTaskMemUTF8( - ((ClassDeclarationSyntax)Nodes[handlePtr]).Identifier.ToString() + ((MemberDeclarationSyntax)Nodes[handlePtr]).Modifiers[index].Text ); } - [UnmanagedCallersOnly(EntryPoint = "GetClassDeclarationMembersCount")] - public static int GetClassDeclarationMembersCount(IntPtr handlePtr) + [UnmanagedCallersOnly(EntryPoint = "GetTypeParameterList")] + public static IntPtr GetTypeParameterList(IntPtr handlePtr) + { + var typeParamList = ((TypeDeclarationSyntax)Nodes[handlePtr]).TypeParameterList; + return typeParamList != null ? Register(typeParamList) : IntPtr.Zero; + } + + [UnmanagedCallersOnly(EntryPoint = "GetTypeParameterListCount")] + public static int GetTypeParameterListCount(IntPtr handlePtr) { - return ((ClassDeclarationSyntax)Nodes[handlePtr]).Members.Count; + return ((TypeParameterListSyntax)Nodes[handlePtr]).Parameters.Count; } - [UnmanagedCallersOnly(EntryPoint = "GetClassDeclarationMember")] - public static IntPtr GetClassDeclarationMember(IntPtr handlePtr, int index) + [UnmanagedCallersOnly(EntryPoint = "GetTypeParameterListParameter")] + public static IntPtr GetTypeParameterListParameter(IntPtr handlePtr, int index) { - return Register(((ClassDeclarationSyntax)Nodes[handlePtr]).Members[index]); + return Marshal.StringToCoTaskMemUTF8( + ((TypeParameterListSyntax)Nodes[handlePtr]).Parameters[index].Identifier.Text + ); } + [UnmanagedCallersOnly(EntryPoint = "GetMethodDeclarationIdentifier")] public static IntPtr GetMethodDeclarationIdentifier(IntPtr handlePtr) { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 17384465a27..aad8d61a6dc 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -97,6 +97,10 @@ interface Csharp : Library { val count = INSTANCE.GetCompilationUnitMembersCount(this) (0 until count).map { i -> INSTANCE.GetCompilationUnitMember(this, i) } } + val usings: List by lazy { + val count = INSTANCE.GetCompilationUnitUsingsCount(this) + (0 until count).map { i -> INSTANCE.GetCompilationUnitUsing(this, i) } + } } /** @@ -105,6 +109,11 @@ interface Csharp : Library { * class. */ open class MemberDeclarationSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val modifiers: List by lazy { + val count = INSTANCE.GetMemberDeclarationModifierCount(this) + (0 until count).map { i -> INSTANCE.GetMemberDeclarationModifier(this, i) } + } + /** * JNA calls this method automatically when converting a native pointer which is * returned by a JNA function into a [MemberDeclarationSyntax] object. We override it to @@ -116,10 +125,11 @@ interface Csharp : Library { return super.fromNative(nativeValue, context) } return when (INSTANCE.GetType(nativeValue)) { - "NamespaceDeclarationSyntax", + "NamespaceDeclarationSyntax" -> NamespaceDeclarationSyntax(nativeValue) "FileScopedNamespaceDeclarationSyntax" -> - NamespaceDeclarationSyntax(nativeValue) + FileScopedNamespaceDeclarationSyntax(nativeValue) "ClassDeclarationSyntax" -> ClassDeclarationSyntax(nativeValue) + "InterfaceDeclarationSyntax" -> InterfaceDeclarationSyntax(nativeValue) "FieldDeclarationSyntax" -> FieldDeclarationSyntax(nativeValue) "MethodDeclarationSyntax" -> MethodDeclarationSyntax(nativeValue) "ConstructorDeclarationSyntax" -> ConstructorDeclarationSyntax(nativeValue) @@ -130,30 +140,124 @@ interface Csharp : Library { /** * Represents the Roslyn - * [`NamespaceDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.namespacedeclarationsyntax) + * [`BaseNamespaceDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.basenamespacedeclarationsyntax) * class. */ - class NamespaceDeclarationSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { + open class BaseNamespaceDeclarationSyntax(p: Pointer? = Pointer.NULL) : + MemberDeclarationSyntax(p) { val name: String by lazy { INSTANCE.GetNamespaceDeclarationName(this) } val members: List by lazy { val count = INSTANCE.GetNamespaceDeclarationMembersCount(this) (0 until count).map { i -> INSTANCE.GetNamespaceDeclarationMember(this, i) } } + val usings: List by lazy { + val count = INSTANCE.GetNamespaceDeclarationUsingsCount(this) + (0 until count).map { i -> INSTANCE.GetNamespaceDeclarationUsing(this, i) } + } } /** * Represents the Roslyn - * [`ClassDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.classdeclarationsyntax) + * [`NamespaceDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.namespacedeclarationsyntax) + * class. + */ + class NamespaceDeclarationSyntax(p: Pointer? = Pointer.NULL) : + BaseNamespaceDeclarationSyntax(p) + + /** + * Represents the Roslyn + * [`FileScopedNamespaceDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.filescopednamespacedeclarationsyntax) + * class. + */ + class FileScopedNamespaceDeclarationSyntax(p: Pointer? = Pointer.NULL) : + BaseNamespaceDeclarationSyntax(p) + + /** + * Represents the Roslyn + * [`BaseTypeDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.basetypedeclarationsyntax) + * class. + */ + open class BaseTypeDeclarationSyntax(p: Pointer? = Pointer.NULL) : + MemberDeclarationSyntax(p) { + val identifier: String by lazy { INSTANCE.GetBaseTypeDeclarationIdentifier(this) } + val baseList: BaseListSyntax? by lazy { INSTANCE.GetTypeDeclarationBaseList(this) } + } + + /** + * Represents the Roslyn + * [`TypeDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.typedeclarationsyntax) * class. */ - class ClassDeclarationSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { - val identifier: String by lazy { INSTANCE.GetClassDeclarationIdentifier(this) } + open class TypeDeclarationSyntax(p: Pointer? = Pointer.NULL) : + BaseTypeDeclarationSyntax(p) { val members: List by lazy { - val count = INSTANCE.GetClassDeclarationMembersCount(this) - (0 until count).map { i -> INSTANCE.GetClassDeclarationMember(this, i) } + val count = INSTANCE.GetTypeDeclarationMembersCount(this) + (0 until count).map { i -> INSTANCE.GetTypeDeclarationMember(this, i) } + } + val typeParameterList: TypeParameterListSyntax? by lazy { + INSTANCE.GetTypeParameterList(this) + } + } + + /** + * Represents the Roslyn + * [`ClassDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.classdeclarationsyntax) + * class. + */ + class ClassDeclarationSyntax(p: Pointer? = Pointer.NULL) : TypeDeclarationSyntax(p) + + /** + * Represents the Roslyn + * [`InterfaceDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.interfacedeclarationsyntax) + * class. + */ + class InterfaceDeclarationSyntax(p: Pointer? = Pointer.NULL) : TypeDeclarationSyntax(p) + + /** + * Represents the Roslyn + * [`BaseListSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.baselistsyntax) + * class. + */ + class BaseListSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val types: List by lazy { + val count = INSTANCE.GetBaseListTypeCount(this) + (0 until count).map { i -> INSTANCE.GetBaseListType(this, i) } } } + /** + * Represents the Roslyn + * [`TypeParameterListSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.typeparameterlistsyntax) + * class. + */ + class TypeParameterListSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val parameters: List by lazy { + val count = INSTANCE.GetTypeParameterListCount(this) + (0 until count).map { i -> INSTANCE.GetTypeParameterListParameter(this, i) } + } + } + + /** + * Represents the Roslyn + * [`NameEqualsSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.nameequalssyntax) + * class. + */ + class NameEqualsSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val name: String by lazy { INSTANCE.GetNameEqualsSyntaxName(this) } + } + + /** + * Represents the Roslyn + * [`UsingDirectiveSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.usingdirectivesyntax) + * class. + */ + class UsingDirectiveSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val name: String by lazy { INSTANCE.GetUsingDirectiveName(this) } + val alias: NameEqualsSyntax? by lazy { INSTANCE.GetUsingDirectiveAlias(this) } + val staticKeyword: String by lazy { INSTANCE.GetUsingDirectiveStaticKeyword(this) } + val globalKeyword: String by lazy { INSTANCE.GetUsingDirectiveGlobalKeyword(this) } + } + /** * Represents the Roslyn * [`BaseMethodDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.basemethoddeclarationsyntax) @@ -241,7 +345,6 @@ interface Csharp : Library { */ class IfStatementSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) { val condition: ExpressionSyntax by lazy { INSTANCE.GetIfStatementSyntaxCondition(this) } - /** The statement that is executed when the condition is true. */ val statement: StatementSyntax by lazy { INSTANCE.GetIfStatementSyntaxStatement(this) } val elseClause: ElseClauseSyntax? by lazy { INSTANCE.GetElseClauseSyntax(this) } } @@ -249,8 +352,7 @@ interface Csharp : Library { /** * Represents the Roslyn * [`ElseClauseSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.elseclausesyntax) - * class. Note: not a [StatementSyntax] because it cannot appear on its own. It's always - * attached to an [IfStatementSyntax]. + * class. */ class ElseClauseSyntax(p: Pointer? = Pointer.NULL) : Node(p) { val statement: StatementSyntax by lazy { INSTANCE.GetElseClauseSyntaxStatement(this) } @@ -642,24 +744,64 @@ interface Csharp : Library { index: Int, ): AST.MemberDeclarationSyntax - fun GetNamespaceDeclarationName(handle: AST.NamespaceDeclarationSyntax): String + fun GetCompilationUnitUsingsCount(handle: AST.CompilationUnitSyntax): Int + + fun GetCompilationUnitUsing( + handle: AST.CompilationUnitSyntax, + index: Int, + ): AST.UsingDirectiveSyntax + + fun GetNamespaceDeclarationName(handle: AST.BaseNamespaceDeclarationSyntax): String - fun GetNamespaceDeclarationMembersCount(handle: AST.NamespaceDeclarationSyntax): Int + fun GetNamespaceDeclarationMembersCount(handle: AST.BaseNamespaceDeclarationSyntax): Int fun GetNamespaceDeclarationMember( - handle: AST.NamespaceDeclarationSyntax, + handle: AST.BaseNamespaceDeclarationSyntax, index: Int, ): AST.MemberDeclarationSyntax - fun GetClassDeclarationIdentifier(handle: AST.ClassDeclarationSyntax): String + fun GetNamespaceDeclarationUsingsCount(handle: AST.BaseNamespaceDeclarationSyntax): Int + + fun GetNamespaceDeclarationUsing( + handle: AST.BaseNamespaceDeclarationSyntax, + index: Int, + ): AST.UsingDirectiveSyntax + + fun GetUsingDirectiveName(handle: AST.UsingDirectiveSyntax): String + + fun GetUsingDirectiveAlias(handle: AST.UsingDirectiveSyntax): AST.NameEqualsSyntax? + + fun GetUsingDirectiveStaticKeyword(handle: AST.UsingDirectiveSyntax): String + + fun GetUsingDirectiveGlobalKeyword(handle: AST.UsingDirectiveSyntax): String - fun GetClassDeclarationMembersCount(handle: AST.ClassDeclarationSyntax): Int + fun GetNameEqualsSyntaxName(handle: AST.NameEqualsSyntax): String - fun GetClassDeclarationMember( - handle: AST.ClassDeclarationSyntax, + fun GetBaseTypeDeclarationIdentifier(handle: AST.BaseTypeDeclarationSyntax): String + + fun GetTypeDeclarationMembersCount(handle: AST.TypeDeclarationSyntax): Int + + fun GetTypeDeclarationMember( + handle: AST.TypeDeclarationSyntax, index: Int, ): AST.MemberDeclarationSyntax + fun GetTypeDeclarationBaseList(handle: AST.BaseTypeDeclarationSyntax): AST.BaseListSyntax? + + fun GetBaseListTypeCount(handle: AST.BaseListSyntax): Int + + fun GetBaseListType(handle: AST.BaseListSyntax, index: Int): AST.TypeSyntax + + fun GetMemberDeclarationModifierCount(handle: AST.MemberDeclarationSyntax): Int + + fun GetMemberDeclarationModifier(handle: AST.MemberDeclarationSyntax, index: Int): String + + fun GetTypeParameterList(handle: AST.MemberDeclarationSyntax): AST.TypeParameterListSyntax? + + fun GetTypeParameterListCount(handle: AST.TypeParameterListSyntax): Int + + fun GetTypeParameterListParameter(handle: AST.TypeParameterListSyntax, index: Int): String + fun GetFieldDeclaration(handle: AST.FieldDeclarationSyntax): AST.VariableDeclarationSyntax fun GetVariableDeclaratorIdentifier(handle: AST.VariableDeclaratorSyntax): String diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt index d46577d4709..675068568b8 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -37,29 +37,31 @@ import de.fraunhofer.aisec.cpg.graph.newParameter import de.fraunhofer.aisec.cpg.graph.newRecord import de.fraunhofer.aisec.cpg.graph.newVariable import de.fraunhofer.aisec.cpg.graph.parseName +import de.fraunhofer.aisec.cpg.graph.types.ParameterizedType import de.fraunhofer.aisec.cpg.graph.unknownType class DeclarationHandler(frontend: CSharpLanguageFrontend) : CSharpHandler(::ProblemDeclaration, frontend) { override fun handleNode(node: Csharp.AST.MemberDeclarationSyntax): Declaration { return when (node) { - is Csharp.AST.NamespaceDeclarationSyntax -> handleNamespaceDeclaration(node) + is Csharp.AST.BaseNamespaceDeclarationSyntax -> handleNamespaceDeclaration(node) is Csharp.AST.ClassDeclarationSyntax -> handleClassDeclaration(node) is Csharp.AST.MethodDeclarationSyntax -> handleMethodDeclaration(node) is Csharp.AST.ConstructorDeclarationSyntax -> handleConstructorDeclaration(node) + is Csharp.AST.InterfaceDeclarationSyntax -> handleInterfaceDeclaration(node) else -> ProblemDeclaration("Not supported: ${node.csharpType}") } } /** - * Translates a [NamespaceDeclarationSyntax][Csharp.AST.NamespaceDeclarationSyntax] into a - * [Namespace]. + * Translates a [BaseNamespaceDeclarationSyntax][Csharp.AST.BaseNamespaceDeclarationSyntax] into + * a [Namespace]. Handles both block-scoped and file-scoped namespaces. * * C# spec: * [Namespace declarations](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/namespaces#143-namespace-declarations) */ private fun handleNamespaceDeclaration( - node: Csharp.AST.NamespaceDeclarationSyntax + node: Csharp.AST.BaseNamespaceDeclarationSyntax ): Declaration { val namespace = newNamespace(node.name, rawNode = node) frontend.scopeManager.enterScope(namespace) @@ -82,8 +84,22 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : */ private fun handleClassDeclaration(node: Csharp.AST.ClassDeclarationSyntax): Declaration { val record = newRecord(node.identifier, "class", rawNode = node) + record.modifiers = node.modifiers.toSet() frontend.scopeManager.enterScope(record) + node.baseList?.let { + for (baseType in it.types) { + record.superClasses += frontend.typeOf(baseType) + } + } + + node.typeParameterList?.let { + frontend.typeManager.addTypeParameter( + record, + it.parameters.map { name -> ParameterizedType(name, language) }, + ) + } + for (member in node.members) { when (member) { is Csharp.AST.FieldDeclarationSyntax -> { @@ -123,6 +139,43 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : return record } + /** + * Translates an [InterfaceDeclarationSyntax][Csharp.AST.InterfaceDeclarationSyntax] into a + * [Record]. + * + * C# spec: + * [Interface declarations](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/interfaces#182-interface-declarations) + */ + private fun handleInterfaceDeclaration( + node: Csharp.AST.InterfaceDeclarationSyntax + ): Declaration { + val record = newRecord(node.identifier, "interface", rawNode = node) + record.modifiers = node.modifiers.toSet() + frontend.scopeManager.enterScope(record) + + node.baseList?.let { + for (baseType in it.types) { + record.superClasses += frontend.typeOf(baseType) + } + } + + node.typeParameterList?.let { + frontend.typeManager.addTypeParameter( + record, + it.parameters.map { name -> ParameterizedType(name, language) }, + ) + } + + for (member in node.members) { + val decl = handle(member) + frontend.scopeManager.addDeclaration(decl) + record.addDeclaration(decl) + } + + frontend.scopeManager.leaveScope(record) + return record + } + /** * Translates a [MethodDeclarationSyntax][Csharp.AST.MethodDeclarationSyntax] into a [Method]. * @@ -131,6 +184,7 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : */ private fun handleMethodDeclaration(node: Csharp.AST.MethodDeclarationSyntax): Declaration { val method = newMethod(node.identifier, rawNode = node) + method.modifiers = node.modifiers.toSet() frontend.scopeManager.enterScope(method) createMethodReceiver(method) @@ -162,6 +216,7 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : ): Declaration { val record = frontend.scopeManager.currentRecord val constructor = newConstructor(node.identifier, record, rawNode = node) + constructor.modifiers = node.modifiers.toSet() frontend.scopeManager.enterScope(constructor) for (parameter in node.parameterList.parameters) { diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt index 6909637d074..111e5de0c69 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt @@ -35,11 +35,15 @@ import de.fraunhofer.aisec.cpg.graph.expressions.Block 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 +import de.fraunhofer.aisec.cpg.graph.types.ParameterizedType +import de.fraunhofer.aisec.cpg.graph.types.recordDeclaration import de.fraunhofer.aisec.cpg.test.* import java.nio.file.Path import kotlin.test.Test +import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertIs import kotlin.test.assertNotNull @@ -351,4 +355,85 @@ class CSharpLanguageFrontendTest : BaseTest() { assertNotNull(innerIf.thenStatement) assertNotNull(innerIf.elseStatement) } + + @Test + fun testInheritance() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("Inheritance.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val iBase = tu.records["IBase"] + assertNotNull(iBase) + assertEquals("interface", iBase.kind) + assertEquals(0, iBase.superClasses.size) + assertContains(iBase.modifiers, "public") + + val foo = tu.records["Foo"] + assertNotNull(foo) + assertEquals("class", foo.kind) + assertEquals(1, foo.superClasses.size) + assertEquals(iBase, foo.superClasses.first().recordDeclaration) + assertEquals(foo.modifiers.size, 2) + assertContains(foo.modifiers, "public") + assertContains(foo.modifiers, "abstract") + + val bar = tu.records["Bar"] + assertNotNull(bar) + assertEquals(1, bar.superClasses.size) + assertEquals(foo, bar.superClasses.first().recordDeclaration) + + val accessMethod = bar.methods["AccessField"] + assertNotNull(accessMethod) + val accessBody = accessMethod.body + assertIs(accessBody) + val returnStmt = accessBody.statements.firstOrNull() + assertNotNull(returnStmt) + assertIs(returnStmt) + val memberAccess = returnStmt.returnValue + assertIs(memberAccess) + assertEquals("x", memberAccess.name.localName) + val fieldX = foo.fields["x"] + assertNotNull(fieldX) + assertEquals(fieldX, memberAccess.refersTo) + + val callMethod = bar.methods["CallInheritedMethod"] + assertNotNull(callMethod) + val callBody = callMethod.body + assertIs(callBody) + val callReturn = callBody.statements.firstOrNull() + assertNotNull(callReturn) + assertIs(callReturn) + val memberCall = callReturn.returnValue + assertIs(memberCall) + val doSomething = foo.methods["DoSomething"] + assertNotNull(doSomething) + assertEquals(doSomething, memberCall.invokes.firstOrNull()) + assertContains(doSomething.modifiers, "public") + } + + @Test + fun testGenericType() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val result = + analyze(listOf(topLevel.resolve("Inheritance.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + val tu = result.components.firstOrNull()?.translationUnits?.firstOrNull() + assertNotNull(tu) + + val container = tu.records["GenericClass"] + assertNotNull(container) + assertContains(container.modifiers, "public") + + val typeT = result.finalCtx.typeManager.getTypeParameter(container, "T") + assertNotNull(typeT) + assertIs(typeT) + } } diff --git a/cpg-language-csharp/src/test/resources/csharp/Inheritance.cs b/cpg-language-csharp/src/test/resources/csharp/Inheritance.cs new file mode 100644 index 00000000000..84c3f0f0c47 --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/Inheritance.cs @@ -0,0 +1,32 @@ +public interface IBase +{ +} + +public abstract class Foo : IBase +{ + int x; + + public int DoSomething() + { + return x; + } +} + +public class GenericClass +{ +} + +class Bar : Foo +{ + int y; + + int AccessField() + { + return this.x; + } + + int CallInheritedMethod() + { + return this.DoSomething(); + } +} \ No newline at end of file From 1d910acf88fd8dd5bfe759466f3ab55cfceb9cb9 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Fri, 10 Apr 2026 16:55:59 +0200 Subject: [PATCH 50/55] Add Switch statement --- .../src/main/csharp/NativeParser/Library.cs | 82 ++++++ .../aisec/cpg/frontends/csharp/CSharp.kt | 192 ++++++++++++- .../cpg/frontends/csharp/StatementHandler.kt | 120 ++++++++- .../csharp/statementhandler/LoopsTest.kt | 23 ++ .../csharp/statementhandler/SwitchTest.kt | 254 ++++++++++++++++++ .../src/test/resources/csharp/Loops.cs | 8 + .../src/test/resources/csharp/Switch.cs | 65 +++++ 7 files changed, 733 insertions(+), 11 deletions(-) create mode 100644 cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/statementhandler/SwitchTest.kt create mode 100644 cpg-language-csharp/src/test/resources/csharp/Switch.cs diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index a856f2c556d..faa86294ac3 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -622,6 +622,88 @@ public static IntPtr GetForEachStatementStatement(IntPtr handlePtr) return Register(((ForEachStatementSyntax)Nodes[handlePtr]).Statement); } + [UnmanagedCallersOnly(EntryPoint = "GetSwitchStatementExpression")] + public static IntPtr GetSwitchStatementExpression(IntPtr handlePtr) + { + return Register(((SwitchStatementSyntax)Nodes[handlePtr]).Expression); + } + + [UnmanagedCallersOnly(EntryPoint = "GetSwitchStatementSectionCount")] + public static int GetSwitchStatementSectionCount(IntPtr handlePtr) + { + return ((SwitchStatementSyntax)Nodes[handlePtr]).Sections.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetSwitchStatementSection")] + public static IntPtr GetSwitchStatementSection(IntPtr handlePtr, int index) + { + return Register(((SwitchStatementSyntax)Nodes[handlePtr]).Sections[index]); + } + + [UnmanagedCallersOnly(EntryPoint = "GetSwitchSectionLabelCount")] + public static int GetSwitchSectionLabelCount(IntPtr handlePtr) + { + return ((SwitchSectionSyntax)Nodes[handlePtr]).Labels.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetSwitchSectionLabel")] + public static IntPtr GetSwitchSectionLabel(IntPtr handlePtr, int index) + { + return Register(((SwitchSectionSyntax)Nodes[handlePtr]).Labels[index]); + } + + [UnmanagedCallersOnly(EntryPoint = "GetSwitchSectionStatementCount")] + public static int GetSwitchSectionStatementCount(IntPtr handlePtr) + { + return ((SwitchSectionSyntax)Nodes[handlePtr]).Statements.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetSwitchSectionStatement")] + public static IntPtr GetSwitchSectionStatement(IntPtr handlePtr, int index) + { + return Register(((SwitchSectionSyntax)Nodes[handlePtr]).Statements[index]); + } + + [UnmanagedCallersOnly(EntryPoint = "GetCaseSwitchLabelValue")] + public static IntPtr GetCaseSwitchLabelValue(IntPtr handlePtr) + { + return Register(((CaseSwitchLabelSyntax)Nodes[handlePtr]).Value); + } + + [UnmanagedCallersOnly(EntryPoint = "GetCasePatternSwitchLabelPattern")] + public static IntPtr GetCasePatternSwitchLabelPattern(IntPtr handlePtr) + { + return Register(((CasePatternSwitchLabelSyntax)Nodes[handlePtr]).Pattern); + } + + [UnmanagedCallersOnly(EntryPoint = "GetCasePatternSwitchLabelWhenClause")] + public static IntPtr GetCasePatternSwitchLabelWhenClause(IntPtr handlePtr) + { + var whenClause = ((CasePatternSwitchLabelSyntax)Nodes[handlePtr]).WhenClause; + return whenClause != null ? Register(whenClause) : IntPtr.Zero; + } + + [UnmanagedCallersOnly(EntryPoint = "GetWhenClauseCondition")] + public static IntPtr GetWhenClauseCondition(IntPtr handlePtr) + { + return Register(((WhenClauseSyntax)Nodes[handlePtr]).Condition); + } + + [UnmanagedCallersOnly(EntryPoint = "GetVarPatternDesignation")] + public static IntPtr GetVarPatternDesignation(IntPtr handlePtr) + { + var varPattern = (VarPatternSyntax)Nodes[handlePtr]; + return Register(varPattern.Designation); + } + + [UnmanagedCallersOnly(EntryPoint = "GetSingleVariableDesignationIdentifier")] + public static IntPtr GetSingleVariableDesignationIdentifier(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8( + ((SingleVariableDesignationSyntax)Nodes[handlePtr]).Identifier.ToString() + ); + } + [UnmanagedCallersOnly(EntryPoint = "GetInvocationExpressionExpression")] public static IntPtr GetInvocationExpressionExpression(IntPtr handlePtr) { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index aad8d61a6dc..e8893969e9d 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -322,6 +322,8 @@ interface Csharp : Library { "DoStatementSyntax" -> DoStatementSyntax(nativeValue) "ForStatementSyntax" -> ForStatementSyntax(nativeValue) "ForEachStatementSyntax" -> ForEachStatementSyntax(nativeValue) + "SwitchStatementSyntax" -> SwitchStatementSyntax(nativeValue) + "BreakStatementSyntax" -> BreakStatementSyntax(nativeValue) else -> super.fromNative(nativeValue, context) } } @@ -416,6 +418,89 @@ interface Csharp : Library { val statement: StatementSyntax by lazy { INSTANCE.GetForEachStatementStatement(this) } } + /** + * Represents the Roslyn + * [`SwitchStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.switchstatementsyntax) + * class. + */ + class SwitchStatementSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) { + val expression: ExpressionSyntax by lazy { INSTANCE.GetSwitchStatementExpression(this) } + val sections: List by lazy { + val count = INSTANCE.GetSwitchStatementSectionCount(this) + (0 until count).map { i -> INSTANCE.GetSwitchStatementSection(this, i) } + } + } + + /** + * Represents the Roslyn + * [`SwitchSectionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.switchsectionsyntax) + * class. + */ + class SwitchSectionSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val labels: List by lazy { + val count = INSTANCE.GetSwitchSectionLabelCount(this) + (0 until count).map { i -> INSTANCE.GetSwitchSectionLabel(this, i) } + } + val statements: List by lazy { + val count = INSTANCE.GetSwitchSectionStatementCount(this) + (0 until count).map { i -> INSTANCE.GetSwitchSectionStatement(this, i) } + } + } + + /** + * Represents the Roslyn + * [`SwitchLabelSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.switchlabelsyntax) + * class. + */ + open class SwitchLabelSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + override fun fromNative(nativeValue: Any?, context: FromNativeContext?): Any { + if (nativeValue !is Pointer) { + return super.fromNative(nativeValue, context) + } + return when (INSTANCE.GetType(nativeValue)) { + "CaseSwitchLabelSyntax" -> CaseSwitchLabelSyntax(nativeValue) + "CasePatternSwitchLabelSyntax" -> CasePatternSwitchLabelSyntax(nativeValue) + "DefaultSwitchLabelSyntax" -> DefaultSwitchLabelSyntax(nativeValue) + else -> super.fromNative(nativeValue, context) + } + } + } + + /** + * Represents the Roslyn + * [`CaseSwitchLabelSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.caseswitchlabelsyntax) + * class. + */ + class CaseSwitchLabelSyntax(p: Pointer? = Pointer.NULL) : SwitchLabelSyntax(p) { + val value: ExpressionSyntax by lazy { INSTANCE.GetCaseSwitchLabelValue(this) } + } + + /** + * Represents the Roslyn + * [`DefaultSwitchLabelSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.defaultswitchlabelsyntax) + * class. + */ + class DefaultSwitchLabelSyntax(p: Pointer? = Pointer.NULL) : SwitchLabelSyntax(p) + + /** + * Represents the Roslyn + * [`CasePatternSwitchLabelSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.casepatternswitchlabelsyntax) + * class. Used for pattern matching in switch statements, e.g. `case var o when condition:`. + */ + class CasePatternSwitchLabelSyntax(p: Pointer? = Pointer.NULL) : SwitchLabelSyntax(p) { + val pattern: PatternSyntax by lazy { INSTANCE.GetCasePatternSwitchLabelPattern(this) } + val whenClause: WhenClauseSyntax? by lazy { + INSTANCE.GetCasePatternSwitchLabelWhenClause(this) + } + } + + /** + * Represents the Roslyn + * [`BreakStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.breakstatementsyntax) + * class. + */ + class BreakStatementSyntax(p: Pointer? = Pointer.NULL) : StatementSyntax(p) + /** * Represents the Roslyn * [`ConstructorDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.constructordeclarationsyntax) @@ -484,12 +569,84 @@ interface Csharp : Library { } } + /** + * Represents the Roslyn + * [`ExpressionOrPatternSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.expressiororpatternsyntax) + * base class. Common base for [ExpressionSyntax] and [PatternSyntax]. + */ + open class ExpressionOrPatternSyntax(p: Pointer? = Pointer.NULL) : Node(p) + + /** + * Represents the Roslyn + * [`PatternSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.patternsyntax) + * base class. + */ + open class PatternSyntax(p: Pointer? = Pointer.NULL) : ExpressionOrPatternSyntax(p) { + override fun fromNative(nativeValue: Any?, context: FromNativeContext?): Any { + if (nativeValue !is Pointer) { + return super.fromNative(nativeValue, context) + } + return when (INSTANCE.GetType(nativeValue)) { + "VarPatternSyntax" -> VarPatternSyntax(nativeValue) + else -> super.fromNative(nativeValue, context) + } + } + } + + /** + * Represents the Roslyn + * [`VariableDesignationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.variabledesignationsyntax) + * base class. + */ + open class VariableDesignationSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + override fun fromNative(nativeValue: Any?, context: FromNativeContext?): Any { + if (nativeValue !is Pointer) { + return super.fromNative(nativeValue, context) + } + return when (INSTANCE.GetType(nativeValue)) { + "SingleVariableDesignationSyntax" -> + SingleVariableDesignationSyntax(nativeValue) + else -> super.fromNative(nativeValue, context) + } + } + } + + /** + * Represents the Roslyn + * [`SingleVariableDesignationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.singlevariabledesignationsyntax) + * class. + */ + class SingleVariableDesignationSyntax(p: Pointer? = Pointer.NULL) : + VariableDesignationSyntax(p) { + val identifier: String by lazy { INSTANCE.GetSingleVariableDesignationIdentifier(this) } + } + + /** + * Represents the Roslyn + * [`VarPatternSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.varpatternsyntax) + * class. Used for `case var o:` patterns where the type is inferred. + */ + class VarPatternSyntax(p: Pointer? = Pointer.NULL) : PatternSyntax(p) { + val designation: VariableDesignationSyntax by lazy { + INSTANCE.GetVarPatternDesignation(this) + } + } + + /** + * Represents the Roslyn + * [`WhenClauseSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.whenclausesyntax) + * class. Contains the guard condition for pattern-matching case labels. + */ + class WhenClauseSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val condition: ExpressionSyntax by lazy { INSTANCE.GetWhenClauseCondition(this) } + } + /** * Represents the Roslyn * [`ExpressionSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.expressionsyntax) * class. */ - open class ExpressionSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + open class ExpressionSyntax(p: Pointer? = Pointer.NULL) : ExpressionOrPatternSyntax(p) { override fun fromNative(nativeValue: Any?, context: FromNativeContext?): Any? { if (nativeValue !is Pointer) { return super.fromNative(nativeValue, context) @@ -922,6 +1079,39 @@ interface Csharp : Library { fun GetForEachStatementStatement(handle: AST.ForEachStatementSyntax): AST.StatementSyntax + fun GetSwitchStatementExpression(handle: AST.SwitchStatementSyntax): AST.ExpressionSyntax + + fun GetSwitchStatementSectionCount(handle: AST.SwitchStatementSyntax): Int + + fun GetSwitchStatementSection( + handle: AST.SwitchStatementSyntax, + index: Int, + ): AST.SwitchSectionSyntax + + fun GetSwitchSectionLabelCount(handle: AST.SwitchSectionSyntax): Int + + fun GetSwitchSectionLabel(handle: AST.SwitchSectionSyntax, index: Int): AST.SwitchLabelSyntax + + fun GetSwitchSectionStatementCount(handle: AST.SwitchSectionSyntax): Int + + fun GetSwitchSectionStatement(handle: AST.SwitchSectionSyntax, index: Int): AST.StatementSyntax + + fun GetCaseSwitchLabelValue(handle: AST.CaseSwitchLabelSyntax): AST.ExpressionSyntax + + fun GetCasePatternSwitchLabelPattern( + handle: AST.CasePatternSwitchLabelSyntax + ): AST.PatternSyntax + + fun GetCasePatternSwitchLabelWhenClause( + handle: AST.CasePatternSwitchLabelSyntax + ): AST.WhenClauseSyntax? + + fun GetWhenClauseCondition(handle: AST.WhenClauseSyntax): AST.ExpressionSyntax + + fun GetVarPatternDesignation(handle: AST.VarPatternSyntax): AST.VariableDesignationSyntax + + fun GetSingleVariableDesignationIdentifier(handle: AST.SingleVariableDesignationSyntax): String + fun GetInvocationExpressionExpression( handle: AST.InvocationExpressionSyntax ): AST.ExpressionSyntax diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt index 414901c0929..73e33a68fa6 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt @@ -26,24 +26,21 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.graph.declarations.* -import de.fraunhofer.aisec.cpg.graph.expressions.Block -import de.fraunhofer.aisec.cpg.graph.expressions.DeclarationStatement -import de.fraunhofer.aisec.cpg.graph.expressions.DoWhile -import de.fraunhofer.aisec.cpg.graph.expressions.Expression -import de.fraunhofer.aisec.cpg.graph.expressions.For -import de.fraunhofer.aisec.cpg.graph.expressions.ForEach -import de.fraunhofer.aisec.cpg.graph.expressions.IfElse -import de.fraunhofer.aisec.cpg.graph.expressions.ProblemExpression -import de.fraunhofer.aisec.cpg.graph.expressions.Return -import de.fraunhofer.aisec.cpg.graph.expressions.While +import de.fraunhofer.aisec.cpg.graph.expressions.* +import de.fraunhofer.aisec.cpg.graph.implicit +import de.fraunhofer.aisec.cpg.graph.newBinaryOperator import de.fraunhofer.aisec.cpg.graph.newBlock +import de.fraunhofer.aisec.cpg.graph.newBreak +import de.fraunhofer.aisec.cpg.graph.newCase import de.fraunhofer.aisec.cpg.graph.newDeclarationStatement +import de.fraunhofer.aisec.cpg.graph.newDefault import de.fraunhofer.aisec.cpg.graph.newDoWhile import de.fraunhofer.aisec.cpg.graph.newExpressionList import de.fraunhofer.aisec.cpg.graph.newFor import de.fraunhofer.aisec.cpg.graph.newForEach import de.fraunhofer.aisec.cpg.graph.newIfElse import de.fraunhofer.aisec.cpg.graph.newReturn +import de.fraunhofer.aisec.cpg.graph.newSwitch import de.fraunhofer.aisec.cpg.graph.newVariable import de.fraunhofer.aisec.cpg.graph.newWhile @@ -63,6 +60,8 @@ class StatementHandler(frontend: CSharpLanguageFrontend) : is Csharp.AST.DoStatementSyntax -> handleDoWhile(node) is Csharp.AST.ForStatementSyntax -> handleFor(node) is Csharp.AST.ForEachStatementSyntax -> handleForEach(node) + is Csharp.AST.SwitchStatementSyntax -> handleSwitch(node) + is Csharp.AST.BreakStatementSyntax -> handleBreak(node) else -> ProblemExpression("Not supported: ${node.csharpType}") } } @@ -233,6 +232,107 @@ class StatementHandler(frontend: CSharpLanguageFrontend) : return forEachStmt } + /** + * Translates a [SwitchStatementSyntax][Csharp.AST.SwitchStatementSyntax] into a [Switch]. + * + * C# spec: + * [SwitchStatement](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#1383-the-switch-statement) + */ + private fun handleSwitch(node: Csharp.AST.SwitchStatementSyntax): Switch { + val switchStmt = newSwitch(rawNode = node) + frontend.scopeManager.enterScope(switchStmt) + switchStmt.selector = frontend.expressionHandler.handle(node.expression) + + val block = newBlock() + for (section in node.sections) { + for (label in section.labels) { + when (label) { + is Csharp.AST.CaseSwitchLabelSyntax -> { + val caseStmt = newCase(rawNode = label) + caseStmt.caseExpression = frontend.expressionHandler.handle(label.value) + block.statements += caseStmt + } + is Csharp.AST.CasePatternSwitchLabelSyntax -> { + block.statements += handleCasePattern(label) + } + is Csharp.AST.DefaultSwitchLabelSyntax -> { + block.statements += newDefault(rawNode = label) + } + } + } + for (stmt in section.statements) { + block.statements += handle(stmt) + } + } + switchStmt.statement = block + + frontend.scopeManager.leaveScope(switchStmt) + return switchStmt + } + + /** + * Handles a [CasePatternSwitchLabelSyntax][Csharp.AST.CasePatternSwitchLabelSyntax], which + * represents C# pattern matching in switch statements, e.g. `case var a when condition:`. + * + * This is modeled as follows: + * - A [VarPatternSyntax][Csharp.AST.VarPatternSyntax] is translated into a variable declaration + * based on its [VariableDesignationSyntax][Csharp.AST.VariableDesignationSyntax] (e.g. + * `SingleVariableDesignationSyntax` for a single variable like `a` in `case var a`). + * - If a `when` clause is present, the case expression is wrapped in an implicit `and` + * [BinaryOperator] with lhs as a declaration and rhs with a condition. + */ + private fun handleCasePattern(node: Csharp.AST.CasePatternSwitchLabelSyntax): Expression { + val caseStmt = newCase(rawNode = node) + val pattern = node.pattern + val whenClause = node.whenClause + + var patternExpr = + when (pattern) { + is Csharp.AST.VarPatternSyntax -> { + val identifier = + when (val designation = pattern.designation) { + is Csharp.AST.SingleVariableDesignationSyntax -> designation.identifier + else -> + return ProblemExpression( + "Variable designation type not yet supported: ${designation.csharpType}" + ) + } + val variable = newVariable(name = identifier, rawNode = pattern) + frontend.scopeManager.addDeclaration(variable) + val declStmt = newDeclarationStatement(rawNode = pattern) + declStmt.declarations += variable + declStmt + } + else -> { + ProblemExpression("Pattern type not yet supported: ${pattern.csharpType}") + } + } + + // If a `when` clause is present, translate the pattern and the condition into an implicit + // 'and' BinaryOperator. + if (whenClause != null) { + val whenCondition = frontend.expressionHandler.handle(whenClause.condition) + patternExpr = + newBinaryOperator(operatorCode = "and").implicit().apply { + this.lhs = patternExpr + this.rhs = whenCondition + } + } + + caseStmt.caseExpression = patternExpr + return caseStmt + } + + /** + * Translates a [BreakStatementSyntax][Csharp.AST.BreakStatementSyntax] into a [Break]. + * + * C# spec: + * [Break statement](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#13101-the-break-statement) + */ + private fun handleBreak(node: Csharp.AST.BreakStatementSyntax): Break { + return newBreak(rawNode = node) + } + /** * Translates a [BlockSyntax][Csharp.AST.BlockSyntax] into a [Block]. * diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/statementhandler/LoopsTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/statementhandler/LoopsTest.kt index 6c778cb5143..ecd9e77c2c1 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/statementhandler/LoopsTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/statementhandler/LoopsTest.kt @@ -75,6 +75,29 @@ class LoopsTest : BaseTest() { assertEquals(iVar, lhsRef.refersTo) } + @Test + fun testWhileLoopWithBreak() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("Loops.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + assertNotNull(tu) + + val method = tu.methods["whileLoopWithBreak"] + assertNotNull(method) + val body = method.body + assertIs(body) + + val whileStmt = body.statements.getOrNull(0) + assertIs(whileStmt) + val loopBody = whileStmt.statement + assertIs(loopBody) + + val breakStmt = loopBody.statements.getOrNull(0) + assertIs(breakStmt) + } + @Test fun testDoWhileLoop() { val topLevel = Path.of("src", "test", "resources", "csharp") diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/statementhandler/SwitchTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/statementhandler/SwitchTest.kt new file mode 100644 index 00000000000..1456fd670b5 --- /dev/null +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/statementhandler/SwitchTest.kt @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 de.fraunhofer.aisec.cpg.frontends.csharp.statementhandler + +import de.fraunhofer.aisec.cpg.frontends.csharp.CSharpLanguage +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.declarations.Variable +import de.fraunhofer.aisec.cpg.graph.expressions.* +import de.fraunhofer.aisec.cpg.test.* +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +class SwitchTest : BaseTest() { + + @Test + fun switchStatementTest() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("Switch.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + assertNotNull(tu) + + val method = tu.methods["SwitchStmt"] + assertNotNull(method) + val body = method.body + assertIs(body) + + // int counter = 0; + val decl = body.statements[0] + assertIs(decl) + + // switch (x) {} + val switchStmt = body.statements[1] + assertIs(switchStmt) + + val selector = switchStmt.selector + assertIs(selector) + assertEquals("x", selector.name.localName) + + val switchBody = switchStmt.statement + assertIs(switchBody) + + // case 1: + val case1 = switchBody.statements[0] + assertIs(case1) + val case1Expr = case1.caseExpression + assertIs>(case1Expr) + assertEquals(1, case1Expr.value) + + // counter = 10; + val assign1 = switchBody.statements[1] + assertIs(assign1) + + // break; + val break1 = switchBody.statements[2] + assertIs(break1) + + // case 2: + val case2 = switchBody.statements[3] + assertIs(case2) + + // case 3: + val case3 = switchBody.statements[4] + assertIs(case3) + + // counter = 20; + val assign2 = switchBody.statements[5] + assertIs(assign2) + + // break; + val break2 = switchBody.statements[6] + assertIs(break2) + + // default: + val defaultStmt = switchBody.statements[7] + assertIs(defaultStmt) + + // counter = -1; + val assign3 = switchBody.statements[8] + assertIs(assign3) + + // break; + val break3 = switchBody.statements[9] + assertIs(break3) + + // return counter; + val returnStmt = body.statements[2] + assertIs(returnStmt) + } + + @Test + fun caseWithDefaultTest() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("Switch.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + assertNotNull(tu) + + val method = tu.methods["CaseWithDefault"] + assertNotNull(method) + val body = method.body + assertIs(body) + + val switchStmt = body.statements[0] + assertIs(switchStmt) + + val switchBody = switchStmt.statement + assertIs(switchBody) + + // case 0: CaseZero(); break; + assertIs(switchBody.statements[0]) + assertIs(switchBody.statements[2]) + + // case 1: CaseOne(); break; + assertIs(switchBody.statements[3]) + assertIs(switchBody.statements[5]) + + // case 2: and default: (same section) + val case2 = switchBody.statements[6] + assertIs(case2) + assertEquals(2, (case2.caseExpression as Literal<*>).value) + val defaultStmt = switchBody.statements[7] + assertIs(defaultStmt) + + // CaseTwo(); break; + assertIs(switchBody.statements[9]) + } + + @Test + fun stringSwitchTest() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("Switch.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + assertNotNull(tu) + + val method = tu.methods["StringSwitch"] + assertNotNull(method) + val body = method.body + assertIs(body) + + val switchStmt = body.statements[0] + assertIs(switchStmt) + + val selector = switchStmt.selector + assertIs(selector) + assertEquals("command", selector.name.localName) + + val switchBody = switchStmt.statement + assertIs(switchBody) + + // case "run": return "running"; + val case1 = switchBody.statements[0] + assertIs(case1) + val case1Expr = case1.caseExpression + assertIs>(case1Expr) + assertEquals("run", case1Expr.value) + + val return1 = switchBody.statements[1] + assertIs(return1) + + // case "stop": return "stopped"; + val case2 = switchBody.statements[2] + assertIs(case2) + assertEquals("stop", (case2.caseExpression as Literal<*>).value) + + // default: return "unknown"; + val defaultStmt = switchBody.statements[4] + assertIs(defaultStmt) + } + + @Test + fun patternMatchSwitchTest() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("Switch.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + assertNotNull(tu) + + val method = tu.methods["PatternMatchSwitch"] + assertNotNull(method) + val body = method.body + assertIs(body) + + val switchStmt = body.statements[0] + assertIs(switchStmt) + + val switchBody = switchStmt.statement + assertIs(switchBody) + + val firstCase = switchBody.statements[0] + assertIs(firstCase) + assertEquals("first case", (firstCase.caseExpression as Literal<*>).value) + + // case var a when a.Length > 10: return a; + // This is modeled as a Case whose caseExpression is an implicit `and` BinaryOperator. + // The lhs is the declaration of variable `o`, the rhs is the condition `o.Length > 10`. + val patternCase = switchBody.statements[2] + assertIs(patternCase) + val andOp = patternCase.caseExpression + assertIs(andOp) + assertEquals("and", andOp.operatorCode) + + // lhs: variable `a` + val patternDecl = andOp.lhs + assertIs(patternDecl) + assertEquals(1, patternDecl.declarations.size) + val aVariable = patternDecl.declarations.single() + assertIs(aVariable) + assertEquals("a", aVariable.name.localName) + + // rhs: condition `a.Length > 10` + val condition = andOp.rhs + assertIs(condition) + + val returnStmt = switchBody.statements[3] + assertIs(returnStmt) + assertUsageOf(returnStmt.returnValue, aVariable) + + // default: return "unknown"; + val defaultStmt = switchBody.statements[4] + assertIs(defaultStmt) + } +} diff --git a/cpg-language-csharp/src/test/resources/csharp/Loops.cs b/cpg-language-csharp/src/test/resources/csharp/Loops.cs index 426e720fcad..1c7121914c0 100644 --- a/cpg-language-csharp/src/test/resources/csharp/Loops.cs +++ b/cpg-language-csharp/src/test/resources/csharp/Loops.cs @@ -11,6 +11,14 @@ void whileLoop() } } + void whileLoopWithBreak() + { + while (true) + { + break; + } + } + void doWhileLoop() { int i = 0; diff --git a/cpg-language-csharp/src/test/resources/csharp/Switch.cs b/cpg-language-csharp/src/test/resources/csharp/Switch.cs new file mode 100644 index 00000000000..5581ad59ce2 --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/Switch.cs @@ -0,0 +1,65 @@ +class Foo +{ + int SwitchStmt(int x) + { + int counter = 0; + switch (x) + { + case 1: + counter = 10; + break; + case 2: + case 3: + counter = 20; + break; + default: + counter = -1; + break; + } + return counter; + } + + void CaseWithDefault(int i) + { + switch (i) + { + case 0: + CaseZero(); + break; + case 1: + CaseOne(); + break; + case 2: + default: + CaseTwo(); + break; + } + } + + string StringSwitch(string command) + { + switch (command) + { + case "run": + return "running"; + case "stop": + return "stopped"; + default: + return "unknown"; + } + } + + string PatternMatchSwitch(string description) + { + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#1383-the-switch-statement + switch (description) + { + case "first case": + return "first"; + case var a when a.Length > 10: + return a; + default: + return "unknown"; + } + } +} \ No newline at end of file From 9134d99b8c55ec827ee992acc6fefa0626044c54 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Fri, 10 Apr 2026 16:57:37 +0200 Subject: [PATCH 51/55] Remove comment --- .../fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt index 73e33a68fa6..dc474640229 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/StatementHandler.kt @@ -308,8 +308,6 @@ class StatementHandler(frontend: CSharpLanguageFrontend) : } } - // If a `when` clause is present, translate the pattern and the condition into an implicit - // 'and' BinaryOperator. if (whenClause != null) { val whenCondition = frontend.expressionHandler.handle(whenClause.condition) patternExpr = From fccc9def3e2a05618b08e7e0d9836b1b74c3eaf3 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Mon, 13 Apr 2026 10:01:39 +0200 Subject: [PATCH 52/55] Add Enum declarations support --- .../src/main/csharp/NativeParser/Library.cs | 13 ++++++ .../aisec/cpg/frontends/csharp/CSharp.kt | 41 +++++++++++++++++++ .../frontends/csharp/DeclarationHandler.kt | 35 ++++++++++++++++ .../csharp/CSharpLanguageFrontendTest.kt | 38 +++++++++++++++++ .../src/test/resources/csharp/Enums.cs | 15 +++++++ 5 files changed, 142 insertions(+) create mode 100644 cpg-language-csharp/src/test/resources/csharp/Enums.cs diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index faa86294ac3..b7ebad573d0 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -174,6 +174,13 @@ public static IntPtr GetEnumMemberDeclarationIdentifier(IntPtr handlePtr) ); } + [UnmanagedCallersOnly(EntryPoint = "GetEnumMemberDeclarationEqualsValue")] + public static IntPtr GetEnumMemberDeclarationEqualsValue(IntPtr handlePtr) + { + var equalsValue = ((EnumMemberDeclarationSyntax)Nodes[handlePtr]).EqualsValue; + return equalsValue != null ? Register(equalsValue.Value) : IntPtr.Zero; + } + // Identifier for Class, Interface, Struct, Record, Enum. [UnmanagedCallersOnly(EntryPoint = "GetBaseTypeDeclarationIdentifier")] public static IntPtr GetBaseTypeDeclarationIdentifier(IntPtr handlePtr) @@ -261,6 +268,12 @@ public static IntPtr GetMethodDeclarationIdentifier(IntPtr handlePtr) ); } + [UnmanagedCallersOnly(EntryPoint = "GetMethodDeclarationReturnType")] + public static IntPtr GetMethodDeclarationReturnType(IntPtr handlePtr) + { + return Register(((MethodDeclarationSyntax)Nodes[handlePtr]).ReturnType); + } + [UnmanagedCallersOnly(EntryPoint = "GetCode")] public static IntPtr GetCode(IntPtr handlePtr) { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index e8893969e9d..538227002a4 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -130,6 +130,7 @@ interface Csharp : Library { FileScopedNamespaceDeclarationSyntax(nativeValue) "ClassDeclarationSyntax" -> ClassDeclarationSyntax(nativeValue) "InterfaceDeclarationSyntax" -> InterfaceDeclarationSyntax(nativeValue) + "EnumDeclarationSyntax" -> EnumDeclarationSyntax(nativeValue) "FieldDeclarationSyntax" -> FieldDeclarationSyntax(nativeValue) "MethodDeclarationSyntax" -> MethodDeclarationSyntax(nativeValue) "ConstructorDeclarationSyntax" -> ConstructorDeclarationSyntax(nativeValue) @@ -213,6 +214,30 @@ interface Csharp : Library { */ class InterfaceDeclarationSyntax(p: Pointer? = Pointer.NULL) : TypeDeclarationSyntax(p) + /** + * Represents the Roslyn + * [`EnumDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.enumdeclarationsyntax) + * class. + */ + class EnumDeclarationSyntax(p: Pointer? = Pointer.NULL) : BaseTypeDeclarationSyntax(p) { + val members: List by lazy { + val count = INSTANCE.GetEnumDeclarationMembersCount(this) + (0 until count).map { i -> INSTANCE.GetEnumDeclarationMember(this, i) } + } + } + + /** + * Represents the Roslyn + * [`EnumMemberDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.enummemberdeclarationsyntax) + * class. + */ + class EnumMemberDeclarationSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { + val identifier: String by lazy { INSTANCE.GetEnumMemberDeclarationIdentifier(this) } + val equalsValue: ExpressionSyntax? by lazy { + INSTANCE.GetEnumMemberDeclarationEqualsValue(this) + } + } + /** * Represents the Roslyn * [`BaseListSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.baselistsyntax) @@ -287,6 +312,7 @@ interface Csharp : Library { */ class MethodDeclarationSyntax(p: Pointer? = Pointer.NULL) : BaseMethodDeclarationSyntax(p) { val identifier: String by lazy { INSTANCE.GetMethodDeclarationIdentifier(this) } + val returnType: TypeSyntax by lazy { INSTANCE.GetMethodDeclarationReturnType(this) } } /** @@ -934,6 +960,19 @@ interface Csharp : Library { fun GetNameEqualsSyntaxName(handle: AST.NameEqualsSyntax): String + fun GetEnumDeclarationMembersCount(handle: AST.EnumDeclarationSyntax): Int + + fun GetEnumDeclarationMember( + handle: AST.EnumDeclarationSyntax, + index: Int, + ): AST.EnumMemberDeclarationSyntax + + fun GetEnumMemberDeclarationIdentifier(handle: AST.EnumMemberDeclarationSyntax): String + + fun GetEnumMemberDeclarationEqualsValue( + handle: AST.EnumMemberDeclarationSyntax + ): AST.ExpressionSyntax? + fun GetBaseTypeDeclarationIdentifier(handle: AST.BaseTypeDeclarationSyntax): String fun GetTypeDeclarationMembersCount(handle: AST.TypeDeclarationSyntax): Int @@ -965,6 +1004,8 @@ interface Csharp : Library { fun GetMethodDeclarationIdentifier(handle: AST.MethodDeclarationSyntax): String + fun GetMethodDeclarationReturnType(handle: AST.MethodDeclarationSyntax): AST.TypeSyntax + fun GetBaseMethodDeclarationParameterList( handle: AST.BaseMethodDeclarationSyntax ): AST.ParameterListSyntax diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt index 675068568b8..6aab4d16239 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -30,6 +30,8 @@ import de.fraunhofer.aisec.cpg.graph.expressions.Construction import de.fraunhofer.aisec.cpg.graph.expressions.New import de.fraunhofer.aisec.cpg.graph.implicit import de.fraunhofer.aisec.cpg.graph.newConstructor +import de.fraunhofer.aisec.cpg.graph.newEnumConstant +import de.fraunhofer.aisec.cpg.graph.newEnumeration import de.fraunhofer.aisec.cpg.graph.newField import de.fraunhofer.aisec.cpg.graph.newMethod import de.fraunhofer.aisec.cpg.graph.newNamespace @@ -46,6 +48,7 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : return when (node) { is Csharp.AST.BaseNamespaceDeclarationSyntax -> handleNamespaceDeclaration(node) is Csharp.AST.ClassDeclarationSyntax -> handleClassDeclaration(node) + is Csharp.AST.EnumDeclarationSyntax -> handleEnumDeclaration(node) is Csharp.AST.MethodDeclarationSyntax -> handleMethodDeclaration(node) is Csharp.AST.ConstructorDeclarationSyntax -> handleConstructorDeclaration(node) is Csharp.AST.InterfaceDeclarationSyntax -> handleInterfaceDeclaration(node) @@ -176,6 +179,37 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : return record } + /** + * Translates an [EnumDeclarationSyntax][Csharp.AST.EnumDeclarationSyntax] into an + * [Enumeration]. + * + * C# spec: + * [Enum declarations](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/enums) + */ + private fun handleEnumDeclaration(node: Csharp.AST.EnumDeclarationSyntax): Declaration { + val enumeration = newEnumeration(node.identifier, rawNode = node) + enumeration.kind = "enum" + enumeration.modifiers = node.modifiers.toSet() + frontend.scopeManager.enterScope(enumeration) + + val enumType = enumeration.toType() + enumeration.entries = + node.members + .map { member -> + val enumConstant = newEnumConstant(member.identifier, rawNode = member) + enumConstant.type = enumType + member.equalsValue?.let { + enumConstant.initializer = frontend.expressionHandler.handle(it) + } + frontend.scopeManager.addDeclaration(enumConstant) + enumConstant + } + .toMutableList() + + frontend.scopeManager.leaveScope(enumeration) + return enumeration + } + /** * Translates a [MethodDeclarationSyntax][Csharp.AST.MethodDeclarationSyntax] into a [Method]. * @@ -199,6 +233,7 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : frontend.scopeManager.addDeclaration(param) method.parameters += param } + method.returnTypes = listOf(frontend.typeOf(node.returnType)) method.body = frontend.statementHandler.handle(node.body) frontend.scopeManager.leaveScope(method) return method diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt index 111e5de0c69..543122b721c 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt @@ -27,6 +27,7 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.declarations.Constructor +import de.fraunhofer.aisec.cpg.graph.declarations.Enumeration import de.fraunhofer.aisec.cpg.graph.declarations.Field import de.fraunhofer.aisec.cpg.graph.declarations.Parameter import de.fraunhofer.aisec.cpg.graph.expressions.Assign @@ -436,4 +437,41 @@ class CSharpLanguageFrontendTest : BaseTest() { assertNotNull(typeT) assertIs(typeT) } + + @Test + fun testEnums() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU(listOf(topLevel.resolve("Enums.cs").toFile()), topLevel, true) { + it.registerLanguage() + } + assertNotNull(tu) + + val enumsEnum = tu.records["Enums"] + assertIs(enumsEnum) + assertEquals("enum", enumsEnum.kind) + assertEquals(listOf("A", "B", "C"), enumsEnum.entries.map { it.name.localName }) + + val b = enumsEnum.entries.single { it.name.localName == "B" } + assertEquals(enumsEnum, b.type.recordDeclaration) + val equalsValueExpression = b.initializer + assertIs>(equalsValueExpression) + assertEquals(5, equalsValueExpression.value) + + val foo = tu.records["Foo"] + assertNotNull(foo) + val bar = foo.methods["Bar"] + assertNotNull(bar) + assertEquals(enumsEnum, bar.returnTypes.singleOrNull()?.recordDeclaration) + + val body = bar.body + assertIs(body) + val returnStmt = body.statements.singleOrNull() + assertIs(returnStmt) + val returnValue = returnStmt.returnValue + assertIs(returnValue) + val base = returnValue.base + assertIs(base) + assertEquals(enumsEnum, base.refersTo) + } } diff --git a/cpg-language-csharp/src/test/resources/csharp/Enums.cs b/cpg-language-csharp/src/test/resources/csharp/Enums.cs new file mode 100644 index 00000000000..9ad73a4a984 --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/Enums.cs @@ -0,0 +1,15 @@ +enum Enums +{ + A, + B = 5, + C +} + +class Foo +{ + Enums Bar() + { + return Enums.B; + } +} + From 5fbe51d8e9b0e25c429389c6d91c07df6ebe554d Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Mon, 27 Apr 2026 00:05:08 +0200 Subject: [PATCH 53/55] Property declaration cs calls --- .../src/main/csharp/NativeParser/Library.cs | 83 +++++++++++ .../aisec/cpg/frontends/csharp/CSharp.kt | 86 ++++++++++- .../frontends/csharp/DeclarationHandler.kt | 138 ++++++++++++++++++ .../src/test/resources/csharp/Properties.cs | 34 +++++ 4 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 cpg-language-csharp/src/test/resources/csharp/Properties.cs diff --git a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs index b7ebad573d0..0a46216d005 100644 --- a/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs +++ b/cpg-language-csharp/src/main/csharp/NativeParser/Library.cs @@ -309,6 +309,89 @@ public static IntPtr GetFieldDeclaration(IntPtr handlePtr) return Register(((FieldDeclarationSyntax)Nodes[handlePtr]).Declaration); } + [UnmanagedCallersOnly(EntryPoint = "GetPropertyDeclarationIdentifier")] + public static IntPtr GetPropertyDeclarationIdentifier(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8( + ((PropertyDeclarationSyntax)Nodes[handlePtr]).Identifier.ToString() + ); + } + + [UnmanagedCallersOnly(EntryPoint = "GetPropertyDeclarationType")] + public static IntPtr GetPropertyDeclarationType(IntPtr handlePtr) + { + return Register(((PropertyDeclarationSyntax)Nodes[handlePtr]).Type); + } + + [UnmanagedCallersOnly(EntryPoint = "GetPropertyDeclarationAccessorList")] + public static IntPtr GetPropertyDeclarationAccessorList(IntPtr handlePtr) + { + var accessorList = ((PropertyDeclarationSyntax)Nodes[handlePtr]).AccessorList; + return accessorList != null ? Register(accessorList) : IntPtr.Zero; + } + + [UnmanagedCallersOnly(EntryPoint = "GetPropertyDeclarationExpressionBody")] + public static IntPtr GetPropertyDeclarationExpressionBody(IntPtr handlePtr) + { + var expressionBody = ((PropertyDeclarationSyntax)Nodes[handlePtr]).ExpressionBody; + return expressionBody != null ? Register(expressionBody.Expression) : IntPtr.Zero; + } + + [UnmanagedCallersOnly(EntryPoint = "GetPropertyDeclarationInitializer")] + public static IntPtr GetPropertyDeclarationInitializer(IntPtr handlePtr) + { + var initializer = ((PropertyDeclarationSyntax)Nodes[handlePtr]).Initializer; + return initializer != null ? Register(initializer.Value) : IntPtr.Zero; + } + + [UnmanagedCallersOnly(EntryPoint = "GetAccessorListAccessorCount")] + public static int GetAccessorListAccessorCount(IntPtr handlePtr) + { + return ((AccessorListSyntax)Nodes[handlePtr]).Accessors.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetAccessorListAccessor")] + public static IntPtr GetAccessorListAccessor(IntPtr handlePtr, int index) + { + return Register(((AccessorListSyntax)Nodes[handlePtr]).Accessors[index]); + } + + [UnmanagedCallersOnly(EntryPoint = "GetAccessorDeclarationKeyword")] + public static IntPtr GetAccessorDeclarationKeyword(IntPtr handlePtr) + { + return Marshal.StringToCoTaskMemUTF8( + ((AccessorDeclarationSyntax)Nodes[handlePtr]).Keyword.Text + ); + } + + [UnmanagedCallersOnly(EntryPoint = "GetAccessorDeclarationModifierCount")] + public static int GetAccessorDeclarationModifierCount(IntPtr handlePtr) + { + return ((AccessorDeclarationSyntax)Nodes[handlePtr]).Modifiers.Count; + } + + [UnmanagedCallersOnly(EntryPoint = "GetAccessorDeclarationModifier")] + public static IntPtr GetAccessorDeclarationModifier(IntPtr handlePtr, int index) + { + return Marshal.StringToCoTaskMemUTF8( + ((AccessorDeclarationSyntax)Nodes[handlePtr]).Modifiers[index].Text + ); + } + + [UnmanagedCallersOnly(EntryPoint = "GetAccessorDeclarationBody")] + public static IntPtr GetAccessorDeclarationBody(IntPtr handlePtr) + { + var body = ((AccessorDeclarationSyntax)Nodes[handlePtr]).Body; + return body != null ? Register(body) : IntPtr.Zero; + } + + [UnmanagedCallersOnly(EntryPoint = "GetAccessorDeclarationExpressionBody")] + public static IntPtr GetAccessorDeclarationExpressionBody(IntPtr handlePtr) + { + var expressionBody = ((AccessorDeclarationSyntax)Nodes[handlePtr]).ExpressionBody; + return expressionBody != null ? Register(expressionBody.Expression) : IntPtr.Zero; + } + [UnmanagedCallersOnly(EntryPoint = "GetVariableDeclaratorIdentifier")] public static IntPtr GetVariableDeclaratorIdentifier(IntPtr handlePtr) { diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt index 538227002a4..8476cd1f9b7 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharp.kt @@ -132,6 +132,7 @@ interface Csharp : Library { "InterfaceDeclarationSyntax" -> InterfaceDeclarationSyntax(nativeValue) "EnumDeclarationSyntax" -> EnumDeclarationSyntax(nativeValue) "FieldDeclarationSyntax" -> FieldDeclarationSyntax(nativeValue) + "PropertyDeclarationSyntax" -> PropertyDeclarationSyntax(nativeValue) "MethodDeclarationSyntax" -> MethodDeclarationSyntax(nativeValue) "ConstructorDeclarationSyntax" -> ConstructorDeclarationSyntax(nativeValue) else -> super.fromNative(nativeValue, context) @@ -333,7 +334,7 @@ interface Csharp : Library { * class. */ open class StatementSyntax(p: Pointer? = Pointer.NULL) : Node(p) { - override fun fromNative(nativeValue: Any?, context: FromNativeContext?): Any { + override fun fromNative(nativeValue: Any?, context: FromNativeContext?): Any? { if (nativeValue !is Pointer) { return super.fromNative(nativeValue, context) } @@ -548,6 +549,54 @@ interface Csharp : Library { } } + /** + * Represents the Roslyn + * [`PropertyDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.propertydeclarationsyntax) + * class. + */ + class PropertyDeclarationSyntax(p: Pointer? = Pointer.NULL) : MemberDeclarationSyntax(p) { + val identifier: String by lazy { INSTANCE.GetPropertyDeclarationIdentifier(this) } + val type: TypeSyntax by lazy { INSTANCE.GetPropertyDeclarationType(this) } + val accessorList: AccessorListSyntax? by lazy { + INSTANCE.GetPropertyDeclarationAccessorList(this) + } + val expressionBody: ExpressionSyntax? by lazy { + INSTANCE.GetPropertyDeclarationExpressionBody(this) + } + val initializer: ExpressionSyntax? by lazy { + INSTANCE.GetPropertyDeclarationInitializer(this) + } + } + + /** + * Represents the Roslyn + * [`AccessorListSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.accessorlistsyntax) + * class. + */ + class AccessorListSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val accessors: List by lazy { + val count = INSTANCE.GetAccessorListAccessorCount(this) + (0 until count).map { i -> INSTANCE.GetAccessorListAccessor(this, i) } + } + } + + /** + * Represents the Roslyn + * [`AccessorDeclarationSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.accessordeclarationsyntax) + * class. + */ + class AccessorDeclarationSyntax(p: Pointer? = Pointer.NULL) : Node(p) { + val keyword: String by lazy { INSTANCE.GetAccessorDeclarationKeyword(this) } + val body: BlockSyntax? by lazy { INSTANCE.GetAccessorDeclarationBody(this) } + val expressionBody: ExpressionSyntax? by lazy { + INSTANCE.GetAccessorDeclarationExpressionBody(this) + } + val modifiers: List by lazy { + val count = INSTANCE.GetAccessorDeclarationModifierCount(this) + (0 until count).map { i -> INSTANCE.GetAccessorDeclarationModifier(this, i) } + } + } + /** * Represents the Roslyn * [`LocalDeclarationStatementSyntax`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.localdeclarationstatementsyntax) @@ -1000,6 +1049,41 @@ interface Csharp : Library { fun GetFieldDeclaration(handle: AST.FieldDeclarationSyntax): AST.VariableDeclarationSyntax + fun GetPropertyDeclarationIdentifier(handle: AST.PropertyDeclarationSyntax): String + + fun GetPropertyDeclarationType(handle: AST.PropertyDeclarationSyntax): AST.TypeSyntax + + fun GetPropertyDeclarationAccessorList( + handle: AST.PropertyDeclarationSyntax + ): AST.AccessorListSyntax? + + fun GetPropertyDeclarationExpressionBody( + handle: AST.PropertyDeclarationSyntax + ): AST.ExpressionSyntax? + + fun GetPropertyDeclarationInitializer( + handle: AST.PropertyDeclarationSyntax + ): AST.ExpressionSyntax? + + fun GetAccessorListAccessorCount(handle: AST.AccessorListSyntax): Int + + fun GetAccessorListAccessor( + handle: AST.AccessorListSyntax, + index: Int, + ): AST.AccessorDeclarationSyntax + + fun GetAccessorDeclarationKeyword(handle: AST.AccessorDeclarationSyntax): String + + fun GetAccessorDeclarationBody(handle: AST.AccessorDeclarationSyntax): AST.BlockSyntax? + + fun GetAccessorDeclarationExpressionBody( + handle: AST.AccessorDeclarationSyntax + ): AST.ExpressionSyntax? + + fun GetAccessorDeclarationModifierCount(handle: AST.AccessorDeclarationSyntax): Int + + fun GetAccessorDeclarationModifier(handle: AST.AccessorDeclarationSyntax, index: Int): String + fun GetVariableDeclaratorIdentifier(handle: AST.VariableDeclaratorSyntax): String fun GetMethodDeclarationIdentifier(handle: AST.MethodDeclarationSyntax): String diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt index 6aab4d16239..28733e367b7 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -27,16 +27,22 @@ package de.fraunhofer.aisec.cpg.frontends.csharp import de.fraunhofer.aisec.cpg.graph.declarations.* import de.fraunhofer.aisec.cpg.graph.expressions.Construction +import de.fraunhofer.aisec.cpg.graph.expressions.MemberAccess import de.fraunhofer.aisec.cpg.graph.expressions.New import de.fraunhofer.aisec.cpg.graph.implicit +import de.fraunhofer.aisec.cpg.graph.newAssign +import de.fraunhofer.aisec.cpg.graph.newBlock import de.fraunhofer.aisec.cpg.graph.newConstructor import de.fraunhofer.aisec.cpg.graph.newEnumConstant import de.fraunhofer.aisec.cpg.graph.newEnumeration import de.fraunhofer.aisec.cpg.graph.newField +import de.fraunhofer.aisec.cpg.graph.newMemberAccess import de.fraunhofer.aisec.cpg.graph.newMethod import de.fraunhofer.aisec.cpg.graph.newNamespace import de.fraunhofer.aisec.cpg.graph.newParameter import de.fraunhofer.aisec.cpg.graph.newRecord +import de.fraunhofer.aisec.cpg.graph.newReference +import de.fraunhofer.aisec.cpg.graph.newReturn import de.fraunhofer.aisec.cpg.graph.newVariable import de.fraunhofer.aisec.cpg.graph.parseName import de.fraunhofer.aisec.cpg.graph.types.ParameterizedType @@ -50,6 +56,7 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : is Csharp.AST.ClassDeclarationSyntax -> handleClassDeclaration(node) is Csharp.AST.EnumDeclarationSyntax -> handleEnumDeclaration(node) is Csharp.AST.MethodDeclarationSyntax -> handleMethodDeclaration(node) + is Csharp.AST.PropertyDeclarationSyntax -> handlePropertyDeclaration(node) is Csharp.AST.ConstructorDeclarationSyntax -> handleConstructorDeclaration(node) is Csharp.AST.InterfaceDeclarationSyntax -> handleInterfaceDeclaration(node) else -> ProblemDeclaration("Not supported: ${node.csharpType}") @@ -239,6 +246,137 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : return method } + /** + * Translates a [PropertyDeclarationSyntax][Csharp.AST.PropertyDeclarationSyntax] into a [Field] + * representing its backing field, and additionally adds the respective getter and setter + * methods to the current record. Accessor bodies follow the C# spec: + * - An accessor with an explicit block body (`get { ... }`) keeps that body. + * - An accessor with an expression body (`get => expr;`) is wrapped into an implicit block + * containing a return (for getters) or an assignment to the backing field (for setters). + * - An auto-accessor (`get;` / `set;`) gets a synthesized body that reads from or writes to the + * backing field (`return this.;` or `this. = value;`). + * - A property-level expression body (`int X => expr;`) produces a single `get` accessor. + * + * C# spec: + * [Properties](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#157-properties) + */ + private fun handlePropertyDeclaration(node: Csharp.AST.PropertyDeclarationSyntax): Declaration { + val record = frontend.scopeManager.currentRecord + val propertyType = frontend.typeOf(node.type) + + val field = newField(node.identifier, rawNode = node) + field.type = propertyType + field.modifiers = node.modifiers.toSet() + + node.initializer?.let { field.initializer = frontend.expressionHandler.handle(it) } + + node.accessorList?.accessors?.forEach { accessor -> + val method = + newMethod( + "${accessor.keyword}_${node.identifier}", + recordDeclaration = record, + rawNode = accessor, + ) + // An accessor-level access modifier (e.g. `private set`) replaces the property's + // access modifier; otherwise the accessor inherits the property's modifiers. + method.modifiers = + if (accessor.modifiers.isNotEmpty()) accessor.modifiers.toSet() + else node.modifiers.toSet() + frontend.scopeManager.enterScope(method) + createMethodReceiver(method) + + val isSetter = accessor.keyword == "set" || accessor.keyword == "init" + if (isSetter) { + val param = newParameter("value", propertyType, rawNode = accessor) + frontend.scopeManager.addDeclaration(param) + method.parameters += param + method.returnTypes = listOf(unknownType()) + } else { + method.returnTypes = listOf(propertyType) + } + + val body = accessor.body + val expressionBody = accessor.expressionBody + if (body != null) { + method.body = frontend.statementHandler.handle(body) + } else if (expressionBody != null) { + // `get => expr;` or `set => expr;` — wrap into an implicit block with either a + // return (for getters) or an assignment to the backing field (for setters). + val block = newBlock().implicit() + val expr = frontend.expressionHandler.handle(expressionBody) + if (isSetter) { + val assign = + newAssign( + operatorCode = "=", + lhs = listOf(implicitThisFieldAccess(field)), + rhs = listOf(expr), + ) + .implicit() + block.statements += assign + } else { + val ret = newReturn().implicit() + ret.returnValue = expr + block.statements += ret + } + method.body = block + } else { + // Auto-accessor (`get;` / `set;` / `init;`) — synthesize a body that reads or + // writes the backing field. + val block = newBlock().implicit() + if (isSetter) { + val valueRef = newReference("value", propertyType).implicit(code = "value") + val assign = + newAssign( + operatorCode = "=", + lhs = listOf(implicitThisFieldAccess(field)), + rhs = listOf(valueRef), + ) + .implicit() + block.statements += assign + } else { + val ret = newReturn().implicit() + ret.returnValue = implicitThisFieldAccess(field) + block.statements += ret + } + method.body = block + } + + frontend.scopeManager.leaveScope(method) + frontend.scopeManager.addDeclaration(method) + record?.addDeclaration(method) + } + + node.expressionBody?.let { + // Property-level `int X => expr;` — equivalent to a single get accessor. + val method = + newMethod("get_${node.identifier}", recordDeclaration = record, rawNode = node) + method.modifiers = node.modifiers.toSet() + frontend.scopeManager.enterScope(method) + createMethodReceiver(method) + method.returnTypes = listOf(propertyType) + val block = newBlock().implicit() + val ret = newReturn().implicit() + ret.returnValue = frontend.expressionHandler.handle(it) + block.statements += ret + method.body = block + frontend.scopeManager.leaveScope(method) + frontend.scopeManager.addDeclaration(method) + record?.addDeclaration(method) + } + + return field + } + + /** Creates an implicit `this.` [MemberAccess] used inside synthetic accessor bodies. */ + private fun implicitThisFieldAccess(field: Field): MemberAccess { + val record = frontend.scopeManager.currentRecord + val thisRef = + newReference(name = "this", type = record?.toType() ?: unknownType()) + .implicit(code = "this") + return newMemberAccess(name = field.name, base = thisRef, operatorCode = ".") + .implicit(code = "this.${field.name.localName}") + } + /** * Translates a [ConstructorDeclarationSyntax][Csharp.AST.ConstructorDeclarationSyntax] into a * [Constructor]. diff --git a/cpg-language-csharp/src/test/resources/csharp/Properties.cs b/cpg-language-csharp/src/test/resources/csharp/Properties.cs new file mode 100644 index 00000000000..b48684f0146 --- /dev/null +++ b/cpg-language-csharp/src/test/resources/csharp/Properties.cs @@ -0,0 +1,34 @@ +namespace HelloWorld +{ + class Foo + { + private int reader; + + // Auto-property with getter and setter + public int AutoProp { get; set; } + + // Auto-property getter-only with initializer + public int GetOnly { get; } = 42; + + // Property with a full getter body + public int WithBody + { + get + { + return reader; + } + } + + // Property-level expression body (get-only) + public int ExprBody => reader; + + // Property with an expression-bodied getter accessor + public int AccessorExpr + { + get => reader; + } + + // Mixed accessor visibility: public getter, private setter + public int Restricted { get; private set; } + } +} \ No newline at end of file From 44bdcb2307d519874b25058f246893ab7fec1f83 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Mon, 27 Apr 2026 00:10:23 +0200 Subject: [PATCH 54/55] First part implementing property declarations --- .../frontends/csharp/DeclarationHandler.kt | 112 +++++++++++------- .../csharp/CSharpLanguageFrontendTest.kt | 19 +++ .../src/test/resources/csharp/Properties.cs | 45 ++++--- 3 files changed, 110 insertions(+), 66 deletions(-) diff --git a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt index 28733e367b7..45b9d22e0d0 100644 --- a/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt +++ b/cpg-language-csharp/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/DeclarationHandler.kt @@ -25,11 +25,13 @@ */ package de.fraunhofer.aisec.cpg.frontends.csharp +import de.fraunhofer.aisec.cpg.graph.Name import de.fraunhofer.aisec.cpg.graph.declarations.* import de.fraunhofer.aisec.cpg.graph.expressions.Construction import de.fraunhofer.aisec.cpg.graph.expressions.MemberAccess import de.fraunhofer.aisec.cpg.graph.expressions.New import de.fraunhofer.aisec.cpg.graph.implicit +import de.fraunhofer.aisec.cpg.graph.incompleteType import de.fraunhofer.aisec.cpg.graph.newAssign import de.fraunhofer.aisec.cpg.graph.newBlock import de.fraunhofer.aisec.cpg.graph.newConstructor @@ -247,15 +249,9 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : } /** - * Translates a [PropertyDeclarationSyntax][Csharp.AST.PropertyDeclarationSyntax] into a [Field] - * representing its backing field, and additionally adds the respective getter and setter - * methods to the current record. Accessor bodies follow the C# spec: - * - An accessor with an explicit block body (`get { ... }`) keeps that body. - * - An accessor with an expression body (`get => expr;`) is wrapped into an implicit block - * containing a return (for getters) or an assignment to the backing field (for setters). - * - An auto-accessor (`get;` / `set;`) gets a synthesized body that reads from or writes to the - * backing field (`return this.;` or `this. = value;`). - * - A property-level expression body (`int X => expr;`) produces a single `get` accessor. + * TODO: Not completed yet Translates a + * [PropertyDeclarationSyntax][Csharp.AST.PropertyDeclarationSyntax] into a [Field] and a + * [Method] per accessor (`get`, `set`). * * C# spec: * [Properties](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#157-properties) @@ -264,33 +260,35 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : val record = frontend.scopeManager.currentRecord val propertyType = frontend.typeOf(node.type) - val field = newField(node.identifier, rawNode = node) + val field = newField(node.identifier, rawNode = node).implicit() field.type = propertyType - field.modifiers = node.modifiers.toSet() node.initializer?.let { field.initializer = frontend.expressionHandler.handle(it) } node.accessorList?.accessors?.forEach { accessor -> - val method = - newMethod( - "${accessor.keyword}_${node.identifier}", - recordDeclaration = record, - rawNode = accessor, + val accessorName = + Name.temporary( + prefix = "${accessor.keyword}_${node.identifier}", + separatorChar = '_', ) - // An accessor-level access modifier (e.g. `private set`) replaces the property's - // access modifier; otherwise the accessor inherits the property's modifiers. + val method = + newMethod(accessorName, recordDeclaration = record, rawNode = accessor) + .implicit( + code = frontend.codeOf(accessor), + location = frontend.locationOf(accessor), + ) method.modifiers = if (accessor.modifiers.isNotEmpty()) accessor.modifiers.toSet() else node.modifiers.toSet() frontend.scopeManager.enterScope(method) createMethodReceiver(method) - val isSetter = accessor.keyword == "set" || accessor.keyword == "init" + val isSetter = accessor.keyword == "set" if (isSetter) { val param = newParameter("value", propertyType, rawNode = accessor) frontend.scopeManager.addDeclaration(param) method.parameters += param - method.returnTypes = listOf(unknownType()) + method.returnTypes = listOf(incompleteType()) } else { method.returnTypes = listOf(propertyType) } @@ -300,42 +298,67 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : if (body != null) { method.body = frontend.statementHandler.handle(body) } else if (expressionBody != null) { - // `get => expr;` or `set => expr;` — wrap into an implicit block with either a - // return (for getters) or an assignment to the backing field (for setters). - val block = newBlock().implicit() + val block = + newBlock() + .implicit( + code = frontend.codeOf(accessor), + location = frontend.locationOf(accessor), + ) val expr = frontend.expressionHandler.handle(expressionBody) if (isSetter) { val assign = newAssign( operatorCode = "=", - lhs = listOf(implicitThisFieldAccess(field)), + lhs = listOf(implicitThisFieldAccess(field, accessor)), rhs = listOf(expr), ) - .implicit() + .implicit( + code = frontend.codeOf(accessor), + location = frontend.locationOf(accessor), + ) block.statements += assign } else { - val ret = newReturn().implicit() + val ret = + newReturn() + .implicit( + code = frontend.codeOf(accessor), + location = frontend.locationOf(accessor), + ) ret.returnValue = expr block.statements += ret } method.body = block } else { - // Auto-accessor (`get;` / `set;` / `init;`) — synthesize a body that reads or - // writes the backing field. - val block = newBlock().implicit() + // Auto-accessor (`get;`, `set;`) + val block = + newBlock() + .implicit( + code = frontend.codeOf(accessor), + location = frontend.locationOf(accessor), + ) if (isSetter) { - val valueRef = newReference("value", propertyType).implicit(code = "value") + val valueRef = + newReference("value", propertyType) + .implicit(code = "value", location = frontend.locationOf(accessor)) val assign = newAssign( operatorCode = "=", - lhs = listOf(implicitThisFieldAccess(field)), + lhs = listOf(implicitThisFieldAccess(field, accessor)), rhs = listOf(valueRef), ) - .implicit() + .implicit( + code = frontend.codeOf(accessor), + location = frontend.locationOf(accessor), + ) block.statements += assign } else { - val ret = newReturn().implicit() - ret.returnValue = implicitThisFieldAccess(field) + val ret = + newReturn() + .implicit( + code = frontend.codeOf(accessor), + location = frontend.locationOf(accessor), + ) + ret.returnValue = implicitThisFieldAccess(field, accessor) block.statements += ret } method.body = block @@ -347,15 +370,21 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : } node.expressionBody?.let { - // Property-level `int X => expr;` — equivalent to a single get accessor. + val accessorName = + Name.temporary(prefix = "get_${node.identifier}", separatorChar = '_') val method = - newMethod("get_${node.identifier}", recordDeclaration = record, rawNode = node) + newMethod(accessorName, recordDeclaration = record, rawNode = node) + .implicit(code = frontend.codeOf(node), location = frontend.locationOf(node)) method.modifiers = node.modifiers.toSet() frontend.scopeManager.enterScope(method) createMethodReceiver(method) method.returnTypes = listOf(propertyType) - val block = newBlock().implicit() - val ret = newReturn().implicit() + val block = + newBlock() + .implicit(code = frontend.codeOf(node), location = frontend.locationOf(node)) + val ret = + newReturn() + .implicit(code = frontend.codeOf(node), location = frontend.locationOf(node)) ret.returnValue = frontend.expressionHandler.handle(it) block.statements += ret method.body = block @@ -367,14 +396,13 @@ class DeclarationHandler(frontend: CSharpLanguageFrontend) : return field } - /** Creates an implicit `this.` [MemberAccess] used inside synthetic accessor bodies. */ - private fun implicitThisFieldAccess(field: Field): MemberAccess { + private fun implicitThisFieldAccess(field: Field, from: Csharp.AST.Node): MemberAccess { val record = frontend.scopeManager.currentRecord val thisRef = newReference(name = "this", type = record?.toType() ?: unknownType()) - .implicit(code = "this") + .implicit(code = "this", location = frontend.locationOf(from)) return newMemberAccess(name = field.name, base = thisRef, operatorCode = ".") - .implicit(code = "this.${field.name.localName}") + .implicit(code = "this.${field.name.localName}", location = frontend.locationOf(from)) } /** diff --git a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt index 543122b721c..e9a07e45ed1 100644 --- a/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt +++ b/cpg-language-csharp/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/csharp/CSharpLanguageFrontendTest.kt @@ -474,4 +474,23 @@ class CSharpLanguageFrontendTest : BaseTest() { assertIs(base) assertEquals(enumsEnum, base.refersTo) } + + @Test + fun testPropertyDeclarations() { + val topLevel = Path.of("src", "test", "resources", "csharp") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("Properties.cs").toFile()), + topLevel, + true, + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val person = tu.records["Person"] + assertNotNull(person) + + TODO() + } } diff --git a/cpg-language-csharp/src/test/resources/csharp/Properties.cs b/cpg-language-csharp/src/test/resources/csharp/Properties.cs index b48684f0146..742c59b78e2 100644 --- a/cpg-language-csharp/src/test/resources/csharp/Properties.cs +++ b/cpg-language-csharp/src/test/resources/csharp/Properties.cs @@ -1,34 +1,31 @@ -namespace HelloWorld +class Person { - class Foo - { - private int reader; + private int age; - // Auto-property with getter and setter - public int AutoProp { get; set; } + // Auto-property with getter and setter + public string Name { get; set; } - // Auto-property getter-only with initializer - public int GetOnly { get; } = 42; + // Auto-property getter-only with initializer + public int BirthYear { get; } = 1990; - // Property with a full getter body - public int WithBody + // Property with a full getter body + public int Age + { + get { - get - { - return reader; - } + return age; } + } - // Property-level expression body (get-only) - public int ExprBody => reader; - - // Property with an expression-bodied getter accessor - public int AccessorExpr - { - get => reader; - } + // Property-level expression body (get-only) + public string FullName => Name; - // Mixed accessor visibility: public getter, private setter - public int Restricted { get; private set; } + // Property with an expression-bodied getter accessor + public int AgeInMonths + { + get => age * 12; } + + // Mixed accessor visibility: public getter, private setter + public string Email { get; private set; } } \ No newline at end of file From 2221bec3474ebc6a32cac9f0d7b923dd875d7e79 Mon Sep 17 00:00:00 2001 From: Leutrim Shala Date: Tue, 14 Jul 2026 11:34:40 +0200 Subject: [PATCH 55/55] Fix csharp config in configure_frontends.sh --- configure_frontends.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure_frontends.sh b/configure_frontends.sh index 4e7a298242a..5c5b2cf051a 100755 --- a/configure_frontends.sh +++ b/configure_frontends.sh @@ -63,6 +63,6 @@ setProperty "enableJVMFrontend" $answerJVM answerINI=$(ask "Do you want to enable the INI frontend? (currently $(getProperty "enableINIFrontend"))") setProperty "enableINIFrontend" $answerINI answerCsharp=$(ask "Do you want to enable the C# frontend? (currently $(getProperty "enableCSharpFrontend"))") -setProperty "enableCSharpFrontend" answerCsharp +setProperty "enableCSharpFrontend" $answerCsharp answerMCP=$(ask "Do you want to enable the MCP module? (currently $(getProperty "enableMCPModule"))") setProperty "enableMCPModule" $answerMCP