diff --git a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForExpressionsCreator.scala b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForExpressionsCreator.scala index d946a18b1aea..d7bd9693b39f 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForExpressionsCreator.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForExpressionsCreator.scala @@ -49,6 +49,7 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { case node: ProcOrLambdaExpr => astForProcOrLambdaExpr(node) case node: RubyCallWithBlock[_] => astsForCallWithBlockInExpr(node) case node: SelfIdentifier => astForSelfIdentifier(node) + case node: BreakStatement => astForBreakStatement(node) case node: StatementList => astForStatementList(node) case node: DummyNode => Ast(node.node) case _ => astForUnknown(node) diff --git a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForStatementsCreator.scala b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForStatementsCreator.scala index 5873c2d1c7b3..67ca26dcaad6 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForStatementsCreator.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForStatementsCreator.scala @@ -319,7 +319,7 @@ trait AstForStatementsCreator(implicit withSchemaValidation: ValidationMode) { t returnAst(returnNode(node, code(node)), List(astForMemberCall(node))) } - private def astForBreakStatement(node: BreakStatement): Ast = { + protected def astForBreakStatement(node: BreakStatement): Ast = { val _node = NewControlStructure() .controlStructureType(ControlStructureTypes.BREAK) .lineNumber(line(node)) @@ -386,8 +386,9 @@ trait AstForStatementsCreator(implicit withSchemaValidation: ValidationMode) { t elseClause.map(transform).orElse(defaultElseBranch(node.span)), ensureClause )(node.span) - case WhileExpression(condition, body) => WhileExpression(condition, transform(body))(node.span) - case UntilExpression(condition, body) => UntilExpression(condition, transform(body))(node.span) + case WhileExpression(condition, body) => WhileExpression(condition, transform(body))(node.span) + case DoWhileExpression(condition, body) => DoWhileExpression(condition, transform(body))(node.span) + case UntilExpression(condition, body) => UntilExpression(condition, transform(body))(node.span) case IfExpression(condition, thenClause, elsifClauses, elseClause) => IfExpression( condition, diff --git a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/RubyIntermediateAst.scala b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/RubyIntermediateAst.scala index 2e12ec550247..7ce3bb8b35ed 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/RubyIntermediateAst.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/RubyIntermediateAst.scala @@ -3,6 +3,8 @@ package io.joern.rubysrc2cpg.astcreation import io.joern.rubysrc2cpg.passes.Defines import io.shiftleft.codepropertygraph.generated.nodes.NewNode +import scala.annotation.tailrec + object RubyIntermediateAst { case class TextSpan( diff --git a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/parser/RubyNodeCreator.scala b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/parser/RubyNodeCreator.scala index 7d79737993d8..30a1b91bb40f 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/parser/RubyNodeCreator.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/parser/RubyNodeCreator.scala @@ -550,6 +550,19 @@ class RubyNodeCreator extends RubyParserBaseVisitor[RubyNode] { override def visitMethodCallWithBlockExpression(ctx: RubyParser.MethodCallWithBlockExpressionContext): RubyNode = { ctx.methodIdentifier().getText match { case Defines.Proc | Defines.Lambda => ProcOrLambdaExpr(visit(ctx.block()).asInstanceOf[Block])(ctx.toTextSpan) + case Defines.Loop => + DoWhileExpression( + SimpleIdentifier(Option(Defines.getBuiltInType(Defines.TrueClass)))( + ctx.methodIdentifier().toTextSpan.spanStart("true") + ), + ctx.block() match { + case b: RubyParser.DoBlockBlockContext => + visit(b.doBlock().bodyStatement()) + case y => + logger.warn(s"Unexpected loop block body ${y.getClass}") + visit(ctx.block()) + } + )(ctx.toTextSpan) case _ => SimpleCallWithBlock(visit(ctx.methodIdentifier()), List(), visit(ctx.block()).asInstanceOf[Block])( ctx.toTextSpan diff --git a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/passes/Defines.scala b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/passes/Defines.scala index 0c6b3091009d..1b5e910c90fd 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/passes/Defines.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/passes/Defines.scala @@ -22,6 +22,7 @@ object Defines { val Lambda: String = "lambda" val Proc: String = "proc" val This: String = "this" + val Loop: String = "loop" val Program: String = ":program" diff --git a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/dataflow/MethodTests.scala b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/dataflow/MethodTests.scala index ac0dc2b1c759..ea25ff35f2c0 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/dataflow/MethodTests.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/dataflow/MethodTests.scala @@ -25,8 +25,7 @@ class MethodTests extends RubyCode2CpgFixture(withPostProcessing = true, withDat sink.reachableByFlows(src).l.size shouldBe 2 } - // Works in deprecated - "Data flow through do-while loop" ignore { + "Data flow through do-while loop" in { val cpg = code(""" |x = 0 |num = -1 @@ -42,7 +41,7 @@ class MethodTests extends RubyCode2CpgFixture(withPostProcessing = true, withDat val source = cpg.identifier.name("x").l val sink = cpg.call.name("puts").l - sink.reachableByFlows(source).l.size shouldBe 2 + sink.reachableByFlows(source).size shouldBe 5 } "Data flow through methodOnlyIdentifier usage" in { diff --git a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/ControlStructureTests.scala b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/ControlStructureTests.scala index d0c8116e5abe..07efd412b63a 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/ControlStructureTests.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/ControlStructureTests.scala @@ -76,6 +76,25 @@ class ControlStructureTests extends RubyCode2CpgFixture { assignment.lineNumber shouldBe Some(4) } + "a break expression nested in a control structure should be represented" in { + val cpg = code(""" + |x = 0 + |num = -1 + |loop do + | num = x + 1 + | x = x + 1 + | if x > 10 + | break + | end + |end + |puts num + |""".stripMargin) + + val List(breakNode) = cpg.break.l + breakNode.code shouldBe "break" + breakNode.lineNumber shouldBe Some(8) + } + "`if-end` statement is represented by an `IF` CONTROL_STRUCTURE node" in { val cpg = code(""" |if __LINE__ > 1 then diff --git a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/MethodTests.scala b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/MethodTests.scala index ac2817edf274..0f15998b9c60 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/MethodTests.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/MethodTests.scala @@ -419,7 +419,7 @@ class MethodTests extends RubyCode2CpgFixture { "break unless statement" should { val cpg = code(""" | def foo - | loop do + | bar do | break unless 1 < 2 | end | end