Skip to content

Commit

Permalink
Initial implementation of a ruby language frontend (#1338)
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 authored Oct 23, 2023
1 parent 01708ff commit baa2626
Show file tree
Hide file tree
Showing 17 changed files with 753 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'beta') && !contains(github.ref, 'alpha')
run: |
export ORG_GRADLE_PROJECT_signingKey=`echo ${{ secrets.GPG_PRIVATE_KEY }} | base64 -d`
./gradlew --no-daemon -Dorg.gradle.internal.publish.checksums.insecure=true --parallel -Pversion=$VERSION -PenableJavaFrontend=true -PenableCXXFrontend=true -PenableGoFrontend=true -PenablePythonFrontend=true -PenableLLVMFrontend=true -PenableTypeScriptFrontend=true publishToSonatype closeSonatypeStagingRepository
./gradlew --no-daemon -Dorg.gradle.internal.publish.checksums.insecure=true --parallel -Pversion=$VERSION -PenableJavaFrontend=true -PenableCXXFrontend=true -PenableGoFrontend=true -PenablePythonFrontend=true -PenableLLVMFrontend=true -PenableTypeScriptFrontend=true -PenableRubyFrontend=true publishToSonatype closeSonatypeStagingRepository
env:
VERSION: ${{ env.version }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.GPG_PASSWORD }}
Expand Down
6 changes: 6 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,9 @@ val enableTypeScriptFrontend: Boolean by extra {
enableTypeScriptFrontend.toBoolean()
}
project.logger.lifecycle("TypeScript frontend is ${if (enableTypeScriptFrontend) "enabled" else "disabled"}")

val enableRubyFrontend: Boolean by extra {
val enableRubyFrontend: String? by project
enableRubyFrontend.toBoolean()
}
project.logger.lifecycle("Ruby frontend is ${if (enableRubyFrontend) "enabled" else "disabled"}")
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ val enableGoFrontend: Boolean by rootProject.extra
val enablePythonFrontend: Boolean by rootProject.extra
val enableLLVMFrontend: Boolean by rootProject.extra
val enableTypeScriptFrontend: Boolean by rootProject.extra
val enableRubyFrontend: Boolean by rootProject.extra

