Skip to content

Commit

Permalink
Handle python BoolOp for multiple arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
KuechA committed Sep 20, 2024
1 parent d00c0e4 commit 7a45945
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -187,16 +187,38 @@ class ExpressionHandler(frontend: PythonLanguageFrontend) :
is Python.AST.And -> "and"
is Python.AST.Or -> "or"
}
val ret = newBinaryOperator(operatorCode = op, rawNode = node)
if (node.values.size != 2) {
return newProblemExpression(
return handleBoolOpRecursively(node.values, op, node)
}

/**
* Recursively generates a (potentially nested) [BinaryOperator] from a `BoolOp`. [values]
* contains the list of operands to consider in this step. If only two operands exist, a simple
* [BinaryOperator] will be generated. Less than two operands don't make sense and will generate
* a [ProblemExpression]. More than two operands will lead to a nested [BinaryOperator] and we
* will call this method recursively to handle all but the first operand in the nested
* [BinaryOperator] used in the `rhs`.
*/
private fun handleBoolOpRecursively(
values: List<Python.AST.BaseExpr>,
op: String,
node: Python.AST.BoolOp
): Expression {
return if (values.size < 2) {
newProblemExpression(
"Expected exactly two expressions but got " + node.values.size,
rawNode = node
)
} else if (values.size == 2) {
val ret = newBinaryOperator(operatorCode = op, rawNode = node)
ret.lhs = handle(values[0])
ret.rhs = handle(values[1])
ret
} else {
val ret = newBinaryOperator(operatorCode = op, rawNode = node)
ret.lhs = handle(values[0])
ret.rhs = handleBoolOpRecursively(values.subList(1, values.size), op, node)
ret
}
ret.lhs = handle(node.values[0])
ret.rhs = handle(node.values[1])
return ret
}

private fun handleList(node: Python.AST.List): Expression {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* 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.frontends.python

import de.fraunhofer.aisec.cpg.graph.*
import de.fraunhofer.aisec.cpg.graph.statements.expressions.BinaryOperator
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Literal
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference
import de.fraunhofer.aisec.cpg.test.analyze
import java.nio.file.Path
import kotlin.test.*

class ExpressionHandlerTest {
@Test
fun testBoolOps() {
val topLevel = Path.of("src", "test", "resources", "python")
val result =
analyze(listOf(topLevel.resolve("boolop.py").toFile()), topLevel, true) {
it.registerLanguage<PythonLanguage>()
}
assertNotNull(result)

val twoBoolOpCondition =
result.functions["twoBoolOp"]?.ifs?.singleOrNull()?.condition as? BinaryOperator
assertNotNull(twoBoolOpCondition)
assertEquals("and", twoBoolOpCondition.operatorCode)
assertEquals("a", (twoBoolOpCondition.lhs as? Reference)?.name?.localName)
assertEquals(true, (twoBoolOpCondition.rhs as? Literal<*>)?.value)

// We expect that lhs comes first in the EOG and then the rhs.
assertContains(twoBoolOpCondition.lhs.nextEOG, twoBoolOpCondition.rhs)

val threeBoolOpCondition =
result.functions["threeBoolOp"]?.ifs?.singleOrNull()?.condition as? BinaryOperator
assertNotNull(threeBoolOpCondition)
assertEquals("and", threeBoolOpCondition.operatorCode)
assertEquals("a", (threeBoolOpCondition.lhs as? Reference)?.name?.localName)
val threeBoolOpConditionRhs = threeBoolOpCondition.rhs as? BinaryOperator
assertNotNull(threeBoolOpConditionRhs)
assertEquals("and", threeBoolOpConditionRhs.operatorCode)
assertEquals(true, (threeBoolOpConditionRhs.lhs as? Literal<*>)?.value)
assertEquals("b", (threeBoolOpConditionRhs.rhs as? Reference)?.name?.localName)

val threeBoolOpNoBoolCondition =
result.functions["threeBoolOpNoBool"]?.ifs?.singleOrNull()?.condition as? BinaryOperator
assertNotNull(threeBoolOpNoBoolCondition)
assertEquals("and", threeBoolOpNoBoolCondition.operatorCode)
assertEquals("a", (threeBoolOpNoBoolCondition.lhs as? Reference)?.name?.localName)
val threeBoolOpNoBoolConditionRhs = threeBoolOpNoBoolCondition.rhs as? BinaryOperator
assertNotNull(threeBoolOpNoBoolConditionRhs)
assertEquals("and", threeBoolOpNoBoolConditionRhs.operatorCode)
assertEquals(true, (threeBoolOpNoBoolConditionRhs.lhs as? Literal<*>)?.value)
assertEquals("foo", (threeBoolOpNoBoolConditionRhs.rhs as? Literal<*>)?.value)

// We expect that lhs comes first in the EOG and then the lhs of the rhs and last the rhs of
// the rhs.
assertContains(threeBoolOpNoBoolCondition.lhs.nextEOG, threeBoolOpNoBoolConditionRhs.lhs)
assertContains(threeBoolOpNoBoolConditionRhs.lhs.nextEOG, threeBoolOpNoBoolConditionRhs.rhs)

val nestedBoolOpDifferentOp =
result.functions["nestedBoolOpDifferentOp"]?.ifs?.singleOrNull()?.condition
as? BinaryOperator
assertNotNull(nestedBoolOpDifferentOp)
assertEquals("or", nestedBoolOpDifferentOp.operatorCode)
assertEquals("b", (nestedBoolOpDifferentOp.rhs as? Reference)?.name?.localName)
val nestedBoolOpDifferentOpLhs = nestedBoolOpDifferentOp.lhs as? BinaryOperator
assertNotNull(nestedBoolOpDifferentOpLhs)
assertEquals("and", nestedBoolOpDifferentOpLhs.operatorCode)
assertEquals(true, (nestedBoolOpDifferentOpLhs.rhs as? Literal<*>)?.value)
assertEquals("a", (nestedBoolOpDifferentOpLhs.lhs as? Reference)?.name?.localName)

// We expect that lhs of the "and" comes first in the EOG and then the rhs of the "and",
// then we evaluate the whole "and" and last the rhs of the "or".
assertContains(nestedBoolOpDifferentOpLhs.lhs.nextEOG, nestedBoolOpDifferentOpLhs.rhs)
assertContains(nestedBoolOpDifferentOpLhs.rhs.nextEOG, nestedBoolOpDifferentOpLhs)
assertContains(nestedBoolOpDifferentOpLhs.nextEOG, nestedBoolOpDifferentOp.rhs)

val nestedBoolOpDifferentOp2 =
result.functions["nestedBoolOpDifferentOp2"]?.ifs?.singleOrNull()?.condition
as? BinaryOperator
assertNotNull(nestedBoolOpDifferentOp2)
assertEquals("or", nestedBoolOpDifferentOp2.operatorCode)
assertEquals("a", (nestedBoolOpDifferentOp2.lhs as? Reference)?.name?.localName)
val nestedBoolOpDifferentOp2Rhs = nestedBoolOpDifferentOp2.rhs as? BinaryOperator
assertNotNull(nestedBoolOpDifferentOp2Rhs)
assertEquals("and", nestedBoolOpDifferentOp2Rhs.operatorCode)
assertEquals(true, (nestedBoolOpDifferentOp2Rhs.lhs as? Literal<*>)?.value)
assertEquals("b", (nestedBoolOpDifferentOp2Rhs.rhs as? Reference)?.name?.localName)

// We expect that lhs comes first in the EOG and then the lhs of the rhs and last the rhs of
// the rhs.
assertContains(nestedBoolOpDifferentOp2.lhs.nextEOG, nestedBoolOpDifferentOp2Rhs.lhs)
assertContains(nestedBoolOpDifferentOp2Rhs.lhs.nextEOG, nestedBoolOpDifferentOp2Rhs.rhs)
}
}
24 changes: 24 additions & 0 deletions cpg-language-python/src/test/resources/python/boolop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
def twoBoolOp(a):
if a and True:
print(a)
return a

def threeBoolOp(a, b):
if a and True and b:
print(a)
return a

def nestedBoolOpDifferentOp(a, b):
if a and True or b:
print(a)
return a

def nestedBoolOpDifferentOp2(a, b):
if a or True and b:
print(a)
return a

def threeBoolOpNoBool(a):
if a and True and "foo":
print(a)
return a

0 comments on commit 7a45945

Please sign in to comment.