Repository: joernio/joern (scala)

Issue:
[Bug] [ruby] Match Error on `mastodon` Project
**Describe the bug**
When parsing `mastodon`, a match error is enountered here
```scala
// AstForExpressionsCreator
// ...
      case selfMethod: SingletonMethodDeclaration =>
        // Last element is the method declaration, the prefix methods would be `foo = def foo (...)` pointers in other
        // contexts, but this would be empty as a method call argument
        val Seq(_, methodDeclAst) = astForSingletonMethodDeclaration(selfMethod) // <--- error
        scope.surroundingTypeFullName.foreach { tfn =>
          methodDeclAst.root.collect { case m: NewMethod =>
            m.astParentType(NodeTypes.TYPE_DECL).astParentFullName(s"$tfn<class>")
          }
        }
```

**To Reproduce**
Steps to reproduce the behavior:
1. `git clone git@github.com:mastodon/mastodon.git --depth 1`
2. `joern-parse mastodon`
3. See error

Pull request:
Added handling for `private_class_method` and `public_class_method` access modifiers, making the function def that follows them have either `PRIVATE` or `PUBLIC` modifiers.

TODO: 
handle multiple args: It's possible to call `private_class_method` with a list of symbols representing function names. All the functions matching these symbols would then also have a PRIVATE access modifier.

Resolves #5061

Patch:
diff --git a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForFunctionsCreator.scala b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForFunctionsCreator.scala
index a8601327a0f2..52711017d0c7 100644
--- a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForFunctionsCreator.scala
+++ b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForFunctionsCreator.scala
@@ -180,6 +180,25 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th
     if isClosure || isSingletonObjectMethod then refs else createMethodRefPointer(method) :: Nil
   }
 
