Skip to content

Commit

Permalink
Preparing to use CallResolutionResult in member call resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
oxisto committed Jul 26, 2024
1 parent 49d44ca commit c3db66d
Show file tree
Hide file tree
Showing 16 changed files with 306 additions and 589 deletions.
164 changes: 36 additions & 128 deletions cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import de.fraunhofer.aisec.cpg.graph.types.FunctionPointerType
import de.fraunhofer.aisec.cpg.graph.types.IncompleteType
import de.fraunhofer.aisec.cpg.graph.types.Type
import de.fraunhofer.aisec.cpg.helpers.Util
import de.fraunhofer.aisec.cpg.passes.SymbolResolver
import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation
import java.util.*
import java.util.function.Predicate
Expand Down Expand Up @@ -596,8 +597,6 @@ class ScopeManager : ScopeProvider {
*
* @param ref
* @return
*
* TODO: We should merge this function with [.resolveFunction]
*/
fun resolveReference(ref: Reference): ValueDeclaration? {
val startScope = ref.scope
Expand All @@ -614,7 +613,10 @@ class ScopeManager : ScopeProvider {
return pair.second
}

val (scope, name) = extractScope(ref, startScope)
var (scope, name) = extractScope(ref, startScope)
if (scope == null) {
scope = startScope
}

// Try to resolve value declarations according to our criteria
val decl =
Expand Down Expand Up @@ -657,111 +659,8 @@ class ScopeManager : ScopeProvider {
}

/**
* Tries to resolve a function in a call expression.
*
* @param call the call expression
* @return a list of possible functions
*/
@JvmOverloads
fun resolveFunctionLegacy(
call: CallExpression,
startScope: Scope? = currentScope
): List<FunctionDeclaration> {
val (scope, name) = extractScope(call, startScope)

val func =
resolve<FunctionDeclaration>(scope) {
it.name.lastPartsMatch(name) &&
it.matchesSignature(call.signature) != IncompatibleSignature
}

return func
}

/**
* This function tries to resolve a [CallExpression] into its matching [FunctionDeclaration] (or
* multiple functions, if applicable). The result is returned in the form of a
* [CallResolutionResult] which holds detail information about intermediate results as well as
* the kind of success the resolution had.
*
* Note: The [CallExpression.callee] needs to be resolved first, otherwise the call resolution
* fails.
*/
fun resolveCall(call: CallExpression, startScope: Scope? = currentScope): CallResolutionResult {
val result =
CallResolutionResult(
call,
setOf(),
setOf(),
mapOf(),
setOf(),
CallResolutionResult.SuccessKind.UNRESOLVED,
startScope,
)
val language = call.language

if (language == null) {
result.success = CallResolutionResult.SuccessKind.PROBLEMATIC
return result
}

// We can only resolve non-dynamic function calls here that have a reference node to our
// function
val callee = call.callee as? Reference ?: return result

val (scope, _) = extractScope(callee, startScope)
result.actualStartScope = scope

// Retrieve a list of possible functions with a matching name
result.candidateFunctions =
callee.candidates.filterIsInstance<FunctionDeclaration>().toSet()

if (call.language !is HasFunctionOverloading) {
// If the function does not allow function overloading, and we have multiple candidate
// symbols, the
// result is "problematic"
if (result.candidateFunctions.size > 1) {
result.success = CallResolutionResult.SuccessKind.PROBLEMATIC
}
}

// Filter functions that match the signature of our call, either directly or with casts;
// those functions are "viable". Take default arguments into account if the language has
// them.
result.signatureResults =
result.candidateFunctions
.map {
Pair(
it,
it.matchesSignature(
call.signature,
call.language is HasDefaultArguments,
call
)
)
}
.filter { it.second is SignatureMatches }
.associate { it }
result.viableFunctions = result.signatureResults.keys

// If we have a "problematic" result, we can stop here. In this case we cannot really
// determine anything more.
if (result.success == CallResolutionResult.SuccessKind.PROBLEMATIC) {
result.bestViable = result.viableFunctions
return result
}

// Otherwise, give the language a chance to narrow down the result (ideally to one) and set
// the success kind.
val pair = language.bestViableResolution(result)
result.bestViable = pair.first
result.success = pair.second

return result
}

/**
* This function extracts a scope for the [Name] in node, e.g. if the name is fully qualified.
* This function extracts a scope for the [Name], e.g. if the name is fully qualified. `null` is
* returned, if no scope can be extracted.
*
* The pair returns the extracted scope and a name that is adjusted by possible import aliases.
* The extracted scope is "responsible" for the name (e.g. declares the parent namespace) and
Expand All @@ -776,7 +675,7 @@ class ScopeManager : ScopeProvider {
* @param scope the current scope relevant for the name resolution, e.g. parent of node
* @return a pair with the scope of node.name and the alias-adjusted name
*/
fun extractScope(node: Node, scope: Scope? = currentScope): Pair<Scope?, Name> {
fun extractScope(node: HasNameAndLocation, scope: Scope? = currentScope): Pair<Scope?, Name> {
return extractScope(node.name, node.location, scope)
}

Expand All @@ -803,7 +702,7 @@ class ScopeManager : ScopeProvider {
scope: Scope? = currentScope,
): Pair<Scope?, Name> {
var n = name
var s = scope
var s: Scope? = null

// First, we need to check, whether we have some kind of scoping.
if (n.isQualified()) {
Expand Down Expand Up @@ -910,12 +809,6 @@ class ScopeManager : ScopeProvider {
return ret
}

fun resolveFunctionStopScopeTraversalOnDefinition(
call: CallExpression
): List<FunctionDeclaration> {
return resolve(currentScope, true) { f -> f.name.lastPartsMatch(call.name) }
}

/**
* Traverses the scope upwards and looks for declarations of type [T] which matches the
* condition [predicate].
Expand All @@ -928,15 +821,15 @@ class ScopeManager : ScopeProvider {
* @param predicate predicate the element must match to
* @param <T>
*/
inline fun <reified T : Declaration> resolve(
internal inline fun <reified T : Declaration> resolve(
searchScope: Scope?,
stopIfFound: Boolean = false,
noinline predicate: (T) -> Boolean
): List<T> {
return resolve(T::class.java, searchScope, stopIfFound, predicate)
}

fun <T : Declaration> resolve(
internal fun <T : Declaration> resolve(
klass: Class<T>,
searchScope: Scope?,
stopIfFound: Boolean = false,
Expand Down Expand Up @@ -1084,9 +977,21 @@ class ScopeManager : ScopeProvider {
predicate: ((Declaration) -> Boolean)? = null,
): List<Declaration> {
val (scope, n) = extractScope(name, location, startScope)

// We need to differentiate between a qualified and unqualified lookup. We have a qualified
// lookup, if the scope is not null. In this case we need to stay within the specified scope
val list =
scope?.lookupSymbol(n.localName, predicate = predicate)?.toMutableList()
?: mutableListOf()
// TODO(oxisto): extractScope does not return null in all cases, so we need to make sure
// that the returned scope is NOT our startscope
if (scope != null && scope is NameScope && scope != startScope) {
scope.lookupSymbol(n.localName, thisScopeOnly = true, predicate = predicate)
}
// Otherwise, we can look up the symbol alone (without any FQN) starting from the
// startScope
else {
startScope?.lookupSymbol(n.localName, predicate = predicate)
}
?.toMutableList() ?: return listOf()

// If we have both the definition and the declaration of a function declaration in our list,
// we chose only the definition
Expand Down Expand Up @@ -1132,8 +1037,8 @@ data class SignatureMatches(override val casts: List<CastResult>) : SignatureRes

fun FunctionDeclaration.matchesSignature(
signature: List<Type>,
arguments: List<Expression>? = null,
useDefaultArguments: Boolean = false,
call: CallExpression? = null,
): SignatureResult {
val casts = mutableListOf<CastResult>()

Expand All @@ -1155,7 +1060,7 @@ fun FunctionDeclaration.matchesSignature(
// Check, if we can cast the arg into our target type; and if, yes, what is
// the "distance" to the base type. We need this to narrow down the type during
// resolving
val match = type.tryCast(param.type, call?.arguments?.getOrNull(i), param)
val match = type.tryCast(param.type, arguments?.getOrNull(i), param)
if (match == CastNotPossible) {
return IncompatibleSignature
}
Expand Down Expand Up @@ -1198,13 +1103,16 @@ fun FunctionDeclaration.matchesSignature(
}

/**
* This is the result of [ScopeManager.resolveCall]. It holds all necessary intermediate results
* (such as [candidateFunctions], [viableFunctions]) as well as the final result (see [bestViable])
* of the call resolution.
* This is the result of [SymbolResolver.resolveWithArguments]. It holds all necessary intermediate
* results (such as [candidateFunctions], [viableFunctions]) as well as the final result (see
* [bestViable]) of the call resolution.
*/
data class CallResolutionResult(
/** The original call expression. */
val call: CallExpression,
/** The original expression that triggered the resolution. Most likely a [CallExpression]. */
val source: Expression,

/** The arguments that were supplied to the expression. */
val arguments: List<Expression>,

/**
* A set of candidate symbols we discovered based on the [CallExpression.callee] (using
Expand Down Expand Up @@ -1235,7 +1143,7 @@ data class CallResolutionResult(
/**
* The actual start scope of the resolution, after [ScopeManager.extractScope] is called on the
* callee. This can differ from the original start scope parameter handed to
* [ScopeManager.resolveCall] if the callee contains an FQN.
* [SymbolResolver.resolveWithArguments] if the callee contains an FQN.
*/
var actualStartScope: Scope?
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ import de.fraunhofer.aisec.cpg.graph.Name
import de.fraunhofer.aisec.cpg.graph.Node
import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration
import de.fraunhofer.aisec.cpg.graph.statements.expressions.BinaryOperator
import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression
import de.fraunhofer.aisec.cpg.graph.types.*
import de.fraunhofer.aisec.cpg.graph.unknownType
import de.fraunhofer.aisec.cpg.passes.SymbolResolver
import java.io.File
import kotlin.reflect.KClass
import kotlin.reflect.full.primaryConstructor
Expand Down Expand Up @@ -278,9 +280,9 @@ abstract class Language<T : LanguageFrontend<*, *>> : Node() {

/**
* This functions gives the language a chance to refine the results of a
* [ScopeManager.resolveCall] by choosing the best viable function(s) out of the set of viable
* functions. It can also influence the [CallResolutionResult.SuccessKind] of the resolution,
* e.g., if the result is ambiguous.
* [SymbolResolver.resolveWithArguments] by choosing the best viable function(s) out of the set
* of viable functions. It can also influence the [CallResolutionResult.SuccessKind] of the
* resolution, e.g., if the result is ambiguous.
*
* The default implementation will follow the following heuristic:
* - If the list of [CallResolutionResult.viableFunctions] is empty, we can directly return.
Expand Down Expand Up @@ -318,22 +320,23 @@ abstract class Language<T : LanguageFrontend<*, *>> : Node() {
// We need to check, whether this language has special handling of templates. In this
// case, we need to check, whether a template matches directly after we have no direct
// matches
if (this is HasTemplates) {
result.call.templateParameterEdges = mutableListOf()
val source = result.source
if (this is HasTemplates && source is CallExpression) {
source.templateParameterEdges = mutableListOf()
val (ok, candidates) =
this.handleTemplateFunctionCalls(
null,
result.call,
source,
false,
result.call.ctx!!,
source.ctx!!,
null,
needsExactMatch = true
)
if (ok) {
return Pair(candidates.toSet(), CallResolutionResult.SuccessKind.SUCCESSFUL)
}

result.call.templateParameterEdges = null
source.templateParameterEdges = null
}

// If the list of viable functions is still empty at this point, the call is unresolved
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import de.fraunhofer.aisec.cpg.graph.declarations.RecordDeclaration
import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration
import de.fraunhofer.aisec.cpg.graph.scopes.*
import de.fraunhofer.aisec.cpg.graph.statements.expressions.*
import de.fraunhofer.aisec.cpg.graph.types.Type
import de.fraunhofer.aisec.cpg.passes.*
import kotlin.reflect.KClass

Expand Down Expand Up @@ -85,29 +84,6 @@ interface HasTemplates : HasGenerics {
*/
interface HasDefaultArguments : LanguageTrait

/**
* A language trait that specifies that this language has a complex call resolution that we need to
* fine-tune in the language implementation.
*/
interface HasComplexCallResolution : LanguageTrait {
/**
* A function that can be used to fine-tune resolution of a method [call].
*
* Note: The function itself should NOT set the [CallExpression.invokes] but rather return a
* list of possible candidates.
*
* @return a list of [FunctionDeclaration] candidates.
*/
fun refineMethodCallResolution(
curClass: RecordDeclaration?,
possibleContainingTypes: Set<Type>,
call: CallExpression,
ctx: TranslationContext,
currentTU: TranslationUnitDeclaration,
callResolver: SymbolResolver
): List<FunctionDeclaration>
}

/** A language trait that specifies if the language supports function pointers. */
interface HasFunctionPointers : LanguageTrait

Expand All @@ -134,12 +110,12 @@ interface HasClasses : LanguageTrait
interface HasSuperClasses : LanguageTrait {
/**
* Determines which keyword is used to access functions, etc. of the superclass of an object
* (often "super).
* (often `super`).
*/
val superClassKeyword: String

fun handleSuperCall(
callee: MemberExpression,
fun handleSuperExpression(
me: MemberExpression,
curClass: RecordDeclaration,
scopeManager: ScopeManager,
): Boolean
Expand Down
Loading

0 comments on commit c3db66d

Please sign in to comment.