Robin Maisch 4 лет назад
Родитель
Сommit
395d0de892

+ 9 - 0
jplag.frontend.R/README.md

@@ -0,0 +1,9 @@
+# JPlag R language frontend
+
+The JPlag R frontend allows the use of JPlag with submissions in R. 
+
+### R specification compatibility
+The underlying [grammar definition](https://github.com/antlr/grammars-v4/tree/master/r) was first created in June 2013, when R 3.0.1 was current. The latest commit is from April 2018, when R 3.5.0 was just released. Whether the grammar has been made to comply with any specific version of the R specification is unclear. Even if some parsing errors occur, the parser should be able to recover and still produce a valid analysis.
+
+### Usage
+To use the R frontend, add the `-l R` flag in the CLI, or use a `JPlagOption` object set to `LanguageOption.R` in the Java API as described in the usage information in the [readme of the main project](https://github.com/jplag/JPlag#usage) and [in the wiki](https://github.com/jplag/JPlag/wiki/1.-How-to-Use-JPlag).

+ 7 - 0
jplag.frontend.R/pom.xml

@@ -17,6 +17,13 @@
             <groupId>de.jplag</groupId>
             <artifactId>frontend-utils</artifactId>
         </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>frontend-testutils</artifactId>
+            <version>${revision}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>

+ 13 - 11
jplag.frontend.R/src/main/java/de/jplag/R/JplagRListener.java

@@ -36,6 +36,10 @@ public class JplagRListener implements RListener, RFilterListener, RTokenConstan
         parserAdapter.addToken(targetType, token.getLine(), token.getCharPositionInLine() + 1, token.getText().length());
     }
 
+    private void transformToken(int targetType, Token start, Token end) {
+        parserAdapter.addToken(targetType, start.getLine(), start.getCharPositionInLine() + 1, end.getStopIndex() - start.getStartIndex() + 1);
+    }
+
     @Override
     public void enterProg(RParser.ProgContext ctx) {
 
@@ -83,12 +87,12 @@ public class JplagRListener implements RListener, RFilterListener, RTokenConstan
 
     @Override
     public void exitFunction_definition(RParser.Function_definitionContext ctx) {
-        transformToken(END_FUNCTION, ctx.getStart());
+        transformToken(END_FUNCTION, ctx.getStop());
     }
 
     @Override
     public void enterFunction_call(RParser.Function_callContext ctx) {
-        transformToken(FUNCTION_CALL, ctx.getStart());
+        transformToken(FUNCTION_CALL, ctx.getStart(), ctx.getStop());
     }
 
     @Override
@@ -153,7 +157,7 @@ public class JplagRListener implements RListener, RFilterListener, RTokenConstan
 
     @Override
     public void exitIf_statement(RParser.If_statementContext ctx) {
-        transformToken(IF_END, ctx.getStart());
+        transformToken(IF_END, ctx.getStop());
     }
 
     @Override
@@ -163,17 +167,17 @@ public class JplagRListener implements RListener, RFilterListener, RTokenConstan
 
     @Override
     public void exitFor_statement(RParser.For_statementContext ctx) {
-        transformToken(FOR_END, ctx.getStart());
+        transformToken(FOR_END, ctx.getStop());
     }
 
     @Override
     public void enterWhile_statement(RParser.While_statementContext ctx) {
-        transformToken(WHILE_BEGIN, ctx.getStart());
+       transformToken(WHILE_BEGIN, ctx.getStart());
     }
 
     @Override
     public void exitWhile_statement(RParser.While_statementContext ctx) {
-        transformToken(WHILE_END, ctx.getStart());
+        transformToken(WHILE_END, ctx.getStop());
     }
 
     @Override
@@ -183,7 +187,7 @@ public class JplagRListener implements RListener, RFilterListener, RTokenConstan
 
     @Override
     public void exitRepeat_statement(RParser.Repeat_statementContext ctx) {
-        transformToken(REPEAT_END, ctx.getStart());
+        transformToken(REPEAT_END, ctx.getStop());
     }
 
     @Override
@@ -213,7 +217,7 @@ public class JplagRListener implements RListener, RFilterListener, RTokenConstan
 
     @Override
     public void exitCompound_statement(RParser.Compound_statementContext ctx) {
-        transformToken(COMPOUND_END, ctx.getStart());
+        transformToken(COMPOUND_END, ctx.getStop());
     }
 
     @Override
@@ -358,9 +362,7 @@ public class JplagRListener implements RListener, RFilterListener, RTokenConstan
 
     @Override
     public void visitTerminal(TerminalNode node) {
-        if (node.getText().equals("=")) {
-            transformToken(ASSIGN, node.getSymbol());
-        }
+
     }
 
     @Override

+ 3 - 3
jplag.frontend.R/src/main/java/de/jplag/R/RParserAdapter.java

@@ -88,10 +88,10 @@ public class RParserAdapter extends AbstractParser implements RTokenConstants {
      * @param type the type of the new {@link de.jplag.Token}
      * @param line the line of the Token in the current file
      * @param start the start column of the Token in the line
-     * @param end the end column of the Token in the line
+     * @param length the length of the Token
      */
-    /* package-private */ void addToken(int type, int line, int start, int end) {
-        tokens.addToken(new RToken(type, currentFile, line, start, end - start));
+    /* package-private */ void addToken(int type, int line, int start, int length) {
+        tokens.addToken(new RToken(type, currentFile, line, start, length));
 
     }
 }

+ 2 - 2
jplag.frontend.R/src/main/java/de/jplag/R/RToken.java

@@ -14,7 +14,7 @@ public class RToken extends de.jplag.Token implements RTokenConstants {
     @Override
     public String type2string() {
         return switch (this.type) {
-            case FILE_END -> "***********";
+            case FILE_END -> "<EOF>";
             case SEPARATOR_TOKEN -> "METHOD_SEPARATOR";
             case BEGIN_FUNCTION -> "FUNCTION{";
             case END_FUNCTION -> "}FUNCTION";
@@ -29,7 +29,7 @@ public class RToken extends de.jplag.Token implements RTokenConstants {
             case INDEX -> "INDEX";
             case PACKAGE -> "PACKAGE";
             case IF_BEGIN -> "IF{";
-            case IF_END -> "}IF";
+            case IF_END -> "}IF-ELSE";
             case FOR_BEGIN -> "FOR{";
             case FOR_END -> "}FOR";
             case WHILE_BEGIN -> "WHILE{";

+ 108 - 0
jplag.frontend.R/src/test/java/de/jplag/R/RFrontendTest.java

@@ -0,0 +1,108 @@
+package de.jplag.R;
+
+import de.jplag.Token;
+import de.jplag.TokenConstants;
+import de.jplag.TokenList;
+import de.jplag.TokenPrinter;
+import de.jplag.testutils.TestErrorConsumer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.IntStream;
+import java.util.stream.StreamSupport;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class RFrontendTest {
+
+    /**
+     *  Regular expression for comments.
+     */
+    private static final String R_COMMENT = "#.*";
+
+    /**
+     *  Test source file that is supposed to produce a complete set of tokens, i.e. all types of tokens.
+     */
+    private static final String COMPLETE_TEST_FILE = "Complete.R";
+    private final Logger logger = LoggerFactory.getLogger("R frontend test");
+    private final String[] testFiles = new String[]{"Game.r", COMPLETE_TEST_FILE};
+    private final File testFileLocation = Path.of("src", "test", "resources", "de", "jplag", "R").toFile();
+    private Language language;
+
+    @BeforeEach
+    void setup() {
+        TestErrorConsumer consumer = new TestErrorConsumer();
+        language = new Language(consumer);
+    }
+
+    @Test
+    void parseTestFiles() {
+        for (String fileName : testFiles) {
+            TokenList tokens = language.parse(testFileLocation, new String[]{fileName});
+            String output = TokenPrinter.printTokens(tokens, testFileLocation, List.of(fileName));
+            logger.info(output);
+
+            testSourceCoverage(fileName, tokens);
+            if (fileName.equals(COMPLETE_TEST_FILE)) testTokenCoverage(tokens, fileName);
+        }
+    }
+
+    /**
+     * Confirms that the code is covered to a basic extent, i.e. each line of code contains at least one token.
+     * @param fileName a code sample file name
+     * @param tokens the TokenList generated from the sample
+     */
+    private void testSourceCoverage(String fileName, TokenList tokens) {
+        File testFile = new File(testFileLocation, fileName);
+
+        try {
+            List<String> lines = Files.readAllLines(testFile.toPath());
+            String commentExpression = getCommentExpression();
+
+            // All lines that contain code
+            var codeLines = IntStream.range(1, lines.size() + 1).filter(idx -> !lines.get(idx - 1).matches("\\s*(%s)?".formatted(commentExpression))).toArray();
+            // All lines that contain token
+            var tokenLines = IntStream.range(0, tokens.size()).mapToObj(tokens::getToken).mapToInt(Token::getLine).distinct().toArray();
+
+            if (codeLines.length > tokenLines.length) {
+                var diffLine = IntStream.range(0, codeLines.length).dropWhile(lineIdx -> lineIdx < tokenLines.length && codeLines[lineIdx] == tokenLines[lineIdx]).findFirst();
+                diffLine.ifPresent(lineIdx -> fail("Line %d of file '%s' is not represented in the token list.".formatted(codeLines[lineIdx], fileName)));
+            }
+            assertArrayEquals(codeLines, tokenLines);
+        } catch (IOException exception) {
+            logger.info("Error while reading test file %s".formatted(fileName), exception);
+            fail();
+        }
+    }
+
+    /**
+     * Confirms that all Token types are 'reachable' with a complete code example.
+     * @param tokens TokenList which is supposed to contain all types of tokens
+     * @param fileName The file name of the complete code example
+     */
+    private void testTokenCoverage(TokenList tokens, String fileName) {
+        var foundTokens = StreamSupport.stream(tokens.allTokens().spliterator(), true).mapToInt(Token::getType)
+                .sorted().distinct().toArray();
+        // Exclude SEPARATOR_TOKEN, as it does not occur
+        var allTokens = IntStream.range(0, RTokenConstants.NUM_DIFF_TOKENS)
+                .filter(i -> i != TokenConstants.SEPARATOR_TOKEN).toArray();
+
+        if (allTokens.length > foundTokens.length) {
+            var diffLine = IntStream.range(0, allTokens.length).dropWhile(lineIdx -> lineIdx < foundTokens.length && allTokens[lineIdx] == foundTokens[lineIdx]).findFirst();
+            diffLine.ifPresent(lineIdx -> fail("Token type %s was not found in the complete code example '%s'.".formatted(new RToken(allTokens[lineIdx], fileName, -1, -1, -1).type2string(), fileName)));
+        }
+        assertArrayEquals(allTokens, foundTokens);
+    }
+
+    private static String getCommentExpression() {
+        return R_COMMENT;
+    }
+
+}

+ 37 - 0
jplag.frontend.R/src/test/resources/de/jplag/R/Complete.R

@@ -0,0 +1,37 @@
+# This R code sample is supposed to contain the corresponding AST structure for each type of RToken.
+# It is also working code.
+# Author: Robin Maisch
+
+main <- function() {
+	sixteen <- square(4);
+	squareOkay <- ifelse(sixteen==16, TRUE, FALSE);
+	cat("Should be 16: ", sixteen, squareOkay);
+	?cat;
+
+	if (squareOkay) {
+		perfectSquare <- square
+	} else {
+		perfectSquare <- function (x) x*x
+	}
+
+	for (i in 1:10) {
+		print(apply(arg=i, func=perfectSquare))
+	}
+
+	repeat {
+		if (FALSE) next
+		else break
+	}
+
+	idx <- 0
+	while (FALSE) {
+		idx <- idx + 1
+		print((0:40)[idx=])
+	}
+}
+oneMore <- function(var) base::'+'(var, 1)  		# weird way to access + operator
+square <- function(x) x+x   						# works for x=2, so that's not bad
+identity <- function(x) x
+apply <- function(arg=0, func=identity) func(arg)
+
+main()

+ 92 - 0
jplag.frontend.R/src/test/resources/de/jplag/R/Game.R

@@ -0,0 +1,92 @@
+# Sample R program
+# Author: Robin Maisch
+
+readBool <- function()
+{ 
+  n <- readline(prompt="Yes or no? (y/n): ")
+  while (!grepl("[YyNn]",n))
+  {
+	 n <- readline()
+  }
+  return(ifelse(n=="y"||n=="Y", TRUE, FALSE))
+}
+
+
+getFilterBalance <- function(f, elements) {
+	return(abs(length(Filter(f, elements)) - length(elements)/2))
+}
+
+divisibleByFilter <- function(i) {
+	res <- c(function(x) x %% i == 0, paste("a multiple of", i), NA);
+	return(res)
+}
+
+greaterThanFilter <- function(i) {
+	res <- c(function(x) x > i, paste("greater than", i), NA);
+	return(res)
+}
+
+smallerThanFilter <- function(i) {
+	res <- c(function(x) x < i, paste("smaller than", i), NA);
+	return(res)
+}
+
+endsWithFilter <- function(i) {
+	res <- c(function(x) x %% 10 == i, paste("'s last digit a'", i), NA);
+	return(res)
+}
+
+# real program start here
+  
+main <- function() {
+	filters <- c(lapply(2:20, divisibleByFilter),
+				lapply(0:100, greaterThanFilter),
+				lapply(0:100, smallerThanFilter),
+				lapply(0:10, endsWithFilter))
+	filters <- aperm(simplify2array(filters, higher=FALSE), c(2,1))
+	activeFilters = list()
+
+	cat("Think of a number between 0 and 100.\n")
+	count <- 1
+	candidates <- c()
+	repeat {
+		cat("\n +++ Round",count,"+++\n\n")
+		candidates = 0:100
+		for (f in activeFilters) {
+			filterFunc = ifelse(f[[3]], function(x) f[[1]](x), function(x) !f[[1]](x))
+			candidates = Filter(filterFunc, candidates)
+		}
+		if (length(candidates) == 1) {
+			break
+		}
+
+		balance <- sapply(1:(length(filters)/3), function(i) {return(getFilterBalance(filters[i,][[1]], candidates))})
+		rank <- rank(balance, ties="first")
+		risk = max(20-2*count, 1)
+		lowerEnd = 1
+		topFiveIdx = order(rank)[lowerEnd:risk]
+		# print(filters[topFiveIdx[1:3],2])
+		winnerIdx = topFiveIdx[sample.int(risk-lowerEnd + 1,1)]
+		winnerFilter = filters[winnerIdx,]
+
+		cat("Is your number", winnerFilter[[2]], "?\n")
+		winnerFilter[[3]] <- readBool()
+
+		cat("Okay, now, let's see...\n")
+		activeFilters[[length(activeFilters)+1]] = winnerFilter
+		# cat("Now we have", length(activeFilters), "active filters.\n")
+
+		filters[- winnerIdx,] # remove element
+		count <- count + 1
+	}
+
+	cat("Your number must be", candidates[1], ", right???\n")
+	answer <- readBool()
+	if (answer) {
+		cat("Hahaha, I knew it. I'm a genius.\n")
+	} else {
+		cat("You must be joking!\n")
+	}
+
+}
+main()