![JAR search and dependency download from the Maven repository](/logo.png)
de.fraunhofer.aisec.cpg.TranslationConfiguration.kt Maven / Gradle / Ivy
/*
* Copyright (c) 2019, 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
import com.fasterxml.jackson.annotation.JsonIdentityInfo
import com.fasterxml.jackson.annotation.JsonIdentityReference
import com.fasterxml.jackson.annotation.ObjectIdGenerators
import de.fraunhofer.aisec.cpg.frontends.CompilationDatabase
import de.fraunhofer.aisec.cpg.frontends.Language
import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend
import de.fraunhofer.aisec.cpg.frontends.cpp.CLanguage
import de.fraunhofer.aisec.cpg.frontends.cpp.CPPLanguage
import de.fraunhofer.aisec.cpg.frontends.java.JavaLanguage
import de.fraunhofer.aisec.cpg.passes.*
import de.fraunhofer.aisec.cpg.passes.order.*
import java.io.File
import java.nio.file.Path
import java.util.*
import kotlin.reflect.full.createInstance
import kotlin.reflect.full.findAnnotations
import kotlin.reflect.full.primaryConstructor
import org.apache.commons.lang3.builder.ToStringBuilder
import org.apache.commons.lang3.builder.ToStringStyle
import org.slf4j.LoggerFactory
/**
* The configuration for the [TranslationManager] holds all information that is used during the
* translation.
*/
class TranslationConfiguration
private constructor(
/** Definition of additional symbols, mostly useful for C++. */
val symbols: Map,
/** Source code files to parse. */
val softwareComponents: Map>,
val topLevel: File?,
/** Set to true to generate debug output for the parser. */
val debugParser: Boolean,
/**
* Should parser/translation fail on parse/resolving errors (true) or try to continue in a
* best-effort manner (false).
*/
val failOnError: Boolean,
/**
* Set to true to transitively load include files into the CPG.
*
* If this value is set to false but includePaths are given, the parser will resolve
* symbols/templates from these include, but do not load the parse tree into the CPG
*/
val loadIncludes: Boolean,
/**
* Paths to look for include files.
*
* It is recommended to set proper include paths as otherwise unresolved symbols/templates will
* result in subsequent parser mistakes, such as treating "<" as a BinaryOperator in the
* following example: <code> std::unique_ptr<Botan::Cipher_Mode> bla; </code>
*
* As long as loadIncludes is set to false, include files will only be parsed, but not loaded
* into the CPG. *
*/
val includePaths: List,
/**
* This acts as a white list for include files, if the array is not empty. Only the specified
* includes files will be parsed and processed in the CPG, unless it is a port of the blacklist,
* in which it will be ignored.
*/
val includeWhitelist: List,
/**
* This acts as a block list for include files, if the array is not empty. The specified include
* files will be excluded from being parsed and processed in the CPG. The blocklist entries
* always take priority over those in the whitelist.
*/
val includeBlocklist: List,
passes: List,
languages: List>,
codeInNodes: Boolean,
processAnnotations: Boolean,
disableCleanup: Boolean,
useUnityBuild: Boolean,
useParallelFrontends: Boolean,
typeSystemActiveInFrontend: Boolean,
inferenceConfiguration: InferenceConfiguration,
compilationDatabase: CompilationDatabase?,
matchCommentsToNodes: Boolean,
addIncludesToGraph: Boolean
) {
/** This list contains all languages which we want to translate. */
val languages: List>
/**
* Switch off cleaning up TypeManager memory after analysis.
*
* Set this to `true` only for testing.
*/
var disableCleanup = false
/** should the code of a node be shown as parameter in the node * */
@JvmField val codeInNodes: Boolean
/** Set to true to process annotations or annotation-like elements. */
val processAnnotations: Boolean
/**
* Only relevant for C++. A unity build refers to a build that consolidates all translation
* units into a single one, which has the advantage that header files are only processed once,
* adding far less duplicate nodes to the graph
*/
val useUnityBuild: Boolean
/**
* If true, the ASTs for the source files are parsed in parallel, but the passes afterwards will
* still run in a single thread. This speeds up initial parsing but makes sure that further
* graph enrichment algorithms remain correct.
*/
val useParallelFrontends: Boolean
/**
* If false, the type listener system is only activated once the frontends are done building the
* initial AST structure. This avoids errors where the type of a node may depend on the order in
* which the source files have been parsed.
*/
val typeSystemActiveInFrontend: Boolean
/**
* This is the data structure for storing the compilation database. It stores a mapping from the
* File to the list of files that have to be included to their path, specified by the parameter
* in the compilation database. This is currently only used by the [CXXLanguageFrontend].
*
* [[CompilationDatabase.Companion.fromFile] can be used to construct a new compilation database
* from a file.
*/
val compilationDatabase: CompilationDatabase?
/**
* If true the frontend shall use a heuristic matching of comments found in the source file to
* match them to the closest AST node and save it in the comment property.
*/
val matchCommentsToNodes: Boolean
/** If true the (cpp) frontend connects a node to required includes. */
val addIncludesToGraph: Boolean
@get:JsonIdentityReference(alwaysAsId = true)
@get:JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator::class,
property = "name"
)
val registeredPasses: List
/** This sub configuration object holds all information about inference and smart-guessing. */
val inferenceConfiguration: InferenceConfiguration
init {
registeredPasses = passes
this.languages = languages
// Make sure to init this AFTER sourceLocations has been set
this.codeInNodes = codeInNodes
this.processAnnotations = processAnnotations
this.disableCleanup = disableCleanup
this.useUnityBuild = useUnityBuild
this.useParallelFrontends = useParallelFrontends
this.typeSystemActiveInFrontend = typeSystemActiveInFrontend
this.inferenceConfiguration = inferenceConfiguration
this.compilationDatabase = compilationDatabase
this.matchCommentsToNodes = matchCommentsToNodes
this.addIncludesToGraph = addIncludesToGraph
}
/** Returns a list of all analyzed files. */
val sourceLocations: List
get() {
val sourceLocations: MutableList = ArrayList()
for ((_, value) in softwareComponents) {
sourceLocations.addAll(value)
}
return sourceLocations
}
/**
* Builds a [TranslationConfiguration].
*
* Example:
* `TranslationManager.builder() .config( TranslationConfiguration.builder()
* .sourceLocations(new File("example.cpp")) .defaultPasses() .debugParser(true) .build())
* .build(); `
*
*/
class Builder {
private var softwareComponents: MutableMap> = HashMap()
private val languages = mutableListOf>()
private var topLevel: File? = null
private var debugParser = false
private var failOnError = false
private var loadIncludes = false
private var symbols = mapOf()
private val includePaths = mutableListOf()
private val includeWhitelist = mutableListOf()
private val includeBlocklist = mutableListOf()
private val passes = mutableListOf()
private var codeInNodes = true
private var processAnnotations = false
private var disableCleanup = false
private var useUnityBuild = false
private var useParallelFrontends = false
private var typeSystemActiveInFrontend = true
private var inferenceConfiguration = InferenceConfiguration.Builder().build()
private var compilationDatabase: CompilationDatabase? = null
private var matchCommentsToNodes = false
private var addIncludesToGraph = true
fun symbols(symbols: Map): Builder {
this.symbols = symbols
return this
}
/**
* Files or directories containing the source code to analyze. Generates a dummy software
* component called "application".
*
* @param sourceLocations The files with the source code
* @return this
*/
fun sourceLocations(vararg sourceLocations: File): Builder {
softwareComponents["application"] = sourceLocations.toMutableList()
return this
}
/**
* Files or directories containing the source code to analyze. Generates a dummy software
* component called "application".
*
* @param sourceLocations The files with the source code
* @return this
*/
fun sourceLocations(sourceLocations: List): Builder {
softwareComponents["application"] = sourceLocations.toMutableList()
return this
}
/**
* Files or directories containing the source code to analyze organized by different
* components
*
* @param softwareComponents A map holding the different components with their files
* @return this
*/
fun softwareComponents(softwareComponents: MutableMap>): Builder {
this.softwareComponents = softwareComponents
return this
}
fun useCompilationDatabase(compilationDatabase: CompilationDatabase?): Builder {
this.compilationDatabase = compilationDatabase
return this
}
fun topLevel(topLevel: File?): Builder {
this.topLevel = topLevel
return this
}
/** Dump parser debug output to the logs (Caution: this will generate a lot of output). */
fun debugParser(debugParser: Boolean): Builder {
this.debugParser = debugParser
return this
}
/** Match comments found in source files to nodes according to a heuristic. */
fun matchCommentsToNodes(matchCommentsToNodes: Boolean): Builder {
this.matchCommentsToNodes = matchCommentsToNodes
return this
}
/** Adds all required includes. */
fun addIncludesToGraph(addIncludesToGraph: Boolean): Builder {
this.addIncludesToGraph = addIncludesToGraph
return this
}
/** Fail analysis on first error. Try to continue otherwise. */
fun failOnError(failOnError: Boolean): Builder {
this.failOnError = failOnError
return this
}
/**
* Load C/C++ include headers before the analysis.
*
* Required for macro expansion.
*/
fun loadIncludes(loadIncludes: Boolean): Builder {
this.loadIncludes = loadIncludes
return this
}
/** Directory containing include headers. */
fun includePath(includePath: String): Builder {
includePaths.add(Path.of(includePath))
return this
}
/** Directory containing include headers. */
fun includePath(includePath: Path): Builder {
includePaths.add(includePath)
return this
}
/**
* Adds the specified file to the include whitelist. Relative and absolute paths are
* supported.
*/
fun includeWhitelist(includeFile: String): Builder {
includeWhitelist.add(Path.of(includeFile))
return this
}
/**
* Adds the specified file to the include whitelist. Relative and absolute paths are
* supported.
*/
fun includeWhitelist(includeFile: Path): Builder {
includeWhitelist.add(includeFile)
return this
}
fun disableCleanup(): Builder {
disableCleanup = true
return this
}
/**
* Adds the specified file to the include blocklist. Relative and absolute paths are
* supported.
*/
fun includeBlocklist(includeFile: String): Builder {
includeBlocklist.add(Path.of(includeFile))
return this
}
/**
* Adds the specified file to the include blocklist. Relative and absolute paths are
* supported.
*/
fun includeBlocklist(includeFile: Path): Builder {
includeBlocklist.add(includeFile)
return this
}
/** Register an additional [Pass]. */
fun registerPass(pass: Pass): Builder {
passes.add(pass)
return this
}
/** Registers an additional [Language]. */
fun registerLanguage(language: Language): Builder {
languages.add(language)
log.info(
"Registered language frontend '${language::class.simpleName}' for following file types: ${language.fileExtensions}"
)
return this
}
/** Registers an additional [Language]. */
inline fun > registerLanguage(): Builder {
T::class.primaryConstructor?.call()?.let { registerLanguage(it) }
return this
}
/**
* Loads and registers an additional [Language] based on a fully qualified class name (FQN).
*/
@Throws(ConfigurationException::class)
fun registerLanguage(className: String): Builder {
try {
val loadedClass = Class.forName(className).kotlin.createInstance() as? Language<*>
if (loadedClass != null) {
registerLanguage(loadedClass)
} else
throw ConfigurationException(
"Failed casting supposed language class '$className'. It does not seem to be an implementation of Language<*>."
)
} catch (e: Exception) {
throw ConfigurationException(
"Failed to load and instantiate class from FQN '$className'. Possible causes of this error:\n" +
"- the given class is unavailable in the class path\n" +
"- the given class does not have a single no-arg constructor\n"
)
}
return this
}
/** Unregisters a registered [de.fraunhofer.aisec.cpg.frontends.Language]. */
fun unregisterLanguage(language: Class?>): Builder {
languages.removeIf { obj: Language? -> language.isInstance(obj) }
return this
}
/**
* Register all default [Pass]es.
*
* This will register
* - [TypeHierarchyResolver]
* - [JavaExternalTypeHierarchyResolver]
* - [ImportResolver]
* - [VariableUsageResolver]
* - [CallResolver]
* - [DFGPass]
* - [FunctionPointerCallResolver]
* - [EvaluationOrderGraphPass]
* - [TypeResolver]
* - [ControlFlowSensitiveDFGPass]
* - [FilenameMapper]
*
* to be executed in the order specified by their annotations.
*/
fun defaultPasses(): Builder {
registerPass(TypeHierarchyResolver())
registerPass(JavaExternalTypeHierarchyResolver())
registerPass(ImportResolver())
registerPass(VariableUsageResolver())
registerPass(CallResolver()) // creates CG
registerPass(DFGPass())
registerPass(FunctionPointerCallResolver())
registerPass(EvaluationOrderGraphPass()) // creates EOG
registerPass(TypeResolver())
registerPass(ControlFlowSensitiveDFGPass())
registerPass(FilenameMapper())
return this
}
/** Register extra passes declared by a frontend with [RegisterExtraPass] */
@Throws(ConfigurationException::class)
private fun registerExtraFrontendPasses() {
for (frontend in languages.map(Language::frontend)) {
val extraPasses = frontend.findAnnotations()
if (extraPasses.isNotEmpty()) {
for (p in extraPasses) {
val pass = p.value.primaryConstructor?.call()
if (pass != null) {
registerPass(pass)
log.info(
"Registered an extra (frontend dependent) default dependency: {}",
p.value
)
} else {
throw ConfigurationException(
"Failed to load frontend because we could not register required pass dependency: ${frontend.simpleName}"
)
}
}
}
}
}
/** Register all default languages. */
fun defaultLanguages(): Builder {
registerLanguage(CLanguage())
registerLanguage(CPPLanguage())
registerLanguage(JavaLanguage())
return this
}
/**
* Safely register an additional [Language] from a class name. If the [Language] given by
* the class name could not be loaded or instantiated, no [Language] is registered and no
* error is thrown. Please have a look at [registerLanguage] if an error should be thrown in
* case the language could not be registered.
*
* @param className Fully qualified class name (FQN) of a [Language] class
* @see [registerLanguage]
*/
fun optionalLanguage(className: String) =
try {
registerLanguage(className)
} catch (e: ConfigurationException) {
this
}
fun codeInNodes(b: Boolean): Builder {
codeInNodes = b
return this
}
/**
* Specifies, whether annotations should be process or not. By default, they are not
* processed, since they might populate the graph too much.
*
* @param b the new value
*/
fun processAnnotations(b: Boolean): Builder {
processAnnotations = b
return this
}
/**
* Only relevant for C++. A unity build refers to a build that consolidates all translation
* units into a single one, which has the advantage that header files are only processed
* once, adding far less duplicate nodes to the graph
*
* @param b the new value
*/
fun useUnityBuild(b: Boolean): Builder {
useUnityBuild = b
return this
}
/**
* If true, the ASTs for the source files are parsed in parallel, but the passes afterwards
* will still run in a single thread. This speeds up initial parsing but makes sure that
* further graph enrichment algorithms remain correct. Please make sure to also set
* [ ][.typeSystemActiveInFrontend] to false to avoid probabilistic errors that appear
* depending on the parsing order.
*
* @param b the new value
*/
fun useParallelFrontends(b: Boolean): Builder {
useParallelFrontends = b
return this
}
/**
* If false, the type system is only activated once the frontends are done building the
* initial AST structure. This avoids errors where the type of a node may depend on the
* order in which the source files have been parsed.
*
* @param b the new value
*/
fun typeSystemActiveInFrontend(b: Boolean): Builder {
typeSystemActiveInFrontend = b
return this
}
fun inferenceConfiguration(configuration: InferenceConfiguration): Builder {
inferenceConfiguration = configuration
return this
}
@Throws(ConfigurationException::class)
fun build(): TranslationConfiguration {
if (useParallelFrontends && typeSystemActiveInFrontend) {
log.warn(
"Not disabling the type system during the frontend " +
"phase is not recommended when using the parallel frontends feature! " +
"This may result in erroneous results."
)
}
registerExtraFrontendPasses()
return TranslationConfiguration(
symbols,
softwareComponents,
topLevel,
debugParser,
failOnError,
loadIncludes,
includePaths,
includeWhitelist,
includeBlocklist,
orderPasses(),
languages,
codeInNodes,
processAnnotations,
disableCleanup,
useUnityBuild,
useParallelFrontends,
typeSystemActiveInFrontend,
inferenceConfiguration,
compilationDatabase,
matchCommentsToNodes,
addIncludesToGraph
)
}
/**
* Collects the requested passes stored in [registeredPasses] and generates a
* [PassWithDepsContainer] consisting of pairs of passes and their dependencies.
*
* @return A populated [PassWithDepsContainer] derived from [registeredPasses].
*/
private fun collectInitialPasses(): PassWithDepsContainer {
val workingList = PassWithDepsContainer()
// Add the "execute before" dependencies.
for (p in passes) {
val executeBefore = p.executeBefore
for (eb in executeBefore) {
passes
.filter { eb.isInstance(it) }
.forEach { it.addSoftDependency(p.javaClass) }
}
}
for (p in passes) {
var passFound = false
for ((pass) in workingList.getWorkingList()) {
if (pass.javaClass == p.javaClass) {
passFound = true
break
}
}
if (!passFound) {
val deps: MutableSet> = HashSet()
deps.addAll(p.hardDependencies)
deps.addAll(p.softDependencies)
workingList.addToWorkingList(PassWithDependencies(p, deps))
}
}
return workingList
}
/**
* This function reorders passes in order to meet their dependency requirements.
* * soft dependencies [DependsOn] with `softDependency == true`: all passes registered as
* soft dependency will be executed before the current pass if they are registered
* * hard dependencies [DependsOn] with `softDependency == false (default)`: all passes
* registered as hard dependency will be executed before the current pass (hard
* dependencies will be registered even if the user did not register them)
* * first pass [ExecuteFirst]: a pass registered as first pass will be executed in the
* beginning
* * last pass [ExecuteLast]: a pass registered as last pass will be executed at the end
*
* This function uses a very simple (and inefficient) logic to meet the requirements above:
* 1. A list of all registered passes and their dependencies is build
* [PassWithDepsContainer.workingList]
* 1. All missing hard dependencies [DependsOn] are added to the
* [PassWithDepsContainer.workingList]
* 1. The first pass [ExecuteFirst] is added to the result and removed from the other passes
* dependencies
* 1. The first pass in the [workingList] without dependencies is added to the result and it
* is removed from the other passes dependencies
* 1. The above step is repeated until all passes are added to the result
*
* @return a sorted list of passes
*/
@Throws(ConfigurationException::class)
private fun orderPasses(): List {
log.info("Passes before enforcing order: {}", passes)
val result = mutableListOf()
// Create a local copy of all passes and their "current" dependencies without possible
// duplicates
val workingList = collectInitialPasses()
log.debug("Working list after initial scan: {}", workingList)
workingList.addMissingDependencies()
log.debug("Working list after adding missing dependencies: {}", workingList)
if (workingList.getFirstPasses().size > 1) {
log.error(
"Too many passes require to be executed as first pass: {}",
workingList.getWorkingList()
)
throw ConfigurationException(
"Too many passes require to be executed as first pass."
)
}
if (workingList.getLastPasses().size > 1) {
log.error(
"Too many passes require to be executed as last pass: {}",
workingList.getLastPasses()
)
throw ConfigurationException("Too many passes require to be executed as last pass.")
}
val firstPass = workingList.getAndRemoveFirstPass()
if (firstPass != null) {
result.add(firstPass)
}
while (!workingList.isEmpty) {
val p = workingList.getAndRemoveFirstPassWithoutDependencies()
if (p != null) {
result.add(p)
} else {
// failed to find a pass that can be added to the result -> deadlock :(
throw ConfigurationException("Failed to satisfy ordering requirements.")
}
}
log.info("Passes after enforcing order: {}", result)
return result
}
}
override fun toString(): String {
return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE)
}
companion object {
private val log = LoggerFactory.getLogger(TranslationConfiguration::class.java)
fun builder(): Builder {
return Builder()
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy