-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial implementation of a ruby language frontend
Very primitive implementation of Ruby; primarily used in our reserach. Not ready for production.
- Loading branch information
Showing
9 changed files
with
445 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
27 changes: 27 additions & 0 deletions
27
...anguage-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/DeclarationHandler.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) | ||
} | ||
} |
174 changes: 174 additions & 0 deletions
174
...language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/ExpressionHandler.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
) | ||
} | ||
} |
61 changes: 61 additions & 0 deletions
61
cpg-language-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguage.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} |
79 changes: 79 additions & 0 deletions
79
...guage-ruby/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ruby/RubyLanguageFrontend.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) {} | ||
} |
Oops, something went wrong.