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": { + + } +}