dependencies {
if (enableJavaFrontend) api(project(":cpg-language-java"))
Expand All @@ -17,4 +18,5 @@ dependencies {
if (enablePythonFrontend) api(project(":cpg-language-python"))
if (enableLLVMFrontend) api(project(":cpg-language-llvm"))
if (enableTypeScriptFrontend) api(project(":cpg-language-typescript"))
if (enableRubyFrontend) api(project(":cpg-language-ruby"))
}
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,84 @@
/*
* 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.
*
* $$$$$$\ $$$$$$$\ $$$$$$\
* $$ __$$\ $$ __$$\ $$ __$$\
* $$ / \__|$$ | $$ |$$ / \__|
* $$ | $$$$$$$ |$$ |$$$$\
* $$ | $$ ____/ $$ |\_$$ |
* $$ | $$\ $$ | $$ | $$ |
* \$$$$$ |$$ | \$$$$$ |
* \______/ \__| \______/
*
*/
package de.fraunhofer.aisec.cpg.frontends.ruby

import de.fraunhofer.aisec.cpg.graph.declarations.Declaration
import de.fraunhofer.aisec.cpg.graph.declarations.FunctionDeclaration
import de.fraunhofer.aisec.cpg.graph.declarations.ProblemDeclaration
import de.fraunhofer.aisec.cpg.graph.newFunctionDeclaration
import de.fraunhofer.aisec.cpg.graph.newParameterDeclaration
import de.fraunhofer.aisec.cpg.graph.newReturnStatement
import de.fraunhofer.aisec.cpg.graph.statements.ReturnStatement
import de.fraunhofer.aisec.cpg.graph.statements.expressions.Block
import org.jruby.ast.ArgumentNode
import org.jruby.ast.DefnNode
import org.jruby.ast.Node

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

override fun handleNode(node: Node): Declaration {
return when (node) {
is ArgumentNode -> handleArgumentNode(node)
is DefnNode -> handleDefnNode(node)
else -> handleNotSupported(node, node::class.simpleName ?: "")
}
}

private fun handleArgumentNode(node: ArgumentNode): Declaration {
return newParameterDeclaration(node.name.idString(), variadic = false)
}

private fun handleDefnNode(node: DefnNode): FunctionDeclaration {
val func = newFunctionDeclaration(node.name.idString())

frontend.scopeManager.enterScope(func)

for (arg in node.argsNode.args) {
frontend.scopeManager.addDeclaration(this.handle(arg))
}

val body = frontend.statementHandler.handle(node.bodyNode)
if (body is Block) {
// get the last statement
val lastStatement = body.statements.lastOrNull()

// add an implicit return statement, if there is no return statement
if (lastStatement !is ReturnStatement) {
val returnStatement = newReturnStatement("return")
returnStatement.isImplicit = true
body += returnStatement

// TODO: Ruby returns the last expression, if there is no explicit return
}
}
func.body = body

frontend.scopeManager.leaveScope(func)

return func
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* 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.
*
* $$$$$$\ $$$$$$$\ $$$$$$\
* $$ __$$\ $$ __$$\ $$ __$$\
* $$ / \__|$$ | $$ |$$ / \__|
* $$ | $$$$$$$ |$$ |$$$$\
* $$ | $$ ____/ $$ |\_$$ |
* $$ | $$\ $$ | $$ | $$ |
* \$$$$$ |$$ | \$$$$$ |
* \______/ \__| \______/
*
*/
package de.fraunhofer.aisec.cpg.frontends.ruby

import de.fraunhofer.aisec.cpg.graph.*
import de.fraunhofer.aisec.cpg.graph.statements.expressions.*
import org.jruby.ast.*
import org.jruby.ast.Node
import org.jruby.ast.types.INameNode
import org.jruby.ast.visitor.OperatorCallNode

class ExpressionHandler(lang: RubyLanguageFrontend) :
RubyHandler<Expression, Node>({ ProblemExpression() }, lang) {

override fun handleNode(node: Node): Expression {
return when (node) {
is OperatorCallNode -> handleOperatorCallNode(node)
is CallNode -> handleCallNode(node)
is FCallNode -> handleFCallNode(node)
is IterNode -> handleIterNode(node)
is StrNode -> handleStrNode(node)
is FixnumNode -> handleFixnumNode(node)
is FloatNode -> handleFloatNode(node)
is DVarNode -> handleINameNode(node)
is LocalVarNode -> handleINameNode(node)
is DAsgnNode -> handleDAsgnNode(node)
is LocalAsgnNode -> handleLocalAsgnNode(node)
else -> handleNotSupported(node, node::class.simpleName ?: "")
}
}

private fun handleOperatorCallNode(node: OperatorCallNode): BinaryOperator {
val binOp = newBinaryOperator(node.name.idString())

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

// Always seems to be an array?
val list = node.argsNode as ArrayNode
(this.handle(list.get(0)) as? Expression)?.let { binOp.rhs = it }

return binOp
}

private fun handleINameNode(node: INameNode): Reference {
return newReference(node.name.idString())
}

private fun handleIterNode(node: IterNode): LambdaExpression {
// 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
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 lambda = newLambdaExpression()
lambda.function = func

return lambda
}

private fun handleDAsgnNode(node: DAsgnNode): AssignExpression {
val assign = newAssignExpression("=")

// we need to build a reference out of the assignment node itself for our LHS
assign.lhs = listOf(handleINameNode(node))
assign.rhs = listOf(this.handle(node.valueNode))

return assign
}

private fun handleLocalAsgnNode(node: LocalAsgnNode): AssignExpression {
val assign = newAssignExpression("=")

// we need to build a reference out of the assignment node itself for our LHS
assign.lhs = listOf(handleINameNode(node))
assign.rhs = listOf(this.handle(node.valueNode))

return assign
}

private fun handleCallNode(node: CallNode): Expression {
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))
}

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

return mce
}

private fun handleFCallNode(node: FCallNode): Expression {
val callee = handleINameNode(node)

val call = newCallExpression(callee)

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

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

return call
}

private fun handleStrNode(node: StrNode): Literal<String> {
return newLiteral(String(node.value.bytes()), primitiveType("String"))
}

private fun handleFixnumNode(node: FixnumNode): Literal<Long> {
return newLiteral(node.value, primitiveType("Integer"))
}

private fun handleFloatNode(node: FloatNode): Literal<Double> {
return newLiteral(node.value, primitiveType("Float"))
}
}
Loading

0 comments on commit baa2626

Please sign in to comment.