From 0f434f60e5a2b27aa73de44f14f6f1ba5dfced18 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Fri, 27 Sep 2024 12:27:11 +0200 Subject: [PATCH] Added a `LookupScopeStatement` node This PR adds a new node type `LookupScopeStatement`, which can be used to adjust the lookup scope of symbols that are resolved in the current scope. Most prominent examples are Python's `global` and `nonlocal` statements. Support for python will be added in a later PR. This will only add the node and provides the basic functionality in the lookup. --- .../aisec/cpg/graph/StatementBuilder.kt | 27 ++++++++ .../aisec/cpg/graph/scopes/Scope.kt | 31 +++++++-- .../graph/statements/LookupScopeStatement.kt | 61 +++++++++++++++++ .../aisec/cpg/graph/scopes/ScopeTest.kt | 67 +++++++++++++++++++ 4 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/LookupScopeStatement.kt create mode 100644 cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/ScopeTest.kt diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/StatementBuilder.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/StatementBuilder.kt index c4157ff013..adc99d1893 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/StatementBuilder.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/StatementBuilder.kt @@ -28,6 +28,8 @@ package de.fraunhofer.aisec.cpg.graph import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend import de.fraunhofer.aisec.cpg.graph.Node.Companion.EMPTY_NAME import de.fraunhofer.aisec.cpg.graph.NodeBuilder.log +import de.fraunhofer.aisec.cpg.graph.scopes.Scope +import de.fraunhofer.aisec.cpg.graph.scopes.Symbol import de.fraunhofer.aisec.cpg.graph.statements.* /** @@ -329,3 +331,28 @@ fun MetadataProvider.newDefaultStatement(rawNode: Any? = null): DefaultStatement log(node) return node } + +/** + * Creates a new [LookupScopeStatement]. The [MetadataProvider] receiver will be used to fill + * different meta-data using [Node.applyMetadata]. Calling this extension function outside of Kotlin + * requires an appropriate [MetadataProvider], such as a [LanguageFrontend] as an additional + * prepended argument. + */ +@JvmOverloads +fun MetadataProvider.newSearchScopeStatement( + symbols: List, + targetScope: Scope? = null, + rawNode: Any? = null +): LookupScopeStatement { + val node = LookupScopeStatement() + node.targetScope = targetScope + node.applyMetadata(this, EMPTY_NAME, rawNode, true) + + // Add it to our scope + for (symbol in symbols) { + node.scope?.predefinedLookupScopes[symbol] = node + } + + log(node) + return node +} diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/Scope.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/Scope.kt index 6194f54495..e128ecd112 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/Scope.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/Scope.kt @@ -32,6 +32,8 @@ import de.fraunhofer.aisec.cpg.graph.Node.Companion.TO_STRING_STYLE import de.fraunhofer.aisec.cpg.graph.declarations.Declaration import de.fraunhofer.aisec.cpg.graph.declarations.ImportDeclaration import de.fraunhofer.aisec.cpg.graph.statements.LabelStatement +import de.fraunhofer.aisec.cpg.graph.statements.LookupScopeStatement +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference import de.fraunhofer.aisec.cpg.helpers.neo4j.NameConverter import org.apache.commons.lang3.builder.ToStringBuilder import org.neo4j.ogm.annotation.GeneratedValue @@ -90,6 +92,16 @@ abstract class Scope( */ @Transient var wildcardImports: MutableSet = mutableSetOf() + /** + * In some languages, the lookup scope of a symbol that is being resolved (e.g. of a + * [Reference]) can be adjusted through keywords (such as `global` in Python or PHP). + * + * We store this information in the form of a [LookupScopeStatement] in the AST, but we need to + * also store this information in the scope to avoid unnecessary AST traversals when resolving + * symbols using [lookupSymbol]. + */ + @Transient var predefinedLookupScopes: MutableMap = mutableMapOf() + /** Adds a [declaration] with the defined [symbol]. */ fun addSymbol(symbol: Symbol, declaration: Declaration) { if (declaration is ImportDeclaration && declaration.wildcardImport) { @@ -123,8 +135,16 @@ abstract class Scope( replaceImports: Boolean = true, predicate: ((Declaration) -> Boolean)? = null ): List { - // First, try to look for the symbol in the current scope - var scope: Scope? = this + // First, try to look for the symbol in the current scope (unless we have a predefined + // search scope). In the latter case we also need to restrict the lookup to the search scope + var modifiedScoped = this.predefinedLookupScopes[symbol]?.targetScope + var scope: Scope? = + if (modifiedScoped != null) { + modifiedScoped + } else { + this + } + var list: MutableList? = null while (scope != null) { @@ -154,10 +174,11 @@ abstract class Scope( } // If we do not have a hit, we can go up one scope, unless thisScopeOnly is set to true - if (!thisScopeOnly) { - scope = scope.parent - } else { + // (or we had a modified scope) + if (thisScopeOnly || modifiedScoped != null) { break + } else { + scope = scope.parent } } diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/LookupScopeStatement.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/LookupScopeStatement.kt new file mode 100644 index 0000000000..67d6d93ad6 --- /dev/null +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/LookupScopeStatement.kt @@ -0,0 +1,61 @@ +/* + * 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.statements + +import de.fraunhofer.aisec.cpg.graph.newSearchScopeStatement +import de.fraunhofer.aisec.cpg.graph.scopes.Scope +import de.fraunhofer.aisec.cpg.graph.scopes.Symbol +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference +import java.util.Objects + +/** + * This statement modifies the lookup scope of one or more [Reference] nodes (or more precise it's + * symbols) within the current [Scope]. The most prominent example of this are the Python `global` + * and `nonlocal` keywords. + * + * This node itself does not implement the actual functionality. It is necessary to add this node + * (or the information therein) to [Scope.predefinedLookupScopes]. The reason for this is that we + * want to avoid AST traversals in the scope/identifier lookup. + * + * The [newSearchScopeStatement] node builder will add this automatically, so it is STRONGLY + * encouraged that the node builder is used instead of creating the node itself. + */ +class LookupScopeStatement : Statement() { + + /** The symbols this statement affects. */ + var symbols: List = listOf() + + /** The target scope to which the references are referring to. */ + var targetScope: Scope? = null + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is LookupScopeStatement) return false + return super.equals(other) && symbols == other.symbols && targetScope == other.targetScope + } + + override fun hashCode() = Objects.hash(super.hashCode(), symbols, targetScope) +} diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/ScopeTest.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/ScopeTest.kt new file mode 100644 index 0000000000..77f6db5b6a --- /dev/null +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/ScopeTest.kt @@ -0,0 +1,67 @@ +/* + * 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.scopes + +import de.fraunhofer.aisec.cpg.graph.Name +import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration +import de.fraunhofer.aisec.cpg.graph.statements.LookupScopeStatement +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Block +import kotlin.test.Test +import kotlin.test.assertEquals + +class ScopeTest { + @Test + fun testLookup() { + // some mock variable declarations, global and local + var globalA = VariableDeclaration() + globalA.name = Name("a") + var localA = VariableDeclaration() + localA.name = Name("a") + + // two scopes, global and local + val globalScope = GlobalScope() + globalScope.addSymbol("a", globalA) + val scope = BlockScope(Block()) + scope.parent = globalScope + scope.addSymbol("a", localA) + + // if we try to resolve "a" now, this should point to the local A since we start there and + // move upwards + var result = scope.lookupSymbol("a") + assertEquals(listOf(localA), result) + + // now, we pretend to have a lookup scope modifier for a symbol, e.g. through "global" in + // Python + var stmt = LookupScopeStatement() + stmt.targetScope = globalScope + stmt.symbols = listOf("a") + scope.predefinedLookupScopes["a"] = stmt + + // let's try the lookup again, this time it should point to the global A + result = scope.lookupSymbol("a") + assertEquals(listOf(globalA), result) + } +}