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 36304dea03..72d09e4d6a 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 @@ -35,7 +35,6 @@ import de.fraunhofer.aisec.cpg.graph.types.FunctionPointerType import de.fraunhofer.aisec.cpg.graph.types.IncompleteType import de.fraunhofer.aisec.cpg.graph.types.Type import de.fraunhofer.aisec.cpg.helpers.Util -import de.fraunhofer.aisec.cpg.passes.SymbolResolver import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation import java.util.* import java.util.function.Predicate 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 e12386d36f..6192a7f130 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 @@ -39,6 +39,7 @@ import de.fraunhofer.aisec.cpg.graph.statements.expressions.BinaryOperator import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression import de.fraunhofer.aisec.cpg.graph.types.* import de.fraunhofer.aisec.cpg.graph.unknownType +import de.fraunhofer.aisec.cpg.passes.SymbolResolver import java.io.File import kotlin.reflect.KClass import kotlin.reflect.full.primaryConstructor diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageTraits.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageTraits.kt index b7a66ac6bf..beb36b3661 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageTraits.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageTraits.kt @@ -29,6 +29,8 @@ import de.fraunhofer.aisec.cpg.ScopeManager import de.fraunhofer.aisec.cpg.TranslationContext import de.fraunhofer.aisec.cpg.graph.HasOperatorCode import de.fraunhofer.aisec.cpg.graph.HasOverloadedOperation +import de.fraunhofer.aisec.cpg.graph.LanguageProvider +import de.fraunhofer.aisec.cpg.graph.Name import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration import de.fraunhofer.aisec.cpg.graph.declarations.RecordDeclaration import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration @@ -235,6 +237,21 @@ interface HasOperatorOverloading : LanguageTrait { * the name of the function. */ val overloadedOperatorNames: Map, String>, Symbol> + + /** + * Returns the matching operator code for [name] in [overloadedOperatorNames]. While + * [overloadedOperatorNames] can have multiple entries for a single operator code (e.g. to + * differentiate between unary and binary ops), we only ever allow one distinct operator code + * for a specific symbol. If non such distinct operator code is found, null is returned. + */ + fun operatorCodeFor(name: Symbol): String? { + return overloadedOperatorNames + .filterValues { it == name } + .keys + .map { it.second } + .distinct() + .singleOrNull() + } } /** @@ -246,3 +263,23 @@ inline infix fun KClass.of( ): Pair, String> { return Pair(T::class, operatorCode) } + +/** Checks whether the name for a function (as [CharSequence]) is a known operator name. */ +context(LanguageProvider) +val CharSequence.isKnownOperatorName: Boolean + get() { + val language = language + if (language !is HasOperatorOverloading) { + return false + } + + // If this is a parsed name, we only are interested in the local name + val name = + if (this is Name) { + this.localName + } else { + this + } + + return language.overloadedOperatorNames.containsValue(name) + } diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/ArgumentHolder.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/ArgumentHolder.kt index a15003b21f..eafe01c47a 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/ArgumentHolder.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/ArgumentHolder.kt @@ -53,6 +53,10 @@ interface ArgumentHolder : Holder { return false } + override fun replace(old: Expression, new: Expression): Boolean { + return replaceArgument(old, new) + } + /** * Replaces the existing argument specified in [old] with the one in [new]. Implementation how * to do that might be specific to the argument holder. diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/DeclarationBuilder.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/DeclarationBuilder.kt index e24ce7ff18..c2e429edfd 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/DeclarationBuilder.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/DeclarationBuilder.kt @@ -107,7 +107,7 @@ fun MetadataProvider.newOperatorDeclaration( operatorCode: String, recordDeclaration: RecordDeclaration? = null, rawNode: Any? = null -): MethodDeclaration { +): OperatorDeclaration { val node = OperatorDeclaration() node.applyMetadata(this, name, rawNode, defaultNamespace = recordDeclaration?.name) 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 8b49b05c84..af68b0f6e8 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 @@ -280,6 +280,30 @@ fun MetadataProvider.newCallExpression( return node } +/** + * Creates a new [MemberCallExpression]. 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.newOperatorCallExpression( + operatorCode: String, + callee: Expression?, + rawNode: Any? = null +): OperatorCallExpression { + val node = OperatorCallExpression() + node.applyMetadata(this, operatorCode, rawNode) + + node.operatorCode = operatorCode + if (callee != null) { + node.callee = callee + } + + log(node) + return node +} + /** * Creates a new [MemberCallExpression]. 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/Extensions.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Extensions.kt index a36e83367a..6b73dc5c29 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Extensions.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Extensions.kt @@ -504,6 +504,10 @@ val Node?.nodes: List val Node?.calls: List get() = this.allChildren() +/** Returns all [OperatorCallExpression] children in this graph, starting with this [Node]. */ +val Node?.operatorCalls: List + get() = this.allChildren() + /** Returns all [MemberCallExpression] children in this graph, starting with this [Node]. */ val Node?.mcalls: List get() = this.allChildren() diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Holder.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Holder.kt index ad44450b1c..7192071242 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Holder.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Holder.kt @@ -37,6 +37,15 @@ import de.fraunhofer.aisec.cpg.graph.statements.expressions.CallExpression * [CallExpression]). */ interface Holder { + + /** + * Replaces the existing node specified in [old] with the one in [new]. Implementation how to do + * that might be specific to the holder. + * + * An indication whether this operation was successful needs to be returned. + */ + fun replace(old: NodeTypeToHold, new: NodeTypeToHold): Boolean + /** Adds a [Node] to the list of "held" nodes. */ operator fun plusAssign(node: NodeTypeToHold) } diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Interfaces.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Interfaces.kt index 3d5971bbf4..103a71d9ad 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Interfaces.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/Interfaces.kt @@ -147,5 +147,5 @@ interface HasOverloadedOperation : HasOperatorCode { * The base expression this operator works on. The [Type] of this is also the source where the * [SymbolResolver] is looking for an overloaded [OperatorDeclaration]. */ - val operatorBase: HasType + val operatorBase: Expression } diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/StatementHolder.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/StatementHolder.kt index 6b97cd923b..2a79238571 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/StatementHolder.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/StatementHolder.kt @@ -50,6 +50,10 @@ interface StatementHolder : Holder { */ var statements: MutableList + override fun replace(old: Statement, new: Statement): Boolean { + return statementEdges.replace(old, new) + } + override operator fun plusAssign(node: Statement) { statementEdges += node } diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/edges/collections/EdgeList.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/edges/collections/EdgeList.kt index d08c95458c..cfbf9a8c03 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/edges/collections/EdgeList.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/edges/collections/EdgeList.kt @@ -82,6 +82,17 @@ abstract class EdgeList>( return ok } + /** Replaces the first occurrence of an edge with [old] with a new edge to [new]. */ + fun replace(old: NodeType, new: NodeType): Boolean { + val idx = this.indexOfFirst { it.end == old } + if (idx != -1) { + this[idx] = init(thisRef, new) + return true + } + + return false + } + override fun clear() { // Make a copy of our edges so we can pass a copy to our on-remove handler val edges = this.toList() diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/BinaryOperator.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/BinaryOperator.kt index 560326b100..c090750d26 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/BinaryOperator.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/BinaryOperator.kt @@ -113,7 +113,7 @@ open class BinaryOperator : get() = listOf(rhs) /** The binary operator operators on the [lhs]. [rhs] is part of the [operatorArguments]. */ - override val operatorBase: HasType + override val operatorBase: Expression get() = lhs override fun equals(other: Any?): Boolean { diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/CallExpression.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/CallExpression.kt index 4530598aab..cf9056e16a 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/CallExpression.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/CallExpression.kt @@ -263,7 +263,7 @@ open class CallExpression : * is overloaded. In this case we want the [operatorBase] to point to [callee], so we can take * its type to lookup the necessary [OperatorDeclaration]. */ - override val operatorBase: HasType + override val operatorBase: Expression get() = callee override fun equals(other: Any?): Boolean { diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/MemberExpression.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/MemberExpression.kt index bd2fb0e0b2..21c0e2cb85 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/MemberExpression.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/MemberExpression.kt @@ -60,7 +60,7 @@ class MemberExpression : Reference(), HasOverloadedOperation, ArgumentHolder, Ha override val operatorArguments: List get() = listOf() - override val operatorBase: HasType + override val operatorBase: Expression get() = base override fun toString(): String { diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/OperatorCallExpression.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/OperatorCallExpression.kt new file mode 100644 index 0000000000..1a2242c328 --- /dev/null +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/OperatorCallExpression.kt @@ -0,0 +1,76 @@ +/* + * 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.statements.expressions + +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.declarations.OperatorDeclaration + +/** + * This special call expression is used when an operator (such as a [BinaryOperator]) is overloaded. + * In this case, we replace the original [BinaryOperator] with an [OperatorCallExpression], which + * points to its respective [OperatorDeclaration]. + */ +class OperatorCallExpression : CallExpression(), HasOperatorCode, HasBase { + + override var operatorCode: String? = null + + override var name: Name + get() = Name(operatorCode ?: "") + set(_) { + // read-only + } + + /** + * The base object. This is basically a shortcut to accessing the base of the [callee], if it + * has one (i.e., if it implements [HasBase]). This is the case for example, if it is a + * [MemberExpression]. + */ + override val base: Expression? + get() { + return (callee as? HasBase)?.base + } +} + +/** + * Creates a new [OperatorCallExpression] to a [OperatorDeclaration] and also sets the appropriate + * fields such as [CallExpression.invokes] and [Reference.refersTo]. + */ +fun operatorCallFromDeclaration( + decl: OperatorDeclaration, + op: HasOverloadedOperation +): OperatorCallExpression { + return with(decl) { + val ref = + newMemberExpression(decl.name, op.operatorBase, operatorCode = ".") + .implicit(decl.name.localName, location = op.location) + ref.refersTo = decl + val call = + newOperatorCallExpression(operatorCode = op.operatorCode ?: "", ref) + .codeAndLocationFrom(ref) + call.invokes = mutableListOf(decl) + call + } +} diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/UnaryOperator.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/UnaryOperator.kt index a5f2d88612..3bb1931a57 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/UnaryOperator.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/statements/expressions/UnaryOperator.kt @@ -57,7 +57,8 @@ class UnaryOperator : Expression(), HasOverloadedOperation, ArgumentHolder, HasT get() = listOf() /** The unary operator operates on [input]. */ - override val operatorBase = input + override val operatorBase + get() = input /** The operator code. */ override var operatorCode: String? = null diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/SubgraphWalker.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/SubgraphWalker.kt index 248ef670ad..e5821b00d7 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/SubgraphWalker.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/helpers/SubgraphWalker.kt @@ -27,10 +27,15 @@ package de.fraunhofer.aisec.cpg.helpers import de.fraunhofer.aisec.cpg.ScopeManager import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend +import de.fraunhofer.aisec.cpg.graph.ArgumentHolder +import de.fraunhofer.aisec.cpg.graph.ContextProvider import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.graph.StatementHolder import de.fraunhofer.aisec.cpg.graph.declarations.RecordDeclaration import de.fraunhofer.aisec.cpg.graph.edges.ast.AstEdge import de.fraunhofer.aisec.cpg.graph.edges.collections.EdgeCollection +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Expression +import de.fraunhofer.aisec.cpg.passes.Pass import de.fraunhofer.aisec.cpg.processing.strategy.Strategy import java.lang.annotation.AnnotationFormatError import java.lang.reflect.Field @@ -340,3 +345,55 @@ object SubgraphWalker { } } } + +/** + * Tries to replace the [old] expression with a [new] one, given the [parent]. + * + * There are two things to consider: + * - First, this only works if [parent] is either an [ArgumentHolder] or [StatementHolder]. + * Otherwise, we cannot instruct the parent to exchange the node + * - Second, since exchanging the node has influence on their edges (such as EOG, DFG, etc.), we + * only support a replacement very early in the pass system. To be specific, we only allow + * replacement before any DFG edges are set. We are re-wiring EOG edges, but nothing else. If one + * tries to replace a node with existing [Node.nextDFG] or [Node.prevDFG], we fail. + */ +context(ContextProvider) +fun SubgraphWalker.ScopedWalker.replace(parent: Node?, old: Expression, new: Expression): Boolean { + // We do not allow to replace nodes where the DFG (or other dependent nodes, such as PDG have + // been set). The reason for that is that these edges contain a lot of information on the edges + // themselves and replacing this edge would be very complicated. + if (old.prevDFG.isNotEmpty() || old.nextDFG.isNotEmpty()) { + return false + } + + val success = + when (parent) { + is ArgumentHolder -> parent.replace(old, new) + is StatementHolder -> parent.replace(old, new) + else -> { + Pass.log.error( + "Parent AST node is not an argument or statement holder. Cannot replace node. Further analysis might not be entirely accurate." + ) + return false + } + } + if (!success) { + Pass.log.error( + "Replacing expression $old was not successful. Further analysis might not be entirely accurate." + ) + } else { + // Store any eventual EOG/DFG nodes and disconnect old node + val oldPrevEOG = old.prevEOG.toMutableList() + val oldNextEOG = old.nextEOG.toMutableList() + old.disconnectFromGraph() + + // Put the stored EOG nodes to the new node + new.prevEOG = oldPrevEOG + new.nextEOG = oldNextEOG + + // Make sure to inform the walker about our change + this.registerReplacement(old, new) + } + + return success +} diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/ResolveCallExpressionAmbiguityPass.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/ResolveCallExpressionAmbiguityPass.kt index 31ce5c5d22..42f5ec7c8b 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/ResolveCallExpressionAmbiguityPass.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/ResolveCallExpressionAmbiguityPass.kt @@ -39,6 +39,7 @@ import de.fraunhofer.aisec.cpg.graph.statements.expressions.* import de.fraunhofer.aisec.cpg.graph.types.ObjectType import de.fraunhofer.aisec.cpg.graph.types.Type import de.fraunhofer.aisec.cpg.helpers.SubgraphWalker +import de.fraunhofer.aisec.cpg.helpers.replace import de.fraunhofer.aisec.cpg.passes.configuration.DependsOn import de.fraunhofer.aisec.cpg.passes.configuration.ExecuteBefore import de.fraunhofer.aisec.cpg.passes.configuration.RequiresLanguageTrait @@ -170,7 +171,7 @@ fun SubgraphWalker.ScopedWalker.replaceCallWithCast( cast.expression = call.arguments.single() cast.name = cast.castType.name - replaceArgument(parent, call, cast) + replace(parent, call, cast) } context(ContextProvider) @@ -188,27 +189,5 @@ fun SubgraphWalker.ScopedWalker.replaceCallWithConstruct( construct.arguments = call.arguments construct.type = type - replaceArgument(parent, call, construct) -} - -context(ContextProvider) -fun SubgraphWalker.ScopedWalker.replaceArgument(parent: Node?, old: Expression, new: Expression) { - if (parent !is ArgumentHolder) { - Pass.log.error( - "Parent AST node of call expression is not an argument holder. Cannot convert to cast expression. Further analysis might not be entirely accurate." - ) - return - } - - val success = parent.replaceArgument(old, new) - if (!success) { - Pass.log.error( - "Replacing expression $old was not successful. Further analysis might not be entirely accurate." - ) - } else { - old.disconnectFromGraph() - - // Make sure to inform the walker about our change - this.registerReplacement(old, new) - } + replace(parent, call, construct) } 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 cd515cd463..6a3d8707a3 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 @@ -37,6 +37,7 @@ import de.fraunhofer.aisec.cpg.graph.statements.expressions.* import de.fraunhofer.aisec.cpg.graph.types.* import de.fraunhofer.aisec.cpg.helpers.SubgraphWalker.ScopedWalker import de.fraunhofer.aisec.cpg.helpers.Util +import de.fraunhofer.aisec.cpg.helpers.replace import de.fraunhofer.aisec.cpg.passes.configuration.DependsOn import de.fraunhofer.aisec.cpg.passes.inference.Inference.TypeInferenceObserver import de.fraunhofer.aisec.cpg.passes.inference.inferFunction @@ -343,7 +344,29 @@ open class SymbolResolver(ctx: TranslationContext) : ComponentPass(ctx) { return null } var member: ValueDeclaration? = null - val record = containingClass.recordDeclaration + var type = containingClass + + // Check for a possible overloaded operator-> + if ( + reference.language is HasOperatorOverloading && + reference is MemberExpression && + reference.operatorCode == "->" && + reference.base.type !is PointerType + ) { + val result = resolveOperator(reference) + val op = result?.bestViable?.singleOrNull() + if (result?.success == SUCCESSFUL && op is OperatorDeclaration) { + type = op.returnTypes.singleOrNull()?.root ?: unknownType() + + // We need to insert a new operator call expression in between + val call = operatorCallFromDeclaration(op, reference) + + // Make the call our new base + reference.base = call + } + } + + val record = type.recordDeclaration if (record != null) { member = record.fields @@ -353,8 +376,8 @@ open class SymbolResolver(ctx: TranslationContext) : ComponentPass(ctx) { } if (member == null) { member = - (containingClass.recordDeclaration?.superTypeDeclarations?.flatMap { it.fields } - ?: listOf()) + type.superTypes + .flatMap { it.recordDeclaration?.fields ?: listOf() } .filter { it.name.localName == reference.name.localName } .map { it.definition } .firstOrNull() @@ -448,6 +471,7 @@ open class SymbolResolver(ctx: TranslationContext) : ComponentPass(ctx) { is Reference -> handleReference(currClass, node) is ConstructExpression -> handleConstructExpression(node) is CallExpression -> handleCallExpression(node) + is HasOverloadedOperation -> handleOverloadedOperator(node) } } @@ -726,6 +750,44 @@ open class SymbolResolver(ctx: TranslationContext) : ComponentPass(ctx) { } } + private fun handleOverloadedOperator(op: HasOverloadedOperation) { + val result = resolveOperator(op) + val decl = result?.bestViable?.singleOrNull() ?: return + + // If the result was successful, we can replace the node + if (result.success == SUCCESSFUL && decl is OperatorDeclaration && op is Expression) { + val call = operatorCallFromDeclaration(decl, op) + walker.replace(op.astParent, op, call) + } + } + + private fun resolveOperator(op: HasOverloadedOperation): CallResolutionResult? { + val language = op.language + val base = op.operatorBase + if (language !is HasOperatorOverloading || language.isPrimitive(base.type)) { + return null + } + + val symbol = language.overloadedOperatorNames[Pair(op::class, op.operatorCode)] + if (symbol == null) { + log.warn( + "Could not resolve operator overloading for unknown operatorCode ${op.operatorCode}" + ) + return null + } + + val possibleTypes = mutableSetOf() + possibleTypes.add(op.operatorBase.type) + possibleTypes.addAll(op.operatorBase.assignedTypes) + + val candidates = + resolveMemberByName(symbol, possibleTypes) + .filterIsInstance() + .toSet() + + return resolveWithArguments(candidates, op.operatorArguments, op as Expression) + } + /** * Returns a set of types in which the callee of our [call] could reside in. More concretely, it * returns a [Pair], where the first element is the set of types and the second is our best diff --git a/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CLanguage.kt b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CLanguage.kt index b510edd302..ad24b11905 100644 --- a/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CLanguage.kt +++ b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CLanguage.kt @@ -27,7 +27,6 @@ package de.fraunhofer.aisec.cpg.frontends.cxx import com.fasterxml.jackson.annotation.JsonIgnore import de.fraunhofer.aisec.cpg.frontends.* -import de.fraunhofer.aisec.cpg.graph.declarations.ParameterDeclaration import de.fraunhofer.aisec.cpg.graph.types.* import kotlin.reflect.KClass import org.neo4j.ogm.annotation.Transient @@ -130,18 +129,6 @@ open class CLanguage : return ImplicitCast } - // Another special rule is that if we have a const reference (e.g. const T&) in a function - // call, this will match the type T because this means that the parameter is given by - // reference rather than by value. - if ( - targetType is ReferenceType && - targetType.elementType == type && - targetHint is ParameterDeclaration && - CONST in targetHint.modifiers - ) { - return DirectMatch - } - return CastNotPossible } } diff --git a/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CPPLanguage.kt b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CPPLanguage.kt index ea6bd14e30..23d7b942fd 100644 --- a/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CPPLanguage.kt +++ b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CPPLanguage.kt @@ -25,11 +25,14 @@ */ package de.fraunhofer.aisec.cpg.frontends.cxx +import de.fraunhofer.aisec.cpg.CallResolutionResult +import de.fraunhofer.aisec.cpg.SignatureMatches import de.fraunhofer.aisec.cpg.TranslationContext import de.fraunhofer.aisec.cpg.frontends.* import de.fraunhofer.aisec.cpg.graph.HasOverloadedOperation import de.fraunhofer.aisec.cpg.graph.Node import de.fraunhofer.aisec.cpg.graph.declarations.* +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.CallExpression @@ -37,6 +40,7 @@ import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberCallExpression import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberExpression import de.fraunhofer.aisec.cpg.graph.statements.expressions.UnaryOperator import de.fraunhofer.aisec.cpg.graph.types.* +import de.fraunhofer.aisec.cpg.matchesSignature import de.fraunhofer.aisec.cpg.passes.* import de.fraunhofer.aisec.cpg.passes.inference.startInference import kotlin.reflect.KClass @@ -170,6 +174,17 @@ open class CPPLanguage : return match } + // Another special rule is that if we have a (const) reference (e.g. const T&) in a function + // call, this will match the type T because this means that the parameter is given by + // reference rather than by value. + if ( + targetType is ReferenceType && + targetType.elementType == type && + targetHint is ParameterDeclaration + ) { + return DirectMatch + } + // In C++, it is possible to have conversion constructors. We will not have full support for // them yet, but at least we should have some common cases here, such as const char* to // std::string @@ -184,6 +199,29 @@ open class CPPLanguage : return CastNotPossible } + override fun bestViableResolution( + result: CallResolutionResult + ): Pair, CallResolutionResult.SuccessKind> { + // There is a sort of weird workaround in C++ to select a prefix vs. postfix operator for + // increment and decrement operators. See + // https://en.cppreference.com/w/cpp/language/operator_incdec. If it is a postfix, we need + // to match for a function with a fake "int" parameter + val expr = result.source + if ( + expr is UnaryOperator && + (expr.operatorCode == "++" || expr.operatorCode == "--") && + expr.isPostfix + ) { + result.signatureResults = + result.candidateFunctions + .map { Pair(it, it.matchesSignature(listOf(primitiveType("int")))) } + .filter { it.second is SignatureMatches } + .associate { it } + } + + return super.bestViableResolution(result) + } + override val startCharacter = '<' override val endCharacter = '>' diff --git a/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/DeclaratorHandler.kt b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/DeclaratorHandler.kt index 0fd21a046c..ddf3ab683d 100644 --- a/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/DeclaratorHandler.kt +++ b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/DeclaratorHandler.kt @@ -26,7 +26,7 @@ package de.fraunhofer.aisec.cpg.frontends.cxx import de.fraunhofer.aisec.cpg.ResolveInFrontend -import de.fraunhofer.aisec.cpg.frontends.HasOperatorOverloading +import de.fraunhofer.aisec.cpg.frontends.isKnownOperatorName import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.declarations.* import de.fraunhofer.aisec.cpg.graph.scopes.NameScope @@ -171,7 +171,7 @@ class DeclaratorHandler(lang: CXXLanguageFrontend) : // Check if it's an operator name.isKnownOperatorName -> { // retrieve the operator code - var operatorCode = name.localName.drop("operator".length) + val operatorCode = name.localName.drop("operator".length) newOperatorDeclaration(name, operatorCode, rawNode = ctx) } // Check, if it's a constructor. This is the case if the local names of the function @@ -509,17 +509,6 @@ class DeclaratorHandler(lang: CXXLanguageFrontend) : frontend.declarationHandler.handle(member) } } - - /** Checks whether the [Name] for a function is a known operator name. */ - val Name.isKnownOperatorName: Boolean - get() { - var language = language - if (language !is HasOperatorOverloading) { - return false - } - - return language.overloadedOperatorNames.containsValue(this.localName) - } } /** diff --git a/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/CXXExtraPass.kt b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/CXXExtraPass.kt index 28559258db..b6eeae1365 100644 --- a/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/CXXExtraPass.kt +++ b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/passes/CXXExtraPass.kt @@ -35,6 +35,7 @@ import de.fraunhofer.aisec.cpg.graph.scopes.ValueDeclarationScope import de.fraunhofer.aisec.cpg.graph.statements.expressions.* import de.fraunhofer.aisec.cpg.graph.types.recordDeclaration import de.fraunhofer.aisec.cpg.helpers.SubgraphWalker +import de.fraunhofer.aisec.cpg.helpers.replace import de.fraunhofer.aisec.cpg.passes.configuration.DependsOn import de.fraunhofer.aisec.cpg.passes.configuration.ExecuteBefore @@ -83,7 +84,7 @@ class CXXExtraPass(ctx: TranslationContext) : ComponentPass(ctx) { // In theory, we could just keep this meaningless unary expression, but it in order // to reduce nodes, we unwrap the reference and exchange it in the arguments of the // binary op - walker.replaceArgument(parent, node, node.input) + walker.replace(parent, node, node.input) } } @@ -144,7 +145,7 @@ class CXXExtraPass(ctx: TranslationContext) : ComponentPass(ctx) { // * replace the binary operator with the cast expression in the parent argument // holder - walker.replaceArgument(parent, binOp, cast) + walker.replace(parent, binOp, cast) } } diff --git a/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CXXDeclarationTest.kt b/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CXXDeclarationTest.kt index 6bfae94a61..b874b2a2ea 100644 --- a/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CXXDeclarationTest.kt +++ b/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CXXDeclarationTest.kt @@ -29,6 +29,7 @@ import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration import de.fraunhofer.aisec.cpg.graph.statements.ReturnStatement import de.fraunhofer.aisec.cpg.graph.statements.expressions.Block +import de.fraunhofer.aisec.cpg.graph.statements.expressions.OperatorCallExpression import de.fraunhofer.aisec.cpg.graph.types.FunctionPointerType import de.fraunhofer.aisec.cpg.test.* import java.io.File @@ -242,8 +243,67 @@ class CXXDeclarationTest { } assertNotNull(result) - var plusplus = result.operators["operator++"] + val integer = result.records["Integer"] + assertNotNull(integer) + + val plusplus = integer.operators["operator++"] assertNotNull(plusplus) assertEquals("++", plusplus.operatorCode) + + val plus = integer.operators("operator+") + assertEquals(2, plus.size) + assertEquals("+", plus.map { it.operatorCode }.distinct().singleOrNull()) + + val main = result.functions["main"] + assertNotNull(main) + + val unaryOp = main.operatorCalls["++"] + assertNotNull(unaryOp) + assertInvokes(unaryOp, plusplus) + + val binaryOp0 = main.operatorCalls("+").getOrNull(0) + assertNotNull(binaryOp0) + assertInvokes(binaryOp0, plus.getOrNull(0)) + + val binaryOp1 = main.operatorCalls("+").getOrNull(1) + assertNotNull(binaryOp1) + assertInvokes(binaryOp1, plus.getOrNull(1)) + } + + @Test + fun testMemberAccessOperator() { + val file = File("src/test/resources/cxx/operators/member_access.cpp") + val result = + analyze(listOf(file), file.parentFile.toPath(), true) { + it.registerLanguage() + } + assertNotNull(result) + + var proxy = result.records["Proxy"] + assertNotNull(proxy) + + var op = proxy.operators["operator->"] + assertNotNull(op) + + var data = result.records["Data"] + assertNotNull(data) + + var size = data.fields["size"] + assertNotNull(size) + + val p = result.refs["p"] + assertNotNull(p) + assertEquals(proxy.toType(), p.type) + + var sizeRef = result.memberExpressions["size"] + assertNotNull(sizeRef) + assertRefersTo(sizeRef, size) + + // we should now have an implicit call to our operator in-between "p" and "size" + val opCall = sizeRef.base + assertNotNull(opCall) + assertIs(opCall) + assertEquals(p, opCall.base) + assertInvokes(opCall, op) } } diff --git a/cpg-language-cxx/src/test/resources/calls/cxxprioresolution/methodresolution/overloadnoresolution.cpp b/cpg-language-cxx/src/test/resources/calls/cxxprioresolution/methodresolution/overloadnoresolution.cpp index 5d1da9cea6..9eab39df76 100644 --- a/cpg-language-cxx/src/test/resources/calls/cxxprioresolution/methodresolution/overloadnoresolution.cpp +++ b/cpg-language-cxx/src/test/resources/calls/cxxprioresolution/methodresolution/overloadnoresolution.cpp @@ -19,6 +19,6 @@ class Overloaded : public Base int main() { Overloaded overload; - cout << overload.calc(1) << '\n'; + overload.calc(1); return 0; } \ No newline at end of file diff --git a/cpg-language-cxx/src/test/resources/cxx/operators/member_access.cpp b/cpg-language-cxx/src/test/resources/cxx/operators/member_access.cpp new file mode 100644 index 0000000000..5136efc645 --- /dev/null +++ b/cpg-language-cxx/src/test/resources/cxx/operators/member_access.cpp @@ -0,0 +1,24 @@ +class Data { +public: + int size; +}; + +class Proxy { +public: + Proxy() { + this->data = new Data(); + } + + Data* operator->() { + return data; + } + + Data* data; +}; + +int main() { + Proxy p; + int size = p->size; + + // int another_size = p.operator->()->size; +} \ No newline at end of file diff --git a/cpg-language-java/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/java/JavaLanguage.kt b/cpg-language-java/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/java/JavaLanguage.kt index 85bbcde207..4a3ed16ee2 100644 --- a/cpg-language-java/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/java/JavaLanguage.kt +++ b/cpg-language-java/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/java/JavaLanguage.kt @@ -39,7 +39,6 @@ import org.neo4j.ogm.annotation.Transient /** The Java language. */ open class JavaLanguage : Language(), - // HasComplexCallResolution, HasClasses, HasSuperClasses, HasGenerics, diff --git a/cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/python/PythonLanguageFrontend.kt b/cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/python/PythonLanguageFrontend.kt index 674894f78f..029981cbe1 100644 --- a/cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/python/PythonLanguageFrontend.kt +++ b/cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/python/PythonLanguageFrontend.kt @@ -156,16 +156,16 @@ class PythonLanguageFrontend(language: Language, ctx: Tr // TODO: This might create problem with nested classes parseName(id) } else { - // If it is not, we want place it in the current namespace - scopeManager.currentNamespace.fqn(id) + // otherwise, we can just simply take the unqualified name and the type + // resolver will take care of the rest + id } objectType(name) } else -> { // The AST supplied us with some kind of type information, but we could not parse - // it, so we - // need to return the unknown type. + // it, so we need to return the unknown type. unknownType() } } diff --git a/cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/python/StatementHandler.kt b/cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/python/StatementHandler.kt index d4d56396f7..4b874063be 100644 --- a/cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/python/StatementHandler.kt +++ b/cpg-language-python/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/python/StatementHandler.kt @@ -25,6 +25,8 @@ */ package de.fraunhofer.aisec.cpg.frontends.python +import de.fraunhofer.aisec.cpg.frontends.HasOperatorOverloading +import de.fraunhofer.aisec.cpg.frontends.isKnownOperatorName import de.fraunhofer.aisec.cpg.frontends.python.Python.AST.IsAsync import de.fraunhofer.aisec.cpg.frontends.python.PythonLanguage.Companion.MODIFIER_POSITIONAL_ONLY_ARGUMENT import de.fraunhofer.aisec.cpg.graph.* @@ -37,6 +39,7 @@ 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.ProblemExpression import de.fraunhofer.aisec.cpg.graph.types.FunctionType +import de.fraunhofer.aisec.cpg.helpers.Util import kotlin.collections.plusAssign class StatementHandler(frontend: PythonLanguageFrontend) : @@ -288,6 +291,7 @@ class StatementHandler(frontend: PythonLanguageFrontend) : s: Python.AST.NormalOrAsyncFunctionDef, recordDeclaration: RecordDeclaration? = null ): DeclarationStatement { + val language = language val result = if (recordDeclaration != null) { if (s.name == "__init__") { @@ -296,6 +300,23 @@ class StatementHandler(frontend: PythonLanguageFrontend) : recordDeclaration = recordDeclaration, rawNode = s ) + } else if (language is HasOperatorOverloading && s.name.isKnownOperatorName) { + var decl = + newOperatorDeclaration( + name = s.name, + recordDeclaration = recordDeclaration, + operatorCode = language.operatorCodeFor(s.name) ?: "", + rawNode = s + ) + if (decl.operatorCode == "") { + Util.warnWithFileLocation( + decl, + log, + "Could not find operator code for operator {}. This will most likely result in a failure", + s.name + ) + } + decl } else { newMethodDeclaration( name = s.name, diff --git a/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/PythonFrontendTest.kt b/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/PythonFrontendTest.kt index b99f5346bb..5024cdd09a 100644 --- a/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/PythonFrontendTest.kt +++ b/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/PythonFrontendTest.kt @@ -438,6 +438,35 @@ class PythonFrontendTest : BaseTest() { assertLocalName("self", fooMemCall.base) } + @Test + fun testClassTypeAnnotations() { + val topLevel = Path.of("src", "test", "resources", "python") + val tu = + analyzeAndGetFirstTU( + listOf(topLevel.resolve("class_type_annotations.py").toFile()), + topLevel, + true + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val other = tu.records["Other"] + assertNotNull(other) + assertFullName("class_type_annotations.Other", other.toType()) + + val foo = tu.records["Foo"] + assertNotNull(foo) + assertFullName("class_type_annotations.Foo", foo.toType()) + + val fromOther = tu.functions["from_other"] + assertNotNull(fromOther) + + val paramType = fromOther.parameters.firstOrNull()?.type + assertNotNull(paramType) + assertEquals(other.toType(), paramType) + } + @Test fun testCtor() { val topLevel = Path.of("src", "test", "resources", "python") diff --git a/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/StatementHandlerTest.kt b/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/StatementHandlerTest.kt index 6aea87792a..94ee977584 100644 --- a/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/StatementHandlerTest.kt +++ b/cpg-language-python/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/python/StatementHandlerTest.kt @@ -28,6 +28,7 @@ package de.fraunhofer.aisec.cpg.frontends.python import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.test.analyze import de.fraunhofer.aisec.cpg.test.analyzeAndGetFirstTU +import de.fraunhofer.aisec.cpg.test.assertResolvedType import java.nio.file.Path import kotlin.test.Test import kotlin.test.assertContains @@ -77,7 +78,6 @@ class StatementHandlerTest { val file = topLevel.resolve("varargs.py").toFile() val result = analyze(listOf(file), topLevel, true) { it.registerLanguage() } - assertNotNull(result) val func = result.functions["test_varargs"] @@ -105,4 +105,36 @@ class StatementHandlerTest { assertNotNull(myOtherFunc) assertEquals(1, myOtherFunc.parameters.size) } + + @Test + fun testOperatorOverload() { + val topLevel = Path.of("src", "test", "resources", "python") + val file = topLevel.resolve("operator.py").toFile() + + val result = analyze(listOf(file), topLevel, true) { it.registerLanguage() } + assertNotNull(result) + + with(result) { + val numberType = assertResolvedType("operator.Number") + val strType = assertResolvedType("str") + + // we should have an operator call to __add__ (+) now + var opCall = result.operatorCalls("+").getOrNull(0) + assertNotNull(opCall) + assertEquals(numberType, opCall.type) + + val add = result.operators["__add__"] + assertNotNull(add) + assertEquals(add, opCall.invokes.singleOrNull()) + + // ... and one to __pos__ (+) + opCall = result.operatorCalls("+").getOrNull(1) + assertNotNull(opCall) + assertEquals(strType, opCall.type) + + val pos = result.operators["__pos__"] + assertNotNull(pos) + assertEquals(pos, opCall.invokes.singleOrNull()) + } + } } diff --git a/cpg-language-python/src/test/resources/python/class_type_annotations.py b/cpg-language-python/src/test/resources/python/class_type_annotations.py new file mode 100644 index 0000000000..60f53bcd5d --- /dev/null +++ b/cpg-language-python/src/test/resources/python/class_type_annotations.py @@ -0,0 +1,8 @@ +class Other: + j: int + +class Foo: + i: int + + def from_other(self, other: Other): + self.i = other.j diff --git a/cpg-language-python/src/test/resources/python/operator.py b/cpg-language-python/src/test/resources/python/operator.py new file mode 100644 index 0000000000..646ec75b29 --- /dev/null +++ b/cpg-language-python/src/test/resources/python/operator.py @@ -0,0 +1,32 @@ +from __future__ import annotations + + +class Number: + i: int + + def __init__(self, i): + self.i = i + + def __add__(self, other: Number) -> Number: + return Number(self.i + other.i) + + def __pos__(self) -> str: + return "python is quite crazy" + + +def main(): + a = Number(5) + b = Number(10) + c = a + b + print(c) + + d = +b + print(d) + + # the following unfortunately does not work yet because the types of a and b does not seem to propagate to c + # currently yet. + d = +c + + +if __name__ == "__main__": + main()