Skip to content

Commit

Permalink
Added a LookupScopeStatement node
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
oxisto committed Sep 27, 2024
1 parent ff75361 commit 0f434f6
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.*

/**
Expand Down Expand Up @@ -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<Symbol>,
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -90,6 +92,16 @@ abstract class Scope(
*/
@Transient var wildcardImports: MutableSet<ImportDeclaration> = 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<Symbol, LookupScopeStatement> = mutableMapOf()

/** Adds a [declaration] with the defined [symbol]. */
fun addSymbol(symbol: Symbol, declaration: Declaration) {
if (declaration is ImportDeclaration && declaration.wildcardImport) {
Expand Down Expand Up @@ -123,8 +135,16 @@ abstract class Scope(
replaceImports: Boolean = true,
predicate: ((Declaration) -> Boolean)? = null
): List<Declaration> {
// 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<Declaration>? = null

while (scope != null) {
Expand Down Expand Up @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Symbol> = 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)
}
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 0f434f6

Please sign in to comment.