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

Supporting posonlyargs in Python #1638

Merged
merged 5 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -31,6 +31,7 @@ import de.fraunhofer.aisec.cpg.frontends.Language
import de.fraunhofer.aisec.cpg.frontends.of
import de.fraunhofer.aisec.cpg.graph.HasOverloadedOperation
import de.fraunhofer.aisec.cpg.graph.autoType
import de.fraunhofer.aisec.cpg.graph.declarations.ParameterDeclaration
import de.fraunhofer.aisec.cpg.graph.scopes.Symbol
import de.fraunhofer.aisec.cpg.graph.statements.expressions.BinaryOperator
import de.fraunhofer.aisec.cpg.graph.statements.expressions.UnaryOperator
Expand Down Expand Up @@ -159,4 +160,20 @@ class PythonLanguage :
// The rest behaves like other languages
return super.propagateTypeOfBinaryOperation(operation)
}

companion object {
/**
* This is a "modifier" to differentiate parameters in functions that are "positional" only.
* This information will be stored in [ParameterDeclaration.modifiers] so that we can use is
* later in call resolving.
*/
const val MODIFIER_POSITIONAL_ONLY_ARGUMENT = "posonlyarg"
oxisto marked this conversation as resolved.
Show resolved Hide resolved

/**
* This is a "modifier" to differentiate parameters in functions that are "keyword" only.
* This information will be stored in [ParameterDeclaration.modifiers] so that we can use is
* later in call resolving.
*/
const val MODIFIER_KEYWORD_ONLY_ARGUMENT = "kwonlyarg"
}
}
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.python.PythonLanguage.Companion.MODIFIER_POSITIONAL_ONLY_ARGUMENT
import de.fraunhofer.aisec.cpg.graph.*
import de.fraunhofer.aisec.cpg.graph.Annotation
import de.fraunhofer.aisec.cpg.graph.declarations.*
Expand All @@ -38,6 +39,7 @@ import de.fraunhofer.aisec.cpg.graph.types.FunctionType

class StatementHandler(frontend: PythonLanguageFrontend) :
PythonHandler<Statement, Python.ASTBASEstmt>(::ProblemExpression, frontend) {

override fun handleNode(node: Python.ASTBASEstmt): Statement {
return when (node) {
is Python.ASTClassDef -> handleClassDef(node)
Expand Down Expand Up @@ -237,7 +239,11 @@ class StatementHandler(frontend: PythonLanguageFrontend) :

frontend.scopeManager.enterScope(cls)

stmt.keywords.map { TODO() }
oxisto marked this conversation as resolved.
Show resolved Hide resolved
stmt.keywords.forEach {
frontend.currentTU?.addDeclaration(
newProblemDeclaration("could not parse keyword $it in class")
)
}

for (s in stmt.body) {
when (s) {
Expand Down Expand Up @@ -382,20 +388,16 @@ class StatementHandler(frontend: PythonLanguageFrontend) :
result: FunctionDeclaration,
recordDeclaration: RecordDeclaration?
) {
// Handle arguments
if (args.posonlyargs.isNotEmpty()) {
val problem =
newProblemDeclaration(
"`posonlyargs` are not yet supported",
problemType = ProblemNode.ProblemType.TRANSLATION,
rawNode = args
)
frontend.scopeManager.addDeclaration(problem)
}
// We can merge posonlyargs and args because both are positional arguments. We do not
// enforce that posonlyargs can ONLY be used in a positional style, whereas args can be used
// both in positional as well as keyword style.
var positionalArguments = args.posonlyargs + args.args

// Handle arguments
if (recordDeclaration != null) {
// first argument is the `receiver`
if (args.args.isEmpty()) {
val recvPythonNode = positionalArguments.firstOrNull()
if (recvPythonNode == null) {
val problem =
newProblemDeclaration(
"Expected a receiver",
Expand All @@ -404,7 +406,6 @@ class StatementHandler(frontend: PythonLanguageFrontend) :
)
frontend.scopeManager.addDeclaration(problem)
} else {
val recvPythonNode = args.args.first()
val tpe = recordDeclaration.toType()
val recvNode =
newVariableDeclaration(
Expand All @@ -424,12 +425,12 @@ class StatementHandler(frontend: PythonLanguageFrontend) :

if (recordDeclaration != null) {
// first argument is the receiver
for (arg in args.args.subList(1, args.args.size)) {
handleArgument(arg)
for (arg in positionalArguments.subList(1, positionalArguments.size)) {
handleArgument(arg, arg in args.posonlyargs)
}
} else {
for (arg in args.args) {
handleArgument(arg)
for (arg in positionalArguments) {
handleArgument(arg, arg in args.posonlyargs)
}
}

Expand Down Expand Up @@ -554,9 +555,12 @@ class StatementHandler(frontend: PythonLanguageFrontend) :
return result
}

internal fun handleArgument(node: Python.ASTarg) {
internal fun handleArgument(node: Python.ASTarg, isPosOnly: Boolean = false) {
val type = frontend.typeOf(node.annotation)
val arg = newParameterDeclaration(name = node.arg, type = type, rawNode = node)
if (isPosOnly) {
arg.modifiers += MODIFIER_POSITIONAL_ONLY_ARGUMENT
}

frontend.scopeManager.addDeclaration(arg)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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.edge.Properties
import de.fraunhofer.aisec.cpg.test.analyze
import java.nio.file.Path
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertNotNull

class StatementHandlerTest {

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

var myClass = result.records["MyClass"]
assertNotNull(myClass)

var func = result.functions["pos_only_and_args"]
assertNotNull(func)

val list = mapOf("a" to true, "b" to true, "c" to false)
list.keys.forEachIndexed { idx, name ->
var param = func.parameterEdges.firstOrNull { it.end.name.localName == name }
assertNotNull(param, "$name should not be empty")
if (list[name] == true) {
assertContains(
param.end.modifiers,
PythonLanguage.MODIFIER_POSITIONAL_ONLY_ARGUMENT
)
}
assertEquals(idx, param.getProperty(Properties.INDEX))
}
oxisto marked this conversation as resolved.
Show resolved Hide resolved
}
}
7 changes: 7 additions & 0 deletions cpg-language-python/src/test/resources/python/arguments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
def pos_only_and_args(a, b, /, c):
pass


class MyClass:
def my_method(self, d, e):
pass
Loading