Skip to content

Commit

Permalink
[c#] lower get accessor declarations to get_* methods
Browse files Browse the repository at this point in the history
  • Loading branch information
xavierpinho committed Jan 28, 2025
1 parent 5ef163c commit 4f062a7
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -547,13 +547,36 @@ trait AstForDeclarationsCreator(implicit withSchemaValidation: ValidationMode) {
}

protected def astForPropertyDeclaration(propertyDecl: DotNetNodeInfo): Seq[Ast] = {
val propertyName = nameFromNode(propertyDecl)
val modifierAst = astForModifiers(propertyDecl)
val typeFullName = nodeTypeFullName(propertyDecl)
val accessorList = createDotNetNodeInfo(propertyDecl.json(ParserKeys.AccessorList))
val accessors = accessorList.json(ParserKeys.Accessors).arr.map(createDotNetNodeInfo)
accessors.flatMap(astForPropertyAccessor(_, propertyDecl)).toList
}

private def astForPropertyAccessor(accessorDecl: DotNetNodeInfo, propertyDecl: DotNetNodeInfo): Seq[Ast] = {
accessorDecl.node match
case GetAccessorDeclaration => astForGetAccessorDeclaration(accessorDecl, propertyDecl)
case _ =>
logger.warn(s"Unhandled property accessor '${accessorDecl.node}''")
Nil
}

val _memberNode = memberNode(propertyDecl, propertyName, propertyDecl.code, typeFullName)
private def astForGetAccessorDeclaration(accessorDecl: DotNetNodeInfo, propertyDecl: DotNetNodeInfo): Seq[Ast] = {
val name = s"get_${nameFromNode(propertyDecl)}"
val modifiers = modifiersForNode(propertyDecl)
val returnType = nodeTypeFullName(propertyDecl)
val baseType = scope.surroundingTypeDeclFullName.getOrElse(Defines.UnresolvedNamespace)
val isStatic = modifiers.exists(_.modifierType == ModifierTypes.STATIC)
val parameters = if isStatic then Nil else astForThisParameter(propertyDecl) :: Nil
val signature = composeMethodLikeSignature(
returnType,
parameters.flatMap(_.nodes.collectFirst { case x: NewMethodParameterIn => x.typeFullName })
)
val fullName = composeMethodFullName(baseType, name, signature)
val body = Ast(blockNode(accessorDecl))
val methodReturn = methodReturnNode(accessorDecl, returnType)
val methodNode_ = methodNode(accessorDecl, name, fullName, signature, relativeFileName)

Seq(Ast(_memberNode).withChildren(modifierAst))
methodAst(methodNode_, parameters, body, methodReturn, modifiers) :: Nil
}

/** Creates an AST for a simple `x => { ... }` style lambda expression
Expand Down Expand Up @@ -606,7 +629,7 @@ trait AstForDeclarationsCreator(implicit withSchemaValidation: ValidationMode) {
Option(NodeTypes.METHOD),
scope.surroundingScopeFullName
)
val modifiers = modifiersForNode(lambdaExpression)
val modifiers = astForModifiers(lambdaExpression).flatMap(_.nodes).collect { case x: NewModifier => x }
val lambdaReturnType = body.lastOption
.getOrElse(Ast())
.nodes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,12 +280,20 @@ object DotNetJsonAst {

object Unknown extends DotNetParserNode

object AccessorList extends DotNetParserNode

object GetAccessorDeclaration extends DotNetParserNode

object SetAccessorDeclaration extends DotNetParserNode

}

/** The JSON key values, in alphabetical order.
*/
object ParserKeys {

val AccessorList = "AccessorList"
val Accessors = "Accessors"
val AstRoot = "AstRoot"
val Arguments = "Arguments"
val ArgumentList = "ArgumentList"
Expand All @@ -311,6 +319,7 @@ object ParserKeys {
val ExpressionBody = "ExpressionBody"
val Finally = "Finally"
val FileName = "FileName"
val GetAccessorDeclaration = "GetAccessorDeclaration"
val Identifier = "Identifier"
val Incrementors = "Incrementors"
val Initializer = "Initializer"
Expand All @@ -333,6 +342,7 @@ object ParserKeys {
val ParameterList = "ParameterList"
val Pattern = "Pattern"
val Sections = "Sections"
val SetAccessorDeclaration = "SetAccessorDeclaration"
val SingleVariableDesignation = "SingleVariableDesignation"
val Statement = "Statement"
val Statements = "Statements"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import io.shiftleft.codepropertygraph.generated.nodes.{Call, Identifier}
import io.shiftleft.semanticcpg.language.*

class MemberAccessTests extends CSharpCode2CpgFixture {
"conditional member access expressions" should {

// TODO: This test-case relies on the usage of getters, that are currently being
// reworked to be METHODs instead of MEMBERs. In particular, `bar?.Qux` should
// resemble `bar.get_Qux()`. We need to adapt astForMemberBindingExpression
// to accommodate this.
"conditional property access expressions" ignore {
val cpg = code("""
|namespace Foo {
| public class Baz {
Expand Down Expand Up @@ -35,6 +40,35 @@ class MemberAccessTests extends CSharpCode2CpgFixture {
}
}

"conditional member access expressions" should {
val cpg = code("""
|namespace Foo {
| public class Baz {
| public int Qux;
| }
| public class Bar {
| public static void Main() {
| var baz = new Baz();
| var a = baz?.Qux;
| }
| }
|}
|""".stripMargin)

"have correct types both on the LHS and RHS" in {
inside(cpg.assignment.l.sortBy(_.lineNumber).drop(1)) {
case a :: Nil =>
inside(a.argument.l) {
case (lhs: Identifier) :: (rhs: Call) :: Nil =>
lhs.typeFullName shouldBe BuiltinTypes.DotNetTypeMap(BuiltinTypes.Int)
rhs.typeFullName shouldBe BuiltinTypes.DotNetTypeMap(BuiltinTypes.Int)
case _ => fail("Expected 2 arguments under the assignment call.")
}
case _ => fail("Expected 1 assignment call.")
}
}
}

"conditional method access expressions" should {
val cpg = code("""
|namespace Foo {
Expand Down Expand Up @@ -216,7 +250,11 @@ class MemberAccessTests extends CSharpCode2CpgFixture {
}
}

"conditional method access expression for chained fields" should {
// TODO: ConditionalAccessExpressions need some work to deal with nested chains.
// This particular test-case relies on the usage of getters, that are currently being
// reworked to be METHODs instead of MEMBERs.
// Revisit this test-case once getters are finished.
"conditional property access expression for chained fields" ignore {
val cpg = code("""
|namespace Foo {
| public class Baz {
Expand Down Expand Up @@ -245,6 +283,35 @@ class MemberAccessTests extends CSharpCode2CpgFixture {
}
}

"conditional method access expression for chained fields" should {
val cpg = code("""
|namespace Foo {
| public class Baz {
| public Baz Qux;
| }
| public class Bar {
| public static void Main() {
| var baz = new Baz();
| var b = baz?.Qux?.Qux;
| }
| }
|}
|""".stripMargin)

"have correct types and attributes both on the LHS and RHS" in {
inside(cpg.assignment.l.sortBy(_.lineNumber).drop(1).l) {
case a :: Nil =>
inside(a.argument.l) {
case (lhs: Identifier) :: (rhs: Call) :: Nil =>
lhs.typeFullName shouldBe "Foo.Baz"
rhs.typeFullName shouldBe "Foo.Baz"
case _ => fail("Expected 2 arguments under the assignment call")
}
case _ => fail("Expected 1 assignment call.")
}
}
}

"combination of method access expression for chained fields" should {
val cpg = code("""
|namespace Foo {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,9 @@ class MemberTests extends CSharpCode2CpgFixture {
}
}

"a basic class declaration with a PropertyDeclaration member" should {
// TODO: Getters/Setters are currently being lowered into get_/set_ methods.
// Adapt this unit-test once that is finished.
"a basic class declaration with a PropertyDeclaration member" ignore {
val cpg = code("""
|public class Foo {
| public int Bar {get; set;}
Expand All @@ -466,6 +468,28 @@ class MemberTests extends CSharpCode2CpgFixture {
}
}

"a basic class declaration with a FieldDeclaration member" should {
val cpg = code("""
|public class Foo {
| public int Bar;
|}
|""".stripMargin)

"create a member for Bar with appropriate properties" in {
inside(cpg.typeDecl.nameExact("Foo").l) {
case fooClass :: Nil =>
inside(fooClass.astChildren.isMember.nameExact("Bar").l) {
case bar :: Nil =>
bar.code shouldBe "int Bar"
bar.typeFullName shouldBe "System.Int32"
bar.astParent shouldBe fooClass
case _ => fail("No member named Bar found inside Foo")
}
case _ => fail("No class named Foo found.")
}
}
}

"a member with a external type" should {
val cpg = code("""
|using Microsoft.Extensions.Logging;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.joern.csharpsrc2cpg.querying.ast

import io.joern.csharpsrc2cpg.testfixtures.CSharpCode2CpgFixture
import io.shiftleft.codepropertygraph.generated.DispatchTypes
import io.shiftleft.codepropertygraph.generated.{DispatchTypes, ModifierTypes}
import io.shiftleft.codepropertygraph.generated.nodes.{Call, Identifier, Literal}
import io.shiftleft.semanticcpg.language.*

Expand Down Expand Up @@ -113,4 +113,146 @@ class PropertyGetterTests extends CSharpCode2CpgFixture {
}
}
}

"uninitialized get-only property declaration" should {
val cpg = code("""
|class C
|{
| public int MyProperty { get; }
|}
|""".stripMargin)

"be lowered into a get_* method" in {
inside(cpg.method.nameExact("get_MyProperty").l) {
case method :: Nil =>
method.fullName shouldBe "C.get_MyProperty:System.Int32(C)"
method.signature shouldBe "System.Int32(C)"
case xs => fail(s"Expected single get_MyProperty method, but got $xs")
}
}

"have correct modifiers" in {
cpg.method.nameExact("get_MyProperty").modifier.modifierType.sorted.l shouldBe List(ModifierTypes.PUBLIC)
}

"have correct parameters" in {
inside(cpg.method.nameExact("get_MyProperty").parameter.l) {
case thisParam :: Nil =>
thisParam.typeFullName shouldBe "C"
thisParam.name shouldBe "this"
case xs => fail(s"Expected this parameter for get_MyProperty, but got $xs")
}
}

"have empty body" in {
cpg.method.nameExact("get_MyProperty").body.astChildren shouldBe empty
}
}

"assignment whose RHS is a get-only property declared in the source-code" should {
val cpg = code("""
|class C { public int MyProperty {get;} }
|class M
|{
| void Run()
| {
| var c = new C();
| var x = c.MyProperty;
| }
|}
|""".stripMargin)

"have a get_* method call on the RHS" in {
inside(cpg.assignment.where(_.target.isIdentifier.nameExact("x")).source.l) {
case (rhs: Call) :: Nil =>
rhs.code shouldBe "c.MyProperty"
rhs.name shouldBe "get_MyProperty"
rhs.methodFullName shouldBe "C.get_MyProperty:System.Int32(C)"
rhs.typeFullName shouldBe "System.Int32"
rhs.dispatchType shouldBe DispatchTypes.DYNAMIC_DISPATCH
case xs => fail(s"Expected single RHS call for the assignment of x, but got $xs")
}
}

"have correct arguments to the get_* call" in {
inside(cpg.call.codeExact("c.MyProperty").argument.sortBy(_.argumentIndex).l) {
case (baseArg: Identifier) :: Nil =>
baseArg.argumentIndex shouldBe 0
baseArg.code shouldBe "c"
baseArg.typeFullName shouldBe "C"
case xs => fail(s"Expected single identifier argument to c.MyProperty, but got $xs")
}
}

"have correct typeFullName for the assignment" in {
cpg.assignment.where(_.target.isIdentifier.nameExact("x")).typeFullName.l shouldBe List("System.Int32")
}
}

"assignment whose RHS is a static get-only property declared in the source-code" should {
val cpg = code("""
|class C { public static int MyProperty {get;} }
|class M
|{
| void Run()
| {
| var c = new C();
| var x = c.MyProperty;
| }
|}
|""".stripMargin)

"have a get_* method call on the RHS" in {
inside(cpg.assignment.where(_.target.isIdentifier.nameExact("x")).source.l) {
case (rhs: Call) :: Nil =>
rhs.code shouldBe "c.MyProperty"
rhs.name shouldBe "get_MyProperty"
rhs.methodFullName shouldBe "C.get_MyProperty:System.Int32()"
rhs.typeFullName shouldBe "System.Int32"
rhs.dispatchType shouldBe DispatchTypes.STATIC_DISPATCH
case xs => fail(s"Expected single RHS call for the assignment of x, but got $xs")
}
}

"have correct arguments to the get_* call" in {
cpg.call.codeExact("c.MyProperty").argument shouldBe empty
}

"have correct typeFullName for the assignment" in {
cpg.assignment.where(_.target.isIdentifier.nameExact("x")).typeFullName.l shouldBe List("System.Int32")
}
}

"uninitialized static get-only property declaration" should {
val cpg = code("""
|public class C
|{
| public static string MyProperty { get; }
|}
|""".stripMargin)

"be lowered into a get_* method" in {
inside(cpg.method.nameExact("get_MyProperty").l) {
case method :: Nil =>
method.fullName shouldBe "C.get_MyProperty:System.String()"
method.signature shouldBe "System.String()"
case xs => fail(s"Expected single get_MyProperty method, but got $xs")
}
}

"have correct modifiers" in {
cpg.method.nameExact("get_MyProperty").modifier.modifierType.sorted.l shouldBe List(
ModifierTypes.PUBLIC,
ModifierTypes.STATIC
)
}

"have no parameters" in {
cpg.method.nameExact("get_MyProperty").parameter shouldBe empty
}

"have empty body" in {
cpg.method.nameExact("get_MyProperty").body.astChildren shouldBe empty
}
}
}

0 comments on commit 4f062a7

Please sign in to comment.