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 22, 2024
1 parent d7abd3e commit 1cde93a
Show file tree
Hide file tree
Showing 16 changed files with 244 additions and 513 deletions.
108 changes: 56 additions & 52 deletions cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -596,8 +596,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 +612,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 @@ -656,28 +657,6 @@ class ScopeManager : ScopeProvider {
return decl
}

/**
* 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
Expand All @@ -686,17 +665,25 @@ class ScopeManager : ScopeProvider {
*
* Note: The [CallExpression.callee] needs to be resolved first, otherwise the call resolution
* fails.
*
* In order to migrate from our legacy resolution system, we already use this function for all
* resolving, but [legacyCandidates] can be used to specify the call's candidate functions based
* on the legacy system rather than the already resolved [CallExpression.callee].
*/
fun resolveCall(call: CallExpression, startScope: Scope? = currentScope): CallResolutionResult {
fun resolveCall(
call: CallExpression,
legacyCandidates: Set<FunctionDeclaration>? = null
): CallResolutionResult {
val result =
CallResolutionResult(
call,
call.arguments,
setOf(),
setOf(),
mapOf(),
setOf(),
CallResolutionResult.SuccessKind.UNRESOLVED,
startScope,
call.scope,
)
val language = call.language

Expand All @@ -709,17 +696,24 @@ class ScopeManager : ScopeProvider {
// function
val callee = call.callee as? Reference ?: return result

val (scope, _) = extractScope(callee, startScope)
result.actualStartScope = scope
// Set the start scope. This can either be the call's scope or a scope specified in an FQN
val (scope, _) = extractScope(callee, call.scope)
result.actualStartScope = scope ?: call.scope

// Retrieve a list of possible functions with a matching name
result.candidateFunctions =
callee.candidates.filterIsInstance<FunctionDeclaration>().toSet()
// If we have pre-resolved candidates from our legacy system, we need to take these
if (legacyCandidates != null) {
result.candidateFunctions = legacyCandidates
}
// Otherwise, retrieve a list of possible functions with a matching name from our
// callee, which is already resolved.
else {
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"
// symbols, the result is "problematic"
if (result.candidateFunctions.size > 1) {
result.success = CallResolutionResult.SuccessKind.PROBLEMATIC
}
Expand All @@ -735,8 +729,8 @@ class ScopeManager : ScopeProvider {
it,
it.matchesSignature(
call.signature,
call.arguments,
call.language is HasDefaultArguments,
call
)
)
}
Expand All @@ -761,7 +755,8 @@ class ScopeManager : ScopeProvider {
}

/**
* 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 +771,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 +798,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 +905,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 +917,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 +1073,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 +1133,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 +1156,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 @@ -1203,8 +1204,11 @@ fun FunctionDeclaration.matchesSignature(
* 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 call: 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
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ 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 java.io.File
Expand Down Expand Up @@ -318,8 +319,9 @@ 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 call = result.call
if (this is HasTemplates && call is CallExpression) {
call.templateParameterEdges = mutableListOf()
val (ok, candidates) =
this.handleTemplateFunctionCalls(
null,
Expand All @@ -333,7 +335,7 @@ abstract class Language<T : LanguageFrontend<*, *>> : Node() {
return Pair(candidates.toSet(), CallResolutionResult.SuccessKind.SUCCESSFUL)
}

result.call.templateParameterEdges = null
call.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
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,21 @@ abstract class Scope(
/**
* Looks up a list of [Declaration] nodes for the specified [symbol]. Optionally, [predicate]
* can be used for additional filtering.
*
* By default, the lookup algorithm will go to the [Scope.parent] if no match was found in the
* current scope. This behaviour can be turned off with [thisScopeOnly]. This is useful for
* qualified lookups, where we want to stay in our lookup-scope.
*
* @param symbol the symbol to lookup
* @param thisScopeOnly whether we should stay in the current scope for lookup or traverse to
* its parents if no match was found.
* @param replaceImports whether any symbols pointing to [ImportDeclaration.importedSymbols] or
* wildcards should be replaced with their actual nodes
* @param predicate An optional predicate which should be used in the lookup.
*/
fun lookupSymbol(
symbol: Symbol,
thisScopeOnly: Boolean = false,
replaceImports: Boolean = true,
predicate: ((Declaration) -> Boolean)? = null
): List<Declaration> {
Expand Down Expand Up @@ -141,8 +153,12 @@ abstract class Scope(
break
}

// If we do not have a hit, we can go up one scope
scope = scope.parent
// If we do not have a hit, we can go up one scope, unless thisScopeOnly is set to true
if (!thisScopeOnly) {
scope = scope.parent
} else {
break
}
}

return list ?: listOf()
Expand Down
Loading

0 comments on commit 1cde93a

Please sign in to comment.