Skip to content

Commit

Permalink
Introduce HasExplicitMemberAccess trait
Browse files Browse the repository at this point in the history
Fixes #1983
  • Loading branch information
oxisto committed Jan 28, 2025
1 parent 6b01783 commit 2160a88
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,13 @@ class ScopeManager : ScopeProvider {
scope: Scope? = node.scope,
predicate: ((Declaration) -> Boolean)? = null,
): List<Declaration> {
return lookupSymbolByName(node.name, node.language, node.location, scope, predicate)
return lookupSymbolByName(
node.name,
node.language,
node.location,
scope,
predicate = predicate,
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,16 @@ interface HasOperatorOverloading : LanguageTrait {
}
}

/**
* A language trait that specifies that this language has explicit member access, meaning that
* fields (and methods) of a class need to be accessed with a dot operator and cannot be accessed in
* an unqualified lookup.
*
* Examples include Python and Go where the name of the receiver such as `self` is always required
* to access a field or method.
*/
interface HasExplicitMemberAccess : LanguageTrait

/**
* Creates a [Pair] of class and operator code used in
* [HasOperatorOverloading.overloadedOperatorNames].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@
package de.fraunhofer.aisec.cpg.graph.scopes

import com.fasterxml.jackson.annotation.JsonBackReference
import de.fraunhofer.aisec.cpg.frontends.HasExplicitMemberAccess
import de.fraunhofer.aisec.cpg.frontends.Language
import de.fraunhofer.aisec.cpg.graph.Node
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.firstParentOrNull
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
Expand Down Expand Up @@ -169,7 +170,17 @@ sealed class Scope(
if (thisScopeOnly || modifiedScoped != null) {
break
} else {
scope = scope.parent
// If our language needs explicit lookup for fields (and other class members), we
// need to skip record scopes unless we are in a qualified lookup
if (
languageOnly is HasExplicitMemberAccess &&
!thisScopeOnly &&
scope.parent is RecordScope
) {
scope = scope.firstParentOrNull { it !is RecordScope }
} else {
scope = scope.parent
}
}
}

Expand Down Expand Up @@ -227,6 +238,20 @@ sealed class Scope(
list += value
}
}

fun firstParentOrNull(predicate: (Scope) -> Boolean): Scope? {
var scope = this.parent
while (scope != null) {
if (predicate(scope)) {
return scope
}

// go upwards in the scope tree
scope = scope.parent
}

return null
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ class PythonLanguage :
HasShortCircuitOperators,
HasOperatorOverloading,
HasFunctionStyleConstruction,
HasMemberExpressionAmbiguity {
HasMemberExpressionAmbiguity,
HasExplicitMemberAccess {
override val fileExtensions = listOf("py", "pyi")
override val namespaceDelimiter = "."
@Transient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,12 @@ class StatementHandler(frontend: PythonLanguageFrontend) :

for (s in stmt.body) {
when (s) {
is Python.AST.FunctionDef -> handleFunctionDef(s, cls)
is Python.AST.FunctionDef -> {
val stmt = handleFunctionDef(s, cls)
// We need to manually set the astParent because we are not assigning it to our
// statements and therefore are not triggering our automagic parent setter
stmt.astParent = cls
}
else -> cls.statements += handleNode(s)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (c) 2025, 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.python

import de.fraunhofer.aisec.cpg.graph.*
import de.fraunhofer.aisec.cpg.graph.declarations.FieldDeclaration
import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberExpression
import de.fraunhofer.aisec.cpg.test.analyze
import de.fraunhofer.aisec.cpg.test.assertRefersTo
import java.io.File
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull

class SymbolResolverTest {
@Test
fun testFields() {
val topLevel = File("src/test/resources/python/fields.py")
val result =
analyze(listOf(topLevel), topLevel.toPath(), true) {
it.registerLanguage<PythonLanguage>()
}

val a =
result.namespaces["fields"]
.variables[{ it.name.localName == "a" && it !is FieldDeclaration }]
assertNotNull(a)

val fieldA = result.records["MyClass"]?.fields["a"]
assertNotNull(fieldA)

val aRefs = result.refs("a")
aRefs.filterIsInstance<MemberExpression>().forEach { assertRefersTo(it, fieldA) }
aRefs.filter { it !is MemberExpression }.forEach { assertRefersTo(it, a) }

val osRefs = result.refs("os")
assertEquals(1, osRefs.size)

val osNameRefs = result.refs("os.name")
assertEquals(1, osNameRefs.size)
}
}
17 changes: 17 additions & 0 deletions cpg-language-python/src/test/resources/python/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import os

a = "Hello"

class MyClass:
def foo(self):
self.a = 1
print(a)

def bar(self):
self.os = 1
print(os.name)


m = MyClass()
m.foo()
m.bar()

0 comments on commit 2160a88

Please # to comment.