diff --git a/joern-cli/frontends/csharpsrc2cpg/build.sbt b/joern-cli/frontends/csharpsrc2cpg/build.sbt index 499ea6892393..cfe27e49b70c 100644 --- a/joern-cli/frontends/csharpsrc2cpg/build.sbt +++ b/joern-cli/frontends/csharpsrc2cpg/build.sbt @@ -37,9 +37,8 @@ lazy val AstgenWinArm = "dotnetastgen-win-arm.exe" lazy val AstgenLinux = "dotnetastgen-linux" lazy val AstgenLinuxArm = "dotnetastgen-linux-arm" lazy val AstgenMac = "dotnetastgen-macos" -lazy val AstgenMacArm = "dotnetastgen-macos-arm" -lazy val AllPlatforms = Seq(AstgenWin, AstgenWinArm, AstgenLinux, AstgenLinuxArm, AstgenMac, AstgenMacArm) +lazy val AllPlatforms = Seq(AstgenWin, AstgenWinArm, AstgenLinux, AstgenLinuxArm, AstgenMac) lazy val astGenDlUrl = settingKey[String]("astgen download url") astGenDlUrl := s"https://github.com/joernio/DotNetAstGen/releases/download/v${astGenVersion.value}/" @@ -72,11 +71,7 @@ astGenBinaryNames := { case Environment.ArchitectureType.ARM => Seq(AstgenLinuxArm) } Seq(AstgenLinux) - case Environment.OperatingSystemType.Mac => - Environment.architecture match { - case Environment.ArchitectureType.X86 => Seq(AstgenMac) - case Environment.ArchitectureType.ARM => Seq(AstgenMacArm) - } + case Environment.OperatingSystemType.Mac => Seq(AstgenMac) case Environment.OperatingSystemType.Unknown => AllPlatforms } @@ -91,13 +86,9 @@ astGenDlTask := { astGenBinaryNames.value.foreach { fileName => val dest = astGenDir / fileName if (!dest.exists) { - val url = s"${astGenDlUrl.value}${fileName.stripSuffix(".exe")}.zip" - val downloadedFile = files.File(SimpleCache.downloadMaybe(url).toPath) - files.File.temporaryDirectory("joern-").apply { unzipTarget => - downloadedFile.unzipTo(unzipTarget) - unzipTarget.list.filter(_.name == fileName).foreach(exec => IO.copyFile(exec.toJava, dest)) - } - downloadedFile.delete(swallowIOExceptions = true) + val url = s"${astGenDlUrl.value}$fileName" + val downloadedFile = SimpleCache.downloadMaybe(url) + IO.copyFile(downloadedFile, dest) } } diff --git a/joern-cli/frontends/csharpsrc2cpg/src/main/resources/application.conf b/joern-cli/frontends/csharpsrc2cpg/src/main/resources/application.conf index 08ef9dc66658..4e6b719223f3 100644 --- a/joern-cli/frontends/csharpsrc2cpg/src/main/resources/application.conf +++ b/joern-cli/frontends/csharpsrc2cpg/src/main/resources/application.conf @@ -1,3 +1,3 @@ csharpsrc2cpg { - dotnetastgen_version: "0.12.0" + dotnetastgen_version: "0.13.0" } diff --git a/joern-cli/frontends/csharpsrc2cpg/src/main/resources/builtin_types.json b/joern-cli/frontends/csharpsrc2cpg/src/main/resources/builtin_types.json new file mode 100644 index 000000000000..914e34ef3344 --- /dev/null +++ b/joern-cli/frontends/csharpsrc2cpg/src/main/resources/builtin_types.json @@ -0,0 +1,30 @@ +{ + "System": [ + { + "name": "Console", + "methods": [ + { + "name": "Write" + }, + { + "name": "WriteLine" + } + ], + "fields": [] + } + ], + "System.Buffers": [ + { + "name": "ArrayBufferWriter", + "methods": [ + { + "name": "Advance" + }, + { + "name": "Clear" + } + ], + "fields": [] + } + ] +} \ No newline at end of file diff --git a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/TypeMap.scala b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/TypeMap.scala index 682725f40ef0..ce2f9cb30f79 100644 --- a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/TypeMap.scala +++ b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/TypeMap.scala @@ -1,5 +1,7 @@ package io.joern.csharpsrc2cpg +import com.typesafe.config.impl.* +import com.typesafe.config.{Config, ConfigFactory} import io.joern.csharpsrc2cpg.astcreation.AstCreatorHelper import io.joern.csharpsrc2cpg.parser.DotNetJsonAst.{ ClassDeclaration, @@ -10,32 +12,67 @@ import io.joern.csharpsrc2cpg.parser.DotNetJsonAst.{ import io.joern.csharpsrc2cpg.parser.{DotNetJsonAst, DotNetJsonParser, DotNetNodeInfo, ParserKeys} import io.joern.x2cpg.astgen.AstGenRunner.AstGenRunnerResult import io.joern.x2cpg.datastructures.Stack.Stack +import io.joern.x2cpg.utils.ConcurrentTaskUtil import io.shiftleft.codepropertygraph.generated.nodes.NewNode +import io.shiftleft.semanticcpg.language.* +import org.slf4j.LoggerFactory +import upickle.default.* +import java.io.InputStream import java.nio.file.Paths import scala.collection.mutable +import scala.jdk.CollectionConverters.* +import scala.util.{Failure, Success, Try} + +type NamespaceToTypeMap = Map[String, Set[CSharpType]] + +/** A mapping of type stubs of known types within the scope of the analysis. + * + * @param astGenResult + * the parsed application code. + * @param initialMappings + * any additional mappings to add to the scope. + * @see + * [[io.joern.csharpsrc2cpg.TypeMap.jsonToInitialMapping]] for generating initial mappings. + */ +class TypeMap(astGenResult: AstGenRunnerResult, initialMappings: List[NamespaceToTypeMap] = List.empty) { + + private val logger = LoggerFactory.getLogger(getClass) + + private def builtinTypes: NamespaceToTypeMap = + jsonToInitialMapping(getClass.getResourceAsStream("/builtin_types.json")) match + case Failure(exception) => logger.warn("Unable to parse JSON type entry from builtin types", exception); Map.empty + case Success(mapping) => mapping + + /** Converts a JSON type mapping to a NamespaceToTypeMap entry. + * @param jsonInputStream + * a JSON file as an input stream. + * @return + * the resulting type map in a Try + */ + def jsonToInitialMapping(jsonInputStream: InputStream): Try[NamespaceToTypeMap] = + Try(read[NamespaceToTypeMap](ujson.Readable.fromByteArray(jsonInputStream.readAllBytes()))) -class TypeMap(astGenResult: AstGenRunnerResult) { - - private val namespaceToType: Map[String, Set[CSharpType]] = astGenResult.parsedFiles - .map { file => + private val namespaceToType: NamespaceToTypeMap = { + def typeMapTasks = astGenResult.parsedFiles.map { file => val parserResult = DotNetJsonParser.readFile(Paths.get(file)) val compilationUnit = AstCreatorHelper.createDotNetNodeInfo(parserResult.json(ParserKeys.AstRoot)) () => parseCompilationUnit(compilationUnit) - } - .map(task => task()) // TODO: To be parallelized with https://github.com/joernio/joern/pull/4009 - .foldLeft(Map.empty[String, Set[CSharpType]])((a, b) => { + }.iterator + val typeMaps = ConcurrentTaskUtil.runUsingSpliterator(typeMapTasks).flatMap(_.toOption) + (builtinTypes +: typeMaps ++: initialMappings).foldLeft(Map.empty[String, Set[CSharpType]])((a, b) => { val accumulator = mutable.HashMap.from(a) val allKeys = accumulator.keySet ++ b.keySet allKeys.foreach(k => accumulator.updateWith(k) { - case Some(existing) => b.get(k).map(x => x ++ existing) - case None => b.get(k) + case Some(existing) => Option(a.getOrElse(k, Set.empty) ++ b.getOrElse(k, Set.empty) ++ existing) + case None => Option(a.getOrElse(k, Set.empty) ++ b.getOrElse(k, Set.empty)) } ) accumulator.toMap }) + } /** For the given namespace, returns the declared classes. */ @@ -86,7 +123,7 @@ class TypeMap(astGenResult: AstGenRunnerResult) { case _ => List.empty } .toList - CSharpType(className, members) + CSharpType(className, members.collectAll[CSharpMethod].l, members.collectAll[CSharpField].l) } private def parseMethodDeclaration(methodDecl: DotNetNodeInfo): List[CSharpMethod] = { @@ -106,12 +143,8 @@ class TypeMap(astGenResult: AstGenRunnerResult) { } -sealed trait CSharpMember { - def name: String -} - -case class CSharpField(name: String) extends CSharpMember +case class CSharpField(name: String) derives ReadWriter -case class CSharpMethod(name: String) extends CSharpMember +case class CSharpMethod(name: String) derives ReadWriter -case class CSharpType(name: String, members: List[CSharpMember]) extends CSharpMember +case class CSharpType(name: String, methods: List[CSharpMethod], fields: List[CSharpField]) derives ReadWriter diff --git a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstCreator.scala b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstCreator.scala index 4e956baa5626..3d32b090895f 100644 --- a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstCreator.scala +++ b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstCreator.scala @@ -1,18 +1,19 @@ package io.joern.csharpsrc2cpg.astcreation import io.joern.csharpsrc2cpg.TypeMap +import io.joern.csharpsrc2cpg.datastructures.CSharpScope import io.joern.csharpsrc2cpg.parser.DotNetJsonAst.* import io.joern.csharpsrc2cpg.parser.{DotNetNodeInfo, ParserKeys} import io.joern.x2cpg.astgen.{AstGenNodeBuilder, ParserResult} import io.joern.x2cpg.datastructures.Scope -import io.joern.x2cpg.datastructures.Stack.Stack +import io.joern.x2cpg.datastructures.Stack.{Stack, StackWrapper} import io.joern.x2cpg.{Ast, AstCreatorBase, ValidationMode} import io.shiftleft.codepropertygraph.generated.NodeTypes import io.shiftleft.codepropertygraph.generated.nodes.{NewFile, NewNode} import org.slf4j.{Logger, LoggerFactory} import overflowdb.BatchedUpdate.DiffGraphBuilder import ujson.Value -import io.joern.x2cpg.datastructures.Stack.StackWrapper + import java.math.BigInteger import java.security.MessageDigest @@ -28,8 +29,7 @@ class AstCreator(val relativeFileName: String, val parserResult: ParserResult, v protected val logger: Logger = LoggerFactory.getLogger(getClass) - protected val methodAstParentStack = new Stack[NewNode]() - protected val scope: Scope[String, (NewNode, String), NewNode] = new Scope() + protected val scope: CSharpScope = new CSharpScope() override def createAst(): DiffGraphBuilder = { val hash = String.format( @@ -71,7 +71,7 @@ class AstCreator(val relativeFileName: String, val parserResult: ParserResult, v case IdentifierName => Seq(astForIdentifier(nodeInfo)) case LocalDeclarationStatement => astForLocalDeclarationStatement(nodeInfo) case GlobalStatement => astForGlobalStatement(nodeInfo) - case _: LiteralExpr => Seq(astForLiteralExpression(nodeInfo)) + case _: LiteralExpr => astForLiteralExpression(nodeInfo) case _ => notHandledYet(nodeInfo) } } diff --git a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstCreatorHelper.scala b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstCreatorHelper.scala index 64ad160a1257..41ee644c3ef2 100644 --- a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstCreatorHelper.scala +++ b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstCreatorHelper.scala @@ -4,8 +4,8 @@ import io.joern.csharpsrc2cpg.astcreation import io.joern.csharpsrc2cpg.parser.DotNetJsonAst.* import io.joern.csharpsrc2cpg.parser.{DotNetJsonAst, DotNetNodeInfo, ParserKeys} import io.joern.x2cpg.{Ast, Defines, ValidationMode} -import io.shiftleft.codepropertygraph.generated.{DispatchTypes, PropertyNames} import io.shiftleft.codepropertygraph.generated.nodes.{NewCall, NewMethod, NewNamespaceBlock, NewTypeDecl} +import io.shiftleft.codepropertygraph.generated.{DispatchTypes, PropertyNames} import ujson.Value import scala.util.{Failure, Success, Try} @@ -37,17 +37,15 @@ trait AstCreatorHelper(implicit withSchemaValidation: ValidationMode) { this: As } protected def astFullName(node: DotNetNodeInfo): String = { - methodAstParentStack.headOption match - case Some(head: NewNamespaceBlock) => s"${head.fullName}.${nameFromNode(node)}" - case Some(head: NewMethod) => s"${head.fullName}.${nameFromNode(node)}" - case Some(head: NewTypeDecl) => s"${head.fullName}.${nameFromNode(node)}" - case _ => nameFromNode(node) + scope.surroundingScopeFullName match + case Some(fullName) => s"$fullName.${nameFromNode(node)}" + case _ => nameFromNode(node) } protected def getTypeFullNameFromAstNode(ast: Seq[Ast]): String = { ast.headOption .flatMap(_.root) - .map(_.properties.get(PropertyNames.TYPE_FULL_NAME).get.toString) + .map(_.properties.getOrElse(PropertyNames.TYPE_FULL_NAME, "ANY").toString) .getOrElse("ANY") } @@ -64,7 +62,10 @@ trait AstCreatorHelper(implicit withSchemaValidation: ValidationMode) { this: As BuiltinTypes.Float case NumericLiteralExpression if node.code.matches("^\\d+\\.?\\d*[m|M]?$") => // e.g. 2m or 2.1M BuiltinTypes.Decimal - case StringLiteralExpression if node.code.matches("^\"\\w+\"$") => BuiltinTypes.String + case StringLiteralExpression => BuiltinTypes.DotNetTypeMap(BuiltinTypes.String) + case IdentifierName => + // TODO: Look at scope object for possible types + "ANY" case _ => Try(createDotNetNodeInfo(node.json(ParserKeys.Type))) match case Success(typeNode) => diff --git a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForDeclarationsCreator.scala b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForDeclarationsCreator.scala index b7514ba201b6..bb79d887cf31 100644 --- a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForDeclarationsCreator.scala +++ b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForDeclarationsCreator.scala @@ -1,5 +1,6 @@ package io.joern.csharpsrc2cpg.astcreation +import io.joern.csharpsrc2cpg.datastructures.{BlockScope, MethodScope, NamespaceScope, TypeScope} import io.joern.csharpsrc2cpg.parser.{DotNetNodeInfo, ParserKeys} import io.joern.x2cpg.datastructures.Stack.StackWrapper import io.joern.x2cpg.utils.NodeBuilders.newModifierNode @@ -22,10 +23,8 @@ trait AstForDeclarationsCreator(implicit withSchemaValidation: ValidationMode) { .columnNumber(columnEnd(namespace)) .filename(relativeFileName) .fullName(fullName) - methodAstParentStack.push(namespaceBlock) - scope.pushNewScope(namespaceBlock) + scope.pushNewScope(NamespaceScope(fullName)) val memberAsts = namespace.json(ParserKeys.Members).arr.flatMap(astForNode).toSeq - methodAstParentStack.pop() scope.popScope() Seq(Ast(namespaceBlock).withChildren(memberAsts)) } @@ -34,11 +33,9 @@ trait AstForDeclarationsCreator(implicit withSchemaValidation: ValidationMode) { val name = nameFromNode(classDecl) val fullName = astFullName(classDecl) val typeDecl = typeDeclNode(classDecl, name, fullName, relativeFileName, code(classDecl)) - methodAstParentStack.push(typeDecl) - scope.pushNewScope(typeDecl) + scope.pushNewScope(TypeScope(fullName)) val modifiers = astForModifiers(classDecl) val members = astForMembers(classDecl.json(ParserKeys.Members).arr.map(createDotNetNodeInfo).toSeq) - methodAstParentStack.pop() scope.popScope() val typeDeclAst = Ast(typeDecl) .withChildren(modifiers) @@ -74,7 +71,7 @@ trait AstForDeclarationsCreator(implicit withSchemaValidation: ValidationMode) { val identifierAst = astForIdentifier(varDecl, typeFullName) val _localNode = localNode(varDecl, name, name, typeFullName) val localNodeAst = Ast(_localNode) - scope.addToScope(name, (_localNode, typeFullName)) + scope.addToScope(name, _localNode) val assignmentNode = callNode( varDecl, code(varDecl), @@ -112,9 +109,9 @@ trait AstForDeclarationsCreator(implicit withSchemaValidation: ValidationMode) { methodSignature(methodReturn, params.flatMap(_.nodes.collectFirst { case x: NewMethodParameterIn => x })) val fullName = s"${astFullName(methodDecl)}:$signature" val methodNode_ = methodNode(methodDecl, name, code(methodDecl), fullName, Option(signature), relativeFileName) - methodAstParentStack.push(methodNode_) - scope.pushNewScope(methodNode_) - val body = astForMethodBody(createDotNetNodeInfo(methodDecl.json(ParserKeys.Body))) + scope.pushNewScope(MethodScope(fullName)) + val body = astForMethodBody(createDotNetNodeInfo(methodDecl.json(ParserKeys.Body))) + scope.popScope() val modifiers = astForModifiers(methodDecl).flatMap(_.nodes).collect { case x: NewModifier => x } val thisNode = if (!modifiers.exists(_.modifierType == ModifierTypes.STATIC)) astForThisNode(methodDecl) @@ -137,20 +134,19 @@ trait AstForDeclarationsCreator(implicit withSchemaValidation: ValidationMode) { } private def astForThisNode(methodDecl: DotNetNodeInfo): Ast = { - val name = "this" - val typeFullName = - methodAstParentStack.headOption.map(_.properties.getOrElse(PropertyNames.FULL_NAME, "ANY").toString) - val param = - parameterInNode(methodDecl, name, name, 0, false, EvaluationStrategies.BY_SHARING.name, typeFullName) + val name = "this" + val typeFullName = scope.surroundingTypeDeclFullName.getOrElse("ANY") + val param = parameterInNode(methodDecl, name, name, 0, false, EvaluationStrategies.BY_SHARING.name, typeFullName) Ast(param) } private def astForMethodBody(body: DotNetNodeInfo): Ast = { - val block = blockNode(body) + val block = blockNode(body) + scope.pushNewScope(BlockScope) val statements = body.json(ParserKeys.Statements).arr.flatMap(astForNode).toList - methodAstParentStack.pop() + val _blockAst = blockAst(block, statements) scope.popScope() - blockAst(block, statements) + _blockAst } private def nodeToMethodReturn(methodReturn: DotNetNodeInfo): NewMethodReturn = { @@ -179,18 +175,10 @@ trait AstForDeclarationsCreator(implicit withSchemaValidation: ValidationMode) { ) val implicitAccessModifier = accessModifiers match // Internal is default for top-level definitions - case Nil - if methodAstParentStack.isEmpty || !methodAstParentStack - .take(2) - .map(_.label()) - .distinct - .contains(NodeTypes.METHOD) => - Ast(newModifierNode(ModifierTypes.INTERNAL)) + case Nil if scope.isTopLevel => Ast(newModifierNode(ModifierTypes.INTERNAL)) // Private is default for nested definitions - case Nil - if methodAstParentStack.headOption.exists(x => x.isInstanceOf[NewMethod] || x.isInstanceOf[NewTypeDecl]) => - Ast(newModifierNode(ModifierTypes.PRIVATE)) - case _ => Ast() + case Nil => Ast(newModifierNode(ModifierTypes.PRIVATE)) + case _ => Ast() implicitAccessModifier :: explicitModifiers } diff --git a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForExpressionsCreator.scala b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForExpressionsCreator.scala index b7fa45c892f3..dc35b0ac4b69 100644 --- a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForExpressionsCreator.scala +++ b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForExpressionsCreator.scala @@ -2,8 +2,8 @@ package io.joern.csharpsrc2cpg.astcreation import io.joern.csharpsrc2cpg.parser.DotNetJsonAst.* import io.joern.csharpsrc2cpg.parser.{DotNetNodeInfo, ParserKeys} -import io.joern.x2cpg.{Ast, ValidationMode} -import io.shiftleft.codepropertygraph.generated.nodes.NewCall +import io.joern.x2cpg.{Ast, Defines, ValidationMode} +import io.shiftleft.codepropertygraph.generated.nodes.{NewCall, NewMethodParameterIn} import io.shiftleft.codepropertygraph.generated.{DispatchTypes, Operators} trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { this: AstCreator => @@ -11,14 +11,17 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { def astForExpression(expr: DotNetNodeInfo): Seq[Ast] = { val expressionNode = createDotNetNodeInfo(expr.json(ParserKeys.Expression)) expressionNode.node match - case _: UnaryExpr => astForUnaryExpression(expressionNode) - case _: BinaryExpr => astForBinaryExpression(expressionNode) - case _ => notHandledYet(expressionNode) + case _: UnaryExpr => astForUnaryExpression(expressionNode) + case _: BinaryExpr => astForBinaryExpression(expressionNode) + case _: LiteralExpr => astForLiteralExpression(expressionNode) + case InvocationExpression => astForInvocationExpression(expressionNode) + case _ => notHandledYet(expressionNode) } - protected def astForLiteralExpression(_literalNode: DotNetNodeInfo): Ast = { - Ast(literalNode(_literalNode, code(_literalNode), nodeTypeFullName(_literalNode))) + protected def astForLiteralExpression(_literalNode: DotNetNodeInfo): Seq[Ast] = { + Seq(Ast(literalNode(_literalNode, code(_literalNode), nodeTypeFullName(_literalNode)))) } + private def astForUnaryExpression(unaryExpr: DotNetNodeInfo): Seq[Ast] = { val operatorToken = unaryExpr.json(ParserKeys.OperatorToken)(ParserKeys.Value).str val operatorName = operatorToken match @@ -79,8 +82,52 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { protected def astForEqualsValueClause(clause: DotNetNodeInfo): Seq[Ast] = { val rhsNode = createDotNetNodeInfo(clause.json(ParserKeys.Value)) rhsNode.node match - case _: LiteralExpr => Seq(astForLiteralExpression(rhsNode)) + case _: LiteralExpr => astForLiteralExpression(rhsNode) case _ => notHandledYet(rhsNode) } + private def astForInvocationExpression(invocationExpr: DotNetNodeInfo): Seq[Ast] = { + val dispatchType = DispatchTypes.STATIC_DISPATCH // TODO + val typeFullName = None // TODO + val arguments = astForArgumentList(createDotNetNodeInfo(invocationExpr.json(ParserKeys.ArgumentList))) + val signature = Option( + s"${typeFullName + .getOrElse("ANY")}:(${arguments.flatMap(_.root).collect { case x: NewMethodParameterIn => x.typeFullName }.mkString(",")})" + ) + + val expression = createDotNetNodeInfo(invocationExpr.json(ParserKeys.Expression)) + val name = nameFromNode(createDotNetNodeInfo(expression.json(ParserKeys.Name))) + + val (receiver, baseTypeFullName) = expression.node match + case SimpleMemberAccessExpression => + val baseNode = createDotNetNodeInfo( + createDotNetNodeInfo(invocationExpr.json(ParserKeys.Expression)).json(ParserKeys.Expression) + ) + val baseIdentifier = + identifierNode(baseNode, nameFromNode(baseNode), code(baseNode), nodeTypeFullName(baseNode)) + (Option(Ast(baseIdentifier)), Option(baseIdentifier.typeFullName)) + case _ => (None, None) + + // TODO: Handle signature + val methodFullName = baseTypeFullName match + case Some(typeFullName) => s"$typeFullName.$name" + case None => s"${Defines.UnresolvedNamespace}.$name" + + val _callAst = callAst( + callNode(invocationExpr, code(invocationExpr), name, methodFullName, dispatchType, signature, typeFullName), + arguments, + receiver + ) + Seq(_callAst) + } + + private def astForArgumentList(argumentList: DotNetNodeInfo): Seq[Ast] = { + argumentList + .json(ParserKeys.Arguments) + .arr + .map(createDotNetNodeInfo) + .flatMap(astForExpression) + .toSeq + } + } diff --git a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForPrimitivesCreator.scala b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForPrimitivesCreator.scala index 75b7b130a7b0..d01fd8251a10 100644 --- a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForPrimitivesCreator.scala +++ b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForPrimitivesCreator.scala @@ -3,24 +3,24 @@ package io.joern.csharpsrc2cpg.astcreation import io.joern.csharpsrc2cpg.parser.DotNetJsonAst.* import io.joern.csharpsrc2cpg.parser.{DotNetNodeInfo, ParserKeys} import io.joern.x2cpg.{Ast, ValidationMode} -import io.shiftleft.codepropertygraph.generated.nodes.NewCall +import io.shiftleft.codepropertygraph.generated.nodes.{NewCall, NewLocal} import io.shiftleft.codepropertygraph.generated.{DispatchTypes, Operators} -trait AstForPrimitivesCreator(implicit withSchemaValidation: ValidationMode) { - this: AstCreator => +trait AstForPrimitivesCreator(implicit withSchemaValidation: ValidationMode) { this: AstCreator => + protected def astForIdentifier(ident: DotNetNodeInfo, typeFullName: String = ""): Ast = { val identifierName = nameFromNode(ident) if identifierName != "_" then { val variableOption = scope.lookupVariable(identifierName) variableOption match - case Some((variable, variableTypeName)) => { - val node = identifierNode(ident, identifierName, ident.code, variableTypeName) + case Some(variable: NewLocal) => + val node = identifierNode(ident, identifierName, ident.code, variable.typeFullName) Ast(node).withRefEdge(node, variable) - } case _ => Ast(identifierNode(ident, identifierName, ident.code, typeFullName)) } else { Ast() } } + } diff --git a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/datastructures/CSharpScope.scala b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/datastructures/CSharpScope.scala new file mode 100644 index 000000000000..cc62cc623674 --- /dev/null +++ b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/datastructures/CSharpScope.scala @@ -0,0 +1,41 @@ +package io.joern.csharpsrc2cpg.datastructures + +import io.joern.x2cpg.datastructures.{Scope, ScopeElement} +import io.shiftleft.codepropertygraph.generated.nodes.{DeclarationNew, NewLocal, NewMethodParameterIn, NewNode} + +class CSharpScope extends Scope[String, DeclarationNew, ScopeType] { + + /** @return + * the surrounding type declaration if one exists. + */ + def surroundingTypeDeclFullName: Option[String] = stack.collectFirst { case ScopeElement(TypeScope(fullName), _) => + fullName + } + + /** @return + * the full name of the surrounding scope. + */ + def surroundingScopeFullName: Option[String] = stack.collectFirst { + case ScopeElement(NamespaceScope(fullName), _) => fullName + case ScopeElement(MethodScope(fullName), _) => fullName + case ScopeElement(TypeScope(fullName), _) => fullName + } + + /** @return + * true if the scope is currently on the top-level, false if the scope is within some nested scope. + */ + def isTopLevel: Boolean = stack + .filterNot(x => x.scopeNode.isInstanceOf[NamespaceScope]) + .exists(x => x.scopeNode.isInstanceOf[MethodScope] || x.scopeNode.isInstanceOf[TypeScope]) + + def tryResolveTypeReference(typeName: String): Option[String] = { + // TODO: Look for a type with a matching name in scope + None + } + + def tryResolveMethodInvocation(typeFullName: String, callName: String): Option[String] = { + // TODO: Use type map to resolve method full name + None + } + +} diff --git a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/datastructures/ScopeType.scala b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/datastructures/ScopeType.scala new file mode 100644 index 000000000000..f54ac4a0230a --- /dev/null +++ b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/datastructures/ScopeType.scala @@ -0,0 +1,11 @@ +package io.joern.csharpsrc2cpg.datastructures + +sealed trait ScopeType + +case class NamespaceScope(fullName: String) extends ScopeType + +case class TypeScope(fullName: String) extends ScopeType + +case class MethodScope(fullName: String) extends ScopeType + +object BlockScope extends ScopeType diff --git a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/parser/DotNetJsonAst.scala b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/parser/DotNetJsonAst.scala index ca9f2e382ae2..5c67fb87106b 100644 --- a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/parser/DotNetJsonAst.scala +++ b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/parser/DotNetJsonAst.scala @@ -44,7 +44,8 @@ object DotNetJsonAst { object FieldDeclaration extends DeclarationExpr - object VariableDeclaration extends DeclarationExpr + object VariableDeclaration extends DeclarationExpr + object LocalDeclarationStatement extends DeclarationExpr object VariableDeclarator extends DeclarationExpr @@ -115,6 +116,16 @@ object DotNetJsonAst { object BitwiseAndExpression extends BinaryExpr object BitwiseOrExpression extends BinaryExpr object ExclusiveOrExpression extends BinaryExpr + + object InvocationExpression extends BaseExpr + + object Argument extends BaseExpr + + object ArgumentList extends BaseExpr + + trait MemberAccessExpr extends BaseExpr + + object SimpleMemberAccessExpression extends MemberAccessExpr } /** The JSON key values, in alphabetical order. @@ -122,35 +133,38 @@ object DotNetJsonAst { object ParserKeys { val AstRoot = "AstRoot" + val Arguments = "Arguments" + val ArgumentList = "ArgumentList" val Body = "Body" val Code = "Code" val ColumnStart = "ColumnStart" val ColumnEnd = "ColumnEnd" val Declaration = "Declaration" val ElementType = "ElementType" + val Expression = "Expression" val FileName = "FileName" val Identifier = "Identifier" val Initializer = "Initializer" - val MetaData = "MetaData" val Keyword = "Keyword" val Kind = "Kind" val Left = "Left" val LineStart = "LineStart" val LineEnd = "LineEnd" + val MetaData = "MetaData" val Members = "Members" val Modifiers = "Modifiers" val Name = "Name" + val Operand = "Operand" + val OperatorToken = "OperatorToken" val Parameters = "Parameters" val ParameterList = "ParameterList" + val Statement = "Statement" + val Statements = "Statements" val ReturnType = "ReturnType" val Right = "Right" val Type = "Type" val Usings = "Usings" val Value = "Value" val Variables = "Variables" - val Statements = "Statements" - val Expression = "Expression" - val OperatorToken = "OperatorToken" - val Operand = "Operand" - val Statement = "Statement" + } diff --git a/joern-cli/frontends/csharpsrc2cpg/src/test/scala/io/joern/csharpsrc2cpg/querying/ast/CallTests.scala b/joern-cli/frontends/csharpsrc2cpg/src/test/scala/io/joern/csharpsrc2cpg/querying/ast/CallTests.scala new file mode 100644 index 000000000000..639cdc206abe --- /dev/null +++ b/joern-cli/frontends/csharpsrc2cpg/src/test/scala/io/joern/csharpsrc2cpg/querying/ast/CallTests.scala @@ -0,0 +1,41 @@ +package io.joern.csharpsrc2cpg.querying.ast + +import io.joern.csharpsrc2cpg.testfixtures.CSharpCode2CpgFixture +import io.shiftleft.codepropertygraph.generated.nodes.{Identifier, Literal} +import io.shiftleft.semanticcpg.language.* + +class CallTests extends CSharpCode2CpgFixture { + + "builtin calls" should { + + val cpg = code(basicBoilerplate()) + + "create a call node with arguments" in { + val writeLine = cpg.call.nameExact("WriteLine").headOption match + case Some(callNode) => callNode + case None => fail("Node not found!") + + writeLine.name shouldBe "WriteLine" + // writeLine.methodFullName shouldBe "System.Console.WriteLine" TODO: Handle when call graph is being done + // writeLine.typeFullName shouldBe "void" + writeLine.code shouldBe "Console.WriteLine(\"Hello, world!\")" + + inside(writeLine.argument.l) { + case (base: Identifier) :: (strArg: Literal) :: Nil => +// base.typeFullName shouldBe "System.Console" + base.name shouldBe "Console" + base.code shouldBe "Console" + base.argumentIndex shouldBe 0 + + strArg.typeFullName shouldBe "System.String" + strArg.code shouldBe "\"Hello, world!\"" + strArg.argumentIndex shouldBe 1 + case _ => fail("Arguments malformed or not found!") + } + } + + "be resolve a method full name without the definition clearly defined" in {} + + } + +} diff --git a/joern-cli/frontends/csharpsrc2cpg/src/test/scala/io/joern/csharpsrc2cpg/testfixtures/CSharpCode2CpgFixture.scala b/joern-cli/frontends/csharpsrc2cpg/src/test/scala/io/joern/csharpsrc2cpg/testfixtures/CSharpCode2CpgFixture.scala index c3bdcb5aec81..e6138c222410 100644 --- a/joern-cli/frontends/csharpsrc2cpg/src/test/scala/io/joern/csharpsrc2cpg/testfixtures/CSharpCode2CpgFixture.scala +++ b/joern-cli/frontends/csharpsrc2cpg/src/test/scala/io/joern/csharpsrc2cpg/testfixtures/CSharpCode2CpgFixture.scala @@ -8,6 +8,8 @@ import io.joern.x2cpg.testfixtures.{Code2CpgFixture, DefaultTestCpg, LanguageFro import io.joern.x2cpg.{ValidationMode, X2Cpg} import io.shiftleft.codepropertygraph.generated.Cpg import io.shiftleft.semanticcpg.language.{ICallResolver, NoResolve} +import io.shiftleft.semanticcpg.layers.LayerCreatorContext +import org.scalatest.Inside import java.io.File @@ -21,7 +23,8 @@ class CSharpCode2CpgFixture( .withExtraFlows(extraFlows) .withPostProcessingPasses(withPostProcessing) ) - with SemanticCpgTestFixture(extraFlows) { + with SemanticCpgTestFixture(extraFlows) + with Inside { implicit val resolver: ICallResolver = NoResolve diff --git a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/datastructures/Scope.scala b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/datastructures/Scope.scala index 5a92bc11d9eb..a9842648fa90 100644 --- a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/datastructures/Scope.scala +++ b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/datastructures/Scope.scala @@ -42,4 +42,6 @@ class Scope[I, V, S] { } } + def size: Int = stack.size + }