diff --git a/joern-cli/frontends/rust2cpg/src/main/scala/io/joern/rust2cpg/astcreation/RustVisitor.scala b/joern-cli/frontends/rust2cpg/src/main/scala/io/joern/rust2cpg/astcreation/RustVisitor.scala index f8b3b7fab2c7..6db67a854774 100644 --- a/joern-cli/frontends/rust2cpg/src/main/scala/io/joern/rust2cpg/astcreation/RustVisitor.scala +++ b/joern-cli/frontends/rust2cpg/src/main/scala/io/joern/rust2cpg/astcreation/RustVisitor.scala @@ -1034,8 +1034,23 @@ trait RustVisitor(implicit withSchemaValidation: ValidationMode) { this: AstCrea // Attr* start:Expr? op:('..' | '..=') end:Expr? private def visitRangeExpr(rangeExpr: RangeExpr): Ast = { (rangeExpr.start, rangeExpr.end, rangeExpr.isInclusive) match { - // a..b becomes Range { start, end } - case (Some(start), Some(end), false) => + // NB: RangeInclusive (a..=b) has private start/end fields + a `new` constructor, whereas + // the remaining ranges have no constructor but public start/end fields. Hence the special + // treatment here. + case (Some(start), Some(end), true) => + // TODO(rust_ast_gen) matches the stdlib type, but it ideally would come from rust_ast_gen. + val methodFullName = "core::ops::range::RangeInclusive::new" + val call = callNode( + rangeExpr, + code(rangeExpr), + "new", + methodFullName, + DispatchTypes.STATIC_DISPATCH, + None, + Some(typeFullNameForExpr(rangeExpr)) + ) + callAst(call, Seq(visitExpr(start), visitExpr(end))) + case (start, end, _) => val rangeType = typeFullNameForExpr(rangeExpr) val tmpName = "tmp" allocBlockAst(rangeExpr, tmpName, rangeType) { mktmpIdent => @@ -1044,10 +1059,8 @@ trait RustVisitor(implicit withSchemaValidation: ValidationMode) { this: AstCrea val lhs = fieldAccessAst(expr, expr, mktmpIdent(expr), s"$tmpName.$fieldName", fieldName, fieldType) callAst(assignmentNode(expr, s"$tmpName.$fieldName = ${code(expr)}"), Seq(lhs, visitExpr(expr))) } - Seq(mkAssign("start", start), mkAssign("end", end)) + (start.map(mkAssign("start", _)) ++ end.map(mkAssign("end", _))).toSeq } - - case _ => notHandledYet(rangeExpr) } } diff --git a/joern-cli/frontends/rust2cpg/src/test/scala/io/joern/rust2cpg/passes/ast/RangeTests.scala b/joern-cli/frontends/rust2cpg/src/test/scala/io/joern/rust2cpg/passes/ast/RangeTests.scala index da3f324fd4d8..a0ae120db3e8 100644 --- a/joern-cli/frontends/rust2cpg/src/test/scala/io/joern/rust2cpg/passes/ast/RangeTests.scala +++ b/joern-cli/frontends/rust2cpg/src/test/scala/io/joern/rust2cpg/passes/ast/RangeTests.scala @@ -93,4 +93,171 @@ class RangeTests extends Rust2CpgSuite { } } } + + "`a..`" should { + val cpg = code(""" + |fn main() { + | 1..; + |} + |""".stripMargin) + + "lower into a block with appropriate type and number of children" in { + inside(cpg.method.name("main").body.astChildren.isBlock.l) { case block :: Nil => + block.code shouldBe "1.." + block.typeFullName shouldBe "core::ops::range::RangeFrom" + block.astChildren.size shouldBe 4 + } + } + + "the block's third child is a field assignment (start)" in { + inside(cpg.block.codeExact("1..").astChildren.order(3).l) { case (assign: Call) :: Nil => + assign.code shouldBe "tmp.start = 1" + + inside(assign.argument(1)) { case fieldAccess: Call => + fieldAccess.code shouldBe "tmp.start" + + inside(fieldAccess.argument(2)) { case fieldIdent: FieldIdentifier => + fieldIdent.canonicalName shouldBe "start" + } + } + + inside(assign.argument(2)) { case lit: Literal => + lit.code shouldBe "1" + lit.typeFullName shouldBe "i32" + } + } + } + } + + "`..b`" should { + val cpg = code(""" + |fn main() { + | ..2; + |} + |""".stripMargin) + + "lower into a block with appropriate type and number of children" in { + inside(cpg.method.name("main").body.astChildren.isBlock.l) { case block :: Nil => + block.code shouldBe "..2" + block.typeFullName shouldBe "core::ops::range::RangeTo" + block.astChildren.size shouldBe 4 + } + } + + "the block's third child is a field assignment (end)" in { + inside(cpg.block.codeExact("..2").astChildren.order(3).l) { case (assign: Call) :: Nil => + assign.code shouldBe "tmp.end = 2" + + inside(assign.argument(1)) { case fieldAccess: Call => + fieldAccess.code shouldBe "tmp.end" + + inside(fieldAccess.argument(2)) { case fieldIdent: FieldIdentifier => + fieldIdent.canonicalName shouldBe "end" + } + } + + inside(assign.argument(2)) { case lit: Literal => + lit.code shouldBe "2" + lit.typeFullName shouldBe "i32" + } + } + } + } + + "`..=b`" should { + val cpg = code(""" + |fn main() { + | ..=2; + |} + |""".stripMargin) + + "lower into a block with appropriate type and number of children" in { + inside(cpg.method.name("main").body.astChildren.isBlock.l) { case block :: Nil => + block.code shouldBe "..=2" + block.typeFullName shouldBe "core::ops::range::RangeToInclusive" + block.astChildren.size shouldBe 4 + } + } + + "the block's third child is a field assignment (end)" in { + inside(cpg.block.codeExact("..=2").astChildren.order(3).l) { case (assign: Call) :: Nil => + assign.code shouldBe "tmp.end = 2" + + inside(assign.argument(2)) { case lit: Literal => + lit.code shouldBe "2" + lit.typeFullName shouldBe "i32" + } + } + } + } + + "`..`" should { + val cpg = code(""" + |fn main() { + | ..; + |} + |""".stripMargin) + + "lower into a block with appropriate type and number of children" in { + inside(cpg.method.name("main").body.astChildren.isBlock.l) { case block :: Nil => + block.code shouldBe ".." + block.typeFullName shouldBe "core::ops::range::RangeFull" + block.astChildren.size shouldBe 3 + } + } + + "the block's first child is a LOCAL declaration" in { + inside(cpg.block.codeExact("..").astChildren.order(1).l) { case (local: Local) :: Nil => + local.name shouldBe "tmp" + local.typeFullName shouldBe "core::ops::range::RangeFull" + } + } + + "the block's second child is an alloc assignment" in { + inside(cpg.block.codeExact("..").astChildren.order(2).l) { case (assign: Call) :: Nil => + assign.code shouldBe s"tmp = ${Operators.alloc}" + } + } + + "the block's third child is an identifier" in { + inside(cpg.block.codeExact("..").astChildren.order(3).l) { case (ident: Identifier) :: Nil => + ident.name shouldBe "tmp" + ident.typeFullName shouldBe "core::ops::range::RangeFull" + } + } + } + + "`a..=b`" should { + val cpg = code(""" + |fn main() { + | let r = 1..=2; + |} + |""".stripMargin) + + "lower the assigned value into a static call to RangeInclusive::new" in { + inside(cpg.assignment.argument(2).l) { case (call: Call) :: Nil => + call.name shouldBe "new" + call.code shouldBe "1..=2" + call.methodFullName shouldBe "core::ops::range::RangeInclusive::new" + call.typeFullName shouldBe "core::ops::range::RangeInclusive" + call.dispatchType shouldBe DispatchTypes.STATIC_DISPATCH + } + } + + "the call's first argument is the start operand" in { + inside(cpg.call.methodFullNameExact("core::ops::range::RangeInclusive::new").argument(1).l) { + case (lit: Literal) :: Nil => + lit.code shouldBe "1" + lit.typeFullName shouldBe "i32" + } + } + + "the call's second argument is the end operand" in { + inside(cpg.call.methodFullNameExact("core::ops::range::RangeInclusive::new").argument(2).l) { + case (lit: Literal) :: Nil => + lit.code shouldBe "2" + lit.typeFullName shouldBe "i32" + } + } + } }