Skip to content

Commit

Permalink
Extracted call/cast replacement into separate pass (#1499)
Browse files Browse the repository at this point in the history
* Extracted call/cast replacement into separate pass

Multiple languages, such as Go and C++ support "functional style casts", in the form of int(5). During the frontend parsing, they are indistinguishable from function calls. Therefore, we need to do a cleanup after all types are known but before other passes are invoked that replace those calls with casts. The proposed solution is to include a new language trait HasFuntionalCasts and to move the logic from the GoExtraPass into a separate, language-neutral one.

Fixes #1487

* Added annotation to pass that requires a language trait
  • Loading branch information
oxisto authored Apr 5, 2024
1 parent 6d3bb29 commit c66aad6
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,7 @@ private constructor(
registerPass<TypeResolver>()
registerPass<ControlFlowSensitiveDFGPass>()
registerPass<FilenameMapper>()
registerPass<ReplaceCallCastPass>()
useDefaultPasses = true
return this
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ import de.fraunhofer.aisec.cpg.graph.declarations.RecordDeclaration
import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration
import de.fraunhofer.aisec.cpg.graph.scopes.GlobalScope
import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression
import de.fraunhofer.aisec.cpg.graph.statements.expressions.CastExpression
import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberExpression
import de.fraunhofer.aisec.cpg.graph.types.Type
import de.fraunhofer.aisec.cpg.passes.ReplaceCallCastPass
import de.fraunhofer.aisec.cpg.passes.SymbolResolver

/**
Expand Down Expand Up @@ -227,22 +229,30 @@ interface HasShortCircuitOperators : LanguageTrait {
* 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
interface HasFirstClassFunctions : LanguageTrait

/**
* A language trait, that specifies that this language has an "anonymous" identifier, used for
* unused parameters or suppressed assignments.
*/
interface HasAnonymousIdentifier {
interface HasAnonymousIdentifier : LanguageTrait {
val anonymousIdentifier: String
get() = "_"
}

/**
* A language trait, that specifies that this language has global variables directly in the
* [GlobalScope], i.e,. not within a namespace, but directly contained in a
* [GlobalScope], i.e., not within a namespace, but directly contained in a
* [TranslationUnitDeclaration].
*/
interface HasGlobalVariables {
interface HasGlobalVariables : LanguageTrait {
val globalVariableScopeClass: Class<out Node>
}

/**
* A language trait, that specifies that the language has so-called functional style casts, meaning
* that they look like regular call expressions. Since we can therefore not distinguish between a
* [CallExpression] and a [CastExpression], we need to employ an additional pass
* ([ReplaceCallCastPass]) after the initial language frontends are done.
*/
interface HasFunctionalCasts : LanguageTrait
38 changes: 33 additions & 5 deletions cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/Pass.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,21 @@ package de.fraunhofer.aisec.cpg.passes
import de.fraunhofer.aisec.cpg.*
import de.fraunhofer.aisec.cpg.frontends.Language
import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend
import de.fraunhofer.aisec.cpg.frontends.LanguageTrait
import de.fraunhofer.aisec.cpg.frontends.TranslationException
import de.fraunhofer.aisec.cpg.graph.*
import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration
import de.fraunhofer.aisec.cpg.graph.scopes.Scope
import de.fraunhofer.aisec.cpg.helpers.Benchmark
import de.fraunhofer.aisec.cpg.helpers.SubgraphWalker.ScopedWalker
import de.fraunhofer.aisec.cpg.passes.order.RequiredFrontend
import de.fraunhofer.aisec.cpg.passes.order.RequiresLanguageTrait
import java.util.concurrent.CompletableFuture
import java.util.function.Consumer
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.findAnnotations
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.primaryConstructor
import org.slf4j.Logger
import org.slf4j.LoggerFactory
Expand Down Expand Up @@ -112,16 +117,36 @@ sealed class Pass<T : Node>(final override val ctx: TranslationContext) :
* [RequiredFrontend]
*/
fun runsWithCurrentFrontend(usedFrontends: Collection<LanguageFrontend<*, *>>): Boolean {
if (!this.javaClass.isAnnotationPresent(RequiredFrontend::class.java)) return true
val requiredFrontend = this.javaClass.getAnnotation(RequiredFrontend::class.java).value
val requiredFrontend = this::class.findAnnotation<RequiredFrontend>() ?: return true
for (used in usedFrontends) {
if (used.javaClass == requiredFrontend.java) return true
if (used::class == requiredFrontend.value) return true
}
return false
}

companion object {
/**
* Checks, if the pass requires a specific [LanguageTrait] and if the current target of the pass
* has this trait.
*
* @return true, if the pass does not require a specific language trait or if it matches the
* [RequiresLanguageTrait].
*/
fun runsWithLanguageTrait(language: Language<*>?): Boolean {
if (language == null) {
return true
}

val requiresLanguageTraits = this::class.findAnnotations<RequiresLanguageTrait>()
for (requiresLanguageTrait in requiresLanguageTraits) {
if (!language::class.isSubclassOf(requiresLanguageTrait.value)) {
return false
}
}

return true
}

companion object {
val log: Logger = LoggerFactory.getLogger(Pass::class.java)
}

Expand Down Expand Up @@ -264,7 +289,10 @@ private inline fun <reified T : Node> consumeTarget(
val realClass = checkForReplacement(cls, language, ctx.config)

val pass = realClass.primaryConstructor?.call(ctx)
if (pass?.runsWithCurrentFrontend(executedFrontends) == true) {
if (
pass?.runsWithCurrentFrontend(executedFrontends) == true &&
pass.runsWithLanguageTrait(language)
) {
pass.accept(target)
pass.cleanup()
return pass
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* 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.passes

import de.fraunhofer.aisec.cpg.TranslationContext
import de.fraunhofer.aisec.cpg.frontends.Handler
import de.fraunhofer.aisec.cpg.frontends.HasFunctionalCasts
import de.fraunhofer.aisec.cpg.frontends.Language
import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend
import de.fraunhofer.aisec.cpg.graph.*
import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration
import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression
import de.fraunhofer.aisec.cpg.graph.statements.expressions.CastExpression
import de.fraunhofer.aisec.cpg.graph.statements.expressions.ConstructExpression
import de.fraunhofer.aisec.cpg.graph.statements.expressions.UnaryOperator
import de.fraunhofer.aisec.cpg.graph.types.Type
import de.fraunhofer.aisec.cpg.helpers.SubgraphWalker
import de.fraunhofer.aisec.cpg.passes.order.DependsOn
import de.fraunhofer.aisec.cpg.passes.order.ExecuteBefore
import de.fraunhofer.aisec.cpg.passes.order.RequiresLanguageTrait

/**
* If a [Language] has the trait [HasFunctionalCasts], we cannot distinguish between a
* [CallExpression] and a [CastExpression] during the initial translation. This stems from the fact
* that we might not know all the types yet. We therefore need to handle them as regular call
* expression in a [LanguageFrontend] or [Handler] and then later replace them with a
* [CastExpression], if the [CallExpression.callee] refers to name of a [Type] rather than a
* function.
*/
@ExecuteBefore(EvaluationOrderGraphPass::class)
@DependsOn(TypeResolver::class)
@RequiresLanguageTrait(HasFunctionalCasts::class)
class ReplaceCallCastPass(ctx: TranslationContext) : TranslationUnitPass(ctx) {
private lateinit var walker: SubgraphWalker.ScopedWalker

override fun accept(tu: TranslationUnitDeclaration) {
walker = SubgraphWalker.ScopedWalker(ctx.scopeManager)
walker.registerHandler { _, parent, node ->
when (node) {
is CallExpression -> handleCall(node, parent)
}
}

walker.iterate(tu)
}

private fun handleCall(call: CallExpression, parent: Node?) {
// Make sure, we are not accidentally handling construct expressions (since they also derive
// from call expressions)
if (call is ConstructExpression) {
return
}

// We need to check, whether the "callee" refers to a type and if yes, convert it into a
// cast expression. And this is only really necessary, if the function call has a single
// argument.
var callee = call.callee
if (parent != null && callee != null && call.arguments.size == 1) {
val language = parent.language

var pointer = false
// If the argument is a UnaryOperator, unwrap them
if (callee is UnaryOperator && callee.operatorCode == "*") {
pointer = true
callee = callee.input
}

// First, check if this is a built-in type
if (language?.builtInTypes?.contains(callee.name.toString()) == true) {
walker.replaceCallWithCast(callee.name.toString(), parent, call, false)
} else {
// If not, then this could still refer to an existing type. We need to make sure
// that we take the current namespace into account
val fqn =
if (callee.name.parent == null) {
scopeManager.currentNamespace.fqn(callee.name.localName)
} else {
callee.name
}

if (typeManager.typeExists(fqn.toString())) {
walker.replaceCallWithCast(fqn, parent, call, pointer)
}
}
}
}

override fun cleanup() {
// Nothing to do
}
}

context(ContextProvider)
fun SubgraphWalker.ScopedWalker.replaceCallWithCast(
typeName: CharSequence,
parent: Node,
call: CallExpression,
pointer: Boolean,
) {
val cast = newCastExpression()
cast.code = call.code
cast.language = call.language
cast.location = call.location
cast.castType =
if (pointer) {
call.objectType(typeName).pointer()
} else {
call.objectType(typeName)
}
cast.expression = call.arguments.single()
cast.name = cast.castType.name

if (parent !is ArgumentHolder) {
Pass.log.error(
"Parent AST node of call expression is not an argument holder. Cannot convert to cast expression. Further analysis might not be entirely accurate."
)
return
}

val success = parent.replaceArgument(call, cast)
if (!success) {
Pass.log.error(
"Replacing call expression with cast expression was not successful. Further analysis might not be entirely accurate."
)
} else {
call.disconnectFromGraph()

// Make sure to inform the walker about our change
this.registerReplacement(call, cast)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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.passes.order

import de.fraunhofer.aisec.cpg.frontends.Language
import de.fraunhofer.aisec.cpg.frontends.LanguageTrait
import kotlin.reflect.KClass

/**
* This annotation can only enable a pass if its target language implements a given [LanguageTrait].
*
* This annotation is [Repeatable]. In this case, all specified language traits must exist on the
* [Language].
*/
@Repeatable annotation class RequiresLanguageTrait(val value: KClass<out LanguageTrait>)
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ open class CPPLanguage :
HasComplexCallResolution,
HasStructs,
HasClasses,
HasUnknownType {
HasUnknownType,
HasFunctionalCasts {
override val fileExtensions = listOf("cpp", "cc", "cxx", "hpp", "hh")
override val elaboratedTypeSpecifier = listOf("class", "struct", "union", "enum")
override val unknownTypeString = listOf("auto")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import de.fraunhofer.aisec.cpg.passes.order.ExecuteBefore
* type information.
*/
@ExecuteBefore(EvaluationOrderGraphPass::class)
@ExecuteBefore(ReplaceCallCastPass::class)
@DependsOn(TypeResolver::class)
class CXXExtraPass(ctx: TranslationContext) : ComponentPass(ctx) {
override fun accept(component: Component) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ class GoLanguage :
HasGenerics,
HasStructs,
HasFirstClassFunctions,
HasAnonymousIdentifier {
HasAnonymousIdentifier,
HasFunctionalCasts {
override val fileExtensions = listOf("go")
override val namespaceDelimiter = "."
@Transient override val frontend = GoLanguageFrontend::class
Expand Down
Loading

0 comments on commit c66aad6

Please sign in to comment.