Skip to content

Commit

Permalink
[ruby] Added mixed-elements functionality to array elements (#4919)
Browse files Browse the repository at this point in the history
  • Loading branch information
AndreiDreyer authored Sep 13, 2024
1 parent fe64ec8 commit bd79b8f
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,18 @@ commandWithDoBlock
| primary (DOT | COLON2) methodName argumentList doBlock
;

bracketedArrayElementList
: bracketedArrayElement (COMMA? NL* bracketedArrayElement)* COMMA?
;

bracketedArrayElement
: operatorExpressionList
| command
| indexingArgument
| associationList
| splattingArgument
;

indexingArgumentList
: operatorExpressionList COMMA?
# operatorExpressionListIndexingArgumentList
Expand All @@ -179,7 +191,7 @@ indexingArgumentList
#indexingArgumentIndexingArgumentList
| associationList COMMA?
# associationListIndexingArgumentList
| splattingArgument
| splattingArgument (COMMA NL* splattingArgument)*
# splattingArgumentIndexingArgumentList
;

Expand Down Expand Up @@ -339,7 +351,7 @@ primaryValue
# methodCallWithParentheses

// Literals
| LBRACK NL* indexingArgumentList? NL* RBRACK
| LBRACK NL* bracketedArrayElementList? NL* RBRACK
# bracketedArrayLiteral
| QUOTED_NON_EXPANDED_STRING_ARRAY_LITERAL_START quotedNonExpandedArrayElementList? QUOTED_NON_EXPANDED_STRING_ARRAY_LITERAL_END
# quotedNonExpandedStringArrayLiteral
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,13 +257,34 @@ object AntlrContextHelpers {
}
}

sealed implicit class BracketedArrayElementListContextHelper(ctx: BracketedArrayElementListContext) {
def elements: List[ParserRuleContext] = {
ctx.bracketedArrayElement.asScala.flatMap(_.element).toList
}
}

sealed implicit class BracketedArrayElementContextHelper(ctx: BracketedArrayElementContext) {
def element: List[ParserRuleContext] = {
ctx.children.asScala
.collect {
case x: OperatorExpressionListContext => x.operatorExpression().asScala
case x: CommandContext => x :: Nil
case x: AssociationListContext => x.associations
case x: SplattingArgumentContext => x :: Nil
case x: IndexingArgumentContext => x :: Nil
}
.toList
.flatten
}
}

sealed implicit class IndexingArgumentListContextHelper(ctx: IndexingArgumentListContext) {
def arguments: List[ParserRuleContext] = ctx match
case ctx: CommandIndexingArgumentListContext => List(ctx.command())
case ctx: OperatorExpressionListIndexingArgumentListContext =>
ctx.operatorExpressionList().operatorExpression().asScala.toList
case ctx: AssociationListIndexingArgumentListContext => ctx.associationList().associations
case ctx: SplattingArgumentIndexingArgumentListContext => ctx.splattingArgument() :: Nil
case ctx: SplattingArgumentIndexingArgumentListContext => ctx.splattingArgument().asScala.toList
case ctx: OperatorExpressionListWithSplattingArgumentIndexingArgumentListContext => ctx.splattingArgument() :: Nil
case ctx: IndexingArgumentIndexingArgumentListContext =>
ctx.indexingArgument().asScala.toList
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,8 @@ class AstPrinter extends RubyParserBaseVisitor[String] {
}

override def visitBracketedArrayLiteral(ctx: RubyParser.BracketedArrayLiteralContext): String = {
val args = Option(ctx.indexingArgumentList()).map(_.arguments).getOrElse(List()).map(visit).mkString(",")
val args = Option(ctx.bracketedArrayElementList()).map(_.elements).getOrElse(List()).map(visit).mkString(",")

s"${ctx.LBRACK.getText}$args${ctx.RBRACK.getText}"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1020,7 +1020,7 @@ class RubyNodeCreator(variableNameGen: FreshNameGenerator[String] = FreshNameGen
}

override def visitBracketedArrayLiteral(ctx: RubyParser.BracketedArrayLiteralContext): RubyExpression = {
ArrayLiteral(Option(ctx.indexingArgumentList()).map(_.arguments).getOrElse(List()).map(visit))(ctx.toTextSpan)
ArrayLiteral(Option(ctx.bracketedArrayElementList()).map(_.elements).getOrElse(List()).map(visit))(ctx.toTextSpan)
}

override def visitQuotedNonExpandedStringArrayLiteral(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package io.joern.rubysrc2cpg.querying
import io.joern.rubysrc2cpg.passes.GlobalTypes.{builtinPrefix, kernelPrefix}
import io.joern.rubysrc2cpg.testfixtures.RubyCode2CpgFixture
import io.shiftleft.codepropertygraph.generated.{DispatchTypes, Operators}
import io.shiftleft.codepropertygraph.generated.nodes.{Call, Literal}
import io.shiftleft.codepropertygraph.generated.nodes.{Block, Call, Identifier, Literal}
import io.shiftleft.semanticcpg.language.*
import io.joern.rubysrc2cpg.passes.Defines
import io.joern.rubysrc2cpg.passes.Defines.RubyOperators
import io.joern.x2cpg.Defines as XDefines

class ArrayTests extends RubyCode2CpgFixture {
Expand Down Expand Up @@ -211,4 +212,45 @@ class ArrayTests extends RubyCode2CpgFixture {
case xs => fail(s"Expected two elements for array init, got ${xs.code.mkString(",")}")
}
}

"Array with mixed elements" in {
val cpg = code("""
|[
| *::ApplicationSettingsHelper.visible_attributes,
| { default_branch_protection_defaults: [
| :allow_force_push,
| :developer_can_initial_push,
| {
| allowed_to_merge: [:access_level],
| allowed_to_push: [:access_level]
| }
| ] },
| :can_create_organization,
| *::ApplicationSettingsHelper.some_other_attributes,
|]
|""".stripMargin)

cpg.call.name(Operators.arrayInitializer).headOption match {
case Some(arrayInit) =>
inside(arrayInit.argument.l) {
case (splatArgOne: Call) :: (hashLiteralArg: Block) :: (symbolArg: Literal) :: (splatArgTwo: Call) :: Nil =>
splatArgOne.methodFullName shouldBe RubyOperators.splat
splatArgOne.code shouldBe "*::ApplicationSettingsHelper.visible_attributes"

symbolArg.code shouldBe ":can_create_organization"
symbolArg.typeFullName shouldBe Defines.getBuiltInType(Defines.Symbol)

splatArgTwo.methodFullName shouldBe RubyOperators.splat
splatArgTwo.code shouldBe "*::ApplicationSettingsHelper.some_other_attributes"

val List(hashInitAssignment: Call, _) =
hashLiteralArg.astChildren.isCall.name(Operators.assignment).l: @unchecked
val List(_: Identifier, hashInitCall: Call) = hashInitAssignment.argument.l: @unchecked
hashInitCall.methodFullName shouldBe RubyOperators.hashInitializer

case xs => fail(s"Expected 4 arguments, got [${xs.code.mkString(",")}]")
}
case None => fail("Expected one call for head arrayInit")
}
}
}

0 comments on commit bd79b8f

Please sign in to comment.