From 8cfd0a12068bc18c17c828b35c2f7794dd8a23a8 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Thu, 19 Oct 2023 22:17:44 +0200 Subject: [PATCH] Initial implementation of a ruby language frontend Very primitive implementation of Ruby; primarily used in our reserach. Not ready for production. --- .github/workflows/build.yml | 2 +- build.gradle.kts | 6 + ...frontend-dependency-conventions.gradle.kts | 2 + configure_frontends.sh | 2 + cpg-language-ruby/build.gradle.kts | 47 ++++++ .../cpg/frontends/ruby/DeclarationHandler.kt | 84 ++++++++++ .../cpg/frontends/ruby/ExpressionHandler.kt | 157 ++++++++++++++++++ .../aisec/cpg/frontends/ruby/RubyHandler.kt | 79 +++++++++ .../aisec/cpg/frontends/ruby/RubyLanguage.kt | 85 ++++++++++ .../frontends/ruby/RubyLanguageFrontend.kt | 105 ++++++++++++ .../cpg/frontends/ruby/StatementHandler.kt | 67 ++++++++ .../ruby/RubyLanguageFrontendTest.kt | 94 +++++++++++ .../src/test/resources/ruby/function.rb | 8 + .../src/test/resources/ruby/iter.rb | 4 + .../src/test/resources/ruby/variables.rb | 6 + gradle.properties.example | 1 + settings.gradle.kts | 5 + 17 files changed, 753 insertions(+), 1 deletion(-) create mode 100644 cpg-language-ruby/build.gradle.kts create mode 100644 cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/DeclarationHandler.kt create mode 100644 cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/ExpressionHandler.kt create mode 100644 cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyHandler.kt create mode 100644 cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguage.kt create mode 100644 cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguageFrontend.kt create mode 100644 cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/StatementHandler.kt create mode 100644 cpg-language-ruby/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguageFrontendTest.kt create mode 100644 cpg-language-ruby/src/test/resources/ruby/function.rb create mode 100644 cpg-language-ruby/src/test/resources/ruby/iter.rb create mode 100644 cpg-language-ruby/src/test/resources/ruby/variables.rb diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cd6aec2f99..c45a815d7f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 }} diff --git a/build.gradle.kts b/build.gradle.kts index baace361f8..a0907183aa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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"}") diff --git a/buildSrc/src/main/kotlin/cpg.frontend-dependency-conventions.gradle.kts b/buildSrc/src/main/kotlin/cpg.frontend-dependency-conventions.gradle.kts index fc5c2914f6..f889db5192 100644 --- a/buildSrc/src/main/kotlin/cpg.frontend-dependency-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/cpg.frontend-dependency-conventions.gradle.kts @@ -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")) @@ -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")) } diff --git a/configure_frontends.sh b/configure_frontends.sh index d4d02998e5..7304b0b04c 100755 --- a/configure_frontends.sh +++ b/configure_frontends.sh @@ -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 diff --git a/cpg-language-ruby/build.gradle.kts b/cpg-language-ruby/build.gradle.kts new file mode 100644 index 0000000000..3165b826d9 --- /dev/null +++ b/cpg-language-ruby/build.gradle.kts @@ -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("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") +} \ No newline at end of file diff --git a/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/DeclarationHandler.kt b/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/DeclarationHandler.kt new file mode 100644 index 0000000000..4070dbdeeb --- /dev/null +++ b/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/DeclarationHandler.kt @@ -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({ 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 + } +} diff --git a/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/ExpressionHandler.kt b/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/ExpressionHandler.kt new file mode 100644 index 0000000000..4764154728 --- /dev/null +++ b/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/ExpressionHandler.kt @@ -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({ 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 { + return newLiteral(String(node.value.bytes()), primitiveType("String")) + } + + private fun handleFixnumNode(node: FixnumNode): Literal { + return newLiteral(node.value, primitiveType("Integer")) + } + + private fun handleFloatNode(node: FloatNode): Literal { + return newLiteral(node.value, primitiveType("Float")) + } +} diff --git a/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyHandler.kt b/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyHandler.kt new file mode 100644 index 0000000000..5c8ba077b0 --- /dev/null +++ b/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyHandler.kt @@ -0,0 +1,79 @@ +/* + * 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.frontends.Handler +import de.fraunhofer.aisec.cpg.graph.Node +import de.fraunhofer.aisec.cpg.graph.ProblemNode +import de.fraunhofer.aisec.cpg.helpers.Util + +abstract class RubyHandler( + configConstructor: () -> ResultNode, + frontend: RubyLanguageFrontend +) : Handler(configConstructor, frontend) { + /** + * We intentionally override the logic of [Handler.handle] because we do not want the map-based + * logic, but rather want to make use of the Kotlin-when syntax. + */ + override fun handle(ctx: HandlerNode): ResultNode { + val node = handleNode(ctx) + + // The language frontend might set a location, which we should respect. Otherwise, we will + // set the location here. + if (node.location == null) { + frontend.setCodeAndLocation(node, ctx) + } + + frontend.setComment(node, ctx) + frontend.process(ctx, node) + + lastNode = node + + return node + } + + abstract fun handleNode(node: HandlerNode): ResultNode + + /** + * This function should be called by classes that derive from [RubyHandler] to denote, that the + * supplied node (type) is not supported. + */ + protected fun handleNotSupported(node: HandlerNode, name: String): ResultNode { + Util.errorWithFileLocation( + frontend, + node, + log, + "Parsing of type $name is not supported (yet)" + ) + + val cpgNode = this.configConstructor.get() + if (cpgNode is ProblemNode) { + cpgNode.problem = "Parsing of type $name is not supported (yet)" + } + + return cpgNode + } +} diff --git a/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguage.kt b/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguage.kt new file mode 100644 index 0000000000..93969276d9 --- /dev/null +++ b/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguage.kt @@ -0,0 +1,85 @@ +/* + * 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.ScopeManager +import de.fraunhofer.aisec.cpg.frontends.* +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(), + HasDefaultArguments, + HasClasses, + HasSuperClasses, + HasShortCircuitOperators { + override val fileExtensions = listOf("rb") + override val namespaceDelimiter = "::" + @Transient override val frontend: KClass = 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") + } +} diff --git a/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguageFrontend.kt b/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguageFrontend.kt new file mode 100644 index 0000000000..22bc8748f7 --- /dev/null +++ b/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguageFrontend.kt @@ -0,0 +1,105 @@ +/* + * 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.TranslationContext +import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration +import de.fraunhofer.aisec.cpg.graph.types.Type +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.MethodDefNode +import org.jruby.ast.RootNode +import org.jruby.parser.Parser +import org.jruby.parser.ParserConfiguration + +class RubyLanguageFrontend(language: RubyLanguage, ctx: @NonNull TranslationContext) : + LanguageFrontend(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) + } + + private fun handleRootNode(node: RootNode): TranslationUnitDeclaration { + val tu = newTranslationUnitDeclaration(node.file, codeOf(node)) + + scopeManager.resetToGlobal(tu) + + // The root node can either contain a single node or a block node + if (node.bodyNode is MethodDefNode) { + val decl = declarationHandler.handle(node.bodyNode) + scopeManager.addDeclaration(decl) + } else if (node.bodyNode is BlockNode) { + // Otherwise, we need to loop over the block + val block = node.bodyNode as BlockNode + for (innerNode in block.filterNotNull()) { + if (innerNode is MethodDefNode) { + val decl = declarationHandler.handle(innerNode) + scopeManager.addDeclaration(decl) + } else { + val stmt = statementHandler.handle(innerNode) + tu += stmt + } + } + } + + 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 autoType() + } + + override fun setComment(node: Node, astNode: org.jruby.ast.Node) { + // not yet implemented + } +} diff --git a/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/StatementHandler.kt b/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/StatementHandler.kt new file mode 100644 index 0000000000..be81d708f8 --- /dev/null +++ b/cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/StatementHandler.kt @@ -0,0 +1,67 @@ +/* + * 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.newBlock +import de.fraunhofer.aisec.cpg.graph.newReturnStatement +import de.fraunhofer.aisec.cpg.graph.statements.ReturnStatement +import de.fraunhofer.aisec.cpg.graph.statements.Statement +import de.fraunhofer.aisec.cpg.graph.statements.expressions.Block +import de.fraunhofer.aisec.cpg.graph.statements.expressions.ProblemExpression +import org.jruby.ast.* + +class StatementHandler(lang: RubyLanguageFrontend) : + RubyHandler({ ProblemExpression() }, lang) { + + override fun handleNode(node: Node): Statement { + return when (node) { + is BlockNode -> handleBlockNode(node) + is ReturnNode -> handleReturnNode(node) + else -> { + // We do not have an explicit statement wrapper around expressions, so we first try + // to parse the remaining nodes as an expression + frontend.expressionHandler.handleNode(node) + } + } + } + + private fun handleBlockNode(blockNode: BlockNode): Block { + val compoundStatement = newBlock() + + for (node in blockNode.filterNotNull()) { + compoundStatement.addStatement(handle(node)) + } + + return compoundStatement + } + + private fun handleReturnNode(node: ReturnNode): ReturnStatement { + val stmt = newReturnStatement() + stmt.returnValue = frontend.expressionHandler.handleNode(node.valueNode) + + return stmt + } +} diff --git a/cpg-language-ruby/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguageFrontendTest.kt b/cpg-language-ruby/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguageFrontendTest.kt new file mode 100644 index 0000000000..7c15d86487 --- /dev/null +++ b/cpg-language-ruby/src/test/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguageFrontendTest.kt @@ -0,0 +1,94 @@ +/* + * 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.TestUtils +import de.fraunhofer.aisec.cpg.assertLocalName +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.graph.statements.expressions.LambdaExpression +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +class RubyLanguageFrontendTest { + @Test + fun testFunctionDeclaration() { + val topLevel = Path.of("src", "test", "resources", "ruby") + val tu = + TestUtils.analyzeAndGetFirstTU( + listOf(topLevel.resolve("function.rb").toFile()), + topLevel, + true + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val myFunction = tu.functions["my_function"] + assertNotNull(myFunction) + + val anotherFunction = tu.functions["another_function"] + assertNotNull(anotherFunction) + } + + @Test + fun testVariables() { + val topLevel = Path.of("src", "test", "resources", "ruby") + val tu = + TestUtils.analyzeAndGetFirstTU( + listOf(topLevel.resolve("variables.rb").toFile()), + topLevel, + true + ) { + it.registerLanguage() + } + assertNotNull(tu) + } + + @Test + fun testIter() { + val topLevel = Path.of("src", "test", "resources", "ruby") + val tu = + TestUtils.analyzeAndGetFirstTU( + listOf(topLevel.resolve("iter.rb").toFile()), + topLevel, + true + ) { + it.registerLanguage() + } + assertNotNull(tu) + + val each = tu.calls["each"] + assertNotNull(each) + + val arg0 = each.arguments[0] + assertIs(arg0) + + val i = arg0.function.parameters[0] + assertLocalName("i", i) + } +} diff --git a/cpg-language-ruby/src/test/resources/ruby/function.rb b/cpg-language-ruby/src/test/resources/ruby/function.rb new file mode 100644 index 0000000000..fc27bc5c9d --- /dev/null +++ b/cpg-language-ruby/src/test/resources/ruby/function.rb @@ -0,0 +1,8 @@ +def my_function(value) + out = 2 * value + return out +end + +def another_function() + a = 2 +end \ No newline at end of file diff --git a/cpg-language-ruby/src/test/resources/ruby/iter.rb b/cpg-language-ruby/src/test/resources/ruby/iter.rb new file mode 100644 index 0000000000..8e9d9901f3 --- /dev/null +++ b/cpg-language-ruby/src/test/resources/ruby/iter.rb @@ -0,0 +1,4 @@ +a = [1,2,3,4,5] +a.each do |i| + puts i +end \ No newline at end of file diff --git a/cpg-language-ruby/src/test/resources/ruby/variables.rb b/cpg-language-ruby/src/test/resources/ruby/variables.rb new file mode 100644 index 0000000000..16ae6f68f5 --- /dev/null +++ b/cpg-language-ruby/src/test/resources/ruby/variables.rb @@ -0,0 +1,6 @@ +$global = "some string" + +def test() + a = 4.0 + puts $global +end diff --git a/gradle.properties.example b/gradle.properties.example index f227448a38..c9e3c651dc 100644 --- a/gradle.properties.example +++ b/gradle.properties.example @@ -7,3 +7,4 @@ enableGoFrontend=true enablePythonFrontend=true enableLLVMFrontend=true enableTypeScriptFrontend=true +enableRubyFrontend=true \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 40507dbf43..b7a66ec8a1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,6 +33,10 @@ val enableTypeScriptFrontend: Boolean by extra { val enableTypeScriptFrontend: String? by settings enableTypeScriptFrontend.toBoolean() } +val enableRubyFrontend: Boolean by extra { + val enableRubyFrontend: String? by settings + enableRubyFrontend.toBoolean() +} if (enableJavaFrontend) include(":cpg-language-java") if (enableCXXFrontend) include(":cpg-language-cxx") @@ -40,3 +44,4 @@ if (enableGoFrontend) include(":cpg-language-go") if (enableLLVMFrontend) include(":cpg-language-llvm") if (enablePythonFrontend) include(":cpg-language-python") if (enableTypeScriptFrontend) include(":cpg-language-typescript") +if (enableRubyFrontend) include(":cpg-language-ruby") \ No newline at end of file