Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into ak/pythonOrElse
Browse files Browse the repository at this point in the history
  • Loading branch information
oxisto committed Sep 26, 2024
2 parents cdbee70 + 1b91777 commit bf5b43b
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,20 @@
*/
package de.fraunhofer.aisec.cpg.graph.statements.expressions

import de.fraunhofer.aisec.cpg.graph.edges.ast.astOptionalEdgeOf
import de.fraunhofer.aisec.cpg.graph.edges.ast.astEdgesOf
import de.fraunhofer.aisec.cpg.graph.edges.unwrapping
import java.util.Objects
import org.neo4j.ogm.annotation.Relationship

class DeleteExpression : Expression() {
@Relationship("OPERAND") var operandEdge = astOptionalEdgeOf<Expression>()
var operand by unwrapping(DeleteExpression::operandEdge)
@Relationship("OPERANDS") var operandEdges = astEdgesOf<Expression>()
var operands by unwrapping(DeleteExpression::operandEdges)

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is DeleteExpression) return false
return super.equals(other) && operand == other.operand
return super.equals(other) && operands == other.operands
}

override fun hashCode() = Objects.hash(super.hashCode(), operand)
override fun hashCode() = Objects.hash(super.hashCode(), operands)
}
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,9 @@ open class EvaluationOrderGraphPass(ctx: TranslationContext) : TranslationUnitPa
}

protected fun handleDeleteExpression(node: DeleteExpression) {
createEOG(node.operand)
for (operand in node.operands) {
createEOG(operand)
}
pushToEOG(node)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ class ExpressionHandler(lang: CXXLanguageFrontend) :
for (name in ctx.implicitDestructorNames) {
log.debug("Implicit constructor name {}", name)
}
deleteExpression.operand = handle(ctx.operand)
handle(ctx.operand)?.let { deleteExpression.operands.add(it) }
return deleteExpression
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class StatementHandler(frontend: PythonLanguageFrontend) :
is Python.AST.Continue -> newContinueStatement(rawNode = node)
is Python.AST.Assert -> handleAssert(node)
is Python.AST.Try -> handleTryStatement(node)
is Python.AST.Delete,
is Python.AST.Delete -> handleDelete(node)
is Python.AST.Global,
is Python.AST.Match,
is Python.AST.Nonlocal,
Expand Down Expand Up @@ -129,6 +129,27 @@ class StatementHandler(frontend: PythonLanguageFrontend) :
return tryStatement
}

/**
* Translates a Python [`Delete`](https://docs.python.org/3/library/ast.html#ast.Delete) into a
* [DeleteExpression].
*/
private fun handleDelete(node: Python.AST.Delete): DeleteExpression {
val delete = newDeleteExpression(rawNode = node)
node.targets.forEach { target ->
if (target is Python.AST.Subscript) {
delete.operands.add(frontend.expressionHandler.handle(target))
} else {
delete.additionalProblems +=
newProblemExpression(
problem =
"handleDelete: 'Name' and 'Attribute' deletions are not supported, as they removes them from the scope.",
rawNode = target
)
}
}
return delete
}

