All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.fraunhofer.aisec.cpg.frontends.golang.GoLanguageFrontend.kt Maven / Gradle / Ivy

There is a newer version: 8.3.0
Show newest version
/*
 * Copyright (c) 2021, 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.frontends.golang

import de.fraunhofer.aisec.cpg.TranslationContext
import de.fraunhofer.aisec.cpg.frontends.Language
import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend
import de.fraunhofer.aisec.cpg.frontends.SupportsParallelParsing
import de.fraunhofer.aisec.cpg.frontends.TranslationException
import de.fraunhofer.aisec.cpg.frontends.golang.GoStandardLibrary.Modfile
import de.fraunhofer.aisec.cpg.frontends.golang.GoStandardLibrary.Parser
import de.fraunhofer.aisec.cpg.graph.*
import de.fraunhofer.aisec.cpg.graph.declarations.DeclarationSequence
import de.fraunhofer.aisec.cpg.graph.declarations.RecordDeclaration
import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration
import de.fraunhofer.aisec.cpg.graph.newNamespaceDeclaration
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Literal
import de.fraunhofer.aisec.cpg.graph.types.*
import de.fraunhofer.aisec.cpg.graph.unknownType
import de.fraunhofer.aisec.cpg.helpers.Util
import de.fraunhofer.aisec.cpg.passes.EvaluationOrderGraphPass
import de.fraunhofer.aisec.cpg.passes.GoEvaluationOrderGraphPass
import de.fraunhofer.aisec.cpg.passes.GoExtraPass
import de.fraunhofer.aisec.cpg.passes.order.RegisterExtraPass
import de.fraunhofer.aisec.cpg.passes.order.ReplacePass
import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation
import de.fraunhofer.aisec.cpg.sarif.Region
import java.io.File
import java.net.URI

/**
 * A language frontend for the [GoLanguage]. It makes use the internal
 * [go/ast](https://pkg.go.dev/go/ast) package of the Go runtime to parse the AST of a Go program.
 * We make use of JNA to call a dynamic library which exports C function wrappers around the Go API.
 * This is needed because we cannot directly export Go structs and pointers to C.
 */
