From f88a81a453e011fd6e0feee9b5a41e0d2388520c Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 30 Nov 2024 00:06:41 +0100 Subject: [PATCH 1/2] Node Builder V2 Work in progress --- .../de/fraunhofer/aisec/cpg/ScopeManager.kt | 7 +- .../aisec/cpg/graph/MetadataProvider.kt | 181 ++++++++++++++++++ .../fraunhofer/aisec/cpg/graph/NodeBuilder.kt | 153 +-------------- .../aisec/cpg/graph/NodeBuilderV2.kt | 106 ++++++++++ .../fraunhofer/aisec/cpg/NodeBuilderV2Test.kt | 100 ++++++++++ .../aisec/cpg/frontends/TestLanguage.kt | 8 +- 6 files changed, 398 insertions(+), 157 deletions(-) create mode 100644 cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/MetadataProvider.kt create mode 100644 cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/NodeBuilderV2.kt create mode 100644 cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/NodeBuilderV2Test.kt diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt index 266ac9604f..3d74ae53ac 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt @@ -359,6 +359,11 @@ class ScopeManager : ScopeProvider { return leaveScope } + /** Declares this [declaration] as a symbol in the [currentScope]. */ + fun declare(declaration: Declaration) { + currentScope?.addSymbol(declaration.symbol, declaration) + } + /** * This function MUST be called when a language frontend first handles a [Declaration]. It adds * a declaration to the scope manager, taking into account the currently active scope. @@ -380,7 +385,7 @@ class ScopeManager : ScopeProvider { fun addDeclaration(declaration: Declaration?, addToAST: Boolean = true) { if (declaration != null) { // New stuff here - currentScope?.addSymbol(declaration.symbol, declaration) + declare(declaration) } // Legacy stuff here diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/MetadataProvider.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/MetadataProvider.kt new file mode 100644 index 0000000000..11957b1f31 --- /dev/null +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/MetadataProvider.kt @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2024, 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.graph + +import de.fraunhofer.aisec.cpg.TranslationContext +import de.fraunhofer.aisec.cpg.frontends.Handler +import de.fraunhofer.aisec.cpg.frontends.Language +import de.fraunhofer.aisec.cpg.frontends.TranslationException +import de.fraunhofer.aisec.cpg.graph.Node.Companion.EMPTY_NAME +import de.fraunhofer.aisec.cpg.graph.NodeBuilder.LOGGER +import de.fraunhofer.aisec.cpg.graph.scopes.Scope +import de.fraunhofer.aisec.cpg.passes.inference.IsImplicitProvider +import de.fraunhofer.aisec.cpg.passes.inference.IsInferredProvider +import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation + +/** + * This interfaces serves as base for different entities that provide some kind of meta-data for a + * [Node], such as its language, code or location. + */ +interface MetadataProvider + +/** + * A simple interface that everything, that supplies a language, should implement. Examples include + * each [Node], but also transformation steps, such as [Handler]. + */ +interface LanguageProvider : MetadataProvider { + val language: Language<*>? +} + +/** + * This interface denotes that the class is able to provide source code and location information for + * a specific node. + */ +interface CodeAndLocationProvider : MetadataProvider { + /** Returns the raw code of the supplied [AstNode]. */ + fun codeOf(astNode: AstNode): String? + + /** Returns the [PhysicalLocation] of the supplied [AstNode]. */ + fun locationOf(astNode: AstNode): PhysicalLocation? +} + +/** + * This interfaces serves as a base for entities that provide the current scope / name prefix. This + * is reserved for future use. + */ +interface ScopeProvider : MetadataProvider { + val scope: Scope? +} + +/** + * This interface denotes that the class is able to provide the current namespace. The + * [applyMetadata] will use this information to set the parent of a [Name]. + */ +interface NamespaceProvider : MetadataProvider { + val namespace: Name? +} + +/** + * Applies various metadata on this [Node], based on the kind of provider in [provider]. This can + * include: + * - Setting [Node.code] and [Node.location], if a [CodeAndLocationProvider] is given + * - Setting [Node.location], if a [LanguageProvider] is given + * - Setting [Node.scope]. if a [ScopeProvider] is given + * - Setting [Node.isInferred], if an [IsInferredProvider] is given + * + * Note, that one provider can implement multiple provider interfaces. + */ +fun Node.applyMetadata( + provider: MetadataProvider?, + name: CharSequence? = EMPTY_NAME, + rawNode: Any? = null, + localNameOnly: Boolean = false, + defaultNamespace: Name? = null, +) { + // We definitely need a context provider, because otherwise we cannot set the context and the + // node cannot access necessary information about the current translation context it lives in. + this.ctx = + (provider as? ContextProvider)?.ctx + ?: throw TranslationException( + "Trying to create a node without a ContextProvider. This will fail." + ) + + // We try to set the code and especially the location as soon as possible because the hashCode + // implementation of the Node class relies on it. Otherwise, we could have a problem that the + // location is not yet set, but the node is put into a hashmap. In this case the hashCode is + // calculated based on an empty location and if we would later set the location, we would have a + // mismatch. Each language frontend and also each handler implements CodeAndLocationProvider, so + // calling a node builder from these should already set the location. + if (provider is CodeAndLocationProvider<*> && rawNode != null) { + @Suppress("UNCHECKED_CAST") + setCodeAndLocation(provider as CodeAndLocationProvider, rawNode) + } + + if (provider is LanguageProvider) { + this.language = provider.language + } + + if (provider is IsInferredProvider) { + this.isInferred = provider.isInferred + } + + if (provider is IsImplicitProvider) { + this.isImplicit = provider.isImplicit + } + + if (provider is ScopeProvider) { + this.scope = provider.scope + } else { + LOGGER.warn( + "No scope provider was provided when creating the node {}. This might be an error", + name + ) + } + + if (name != null) { + val namespace = + if (provider is NamespaceProvider) { + provider.namespace ?: defaultNamespace + } else { + defaultNamespace + } + this.name = this.newName(name, localNameOnly, namespace) + } +} + +/** + * This internal function sets the code and location according to the [CodeAndLocationProvider]. + * This also performs some checks, e.g., if the config disabled setting the code. + */ +internal fun Node.setCodeAndLocation( + provider: CodeAndLocationProvider, + rawNode: AstNode +) { + if (this.ctx?.config?.codeInNodes == true) { + // only set code, if it's not already set or empty + val code = provider.codeOf(rawNode) + if (code != null) { + this.code = code + } else { + LOGGER.warn("Unexpected: No code for node {}", rawNode) + } + } + this.location = provider.locationOf(rawNode) +} + +interface ContextProvider : MetadataProvider { + val ctx: TranslationContext? +} + +/** + * This [MetadataProvider] makes sure that we can type our node builder functions correctly. For + * language frontend and handlers, [T] should be set to the type of the raw node. For passes, [T] + * should be set to [Nothing], since we do not have raw nodes there. + * + * Note: This does not work yet to 100 % satisfaction and is therefore not yet activated in the + * builders. + */ +interface RawNodeTypeProvider : MetadataProvider diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/NodeBuilder.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/NodeBuilder.kt index ac060fa20a..961adac0e0 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/NodeBuilder.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/NodeBuilder.kt @@ -25,16 +25,11 @@ */ package de.fraunhofer.aisec.cpg.graph -import de.fraunhofer.aisec.cpg.TranslationContext import de.fraunhofer.aisec.cpg.frontends.* -import de.fraunhofer.aisec.cpg.graph.Node.Companion.EMPTY_NAME import de.fraunhofer.aisec.cpg.graph.NodeBuilder.LOGGER import de.fraunhofer.aisec.cpg.graph.NodeBuilder.log -import de.fraunhofer.aisec.cpg.graph.scopes.Scope import de.fraunhofer.aisec.cpg.graph.statements.expressions.* import de.fraunhofer.aisec.cpg.helpers.getCodeOfSubregion -import de.fraunhofer.aisec.cpg.passes.inference.IsImplicitProvider -import de.fraunhofer.aisec.cpg.passes.inference.IsInferredProvider import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation import de.fraunhofer.aisec.cpg.sarif.Region import java.net.URI @@ -48,116 +43,6 @@ object NodeBuilder { } } -/** - * This interfaces serves as base for different entities that provide some kind of meta-data for a - * [Node], such as its language, code or location. - */ -interface MetadataProvider - -/** - * A simple interface that everything, that supplies a language, should implement. Examples include - * each [Node], but also transformation steps, such as [Handler]. - */ -interface LanguageProvider : MetadataProvider { - val language: Language<*>? -} - -/** - * This interface denotes that the class is able to provide source code and location information for - * a specific node. - */ -interface CodeAndLocationProvider : MetadataProvider { - /** Returns the raw code of the supplied [AstNode]. */ - fun codeOf(astNode: AstNode): String? - - /** Returns the [PhysicalLocation] of the supplied [AstNode]. */ - fun locationOf(astNode: AstNode): PhysicalLocation? -} - -/** - * This interfaces serves as a base for entities that provide the current scope / name prefix. This - * is reserved for future use. - */ -interface ScopeProvider : MetadataProvider { - val scope: Scope? -} - -/** - * This interface denotes that the class is able to provide the current namespace. The - * [applyMetadata] will use this information to set the parent of a [Name]. - */ -interface NamespaceProvider : MetadataProvider { - val namespace: Name? -} - -/** - * Applies various metadata on this [Node], based on the kind of provider in [provider]. This can - * include: - * - Setting [Node.code] and [Node.location], if a [CodeAndLocationProvider] is given - * - Setting [Node.location], if a [LanguageProvider] is given - * - Setting [Node.scope]. if a [ScopeProvider] is given - * - Setting [Node.isInferred], if an [IsInferredProvider] is given - * - * Note, that one provider can implement multiple provider interfaces. - */ -fun Node.applyMetadata( - provider: MetadataProvider?, - name: CharSequence? = EMPTY_NAME, - rawNode: Any? = null, - localNameOnly: Boolean = false, - defaultNamespace: Name? = null, -) { - // We definitely need a context provider, because otherwise we cannot set the context and the - // node cannot access necessary information about the current translation context it lives in. - this.ctx = - (provider as? ContextProvider)?.ctx - ?: throw TranslationException( - "Trying to create a node without a ContextProvider. This will fail." - ) - - // We try to set the code and especially the location as soon as possible because the hashCode - // implementation of the Node class relies on it. Otherwise, we could have a problem that the - // location is not yet set, but the node is put into a hashmap. In this case the hashCode is - // calculated based on an empty location and if we would later set the location, we would have a - // mismatch. Each language frontend and also each handler implements CodeAndLocationProvider, so - // calling a node builder from these should already set the location. - if (provider is CodeAndLocationProvider<*> && rawNode != null) { - @Suppress("UNCHECKED_CAST") - setCodeAndLocation(provider as CodeAndLocationProvider, rawNode) - } - - if (provider is LanguageProvider) { - this.language = provider.language - } - - if (provider is IsInferredProvider) { - this.isInferred = provider.isInferred - } - - if (provider is IsImplicitProvider) { - this.isImplicit = provider.isImplicit - } - - if (provider is ScopeProvider) { - this.scope = provider.scope - } else { - LOGGER.warn( - "No scope provider was provided when creating the node {}. This might be an error", - name - ) - } - - if (name != null) { - val namespace = - if (provider is NamespaceProvider) { - provider.namespace ?: defaultNamespace - } else { - defaultNamespace - } - this.name = this.newName(name, localNameOnly, namespace) - } -} - /** * Generates a [Name] object from the given [name]. If [localNameOnly] is set, only the localName is * used, otherwise the [namespace] is added to generate a fqn if the [name] is not a fqn anyway. @@ -233,20 +118,6 @@ fun NamespaceProvider.fqn(localName: String): Name { return this.namespace.fqn(localName) } -interface ContextProvider : MetadataProvider { - val ctx: TranslationContext? -} - -/** - * This [MetadataProvider] makes sure that we can type our node builder functions correctly. For - * language frontend and handlers, [T] should be set to the type of the raw node. For passes, [T] - * should be set to [Nothing], since we do not have raw nodes there. - * - * Note: This does not work yet to 100 % satisfaction and is therefore not yet activated in the - * builders. - */ -interface RawNodeTypeProvider : MetadataProvider - /** * A small helper function that can be used in building a [Node] with [Node.isImplicit] set to true. * In this case, no "rawNode" exists that can be used for the node builder. But, in order to @@ -299,8 +170,8 @@ fun T.codeAndLocationFromOtherRawNode(rawNode: AstNode?): T * are between the child nodes. * * @param parentNode Used to extract the code for this node. - * @param newLineType The char(s) used to describe a new line, usually either "\n" or "\r\n". This - * is needed because the location block spanning the children usually comprises more than one + * @param lineBreakSequence The char(s) used to describe a new line, usually either "\n" or "\r\n". + * This is needed because the location block spanning the children usually comprises more than one * line. */ context(CodeAndLocationProvider) @@ -369,23 +240,3 @@ fun T.codeAndLocationFromChildren( return this } - -/** - * This internal function sets the code and location according to the [CodeAndLocationProvider]. - * This also performs some checks, e.g., if the config disabled setting the code. - */ -private fun Node.setCodeAndLocation( - provider: CodeAndLocationProvider, - rawNode: AstNode -) { - if (this.ctx?.config?.codeInNodes == true) { - // only set code, if it's not already set or empty - val code = provider.codeOf(rawNode) - if (code != null) { - this.code = code - } else { - LOGGER.warn("Unexpected: No code for node {}", rawNode) - } - } - this.location = provider.locationOf(rawNode) -} diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/NodeBuilderV2.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/NodeBuilderV2.kt new file mode 100644 index 0000000000..2a12cf2303 --- /dev/null +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/NodeBuilderV2.kt @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2024, 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. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +@file:OptIn(ExperimentalContracts::class) + +package de.fraunhofer.aisec.cpg.graph + +import de.fraunhofer.aisec.cpg.frontends.TranslationException +import de.fraunhofer.aisec.cpg.graph.declarations.* +import de.fraunhofer.aisec.cpg.graph.types.Type +import kotlin.contracts.ExperimentalContracts + +context(ContextProvider) +fun TranslationUnitDeclaration.globalScope( + init: TranslationUnitDeclaration.() -> Unit +): TranslationUnitDeclaration { + val ctx = ctx ?: throw TranslationException("context not available") + + ctx.scopeManager.resetToGlobal(this) + init(this) + + return this +} + +context(ContextProvider) +fun T.withScope(init: T.() -> Unit): T { + val ctx = ctx ?: throw TranslationException("context not available") + + ctx.scopeManager.enterScope(this) + init(this) + ctx.scopeManager.leaveScope(this) + + return this +} + +context(ContextProvider) +operator fun DeclarationHolder.plusAssign(declaration: Declaration) { + addDeclaration(declaration) + ctx?.scopeManager?.declare(declaration) +} + +/** Creates a new [TranslationUnitDeclaration] with the given [name]. */ +fun RawNodeTypeProvider.translationUnitDeclaration( + name: CharSequence, + rawNode: T? = null, + init: (TranslationUnitDeclaration.() -> Unit)? = null +): TranslationUnitDeclaration { + val node = TranslationUnitDeclaration() + node.applyMetadata(this, name = name, rawNode = rawNode) + + init?.invoke(node) + + return node +} + +/** Creates a new [FunctionDeclaration] with the given [name]. */ +fun RawNodeTypeProvider.functionDeclaration( + name: CharSequence, + rawNode: T? = null, + init: (FunctionDeclaration.() -> Unit)? = null +): FunctionDeclaration { + val node = FunctionDeclaration() + node.applyMetadata(this, name = name, rawNode = rawNode) + + init?.invoke(node) + + return node +} + +/** Creates a new [ParameterDeclaration] with the given [name]. */ +fun RawNodeTypeProvider.parameterDeclaration( + name: CharSequence, + type: Type, + rawNode: T? = null, + init: (ParameterDeclaration.() -> Unit)? = null +): ParameterDeclaration { + val node = ParameterDeclaration() + node.applyMetadata(this, name = name, rawNode = rawNode, localNameOnly = true) + node.type = type + + init?.invoke(node) + + return node +} diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/NodeBuilderV2Test.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/NodeBuilderV2Test.kt new file mode 100644 index 0000000000..4d84f1a9ff --- /dev/null +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/NodeBuilderV2Test.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2024, 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 + +import de.fraunhofer.aisec.cpg.frontends.Handler +import de.fraunhofer.aisec.cpg.frontends.TestLanguageFrontend +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.declarations.Declaration +import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.ProblemDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration +import kotlin.test.Test +import kotlin.test.assertNotNull + +class RawFileDeclarationNode(var children: List) : RawDeclarationNode() + +class RawFunctionDeclarationNode : RawDeclarationNode() + +open class RawDeclarationNode : RawNode() + +open class RawNode + +private class TestDeclarationHandler(frontend: TestLanguageFrontend) : + Handler(::ProblemDeclaration, frontend) { + override fun handle(node: RawDeclarationNode): Declaration { + when (node) { + is RawFunctionDeclarationNode -> handleFunction(node) + is RawFileDeclarationNode -> handleFile(node) + } + + return ProblemDeclaration() + } + + private fun handleFile(node: RawFileDeclarationNode): TranslationUnitDeclaration { + return translationUnitDeclaration("test.file").globalScope { + for (child in node.children) { + this += handle(child) + } + } + } + + private fun handleFunction(node: RawFunctionDeclarationNode): FunctionDeclaration { + return functionDeclaration("main", rawNode = node).withScope { + val param = parameterDeclaration("argc", objectType("int")) + + this += param + } + } +} + +class NodeBuilderV2Test { + @Test + fun testBuild() { + val frontend = TestLanguageFrontend() + val tu = + with(frontend) { + translationUnitDeclaration("test.file").globalScope { + val func = + functionDeclaration("main").withScope { + val param = parameterDeclaration("argc", objectType("int")) + + this += param + } + + this += func + } + } + + assertNotNull(tu) + + val func = tu.functions["main"] + assertNotNull(func) + + val param = func.parameters["argc"] + assertNotNull(param) + } +} diff --git a/cpg-core/src/testFixtures/kotlin/de/fraunhofer/aisec/cpg/frontends/TestLanguage.kt b/cpg-core/src/testFixtures/kotlin/de/fraunhofer/aisec/cpg/frontends/TestLanguage.kt index 9c0036b1b2..4c2e631aae 100644 --- a/cpg-core/src/testFixtures/kotlin/de/fraunhofer/aisec/cpg/frontends/TestLanguage.kt +++ b/cpg-core/src/testFixtures/kotlin/de/fraunhofer/aisec/cpg/frontends/TestLanguage.kt @@ -92,16 +92,14 @@ open class TestLanguageFrontend( } override fun codeOf(astNode: Any): String? { - TODO("Not yet implemented") + return astNode.toString() } override fun locationOf(astNode: Any): PhysicalLocation? { - TODO("Not yet implemented") + return null } - override fun setComment(node: Node, astNode: Any) { - TODO("Not yet implemented") - } + override fun setComment(node: Node, astNode: Any) {} } class TestHandler(frontend: TestLanguageFrontend) : From bea15bacceb0c5d7b14bf23841834addae40130c Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 30 Nov 2024 16:57:53 +0100 Subject: [PATCH 2/2] Extracted demo into example language frontend --- .../aisec/cpg/frontends/LanguageFrontend.kt | 7 +- .../fraunhofer/aisec/cpg/NodeBuilderV2Test.kt | 70 ++++----- .../aisec/cpg/example/ExampleLanguage.kt | 148 ++++++++++++++++++ .../cpg/example/RawExampleLanguageParser.kt | 43 +++++ 4 files changed, 227 insertions(+), 41 deletions(-) create mode 100644 cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/example/ExampleLanguage.kt create mode 100644 cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/example/RawExampleLanguageParser.kt diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageFrontend.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageFrontend.kt index e6e97aa12d..e12d77b11b 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageFrontend.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageFrontend.kt @@ -42,7 +42,12 @@ import org.slf4j.LoggerFactory * after having processed the files, i.e., it won't be available in passes. * * More information can be found in the - * [GitHub wiki page](https://github.com/Fraunhofer-AISEC/cpg/wiki/Language-Frontends). + * [Documentation](https://fraunhofer-aisec.github.io/cpg/CPG/impl/language/#languagefrontend). + * + * @param AstNode This type parameter should be set to a class that is common to all AST nodes in + * the language. + * @param TypeNode This type parameter should be set to a class that is common to all type nodes in + * the language. */ abstract class LanguageFrontend( /** The language this frontend works for. */ diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/NodeBuilderV2Test.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/NodeBuilderV2Test.kt index 4d84f1a9ff..801ce81509 100644 --- a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/NodeBuilderV2Test.kt +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/NodeBuilderV2Test.kt @@ -25,52 +25,20 @@ */ package de.fraunhofer.aisec.cpg -import de.fraunhofer.aisec.cpg.frontends.Handler +import de.fraunhofer.aisec.cpg.example.ExampleLanguageFrontend +import de.fraunhofer.aisec.cpg.example.RawFileNode +import de.fraunhofer.aisec.cpg.example.RawFunctionNode +import de.fraunhofer.aisec.cpg.example.RawParameterNode +import de.fraunhofer.aisec.cpg.example.RawTypeNode import de.fraunhofer.aisec.cpg.frontends.TestLanguageFrontend import de.fraunhofer.aisec.cpg.graph.* -import de.fraunhofer.aisec.cpg.graph.declarations.Declaration -import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration -import de.fraunhofer.aisec.cpg.graph.declarations.ProblemDeclaration import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration +import de.fraunhofer.aisec.cpg.graph.plusAssign +import java.io.File import kotlin.test.Test +import kotlin.test.assertIs import kotlin.test.assertNotNull -class RawFileDeclarationNode(var children: List) : RawDeclarationNode() - -class RawFunctionDeclarationNode : RawDeclarationNode() - -open class RawDeclarationNode : RawNode() - -open class RawNode - -private class TestDeclarationHandler(frontend: TestLanguageFrontend) : - Handler(::ProblemDeclaration, frontend) { - override fun handle(node: RawDeclarationNode): Declaration { - when (node) { - is RawFunctionDeclarationNode -> handleFunction(node) - is RawFileDeclarationNode -> handleFile(node) - } - - return ProblemDeclaration() - } - - private fun handleFile(node: RawFileDeclarationNode): TranslationUnitDeclaration { - return translationUnitDeclaration("test.file").globalScope { - for (child in node.children) { - this += handle(child) - } - } - } - - private fun handleFunction(node: RawFunctionDeclarationNode): FunctionDeclaration { - return functionDeclaration("main", rawNode = node).withScope { - val param = parameterDeclaration("argc", objectType("int")) - - this += param - } - } -} - class NodeBuilderV2Test { @Test fun testBuild() { @@ -97,4 +65,26 @@ class NodeBuilderV2Test { val param = func.parameters["argc"] assertNotNull(param) } + + @Test + fun testFrontend() { + val file = + RawFileNode( + "file.example", + children = + listOf( + RawFunctionNode( + "main", + params = listOf(RawParameterNode("argc", type = RawTypeNode("int"))) + ) + ) + ) + + val frontend = ExampleLanguageFrontend(rawFileNode = file) + val node = frontend.parse(File("")) + assertIs(node) + + val main = node.functions["main"] + assertNotNull(main) + } } diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/example/ExampleLanguage.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/example/ExampleLanguage.kt new file mode 100644 index 0000000000..67dc3a02ad --- /dev/null +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/example/ExampleLanguage.kt @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2024, 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.example + +import de.fraunhofer.aisec.cpg.ScopeManager +import de.fraunhofer.aisec.cpg.TranslationConfiguration +import de.fraunhofer.aisec.cpg.TranslationContext +import de.fraunhofer.aisec.cpg.TypeManager +import de.fraunhofer.aisec.cpg.frontends.Handler +import de.fraunhofer.aisec.cpg.frontends.HasClasses +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.Declaration +import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.ParameterDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.ProblemDeclaration +import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration +import de.fraunhofer.aisec.cpg.graph.functionDeclaration +import de.fraunhofer.aisec.cpg.graph.globalScope +import de.fraunhofer.aisec.cpg.graph.objectType +import de.fraunhofer.aisec.cpg.graph.parameterDeclaration +import de.fraunhofer.aisec.cpg.graph.plusAssign +import de.fraunhofer.aisec.cpg.graph.translationUnitDeclaration +import de.fraunhofer.aisec.cpg.graph.types.IntegerType +import de.fraunhofer.aisec.cpg.graph.types.Type +import de.fraunhofer.aisec.cpg.graph.withScope +import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation +import java.io.File +import kotlin.reflect.KClass + +/** + * This is an example language, that demonstrate how to construct a [Language]. Our language has the + * following features: + * - It uses the `.example` file extension + * - It has classes and namespaces + * - Namespaces are separated by a `::` delimiter + * - It features some built-in types, such as `int` + */ +class ExampleLanguage : Language(), HasClasses { + override val fileExtensions: List = listOf("example") + override val namespaceDelimiter: String = "::" + override val frontend: KClass = ExampleLanguageFrontend::class + override val builtInTypes: Map = mapOf("int" to IntegerType()) + override val compoundAssignmentOperators = setOf() +} + +/** + * This is an example language frontend, that demonstrates how to translate languages into the CPG. + * In this case, we want to translate our [ExampleLanguage]. It takes nodes that derive from + * [RawNode] -- these would come from a real parser in a real language -- and translates them into a + * CPG [Node]. + */ +class ExampleLanguageFrontend( + language: ExampleLanguage = ExampleLanguage(), + ctx: TranslationContext = + TranslationContext( + TranslationConfiguration.builder().build(), + ScopeManager(), + TypeManager() + ), + var rawFileNode: RawFileNode +) : LanguageFrontend(language, ctx) { + + /** + * We want to store an instance of our [Handler] here, in this case one that takes care of a + * [Declaration]. + */ + private var declarationHandler = ExampleDeclarationHandler(this) + + override fun parse(file: File): TranslationUnitDeclaration { + // In a real language frontend, we would read the AST from the contents of the file, but + // since we are not fully implementing a parser, we are using the rawFileNode from our + // constructor instead. + val parsedFile = rawFileNode + + return translationUnitDeclaration(parsedFile.name).globalScope { + for (child in parsedFile.children) { + this += declarationHandler.handle(child) + } + } + } + + override fun typeOf(type: RawTypeNode): Type { + return objectType(type.name) + } + + override fun codeOf(astNode: RawNode): String? { + return astNode.code + } + + override fun locationOf(astNode: RawNode): PhysicalLocation? { + return null + } + + override fun setComment(node: Node, astNode: RawNode) {} +} + +class ExampleDeclarationHandler(frontend: ExampleLanguageFrontend) : + Handler( + ::ProblemDeclaration, + frontend + ) { + override fun handle(node: RawDeclarationNode): Declaration { + return when (node) { + is RawFunctionNode -> handleFunction(node) + is RawParameterNode -> handleParameter(node) + else -> ProblemDeclaration() + } + } + + private fun handleFunction(node: RawFunctionNode): FunctionDeclaration { + return functionDeclaration(node.name, rawNode = node).withScope { + for (param in node.params) { + val decl = handle(param) + + this += decl + } + } + } + + private fun handleParameter(node: RawParameterNode): ParameterDeclaration { + return parameterDeclaration(node.name, type = frontend.typeOf(node.type)) + } +} diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/example/RawExampleLanguageParser.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/example/RawExampleLanguageParser.kt new file mode 100644 index 0000000000..4a104a8d16 --- /dev/null +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/example/RawExampleLanguageParser.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024, 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.example + +class RawFileNode(name: String, val children: List) : RawDeclarationNode(name) + +class RawFunctionNode(name: String, val params: List) : RawDeclarationNode(name) + +class RawParameterNode(name: String, val type: RawTypeNode) : RawDeclarationNode(name) + +open class RawDeclarationNode(val name: String) : RawNode() + +/** This node represents some sort of type in the example language. */ +open class RawTypeNode(val name: String) + +/** + * This node represents an in-memory object of a raw node of our example language that a parser + * would create. It is the base node for all other classes. + */ +open class RawNode(val code: String? = null)