/**
* Translates a Python [`Assert`](https://docs.python.org/3/library/ast.html#ast.Assert) into a
* [AssertStatement].
Expand Down Expand Up @@ -420,7 +441,12 @@ class StatementHandler(frontend: PythonLanguageFrontend) :
handleArguments(s.args, result, recordDeclaration)

if (s.body.isNotEmpty()) {
result.body = makeBlock(s.body, parentNode = s)
// Make sure we open a new (block) scope for the function body. This is not a 1:1
// mapping to python scopes, since python only has a "function scope", but in the CPG
// the function scope only comprises the function arguments, and we need a block scope
// to
// hold all local variables within the function body.
result.body = makeBlock(s.body, parentNode = s, enterScope = true)
}

frontend.scopeManager.leaveScope(result)
Expand Down Expand Up @@ -626,16 +652,29 @@ class StatementHandler(frontend: PythonLanguageFrontend) :
* This function "wraps" a list of [Python.AST.BaseStmt] nodes into a [Block]. Since the list
* itself does not have a code/location, we need to employ [codeAndLocationFromChildren] on the
* [parentNode].
*
* Optionally, a new scope will be opened when [enterScope] is specified. This should be done
* VERY carefully, as Python has a very limited set of scopes and is most likely only to be used
* by [handleFunctionDef].
*/
private fun makeBlock(
stmts: List<Python.AST.BaseStmt>,
parentNode: Python.AST.WithLocation
parentNode: Python.AST.WithLocation,
enterScope: Boolean = false,
): Block {
val result = newBlock()
if (enterScope) {
frontend.scopeManager.enterScope(result)
}

for (stmt in stmts) {
result.statements += handle(stmt)
}

if (enterScope) {
frontend.scopeManager.leaveScope(result)
}

// Try to retrieve the code and location from the parent node, if it is a base stmt
var baseStmt = parentNode as? Python.AST.BaseStmt
return if (baseStmt != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,13 @@ import de.fraunhofer.aisec.cpg.graph.statements.*
import de.fraunhofer.aisec.cpg.graph.statements.expressions.*
import de.fraunhofer.aisec.cpg.helpers.Util
import de.fraunhofer.aisec.cpg.test.*
import de.fraunhofer.aisec.cpg.test.analyze
import de.fraunhofer.aisec.cpg.test.analyzeAndGetFirstTU
import de.fraunhofer.aisec.cpg.test.assertResolvedType
import java.nio.file.Path
import kotlin.test.*
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.TestInstance
import kotlin.test.*

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class StatementHandlerTest : BaseTest() {
Expand Down Expand Up @@ -184,18 +187,49 @@ class StatementHandlerTest : BaseTest() {
assertEquals("Test message", message.value, "The assert message is incorrect")
}

@Test
fun testDeleteStatements() {
analyzeFile("delete.py")

val deleteExpressions = result.statements.filterIsInstance<DeleteExpression>()
assertEquals(4, deleteExpressions.size)

// Test for `del a`
val deleteStmt1 = deleteExpressions[0]
assertEquals(0, deleteStmt1.operands.size)
assertEquals(1, deleteStmt1.additionalProblems.size)

// Test for `del my_list[2]`
val deleteStmt2 = deleteExpressions[1]
assertEquals(1, deleteStmt2.operands.size)
assertTrue(deleteStmt2.operands.first() is SubscriptExpression)
assertTrue(deleteStmt2.additionalProblems.isEmpty())

// Test for `del my_dict['b']`
val deleteStmt3 = deleteExpressions[2]
assertEquals(1, deleteStmt3.operands.size)
assertTrue(deleteStmt3.operands.first() is SubscriptExpression)
assertTrue(deleteStmt3.additionalProblems.isEmpty())

// Test for `del obj.d`
val deleteStmt4 = deleteExpressions[3]
assertEquals(0, deleteStmt4.operands.size)
assertEquals(1, deleteStmt4.additionalProblems.size)
}

@Test
fun testTypeHints() {
analyzeFile("type_hints.py")

// type comments
val a = result.refs["a"]
assertNotNull(a)
assertEquals(with(result) { assertResolvedType("int") }, a.type)

// type annotation
val b = result.refs["b"]
assertNotNull(b)
assertEquals(with(result) { assertResolvedType("str") }, b.type)
with(result) {
// type comments
val a = result.refs["a"]
assertNotNull(a)
assertEquals(assertResolvedType("int"), a.type)

// type annotation
val b = result.refs["b"]
assertNotNull(b)
assertEquals(assertResolvedType("str"), b.type)
}
}
}
16 changes: 16 additions & 0 deletions cpg-language-python/src/test/resources/python/delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
a = 5
b = a
del a

my_list = [1, 2, 3]
del my_list[2]

my_dict = {'a': 1, 'b': 2}
del my_dict['b']

class MyClass:
def __init__(self):
self.d = 1

obj = MyClass()
del obj.d

0 comments on commit bf5b43b

Please sign in to comment.