Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of a ruby language frontend #1338

Merged
merged 1 commit into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading