Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing DynamicType and removing usage of AutoType from python #1668

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -236,20 +236,27 @@ abstract class Language<T : LanguageFrontend<*, *>> : Node {
open fun shouldPropagateType(hasType: HasType, srcType: Type): Boolean {
val nodeType = hasType.type

// We only want to add certain types, in case we have a numeric type
if (nodeType is NumericType) {
// We do not allow to propagate non-numeric types into numeric types
return if (srcType !is NumericType) {
return when {
// We only want to add certain types, in case we have a numeric type
nodeType is NumericType -> {
// We do not allow to propagate non-numeric types into numeric types
if (srcType !is NumericType) {
false
} else {
val srcWidth = srcType.bitWidth
val lhsWidth = nodeType.bitWidth
// Do not propagate anything if the new type is too big for the current type.
return !(lhsWidth != null && srcWidth != null && lhsWidth < srcWidth)
}
}
// We do not want to propagate a dynamic type
srcType is DynamicType -> {
false
} else {
val srcWidth = srcType.bitWidth
val lhsWidth = nodeType.bitWidth
// Do not propagate anything if the new type is too big for the current type.
return !(lhsWidth != null && srcWidth != null && lhsWidth < srcWidth)
}
else -> {
true
}
}

return true
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ fun LanguageProvider.incompleteType(): Type {
return IncompleteType(this.language)
}

fun LanguageProvider.dynamicType(): Type {
return DynamicType(this.language)
}

/** Returns a [PointerType] that describes an array reference to the current type. */
context(ContextProvider)
fun Type.array(): Type {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,21 @@ 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
import de.fraunhofer.aisec.cpg.graph.types.DynamicType
import de.fraunhofer.aisec.cpg.graph.types.FunctionType.Companion.buildSignature
import de.fraunhofer.aisec.cpg.graph.types.FunctionType.Companion.computeType
import de.fraunhofer.aisec.cpg.graph.types.HasType
import de.fraunhofer.aisec.cpg.graph.types.Type
import de.fraunhofer.aisec.cpg.persistence.DoNotPersist
import java.util.*
import org.apache.commons.lang3.builder.ToStringBuilder
import org.neo4j.ogm.annotation.Relationship

/** Represents the declaration or definition of a function. */
open class FunctionDeclaration : ValueDeclaration(), DeclarationHolder, EOGStarterHolder {
open class FunctionDeclaration :
ValueDeclaration(), DeclarationHolder, EOGStarterHolder, HasType.TypeObserver {
@Relationship("BODY") var bodyEdge = astOptionalEdgeOf<Statement>()
/** The function body. Usualfly a [Block]. */
/** The function body. Usually a [Block]. */
var body by unwrapping(FunctionDeclaration::bodyEdge)

/** The list of function parameters. */
Expand Down Expand Up @@ -97,18 +102,7 @@ open class FunctionDeclaration : ValueDeclaration(), DeclarationHolder, EOGStart
}

val signature: String
get() =
name.localName +
parameters.joinToString(COMMA + WHITESPACE, BRACKET_LEFT, BRACKET_RIGHT) {
it.type.typeName
} +
(if (returnTypes.size == 1) {
returnTypes.first().typeName
} else {
returnTypes.joinToString(COMMA + WHITESPACE, BRACKET_LEFT, BRACKET_RIGHT) {
it.typeName
}
})
get() = buildSignature(this, returnTypes)

fun isOverrideCandidate(other: FunctionDeclaration): Boolean {
return other.name.localName == name.localName &&
Expand Down Expand Up @@ -200,6 +194,27 @@ open class FunctionDeclaration : ValueDeclaration(), DeclarationHolder, EOGStart
return this.body?.cyclomaticComplexity ?: 0
}

override fun typeChanged(newType: Type, src: HasType) {
// We cannot really change the "type" of a function declaration, we want to stick to the
// assigned type
}

override fun assignedTypeChanged(assignedTypes: Set<Type>, src: HasType) {
// We want to propagate the assigned types to the return type of the function and adjust the
// function's type accordingly, but we only do this for dynamic types. And we only support
// one return type for now.
if (returnTypes.singleOrNull() !is DynamicType) {
return
}

// Build new function types out of our function declaration and the assigned types
var returnFuncTypes =
assignedTypes.map { computeType(this, returnTypes = listOf(it)) }.toSet()

// And assign it us
addAssignedTypes(returnFuncTypes)
}

companion object {
const val WHITESPACE = " "
const val BRACKET_LEFT = "("
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,31 @@
package de.fraunhofer.aisec.cpg.graph.statements

import de.fraunhofer.aisec.cpg.graph.ArgumentHolder
import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration
import de.fraunhofer.aisec.cpg.graph.edges.ast.astEdgesOf
import de.fraunhofer.aisec.cpg.graph.edges.unwrapping
import de.fraunhofer.aisec.cpg.graph.firstScopeParentOrNull
import de.fraunhofer.aisec.cpg.graph.scopes.FunctionScope
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Expression
import java.util.Objects
import org.apache.commons.lang3.builder.ToStringBuilder
import org.neo4j.ogm.annotation.Relationship

/** Represents a statement that returns out of the current function. */
class ReturnStatement : Statement(), ArgumentHolder {
@Relationship(value = "RETURN_VALUES") var returnValueEdges = astEdgesOf<Expression>()
@Relationship(value = "RETURN_VALUES")
var returnValueEdges =
astEdgesOf<Expression>(
onAdd = { edge ->
val func =
(this.scope as? FunctionScope
?: this.scope?.firstScopeParentOrNull<FunctionScope>())
?.astNode as? FunctionDeclaration
if (func != null) {
edge.end.registerTypeObserver(func)
}
}
)

/** The expression whose value will be returned. */
var returnValues by unwrapping(ReturnStatement::returnValueEdges)
Expand All @@ -50,7 +65,7 @@ class ReturnStatement : Statement(), ArgumentHolder {
return returnValues.singleOrNull()
}
set(value) {
value?.let { returnValues = mutableListOf(it) }
value?.let { returnValueEdges.resetTo(listOf(it)) }
}

override fun toString(): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,15 @@ open class CallExpression :
}

override fun assignedTypeChanged(assignedTypes: Set<Type>, src: HasType) {
// Nothing to do
// Propagate assigned func types from the function declaration to the call expression
val assignedFuncTypes = assignedTypes.filterIsInstance<FunctionType>()
assignedFuncTypes.forEach {
if (it.returnTypes.size == 1) {
addAssignedType(it.returnTypes.single())
} else if (it.returnTypes.size > 1) {
addAssignedType(TupleType(it.returnTypes))
}
}
}

override fun toString(): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,17 @@ import de.fraunhofer.aisec.cpg.graph.unknownType

/**
* This type represents a [Type] that uses auto-inference (usually from an initializer) to determine
* it's actual type. It is commonly used in dynamically typed languages or in languages that have a
* special keyword, such as `auto` in C++.
* its actual type. It is commonly used in languages that have a special keyword, such as `auto` in
* C++.
*
* Note: This is intentionally a distinct type and not the [UnknownType].
* Things to consider:
* 1) This is intentionally a distinct type and not the [UnknownType]. The type is known to the
* compiler (or to us) at some point, e.g., after an assignment, but it is not specifically
* specified in the source-code.
* 2) This should not be used to languages that have dynamic types. Once auto-type who was assigned
* to [Expression.type] is "resolved", it should be replaced by the actual type that it
* represents. Contrary to that, a [DynamicType] can change its internal type representation at
* any point, e.g., after the next assignment.
*/
class AutoType(override var language: Language<*>) : Type("auto", language) {
override fun reference(pointer: PointerType.PointerOrigin?): Type {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2024, 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.types

import de.fraunhofer.aisec.cpg.frontends.Language
import de.fraunhofer.aisec.cpg.graph.dynamicType

/**
* This type represents a [Type] that is dynamically determined at run-time. This is used for a
* [Language], which has dynamic runtime typing, such as Python or Java/TypeScript.
*/
class DynamicType(override var language: Language<*>) : Type("dynamic", language) {
override fun reference(pointer: PointerType.PointerOrigin?): Type {
return dynamicType()
}

override fun dereference(): Type {
return dynamicType()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ package de.fraunhofer.aisec.cpg.graph.types
import de.fraunhofer.aisec.cpg.frontends.Language
import de.fraunhofer.aisec.cpg.frontends.TranslationException
import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration
import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration.Companion.BRACKET_LEFT
import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration.Companion.BRACKET_RIGHT
import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration.Companion.COMMA
import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration.Companion.WHITESPACE
import de.fraunhofer.aisec.cpg.graph.unknownType

/**
Expand Down Expand Up @@ -64,17 +68,33 @@ constructor(
* This helper function computes a [FunctionType] out of an existing [FunctionDeclaration].
*/
@JvmStatic
fun computeType(func: FunctionDeclaration): FunctionType {
fun computeType(
func: FunctionDeclaration,
returnTypes: List<Type> = func.returnTypes.toList(),
): FunctionType {
val type =
FunctionType(
func.signature,
buildSignature(func, returnTypes),
func.parameters.map { it.type },
func.returnTypes.toList(),
returnTypes,
func.language,
)

val c = func.ctx ?: throw TranslationException("context not available")
return c.typeManager.registerType(type)
}

fun buildSignature(func: FunctionDeclaration, returnTypes: List<Type>): String =
func.name.localName +
func.parameters.joinToString(COMMA + WHITESPACE, BRACKET_LEFT, BRACKET_RIGHT) {
it.type.typeName
} +
(if (returnTypes.size == 1) {
returnTypes.first().typeName
} else {
returnTypes.joinToString(COMMA + WHITESPACE, BRACKET_LEFT, BRACKET_RIGHT) {
it.typeName
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -523,8 +523,19 @@ open class SymbolResolver(ctx: TranslationContext) : ComponentPass(ctx) {
)
val language = source.language

// Set the start scope. This can either be the call's scope or a scope specified in an FQN
val extractedScope = ctx.scopeManager.extractScope(source, language, source.scope)
// Set the start scope. This can either be the call's scope or a scope specified in an FQN.
// If our base is a dynamic or unknown type, we can skip the scope extraction because it
// will always
// fail
val extractedScope =
if (
source is MemberCallExpression &&
(source.base?.type is DynamicType || source.base?.type is UnknownType)
) {
ScopeManager.ScopeExtraction(null, Name(""))
} else {
ctx.scopeManager.extractScope(source, language, source.scope)
}

// If we could not extract the scope (even though one was specified), we can only return an
// empty result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,13 @@ class TypeTest {
assertLocalName("myNewClass***", type)
}
}

@Test
fun testDynamicType() {
with(TestLanguageFrontend()) {
var type = dynamicType()
assertIs<DynamicType>(type.reference(PointerType.PointerOrigin.ARRAY))
assertIs<DynamicType>(type.dereference())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ class ExpressionHandler(frontend: PythonLanguageFrontend) :
is Float,
is Double -> primitiveType("float")
else -> {
autoType()
unknownType()
}
}
return newLiteral(node.value, type = tpe, rawNode = node)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
import de.fraunhofer.aisec.cpg.graph.HasOverloadedOperation
import de.fraunhofer.aisec.cpg.graph.Name
import de.fraunhofer.aisec.cpg.graph.Node
import de.fraunhofer.aisec.cpg.graph.autoType
import de.fraunhofer.aisec.cpg.graph.declarations.ParameterDeclaration
import de.fraunhofer.aisec.cpg.graph.primitiveType
import de.fraunhofer.aisec.cpg.graph.scopes.Symbol
import de.fraunhofer.aisec.cpg.graph.statements.expressions.BinaryOperator
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Reference
Expand Down Expand Up @@ -181,14 +181,13 @@
)

override fun propagateTypeOfBinaryOperation(operation: BinaryOperator): Type {
val autoType = autoType()
if (
operation.operatorCode == "/" &&
operation.lhs.type is NumericType &&
operation.rhs.type is NumericType
) {
// In Python, the / operation automatically casts the result to a float
return getSimpleTypeOf("float") ?: autoType
return primitiveType("float")
} else if (
operation.operatorCode == "//" &&
operation.lhs.type is NumericType &&
Expand All @@ -197,9 +196,9 @@
return if (operation.lhs.type is IntegerType && operation.rhs.type is IntegerType) {
// In Python, the // operation keeps the type as an int if both inputs are integers
// or casts it to a float otherwise.
getSimpleTypeOf("int") ?: autoType
primitiveType("int")

Check warning on line 199 in cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/python/PythonLanguage.kt

View check run for this annotation

Codecov / codecov/patch

cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/python/PythonLanguage.kt#L199

Added line #L199 was not covered by tests
} else {
getSimpleTypeOf("float") ?: autoType
primitiveType("float")

Check warning on line 201 in cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/python/PythonLanguage.kt

View check run for this annotation

Codecov / codecov/patch

cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/python/PythonLanguage.kt#L201

Added line #L201 was not covered by tests
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ class PythonLanguageFrontend(ctx: TranslationContext, language: Language<PythonL
override fun typeOf(type: Python.AST.AST?): Type {
return when (type) {
null -> {
// No type information -> we return an autoType to infer things magically
autoType()
// No type information -> we return a dynamic type to infer things magically
dynamicType()
}

is Python.AST.Name -> {
Expand Down
Loading
Loading