Skip to content

Commit

Permalink
Added language traits HasFirstClassFunctions and `HasAnonymousIdent…
Browse files Browse the repository at this point in the history
…ifier`

This PR adds two new language traits, which represents languages that treat functions as first-class citizens, as well as languages that have anonymous identifiers for variables.
  • Loading branch information
oxisto committed Aug 30, 2023
1 parent 2ff78e1 commit 95f59ba
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 29 deletions.
102 changes: 77 additions & 25 deletions cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
*/
package de.fraunhofer.aisec.cpg

import de.fraunhofer.aisec.cpg.frontends.HasFirstClassFunctions
import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend
import de.fraunhofer.aisec.cpg.graph.*
import de.fraunhofer.aisec.cpg.graph.declarations.*
Expand All @@ -36,6 +37,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.sarif.PhysicalLocation
import java.util.*
import java.util.function.Predicate
import org.slf4j.LoggerFactory
Expand Down Expand Up @@ -68,6 +70,14 @@ class ScopeManager : ScopeProvider {
var currentScope: Scope? = null
private set

data class Alias(var from: Name, var to: Name)

/**
* In some languages, we can define aliases. They can probably be defined at different scopes,
* but for now we only allow it for the current file.
*/
private val aliases = mutableMapOf<PhysicalLocation.ArtifactLocation, MutableSet<Alias>>()

/**
* The language frontend tied to the scope manager. Can be used to implement language specific
* scope resolution or lookup.
Expand Down Expand Up @@ -600,27 +610,44 @@ class ScopeManager : ScopeProvider {
ref: DeclaredReferenceExpression,
scope: Scope? = currentScope
): ValueDeclaration? {
return resolve<ValueDeclaration>(scope) {
if (
it.name.lastPartsMatch(ref.name)
) { // TODO: This place is likely to make things fail
var helper = ref.resolutionHelper
// If the reference seems to point to a function the entire signature is checked
// for equality
if (helper?.type is FunctionPointerType && it is FunctionDeclaration) {
val fptrType = helper.type as FunctionPointerType
// TODO(oxisto): This is the third place where function pointers are
// resolved. WHY?
// TODO(oxisto): Support multiple return values
val returnType = it.returnTypes.firstOrNull() ?: IncompleteType()
if (
// Unfortunately, we still have an issue about duplicate declarations because header files
// are included multiple times, so we need to exclude the C++ frontend (for now).
val language = ref.language
val (s, name) =
if (
language?.name?.localName != "CLanguage" &&
(language?.name?.localName != "CPPLanguage")
) {
// For all other languages, we can extract the scope information out of the name and
// start our search at the dedicated scope.
extractScope(ref, scope)
} else {
Pair(scope, ref.name)
}

// Try to resolve value declarations according to our criteria
return resolve<ValueDeclaration>(s) {
if (it.name.lastPartsMatch(name)) {
val helper = ref.resolutionHelper
return@resolve when {
// If the reference seems to point to a function (using a function pointer)
// the entire signature is checked for equality
helper?.type is FunctionPointerType && it is FunctionDeclaration -> {
val fptrType = helper.type as FunctionPointerType
// TODO(oxisto): Support multiple return values
val returnType = it.returnTypes.firstOrNull() ?: IncompleteType()
returnType == fptrType.returnType &&
it.hasSignature(fptrType.parameters)
) {
return@resolve true
}
} else {
return@resolve it !is FunctionDeclaration
// If our language has first-class functions, we can safely return them as a
// reference
ref.language is HasFirstClassFunctions -> {
true
}
// Otherwise, we are not looking for functions here
else -> {
it !is FunctionDeclaration
}
}
}

Expand All @@ -640,21 +667,40 @@ class ScopeManager : ScopeProvider {
call: CallExpression,
scope: Scope? = currentScope
): List<FunctionDeclaration> {
val s = extractScope(call, scope)
val (s, name) = extractScope(call, scope)

return resolve(s) { it.name.lastPartsMatch(call.name) && it.hasSignature(call.signature) }
val func =
resolve<FunctionDeclaration>(s) {
it.name.lastPartsMatch(name) && it.hasSignature(call.signature)
}

return func
}

fun extractScope(node: Node, scope: Scope? = currentScope): Scope? {
/**
* This function extracts a possible scope out of a [Name], e.g. if the name is fully qualified.
* This also resolves possible name aliases (e.g. because of imports). It returns a pair of a
* scope (if found) as well as the name, which is possibly adjusted for the aliases.
*/
fun extractScope(node: Node, scope: Scope? = currentScope): Pair<Scope?, Name> {
var name: Name = node.name
var s = scope

// First, we need to check, whether we have some kind of scoping.
if (node.name.parent != null) {
// extract the scope name, it is usually a name space, but could probably be something
// else as well in other languages
val scopeName = node.name.parent

// TODO: proper scope selection
var scopeName = node.name.parent

// We need to check, if have an alias for this file
val list = aliases[node.location?.artifactLocation]
val alias = list?.firstOrNull { it.to == scopeName }?.from
if (alias != null) {
scopeName = alias
// Reconstruct the original name with the alias, so we can resolve declarations with
// the namescape
name = Name(name.localName, alias)
}

// this is a scoped call. we need to explicitly jump to that particular scope
val scopes = filterScopes { (it is NameScope && it.name == scopeName) }
Expand All @@ -671,7 +717,7 @@ class ScopeManager : ScopeProvider {
}
}

return s
return Pair(s, name)
}

/**
Expand Down Expand Up @@ -790,6 +836,12 @@ class ScopeManager : ScopeProvider {
.firstOrNull()
}

fun addAlias(file: PhysicalLocation.ArtifactLocation, from: Name, to: Name) {
val list = aliases.computeIfAbsent(file) { mutableSetOf() }

list += Alias(from, to)
}

/** Returns the current scope for the [ScopeProvider] interface. */
override val scope: Scope?
get() = currentScope
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,16 @@ import java.util.regex.Pattern
*
* Currently, this interface has no methods. However, in the future, this could be used to execute
* language/frontend-specific code for the particular trait. This could help to fine-tune the
* [de.fraunhofer.aisec.cpg.passes.CallResolver] for specific languages.
* [CallResolver] for specific languages.
*/
interface LanguageTrait

/** A language trait, that specifies that this language has support for templates or generics. */
interface HasGenerics : LanguageTrait {
/** The char starting the template specific code (e.g. '<') */
/** The char starting the template specific code (e.g. `<`) */
val startCharacter: Char

/** The char ending the template specific code (e.g. '>') */
/** The char ending the template specific code (e.g. `>`) */
val endCharacter: Char
}

Expand Down Expand Up @@ -223,3 +223,18 @@ interface HasShortCircuitOperators : LanguageTrait {
val operatorCodes: Set<String>
get() = conjunctiveOperators.union(disjunctiveOperators)
}

/**
* A language trait, that specifies that this language treats functions "first-class citizens",
* meaning they can be assigned to variables and passed as arguments to other functions.
*/
interface HasFirstClassFunctions

/**
* A language trait, that specifies that this language has an "anonymous" identifier, used for
* unused parameters or suppressed assignments.
*/
interface HasAnonymousIdentifier {
val anonymousIdentifier: String
get() = "_"
}
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ open class CallResolver(ctx: TranslationContext) : SymbolResolverPass(ctx) {
// the TU. It is also a little bit redundant, since ScopeManager.resolveFunction
// (which gets called before) already extracts the scope, but this information
// gets lost.
val scope = scopeManager.extractScope(call, scopeManager.globalScope)
val (scope, _) = scopeManager.extractScope(call, scopeManager.globalScope)

// We have two possible start points, a namespace declaration or a translation
// unit. Nothing else is allowed (fow now)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
package de.fraunhofer.aisec.cpg.passes

import de.fraunhofer.aisec.cpg.TranslationContext
import de.fraunhofer.aisec.cpg.frontends.HasAnonymousIdentifier
import de.fraunhofer.aisec.cpg.frontends.HasStructs
import de.fraunhofer.aisec.cpg.frontends.HasSuperClasses
import de.fraunhofer.aisec.cpg.graph.*
Expand Down Expand Up @@ -114,6 +115,14 @@ open class VariableUsageResolver(ctx: TranslationContext) : SymbolResolverPass(c

if (current !is DeclaredReferenceExpression || current is MemberExpression) return

// We can safely ignore references to the anonymous identifier, e.g. _ in Go.
if (
language is HasAnonymousIdentifier &&
current.name.localName == language.anonymousIdentifier
) {
return
}

// For now, we need to ignore reference expressions that are directly embedded into call
// expressions, because they are the "callee" property. In the future, we will use this
// property to actually resolve the function call. However, there is a special case that
Expand Down

0 comments on commit 95f59ba

Please sign in to comment.