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 2b269bec8e5a..90291c57fcb6 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 @@ -27,7 +27,6 @@ class AstCreator( with AstForPrimitivesCreator with AstForExpressionsCreator with AstForStatementsCreator - with AstForControlStructuresCreator with AstSummaryVisitor with AstGenNodeBuilder[AstCreator] { diff --git a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForControlStructuresCreator.scala b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForControlStructuresCreator.scala deleted file mode 100644 index 2558efea8a27..000000000000 --- a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForControlStructuresCreator.scala +++ /dev/null @@ -1,76 +0,0 @@ -package io.joern.csharpsrc2cpg.astcreation - -import io.joern.csharpsrc2cpg.CSharpOperators -import io.joern.csharpsrc2cpg.parser.{DotNetNodeInfo, ParserKeys} -import io.joern.x2cpg.{Ast, ValidationMode} -import io.shiftleft.codepropertygraph.generated.ControlStructureTypes -import io.shiftleft.codepropertygraph.generated.nodes.{NewBlock, NewControlStructure} - -import scala.util.{Failure, Success, Try} - -trait AstForControlStructuresCreator(implicit withSchemaValidation: ValidationMode) { this: AstCreator => - - protected def astForThrowStatement(throwStmt: DotNetNodeInfo): Seq[Ast] = { - val expr = createDotNetNodeInfo(throwStmt.json(ParserKeys.Expression)) - val args = astForNode(expr) - val throwCall = createCallNodeForOperator( - throwStmt, - CSharpOperators.throws, - typeFullName = Option(getTypeFullNameFromAstNode(args)) - ) - Seq(callAst(throwCall, args)) - } - - protected def astForTryStatement(tryStmt: DotNetNodeInfo): Seq[Ast] = { - val tryNode = NewControlStructure() - .controlStructureType(ControlStructureTypes.TRY) - .code("try") - .lineNumber(line(tryStmt)) - .columnNumber(column(tryStmt)) - val tryAst = astForBlock(createDotNetNodeInfo(tryStmt.json(ParserKeys.Block)), Option("try")) - - val catchAsts = Try(tryStmt.json(ParserKeys.Catches)).map(_.arr.flatMap(astForNode).toSeq).getOrElse(Seq.empty) - val catchBlock = Option - .when(catchAsts.nonEmpty) { - Ast(blockNode(tryStmt).code("catch")).withChildren(catchAsts) - } - .toList - - val finallyBlock = Try(createDotNetNodeInfo(tryStmt.json(ParserKeys.Finally))).map(astForFinallyClause) match - case Success(finallyAst :: Nil) => - finallyAst.root.collect { case x: NewBlock => x.code("finally") } - finallyAst - case _ => Ast() - - val controlStructureAst = Ast(tryNode) - .withChild(tryAst) - .withChildren(catchBlock) - .withChild(finallyBlock) - - Seq(controlStructureAst) - } - - protected def astForFinallyClause(finallyClause: DotNetNodeInfo): Seq[Ast] = { - Seq(astForBlock(createDotNetNodeInfo(finallyClause.json(ParserKeys.Block)), code = Option(code(finallyClause)))) - } - - protected def astForCatchClause(catchClause: DotNetNodeInfo): Seq[Ast] = { - val declAst = astForNode(catchClause.json(ParserKeys.Declaration)).toList - val blockAst = astForBlock( - createDotNetNodeInfo(catchClause.json(ParserKeys.Block)), - code = Option(code(catchClause)), - prefixAsts = declAst - ) - Seq(blockAst) - } - - protected def astForCatchDeclaration(catchDeclaration: DotNetNodeInfo): Seq[Ast] = { - val name = nameFromNode(catchDeclaration) - val typeFullName = nodeTypeFullName(catchDeclaration) - val _localNode = localNode(catchDeclaration, name, name, typeFullName) - val localNodeAst = Ast(_localNode) - scope.addToScope(name, _localNode) - Seq(localNodeAst) - } - -} 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 2a1819a0206a..0915fb5fb893 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 @@ -21,7 +21,7 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { } def astForExpression(expr: DotNetNodeInfo): Seq[Ast] = { - expr.node match + expr.node match { case _: UnaryExpr => astForUnaryExpression(expr) case _: BinaryExpr => astForBinaryExpression(expr) case _: LiteralExpr => astForLiteralExpression(expr) @@ -39,6 +39,7 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { case SuppressNullableWarningExpression => astForSuppressNullableWarningExpression(expr) case _: BaseLambdaExpression => astForSimpleLambdaExpression(expr) case _ => notHandledYet(expr) + } } private def astForAwaitExpression(awaitExpr: DotNetNodeInfo): Seq[Ast] = { diff --git a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForStatementsCreator.scala b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForStatementsCreator.scala index 3fd71c0f1269..a1cdd8678373 100644 --- a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForStatementsCreator.scala +++ b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/astcreation/AstForStatementsCreator.scala @@ -1,39 +1,14 @@ package io.joern.csharpsrc2cpg.astcreation -import io.circe.Json -import io.joern.csharpsrc2cpg.parser.DotNetJsonAst.{ - BinaryExpr, - Block, - BreakStatement, - CasePatternSwitchLabel, - CaseSwitchLabel, - ContinueStatement, - DefaultSwitchLabel, - DoStatement, - ExpressionStatement, - ForEachStatement, - ForStatement, - GenericName, - GlobalStatement, - GotoStatement, - IfStatement, - JumpStatement, - LiteralExpr, - ReturnStatement, - SwitchStatement, - ThrowStatement, - TryStatement, - UnaryExpr, - WhileStatement -} +import io.joern.csharpsrc2cpg.CSharpOperators +import io.joern.csharpsrc2cpg.parser.DotNetJsonAst.* import io.joern.csharpsrc2cpg.parser.{DotNetNodeInfo, ParserKeys} import io.joern.x2cpg.{Ast, ValidationMode} -import io.shiftleft.codepropertygraph.generated.ControlStructureTypes -import io.shiftleft.codepropertygraph.generated.nodes.ControlStructure +import io.shiftleft.codepropertygraph.generated.{ControlStructureTypes, DispatchTypes} +import io.shiftleft.codepropertygraph.generated.nodes.{NewBlock, NewControlStructure, NewIdentifier} import scala.:: -import scala.collection.mutable.ArrayBuffer -import scala.util.{Failure, Success, Try} +import scala.util.{Success, Try} trait AstForStatementsCreator(implicit withSchemaValidation: ValidationMode) { this: AstCreator => @@ -57,7 +32,7 @@ trait AstForStatementsCreator(implicit withSchemaValidation: ValidationMode) { t } protected def astForStatement(nodeInfo: DotNetNodeInfo): Seq[Ast] = { - nodeInfo.node match + nodeInfo.node match { case ExpressionStatement => astForExpressionStatement(nodeInfo) case GlobalStatement => astForGlobalStatement(nodeInfo) case IfStatement => astForIfStatement(nodeInfo) @@ -68,8 +43,10 @@ trait AstForStatementsCreator(implicit withSchemaValidation: ValidationMode) { t case DoStatement => astForDoStatement(nodeInfo) case WhileStatement => astForWhileStatement(nodeInfo) case SwitchStatement => astForSwitchStatement(nodeInfo) + case UsingStatement => astForUsingStatement(nodeInfo) case _: JumpStatement => astForJumpStatement(nodeInfo) case _ => notHandledYet(nodeInfo) + } } private def astForSwitchLabel(labelNode: DotNetNodeInfo): Seq[Ast] = { @@ -222,4 +199,95 @@ trait AstForStatementsCreator(implicit withSchemaValidation: ValidationMode) { t val _returnNode = returnNode(returnStmt, returnStmt.code) Seq(returnAst(_returnNode, identifierAst)) } + + protected def astForThrowStatement(throwStmt: DotNetNodeInfo): Seq[Ast] = { + val expr = createDotNetNodeInfo(throwStmt.json(ParserKeys.Expression)) + val args = astForNode(expr) + val throwCall = createCallNodeForOperator( + throwStmt, + CSharpOperators.throws, + typeFullName = Option(getTypeFullNameFromAstNode(args)) + ) + Seq(callAst(throwCall, args)) + } + + protected def astForTryStatement(tryStmt: DotNetNodeInfo): Seq[Ast] = { + val tryNode = NewControlStructure() + .controlStructureType(ControlStructureTypes.TRY) + .code("try") + .lineNumber(line(tryStmt)) + .columnNumber(column(tryStmt)) + val tryAst = astForBlock(createDotNetNodeInfo(tryStmt.json(ParserKeys.Block)), Option("try")) + + val catchAsts = Try(tryStmt.json(ParserKeys.Catches)).map(_.arr.flatMap(astForNode).toSeq).getOrElse(Seq.empty) + + val finallyBlock = Try(createDotNetNodeInfo(tryStmt.json(ParserKeys.Finally))).map(astForFinallyClause) match + case Success(finallyAst :: Nil) => + finallyAst.root.collect { case x: NewBlock => x.code("finally") } + finallyAst + case _ => Ast() + + val controlStructureAst = tryCatchAst(tryNode, tryAst, catchAsts, Option(finallyBlock)) + Seq(controlStructureAst) + } + + protected def astForFinallyClause(finallyClause: DotNetNodeInfo): Seq[Ast] = { + Seq(astForBlock(createDotNetNodeInfo(finallyClause.json(ParserKeys.Block)), code = Option(code(finallyClause)))) + } + + /** Variables using the IDisposable interface may + * be used in `using`, where a call to `Dispose` is guaranteed. + * + * Thus, this is lowered as a try-finally, with finally making a call to `Dispose` on the declared variable. + */ + private def astForUsingStatement(usingStmt: DotNetNodeInfo): Seq[Ast] = { + val tryNode = NewControlStructure() + .controlStructureType(ControlStructureTypes.TRY) + .code("try") + .lineNumber(line(usingStmt)) + .columnNumber(column(usingStmt)) + val tryAst = astForBlock(createDotNetNodeInfo(usingStmt.json(ParserKeys.Statement)), Option("try")) + val declNode = createDotNetNodeInfo(usingStmt.json(ParserKeys.Declaration)) + val declAst = astForNode(declNode) + + val finallyAst = declAst.flatMap(_.nodes).collectFirst { case x: NewIdentifier => x.copy }.map { id => + val callCode = s"${id.name}.Dispose()" + id.code(callCode) + val disposeCall = callNode( + usingStmt, + callCode, + "Dispose", + "System.Disposable.Dispose:System.Void()", + DispatchTypes.DYNAMIC_DISPATCH, + Option("System.Void()"), + Option("System.Void") + ) + val disposeAst = callAst(disposeCall, receiver = Option(Ast(id))) + Ast(blockNode(usingStmt).code("finally")) + .withChild(disposeAst) + } + + declAst :+ tryCatchAst(tryNode, tryAst, Seq.empty, finallyAst) + } + + protected def astForCatchClause(catchClause: DotNetNodeInfo): Seq[Ast] = { + val declAst = astForNode(catchClause.json(ParserKeys.Declaration)).toList + val blockAst = astForBlock( + createDotNetNodeInfo(catchClause.json(ParserKeys.Block)), + code = Option(code(catchClause)), + prefixAsts = declAst + ) + Seq(blockAst) + } + + protected def astForCatchDeclaration(catchDeclaration: DotNetNodeInfo): Seq[Ast] = { + val name = nameFromNode(catchDeclaration) + val typeFullName = nodeTypeFullName(catchDeclaration) + val _localNode = localNode(catchDeclaration, name, name, typeFullName) + val localNodeAst = Ast(_localNode) + scope.addToScope(name, _localNode) + Seq(localNodeAst) + } + } 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 b0ad7bee9dd9..d5ece496724a 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 @@ -194,6 +194,8 @@ object DotNetJsonAst { object SwitchSection extends BaseExpr + object UsingStatement extends BaseStmt + object RelationalPattern extends BasePattern object ConstantPattern extends BasePattern diff --git a/joern-cli/frontends/csharpsrc2cpg/src/test/scala/io/joern/csharpsrc2cpg/querying/ast/ControlStructureTests.scala b/joern-cli/frontends/csharpsrc2cpg/src/test/scala/io/joern/csharpsrc2cpg/querying/ast/ControlStructureTests.scala index 5a19c4f9d374..53565f2fb15d 100644 --- a/joern-cli/frontends/csharpsrc2cpg/src/test/scala/io/joern/csharpsrc2cpg/querying/ast/ControlStructureTests.scala +++ b/joern-cli/frontends/csharpsrc2cpg/src/test/scala/io/joern/csharpsrc2cpg/querying/ast/ControlStructureTests.scala @@ -57,7 +57,7 @@ class ControlStructureTests extends CSharpCode2CpgFixture { inside(tryBlocks) { case t :: c :: f :: Nil => t.code shouldBe "try" - c.code shouldBe "catch" + c.code shouldBe "catch (Exception e)" f.code shouldBe "finally" case _ => fail("Invalid number of children under the `try` control structure!") } @@ -77,16 +77,12 @@ class ControlStructureTests extends CSharpCode2CpgFixture { "generate a catch-block with a child block containing the correct console call" in { inside(Option(tryBlocks(1))) { - case Some(ctl) => - inside(ctl.astChildren.l) { - case (catchBlock: Block) :: Nil => - inside(catchBlock.astChildren.l) { - case (excepDecl: Local) :: (consoleCall: Call) :: Nil => - excepDecl.name shouldBe "e" - consoleCall.code shouldBe "Console.WriteLine(\"Uh, oh!\")" - case _ => fail("Invalid `catch` block children!") - } - case _ => fail("No `catch` handler block found!") + case Some(catchBlock: Block) => + inside(catchBlock.astChildren.l) { + case (excepDecl: Local) :: (consoleCall: Call) :: Nil => + excepDecl.name shouldBe "e" + consoleCall.code shouldBe "Console.WriteLine(\"Uh, oh!\")" + case _ => fail("Invalid `catch` block children!") } case None => fail("No `catch` block found!") } @@ -183,4 +179,47 @@ class ControlStructureTests extends CSharpCode2CpgFixture { } + "a using statement" should { + val cpg = code(basicBoilerplate(""" + |var numbers = new List(); + |using (StreamReader reader = File.OpenText("numbers.txt")) + |{ + | string line; + | while ((line = reader.ReadLine()) is not null) + | { + | if (int.TryParse(line, out int number)) + | { + | numbers.Add(number); + | } + | } + |} + |""".stripMargin)) + + val tryBlocks = cpg.controlStructure.controlStructureTypeExact(ControlStructureTypes.TRY).astChildren.isBlock.l + + "generate a try control structure with two children blocks" in { + inside(tryBlocks) { + case t :: f :: Nil => + t.code shouldBe "try" + f.code shouldBe "finally" + case _ => fail("Invalid number of children under the `try` control structure!") + } + } + + "generate a finally-block with the correct dispose call" in { + inside(tryBlocks.lastOption) { + case Some(ctl) => + inside(ctl.astChildren.isCall.l) { + case disposeCall :: Nil => + disposeCall.code shouldBe "reader.Dispose()" + disposeCall.name shouldBe "Dispose" + disposeCall.methodFullName shouldBe "System.Disposable.Dispose:System.Void()" + case _ => fail("No call node found!") + } + case None => fail("No `finally` block found!") + } + } + + } + }