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

Create a LocalScope for CollectionComprehensions and generate a Declaration #2019

Draft
wants to merge 54 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
3387229
Create scope for collection comprehensions
KuechA Jan 31, 2025
209c218
generate local variables in python add declarations pass for comprehe…
KuechA Jan 31, 2025
1ff35b8
Extend test to check fixes
KuechA Jan 31, 2025
501d597
Remove useless flag
KuechA Jan 31, 2025
cb0b660
Add test case
KuechA Jan 31, 2025
8b8fccd
Adapt test
KuechA Jan 31, 2025
c7893a6
Test resolution of param
KuechA Jan 31, 2025
110c288
Reduce c&p code
KuechA Jan 31, 2025
8011422
add documentation
KuechA Jan 31, 2025
9489ecf
Move applyWithScope to Node
KuechA Jan 31, 2025
091041a
python style
KuechA Jan 31, 2025
d75f3f3
Merge branch 'main' into ak/python-comprehension-scope
KuechA Jan 31, 2025
4a53e41
more tests: binding
maximiliankaul Feb 5, 2025
2c17051
Merge branch 'main' into ak/python-comprehension-scope
KuechA Feb 5, 2025
b6e7546
Enter scope only for python3
KuechA Feb 5, 2025
485fc73
numbers to text
KuechA Feb 5, 2025
a0ff2fb
Test for python2 behavior
KuechA Feb 5, 2025
5dabf21
Document test
KuechA Feb 5, 2025
2408e3f
Leave and re-enter scopes of comprehensions for assign expressions in…
KuechA Feb 5, 2025
e24819f
Add test for nested comprehensions with := assignment
KuechA Feb 5, 2025
b6e386f
Higher code coverage in test
KuechA Feb 5, 2025
e98d6b8
Test version info
KuechA Feb 5, 2025
3529bbb
Extract collection comprehension tests to own file
KuechA Feb 6, 2025
deab3da
Rename
KuechA Feb 6, 2025
43c1400
Document test
KuechA Feb 6, 2025
7cca036
Document more asserts and rename wrong variable names
KuechA Feb 6, 2025
97975a3
Document CollectionComprehensionPython3Test.testDictComprehensions
KuechA Feb 6, 2025
96ddeab
Document CollectionComprehensionPython3Test.testSetComprehensions
KuechA Feb 6, 2025
166a638
Document CollectionComprehensionPython3Test.testListComprehensions
KuechA Feb 6, 2025
74f0aa9
Fix error in CollectionComprehensionPython3Test.testListComprehensions
KuechA Feb 6, 2025
86b36cc
Fix style of CollectionComprehensionPython2Test.testComprehensionExpr…
KuechA Feb 6, 2025
0d383fc
Fix tests
KuechA Feb 6, 2025
3b37c2a
Test improvements
KuechA Feb 6, 2025
92ddb45
Remove python2 test
KuechA Feb 6, 2025
e55f209
Remove version-based logic which is no longer required
KuechA Feb 6, 2025
ddc7ed2
Merge branch 'main' into ak/python-comprehension-scope
KuechA Feb 6, 2025
cdb92e8
no more typos
KuechA Feb 6, 2025
4908a5e
object type to primitive type for consistency with other frontends an…
KuechA Feb 6, 2025
2482db0
versionInfo as extension variable
oxisto Feb 6, 2025
10a1603
Merge branch 'main' into ak/python-comprehension-scope
KuechA Feb 7, 2025
b4eb266
Revert changes to VersionInfo
KuechA Feb 7, 2025
7de93e2
Merge branch 'main' into ak/python-comprehension-scope
KuechA Feb 7, 2025
31f7377
Add test for write to list index
KuechA Feb 7, 2025
68dc283
Add another test
KuechA Feb 7, 2025
5c9a6af
Add another test: reversed order in tuple
KuechA Feb 7, 2025
4863da8
fix documentation
KuechA Feb 7, 2025
f521517
Another test
KuechA Feb 7, 2025
cb272ae
Merge branch 'main' into ak/python-comprehension-scope
KuechA Feb 7, 2025
edc3170
Another test for more code coverage
KuechA Feb 7, 2025
8707109
Test applyWithScope without ctx
KuechA Feb 7, 2025
af9184e
Fix test and typos
KuechA Feb 7, 2025
1ebbc6f
Another assertion
KuechA Feb 7, 2025
20351d3
Add more strict assumptions again
KuechA Feb 7, 2025
d4bcd26
Add another test for fields
KuechA Feb 7, 2025
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 @@ -240,6 +240,7 @@ class ScopeManager : ScopeProvider {
is TryStatement,
is IfStatement,
is CatchClause,
is CollectionComprehension,
is Block -> LocalScope(nodeToScope)
is FunctionDeclaration -> FunctionScope(nodeToScope)
is RecordDeclaration -> RecordScope(nodeToScope)
Expand Down
12 changes: 12 additions & 0 deletions cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Node.kt
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,15 @@ abstract class Node :
const val EMPTY_NAME = ""
}
}

