Skip to content

Commit

Permalink
Implement a throw or raise statement (#1733)
Browse files Browse the repository at this point in the history
* new node for RaiseStatement + NodeBuilder code

* don't add python stuff in this branch

* started work on EOG and DFG

* doc

* raise: fluent and first test

* fluent: patch @oxisto

* Start work on DFG test for raise

* rename raise -> throw

* more renaming raise -> throw

* copy & paste handleThrowOperator

* Update cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/graph/edges/flows/DataflowTest.kt

Co-authored-by: KuechA <31155350+KuechA@users.noreply.github.com>

* Rename `findSymbols` into `lookupSymbolByName` (#1772)

* Rename `findSymbols` into `lookupSymbolByName`

This PR renames `findSymbols` into `lookupSymbolByName` as a more appropriate name, because it lookups a symbol by its name.

Fixes #1767

* Update cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt

Co-authored-by: KuechA <31155350+KuechA@users.noreply.github.com>

* Added documentation

---------

Co-authored-by: KuechA <31155350+KuechA@users.noreply.github.com>

* Update dependency rollup to v4.24.0 (#1774)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Added language trait `HasImplicitReceiver` (#1778)

Added language trait `HasImplicit Receiver`

This is needed in preparation for #1777 to better handle access to fields/members of a class when an implicit receiver is used.

* Cleanup of `SymbolResolver` (#1777)

* Fixed crash in `getCodeOfSubregion` (#1776)

* Add new function `lookupUniqueTypeSymbolByName` (#1781)

* Add new function `lookupUniqueTypeSymbolByName`

This adds two new functions `ScopeManager.lookupUniqueTypeSymbolByName` and `Reference.doesReferToType`. This harmonizes a lot of boilerplate code in type resolver, cxx extra pass and resolve ambuigity pass, which were all trying to achieve the same thing.

Fixes #1766

* Fixed issue with Go test, more robust handling of wrapped references

* Addressed code review

* Make sure to move `typeObservers` from old to new node when replacing nodes (#1783)

* Make sure to move `typeObservers` from old to new node when replacing nodes

* Added doc for typeobservers

* `implicit()` only triggers code/location update now if its not empty (#1784)

Otherwise, we override the code/location again.

* Added `:=` as simple operator in Python (#1785)

Named expressions in Python use `:=` as operator. Therefore we need to include it in the language definition. Otherwise, the `access` value of a reference will not be set correctly.

* code review

* code review

* code review

* rename cause to parentException

* doc

* ThrowStatement: add toString

* fix tests

* Merge EOG, add tests

* Documentation

* More doc of EOG handling

---------

Co-authored-by: KuechA <31155350+KuechA@users.noreply.github.com>
Co-authored-by: Christian Banse <christian.banse@aisec.fraunhofer.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alexander Kuechler <alexander.kuechler@aisec.fraunhofer.de>
  • Loading branch information
5 people authored Oct 18, 2024
1 parent e956427 commit a12951b
Show file tree
Hide file tree
Showing 10 changed files with 351 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -356,3 +356,18 @@ fun MetadataProvider.newLookupScopeStatement(
log(node)
return node
}

/**
* Creates a new [ThrowStatement]. 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.newThrowStatement(rawNode: Any? = null): ThrowStatement {
val node = ThrowStatement()
node.applyMetadata(this, EMPTY_NAME, rawNode, true)

log(node)
return node
}
Original file line number Diff line number Diff line change
Expand Up @@ -1400,6 +1400,7 @@ infix fun Expression.assignAsExpr(rhs: Expression): AssignExpression {

return node
}

/**
* Creates a new [AssignExpression] with a `=` [AssignExpression.operatorCode] in the Fluent Node
* DSL and adds it to the nearest enclosing [StatementHolder].
Expand All @@ -1414,6 +1415,23 @@ infix fun Expression.assignAsExpr(rhs: AssignExpression.() -> Unit): AssignExpre
return node
}

/**
* Creates a new [ThrowStatement] in the Fluent Node DSL and adds it to the nearest enclosing
* [StatementHolder].
*/
context(LanguageFrontend<*, *>, Holder<out Node>)
infix fun Expression.`throw`(init: (ThrowStatement.() -> Unit)?): ThrowStatement {
val node = (this@LanguageFrontend).newThrowStatement()
if (init != null) init(node)

val holder = this@Holder
if (holder is StatementHolder) {
holder += node
}

return node
}

/** Creates a new [Type] with the given [name] in the Fluent Node DSL. */
fun LanguageFrontend<*, *>.t(name: CharSequence, generics: List<Type> = listOf()) =
objectType(name, generics)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* 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.ArgumentHolder
import de.fraunhofer.aisec.cpg.graph.edges.ast.astOptionalEdgeOf
import de.fraunhofer.aisec.cpg.graph.edges.unwrapping
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Expression
import java.util.Objects
import org.apache.commons.lang3.builder.ToStringBuilder
import org.neo4j.ogm.annotation.Relationship

/** Represents a `throw` or `raise` statement. */
class ThrowStatement : Statement(), ArgumentHolder {

/** The exception object to be raised. */
@Relationship(value = "EXCEPTION") var exceptionEdge = astOptionalEdgeOf<Expression>()
var exception by unwrapping(ThrowStatement::exceptionEdge)

/**
* Some languages (Python) can add a parent exception (or `cause`) to indicate that an exception
* was raised while handling another exception.
*/
@Relationship(value = "PARENT_EXCEPTION")
var parentExceptionEdge = astOptionalEdgeOf<Expression>()
var parentException by unwrapping(ThrowStatement::parentExceptionEdge)

override fun addArgument(expression: Expression) {
when {
exception == null -> exception = expression
parentException == null -> parentException = expression
}
}

override fun replaceArgument(old: Expression, new: Expression): Boolean {
return when {
exception == old -> {
exception = new
true
}
parentException == old -> {
parentException = new
true
}
else -> false
}
}

override fun hasArgument(expression: Expression): Boolean {
return exception == expression || parentException == expression
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ThrowStatement) return false
return super.equals(other) &&
exception == other.exception &&
parentException == other.parentException
}

override fun hashCode() = Objects.hash(super.hashCode(), exception, parentException)

override fun toString(): String {
return ToStringBuilder(this, TO_STRING_STYLE)
.appendSuper(super.toString())
.append("exception", exception)
.append("parentException", parentException)
.toString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ class DFGPass(ctx: TranslationContext) : ComponentPass(ctx) {
is ForStatement -> handleForStatement(node)
is SwitchStatement -> handleSwitchStatement(node)
is IfStatement -> handleIfStatement(node)
is ThrowStatement -> handleThrowStatement(node)
// Declarations
is FieldDeclaration -> handleFieldDeclaration(node)
is FunctionDeclaration -> handleFunctionDeclaration(node, functionSummaries)
Expand All @@ -138,6 +139,12 @@ class DFGPass(ctx: TranslationContext) : ComponentPass(ctx) {
}
}

/** Handle a [ThrowStatement]. The exception and parent exception flow into the node. */
protected fun handleThrowStatement(node: ThrowStatement) {
node.exception?.let { node.prevDFGEdges += it }
node.parentException?.let { node.prevDFGEdges += it }
}

protected fun handleAssignExpression(node: AssignExpression) {
// If this is a compound assign, we also need to model a dataflow to the node itself
if (node.isCompoundAssignment) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ open class EvaluationOrderGraphPass(ctx: TranslationContext) : TranslationUnitPa
map[LookupScopeStatement::class.java] = {
handleLookupScopeStatement(it as LookupScopeStatement)
}
map[ThrowStatement::class.java] = { handleThrowStatement(it as ThrowStatement) }
}

protected fun doNothing() {
Expand Down Expand Up @@ -546,27 +547,38 @@ open class EvaluationOrderGraphPass(ctx: TranslationContext) : TranslationUnitPa
// TODO(oxisto): These operator codes are highly language specific and might be more suited
// to be handled differently (see https://github.com/Fraunhofer-AISEC/cpg/issues/1161)
if (node.operatorCode == "throw") {
handleThrowOperator(node)
handleThrowOperator(node, node.input)
} else {
handleUnspecificUnaryOperator(node)
}
}

protected fun handleThrowOperator(node: UnaryOperator) {
val input = node.input
createEOG(input)

val catchingScope =
scopeManager.firstScopeOrNull { scope -> scope is TryScope || scope is FunctionScope }

val throwType = input.type
/**
* Generates the EOG for a [node] which represents a statement/expression which throws an
* exception. Since some languages may accept different inputs to a throw statement (typically
* 1, sometimes 2, 0 is also possible), we have collect these in [inputs]. The input which is
* evaluated first, must be the first item in the vararg! Any `null` object in `inputs` will be
* filtered. We connect the throw statement internally, i.e., the inputs are evaluated from
* index 0 to n and then the whole node is evaluated.
*/
protected fun handleThrowOperator(node: Node, vararg inputs: Expression?) {
inputs.filterNotNull().forEach { createEOG(it) }
pushToEOG(node)
if (catchingScope is TryScope) {
catchingScope.catchesOrRelays[throwType] = currentPredecessors.toMutableList()
} else if (catchingScope is FunctionScope) {
catchingScope.catchesOrRelays[throwType] = currentPredecessors.toMutableList()

val input = inputs.firstOrNull()
val throwType = input?.type
if (throwType != null) {
val catchingScope =
scopeManager.firstScopeOrNull { scope ->
scope is TryScope || scope is FunctionScope
}
if (catchingScope is TryScope) {
catchingScope.catchesOrRelays[throwType] = currentPredecessors.toMutableList()
} else if (catchingScope is FunctionScope) {
catchingScope.catchesOrRelays[throwType] = currentPredecessors.toMutableList()
}
}
currentPredecessors.clear()
currentPredecessors.clear() // TODO: Should this be here or inside the if statement?
}

/**
Expand Down Expand Up @@ -1032,6 +1044,11 @@ open class EvaluationOrderGraphPass(ctx: TranslationContext) : TranslationUnitPa
pushToEOG(stmt)
}

/** This is copied & pasted with minimal adjustments from [handleThrowOperator]. */
protected fun handleThrowStatement(statement: ThrowStatement) {
handleThrowOperator(statement, statement.exception, statement.parentException)
}

companion object {
protected val LOGGER = LoggerFactory.getLogger(EvaluationOrderGraphPass::class.java)

Expand Down
20 changes: 20 additions & 0 deletions cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/GraphExamples.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1342,5 +1342,25 @@ class GraphExamples {
}
}
}

fun prepareThrowDFGTest(
config: TranslationConfiguration =
TranslationConfiguration.builder()
.defaultPasses()
.registerLanguage(TestLanguage("."))
.build()
) =
testFrontend(config).build {
translationResult {
translationUnit("some.file") {
function("foo", t("void")) {
body {
declare { variable("a", t("short")) { literal(42) } }
`throw` { call("SomeError") { ref("a") } }
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright (c) 2022, 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

import de.fraunhofer.aisec.cpg.GraphExamples.Companion.testFrontend
import de.fraunhofer.aisec.cpg.TranslationConfiguration
import de.fraunhofer.aisec.cpg.frontends.TestLanguage
import de.fraunhofer.aisec.cpg.graph.builder.*
import de.fraunhofer.aisec.cpg.graph.statements.ThrowStatement
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Block
import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression
import de.fraunhofer.aisec.cpg.test.assertLocalName
import kotlin.test.*

class ThrowStatementTest {
@Test
fun testThrow() {
val result =
testFrontend(
TranslationConfiguration.builder()
.defaultPasses()
.registerLanguage(TestLanguage("."))
.build()
)
.build {
translationResult {
translationUnit("some.file") {
function("foo", t("void")) {
body {
`throw` {}
`throw` { call("SomeError") }
`throw` {
call("SomeError")
call("SomeError2")
}
}
}
}
}
}

// Let's assert that we did this correctly
val main = result.functions["foo"]
assertNotNull(main)
val body = main.body
assertIs<Block>(body)

val emptyThrow = body.statements.getOrNull(0)
assertIs<ThrowStatement>(emptyThrow)
println(emptyThrow.toString()) // This is only here to simulate a higher test coverage
assertNull(emptyThrow.exception)
assertTrue(emptyThrow.prevDFG.isEmpty())

val throwWithExc = body.statements.getOrNull(1)
assertIs<ThrowStatement>(throwWithExc)
println(throwWithExc.toString()) // This is only here to simulate a higher test coverage
val throwCall = throwWithExc.exception
assertIs<CallExpression>(throwCall)
assertLocalName("SomeError", throwCall)
assertEquals(setOf<Node>(throwCall), throwWithExc.prevDFG.toSet())

val throwWithExcAndParent = body.statements.getOrNull(2)
assertIs<ThrowStatement>(throwWithExcAndParent)
println(
throwWithExcAndParent.toString()
) // This is only here to simulate a higher test coverage
val throwCallException = throwWithExcAndParent.exception
assertIs<CallExpression>(throwCallException)
assertLocalName("SomeError", throwCallException)
val throwCallParent = throwWithExcAndParent.parentException
assertIs<CallExpression>(throwCallParent)
assertLocalName("SomeError2", throwCallParent)
assertEquals(
setOf<Node>(throwCallException, throwCallParent),
throwWithExcAndParent.prevDFG.toSet()
)
}
}
Loading

0 comments on commit a12951b

Please # to comment.