@SupportsParallelParsing(false)
@RegisterExtraPass(GoExtraPass::class)
@ReplacePass(
    lang = GoLanguage::class,
    old = EvaluationOrderGraphPass::class,
    with = GoEvaluationOrderGraphPass::class
)
class GoLanguageFrontend(language: Language, ctx: TranslationContext) :
    LanguageFrontend(language, ctx) {

    private var currentFileSet: GoStandardLibrary.Ast.FileSet? = null
    private var currentModule: GoStandardLibrary.Modfile.File? = null
    private var commentMap: GoStandardLibrary.Ast.CommentMap? = null
    var currentFile: GoStandardLibrary.Ast.File? = null

    var isDependency: Boolean = false

    val declarationHandler = DeclarationHandler(this)
    val specificationHandler = SpecificationHandler(this)
    var statementHandler = StatementHandler(this)
    var expressionHandler = ExpressionHandler(this)

    /**
     * This helper class contains values needed to properly decide in which state const declaration
     * / specifications are in.
     */
    class DeclarationContext {
        /**
         * The current value of `iota`. This needs to be reset for each
         * [GoStandardLibrary.Ast.GenDecl] and incremented for each observed
         * [GoStandardLibrary.Ast.ValueSpec].
         */
        var iotaValue = -1

        /**
         * The current initializers in a list representing the different "columns". For example in
         * the following code:
         * ```go
         * const (
         *   a, b = 1, 2
         *   c, d
         *   e, f = 4, 5
         * )
         * ```
         *
         * The current list of initializers would first be (`1`,`2`) until a new set of initializers
         * is declared in the last spec. The key corresponds to the "column" of the variable
         * (a=0,b=1).
         */
        var constInitializers = mutableMapOf()

        /** The current const type, which is valid until a new initializer is present */
        var constType: Type? = null

        /** The current [GoStandardLibrary.Ast.GenDecl] that is being processed. */
        var currentDecl: GoStandardLibrary.Ast.GenDecl? = null
    }

    /**
     * The current [DeclarationContext]. This is somewhat of a workaround since we cannot properly
     * communicate state between different handlers. However, because *within* a [LanguageFrontend],
     * everything is parsed sequentially according to AST order, we can safely use this context
     * here.
     */
    var declCtx = DeclarationContext()

    @Throws(TranslationException::class)
    override fun parse(file: File): TranslationUnitDeclaration {
        if (!shouldBeBuild(file, ctx.config.symbols)) {
            log.debug(
                "Ignoring the contents of {} because of missing build tags or different GOOS/GOARCH.",
                file
            )
            return newTranslationUnitDeclaration(file.name)
        }

        val dependency =
            ctx.config.includePaths.firstOrNull {
                file.absolutePath.contains(it.toAbsolutePath().toString())
            }

        // Make sure, that our top level is set either way
        val topLevel =
            // If this file is part of an include, we set the top level to the root of the include
            when {
                dependency != null -> {
                    isDependency = true
                    dependency.toFile()
                }
                config.topLevel != null -> config.topLevel
                else -> file.parentFile
            }!!

        val std = GoStandardLibrary.INSTANCE

        // Try to parse a possible go.mod
        val goModFile = topLevel.resolve("go.mod")
        if (goModFile.exists()) {
            currentModule = Modfile.parse(goModFile.absolutePath, goModFile.readText())
        }

        val fset = std.NewFileSet()
        val f = Parser.parseFile(fset, file.absolutePath)

        this.commentMap = std.NewCommentMap(fset, f, f.comments)

        currentFile = f
        currentFileSet = fset

        val tu = newTranslationUnitDeclaration(file.absolutePath, rawNode = f)
        scopeManager.resetToGlobal(tu)
        currentTU = tu

        for (spec in f.imports) {
            val import = specificationHandler.handle(spec)
            scopeManager.addDeclaration(import)
        }

        val p = newNamespaceDeclaration(f.name.name)
        scopeManager.enterScope(p)

        try {
            // we need to construct the package "path" (e.g. "encoding/json") out of the
            // module path as well as the current directory in relation to the topLevel
            var packagePath = file.parentFile.relativeTo(topLevel)

            // If we are in a module, we need to prepend the module path to it. There is an
            // exception if we are in the "std" module, which represents the standard library
            val modulePath = currentModule?.module?.mod?.path
            if (modulePath != null && modulePath != "std") {
                packagePath = File(modulePath).resolve(packagePath)
            }

            p.path = packagePath.path
        } catch (ex: IllegalArgumentException) {
            log.error(
                "Could not relativize package path to top level. Cannot set package path.",
                ex
            )
        }

        for (decl in f.decls) {
            // Retrieve all top level declarations. One "Decl" could potentially
            // contain multiple CPG declarations.
            val declaration = declarationHandler.handle(decl)
            if (declaration is DeclarationSequence) {
                declaration.declarations.forEach { scopeManager.addDeclaration(it) }
            } else {
                scopeManager.addDeclaration(declaration)
            }
        }

        scopeManager.leaveScope(p)

        scopeManager.addDeclaration(p)

        return tu
    }

    override fun typeOf(type: GoStandardLibrary.Ast.Expr): Type {
        val type =
            when (type) {
                is GoStandardLibrary.Ast.Ident -> {
                    val name: String =
                        if (isBuiltinType(type.name)) {
                            // Definitely not an FQN type
                            type.name
                        } else {
                            // FQN'ize this name (with the current file)
                            "${currentFile?.name?.name}.${type.name}" // this.File.Name.Name
                        }

                    objectType(name)
                }
                is GoStandardLibrary.Ast.SelectorExpr -> {
                    // This is a FQN type
                    val baseName = (type.x as? GoStandardLibrary.Ast.Ident)?.name?.let { Name(it) }

                    return objectType(Name(type.sel.name, baseName))
                }
                is GoStandardLibrary.Ast.ArrayType -> {
                    return typeOf(type.elt).array()
                }
                is GoStandardLibrary.Ast.ChanType -> {
                    // Handle them similar to a map type (see below)
                    return objectType("chan", listOf(typeOf(type.value)))
                }
                is GoStandardLibrary.Ast.FuncType -> {
                    val paramTypes =
                        type.params.list
                            .flatMap { field ->
                                // Because we can have unnamed parameters or multiple parameters
                                // declared at once, we need to expand the list of types according
                                // to the list of names
                                if (field.names.isEmpty()) {
                                    listOf(field.type)
                                } else {
                                    field.names.map { field.type }
                                }
                            }
                            .map { fieldTypeOf(it).first }
                    val returnTypes = type.results?.list?.map { typeOf(it.type) } ?: listOf()
                    val name = funcTypeName(paramTypes, returnTypes)

                    FunctionType(name, paramTypes, returnTypes, this.language)
                }
                is GoStandardLibrary.Ast.IndexExpr -> {
                    // A go type constraint, aka generic
                    val baseType = typeOf(type.x)
                    val generics = listOf(typeOf(type.index))
                    objectType(baseType.name, generics)
                }
                is GoStandardLibrary.Ast.IndexListExpr -> {
                    // A go type constraint, aka generic with multiple types
                    val baseType = typeOf(type.x)
                    val generics = type.indices.map { typeOf(it) }
                    objectType(baseType.name, generics)
                }
                is GoStandardLibrary.Ast.StructType -> {
                    // Go allows to use anonymous structs as type. This is something we cannot model
                    // properly in the CPG yet. In order to at least deal with this partially, we
                    // construct a ObjectType and put the fields and their types into the type.
                    // This will result in something like `struct{name string; args util.args; want
                    // string}`
                    val parts =
                        type.fields.list.map { field ->
                            var desc = ""
                            // Name can be optional, if its embedded
                            field.names.getOrNull(0)?.let { desc += it }
                            desc += " "
                            desc += fieldTypeOf(field.type).first.name
                            desc
                        }

                    val name = parts.joinToString("; ", "struct{", "}")

                    // Create an anonymous struct, this will add it to the scope manager. This is
                    // somewhat duplicate, but the easiest for now. We need to create it in the
                    // global
                    // scope to avoid namespace issues
                    var record =
                        scopeManager.withScope(scopeManager.globalScope) {
                            specificationHandler.buildRecordDeclaration(type, name)
                        }

                    record.toType()
                }
                is GoStandardLibrary.Ast.InterfaceType -> {
                    // Go allows to use anonymous interface as type. This is something we cannot
                    // model
                    // properly in the CPG yet. In order to at least deal with this partially, we
                    // construct a ObjectType and put the methods and their types into the type.

                    // In the easiest case this is the empty interface `interface{}`, which we then
                    // consider to be the "any" type. `any` is actually a type alias for
                    // `interface{}`,
                    // but in modern Go `any` is preferred.
                    if (type.methods.list.isEmpty()) {
                        return primitiveType("any")
                    }

                    val parts =
                        type.methods.list.map { method ->
                            var desc = ""
                            // Name can be optional, if its embedded
                            method.names.getOrNull(0)?.let { desc += it }
                            // the function type has a weird "func" prefix, which we do not want
                            desc += typeOf(method.type).name.toString().removePrefix("func")
                            desc
                        }

                    objectType(parts.joinToString("; ", "interface{", "}"))
                }
                is GoStandardLibrary.Ast.MapType -> {
                    // We cannot properly represent Go's built-in map types, yet so we have
                    // to make a shortcut here and represent it as a Java-like map type.
                    return objectType("map", listOf(typeOf(type.key), typeOf(type.value)))
                }
                is GoStandardLibrary.Ast.StarExpr -> {
                    typeOf(type.x).pointer()
                }
                else -> {
                    Util.warnWithFileLocation(
                        this,
                        type,
                        log,
                        "Not parsing type of type ${type.goType} yet"
                    )
                    unknownType()
                }
            }

        return typeManager.registerType(typeManager.resolvePossibleTypedef(type, scopeManager))
    }

    /**
     * A quick helper function to retrieve the type of a field, to check for possible variadic
     * arguments.
     */
    internal fun fieldTypeOf(
        paramType: GoStandardLibrary.Ast.Expr,
    ): Pair {
        var variadic = false
        val type =
            if (paramType is GoStandardLibrary.Ast.Ellipsis) {
                variadic = true
                typeOf(paramType.elt).array()
            } else {
                typeOf(paramType)
            }
        return Pair(type, variadic)
    }

    private fun isBuiltinType(name: String): Boolean {
        return language.primitiveTypeNames.contains(name)
    }

    override fun codeOf(astNode: GoStandardLibrary.Ast.Node): String? {
        return currentFileSet?.code(astNode)
    }

    override fun locationOf(astNode: GoStandardLibrary.Ast.Node): PhysicalLocation? {
        val start = currentFileSet?.position(astNode.pos) ?: return null
        val end = currentFileSet?.position(astNode.end) ?: return null
        val url = currentFileSet?.fileName(astNode.pos)?.let { URI(it) } ?: return null

        return PhysicalLocation(url, Region(start.line, start.column, end.line, end.column))
    }

    override fun setComment(node: Node, astNode: GoStandardLibrary.Ast.Node) {
        // Since we are potentially calling this function more than once on a node because of the
        // way go is structured (one decl can contain multiple specs), we need to make sure, that we
        // are not "overriding" more specific comments with more global ones.
        if (node.comment == null) {
            val comment = this.commentMap?.comment(astNode)
            node.comment = comment
        }
    }

    companion object {
        /**
         * All possible goos values. See
         * https://github.com/golang/go/blob/release-branch.go1.21/src/go/build/syslist.go#L11
         */
        val goosValues =
            listOf(
                "aix",
                "android",
                "darwin",
                "dragonfly",
                "freebsd",
                "hurd",
                "illumos",
                "ios",
                "js",
                "linux",
                "nacl",
                "netbsd",
                "openbsd",
                "plan9",
                "solaris",
                "wasip1",
                "windows",
                "zos"
            )

        /**
         * All possible architecture values. See
         * https://github.com/golang/go/blob/release-branch.go1.21/src/go/build/syslist.go#L54
         */
        val goarchValues =
            listOf(
                "386",
                "amd64",
                "arm",
                "arm64",
                "loong64",
                "mips",
                "mips64",
                "mips64le",
                "mipsle",
                "ppc64",
                "ppc64le",
                "riscv64",
                "s390x"
            )
    }
}

