diff --git a/README.md b/README.md index 7c5b791cb3..185ec6de98 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ In order to get familiar with the graph itself, you can use the subproject [cpg- ### As Library The most recent version is being published to Maven central and can be used as a simple dependency, either using Maven or Gradle. Since Eclipse CDT is not published on maven central, it is necessary to add a repository with a custom layout to find the released CDT files. For example, using Gradle's Kotlin syntax: -``` +```kotlin repositories { ivy { setUrl("https://download.eclipse.org/tools/cdt/releases/11.0/cdt-11.0.0/plugins") @@ -121,7 +121,7 @@ Instead of manually editing the `gradle.properties` file, you can also use the ` #### Golang -In the case of Golang, the necessary native code can be found in the `src/main/golang` folder of the `cpg-language-go` submodule. Gradle should automatically find JNI headers and stores the finished library in the `src/main/golang` folder. This currently only works for Linux and macOS. In order to use it in an external project, the resulting library needs to be placed somewhere in `java.library.path`. +In the case of Golang, the necessary native code can be found in the `src/main/golang` folder of the `cpg-language-go` submodule. Gradle should automatically store the finished library in the `src/main/golang` folder. This currently only works for Linux and macOS. #### Python @@ -145,7 +145,6 @@ Through the `JepSingleton`, the CPG library will look for well known paths on Li For parsing TypeScript, the necessary NodeJS-based code can be found in the `src/main/nodejs` directory of the `cpg-language-typescript` submodule. Gradle should build the script automatically, provided NodeJS (>=16) is installed. The bundles script will be placed inside the jar's resources and should work out of the box. - ### Code Style We use [Google Java Style](https://github.com/google/google-java-format) as a formatting. Please install the appropriate plugin for your IDE, such as the [google-java-format IntelliJ plugin](https://plugins.jetbrains.com/plugin/8527-google-java-format) or [google-java-format Eclipse plugin](https://github.com/google/google-java-format/releases/download/google-java-format-1.6/google-java-format-eclipse-plugin_1.6.0.jar). @@ -183,8 +182,7 @@ The following authors have contributed to this project (in alphabetical order): ## Contributing -We are currently discussing the implementation of a Contributor License Agreement (CLA). Unfortunately, -we cannot merge external pull requests until this issue is resolved. +Before accepting external contributions, you need to sign our [CLA](https://cla-assistant.io/Fraunhofer-AISEC/cpg). Our CLA assistent will check, whether you already signed the CLA when you open your first pull request. ## Further reading diff --git a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/ValueEvaluator.kt b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/ValueEvaluator.kt index 04ddcdd46c..4c3c2a14c7 100644 --- a/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/ValueEvaluator.kt +++ b/cpg-analysis/src/main/kotlin/de/fraunhofer/aisec/cpg/analysis/ValueEvaluator.kt @@ -163,6 +163,11 @@ open class ValueEvaluator( "/=" -> handleDiv(lhsValue, rhsValue, expr) "*", "*=" -> handleTimes(lhsValue, rhsValue, expr) + "<<" -> handleShiftLeft(lhsValue, rhsValue, expr) + ">>" -> handleShiftRight(lhsValue, rhsValue, expr) + "&" -> handleBitwiseAnd(lhsValue, rhsValue, expr) + "|" -> handleBitwiseOr(lhsValue, rhsValue, expr) + "^" -> handleBitwiseXor(lhsValue, rhsValue, expr) ">" -> handleGreater(lhsValue, rhsValue, expr) ">=" -> handleGEq(lhsValue, rhsValue, expr) "<" -> handleLess(lhsValue, rhsValue, expr) @@ -202,6 +207,51 @@ open class ValueEvaluator( } } + private fun handleShiftLeft(lhsValue: Any?, rhsValue: Any?, expr: Expression?): Any? { + return when { + // right side must always be an int + lhsValue is Int && rhsValue is Int -> lhsValue shl rhsValue + lhsValue is Long && rhsValue is Int -> lhsValue shl rhsValue + else -> cannotEvaluate(expr, this) + } + } + + private fun handleShiftRight(lhsValue: Any?, rhsValue: Any?, expr: Expression?): Any? { + return when { + // right side must always be an int + lhsValue is Int && rhsValue is Int -> lhsValue shr rhsValue + lhsValue is Long && rhsValue is Int -> lhsValue shr rhsValue + else -> cannotEvaluate(expr, this) + } + } + + private fun handleBitwiseAnd(lhsValue: Any?, rhsValue: Any?, expr: Expression?): Any? { + return when { + // left and right must be equal and only long and int are supported + lhsValue is Int && rhsValue is Int -> lhsValue and rhsValue + lhsValue is Long && rhsValue is Long -> lhsValue and rhsValue + else -> cannotEvaluate(expr, this) + } + } + + private fun handleBitwiseOr(lhsValue: Any?, rhsValue: Any?, expr: Expression?): Any? { + return when { + // left and right must be equal and only long and int are supported + lhsValue is Int && rhsValue is Int -> lhsValue or rhsValue + lhsValue is Long && rhsValue is Long -> lhsValue or rhsValue + else -> cannotEvaluate(expr, this) + } + } + + private fun handleBitwiseXor(lhsValue: Any?, rhsValue: Any?, expr: Expression?): Any? { + return when { + // left and right must be equal and only long and int are supported + lhsValue is Int && rhsValue is Int -> lhsValue xor rhsValue + lhsValue is Long && rhsValue is Long -> lhsValue xor rhsValue + else -> cannotEvaluate(expr, this) + } + } + private fun handleGreater(lhsValue: Any?, rhsValue: Any?, expr: Expression?): Any? { return if (lhsValue is Number && rhsValue is Number) { lhsValue.compareTo(rhsValue) > 0 diff --git a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/ValueEvaluatorTest.kt b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/ValueEvaluatorTest.kt index 91d8deb9b0..500144e2ab 100644 --- a/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/ValueEvaluatorTest.kt +++ b/cpg-analysis/src/test/kotlin/de/fraunhofer/aisec/cpg/analysis/ValueEvaluatorTest.kt @@ -639,6 +639,106 @@ class ValueEvaluatorTest { } } + @Test + fun testHandleShiftLeft() { + with(TestHandler(TestLanguageFrontend())) { + val binOp = newBinaryOperator("<<") + // Int.plus + binOp.lhs = newLiteral(3, primitiveType("int")) + binOp.rhs = newLiteral(2, primitiveType("int")) + assertEquals(12, ValueEvaluator().evaluate(binOp)) + + // Long.plus + binOp.lhs = newLiteral(3L, primitiveType("long")) + binOp.rhs = newLiteral(2, primitiveType("int")) + assertEquals(12L, ValueEvaluator().evaluate(binOp)) + + binOp.lhs = newLiteral("Hello", primitiveType("string")) + binOp.rhs = newLiteral(" world", primitiveType("string")) + assertEquals("{<<}", ValueEvaluator().evaluate(binOp)) + } + } + + @Test + fun testHandleShiftRight() { + with(TestHandler(TestLanguageFrontend())) { + val binOp = newBinaryOperator(">>") + // Int.plus + binOp.lhs = newLiteral(3, primitiveType("int")) + binOp.rhs = newLiteral(2, primitiveType("int")) + assertEquals(0, ValueEvaluator().evaluate(binOp)) + + // Long.plus + binOp.lhs = newLiteral(3L, primitiveType("long")) + binOp.rhs = newLiteral(2, primitiveType("int")) + assertEquals(0L, ValueEvaluator().evaluate(binOp)) + + binOp.lhs = newLiteral("Hello", primitiveType("string")) + binOp.rhs = newLiteral(" world", primitiveType("string")) + assertEquals("{>>}", ValueEvaluator().evaluate(binOp)) + } + } + + @Test + fun testHandleBitwiseAnd() { + with(TestHandler(TestLanguageFrontend())) { + val binOp = newBinaryOperator("&") + // Int.plus + binOp.lhs = newLiteral(3, primitiveType("int")) + binOp.rhs = newLiteral(2, primitiveType("int")) + assertEquals(2, ValueEvaluator().evaluate(binOp)) + + // Long.plus + binOp.lhs = newLiteral(3L, primitiveType("long")) + binOp.rhs = newLiteral(2L, primitiveType("long")) + assertEquals(2L, ValueEvaluator().evaluate(binOp)) + + binOp.lhs = newLiteral("Hello", primitiveType("string")) + binOp.rhs = newLiteral(" world", primitiveType("string")) + assertEquals("{&}", ValueEvaluator().evaluate(binOp)) + } + } + + @Test + fun testHandleBitwiseOr() { + with(TestHandler(TestLanguageFrontend())) { + val binOp = newBinaryOperator("|") + // Int.plus + binOp.lhs = newLiteral(3, primitiveType("int")) + binOp.rhs = newLiteral(2, primitiveType("int")) + assertEquals(3, ValueEvaluator().evaluate(binOp)) + + // Long.plus + binOp.lhs = newLiteral(3L, primitiveType("long")) + binOp.rhs = newLiteral(2L, primitiveType("long")) + assertEquals(3L, ValueEvaluator().evaluate(binOp)) + + binOp.lhs = newLiteral("Hello", primitiveType("string")) + binOp.rhs = newLiteral(" world", primitiveType("string")) + assertEquals("{|}", ValueEvaluator().evaluate(binOp)) + } + } + + @Test + fun testHandleBitwiseXor() { + with(TestHandler(TestLanguageFrontend())) { + val binOp = newBinaryOperator("^") + // Int.plus + binOp.lhs = newLiteral(3, primitiveType("int")) + binOp.rhs = newLiteral(2, primitiveType("int")) + assertEquals(1, ValueEvaluator().evaluate(binOp)) + + // Long.plus + binOp.lhs = newLiteral(3L, primitiveType("long")) + binOp.rhs = newLiteral(2L, primitiveType("long")) + assertEquals(1L, ValueEvaluator().evaluate(binOp)) + + binOp.lhs = newLiteral("Hello", primitiveType("string")) + binOp.rhs = newLiteral(" world", primitiveType("string")) + assertEquals("{^}", ValueEvaluator().evaluate(binOp)) + } + } + @Test fun testHandleUnary() { with(TestHandler(TestLanguageFrontend())) { 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 5e7c57d990..49fff98607 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 @@ -34,6 +34,7 @@ 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.Reference +import de.fraunhofer.aisec.cpg.graph.statements.expressions.ReferenceTag import de.fraunhofer.aisec.cpg.graph.types.FunctionPointerType import de.fraunhofer.aisec.cpg.graph.types.IncompleteType import de.fraunhofer.aisec.cpg.graph.types.Type @@ -74,6 +75,12 @@ class ScopeManager : ScopeProvider { /** Represents an alias with the name [to] for the particular name [from]. */ 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. + */ + private val symbolTable = mutableMapOf() + /** * In some languages, we can define aliases for names. An example is renaming package imports in * Go, e.g., to avoid name conflicts. @@ -607,45 +614,62 @@ class ScopeManager : ScopeProvider { * Resolves only references to Values in the current scope, static references to other visible * records are not resolved over the ScopeManager. * - * @param scope * @param ref * @return * * TODO: We should merge this function with [.resolveFunction] */ - @JvmOverloads - fun resolveReference(ref: Reference, startScope: Scope? = currentScope): ValueDeclaration? { + fun resolveReference(ref: Reference): ValueDeclaration? { + 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 (scope, name) = extractScope(ref, startScope) // Try to resolve value declarations according to our criteria - return resolve(scope) { - 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) - } - // 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 + decl = + resolve(scope) { + 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) + } + // 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 + } } } + + return@resolve false } + .firstOrNull() - return@resolve false - } - .firstOrNull() + // Update the symbol cache, if we found a declaration for the tag + if (decl != null) { + symbolTable[tag] = decl + } + + return decl } /** diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/Language.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/Language.kt index 5d1fdb6849..c0e25e1439 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/Language.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/Language.kt @@ -164,6 +164,9 @@ abstract class Language> : Node() { "-", "*", "/" -> arithmeticOpTypePropagation(operation.lhs.type, operation.rhs.type) + "&", + "|", + "^", "<<", ">>" -> if (operation.lhs.type.isPrimitive && operation.rhs.type.isPrimitive) { 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 a7255d0165..02610fc6a1 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 @@ -82,6 +82,23 @@ abstract class Scope( return this is LoopScope } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Scope + + if (astNode != other.astNode) return false + return name == other.name + } + + override fun hashCode(): Int { + var result = astNode?.hashCode() ?: 0 + result = 31 * result + (parent?.hashCode() ?: 0) + result = 31 * result + (name?.hashCode() ?: 0) + return result + } + /** Returns the [GlobalScope] of this scope by traversing its parents upwards. */ val globalScope: Scope? get() { 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 11d47bb3a8..5eddd269f1 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 @@ -153,4 +153,16 @@ open class Reference : Expression(), HasType.TypeObserver { prev.registerTypeObserver(this) } } + + /** + * 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). + */ + val uniqueTag: ReferenceTag + get() { + return Objects.hash(this.name, this.resolutionHelper, this.scope) + } } + +typealias ReferenceTag = Int diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolver.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolver.kt index 5e3ec966b7..827ca9425c 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolver.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/SymbolResolver.kt @@ -205,7 +205,7 @@ open class SymbolResolver(ctx: TranslationContext) : ComponentPass(ctx) { // Peek into the declaration, and if it is a variable, we can proceed normally, as we // are running into the special case explained above. Otherwise, we abort here (for // now). - wouldResolveTo = scopeManager.resolveReference(current, current.scope) + wouldResolveTo = scopeManager.resolveReference(current) if (wouldResolveTo !is VariableDeclaration && wouldResolveTo !is ParameterDeclaration) { return } @@ -214,9 +214,7 @@ open class SymbolResolver(ctx: TranslationContext) : ComponentPass(ctx) { // Only consider resolving, if the language frontend did not specify a resolution. If we // already have populated the wouldResolveTo variable, we can re-use this instead of // resolving again - var refersTo = - current.refersTo - ?: wouldResolveTo ?: scopeManager.resolveReference(current, current.scope) + var refersTo = current.refersTo ?: wouldResolveTo ?: scopeManager.resolveReference(current) var recordDeclType: Type? = null if (currentClass != null) { 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 0f141ca244..24415bf6ee 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 @@ -31,6 +31,8 @@ import de.fraunhofer.aisec.cpg.TestUtils.assertRefersTo import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.statements.expressions.ConstructExpression import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberCallExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference +import de.fraunhofer.aisec.cpg.graph.statements.expressions.ReferenceTag import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs @@ -71,4 +73,23 @@ class SymbolResolverTest { assertNotNull(construct) assertInvokes(construct, constructor) } + + @Test + fun testUniqueTags() { + val result = GraphExamples.getConditionalExpression() + + val map = mutableMapOf>() + + val refs = result.refs + 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() } + list += it + + // All elements in the list must have the same scope and name + assertEquals(1, list.map { ref -> ref.scope }.toSet().size) + assertEquals(1, list.map { ref -> ref.name }.toSet().size) + } + } } diff --git a/cpg-language-go/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/DeclarationTest.kt b/cpg-language-go/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/DeclarationTest.kt index 62a1cb78db..6a3be0a564 100644 --- a/cpg-language-go/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/DeclarationTest.kt +++ b/cpg-language-go/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/DeclarationTest.kt @@ -303,10 +303,16 @@ class DeclarationTest { with(tu) { val values = mapOf( + "zeroShift" to Pair(0, objectType("int")), + "zeroAnd" to Pair(0, objectType("int")), "one" to Pair(1, objectType("p.custom")), "oneAsWell" to Pair(1, objectType("p.custom")), + "oneShift" to Pair(1, primitiveType("int")), "two" to Pair(2, primitiveType("int")), + "twoShift" to Pair(2, primitiveType("int")), "three" to Pair(3, primitiveType("int")), + "threeOr" to Pair(3, primitiveType("int")), + "threeXor" to Pair(3, primitiveType("int")), "four" to Pair(4, primitiveType("int")), "tenAsWell" to Pair(10, primitiveType("int")), "five" to Pair(5, primitiveType("int")), diff --git a/cpg-language-go/src/test/resources/golang/const.go b/cpg-language-go/src/test/resources/golang/const.go index bd87af2206..3338dbfa3c 100644 --- a/cpg-language-go/src/test/resources/golang/const.go +++ b/cpg-language-go/src/test/resources/golang/const.go @@ -24,3 +24,12 @@ const ( fiveAsWell = 5 + iota*100 onehundredandfive ) + +const ( + oneShift = 1 << iota + twoShift + zeroShift = 1 >> iota + zeroAnd = oneShift & twoShift + threeOr = oneShift | twoShift + threeXor = oneShift ^ twoShift +) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 65b269ff08..c700c24310 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.20.0" +log4j = "2.21.0" sonarqube = "4.4.0.3356" spotless = "6.22.0" nexus-publish = "1.3.0" @@ -19,8 +19,7 @@ log4j-core = { module= "org.apache.logging.log4j:log4j-core", version.ref = "log jline = { module = "org.jline:jline", version = "3.23.0" } apache-commons-lang3 = { module = "org.apache.commons:commons-lang3", version = "3.13.0"} neo4j-ogm-core = { module = "org.neo4j:neo4j-ogm-core", version.ref = "neo4j"} -neo4j-ogm = { module = "org.neo4j:neo4j-ogm", version.ref = "neo4j"} -neo4j-ogm-bolt = { module = "org.neo4j:neo4j-ogm-bolt-driver", version.ref = "neo4j"} +neo4j-ogm-bolt-driver = { module = "org.neo4j:neo4j-ogm-bolt-driver", version.ref = "neo4j"} javaparser = { module = "com.github.javaparser:javaparser-symbol-solver-core", version = "3.25.4"} jackson = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version = "2.15.0"} @@ -49,7 +48,7 @@ nexus-publish-gradle = { module = "io.github.gradle-nexus:publish-plugin", versi [bundles] log4j = ["log4j-impl", "log4j-core"] -neo4j = ["neo4j-ogm-core", "neo4j-ogm", "neo4j-ogm-bolt"] +neo4j = ["neo4j-ogm-core", "neo4j-ogm-bolt-driver"] [plugins] kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin"}