Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial version of python try with all things #1704

Merged
merged 16 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,44 @@ import org.neo4j.ogm.annotation.Relationship

/** A [Statement] which represents a try/catch block, primarily used for exception handling. */
class TryStatement : Statement() {
/**
* This represents some kind of resource which is typically opened (or similar) while entering
* the [tryBlock]. If this operation fails, we may continue with the [finallyBlock]. However,
* there is no exception raised but if an exception occurs while opening the resource, we won't
* enter the [tryBlock].
*/
@Relationship(value = "RESOURCES", direction = Relationship.Direction.OUTGOING)
var resourceEdges = astEdgesOf<Statement>()
var resources by unwrapping(TryStatement::resourceEdges)

/**
* This represents a block whose statements can throw exceptions which are handled by the
* [catchClauses].
*/
@Relationship(value = "TRY_BLOCK") var tryBlockEdge = astOptionalEdgeOf<Block>()
var tryBlock by unwrapping(TryStatement::tryBlockEdge)

/**
* This represents a block whose statements are only executed if the [tryBlock] finished without
* exceptions. Note that any exception thrown in this block is no longer caught by the
* [catchClauses].
*/
@Relationship(value = "ELSE_BLOCK") var elseBlockEdge = astOptionalEdgeOf<Block>()
var elseBlock by unwrapping(TryStatement::elseBlockEdge)
lshala marked this conversation as resolved.
Show resolved Hide resolved

/**
* This represents a block of statements which is always executed after finishing the [tryBlock]
* or one of the [catchClauses]. Note that any exception thrown in this block is no longer
* caught by the [catchClauses].
*/
@Relationship(value = "FINALLY_BLOCK") var finallyBlockEdge = astOptionalEdgeOf<Block>()
var finallyBlock by unwrapping(TryStatement::finallyBlockEdge)

/**
* This represents a set of blocks whose statements handle the exceptions which are thrown in
* the [tryBlock]. There can be multiple catch clauses, but it is also possible that none
* exists.
*/
@Relationship(value = "CATCH_CLAUSES", direction = Relationship.Direction.OUTGOING)
var catchClauseEdges = astEdgesOf<CatchClause>()
var catchClauses by unwrapping(TryStatement::catchClauseEdges)
Expand All @@ -58,9 +86,10 @@ class TryStatement : Statement() {
tryBlock == other.tryBlock &&
finallyBlock == other.finallyBlock &&
catchClauses == other.catchClauses &&
elseBlock == other.elseBlock &&
propertyEqualsList(catchClauseEdges, other.catchClauseEdges))
}

override fun hashCode() =
Objects.hash(super.hashCode(), resources, tryBlock, finallyBlock, catchClauses)
Objects.hash(super.hashCode(), resources, tryBlock, finallyBlock, catchClauses, elseBlock)
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import de.fraunhofer.aisec.cpg.helpers.SubgraphWalker
import de.fraunhofer.aisec.cpg.helpers.Util
import de.fraunhofer.aisec.cpg.tryCast
import java.util.*
import kotlin.collections.ArrayList
import org.slf4j.LoggerFactory

/**
Expand Down Expand Up @@ -599,6 +600,7 @@ open class EvaluationOrderGraphPass(ctx: TranslationContext) : TranslationUnitPa

createEOG(node.tryBlock)
val tmpEOGNodes = ArrayList(currentPredecessors)
val catchEnds = ArrayList<Node>()
konradweiss marked this conversation as resolved.
Show resolved Hide resolved
val catchesOrRelays = tryScope?.catchesOrRelays
for (catchClause in node.catchClauses) {
currentPredecessors.clear()
Expand All @@ -617,8 +619,21 @@ open class EvaluationOrderGraphPass(ctx: TranslationContext) : TranslationUnitPa
toRemove.forEach { catchesOrRelays?.remove(it) }
pushToEOG(catchClause)
createEOG(catchClause.body)
catchEnds.addAll(currentPredecessors)
}

// We need to handle the else block after the catch clauses, as the else could contain a
// throw itself
// that should not be caught be the catch clauses.
if (node.elseBlock != null) {
currentPredecessors.clear()
currentPredecessors.addAll(tmpEOGNodes)
createEOG(node.elseBlock)
// All valid try ends got through the else block.
tmpEOGNodes.clear()
tmpEOGNodes.addAll(currentPredecessors)
}
tmpEOGNodes.addAll(catchEnds)

val canTerminateExceptionfree = tmpEOGNodes.any { reachableFromValidEOGRoot(it) }
currentPredecessors.clear()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ interface Python {
val body: kotlin.collections.List<BaseStmt> by lazy { "body" of pyObject }
val handlers: kotlin.collections.List<excepthandler> by lazy { "handlers" of pyObject }
val orelse: kotlin.collections.List<BaseStmt> by lazy { "orelse" of pyObject }
val stmt: kotlin.collections.List<BaseStmt> by lazy { "StmtBase" of pyObject }
val finalbody: kotlin.collections.List<BaseStmt> by lazy { "finalbody" of pyObject }
}

/**
Expand Down Expand Up @@ -1321,8 +1321,8 @@ interface Python {
* TODO: excepthandler <-> ExceptHandler
*/
class excepthandler(pyObject: PyObject) : AST(pyObject), WithLocation {
val type: BaseExpr by lazy { "type" of pyObject }
val name: String by lazy { "name" of pyObject }
val type: BaseExpr? by lazy { "type" of pyObject }
val name: String? by lazy { "name" of pyObject }
val body: kotlin.collections.List<BaseStmt> by lazy { "body" of pyObject }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,7 @@
*/
package de.fraunhofer.aisec.cpg.frontends.python

import de.fraunhofer.aisec.cpg.frontends.HasFunctionStyleConstruction
import de.fraunhofer.aisec.cpg.frontends.HasOperatorOverloading
import de.fraunhofer.aisec.cpg.frontends.HasShortCircuitOperators
import de.fraunhofer.aisec.cpg.frontends.Language
import de.fraunhofer.aisec.cpg.frontends.of
import de.fraunhofer.aisec.cpg.frontends.*
import de.fraunhofer.aisec.cpg.graph.HasOverloadedOperation
import de.fraunhofer.aisec.cpg.graph.autoType
import de.fraunhofer.aisec.cpg.graph.declarations.ParameterDeclaration
Expand All @@ -45,7 +41,8 @@ class PythonLanguage :
Language<PythonLanguageFrontend>(),
HasShortCircuitOperators,
HasOperatorOverloading,
HasFunctionStyleConstruction {
HasFunctionStyleConstruction,
HasAnonymousIdentifier {
konradweiss marked this conversation as resolved.
Show resolved Hide resolved
override val fileExtensions = listOf("py", "pyi")
override val namespaceDelimiter = "."
@Transient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ fun fromPython(pyObject: Any?): Python.BaseObject {
"ast.arg" -> Python.AST.arg(pyObject)
"ast.arguments" -> Python.AST.arguments(pyObject)
"ast.comprehension" -> Python.AST.comprehension(pyObject)
"ast.ExceptHandler",
oxisto marked this conversation as resolved.
Show resolved Hide resolved
"ast.excepthandler" -> Python.AST.excepthandler(pyObject)
"ast.keyword" -> Python.AST.keyword(pyObject)
"ast.match_case" -> Python.AST.match_case(pyObject)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
*/
package de.fraunhofer.aisec.cpg.frontends.python

import de.fraunhofer.aisec.cpg.frontends.HasAnonymousIdentifier
import de.fraunhofer.aisec.cpg.frontends.HasOperatorOverloading
import de.fraunhofer.aisec.cpg.frontends.isKnownOperatorName
import de.fraunhofer.aisec.cpg.frontends.python.Python.AST.IsAsync
Expand All @@ -33,9 +34,7 @@ import de.fraunhofer.aisec.cpg.frontends.python.PythonLanguage.Companion.MODIFIE
import de.fraunhofer.aisec.cpg.graph.*
import de.fraunhofer.aisec.cpg.graph.Annotation
import de.fraunhofer.aisec.cpg.graph.declarations.*
import de.fraunhofer.aisec.cpg.graph.statements.AssertStatement
import de.fraunhofer.aisec.cpg.graph.statements.DeclarationStatement
import de.fraunhofer.aisec.cpg.graph.statements.Statement
import de.fraunhofer.aisec.cpg.graph.statements.*
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Block
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Expression
import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberExpression
Expand Down Expand Up @@ -67,12 +66,12 @@ class StatementHandler(frontend: PythonLanguageFrontend) :
is Python.AST.Break -> newBreakStatement(rawNode = node)
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.Global,
is Python.AST.Match,
is Python.AST.Nonlocal,
is Python.AST.Raise,
is Python.AST.Try,
is Python.AST.TryStar,
is Python.AST.With,
is Python.AST.AsyncWith ->
Expand All @@ -84,7 +83,51 @@ class StatementHandler(frontend: PythonLanguageFrontend) :
}

/**
* Translates a Python (https://docs.python.org/3/library/ast.html#ast.Assert] into a
* Translates an [`ExceptHandler`](https://docs.python.org/3/library/ast.html#ast.ExceptHandler)
* to a [CatchClause].
*
* It adds all the statements to the body and will set a parameter if it exists. For the
* catch-all clause, we do not set the [CatchClause.parameter].
*/
private fun handleExcepthandler(node: Python.AST.excepthandler): CatchClause {
val catchClause = newCatchClause(rawNode = node)
catchClause.body = makeBlock(node.body, node)
// The parameter can have a type but if the type is None/null, it's the "catch-all" clause.
// In this case, it also cannot have a name, so we can skip the variable declaration.
if (node.type != null) {
// the parameter can have a name, or we use the anonymous identifier _
konradweiss marked this conversation as resolved.
Show resolved Hide resolved
catchClause.parameter =
newVariableDeclaration(
name = node.name ?: (language as? HasAnonymousIdentifier)?.anonymousIdentifier,
konradweiss marked this conversation as resolved.
Show resolved Hide resolved
type = frontend.typeOf(node.type),
rawNode = node
)
}
return catchClause
}

/**
* Translates a Python [`Try`](https://docs.python.org/3/library/ast.html#ast.Try) into a
* [TryStatement].
*/
private fun handleTryStatement(node: Python.AST.Try): TryStatement {
val tryStatement = newTryStatement(rawNode = node)
tryStatement.tryBlock = makeBlock(node.body, node)

tryStatement.catchClauses.addAll(node.handlers.map { handleExcepthandler(it) })

if (node.orelse.isNotEmpty()) {
tryStatement.elseBlock = makeBlock(node.orelse, node)
}

if (node.finalbody.isNotEmpty()) {
tryStatement.finallyBlock = makeBlock(node.finalbody, node)
}

return tryStatement
}
/**
* Translates a Python [`Assert`](https://docs.python.org/3/library/ast.html#ast.Assert) into a
* [AssertStatement].
*/
private fun handleAssert(node: Python.AST.Assert): AssertStatement {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ import de.fraunhofer.aisec.cpg.frontends.python.PythonLanguage
import de.fraunhofer.aisec.cpg.graph.*
import de.fraunhofer.aisec.cpg.graph.statements.AssertStatement
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Literal
import de.fraunhofer.aisec.cpg.helpers.Util
import de.fraunhofer.aisec.cpg.test.analyze
import de.fraunhofer.aisec.cpg.test.analyzeAndGetFirstTU
import de.fraunhofer.aisec.cpg.test.assertLocalName
import de.fraunhofer.aisec.cpg.test.assertResolvedType
import java.nio.file.Path
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.*
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.TestInstance

Expand All @@ -59,6 +59,69 @@ class StatementHandlerTest {
assertNotNull(result)
}

@Test
fun testTry() {
val tu =
analyzeAndGetFirstTU(listOf(topLevel.resolve("try.py").toFile()), topLevel, true) {
it.registerLanguage<PythonLanguage>()
}
assertNotNull(tu)

val tryAll = tu.functions["tryAll"]?.trys?.singleOrNull()
assertNotNull(tryAll)

assertEquals(1, tryAll.tryBlock?.statements?.size)

assertEquals(3, tryAll.catchClauses.size)
assertLocalName("_", tryAll.catchClauses[0].parameter)
assertLocalName("e", tryAll.catchClauses[1].parameter)
assertNull(tryAll.catchClauses[2].parameter)

assertEquals(1, tryAll.elseBlock?.statements?.size)
assertEquals(1, tryAll.finallyBlock?.statements?.size)

val tryOnlyFinally = tu.functions["tryOnlyFinally"]?.trys?.singleOrNull()
assertNotNull(tryOnlyFinally)

assertEquals(1, tryOnlyFinally.tryBlock?.statements?.size)

assertEquals(0, tryOnlyFinally.catchClauses.size)

assertNull(tryOnlyFinally.elseBlock)
assertEquals(1, tryOnlyFinally.finallyBlock?.statements?.size)

val tryOnlyExcept = tu.functions["tryOnlyExcept"]?.trys?.singleOrNull()
assertNotNull(tryOnlyExcept)

assertEquals(1, tryOnlyExcept.tryBlock?.statements?.size)

assertEquals(1, tryOnlyExcept.catchClauses.size)
assertNull(tryOnlyExcept.catchClauses.single().parameter)

assertNull(tryOnlyExcept.elseBlock)
assertNull(tryOnlyExcept.finallyBlock)

// Test EOG integrity with else block

// All entries to the else block must come from the try block
assertTrue(
Util.eogConnect(
n = tryAll.elseBlock,
en = Util.Edge.ENTRIES,
refs = listOf(tryAll.tryBlock)
)
)

// All exits from the else block must go to the entries of the non-empty finals block
assertTrue(
Util.eogConnect(
n = tryAll.elseBlock,
en = Util.Edge.EXITS,
refs = listOf(tryAll.finallyBlock)
)
)
}

@Test
fun testAsync() {
val tu =
Expand Down
25 changes: 25 additions & 0 deletions cpg-language-python/src/test/resources/python/try.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
def tryAll(a):
try:
b = a+2
except Exception1:
print("There was an occurrence of Exception1")
except OtherException as e:
print("We saw exception" + e)
except:
print("Catch all!")
else:
print("All good, got " + b)
finally:
print("It's over")

def tryOnlyFinally(a):
try:
b = a+2
finally:
print("It's over")

def tryOnlyExcept(a):
try:
b = a+2
except:
print("Fail")
Loading