diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/ExpressionBuilder.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/ExpressionBuilder.kt index bd6976f7d28..2dde9455f1e 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/ExpressionBuilder.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/ExpressionBuilder.kt @@ -413,6 +413,24 @@ fun MetadataProvider.newArraySubscriptionExpression( return node } +/** + * Creates a new [SliceExpression]. The [MetadataProvider] receiver will be used to fill different + * meta-data using [Node.applyMetadata]. Calling this extension function outside of Kotlin requires + * an appropriate [MetadataProvider], such as a [LanguageFrontend] as an additional prepended + * argument. + */ +@JvmOverloads +fun MetadataProvider.newSliceExpression( + code: String? = null, + rawNode: Any? = null +): SliceExpression { + val node = SliceExpression() + node.applyMetadata(this, EMPTY_NAME, rawNode, code, true) + + log(node) + return node +} + /** * Creates a new [ArrayCreationExpression]. The [MetadataProvider] receiver will be used to fill * different meta-data using [Node.applyMetadata]. Calling this extension function outside of Kotlin diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/ArraySubscriptionExpression.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/ArraySubscriptionExpression.kt index 0c3783fdefc..0a4f6eda277 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/ArraySubscriptionExpression.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/ArraySubscriptionExpression.kt @@ -50,8 +50,8 @@ class ArraySubscriptionExpression : Expression(), HasType.TypeListener, HasBase /** * The expression which represents the "subscription" or index on which the array is accessed. - * This can for example be a reference to another variable ([DeclaredReferenceExpression]) or a - * [Literal]. + * This can for example be a reference to another variable ([DeclaredReferenceExpression]), a + * [Literal] or a [SliceExpression]. */ @field:SubGraph("AST") var subscriptExpression: Expression = ProblemExpression("could not parse index expression") @@ -62,8 +62,18 @@ class ArraySubscriptionExpression : Expression(), HasType.TypeListener, HasBase override val operatorCode: String get() = "[]" + /** + * This helper function returns the subscript type of the [arrayType]. We have to differentiate + * here between to types of subscripts: + * * Slices (in the form of a [SliceExpression] return the same type as the array + * * Everything else (for example a [Literal] or any other [Expression] that is being evaluated) + * returns the de-referenced type + */ private fun getSubscriptType(arrayType: Type): Type { - return arrayType.dereference() + return when (subscriptExpression) { + is SliceExpression -> arrayType + else -> arrayType.dereference() + } } override fun typeChanged(src: HasType, root: MutableList, oldType: Type) { diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/SliceExpression.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/SliceExpression.kt new file mode 100644 index 00000000000..9c798753378 --- /dev/null +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/SliceExpression.kt @@ -0,0 +1,60 @@ +/* + * 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.graph.statements.expressions + +/** + * Represents the specification of a slice (e.g., of an array). Usually used in combination with an + * [ArraySubscriptionExpression] as the [ArraySubscriptionExpression.subscriptExpression]. + * + * Examples can be found in Go: + * ```go + * a := []int{1,2,3} + * b := a[:1] + * ``` + * + * or Python: + * ```python + * a = (1,2,3) + * b = a[:1] + * ``` + * + * Individual meaning of the slice indices might differ per language. + */ +class SliceExpression : Expression() { + + /** The lower bound of the slice. This index is usually *inclusive*. */ + var lowerBound: Expression? = null + + /** The upper bound of the slice. This index is usually *exclusive*. */ + var upperBound: Expression? = null + + /** + * Some languages offer a third value. The meaning depends completely on the language. For + * example, Python allows specifying a step, while Go allows to control the underlying array's + * capacity (not length). + */ + var third: Expression? = null +} diff --git a/cpg-core/src/testFixtures/kotlin/de/fraunhofer/aisec/cpg/TestUtils.kt b/cpg-core/src/testFixtures/kotlin/de/fraunhofer/aisec/cpg/TestUtils.kt index 16f28fa9770..0e2907a6065 100644 --- a/cpg-core/src/testFixtures/kotlin/de/fraunhofer/aisec/cpg/TestUtils.kt +++ b/cpg-core/src/testFixtures/kotlin/de/fraunhofer/aisec/cpg/TestUtils.kt @@ -31,10 +31,7 @@ import de.fraunhofer.aisec.cpg.graph.TypeManager import de.fraunhofer.aisec.cpg.graph.declarations.Declaration import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration 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.DeclaredReferenceExpression -import de.fraunhofer.aisec.cpg.graph.statements.expressions.Expression -import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.* import java.io.File import java.nio.file.Files import java.nio.file.Path @@ -297,10 +294,18 @@ object TestUtils { fun assertFullName(fqn: String, node: Node?, message: String? = null) { assertNotNull(node) - asserter.assertEquals(message, fqn, node.name.toString()) + assertEquals(fqn, node.name.toString(), message) } fun assertLocalName(localName: String, node: Node?, message: String? = null) { assertNotNull(node) - asserter.assertEquals(message, localName, node.name.localName) + assertEquals(localName, node.name.localName, message) +} + +/** + * Asserts that a) the expression in [expr] is a [Literal] and b) that it's value is equal to + * [expected]. + */ +fun assertLiteralValue(expected: T, expr: Expression?, message: String? = null) { + assertEquals(expected, assertIs>(expr).value, message) } diff --git a/cpg-language-go/src/main/golang/expressions.go b/cpg-language-go/src/main/golang/expressions.go index 8599f91e732..cad412b0282 100644 --- a/cpg-language-go/src/main/golang/expressions.go +++ b/cpg-language-go/src/main/golang/expressions.go @@ -59,6 +59,7 @@ type CastExpression Expression type NewExpression Expression type ArrayCreationExpression Expression type ArraySubscriptionExpression Expression +type SliceExpression Expression type ConstructExpression Expression type InitializerListExpression Expression type MemberCallExpression CallExpression @@ -205,6 +206,18 @@ func (r *ArraySubscriptionExpression) SetSubscriptExpression(e *Expression) { (*jnigi.ObjectRef)(r).CallMethod(env, "setSubscriptExpression", nil, (*jnigi.ObjectRef)(e).Cast(ExpressionClass)) } +func (s *SliceExpression) SetLowerBound(e *Expression) { + (*jnigi.ObjectRef)(s).CallMethod(env, "setLowerBound", nil, (*jnigi.ObjectRef)(e).Cast(ExpressionClass)) +} + +func (s *SliceExpression) SetUpperBound(e *Expression) { + (*jnigi.ObjectRef)(s).CallMethod(env, "setUpperBound", nil, (*jnigi.ObjectRef)(e).Cast(ExpressionClass)) +} + +func (s *SliceExpression) SetThird(e *Expression) { + (*jnigi.ObjectRef)(s).CallMethod(env, "setThird", nil, (*jnigi.ObjectRef)(e).Cast(ExpressionClass)) +} + func (c *ConstructExpression) AddArgument(e *Expression) { (*jnigi.ObjectRef)(c).CallMethod(env, "addArgument", nil, (*jnigi.ObjectRef)(e).Cast(ExpressionClass)) } diff --git a/cpg-language-go/src/main/golang/frontend/expression_builder.go b/cpg-language-go/src/main/golang/frontend/expression_builder.go index b74560714f0..e998abe53da 100644 --- a/cpg-language-go/src/main/golang/frontend/expression_builder.go +++ b/cpg-language-go/src/main/golang/frontend/expression_builder.go @@ -70,6 +70,10 @@ func (frontend *GoLanguageFrontend) NewArraySubscriptionExpression(fset *token.F return (*cpg.ArraySubscriptionExpression)(frontend.NewExpression("ArraySubscriptionExpression", fset, astNode)) } +func (frontend *GoLanguageFrontend) NewSliceExpression(fset *token.FileSet, astNode ast.Node) *cpg.SliceExpression { + return (*cpg.SliceExpression)(frontend.NewExpression("SliceExpression", fset, astNode)) +} + func (frontend *GoLanguageFrontend) NewConstructExpression(fset *token.FileSet, astNode ast.Node) *cpg.ConstructExpression { return (*cpg.ConstructExpression)(frontend.NewExpression("ConstructExpression", fset, astNode)) } @@ -104,6 +108,10 @@ func (frontend *GoLanguageFrontend) NewLiteral(fset *token.FileSet, astNode ast. value = value.Cast("java/lang/Object") } + if typ == nil { + panic("typ is nil") + } + return (*cpg.Literal)(frontend.NewExpression("Literal", fset, astNode, value, typ.Cast(cpg.TypeClass))) } diff --git a/cpg-language-go/src/main/golang/frontend/handler.go b/cpg-language-go/src/main/golang/frontend/handler.go index 08614cf8f6b..a54a9e2c71d 100644 --- a/cpg-language-go/src/main/golang/frontend/handler.go +++ b/cpg-language-go/src/main/golang/frontend/handler.go @@ -705,6 +705,8 @@ func (this *GoLanguageFrontend) handleExpr(fset *token.FileSet, expr ast.Expr) ( e = (*cpg.Expression)(this.handleStarExpr(fset, v)) case *ast.SelectorExpr: e = (*cpg.Expression)(this.handleSelectorExpr(fset, v)) + case *ast.SliceExpr: + e = (*cpg.Expression)(this.handleSliceExpr(fset, v)) case *ast.KeyValueExpr: e = (*cpg.Expression)(this.handleKeyValueExpr(fset, v)) case *ast.BasicLit: @@ -960,13 +962,40 @@ func (this *GoLanguageFrontend) handleCallExpr(fset *token.FileSet, callExpr *as return (*cpg.Expression)(c) } -func (this *GoLanguageFrontend) handleIndexExpr(fset *token.FileSet, indexExpr *ast.IndexExpr) *cpg.Expression { +func (this *GoLanguageFrontend) handleIndexExpr(fset *token.FileSet, indexExpr *ast.IndexExpr) *cpg.ArraySubscriptionExpression { a := this.NewArraySubscriptionExpression(fset, indexExpr) a.SetArrayExpression(this.handleExpr(fset, indexExpr.X)) a.SetSubscriptExpression(this.handleExpr(fset, indexExpr.Index)) - return (*cpg.Expression)(a) + return a +} + +// handleSliceExpr handles a [ast.SliceExpr], which is an extended version of +// [ast.IndexExpr]. We are modelling this as a combination of a +// [cpg.ArraySubscriptionExpression] that contains a [cpg.SliceExpression] as +// its subscriptExpression to share some code between this and an index +// expression. +func (this *GoLanguageFrontend) handleSliceExpr(fset *token.FileSet, sliceExpr *ast.SliceExpr) *cpg.ArraySubscriptionExpression { + a := this.NewArraySubscriptionExpression(fset, sliceExpr) + + a.SetArrayExpression(this.handleExpr(fset, sliceExpr.X)) + + // Build the slice expression + s := this.NewSliceExpression(fset, sliceExpr) + if sliceExpr.Low != nil { + s.SetLowerBound(this.handleExpr(fset, sliceExpr.Low)) + } + if sliceExpr.High != nil { + s.SetUpperBound(this.handleExpr(fset, sliceExpr.High)) + } + if sliceExpr.Max != nil { + s.SetThird(this.handleExpr(fset, sliceExpr.Max)) + } + + a.SetSubscriptExpression((*cpg.Expression)(s)) + + return a } func (this *GoLanguageFrontend) handleNewExpr(fset *token.FileSet, callExpr *ast.CallExpr) *cpg.Expression { @@ -1164,8 +1193,11 @@ func (this *GoLanguageFrontend) handleBasicLit(fset *token.FileSet, lit *ast.Bas value = cpg.NewDouble(f) t = cpg.TypeParser_createFrom("float64", lang) case token.IMAG: + // TODO + t = &cpg.UnknownType_getUnknown(lang).Type case token.CHAR: value = cpg.NewString(lit.Value) + t = cpg.TypeParser_createFrom("rune", lang) break } diff --git a/cpg-language-go/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/GoTypeCastingPass.kt b/cpg-language-go/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/GoTypeCastingPass.kt index ea3f09f4c24..4abfa350645 100644 --- a/cpg-language-go/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/GoTypeCastingPass.kt +++ b/cpg-language-go/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/GoTypeCastingPass.kt @@ -56,7 +56,7 @@ class GoTypeCastingPass : Pass() { override fun accept(t: TranslationResult) { scopeManager = t.scopeManager - var walker = SubgraphWalker.ScopedWalker(scopeManager) + val walker = SubgraphWalker.ScopedWalker(scopeManager) walker.registerHandler { _, parent, node -> // We are only interested in CallExpressions if (node is CallExpression) { @@ -78,7 +78,7 @@ class GoTypeCastingPass : Pass() { val language = parent.language ?: GoLanguage() // First, check if this is a built-in type - if (language.primitiveTypes.contains(callee.name.toString())) { + if (language.builtInTypes.contains(callee.name.toString())) { replaceCallWithCast(callee.name.toString(), language, parent, call) } else { // If not, then this could still refer to an existing type. We need to make sure diff --git a/cpg-language-go/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/ExpressionTest.kt b/cpg-language-go/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/ExpressionTest.kt index aeb41242cd3..ffd2970b0bc 100644 --- a/cpg-language-go/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/ExpressionTest.kt +++ b/cpg-language-go/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/golang/ExpressionTest.kt @@ -27,6 +27,8 @@ package de.fraunhofer.aisec.cpg.frontends.golang import de.fraunhofer.aisec.cpg.TestUtils import de.fraunhofer.aisec.cpg.assertFullName +import de.fraunhofer.aisec.cpg.assertLiteralValue +import de.fraunhofer.aisec.cpg.assertLocalName import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.bodyOrNull import de.fraunhofer.aisec.cpg.graph.byNameOrNull @@ -34,13 +36,9 @@ import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration import de.fraunhofer.aisec.cpg.graph.declarations.NamespaceDeclaration import de.fraunhofer.aisec.cpg.graph.declarations.VariableDeclaration import de.fraunhofer.aisec.cpg.graph.statements.DeclarationStatement -import de.fraunhofer.aisec.cpg.graph.statements.expressions.CastExpression -import de.fraunhofer.aisec.cpg.graph.statements.expressions.DeclaredReferenceExpression +import de.fraunhofer.aisec.cpg.graph.statements.expressions.* import java.nio.file.Path -import kotlin.test.Test -import kotlin.test.assertIs -import kotlin.test.assertNotNull -import kotlin.test.assertSame +import kotlin.test.* class ExpressionTest { @Test @@ -80,4 +78,66 @@ class ExpressionTest { val ignored = main.variables("_") ignored.forEach { assertIs(it.initializer) } } + + @Test + fun testSliceExpression() { + val topLevel = Path.of("src", "test", "resources", "golang") + val tu = + TestUtils.analyzeAndGetFirstTU( + listOf(topLevel.resolve("slices.go").toFile()), + topLevel, + true + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val a = tu.variables["a"] + assertNotNull(a) + assertLocalName("int[]", a.type) + + val b = tu.variables["b"] + assertNotNull(b) + assertLocalName("int[]", b.type) + + // TODO: actually this should be an assign, not the initializer + // [:1] + var slice = + assertIs( + assertIs(b.initializer).subscriptExpression + ) + assertNull(slice.lowerBound) + assertLiteralValue(1, slice.upperBound) + assertNull(slice.third) + + val c = tu.variables["c"] + assertNotNull(c) + assertLocalName("int[]", c.type) + + // [1:] + slice = assertIs(assertIs(c.initializer).subscriptExpression) + assertLiteralValue(1, slice.lowerBound) + assertNull(slice.upperBound) + assertNull(slice.third) + + val d = tu.variables["d"] + assertNotNull(d) + assertLocalName("int[]", d.type) + + // [0:1] + slice = assertIs(assertIs(d.initializer).subscriptExpression) + assertLiteralValue(0, slice.lowerBound) + assertLiteralValue(1, slice.upperBound) + assertNull(slice.third) + + val e = tu.variables["e"] + assertNotNull(e) + assertLocalName("int[]", e.type) + + // [0:1:1] + slice = assertIs(assertIs(e.initializer).subscriptExpression) + assertLiteralValue(0, slice.lowerBound) + assertLiteralValue(1, slice.upperBound) + assertLiteralValue(1, slice.third) + } } diff --git a/cpg-language-go/src/test/resources/golang/slices.go b/cpg-language-go/src/test/resources/golang/slices.go new file mode 100644 index 00000000000..a8483756b1f --- /dev/null +++ b/cpg-language-go/src/test/resources/golang/slices.go @@ -0,0 +1,17 @@ +package main + +import "fmt" + +func main() { + a := []int{1,2,3} + // [1] + b := a[:1] + // [2, 3] + c := a[1:] + // [1] + d := a[0:1] + // [1] + e := a[0:1:1] + + fmt.Printf("%v %v %v %v %v", a, b, c, d, e) +} \ No newline at end of file diff --git a/cpg-language-go/src/test/resources/log4j2.xml b/cpg-language-go/src/test/resources/log4j2.xml index 747860628a4..5b73082e2c0 100644 --- a/cpg-language-go/src/test/resources/log4j2.xml +++ b/cpg-language-go/src/test/resources/log4j2.xml @@ -6,7 +6,7 @@ - +