diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8835fde1c6..1d3f2d337f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,6 +9,7 @@ on: - v*.** paths-ignore: - "docs/**" + merge_group: pull_request: types: [opened, synchronize, reopened] paths-ignore: diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt index 49fff98607..07755b54be 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt @@ -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() + private val symbolTable = mutableMapOf>() /** * In some languages, we can define aliases for names. An example is renaming package imports in @@ -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(scope) { if (it.name.lastPartsMatch(name)) { val helper = ref.resolutionHelper @@ -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 diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationConfiguration.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationConfiguration.kt index 5bd7104a66..89a45e840c 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationConfiguration.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationConfiguration.kt @@ -108,7 +108,8 @@ private constructor( inferenceConfiguration: InferenceConfiguration, compilationDatabase: CompilationDatabase?, matchCommentsToNodes: Boolean, - addIncludesToGraph: Boolean + addIncludesToGraph: Boolean, + passConfigurations: Map>, PassConfiguration>, ) { /** This list contains all languages which we want to translate. */ val languages: List> @@ -165,6 +166,8 @@ private constructor( /** This sub configuration object holds all information about inference and smart-guessing. */ val inferenceConfiguration: InferenceConfiguration + val passConfigurations: Map>, PassConfiguration> + init { registeredPasses = passes this.languages = languages @@ -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. */ @@ -228,6 +232,8 @@ private constructor( private var matchCommentsToNodes = false private var addIncludesToGraph = true private var useDefaultPasses = false + private var passConfigurations: MutableMap>, PassConfiguration> = + mutableMapOf() fun symbols(symbols: Map): Builder { this.symbols = symbols @@ -409,6 +415,15 @@ private constructor( return this } + fun > configurePass(clazz: KClass, config: PassConfiguration): Builder { + this.passConfigurations[clazz] = config + return this + } + + inline fun > configurePass(config: PassConfiguration): Builder { + return this.configurePass(T::class, config) + } + /** * Loads and registers an additional [Language] based on a fully qualified class name (FQN). */ @@ -594,7 +609,8 @@ private constructor( inferenceConfiguration, compilationDatabase, matchCommentsToNodes, - addIncludesToGraph + addIncludesToGraph, + passConfigurations ) } diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt index b760a9e50e..b82594f538 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/builder/Fluent.kt @@ -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) diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/declarations/FunctionDeclaration.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/declarations/FunctionDeclaration.kt index 709e89db77..2089b14720 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/declarations/FunctionDeclaration.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/declarations/FunctionDeclaration.kt @@ -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 @@ -264,6 +264,12 @@ 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 = "(" @@ -271,3 +277,32 @@ open class FunctionDeclaration : ValueDeclaration(), DeclarationHolder, Resoluti 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 + } diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/Scope.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/Scope.kt index 02610fc6a1..91e970c3c7 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/Scope.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/Scope.kt @@ -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 } diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/Reference.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/Reference.kt index 5eddd269f1..57ce5a3cba 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/Reference.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/Reference.kt @@ -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 @@ -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) } diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/ControlFlowSensitiveDFGPass.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/ControlFlowSensitiveDFGPass.kt index 656ecbed93..21f5cb40c5 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/ControlFlowSensitiveDFGPass.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/ControlFlowSensitiveDFGPass.kt @@ -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 } @@ -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()?.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>() + startState.declarationsState.push(node, PowersetLattice(setOf())) val finalState = iterateEOG(node.nextEOGEdges, startState, ::transfer) as? DFGPassState ?: return @@ -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( diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/Pass.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/Pass.kt index 2384b3b182..3c35e53c47 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/Pass.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/Pass.kt @@ -63,6 +63,8 @@ abstract class TranslationUnitPass(ctx: TranslationContext) : Pass(final override val ctx: TranslationContext) : val log: Logger = LoggerFactory.getLogger(Pass::class.java) } + + fun passConfig(): T? { + return this.config.passConfigurations[this::class] as? T + } } /** diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/ControlFlowSensitiveDFGPassTest.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/ControlFlowSensitiveDFGPassTest.kt new file mode 100644 index 0000000000..f877ec9156 --- /dev/null +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/ControlFlowSensitiveDFGPassTest.kt @@ -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() + .configurePass( + 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") } + } + } + } + } + } +} diff --git a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolverTest.kt b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolverTest.kt index 24415bf6ee..51458582dd 100644 --- a/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolverTest.kt +++ b/cpg-core/src/test/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolverTest.kt @@ -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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e55043f862..48056d7ae4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "1.9.0" neo4j = "4.0.6" -log4j = "2.21.0" +log4j = "2.22.0" sonarqube = "4.4.1.3373" spotless = "6.22.0" nexus-publish = "1.3.0"