+  protected def astForMethodAccessModifier(node: MethodAccessModifier): Seq[Ast] = {
+    val originalAccessModifier = currentAccessModifier
+    popAccessModifier()
+
+    node match {
+      case _: PrivateMethodModifier =>
+        pushAccessModifier(ModifierTypes.PRIVATE)
+      case _: PublicMethodModifier =>
+        pushAccessModifier(ModifierTypes.PUBLIC)
+    }
+
+    val methodAst = astsForStatement(node.method)
+
+    popAccessModifier()
+    pushAccessModifier(originalAccessModifier)
+
+    methodAst
+  }
+
   private def transformAsClosureBody(refs: List[Ast], baseStmtBlockAst: Ast) = {
     // Determine which locals are captured
     val capturedLocalNodes = baseStmtBlockAst.nodes
@@ -440,7 +459,7 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th
             parameterAsts ++ anonProcParam,
             stmtBlockAst,
             methodReturnNode(node, Defines.Any),
-            newModifierNode(ModifierTypes.VIRTUAL) :: Nil
+            newModifierNode(ModifierTypes.VIRTUAL) :: newModifierNode(currentAccessModifier) :: Nil
           )
 
         _methodAst :: methodTypeDeclAst :: Nil foreach (Ast.storeInDiffGraph(_, diffGraph))
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 6fdedb808472..6b441a0302b5 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
@@ -24,6 +24,7 @@ trait AstForStatementsCreator(implicit withSchemaValidation: ValidationMode) { t
       case node: FieldsDeclaration          => astsForFieldDeclarations(node)
       case node: AccessModifier             => registerAccessModifier(node)
       case node: MethodDeclaration          => astForMethodDeclaration(node)
+      case node: MethodAccessModifier       => astForMethodAccessModifier(node)
       case node: SingletonMethodDeclaration => astForSingletonMethodDeclaration(node)
       case node: MultipleAssignment         => node.assignments.map(astForExpression)
       case node: BreakExpression            => astForBreakExpression(node) :: Nil
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 377ce37ea56b..090966585ee6 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
@@ -475,6 +475,10 @@ object RubyIntermediateAst {
     def toSimpleIdentifier: SimpleIdentifier
   }
 
+  sealed trait MethodAccessModifier extends AllowedTypeDeclarationChild {
+    def method: RubyExpression
+  }
+
   final case class PublicModifier()(span: TextSpan) extends RubyExpression(span) with AccessModifier {
     override def toSimpleIdentifier: SimpleIdentifier = SimpleIdentifier(None)(span)
   }
@@ -487,6 +491,14 @@ object RubyIntermediateAst {
     override def toSimpleIdentifier: SimpleIdentifier = SimpleIdentifier(None)(span)
   }
 
+  final case class PrivateMethodModifier(method: RubyExpression)(span: TextSpan)
+      extends RubyExpression(span)
+      with MethodAccessModifier
+
+  final case class PublicMethodModifier(method: RubyExpression)(span: TextSpan)
+      extends RubyExpression(span)
+      with MethodAccessModifier
+
   /** Represents standalone `proc { ... }` or `lambda { ... }` expressions
     */
   final case class ProcOrLambdaExpr(block: Block)(span: TextSpan) extends RubyExpression(span)
diff --git a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/parser/RubyJsonToNodeCreator.scala b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/parser/RubyJsonToNodeCreator.scala
index cccbe045baa3..23f81903b07f 100644
--- a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/parser/RubyJsonToNodeCreator.scala
+++ b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/parser/RubyJsonToNodeCreator.scala
@@ -628,6 +628,23 @@ class RubyJsonToNodeCreator(
     MemberCall(lhs, ".", RubyOperators.regexpMatch, rhs :: Nil)(obj.toTextSpan)
   }
 
+  private def visitMethodAccessModifier(obj: Obj): RubyExpression = {
+    val body = obj.visitArray(ParserKeys.Arguments) match {
+      case head :: Nil => head
+      case xs          => xs.head
+    }
+
+    obj(ParserKeys.Name).str match {
+      case "public_class_method" =>
+        PublicMethodModifier(body)(obj.toTextSpan)
+      case "private_class_method" =>
+        PrivateMethodModifier(body)(obj.toTextSpan)
+      case modifierName =>
+        logger.warn(s"Unknown modifier type $modifierName")
+        defaultResult(Option(obj.toTextSpan))
+    }
+  }
+
   private def visitMethodDefinition(obj: Obj): RubyExpression = {
     val name       = obj(ParserKeys.Name).str
     val parameters = obj(ParserKeys.Arguments).asInstanceOf[ujson.Obj].visitArray(ParserKeys.Children)
@@ -870,6 +887,7 @@ class RubyJsonToNodeCreator(
       case "include"                                                            => visitInclude(obj)
       case "attr_reader" | "attr_writer" | "attr_accessor"                      => visitFieldDeclaration(obj)
       case "private" | "public" | "protected"                                   => visitAccessModifier(obj)
+      case "private_class_method" | "public_class_method"                       => visitMethodAccessModifier(obj)
       case requireLike if ImportCallNames.contains(requireLike) && !hasReceiver => visitRequireLike(obj)
       case _ if BinaryOperators.isBinaryOperatorName(callName) =>
         val lhs = visit(obj(ParserKeys.Receiver))


Test patch:
diff --git a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/ModuleTests.scala b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/ModuleTests.scala
index 236be162c313..9dc5dbc08163 100644
--- a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/ModuleTests.scala
+++ b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/ModuleTests.scala
@@ -3,7 +3,7 @@ package io.joern.rubysrc2cpg.querying
 import io.joern.rubysrc2cpg.passes.Defines
 import io.joern.rubysrc2cpg.passes.Defines.Main
 import io.joern.rubysrc2cpg.testfixtures.RubyCode2CpgFixture
-import io.shiftleft.codepropertygraph.generated.NodeTypes
+import io.shiftleft.codepropertygraph.generated.{ModifierTypes, NodeTypes}
 import io.shiftleft.codepropertygraph.generated.nodes.{File, NamespaceBlock}
 import io.shiftleft.semanticcpg.language.*
 
@@ -77,4 +77,46 @@ class ModuleTests extends RubyCode2CpgFixture {
       case xs => fail(s"Expected one class decl, got [${xs.code.mkString(",")}]")
     }
   }
+
+  "Class Method Modifiers" should {
+    val cpg = code("""
+        |# Taken from Mastodon Repo
+        |module LanguagesHelper
+        |  ISO_639_1 = {}
+        |  ISO_639_3 = {}
+        |  SUPPORTED_LOCALES = {}
+        |  REGIONAL_LOCALE_NAMES = {}
+        |
+        |  private_class_method def self.locale_name_for_sorting(locale)
+        |    if (supported_locale = SUPPORTED_LOCALES[locale.to_sym])
+        |      ASCIIFolding.new.fold(supported_locale[1]).downcase
+        |    elsif (regional_locale = REGIONAL_LOCALE_NAMES[locale.to_sym])
+        |      ASCIIFolding.new.fold(regional_locale).downcase
+        |    else
+        |      locale
+        |    end
+        |  end
+        |
+        |  def publicMethodAfterwards
+        |  end
+        |end
+        |""".stripMargin)
+    "Generate private modifier on method" in {
+      inside(cpg.method.name("locale_name_for_sorting")._modifierViaAstOut.l) {
+        case virtualModifier :: privateModifier :: Nil =>
+          virtualModifier.modifierType shouldBe ModifierTypes.VIRTUAL
+          privateModifier.modifierType shouldBe ModifierTypes.PRIVATE
+        case xs => fail(s"Expected two modifiers, got [${xs.modifierType.mkString(",")}]")
+      }
+    }
+
+    "Revert to original access modifier after previous method def" in {
+      inside(cpg.method.name("publicMethodAfterwards")._modifierViaAstOut.l) {
+        case virtualModifier :: publicModifier :: Nil =>
+          virtualModifier.modifierType shouldBe ModifierTypes.VIRTUAL
+          publicModifier.modifierType shouldBe ModifierTypes.PUBLIC
+        case xs => fail(s"Expected got [${xs.modifierType.mkString(",")}]")
+      }
+    }
+  }
 }