Skip to content

Commit

Permalink
Initial implementation of a ruby language frontend
Browse files Browse the repository at this point in the history
Very primitive implementation of Ruby; primarily used in our reserach. Not ready for production.
  • Loading branch information
oxisto committed Oct 18, 2023
1 parent e89fcd1 commit 7b1b0c2
Show file tree
Hide file tree
Showing 9 changed files with 445 additions and 0 deletions.
2 changes: 2 additions & 0 deletions configure_frontends.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,5 @@ answerLLVM=$(ask "Do you want to enable the LLVM frontend? (currently $(getPrope
setProperty "enableLLVMFrontend" $answerLLVM
answerTypescript=$(ask "Do you want to enable the TypeScript frontend? (currently $(getProperty "enableTypeScriptFrontend"))")
setProperty "enableTypeScriptFrontend" $answerTypescript
$answerRuby=$(ask "Do you want to enable the Ruby frontend? (currently $(getProperty "enableRubyFrontend"))")
setProperty "enableRubyFrontend" $answerRuby
47 changes: 47 additions & 0 deletions cpg-language-ruby/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2023, Fraunhofer AISEC. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* $$$$$$\ $$$$$$$\ $$$$$$\
* $$ __$$\ $$ __$$\ $$ __$$\
* $$ / \__|$$ | $$ |$$ / \__|
* $$ | $$$$$$$ |$$ |$$$$\
* $$ | $$ ____/ $$ |\_$$ |
* $$ | $$\ $$ | $$ | $$ |
* \$$$$$ |$$ | \$$$$$ |
* \______/ \__| \______/
*
*/
import com.github.gradle.node.npm.task.NpmTask

plugins {
id("cpg.frontend-conventions")
alias(libs.plugins.node)
}

publishing {
publications {
named<MavenPublication>("cpg-language-ruby") {
pom {
artifactId = "cpg-language-ruby"
name.set("Code Property Graph - Ruby")
description.set("A Ruby language frontend for the CPG")
}
}
}
}

dependencies {
implementation("org.jruby:jruby-core:9.4.3.0")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package de.fraunhofer.aisec.cpg.frontends.ruby

import de.fraunhofer.aisec.cpg.frontends.Handler
import de.fraunhofer.aisec.cpg.graph.declarations.Declaration
import de.fraunhofer.aisec.cpg.graph.declarations.ProblemDeclaration
import de.fraunhofer.aisec.cpg.graph.newParameterDeclaration
import org.jruby.ast.ArgumentNode
import org.jruby.ast.Node

class DeclarationHandler(lang: RubyLanguageFrontend) :
Handler<Declaration, Node, RubyLanguageFrontend>({ ProblemDeclaration() }, lang) {

init {
map.put(ArgumentNode::class.java, ::handleArgumentNode)
}

private fun handleArgumentNode(node: Node?): Declaration? {
if (node !is ArgumentNode) {
return null
}

return newParameterDeclaration(
node.name.idString(),
variadic = false
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package de.fraunhofer.aisec.cpg.frontends.ruby

import de.fraunhofer.aisec.cpg.frontends.Handler
import de.fraunhofer.aisec.cpg.graph.*
import de.fraunhofer.aisec.cpg.graph.statements.Statement
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Expression
import de.fraunhofer.aisec.cpg.graph.statements.expressions.ProblemExpression
import de.fraunhofer.aisec.cpg.graph.types.UnknownType
import org.jruby.ast.*
import org.jruby.ast.Node

class ExpressionHandler(lang: RubyLanguageFrontend) :
Handler<Statement, Node, RubyLanguageFrontend>({ ProblemExpression() }, lang) {

init {
map.put(CallNode::class.java, ::handleCallNode)
map.put(FCallNode::class.java, ::handleFCallNode)
map.put(IterNode::class.java, ::handleIterNode)
map.put(StrNode::class.java, ::handleStrNode)
map.put(DVarNode::class.java, ::handleDVarNode)
map.put(AttrAssignNode::class.java, ::handleAttrAssignNode)
map.put(AssignableNode::class.java, ::handleAssignableNode)
}

private fun handleFCallNode(node: Node?): Statement? {
if (node !is FCallNode) {
return null
}
// FIXME: what is this? Unimplemented or intentional?
return null
}

private fun handleAttrAssignNode(node: Node?): Statement? {
if (node !is AttrAssignNode) {
return null
}

val binOp = newBinaryOperator("=", frontend.codeOf(node))

val base =
this.handle(node.receiverNode) as? Expression
?: return ProblemExpression("could not parse base")
val expr =
newMemberExpression(
node.name.idString(),
base,
UnknownType.getUnknownType(frontend.language),
"="
)

binOp.lhs = expr
(this.handle(node.argsNode) as? Expression)?.let { binOp.rhs = it }

return expr
}

private fun handleDVarNode(node: Node?): Statement? {
if (node !is DVarNode) {
return null
}

return newReference(
node.name.idString()
)
}

private fun handleAssignableNode(node: Node?): Statement? {
if (node !is DAsgnNode && node !is LocalAsgnNode) {
return null
}

val name =
if (node is DAsgnNode) {
node.name
} else {
(node as LocalAsgnNode).name
}

// either a binary operator or a variable declaration
val lhs =
newReference(
name.idString()
)
val rhs = this.handle((node as AssignableNode).valueNode) as? Expression

// can we resolve it?
var decl = frontend.scopeManager.resolveReference(lhs)

if (decl == null) {
val stmt = newDeclarationStatement(frontend.codeOf(node))
decl =
newVariableDeclaration(
lhs.name,
UnknownType.getUnknownType(language),
frontend.codeOf(node),
false
)
decl.initializer = rhs

stmt.singleDeclaration = decl

return stmt
}

val binOp = newBinaryOperator("=")
binOp.lhs = lhs
rhs?.let { binOp.rhs = it }

return binOp
}

private fun handleCallNode(node: Node): Expression? {
if (node !is CallNode) {
return null
}

val base =
handle(node.receiverNode) as? Expression
?: return ProblemExpression("could not parse base")
val callee = newMemberExpression(node.name.asJavaString(), base)

val mce = newMemberCallExpression(callee, false)

for (arg in node.argsNode?.childNodes() ?: emptyList()) {
mce.addArgument(handle(arg) as Expression)
}

// add the iterNode as last argument
node.iterNode?.let { mce.addArgument(handle(it) as Expression) }

return mce
}

private fun handleIterNode(node: Node): Expression? {
if (node !is IterNode) {
return null
}

// a complete hack, to handle iter nodes, which is sort of a lambda expression
// so we create an anonymous function declaration out of the bodyNode and varNode
// and a declared reference expressions to that anonymous function
val func = newFunctionDeclaration("", frontend.codeOf(node))

frontend.scopeManager.enterScope(func)

for (arg in node.argsNode.args) {
val param = frontend.declarationHandler.handle(arg)
frontend.scopeManager.addDeclaration(param)
}

func.body = frontend.statementHandler.handle(node.bodyNode)

frontend.scopeManager.leaveScope(func)

val def = newDeclarationStatement(frontend.codeOf(node))
def.singleDeclaration = func

val cse = newBlock()
cse.statements = listOf(def)

return cse
}

private fun handleStrNode(node: Node): Expression? {
if (node !is StrNode) {
return null
}

return newLiteral(
String(node.value.bytes()),
primitiveType("string")
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package de.fraunhofer.aisec.cpg.frontends.ruby

import de.fraunhofer.aisec.cpg.ScopeManager
import de.fraunhofer.aisec.cpg.frontends.*
import de.fraunhofer.aisec.cpg.graph.Name
import de.fraunhofer.aisec.cpg.graph.declarations.RecordDeclaration
import de.fraunhofer.aisec.cpg.graph.statements.expressions.MemberExpression
import de.fraunhofer.aisec.cpg.graph.types.*
import kotlin.reflect.KClass

/** The Ruby Language */
class RubyLanguage() :
Language<RubyLanguageFrontend>(),
HasDefaultArguments,
HasClasses,
HasSuperClasses,
HasShortCircuitOperators {
override val fileExtensions = listOf("rb")
override val namespaceDelimiter = "::"
@Transient override val frontend: KClass<out RubyLanguageFrontend> = RubyLanguageFrontend::class
override val superClassKeyword = "super"

override val conjunctiveOperators = listOf("&&")
override val disjunctiveOperators = listOf("||")

@Transient
/** See [The RubySpec](https://github.com/ruby/spec) */
override val builtInTypes =
mapOf(
// The bit width of the Integer type in Ruby is only limited by your memory
"Integer" to IntegerType("Integer", null, this, NumericType.Modifier.SIGNED),
"Float" to FloatingPointType("Float", 64, this, NumericType.Modifier.SIGNED),
"String" to StringType("String", this),
// The bit width of Booleans is not defined in the specification and
// implementation-dependant
"Boolean" to BooleanType("Boolean", null, this, NumericType.Modifier.NOT_APPLICABLE)
)

override val compoundAssignmentOperators =
setOf(
"+=", // Addition assignment
"-=", // Subtraction assignment
"*=", // Multiplication assignment
"/=", // Division assignment
"%=", // Modulo assignment
"**=", // Exponentiation assignment
"<<=", // Left shift assignment
">>=", // Right shift assignment
"&=", // Bitwise AND assignment
"|=", // Bitwise OR assignment
"^=" // Bitwise XOR assignment
)

override fun handleSuperCall(
callee: MemberExpression,
curClass: RecordDeclaration,
scopeManager: ScopeManager
): Boolean {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package de.fraunhofer.aisec.cpg.frontends.ruby

import de.fraunhofer.aisec.cpg.TranslationContext
import de.fraunhofer.aisec.cpg.frontends.Language
import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend
import de.fraunhofer.aisec.cpg.graph.Node
import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration
import de.fraunhofer.aisec.cpg.graph.newFunctionDeclaration
import de.fraunhofer.aisec.cpg.graph.newTranslationUnitDeclaration
import de.fraunhofer.aisec.cpg.graph.types.Type
import de.fraunhofer.aisec.cpg.graph.unknownType
import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation
import java.io.File
import org.checkerframework.checker.nullness.qual.NonNull
import org.jruby.Ruby
import org.jruby.ast.BlockNode
import org.jruby.ast.RootNode
import org.jruby.parser.Parser
import org.jruby.parser.ParserConfiguration

class RubyLanguageFrontend(
language: RubyLanguage,
ctx: @NonNull TranslationContext
) : LanguageFrontend<org.jruby.ast.Node, org.jruby.ast.Node>(language, ctx) {
val declarationHandler: DeclarationHandler = DeclarationHandler(this)
val expressionHandler: ExpressionHandler = ExpressionHandler(this)
val statementHandler: StatementHandler = StatementHandler(this)

override fun parse(file: File): TranslationUnitDeclaration {
val ruby = Ruby.getGlobalRuntime()
val parser = Parser(ruby)

val node =
parser.parse(
file.path,
file.inputStream(),
null,
ParserConfiguration(ruby, 0, false, true, false)
) as
RootNode

return handleRootNode(node, file)
}

private fun handleRootNode(node: RootNode, file: File): TranslationUnitDeclaration {
val tu = newTranslationUnitDeclaration(node.file, codeOf(node))

scopeManager.resetToGlobal(tu)

// wrap everything into a virtual global function because we only have declarations on the
// top
val func =
newFunctionDeclaration(file.nameWithoutExtension + "_global", codeOf(node))

scopeManager.enterScope(func)

func.body = statementHandler.handle(node.bodyNode as BlockNode)

scopeManager.leaveScope(func)

scopeManager.addDeclaration(func)

return tu
}

override fun codeOf(astNode: org.jruby.ast.Node): String? {
return ""
}

override fun locationOf(astNode: org.jruby.ast.Node): PhysicalLocation? {
return null
}

override fun typeOf(type: org.jruby.ast.Node): Type {
return unknownType()
}

override fun setComment(s: Node, ctx: org.jruby.ast.Node) {}
}
Loading

0 comments on commit 7b1b0c2

Please sign in to comment.