val Type?.underlyingType: Type?
    get() {
        return (this as? ObjectType)?.recordDeclaration?.superClasses?.singleOrNull()
    }

val Type?.isOverlay: Boolean
    get() {
        return this is ObjectType && this.recordDeclaration?.kind == "overlay"
    }

val Type.isInterface: Boolean
    get() {
        return this is ObjectType && this.recordDeclaration?.kind == "interface"
    }

val Type.isMap: Boolean
    get() {
        return this is ObjectType && this.name.localName == "map"
    }

val Type.isChannel: Boolean
    get() {
        return this is ObjectType && this.name.localName == "chan"
    }

val HasType?.isNil: Boolean
    get() {
        return this is Literal<*> && this.name.localName == "nil"
    }

/**
 * This function produces a Go-style function type name such as `func(int, string) string` or
 * `func(int) (error, string)`
 */
fun funcTypeName(paramTypes: List, returnTypes: List): String {
    val rn = mutableListOf()
    val pn = mutableListOf()

    for (t in paramTypes) {
        pn += t.name.toString()
    }

    for (t in returnTypes) {
        rn += t.name.toString()
    }

    val rs =
        if (returnTypes.size > 1) {
            rn.joinToString(", ", prefix = " (", postfix = ")")
        } else if (returnTypes.isNotEmpty()) {
            rn.joinToString(", ", prefix = " ")
        } else {
            ""
        }

    return pn.joinToString(", ", prefix = "func(", postfix = ")$rs")
}

val RecordDeclaration.embeddedStructs: List
    get() {
        return this.fields
            .filter { "embedded" in it.modifiers }
            .mapNotNull { it.type.root.recordDeclaration }
    }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy