diff --git a/bin/clone-java-repositories.sh b/bin/clone-java-repositories.sh
index 22d932fc0..a53f7b8d5 100755
--- a/bin/clone-java-repositories.sh
+++ b/bin/clone-java-repositories.sh
@@ -42,3 +42,6 @@ git -C "$1" clone https://github.com/zulip/zulip.git
# TypeScript Repos
git -C "$1" clone https://github.com/microsoft/vscode.git
+
+# Ruby Repos
+git -C "$1" clone https://github.com/fastlane/fastlane.git
diff --git a/pom.xml b/pom.xml
index 3d0b1498b..2a7d59b1e 100755
--- a/pom.xml
+++ b/pom.xml
@@ -90,6 +90,11 @@
${j2v8.artifactId}
4.8.0
+
+ org.jruby
+ jrubyparser
+ 0.5.3
+
diff --git a/src/main/java/com/felixgrund/codeshovel/parser/impl/PythonFunction.java b/src/main/java/com/felixgrund/codeshovel/parser/impl/PythonFunction.java
index eb5b973f7..5b0bff637 100644
--- a/src/main/java/com/felixgrund/codeshovel/parser/impl/PythonFunction.java
+++ b/src/main/java/com/felixgrund/codeshovel/parser/impl/PythonFunction.java
@@ -36,6 +36,7 @@ private Yparameter getParameter(PythonParser.Def_parameterContext param) {
private Yparameter getParameter(PythonParser.Named_parameterContext param) {
String argumentName = "";
+ // TODO should * and ** be part of the metadata?
if (param.getParent().getChild(0).getText().contains("*")) {
// OK this looks a little shaky but is meant to include *args and **kwargs
argumentName += param.getParent().getChild(0).getText();
diff --git a/src/main/java/com/felixgrund/codeshovel/parser/impl/RubyFunction.java b/src/main/java/com/felixgrund/codeshovel/parser/impl/RubyFunction.java
new file mode 100644
index 000000000..4cf2ec61c
--- /dev/null
+++ b/src/main/java/com/felixgrund/codeshovel/parser/impl/RubyFunction.java
@@ -0,0 +1,104 @@
+package com.felixgrund.codeshovel.parser.impl;
+
+import com.felixgrund.codeshovel.entities.Yexceptions;
+import com.felixgrund.codeshovel.entities.Ymodifiers;
+import com.felixgrund.codeshovel.entities.Yparameter;
+import com.felixgrund.codeshovel.parser.AbstractFunction;
+import com.felixgrund.codeshovel.parser.Yfunction;
+import com.felixgrund.codeshovel.wrappers.Commit;
+import org.jrubyparser.ast.*;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class RubyFunction extends AbstractFunction implements Yfunction {
+
+ RubyFunction(MethodDefNode function, Commit commit, String sourceFilePath, String sourceFileContent) {
+ super(function, commit, sourceFilePath, sourceFileContent);
+ }
+
+ @Override
+ protected String getInitialName(MethodDefNode rawMethod) {
+ return rawMethod.getName();
+ }
+
+ @Override
+ protected String getInitialType(MethodDefNode rawMethod) {
+ return null;
+ }
+
+ @Override
+ protected Ymodifiers getInitialModifiers(MethodDefNode rawMethod) {
+ // TODO is this the best place to put a receiver? Perhaps this should be a context?
+ if (rawMethod instanceof DefsNode) {
+ DefsNode singletonMethod = (DefsNode) rawMethod;
+ Node receiver = singletonMethod.getReceiver();
+ if (receiver instanceof INameNode) {
+ INameNode namedReceiver = (INameNode) receiver;
+ String receiverName = namedReceiver.getName();
+ return new Ymodifiers(Collections.singletonList(receiverName));
+ }
+ }
+ return Ymodifiers.NONE;
+ }
+
+ @Override
+ protected Yexceptions getInitialExceptions(MethodDefNode rawMethod) {
+ return Yexceptions.NONE;
+ }
+
+ @Override
+ protected List getInitialParameters(MethodDefNode rawMethod) {
+ // TODO how does namesOnly affect this line?
+ List parameterStrings = rawMethod.getArgs().getNormativeParameterNameList(false);
+ List parametersList = new ArrayList<>();
+ for (String parameterString : parameterStrings) {
+ Yparameter parameter = new Yparameter(parameterString, null);
+ // TODO set metadata using default arguments and optional arguments
+ // see: https://medium.com/podiihq/ruby-parameters-c178fdcd1f4e
+ // parameter.setMetadata();
+ parametersList.add(parameter);
+ }
+ return parametersList;
+ }
+
+ @Override
+ protected String getInitialBody(MethodDefNode rawMethod) {
+ return rawMethod.getBody().toString();
+ }
+
+ @Override
+ protected int getInitialBeginLine(MethodDefNode rawMethod) {
+ return rawMethod.getNamePosition().getStartLine() + 1;
+ }
+
+ @Override
+ protected int getInitialEndLine(MethodDefNode rawMethod) {
+ return rawMethod.getPosition().getEndLine() + 1;
+ }
+
+ @Override
+ protected String getInitialParentName(MethodDefNode rawMethod) {
+ // TODO this currently only covers class and module
+ // node.getClosestIScope is probably more what we want, but we need custom
+ // name retrieval for every node that implements IScope
+ for(Node scope = rawMethod.getParent(); scope != null; scope = scope.getParent()) {
+ if (scope instanceof IScopingNode) {
+ return ((IScopingNode) scope).getCPath().getName();
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ protected String getInitialFunctionPath(MethodDefNode rawMethod) {
+ return null;
+ }
+
+ @Override
+ protected String getInitialAnnotation(MethodDefNode rawMethod) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/felixgrund/codeshovel/parser/impl/RubyParser.java b/src/main/java/com/felixgrund/codeshovel/parser/impl/RubyParser.java
new file mode 100644
index 000000000..327e7affc
--- /dev/null
+++ b/src/main/java/com/felixgrund/codeshovel/parser/impl/RubyParser.java
@@ -0,0 +1,128 @@
+package com.felixgrund.codeshovel.parser.impl;
+
+import com.felixgrund.codeshovel.changes.*;
+import com.felixgrund.codeshovel.entities.Ycommit;
+import com.felixgrund.codeshovel.exceptions.ParseException;
+import com.felixgrund.codeshovel.parser.AbstractParser;
+import com.felixgrund.codeshovel.parser.Yfunction;
+import com.felixgrund.codeshovel.parser.Yparser;
+import com.felixgrund.codeshovel.util.Utl;
+import com.felixgrund.codeshovel.wrappers.Commit;
+import com.felixgrund.codeshovel.wrappers.StartEnvironment;
+import org.jrubyparser.CompatVersion;
+import org.jrubyparser.Parser;
+import org.jrubyparser.ast.DefnNode;
+import org.jrubyparser.ast.DefsNode;
+import org.jrubyparser.ast.MethodDefNode;
+import org.jrubyparser.ast.Node;
+import org.jrubyparser.parser.ParserConfiguration;
+import org.jrubyparser.util.NoopVisitor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+
+public class RubyParser extends AbstractParser implements Yparser {
+
+ public static final String ACCEPTED_FILE_EXTENSION = ".*\\.rb$";
+ private Logger log = LoggerFactory.getLogger(RubyParser.class);
+
+ public RubyParser(StartEnvironment startEnv, String filePath, String fileContent, Commit commit) throws ParseException {
+ super(startEnv, filePath, fileContent, commit);
+ }
+
+ @Override
+ protected List parseMethods() throws ParseException {
+ try {
+ Parser rubyParser = new Parser();
+ StringReader in = new StringReader(this.fileContent);
+ CompatVersion version = CompatVersion.RUBY2_0;
+ ParserConfiguration config = new ParserConfiguration(0, version);
+ Node rootNode = rubyParser.parse(this.filePath, in, config);
+ RubyMethodVisitor visitor = new RubyMethodVisitor() {
+ @Override
+ public boolean methodMatches(Yfunction method) {
+ return method.getBody() != null;
+ }
+ };
+ rootNode.accept(visitor);
+ return visitor.getMatchedNodes();
+ } catch (Exception e) {
+ System.err.println("JavaParser::parseMethods() - parse error. path: " + this.filePath + "; content:\n" + this.fileContent);
+ throw new ParseException("Error during parsing of content", this.filePath, this.fileContent);
+ }
+ }
+
+ @Override
+ public double getScopeSimilarity(Yfunction function, Yfunction compareFunction) {
+ // TODO
+ return Utl.parentNamesMatch(function, compareFunction) ? 1.0 : 0.0;
+ }
+
+ @Override
+ public String getAcceptedFileExtension() {
+ return ACCEPTED_FILE_EXTENSION;
+ }
+
+ @Override
+ public List getMinorChanges(Ycommit commit, Yfunction compareFunction) {
+ List changes = new ArrayList<>();
+ Yreturntypechange yreturntypechange = getReturnTypeChange(commit, compareFunction);
+ Ymodifierchange ymodifierchange = getModifiersChange(commit, compareFunction);
+ Ybodychange ybodychange = getBodyChange(commit, compareFunction);
+
+ // TODO this currently does nothing. Parameter metadata is not collected
+ Yparametermetachange yparametermetachange = getParametersMetaChange(commit, compareFunction);
+ if (yreturntypechange != null) {
+ changes.add(yreturntypechange);
+ }
+ if (ymodifierchange != null) {
+ changes.add(ymodifierchange);
+ }
+ if (ybodychange != null) {
+ changes.add(ybodychange);
+ }
+ if (yparametermetachange != null) {
+ changes.add(yparametermetachange);
+ }
+ return changes;
+ }
+
+
+ private Yfunction transformMethod(MethodDefNode method) {
+ return new RubyFunction(method, this.commit, this.filePath, this.fileContent);
+ }
+
+ public abstract class RubyMethodVisitor extends NoopVisitor {
+
+ private List matchedNodes = new ArrayList<>();
+
+ public abstract boolean methodMatches(Yfunction method);
+
+ private void addMethodIfMatches(MethodDefNode method) {
+ Yfunction yfunction = transformMethod(method);
+ if (methodMatches(yfunction)) {
+ matchedNodes.add(yfunction);
+ }
+ }
+
+ @Override
+ public Object visitDefnNode(DefnNode defnNode) {
+ addMethodIfMatches(defnNode);
+ return this.visit(defnNode);
+ }
+
+ @Override
+ public Object visitDefsNode(DefsNode defsNode) {
+ addMethodIfMatches(defsNode);
+ return this.visit(defsNode);
+ }
+
+ public List getMatchedNodes() {
+ return matchedNodes;
+ }
+ }
+
+}
diff --git a/src/main/java/com/felixgrund/codeshovel/parser/impl/TypeScriptFunction.java b/src/main/java/com/felixgrund/codeshovel/parser/impl/TypeScriptFunction.java
index 7fcfb9abd..dc73f3b99 100644
--- a/src/main/java/com/felixgrund/codeshovel/parser/impl/TypeScriptFunction.java
+++ b/src/main/java/com/felixgrund/codeshovel/parser/impl/TypeScriptFunction.java
@@ -21,6 +21,7 @@ public class TypeScriptFunction extends AbstractFunction implements Yf
}
private Yparameter getParameter(V8Object param) {
+ // TODO should `?` (implying optional) or `...` (implying varargs) be in the metadata?
String stype;
V8Object v8type = param.getObject("type");
if (v8type.isUndefined()) {
diff --git a/src/main/java/com/felixgrund/codeshovel/parser/impl/TypeScriptParser.java b/src/main/java/com/felixgrund/codeshovel/parser/impl/TypeScriptParser.java
index 2e4d42d33..d63f22318 100644
--- a/src/main/java/com/felixgrund/codeshovel/parser/impl/TypeScriptParser.java
+++ b/src/main/java/com/felixgrund/codeshovel/parser/impl/TypeScriptParser.java
@@ -61,6 +61,7 @@ public List getMinorChanges(Ycommit commit, Yfunction compareFunction)
Ymodifierchange ymodifierchange = getModifiersChange(commit, compareFunction);
Ybodychange ybodychange = getBodyChange(commit, compareFunction);
Yannotationchange yannotationchange = getAnnotationChange(commit, compareFunction);
+ // TODO consider parameter meta change
// TODO: handle Ydocchange
if (yreturntypechange != null) {
changes.add(yreturntypechange);
diff --git a/src/main/java/com/felixgrund/codeshovel/util/ParserFactory.java b/src/main/java/com/felixgrund/codeshovel/util/ParserFactory.java
index a5117219e..f67ddcf54 100644
--- a/src/main/java/com/felixgrund/codeshovel/util/ParserFactory.java
+++ b/src/main/java/com/felixgrund/codeshovel/util/ParserFactory.java
@@ -5,6 +5,7 @@
import com.felixgrund.codeshovel.parser.Yparser;
import com.felixgrund.codeshovel.parser.impl.JavaParser;
import com.felixgrund.codeshovel.parser.impl.PythonParser;
+import com.felixgrund.codeshovel.parser.impl.RubyParser;
import com.felixgrund.codeshovel.parser.impl.TypeScriptParser;
import com.felixgrund.codeshovel.wrappers.Commit;
import com.felixgrund.codeshovel.wrappers.StartEnvironment;
@@ -27,6 +28,8 @@ public static Yparser getParser(StartEnvironment startEnv, String filePath, Stri
parser = new PythonParser(startEnv, filePath, fileContent, commit);
} else if (filePath.matches(TypeScriptParser.ACCEPTED_FILE_EXTENSION)) {
parser = new TypeScriptParser(startEnv, filePath, fileContent, commit);
+ } else if (filePath.matches(RubyParser.ACCEPTED_FILE_EXTENSION)) {
+ parser = new RubyParser(startEnv, filePath, fileContent, commit);
} else {
throw new NoParserFoundException("No parser found for filename " + filePath);
}
diff --git a/src/test/resources/oracles/ruby/fastland-detect_values-detect_selected_provisioning_profiles.json b/src/test/resources/oracles/ruby/fastland-detect_values-detect_selected_provisioning_profiles.json
new file mode 100644
index 000000000..a2886a3f1
--- /dev/null
+++ b/src/test/resources/oracles/ruby/fastland-detect_values-detect_selected_provisioning_profiles.json
@@ -0,0 +1,10 @@
+{
+ "repositoryName": "fastlane",
+ "filePath": "gym/lib/gym/detect_values.rb",
+ "functionName": "detect_selected_provisioning_profiles",
+ "functionStartLine": 79,
+ "startCommitName": "2c34e6573dcf0014670af3a554fa92a9ad2566b7",
+ "expectedResult": {
+
+ }
+}