Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make control-flow sensitive DFG pass configurable by complexity #1356

Merged
merged 5 commits into from
Nov 21, 2023
Merged
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
26 changes: 15 additions & 11 deletions cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,11 @@ class ScopeManager : ScopeProvider {
data class Alias(var from: Name, var to: Name)

/**
* A cache map of unique tags (computed with [Reference.buildUniqueTag]) and their respective
* [ValueDeclaration]. This is used by [resolveReference] as a caching mechanism.
* A cache map of reference tags (computed with [Reference.referenceTag]) and their respective
* pair of original [Reference] and resolved [ValueDeclaration]. This is used by
* [resolveReference] as a caching mechanism.
*/
private val symbolTable = mutableMapOf<ReferenceTag, ValueDeclaration>()
private val symbolTable = mutableMapOf<ReferenceTag, Pair<Reference, ValueDeclaration>>()

/**
* In some languages, we can define aliases for names. An example is renaming package imports in
Expand Down Expand Up @@ -623,18 +624,21 @@ class ScopeManager : ScopeProvider {
val startScope = ref.scope

// Retrieve a unique tag for the particular reference based on the current scope
val tag = ref.uniqueTag

// If we find a match in our symbol table, we can immediately return the declaration
var decl = symbolTable[tag]
if (decl != null) {
return decl
val tag = ref.referenceTag

// If we find a match in our symbol table, we can immediately return the declaration. We
// need to be careful about potential collisions in our tags, since they are based on the
// hash-code of the scope. We therefore take the extra precaution to compare the scope in
// case we get a hit. This should not take too much performance overhead.
val pair = symbolTable[tag]
if (pair != null && ref.scope == pair.first.scope) {
return pair.second
}

val (scope, name) = extractScope(ref, startScope)

// Try to resolve value declarations according to our criteria
decl =
val decl =
resolve<ValueDeclaration>(scope) {
if (it.name.lastPartsMatch(name)) {
val helper = ref.resolutionHelper
Expand Down Expand Up @@ -666,7 +670,7 @@ class ScopeManager : ScopeProvider {

// Update the symbol cache, if we found a declaration for the tag
if (decl != null) {
symbolTable[tag] = decl
symbolTable[tag] = Pair(ref, decl)
}

return decl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ private constructor(
inferenceConfiguration: InferenceConfiguration,
compilationDatabase: CompilationDatabase?,
matchCommentsToNodes: Boolean,
addIncludesToGraph: Boolean
addIncludesToGraph: Boolean,
passConfigurations: Map<KClass<out Pass<*>>, PassConfiguration>,
) {
/** This list contains all languages which we want to translate. */
val languages: List<Language<*>>
Expand Down Expand Up @@ -165,6 +166,8 @@ private constructor(
/** This sub configuration object holds all information about inference and smart-guessing. */
val inferenceConfiguration: InferenceConfiguration

val passConfigurations: Map<KClass<out Pass<*>>, PassConfiguration>

init {
registeredPasses = passes
this.languages = languages
Expand All @@ -178,6 +181,7 @@ private constructor(
this.compilationDatabase = compilationDatabase
this.matchCommentsToNodes = matchCommentsToNodes
this.addIncludesToGraph = addIncludesToGraph
this.passConfigurations = passConfigurations
}

/** Returns a list of all analyzed files. */
Expand Down Expand Up @@ -228,6 +232,8 @@ private constructor(
private var matchCommentsToNodes = false
private var addIncludesToGraph = true
private var useDefaultPasses = false
private var passConfigurations: MutableMap<KClass<out Pass<*>>, PassConfiguration> =
mutableMapOf()

fun symbols(symbols: Map<String, String>): Builder {
this.symbols = symbols
Expand Down Expand Up @@ -409,6 +415,15 @@ private constructor(
return this
}

fun <T : Pass<*>> configurePass(clazz: KClass<T>, config: PassConfiguration): Builder {
this.passConfigurations[clazz] = config
return this
}

inline fun <reified T : Pass<*>> configurePass(config: PassConfiguration): Builder {
return this.configurePass(T::class, config)
}

/**
* Loads and registers an additional [Language] based on a fully qualified class name (FQN).
*/
Expand Down Expand Up @@ -594,7 +609,8 @@ private constructor(
inferenceConfiguration,
compilationDatabase,
matchCommentsToNodes,
addIncludesToGraph
addIncludesToGraph,
passConfigurations
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -759,9 +759,9 @@ fun LanguageFrontend<*, *>.loopBody(init: Block.() -> Unit): Block {
}

/**
* Creates a new [Block] in the Fluent Node DSL and sets it to the [WhileStatement.statement] of the
* nearest enclosing [WhileStatement]. The [init] block can be used to create further sub-nodes as
* well as configuring the created node itself.
* Creates a new [Block] in the Fluent Node DSL and sets it to the [ForEachStatement.statement] of
* the nearest enclosing [ForEachStatement]. The [init] block can be used to create further
* sub-nodes as well as configuring the created node itself.
*/
context(ForEachStatement)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import de.fraunhofer.aisec.cpg.graph.edge.Properties
import de.fraunhofer.aisec.cpg.graph.edge.PropertyEdge
import de.fraunhofer.aisec.cpg.graph.edge.PropertyEdge.Companion.propertyEqualsList
import de.fraunhofer.aisec.cpg.graph.edge.PropertyEdgeDelegate
import de.fraunhofer.aisec.cpg.graph.statements.Statement
import de.fraunhofer.aisec.cpg.graph.statements.*
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Block
import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Expression
Expand Down Expand Up @@ -264,10 +264,45 @@ open class FunctionDeclaration : ValueDeclaration(), DeclarationHolder, Resoluti
return list
}

/** This returns a simple heuristic for the complexity of a function declaration. */
val complexity: Int
get() {
return this.body?.cyclomaticComplexity ?: 0
}

companion object {
const val WHITESPACE = " "
const val BRACKET_LEFT = "("
const val COMMA = ","
const val BRACKET_RIGHT = ")"
}
}

/** This is a very basic implementation of Cyclomatic Complexity. */
val Statement.cyclomaticComplexity: Int
get() {
var i = 0
for (stmt in (this as? StatementHolder)?.statements ?: listOf(this)) {
when (stmt) {
is ForEachStatement -> {
// add one and include the children
i += (stmt.statement?.cyclomaticComplexity ?: 0) + 1
}
is IfStatement -> {
// add one for each branch (and include the children)
stmt.thenStatement?.let { i += it.cyclomaticComplexity + 1 }
stmt.elseStatement?.let { i += it.cyclomaticComplexity + 1 }
}
is SwitchStatement -> {
// forward it to the block containing the case statements
stmt.statement?.let { i += it.cyclomaticComplexity }
}
is CaseStatement -> {
// add one for each branch (and include the children)
stmt.caseExpression?.let { i += it.cyclomaticComplexity }
}
}
}

return i
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ abstract class Scope(

override fun hashCode(): Int {
var result = astNode?.hashCode() ?: 0
result = 31 * result + (parent?.hashCode() ?: 0)
result = 31 * result + (name?.hashCode() ?: 0)
return result
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import de.fraunhofer.aisec.cpg.graph.declarations.Declaration
import de.fraunhofer.aisec.cpg.graph.declarations.ValueDeclaration
import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration
import de.fraunhofer.aisec.cpg.graph.edge.Properties
import de.fraunhofer.aisec.cpg.graph.scopes.Scope
import de.fraunhofer.aisec.cpg.graph.types.HasType
import de.fraunhofer.aisec.cpg.graph.types.Type
import de.fraunhofer.aisec.cpg.passes.SymbolResolver
Expand Down Expand Up @@ -155,11 +156,15 @@ open class Reference : Expression(), HasType.TypeObserver {
}

/**
* This function builds a unique tag for the particular reference, based on the [startScope].
* Its purpose is to cache symbol resolutions, similar to LLVMs system of Unified Symbol
* Resolution (USR).
* This function builds a tag for the particular reference, based on its [name],
* [resolutionHelper] and [scope]. Its purpose is to cache symbol resolutions, similar to LLVMs
* system of Unified Symbol Resolution (USR). Please be aware, that this tag is not guaranteed
* to be 100 % unique, especially if the language frontend is missing [Node.location]
* information (of the [Scope.astNode]. Therefore, its usage should be similar to a [hashCode],
* so that in case of an equal hash-code, a [equals] comparison (in this case of the [scope]) is
* needed.
*/
val uniqueTag: ReferenceTag
val referenceTag: ReferenceTag
get() {
return Objects.hash(this.name, this.resolutionHelper, this.scope)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ import kotlin.contracts.contract
@DependsOn(DFGPass::class)
open class ControlFlowSensitiveDFGPass(ctx: TranslationContext) : TranslationUnitPass(ctx) {

class Configuration(
/**
* This specifies the maximum complexity (as calculated per [Statement.cyclomaticComplexity]
* a [FunctionDeclaration] must have in order to be considered.
*/
var maxComplexity: Int? = null
) : PassConfiguration()

override fun cleanup() {
// Nothing to do
}
Expand All @@ -61,9 +69,28 @@ open class ControlFlowSensitiveDFGPass(ctx: TranslationContext) : TranslationUni
* @param node every node in the TranslationResult
*/
protected fun handle(node: Node) {
val max = passConfig<Configuration>()?.maxComplexity

if (node is FunctionDeclaration) {
// Skip empty functions
if (node.body == null) {
return
}

// Calculate the complexity of the function and see, if it exceeds our threshold
if (max != null) {
val c = node.body?.cyclomaticComplexity ?: 0
if (c > max) {
log.info(
"Ignoring function ${node.name} because its complexity (${c}) is greater than the configured maximum (${max})"
)
return
}
}

clearFlowsOfVariableDeclarations(node)
val startState = DFGPassState<Set<Node>>()

startState.declarationsState.push(node, PowersetLattice(setOf()))
val finalState =
iterateEOG(node.nextEOGEdges, startState, ::transfer) as? DFGPassState ?: return
Expand All @@ -78,7 +105,7 @@ open class ControlFlowSensitiveDFGPass(ctx: TranslationContext) : TranslationUni
for ((key, value) in finalState.generalState) {
if (key is TupleDeclaration) {
// We need a little hack for tuple statements to set the index. We have the
// outer part (i.e., the tuple) here but we generate the DFG edges to the
// outer part (i.e., the tuple) here, but we generate the DFG edges to the
// elements. We have the indices here, so it's amazing.
key.elements.forEachIndexed { i, element ->
element.addAllPrevDFG(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ abstract class TranslationUnitPass(ctx: TranslationContext) : Pass<TranslationUn
*/
interface PassTarget

open class PassConfiguration {}

/**
* Represents an abstract class that enhances the graph before it is persisted. Passes can exist at
* three different levels:
Expand Down Expand Up @@ -109,6 +111,10 @@ sealed class Pass<T : PassTarget>(final override val ctx: TranslationContext) :

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

fun <T : PassConfiguration> passConfig(): T? {
return this.config.passConfigurations[this::class] as? T
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2023, 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.TranslationConfiguration
import de.fraunhofer.aisec.cpg.frontends.TestLanguage
import de.fraunhofer.aisec.cpg.graph.builder.*
import kotlin.test.Test
import kotlin.test.assertNotNull

class ControlFlowSensitiveDFGPassTest {
@Test
fun testConfiguration() {
val result = getForEachTest()
assertNotNull(result)
}

fun getForEachTest() =
ControlDependenceGraphPassTest.testFrontend(
TranslationConfiguration.builder()
.registerLanguage(TestLanguage("::"))
.defaultPasses()
.registerPass<ControlFlowSensitiveDFGPass>()
.configurePass<ControlFlowSensitiveDFGPass>(
ControlFlowSensitiveDFGPass.Configuration(maxComplexity = 0)
)
.build()
)
.build {
translationResult {
translationUnit("forEach.cpp") {
// The main method
function("main", t("int")) {
body {
declare { variable("i", t("int")) { literal(0, t("int")) } }
forEachStmt {
declare { variable("loopVar", t("string")) }
call("magicFunction")
loopBody {
call("printf") {
literal("loop: \${}\n", t("string"))
ref("loopVar")
}
}
}
call("printf") { literal("1\n", t("string")) }

returnStmt { ref("i") }
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class SymbolResolverTest {
refs.forEach {
// Build a unique tag based on the scope of the reference is in (since this is usually
// the start scope)
val list = map.computeIfAbsent(it.uniqueTag) { mutableListOf() }
val list = map.computeIfAbsent(it.referenceTag) { mutableListOf() }
list += it

// All elements in the list must have the same scope and name
Expand Down
Loading