Skip to content
Draft
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 @@ -96,7 +96,6 @@ fun MetadataProvider.newHttpRequestHandler(
* @param handler The handler name for the endpoint.
* @param method The [HttpMethod] the created [HttpEndpoint] listens to.
* @param path The path of the created [HttpEndpoint].
* @param url The URL of the created [HttpEndpoint].
* @param authenticity The [Authenticity] method used by the [HttpEndpoint].
* @param authorization The [Authorization] mechanism defining access control.
* @param httpRequestContext The [HttpRequestContext] providing additional contextual metadata.
Expand All @@ -113,8 +112,8 @@ fun MetadataProvider.newHttpEndpoint(
userInput: MutableList<Node>,
handler: String?,
method: HttpMethod,
arguments: List<Node>,
path: String?,
url: String?,
authenticity: Authenticity?,
authorization: Authorization?,
httpRequestContext: HttpRequestContext?,
Expand All @@ -130,8 +129,8 @@ fun MetadataProvider.newHttpEndpoint(
userInput = userInput,
handler = handler,
method = method,
arguments = arguments,
path = path,
url = url,
authenticity = authenticity,
authorization = authorization,
httpRequestContext = httpRequestContext,
Expand Down Expand Up @@ -163,6 +162,7 @@ fun MetadataProvider.newHttpRequest(
concept: HttpClient,
arguments: List<Node>,
method: HttpMethod,
url: String,
call: String?,
reqBody: String?,
httpEndpoint: HttpEndpoint?,
Expand All @@ -173,6 +173,7 @@ fun MetadataProvider.newHttpRequest(
HttpRequest(
arguments = arguments,
method = method,
url = url,
call = call,
reqBody = reqBody,
httpEndpoint = httpEndpoint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ open class HttpEndpoint(
var rateLimiting: RateLimiting?,
var maxInputSize: Int?,
var userInput: MutableList<Node>,
var arguments: List<Node>,
var handler: String?,
var method: HttpMethod,
var path: String?,
var url: String?,
var authenticity: Authenticity?,
var authorization: Authorization?,
var httpRequestContext: HttpRequestContext?,
Expand All @@ -57,23 +57,22 @@ open class HttpEndpoint(
other.handler == this.handler &&
other.method == this.method &&
other.path == this.path &&
other.url == this.url &&
other.authenticity == this.authenticity &&
other.authorization == this.authorization &&
other.httpRequestContext == this.httpRequestContext &&
other.proxyTarget == this.proxyTarget &&
other.transportEncryption == this.transportEncryption &&
other.rateLimiting == this.rateLimiting &&
other.maxInputSize == this.maxInputSize &&
other.userInput == this.userInput
other.userInput == this.userInput &&
other.arguments == this.arguments

override fun hashCode(): Int =
Objects.hash(
super.hashCode(),
handler,
method,
path,
url,
authenticity,
authorization,
httpRequestContext,
Expand All @@ -82,6 +81,7 @@ open class HttpEndpoint(
rateLimiting,
maxInputSize,
userInput,
arguments,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ import kotlin.String
public open class HttpRequest(
val arguments: List<Node>,
val method: HttpMethod,
val url: String,
public val call: String?,
public val reqBody: String?,
public val httpEndpoint: HttpEndpoint?,
public var httpEndpoint: HttpEndpoint?,
linkedConcept: HttpClient,
underlyingNode: Node? = null,
) : HttpClientOperation(linkedConcept, underlyingNode) {
Expand All @@ -48,8 +49,9 @@ public open class HttpRequest(
other.reqBody == this.reqBody &&
other.httpEndpoint == this.httpEndpoint &&
other.arguments == this.arguments &&
other.method == this.method
other.method == this.method &&
other.url == this.url

override fun hashCode(): Int =
Objects.hash(super.hashCode(), call, reqBody, httpEndpoint, arguments, method)
Objects.hash(super.hashCode(), call, reqBody, httpEndpoint, arguments, method, url)
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ class DFGPass(ctx: TranslationContext) : ComponentPass(ctx) {
is Throw -> handleThrow(node)
// Declarations
is Field -> handleField(node)
is Function -> handleFunction(node, functionSummaries)
is Function -> handleFunction(node, functionSummaries, inferDfgForUnresolvedSymbols)
is Tuple -> handleTuple(node)
is Variable -> handleVariable(node)
}
Expand Down Expand Up @@ -249,7 +249,11 @@ class DFGPass(ctx: TranslationContext) : ComponentPass(ctx) {
* Adds the DFG edge for a [Function]. The data flows from the return statement(s) to the
* function.
*/
protected fun handleFunction(node: Function, functionSummaries: DFGFunctionSummaries) {
protected fun handleFunction(
node: Function,
functionSummaries: DFGFunctionSummaries,
inferDfgForUnresolvedSymbols: Boolean,
) {
if (node.isInferred) {
val summaryExists = with(functionSummaries) { addFlowsToFunctionDeclaration(node) }

Expand All @@ -262,6 +266,32 @@ class DFGPass(ctx: TranslationContext) : ComponentPass(ctx) {
if (node is Method) {
node.receiver?.let { node.prevDFGEdges += it }
}

// For inferred constructors, we also try to connect parameters to fields of the
// record.
if (inferDfgForUnresolvedSymbols && node is Constructor) {
val record = node.recordDeclaration ?: return
val fields = record.fields

for (param in node.parameters) {
val matchingField =
fields.firstOrNull { it.name.localName == param.name.localName }
// Only add this edge if the field has no read usages. If it does,
// handleMemberExpression will create
// DFG edges for those reads, and our 'extra' edge leads to an infinite
// loop.
val hasReadUsages =
matchingField?.usages?.any {
it is MemberAccess && it.access == AccessValues.READ
} ?: false
if (matchingField != null && !hasReadUsages) {
param.nextDFGEdges += matchingField
}
}
fields.forEach { field ->
field.nextDFGEdges.add(node) { granularity = partial(field.name.localName) }
}
}
}
} else {
node.allChildren<Return>().forEach { node.prevDFGEdges += it }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -707,10 +707,12 @@ open class SymbolResolver(ctx: TranslationContext) : EOGStarterPass(ctx) {
) != IncompatibleSignature
}

val argumentNames = constructExpression.argumentEdges.map { edge -> edge.name }

return constructorCandidate
?: recordDeclaration
.startInference(ctx)
?.createInferredConstructor(constructExpression.signature)
?.createInferredConstructor(constructExpression.signature, argumentNames)
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,14 @@ class Inference internal constructor(val start: Node, override val ctx: Translat
)
}

fun createInferredConstructor(signature: List<Type?>): Constructor {
fun createInferredConstructor(
signature: List<Type?>,
argumentNames: List<String?> = emptyList(),
): Constructor {
return inferInScopeOf(start) {
val record = start as? Record
val inferred = newConstructor(start.name.localName, record)
createInferredParameters(inferred, signature)
createInferredParameters(inferred, signature, argumentNames)

scopeManager.addDeclaration(inferred)
record?.constructors += inferred
Expand Down Expand Up @@ -208,16 +211,25 @@ class Inference internal constructor(val start: Node, override val ctx: Translat
method.receiver = receiver
}

/** This function creates a [Parameter] for each parameter in the [function]'s [signature]. */
private fun createInferredParameters(function: Function, signature: List<Type?>) {
/**
* This function creates a [Parameter] for each parameter in the [function]'s [signature]. If
* [argumentNames] are provided, those names will be used for the parameters
* * instead of generating names from the types.
*/
private fun createInferredParameters(
function: Function,
signature: List<Type?>,
argumentNames: List<String?> = emptyList(),
) {
// To save some unnecessary scopes, we only want to "enter" the function if it is necessary,
// e.g., if we need to create parameters
if (signature.isNotEmpty()) {
scopeManager.enterScope(function)

for (i in signature.indices) {
val targetType = signature[i] ?: UnknownType.getUnknownType(function.language)
val paramName = generateParamName(i, targetType)
// Use the argument name if available, otherwise generate a name from the type
val paramName = argumentNames.getOrNull(i) ?: generateParamName(i, targetType)
val param = newParameter(paramName, targetType, false)
param.argumentIndex = i

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,10 @@ class PythonAddDeclarationsPass(ctx: TranslationContext) : ComponentPass(ctx), L
}

// If this is a member expression, and we do not know the base's type, we cannot create a
// declaration
if (ref is MemberAccess && ref.base.type is UnknownType) {
// declaration. However, we need to exclude the receiver (e.g. "self") because its type is
// not yet resolved at this point, but we still need to
// create field declarations for assignments".
if (ref is MemberAccess && ref.base.type is UnknownType && !ref.refersToReceiver) {
return null
}

Expand Down Expand Up @@ -135,11 +137,14 @@ class PythonAddDeclarationsPass(ctx: TranslationContext) : ComponentPass(ctx), L
when {
// Check, whether we are referring to a "self.X", which would create a field
scopeManager.isInRecord && scopeManager.isInFunction && ref.refersToReceiver -> {
val recordScope = scopeManager.firstScopeIsInstanceOrNull<RecordScope>()
// If the field already exists in the record scope, do not create a duplicate
if (recordScope?.symbols?.containsKey(ref.name.localName) == true) {
return null
}
// We need to temporarily jump into the scope of the current record to
// add the field. These are instance attributes
scopeManager.withScope(scopeManager.firstScopeIsInstanceOrNull<RecordScope>()) {
newField(ref.name)
}
scopeManager.withScope(recordScope) { newField(ref.name) }
}
scopeManager.isInRecord && scopeManager.isInFunction && ref is MemberAccess -> {
// If this is any other member expression, we are usually not interested in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1022,4 +1022,50 @@ class DFGTest {
"Expected DFG path from 'baz' through the constructor to return f",
)
}

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

// Test case 1: A(b=b)
val fooFunc = result.functions["foo"]
assertNotNull(fooFunc)
val aRef = fooFunc.refs["a_var"]
assertNotNull(aRef)

val classA = result.records["A"]
assertNotNull(classA)

val fieldB = classA.fields["b"]
assertNotNull(fieldB)

val pathsToFieldB = aRef.followDFGEdgesUntilHit { it == fieldB }
assertTrue(
pathsToFieldB.fulfilled.isNotEmpty(),
"Expected DFG path from 'a_var' through constructor to field b",
)

// Test case 2: B(x=some_other_var)
val barFunc = result.functions["bar"]
assertNotNull(barFunc)

val someOtherVarRef = barFunc.refs["some_other_var"]
assertNotNull(someOtherVarRef)

val pathsToFieldX =
someOtherVarRef.followDFGEdgesUntilHit(collectFailedPaths = true) { it == fieldB }
assertTrue(
pathsToFieldX.fulfilled.isNotEmpty(),
"Expected DFG path from 'some_other_var' through constructor to field 'b'",
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ import de.fraunhofer.aisec.cpg.graph.declarations.Method
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Call
import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberAccess
import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberCall
import de.fraunhofer.aisec.cpg.graph.types.ObjectType
import de.fraunhofer.aisec.cpg.test.analyze
import de.fraunhofer.aisec.cpg.test.assertFullName
import de.fraunhofer.aisec.cpg.test.assertInvokes
import de.fraunhofer.aisec.cpg.test.assertNotRefersTo
import de.fraunhofer.aisec.cpg.test.assertRefersTo
import java.io.File
Expand Down Expand Up @@ -125,4 +127,48 @@ class SymbolResolverTest {
assertNotNull(doSomething, "Expected to find a single call to 'do_something'")
assertIs<MemberCall>(doSomething, "'do_something' should be a member call")
}

@Test
fun testFieldCallResolution() {
val topLevel = File("src/test/resources/python/field_call_resolution.py")
val result =
analyze(listOf(topLevel), topLevel.toPath(), true) {
it.registerLanguage<PythonLanguage>()
}
assertNotNull(result)

val clientClass = result.records["Client"]
assertNotNull(clientClass)

val serviceClass = result.records["Service"]
assertNotNull(serviceClass)

val clientField = serviceClass.fields["client"]
assertIs<Field>(clientField, "Expected a field 'client'")

val fieldType = clientField.type
assertIs<ObjectType>(fieldType, "Expected field 'client' to have an ObjectType")

val sendMethod = clientClass.methods["send"]
assertNotNull(sendMethod)

val sendCall = result.calls["send"]
assertNotNull(sendCall)
assertIs<MemberCall>(sendCall)
assertInvokes(sendCall, sendMethod)
}

@Test
fun testFieldCallResolution2() {
val topLevel = File("src/test/resources/python/field_call_resolution.py")
val result =
analyze(listOf(topLevel), topLevel.toPath(), true) {
it.registerLanguage<PythonLanguage>()
}
assertNotNull(result)

// TODO: Not sure if this fix in PythonAddDeclarationsPass is correct, as in some cases like
// the following one it won't work
/** def __init__(self, client): self.client = client or Client() */
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Client:
def send(self, data):
return data


class Service:
def __init__(self):
self.client = Client()

def do_work(self):
result = self.client.send("hello")
return result
Loading
Loading