/**
* Works similar to [apply] but before executing [block], it enters the scope for this object and
* afterward leaves the scope again.
*/
inline fun <reified T : Node> T.applyWithScope(block: T.() -> Unit): T {
return this.apply {
ctx?.scopeManager?.enterScope(this)
block()
ctx?.scopeManager?.leaveScope(this)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ fun LanguageProvider.objectType(
): Type {
// First, we check, whether this is a built-in type, to avoid necessary allocations of simple
// types
val builtIn = language?.getSimpleTypeOf(name.toString())
val builtIn = language.getSimpleTypeOf(name.toString())
if (builtIn != null) {
return builtIn
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class ExpressionHandler(frontend: PythonLanguageFrontend) :
* [CollectionComprehension].
*/
private fun handleGeneratorExp(node: Python.AST.GeneratorExp): CollectionComprehension {
return newCollectionComprehension(rawNode = node).apply {
return newCollectionComprehension(rawNode = node).applyWithScope {
statement = handle(node.elt)
comprehensionExpressions += node.generators.map { handleComprehension(it, node) }
type = objectType("Generator")
Expand All @@ -117,10 +117,10 @@ class ExpressionHandler(frontend: PythonLanguageFrontend) :
* into a [CollectionComprehension].
*/
private fun handleListComprehension(node: Python.AST.ListComp): CollectionComprehension {
return newCollectionComprehension(rawNode = node).apply {
return newCollectionComprehension(rawNode = node).applyWithScope {
statement = handle(node.elt)
comprehensionExpressions += node.generators.map { handleComprehension(it, node) }
type = objectType("list") // TODO: Replace this once we have dedicated types
type = objectType("list")
oxisto marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand All @@ -129,10 +129,10 @@ class ExpressionHandler(frontend: PythonLanguageFrontend) :
* a [CollectionComprehension].
*/
private fun handleSetComprehension(node: Python.AST.SetComp): CollectionComprehension {
return newCollectionComprehension(rawNode = node).apply {
return newCollectionComprehension(rawNode = node).applyWithScope {
oxisto marked this conversation as resolved.
Show resolved Hide resolved
this.statement = handle(node.elt)
this.comprehensionExpressions += node.generators.map { handleComprehension(it, node) }
this.type = objectType("set") // TODO: Replace this once we have dedicated types
this.type = objectType("set")
}
}

Expand All @@ -141,15 +141,15 @@ class ExpressionHandler(frontend: PythonLanguageFrontend) :
* into a [CollectionComprehension].
*/
private fun handleDictComprehension(node: Python.AST.DictComp): CollectionComprehension {
return newCollectionComprehension(rawNode = node).apply {
return newCollectionComprehension(rawNode = node).applyWithScope {
this.statement =
newKeyValueExpression(
key = handle(node.key),
value = handle(node.value),
rawNode = node,
)
this.comprehensionExpressions += node.generators.map { handleComprehension(it, node) }
this.type = objectType("dict") // TODO: Replace this once we have dedicated types
this.type = objectType("dict")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration
import de.fraunhofer.aisec.cpg.graph.scopes.RecordScope
import de.fraunhofer.aisec.cpg.graph.statements.ForEachStatement
import de.fraunhofer.aisec.cpg.graph.statements.expressions.AssignExpression
import de.fraunhofer.aisec.cpg.graph.statements.expressions.ComprehensionExpression
import de.fraunhofer.aisec.cpg.graph.statements.expressions.InitializerListExpression
import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberExpression
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference
Expand Down Expand Up @@ -67,15 +68,15 @@ class PythonAddDeclarationsPass(ctx: TranslationContext) : ComponentPass(ctx), L
}

/**
* This function checks for each [AssignExpression] whether there is already a matching variable
* or not. New variables can be one of:
* This function checks for each [AssignExpression], [ComprehensionExpression] and
* [ForEachStatement] whether there is already a matching variable or not. New variables can be
* one of:
* - [FieldDeclaration] if we are currently in a record
* - [VariableDeclaration] otherwise
*
* TODO: loops
*/
private fun handle(node: Node?) {
when (node) {
is ComprehensionExpression -> handleComprehensionExpression(node)
is AssignExpression -> handleAssignExpression(node)
is ForEachStatement -> handleForEach(node)
else -> {
Expand Down Expand Up @@ -194,6 +195,27 @@ class PythonAddDeclarationsPass(ctx: TranslationContext) : ComponentPass(ctx), L
this.base.name == scopeManager.currentMethod?.receiver?.name
}

/**
* Generates a new [VariableDeclaration] for [Reference] (and those included in a
* [InitializerListExpression]) in the [ComprehensionExpression.variable].
*/
private fun handleComprehensionExpression(comprehensionExpression: ComprehensionExpression) {
when (val variable = comprehensionExpression.variable) {
is Reference -> {
variable.access = AccessValues.WRITE
handleWriteToReference(variable)
}
is InitializerListExpression -> {
variable.initializers.forEach {
(it as? Reference)?.let { ref ->
ref.access = AccessValues.WRITE
handleWriteToReference(ref)
}
}
}
}
}

/**
* Generates a new [VariableDeclaration] if [target] is a [Reference] and there is no existing
* declaration yet.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,67 @@
package de.fraunhofer.aisec.cpg.frontends.python

import de.fraunhofer.aisec.cpg.graph.*
import de.fraunhofer.aisec.cpg.graph.statements.expressions.AssignExpression
import de.fraunhofer.aisec.cpg.graph.statements.expressions.BinaryOperator
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Block
import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression
import de.fraunhofer.aisec.cpg.graph.statements.expressions.CollectionComprehension
import de.fraunhofer.aisec.cpg.graph.statements.expressions.KeyValueExpression
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference
import de.fraunhofer.aisec.cpg.test.analyze
import de.fraunhofer.aisec.cpg.test.assertLiteralValue
import de.fraunhofer.aisec.cpg.test.assertLocalName
import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration
import de.fraunhofer.aisec.cpg.graph.declarations.ParameterDeclaration
import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration
import de.fraunhofer.aisec.cpg.graph.scopes.LocalScope
import de.fraunhofer.aisec.cpg.graph.statements.expressions.*
import de.fraunhofer.aisec.cpg.test.*
import java.nio.file.Path
import kotlin.test.*

class ExpressionHandlerTest {

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

val tupleComp = result.functions["tuple_comp"]
assertNotNull(tupleComp)

val body = tupleComp.body
assertIs<Block>(body)
val tupleAsVariableAssignment = body.statements[0]
oxisto marked this conversation as resolved.
Show resolved Hide resolved
assertIs<AssignExpression>(tupleAsVariableAssignment)
val tupleAsVariable = tupleAsVariableAssignment.rhs[0]
assertIs<CollectionComprehension>(tupleAsVariable)
val barCall = tupleAsVariable.statement
assertIs<CallExpression>(barCall)
assertLocalName("bar", barCall)
val argK = barCall.arguments[0]
assertIs<Reference>(argK)
assertLocalName("k", argK)
val argV = barCall.arguments[1]
assertIs<Reference>(argV)
assertLocalName("v", argV)
assertEquals(1, tupleAsVariable.comprehensionExpressions.size)
val initializerListExpression = tupleAsVariable.comprehensionExpressions[0].variable
assertIs<InitializerListExpression>(initializerListExpression)
val variableK = initializerListExpression.initializers[0]
assertIs<Reference>(variableK)
assertLocalName("k", variableK)
val variableV = initializerListExpression.initializers[1]
assertIs<Reference>(variableV)
assertLocalName("v", variableV)

// Check that the declarations exist for the variables k and v
val declK = variableK.refersTo
assertIs<VariableDeclaration>(declK)
assertIs<LocalScope>(declK.scope)
assertEquals(tupleAsVariable, declK.scope?.astNode)
assertRefersTo(argK, declK)
val declV = variableV.refersTo
assertIs<VariableDeclaration>(declV)
assertIs<LocalScope>(declV.scope)
assertEquals(tupleAsVariable, declV.scope?.astNode)
assertRefersTo(argV, declV)
}

@Test
fun testListComprehensions() {
val topLevel = Path.of("src", "test", "resources", "python")
Expand All @@ -48,23 +95,44 @@ class ExpressionHandlerTest {
it.registerLanguage<PythonLanguage>()
}
assertNotNull(result)
val listComp = result.functions["listComp"]
val listComp = result.functions["list_comp"]
assertNotNull(listComp)
val paramX = listComp.parameters[0]
assertIs<ParameterDeclaration>(paramX)
assertLocalName("x", paramX)

val body = listComp.body
assertIs<Block>(body)
val singleWithIfAssignment = body.statements[0]
assertIs<AssignExpression>(singleWithIfAssignment)
val singleWithIf = singleWithIfAssignment.rhs[0]
assertIs<CollectionComprehension>(singleWithIf)
assertIs<CallExpression>(singleWithIf.statement)
val fooCall = singleWithIf.statement
assertIs<CallExpression>(fooCall)
val usageI = fooCall.arguments[0]
assertIs<Reference>(usageI)
assertEquals(1, singleWithIf.comprehensionExpressions.size)
assertLocalName("i", singleWithIf.comprehensionExpressions[0].variable)
assertIs<Reference>(singleWithIf.comprehensionExpressions[0].iterable)
assertLocalName("x", singleWithIf.comprehensionExpressions[0].iterable)
val variableI = singleWithIf.comprehensionExpressions[0].variable
assertIs<Reference>(variableI)
assertLocalName("i", variableI)
val declI = variableI.refersTo
assertIs<VariableDeclaration>(declI)
assertEquals(singleWithIf, declI.scope?.astNode)
val iterableX = singleWithIf.comprehensionExpressions[0].iterable
assertIs<Reference>(iterableX)
assertLocalName("x", iterableX)
assertRefersTo(iterableX, paramX)
val ifPredicate = singleWithIf.comprehensionExpressions[0].predicate
assertIs<BinaryOperator>(ifPredicate)
assertEquals("==", ifPredicate.operatorCode)
assertRefersTo(usageI, declI)

val fooIOutside = body.statements[4]
assertIs<CallExpression>(fooIOutside)
val outsideI = fooIOutside.arguments[0]
assertIs<Reference>(outsideI)
assertLocalName("i", outsideI)
assertNotRefersTo(outsideI, declI)

val singleWithoutIfAssignment = body.statements[1]
assertIs<AssignExpression>(singleWithoutIfAssignment)
Expand Down Expand Up @@ -107,7 +175,7 @@ class ExpressionHandlerTest {
it.registerLanguage<PythonLanguage>()
}
assertNotNull(result)
val listComp = result.functions["setComp"]
val listComp = result.functions["set_comp"]
assertNotNull(listComp)

val body = listComp.body as? Block
Expand Down Expand Up @@ -165,7 +233,7 @@ class ExpressionHandlerTest {
it.registerLanguage<PythonLanguage>()
}
assertNotNull(result)
val listComp = result.functions["dictComp"]
val listComp = result.functions["dict_comp"]
assertNotNull(listComp)

val body = listComp.body as? Block
Expand Down Expand Up @@ -269,6 +337,86 @@ class ExpressionHandlerTest {
assertNull(singleWithoutIf.comprehensionExpressions[0].predicate)
}

@Test
/**
* This test ensures that variables in a comprehension do not bind to the outer scope. See
* [testCompBindingAssignExpr] for exceptions.
*/
fun testCompBinding() {
val topLevel = Path.of("src", "test", "resources", "python")
val result =
analyze(listOf(topLevel.resolve("comprehension.py").toFile()), topLevel, true) {
it.registerLanguage<PythonLanguage>()
}
assertNotNull(result)
val compBindingFunc = result.functions["comp_binding"]
assertIs<FunctionDeclaration>(compBindingFunc)

val xDecl = compBindingFunc.variables.firstOrNull()
assertIs<VariableDeclaration>(xDecl)

assertEquals(
5,
compBindingFunc.variables.size,
"Expected 5 variables. One for the \"outside\" x and one for each of the 4 comprehensions.",
)

assertEquals(
2,
xDecl.usages.size,
"Expected two usages: one for the initial assignment and one for the usage in \"print(x)\".",
)

val comprehensions =
compBindingFunc.body.statements.filterIsInstance<CollectionComprehension>()
assertEquals(4, comprehensions.size, "Expected to find 4 comprehensions.")

comprehensions.forEach { it.refs.forEach { ref -> assertNotRefersTo(ref, xDecl) } }
}

@Test
/**
* This test ensures that variables in a comprehension do not bind to the outer scope if they
* are used in an `AssignExpr`. See https://peps.python.org/pep-0572/#scope-of-the-target
*/
fun testCompBindingAssignExpr() {
val topLevel = Path.of("src", "test", "resources", "python")
val result =
analyze(listOf(topLevel.resolve("comprehension.py").toFile()), topLevel, true) {
it.registerLanguage<PythonLanguage>()
}
assertNotNull(result)

val compBindingAssignFunc = result.functions["comp_binding_assign_expr"]
assertIs<FunctionDeclaration>(compBindingAssignFunc)

val xDecl = compBindingAssignFunc.variables.firstOrNull()
assertIs<VariableDeclaration>(xDecl)

assertEquals(
2,
compBindingAssignFunc.variables.size,
"Expected 2 variables. One for the \"outside\" x and one for the \"temp\" inside the comprehension.",
)

assertEquals(
3,
xDecl.usages.size,
"Expected 3 usages: one for the initial assignment, one for the comprehension and one for the usage in \"print(x)\".",
)

val comprehension =
compBindingAssignFunc.body.statements.singleOrNull { it is CollectionComprehension }
assertNotNull(comprehension)
val xRef = comprehension.refs("x").singleOrNull()
assertNotNull(xRef)
assertRefersTo(xRef, xDecl)

// TODO: test for other types of comprehensions
// TODO: test nested comprehensions -> AssignExpression binds to
// containing scope
}

@Test
fun testBoolOps() {
val topLevel = Path.of("src", "test", "resources", "python")
Expand Down
Loading
Loading