Robin Maisch 4 лет назад
Родитель
Сommit
4f8dad16b4
24 измененных файлов с 1755 добавлено и 299 удалено
  1. 13 0
      .github/workflows/maven.yml
  2. 1 1
      .github/workflows/publish.yml
  3. 14 1
      .github/workflows/report-viewer-test.yml
  4. 13 0
      .github/workflows/spotless.yml
  5. 16 0
      jplag.frontend.rlang/README.md
  6. 45 0
      jplag.frontend.rlang/pom.xml
  7. 216 0
      jplag.frontend.rlang/src/main/antlr4/de/jplag/R/grammar/R.g4
  8. 83 0
      jplag.frontend.rlang/src/main/antlr4/de/jplag/R/grammar/RFilter.g4
  9. 158 0
      jplag.frontend.rlang/src/main/java/de/jplag/R/JplagRListener.java
  10. 71 0
      jplag.frontend.rlang/src/main/java/de/jplag/R/Language.java
  11. 342 0
      jplag.frontend.rlang/src/main/java/de/jplag/R/RCombinedBaseListener.java
  12. 97 0
      jplag.frontend.rlang/src/main/java/de/jplag/R/RParserAdapter.java
  13. 46 0
      jplag.frontend.rlang/src/main/java/de/jplag/R/RToken.java
  14. 39 0
      jplag.frontend.rlang/src/main/java/de/jplag/R/RTokenConstants.java
  15. 114 0
      jplag.frontend.rlang/src/test/java/de/jplag/R/RFrontendTest.java
  16. 37 0
      jplag.frontend.rlang/src/test/resources/de/jplag/R/Complete.R
  17. 92 0
      jplag.frontend.rlang/src/test/resources/de/jplag/R/Game.R
  18. 5 1
      jplag/pom.xml
  19. 2 1
      jplag/src/main/java/de/jplag/options/LanguageOption.java
  20. 3 3
      jplag/src/main/java/de/jplag/reporting/JsonReport.java
  21. 4 2
      jplag/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java
  22. 9 3
      pom.xml
  23. 325 277
      report-viewer/package-lock.json
  24. 10 10
      report-viewer/package.json

+ 13 - 0
.github/workflows/maven.yml

@@ -9,7 +9,20 @@ on:
   workflow_dispatch:
 
 jobs:
+  pre_job:
+    runs-on: ubuntu-latest
+    outputs:
+      should_skip: ${{ steps.skip_check.outputs.should_skip }}
+    steps:
+      - id: skip_check
+        uses: fkirc/skip-duplicate-actions@master
+        with:
+          concurrent_skipping: 'same_content'
+          skip_after_successful_duplicate: 'true'
+
   build:
+    needs: pre_job
+    if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
     runs-on: ubuntu-latest
 
     steps:

+ 1 - 1
.github/workflows/publish.yml

@@ -18,7 +18,7 @@ jobs:
         with:
           servers: '[{ "id": "ossrh", "username": "jplag", "password": "${{ secrets.OSSRH_TOKEN }}" }]'
       - name: Import GPG key
-        uses: crazy-max/ghaction-import-gpg@v4
+        uses: crazy-max/ghaction-import-gpg@v5
         with:
           gpg_private_key: ${{ secrets.PGP_SECRET }}
           passphrase: ${{ secrets.PGP_PW }}

+ 14 - 1
.github/workflows/report-viewer-test.yml

@@ -6,7 +6,20 @@ on:
     types: [opened, synchronize, reopened]
       
 jobs:
-  build-and-deploy:
+  pre_job:
+    runs-on: ubuntu-latest
+    outputs:
+      should_skip: ${{ steps.skip_check.outputs.should_skip }}
+    steps:
+      - id: skip_check
+        uses: fkirc/skip-duplicate-actions@master
+        with:
+          concurrent_skipping: 'same_content'
+          skip_after_successful_duplicate: 'true'
+
+  build:
+    needs: pre_job
+    if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
     runs-on: ubuntu-latest
     steps:
       - name: Checkout 🛎️

+ 13 - 0
.github/workflows/spotless.yml

@@ -9,7 +9,20 @@ on:
   workflow_dispatch:
 
 jobs:
+  pre_job:
+    runs-on: ubuntu-latest
+    outputs:
+      should_skip: ${{ steps.skip_check.outputs.should_skip }}
+    steps:
+      - id: skip_check
+        uses: fkirc/skip-duplicate-actions@master
+        with:
+          concurrent_skipping: 'same_content'
+          skip_after_successful_duplicate: 'true'
+
   build:
+    needs: pre_job
+    if: ${{ needs.pre_job.outputs.should_skip != 'true' && github.actor != 'dependabot[bot]' }}
     runs-on: ubuntu-latest
 
     steps:

+ 16 - 0
jplag.frontend.rlang/README.md

@@ -0,0 +1,16 @@
+# JPlag R language frontend
+
+The JPlag R frontend allows the use of JPlag with submissions in R. <br>
+It was in part adapted from a [JPLag fork by CodeGra-de](https://github.com/CodeGra-de/jplag/tree/master/jplag.frontend.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.
+
+### Token Extraction
+
+The choice of tokens is based directly on the CodeGra-de version, whereas the extraction process itself contains some fixes.
+
+Like in other frontends, e.g. for Java and C#, the tokens account for the beginning and the end of control flow structures, for control flow keywords, and some kinds of expressions. As R is very different from other programming languages in JPlag, it remains to be seen whether the R frontend can hold up to the others.
+
+### 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).

+ 45 - 0
jplag.frontend.rlang/pom.xml

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>de.jplag</groupId>
+        <artifactId>aggregator</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <artifactId>rlang</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.antlr</groupId>
+            <artifactId>antlr4-runtime</artifactId>
+        </dependency>
+        <dependency>
+            <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>
+        <plugins>
+            <plugin>
+                <groupId>org.antlr</groupId>
+                <artifactId>antlr4-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>antlr4</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+        </plugins>
+    </build>
+</project>

+ 216 - 0
jplag.frontend.rlang/src/main/antlr4/de/jplag/R/grammar/R.g4

@@ -0,0 +1,216 @@
+/*
+ [The "BSD licence"]
+ Copyright (c) 2013 Terence Parr
+ All rights reserved.
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in the
+    documentation and/or other materials provided with the distribution.
+ 3. The name of the author may not be used to endorse or promote products
+    derived from this software without specific prior written permission.
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+/**
+derived from http://svn.r-project.org/R/trunk/src/main/gram.y
+http://cran.r-project.org/doc/manuals/R-lang.html#Parser
+I'm no R genius but this seems to work.
+Requires RFilter.g4 to strip away NL that are really whitespace,
+not end-of-command. See TestR.java
+Usage:
+$ antlr4 R.g4 RFilter.g4
+$ javac *.java
+$ java TestR sample.R
+... prints parse tree ...
+*/
+
+/*
+Modified version of the original in https://github.com/antlr/grammars-v4/blob/master/r/R.g4 so that I can separate the most relevant tokens of R in 
+the JplagRListenter.java file.
+Author of the modification: Antonio Javier Rodriguez Perez
+*/
+
+grammar R;
+
+prog:   (   expr (';'|NL)
+        |   NL
+        )*
+        EOF
+    ;
+
+/*
+expr_or_assign
+    :   expr ('<-'|'='|'<<-') expr_or_assign
+    |   expr
+    ;
+*/
+
+expr:   expr index_statement  // '[[' follows R's yacc grammar
+    |   expr access_package expr
+    |   expr ('$'|'@') expr
+    |   <assoc=right> expr '^' expr
+    |   ('-'|'+') expr
+    |   expr ':' expr
+    |   expr USER_OP expr // anything wrappedin %: '%' .* '%'
+    |   expr ('*'|'/') expr
+    |   expr ('+'|'-') expr
+    |   expr ('>'|'>='|'<'|'<='|'=='|'!=') expr
+    |   '!' expr
+    |   expr ('&'|'&&') expr
+    |   expr ('|'|'||') expr
+    |   '~' expr
+    |   expr '~' expr
+    |   expr assign_value expr
+    |   function_definition                 // define function
+    |   expr function_call                  // call function
+    |   compound_statement
+    |   if_statement
+    |   for_statement
+    |   while_statement
+    |   repeat_statement
+    |   help
+    |   next_statement
+    |   break_statement
+    |   '(' expr ')'
+    |   ID
+    |   constant
+    ;
+
+index_statement : '[[' sublist ']' ']' | '[' sublist ']' ;
+
+access_package: '::'|':::' ;
+
+function_definition: 'function' '(' formlist? ')' expr ;
+
+function_call : '(' sublist ')' ;
+
+constant: constant_number | constant_string | constant_bool | 'NULL' | 'NA' | 'Inf' | 'NaN' ;
+
+constant_number: HEX | INT | FLOAT | COMPLEX ;
+
+constant_string: STRING ;
+
+constant_bool: 'TRUE' | 'FALSE' ;
+
+help: '?' expr ; // get help on expr, usually string or ID
+
+if_statement :  'if' '(' expr ')' expr | 'if' '(' expr ')' expr 'else' expr ;
+
+for_statement : 'for' '(' ID 'in' expr ')' expr ;
+
+while_statement : 'while' '(' expr ')' expr ;
+
+repeat_statement: 'repeat' expr ;
+
+next_statement: 'next' ;
+
+break_statement: 'break' ;
+
+compound_statement: '{' exprlist '}' ;
+
+exprlist
+    :   expr ((';'|NL) expr?)*
+    |
+    ;
+
+formlist : form (',' form)* ;
+
+form:   ID
+    |   assign_func_declaration
+    ;
+
+sublist : sub (',' sub)* ;
+
+sub :   expr
+    |   assign_value_list
+    |
+    ;
+
+assign_value: '<-'|'<<-'|'='|'->'|'->>'|':=';
+
+assign_func_declaration: ID '=' expr | '...' ;
+
+assign_value_list: ID '=' | ID '=' expr | constant_string '=' | constant_string '=' expr | 'NULL' '=' | 'NULL' '=' expr | '...' ;
+
+
+
+HEX :   '0' ('x'|'X') HEXDIGIT+ [Ll]? ;
+
+INT :   DIGIT+ [Ll]? ;
+
+fragment
+HEXDIGIT : ('0'..'9'|'a'..'f'|'A'..'F') ;
+
+FLOAT:  DIGIT+ '.' DIGIT* EXP? [Ll]?
+    |   DIGIT+ EXP? [Ll]?
+    |   '.' DIGIT+ EXP? [Ll]?
+    ;
+
+fragment
+DIGIT:  '0'..'9' ;
+
+fragment
+EXP :   ('E' | 'e') ('+' | '-')? INT ;
+
+COMPLEX
+    :   INT 'i'
+    |   FLOAT 'i'
+    ;
+
+STRING
+    :   '"' ( ESC | ~[\\"] )*? '"'
+    |   '\'' ( ESC | ~[\\'] )*? '\''
+    |   '`' ( ESC | ~[\\'] )*? '`'
+    ;
+fragment
+ESC :   '\\' [abtnfrv"'\\]
+    |   UNICODE_ESCAPE
+    |   HEX_ESCAPE
+    |   OCTAL_ESCAPE
+    ;
+
+fragment
+UNICODE_ESCAPE
+    :   '\\' 'u' HEXDIGIT HEXDIGIT HEXDIGIT HEXDIGIT
+    |   '\\' 'u' '{' HEXDIGIT HEXDIGIT HEXDIGIT HEXDIGIT '}'
+    ;
+
+fragment
+OCTAL_ESCAPE
+    :   '\\' [0-3] [0-7] [0-7]
+    |   '\\' [0-7] [0-7]
+    |   '\\' [0-7]
+    ;
+
+fragment
+HEX_ESCAPE
+    :   '\\' HEXDIGIT HEXDIGIT?
+    ;
+
+ID  :   '.' (LETTER|'_'|'.') (LETTER|DIGIT|'_'|'.')*
+    |   LETTER (LETTER|DIGIT|'_'|'.')*
+    ;
+    
+fragment LETTER  : [a-zA-Z] ;
+
+USER_OP :   '%' .*? '%' ;
+
+COMMENT :   '#' .*? '\r'? '\n' -> type(NL) ;
+
+// Match both UNIX and Windows newlines
+NL      :   '\r'? '\n' ;
+
+WS      :   [ \t\u000C]+ -> skip ;

+ 83 - 0
jplag.frontend.rlang/src/main/antlr4/de/jplag/R/grammar/RFilter.g4

@@ -0,0 +1,83 @@
+/*
+ [The "BSD licence"]
+ Copyright (c) 2013 Terence Parr
+ All rights reserved.
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in the
+    documentation and/or other materials provided with the distribution.
+ 3. The name of the author may not be used to endorse or promote products
+    derived from this software without specific prior written permission.
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+/** Must process R input with this before passing to R.g4; see TestR.java
+    We strip NL inside expressions.
+ */
+
+parser grammar RFilter;
+
+options { tokenVocab=R; }
+
+@members {
+protected int curlies = 0;
+}
+
+// TODO: MAKE THIS GET ONE COMMAND ONLY
+stream : (element|NL|';')* EOF ;
+
+eat :   (NL {((WritableToken)$NL).setChannel(Token.HIDDEN_CHANNEL);})+ ;
+
+element:   op eat?
+    |   atom
+    |   '{' eat? {curlies++;} (element|NL|';')* {curlies--;} '}'
+    |   '(' (element|eat)* ')'
+    |   '[' (element|eat)* ']'
+    |   '[[' (element|eat)* ']' ']'
+    |   'function' eat? '(' (element|eat)* ')' eat?
+    |   'for' eat? '(' (element|eat)* ')' eat?
+    |   'while' eat? '(' (element|eat)* ')' eat?
+    |   'if' eat? '(' (element|eat)* ')' eat?
+    |   'else'
+        {
+        // ``inside a compound expression, a newline before else is discarded,
+        // whereas at the outermost level, the newline terminates the if
+        // construction and a subsequent else causes a syntax error.''
+        /*
+        Works here
+            if (1==0) { print(1) } else { print(2) }
+        and correctly gets error here:
+            if (1==0) { print(1) }
+            else { print(2) }
+        this works too:
+            if (1==0) {
+              if (2==0) print(1)
+              else print(2)
+            }
+        */
+        WritableToken tok = (WritableToken)_input.LT(-2);
+        if (curlies>0&&tok.getType()==NL) tok.setChannel(Token.HIDDEN_CHANNEL);
+        }
+    ;
+
+atom:   'next' | 'break' | ID | STRING | HEX | INT | FLOAT | COMPLEX | 'NULL'
+    |   'NA' | 'Inf' | 'NaN' | 'TRUE' | 'FALSE'
+    ;
+
+op  :   '+'|'-'|'*'|'/'|'^'|'<'|'<='|'>='|'>'|'=='|'!='|'&'|'&&'|USER_OP|
+        'repeat'|'in'|'?'|'!'|'='|':'|'~'|'$'|'@'|'<-'|'->'|'='|'::'|':::'|
+        ','|'...'|'||'| '|'
+    ;

+ 158 - 0
jplag.frontend.rlang/src/main/java/de/jplag/R/JplagRListener.java

@@ -0,0 +1,158 @@
+package de.jplag.R;
+
+import org.antlr.v4.runtime.Token;
+
+import de.jplag.R.grammar.*;
+
+/**
+ * Listener class for visiting the R ANTLR parse tree. Transforms the visited ANTLR token into JPlag tokens. Based on an
+ * R frontend for JPlag v2.15 by Olmo Kramer, see their
+ * <a href="https://github.com/CodeGra-de/jplag/tree/master/jplag.frontend.R">JPlag fork</a>.
+ * @author Robin Maisch
+ */
+public class JplagRListener extends RCombinedBaseListener implements RTokenConstants {
+
+    private final RParserAdapter parserAdapter;
+
+    /**
+     * Creates the listener.
+     * @param parserAdapter the JPlag parser adapter which receives the transformed tokens.
+     */
+    public JplagRListener(RParserAdapter parserAdapter) {
+        this.parserAdapter = parserAdapter;
+    }
+
+    /**
+     * Transforms an ANTLR Token into a JPlag token and transfers it to the token adapter.
+     * @param targetType the type of the JPlag token to be created.
+     * @param token the ANTLR token.
+     */
+    private void transformToken(int targetType, Token token) {
+        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 enterIndex_statement(RParser.Index_statementContext context) {
+        transformToken(INDEX, context.getStart(), context.getStop());
+    }
+
+    @Override
+    public void enterAccess_package(RParser.Access_packageContext context) {
+        transformToken(PACKAGE, context.getStart());
+    }
+
+    @Override
+    public void enterFunction_definition(RParser.Function_definitionContext context) {
+        transformToken(BEGIN_FUNCTION, context.getStart());
+    }
+
+    @Override
+    public void exitFunction_definition(RParser.Function_definitionContext context) {
+        transformToken(END_FUNCTION, context.getStop());
+    }
+
+    @Override
+    public void enterFunction_call(RParser.Function_callContext context) {
+        transformToken(FUNCTION_CALL, context.getStart(), context.getStop());
+    }
+
+    @Override
+    public void enterConstant_number(RParser.Constant_numberContext context) {
+        transformToken(NUMBER, context.getStart());
+    }
+
+    @Override
+    public void enterConstant_string(RParser.Constant_stringContext context) {
+        transformToken(STRING, context.getStart());
+    }
+
+    @Override
+    public void enterConstant_bool(RParser.Constant_boolContext context) {
+        transformToken(BOOL, context.getStart());
+    }
+
+    @Override
+    public void enterHelp(RParser.HelpContext context) {
+        transformToken(HELP, context.getStart());
+    }
+
+    @Override
+    public void enterIf_statement(RParser.If_statementContext context) {
+        transformToken(IF_BEGIN, context.getStart());
+    }
+
+    @Override
+    public void exitIf_statement(RParser.If_statementContext context) {
+        transformToken(IF_END, context.getStop());
+    }
+
+    @Override
+    public void enterFor_statement(RParser.For_statementContext context) {
+        transformToken(FOR_BEGIN, context.getStart());
+    }
+
+    @Override
+    public void exitFor_statement(RParser.For_statementContext context) {
+        transformToken(FOR_END, context.getStop());
+    }
+
+    @Override
+    public void enterWhile_statement(RParser.While_statementContext context) {
+        transformToken(WHILE_BEGIN, context.getStart());
+    }
+
+    @Override
+    public void exitWhile_statement(RParser.While_statementContext context) {
+        transformToken(WHILE_END, context.getStop());
+    }
+
+    @Override
+    public void enterRepeat_statement(RParser.Repeat_statementContext context) {
+        transformToken(REPEAT_BEGIN, context.getStart());
+    }
+
+    @Override
+    public void exitRepeat_statement(RParser.Repeat_statementContext context) {
+        transformToken(REPEAT_END, context.getStop());
+    }
+
+    @Override
+    public void enterNext_statement(RParser.Next_statementContext context) {
+        transformToken(NEXT, context.getStart());
+    }
+
+    @Override
+    public void enterBreak_statement(RParser.Break_statementContext context) {
+        transformToken(BREAK, context.getStart());
+    }
+
+    @Override
+    public void enterCompound_statement(RParser.Compound_statementContext context) {
+        transformToken(COMPOUND_BEGIN, context.getStart());
+    }
+
+    @Override
+    public void exitCompound_statement(RParser.Compound_statementContext context) {
+        transformToken(COMPOUND_END, context.getStop());
+    }
+
+    @Override
+    public void enterAssign_value(RParser.Assign_valueContext context) {
+        transformToken(ASSIGN, context.getStart());
+    }
+
+    @Override
+    public void enterAssign_func_declaration(RParser.Assign_func_declarationContext context) {
+        transformToken(ASSIGN_FUNC, context.getStart());
+    }
+
+    @Override
+    public void enterAssign_value_list(RParser.Assign_value_listContext context) {
+        transformToken(ASSIGN_LIST, context.getStart());
+    }
+
+}

+ 71 - 0
jplag.frontend.rlang/src/main/java/de/jplag/R/Language.java

@@ -0,0 +1,71 @@
+package de.jplag.R;
+
+import java.io.File;
+
+import de.jplag.ErrorConsumer;
+import de.jplag.TokenList;
+
+/**
+ * This represents the R language as a language supported by JPlag.
+ */
+public class Language implements de.jplag.Language {
+
+    public static final String NAME = "R Parser";
+    public static final String SHORT_NAME = "R";
+    public static final int DEFAULT_MIN_TOKEN_MATCH = 8;
+    private final RParserAdapter parserAdapter;
+
+    public Language(ErrorConsumer consumer) {
+        this.parserAdapter = new RParserAdapter(consumer);
+    }
+
+    @Override
+    public String[] suffixes() {
+        return new String[] {".R", ".r"};
+    }
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+
+    @Override
+    public String getShortName() {
+        return SHORT_NAME;
+    }
+
+    @Override
+    public int minimumTokenMatch() {
+        return DEFAULT_MIN_TOKEN_MATCH;
+    }
+
+    @Override
+    public TokenList parse(File directory, String[] files) {
+        return parserAdapter.parse(directory, files);
+    }
+
+    @Override
+    public boolean hasErrors() {
+        return parserAdapter.hasErrors();
+    }
+
+    @Override
+    public boolean supportsColumns() {
+        return true;
+    }
+
+    @Override
+    public boolean isPreformatted() {
+        return true;
+    }
+
+    @Override
+    public boolean usesIndex() {
+        return false;
+    }
+
+    @Override
+    public int numberOfTokens() {
+        return RTokenConstants.NUM_DIFF_TOKENS;
+    }
+}

+ 342 - 0
jplag.frontend.rlang/src/main/java/de/jplag/R/RCombinedBaseListener.java

@@ -0,0 +1,342 @@
+package de.jplag.R;
+
+import org.antlr.v4.runtime.ParserRuleContext;
+import org.antlr.v4.runtime.tree.ErrorNode;
+import org.antlr.v4.runtime.tree.TerminalNode;
+
+import de.jplag.R.grammar.*;
+
+/**
+ * Empty base implementation for {@link RListener} and {@link RFilterListener}.
+ */
+public abstract class RCombinedBaseListener implements RListener, RFilterListener {
+    @Override
+    public void enterStream(RFilter.StreamContext context) {
+
+    }
+
+    @Override
+    public void exitStream(RFilter.StreamContext context) {
+
+    }
+
+    @Override
+    public void enterEat(RFilter.EatContext context) {
+
+    }
+
+    @Override
+    public void exitEat(RFilter.EatContext context) {
+
+    }
+
+    @Override
+    public void enterElement(RFilter.ElementContext context) {
+
+    }
+
+    @Override
+    public void exitElement(RFilter.ElementContext context) {
+
+    }
+
+    @Override
+    public void enterAtom(RFilter.AtomContext context) {
+
+    }
+
+    @Override
+    public void exitAtom(RFilter.AtomContext context) {
+
+    }
+
+    @Override
+    public void enterOp(RFilter.OpContext context) {
+
+    }
+
+    @Override
+    public void exitOp(RFilter.OpContext context) {
+
+    }
+
+    @Override
+    public void enterProg(RParser.ProgContext context) {
+
+    }
+
+    @Override
+    public void exitProg(RParser.ProgContext context) {
+
+    }
+
+    @Override
+    public void enterExpr(RParser.ExprContext context) {
+
+    }
+
+    @Override
+    public void exitExpr(RParser.ExprContext context) {
+
+    }
+
+    @Override
+    public void enterIndex_statement(RParser.Index_statementContext context) {
+
+    }
+
+    @Override
+    public void exitIndex_statement(RParser.Index_statementContext context) {
+
+    }
+
+    @Override
+    public void enterAccess_package(RParser.Access_packageContext context) {
+
+    }
+
+    @Override
+    public void exitAccess_package(RParser.Access_packageContext context) {
+
+    }
+
+    @Override
+    public void enterFunction_definition(RParser.Function_definitionContext context) {
+
+    }
+
+    @Override
+    public void exitFunction_definition(RParser.Function_definitionContext context) {
+
+    }
+
+    @Override
+    public void enterFunction_call(RParser.Function_callContext context) {
+
+    }
+
+    @Override
+    public void exitFunction_call(RParser.Function_callContext context) {
+
+    }
+
+    @Override
+    public void enterConstant(RParser.ConstantContext context) {
+
+    }
+
+    @Override
+    public void exitConstant(RParser.ConstantContext context) {
+
+    }
+
+    @Override
+    public void enterConstant_number(RParser.Constant_numberContext context) {
+
+    }
+
+    @Override
+    public void exitConstant_number(RParser.Constant_numberContext context) {
+
+    }
+
+    @Override
+    public void enterConstant_string(RParser.Constant_stringContext context) {
+
+    }
+
+    @Override
+    public void exitConstant_string(RParser.Constant_stringContext context) {
+
+    }
+
+    @Override
+    public void enterConstant_bool(RParser.Constant_boolContext context) {
+
+    }
+
+    @Override
+    public void exitConstant_bool(RParser.Constant_boolContext context) {
+
+    }
+
+    @Override
+    public void enterHelp(RParser.HelpContext context) {
+
+    }
+
+    @Override
+    public void exitHelp(RParser.HelpContext context) {
+
+    }
+
+    @Override
+    public void enterIf_statement(RParser.If_statementContext context) {
+
+    }
+
+    @Override
+    public void exitIf_statement(RParser.If_statementContext context) {
+
+    }
+
+    @Override
+    public void enterFor_statement(RParser.For_statementContext context) {
+
+    }
+
+    @Override
+    public void exitFor_statement(RParser.For_statementContext context) {
+
+    }
+
+    @Override
+    public void enterWhile_statement(RParser.While_statementContext context) {
+
+    }
+
+    @Override
+    public void exitWhile_statement(RParser.While_statementContext context) {
+
+    }
+
+    @Override
+    public void enterRepeat_statement(RParser.Repeat_statementContext context) {
+
+    }
+
+    @Override
+    public void exitRepeat_statement(RParser.Repeat_statementContext context) {
+
+    }
+
+    @Override
+    public void enterNext_statement(RParser.Next_statementContext context) {
+
+    }
+
+    @Override
+    public void exitNext_statement(RParser.Next_statementContext context) {
+
+    }
+
+    @Override
+    public void enterBreak_statement(RParser.Break_statementContext context) {
+
+    }
+
+    @Override
+    public void exitBreak_statement(RParser.Break_statementContext context) {
+
+    }
+
+    @Override
+    public void enterCompound_statement(RParser.Compound_statementContext context) {
+
+    }
+
+    @Override
+    public void exitCompound_statement(RParser.Compound_statementContext context) {
+
+    }
+
+    @Override
+    public void enterExprlist(RParser.ExprlistContext context) {
+
+    }
+
+    @Override
+    public void exitExprlist(RParser.ExprlistContext context) {
+
+    }
+
+    @Override
+    public void enterFormlist(RParser.FormlistContext context) {
+
+    }
+
+    @Override
+    public void exitFormlist(RParser.FormlistContext context) {
+
+    }
+
+    @Override
+    public void enterForm(RParser.FormContext context) {
+
+    }
+
+    @Override
+    public void exitForm(RParser.FormContext context) {
+
+    }
+
+    @Override
+    public void enterSublist(RParser.SublistContext context) {
+
+    }
+
+    @Override
+    public void exitSublist(RParser.SublistContext context) {
+
+    }
+
+    @Override
+    public void enterSub(RParser.SubContext context) {
+
+    }
+
+    @Override
+    public void exitSub(RParser.SubContext context) {
+
+    }
+
+    @Override
+    public void enterAssign_value(RParser.Assign_valueContext context) {
+
+    }
+
+    @Override
+    public void exitAssign_value(RParser.Assign_valueContext context) {
+
+    }
+
+    @Override
+    public void enterAssign_func_declaration(RParser.Assign_func_declarationContext context) {
+
+    }
+
+    @Override
+    public void exitAssign_func_declaration(RParser.Assign_func_declarationContext context) {
+
+    }
+
+    @Override
+    public void enterAssign_value_list(RParser.Assign_value_listContext context) {
+
+    }
+
+    @Override
+    public void exitAssign_value_list(RParser.Assign_value_listContext context) {
+
+    }
+
+    @Override
+    public void visitTerminal(TerminalNode node) {
+
+    }
+
+    @Override
+    public void visitErrorNode(ErrorNode node) {
+
+    }
+
+    @Override
+    public void enterEveryRule(ParserRuleContext context) {
+
+    }
+
+    @Override
+    public void exitEveryRule(ParserRuleContext context) {
+
+    }
+}

+ 97 - 0
jplag.frontend.rlang/src/main/java/de/jplag/R/RParserAdapter.java

@@ -0,0 +1,97 @@
+package de.jplag.R;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.ParserRuleContext;
+import org.antlr.v4.runtime.tree.ParseTree;
+import org.antlr.v4.runtime.tree.ParseTreeWalker;
+
+import de.jplag.AbstractParser;
+import de.jplag.ErrorConsumer;
+import de.jplag.R.grammar.RFilter;
+import de.jplag.R.grammar.RLexer;
+import de.jplag.R.grammar.RParser;
+import de.jplag.TokenList;
+
+/**
+ * This class sets up the lexer and parser generated by ANTLR4, feeds the submissions through them and passes the
+ * selected tokens on to the main program.
+ */
+public class RParserAdapter extends AbstractParser implements RTokenConstants {
+
+    private String currentFile;
+    private TokenList tokens;
+
+    /**
+     * Creates the RParserAdapter
+     * @param errorConsumer the ErrorConsumer that parser errors are passed on to.
+     */
+    public RParserAdapter(ErrorConsumer errorConsumer) {
+        super(errorConsumer);
+    }
+
+    /**
+     * Parsers a list of files into a single {@link TokenList}.
+     * @param directory the directory of the files.
+     * @param fileNames the file names of the files.
+     * @return a {@link TokenList} containing all tokens of all files.
+     */
+    public TokenList parse(File directory, String[] fileNames) {
+        tokens = new TokenList();
+        errors = 0;
+        for (String fileName : fileNames) {
+            if (!parseFile(directory, fileName)) {
+                errors++;
+            }
+            tokens.addToken(new RToken(FILE_END, fileName, -1, -1, -1));
+        }
+        return tokens;
+    }
+
+    private boolean parseFile(File directory, String fileName) {
+        File file = new File(directory, fileName);
+        try (FileInputStream inputStream = new FileInputStream(file)) {
+            currentFile = fileName;
+
+            // create a lexer, a parser and a buffer between them.
+            RLexer lexer = new RLexer(CharStreams.fromStream(inputStream));
+            CommonTokenStream tokens = new CommonTokenStream(lexer);
+
+            RFilter filter = new RFilter(tokens);
+            filter.stream();
+            tokens.seek(0);
+
+            RParser parser = new RParser(tokens);
+
+            // Create a tree walker and the entry context defined by the parser grammar
+            ParserRuleContext entryContext = parser.prog();
+            ParseTreeWalker treeWalker = new ParseTreeWalker();
+
+            // Walk over the parse tree:
+            for (int i = 0; i < entryContext.getChildCount(); i++) {
+                ParseTree parseTree = entryContext.getChild(i);
+                treeWalker.walk(new JplagRListener(this), parseTree);
+            }
+        } catch (IOException exception) {
+            getErrorConsumer().addError("Parsing Error in '" + fileName + "':" + File.separator + exception);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Adds a new {@link de.jplag.Token} to the current {@link TokenList}.
+     * @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 length the length of the Token
+     */
+    /* package-private */ void addToken(int type, int line, int start, int length) {
+        tokens.addToken(new RToken(type, currentFile, line, start, length));
+
+    }
+}

+ 46 - 0
jplag.frontend.rlang/src/main/java/de/jplag/R/RToken.java

@@ -0,0 +1,46 @@
+package de.jplag.R;
+
+/**
+ * This class represents the occurrence of an R Token in the source code. Based on an R frontend for JPlag v2.15 by Olmo
+ * Kramer, see their <a href="https://github.com/CodeGra-de/jplag/tree/master/jplag.frontend.R">JPlag fork</a>.
+ */
+public class RToken extends de.jplag.Token implements RTokenConstants {
+
+    public RToken(int type, String file, int line, int column, int length) {
+        super(type, file, line, column, length);
+    }
+
+    @Override
+    public String type2string() {
+        return switch (this.type) {
+            case FILE_END -> "<EOF>";
+            case SEPARATOR_TOKEN -> "METHOD_SEPARATOR";
+            case BEGIN_FUNCTION -> "FUNCTION{";
+            case END_FUNCTION -> "}FUNCTION";
+            case FUNCTION_CALL -> "FUNCTION()";
+            case NUMBER -> "NUMBER";
+            case STRING -> "STRING";
+            case BOOL -> "BOOL";
+            case ASSIGN -> "ASSIGN";
+            case ASSIGN_FUNC -> "ASSIGN_FUNC";
+            case ASSIGN_LIST -> "ASSIGN_LIST";
+            case HELP -> "HELP";
+            case INDEX -> "INDEX";
+            case PACKAGE -> "PACKAGE";
+            case IF_BEGIN -> "IF{";
+            case IF_END -> "}IF-ELSE";
+            case FOR_BEGIN -> "FOR{";
+            case FOR_END -> "}FOR";
+            case WHILE_BEGIN -> "WHILE{";
+            case WHILE_END -> "}WHILE";
+            case REPEAT_BEGIN -> "REPEAT{";
+            case REPEAT_END -> "}REPEAT";
+            case NEXT -> "NEXT";
+            case BREAK -> "BREAK";
+            case COMPOUND_BEGIN -> "COMPOUND{";
+            case COMPOUND_END -> "}COMPOUND";
+            default -> "<UNKNOWN%d>".formatted(type);
+        };
+    }
+
+}

+ 39 - 0
jplag.frontend.rlang/src/main/java/de/jplag/R/RTokenConstants.java

@@ -0,0 +1,39 @@
+package de.jplag.R;
+
+import de.jplag.TokenConstants;
+
+/**
+ * Tokens in R that are deemed important when comparing submissions for plagiarisms. Based on an R frontend for JPlag
+ * v2.15 by Olmo Kramer, see their <a href="https://github.com/CodeGra-de/jplag/tree/master/jplag.frontend.R">JPlag
+ * fork</a>.
+ * @author Robin Maisch
+ */
+public interface RTokenConstants extends TokenConstants {
+
+    int BEGIN_FUNCTION = 2;
+    int END_FUNCTION = 3;
+    int FUNCTION_CALL = 4;
+    int NUMBER = 5;
+    int STRING = 6;
+    int BOOL = 7;
+    int ASSIGN = 8;
+    int ASSIGN_FUNC = 9;
+    int ASSIGN_LIST = 10;
+    int HELP = 11;
+    int INDEX = 12;
+    int PACKAGE = 13;
+    int IF_BEGIN = 14;
+    int IF_END = 15;
+    int FOR_BEGIN = 16;
+    int FOR_END = 17;
+    int WHILE_BEGIN = 18;
+    int WHILE_END = 19;
+    int REPEAT_BEGIN = 20;
+    int REPEAT_END = 21;
+    int NEXT = 22;
+    int BREAK = 23;
+    int COMPOUND_BEGIN = 24;
+    int COMPOUND_END = 25;
+
+    int NUM_DIFF_TOKENS = 26;
+}

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

@@ -0,0 +1,114 @@
+package de.jplag.R;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+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 org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.jplag.Token;
+import de.jplag.TokenConstants;
+import de.jplag.TokenList;
+import de.jplag.TokenPrinter;
+import de.jplag.testutils.TestErrorConsumer;
+
+public class RFrontendTest {
+
+    /**
+     * Regular expression for lines that contain no code.
+     */
+    private static final String R_NO_CODE_LINE = "\\s*(?:#.*)?";
+
+    /**
+     * 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";
+    public static final int NOT_SET = -1;
+
+    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 emptyLineExpression = getNoCodeLineExpression();
+
+            // All lines that contain code
+            var codeLines = IntStream.range(1, lines.size() + 1).filter(idx -> !lines.get(idx - 1).matches(emptyLineExpression)).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(lineIndex -> lineIndex < tokenLines.length && codeLines[lineIndex] == tokenLines[lineIndex]).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(lineIndex -> lineIndex < foundTokens.length && allTokens[lineIndex] == foundTokens[lineIndex]).findFirst();
+            diffLine.ifPresent(lineIdx -> fail("Token type %s was not found in the complete code example '%s'."
+                    .formatted(new RToken(allTokens[lineIdx], fileName, NOT_SET, NOT_SET, NOT_SET).type2string(), fileName)));
+        }
+        assertArrayEquals(allTokens, foundTokens);
+    }
+
+    private static String getNoCodeLineExpression() {
+        return R_NO_CODE_LINE;
+    }
+
+}

+ 37 - 0
jplag.frontend.rlang/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.rlang/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()

+ 5 - 1
jplag/pom.xml

@@ -14,7 +14,7 @@
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-databind</artifactId>
-            <version>2.13.2.2</version>
+            <version>2.13.3</version>
         </dependency>
         <dependency>
             <groupId>org.apache.commons</groupId>
@@ -45,6 +45,10 @@
             <groupId>de.jplag</groupId>
             <artifactId>cpp</artifactId>
         </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>rlang</artifactId>
+        </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>scheme</artifactId>

+ 2 - 1
jplag/src/main/java/de/jplag/options/LanguageOption.java

@@ -13,6 +13,7 @@ public enum LanguageOption {
     PYTHON_3("python3", "de.jplag.python3.Language"),
     C_CPP("cpp", "de.jplag.cpp.Language"),
     C_SHARP("csharp", "de.jplag.csharp.Language"),
+    R_LANG("rlang", "de.jplag.rlang.Language"),
     CHAR("char", "de.jplag.chars.Language"),
     TEXT("text", "de.jplag.text.Language"),
     SCHEME("scheme", "de.jplag.scheme.Language");
@@ -34,7 +35,7 @@ public enum LanguageOption {
     }
 
     public static LanguageOption fromDisplayName(String displayName) {
-        return Arrays.stream(LanguageOption.values()).filter(languageOption -> languageOption.displayName.equals(displayName)).findFirst()
+        return Arrays.stream(LanguageOption.values()).filter(languageOption -> languageOption.displayName.equalsIgnoreCase(displayName)).findFirst()
                 .orElse(getDefault());
     }
 

+ 3 - 3
jplag/src/main/java/de/jplag/reporting/JsonReport.java

@@ -24,9 +24,9 @@ public class JsonReport implements Report {
     @Override
     public boolean saveReport(JPlagResult result, String path) {
         JPlagReport report = ReportObjectFactory.getReportObject(result);
-        File dir = new File(path);
-        if (!dir.exists()) {
-            if (!dir.mkdir()) {
+        File directory = new File(path);
+        if (!directory.exists()) {
+            if (!directory.mkdirs()) {
                 logger.error("Failed to create dir.");
             }
         }

+ 4 - 2
jplag/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java

@@ -36,7 +36,8 @@ public class ReportObjectFactory {
      * Generates an Overview DTO of a JPlagResult.
      */
     private static OverviewReport generateOverviewReport(JPlagResult result) {
-        List<JPlagComparison> comparisons = result.getComparisons();
+        int numberOfComparisons = result.getOptions().getMaximumNumberOfComparisons();
+        List<JPlagComparison> comparisons = result.getComparisons(numberOfComparisons);
         OverviewReport overviewReport = new OverviewReport();
 
         // TODO: Consider to treat entries that were checked differently from old entries with prior work.
@@ -69,7 +70,8 @@ public class ReportObjectFactory {
      */
     private static List<ComparisonReport> generateComparisonReports(JPlagResult result) {
         List<ComparisonReport> comparisons = new ArrayList<>();
-        result.getComparisons().forEach(comparison -> comparisons.add( //
+        int numberOfComparisons = result.getOptions().getMaximumNumberOfComparisons();
+        result.getComparisons(numberOfComparisons).forEach(comparison -> comparisons.add( //
                 new ComparisonReport(comparison.getFirstSubmission().getName(), //
                         comparison.getSecondSubmission().getName(), //
                         comparison.similarity(), //

+ 9 - 3
pom.xml

@@ -49,6 +49,7 @@
         <module>jplag.frontend.csharp-6</module>
         <module>jplag.frontend.java</module>
         <module>jplag.frontend.python-3</module>
+        <module>jplag.frontend.rlang</module>
         <module>jplag.frontend.scheme</module>
         <module>jplag.frontend.text</module>
         <module>jplag</module>
@@ -83,7 +84,7 @@
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <maven.compiler.source>17</maven.compiler.source>
         <maven.compiler.target>17</maven.compiler.target>
-        <spotless.version>2.22.5</spotless.version>
+        <spotless.version>2.22.6</spotless.version>
         <slf4j.version>1.7.36</slf4j.version>
         <!-- The Revision of JPlag -->
         <revision>4.0.0-SNAPSHOT</revision>
@@ -156,6 +157,11 @@
                 <artifactId>csharp-6</artifactId>
                 <version>${project.version}</version>
             </dependency>
+            <dependency>
+                <groupId>${project.groupId}</groupId>
+                <artifactId>rlang</artifactId>
+                <version>${project.version}</version>
+            </dependency>
             <dependency>
                 <groupId>${project.groupId}</groupId>
                 <artifactId>cpp</artifactId>
@@ -205,7 +211,7 @@
         <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
-            <version>4.5.1</version>
+            <version>4.6.1</version>
             <scope>test</scope>
         </dependency>
         <dependency>
@@ -338,7 +344,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-surefire-plugin</artifactId>
-                <version>3.0.0-M6</version>
+                <version>3.0.0-M7</version>
             </plugin>
             <plugin>
                 <groupId>org.codehaus.mojo</groupId>

Разница между файлами не показана из-за своего большого размера
+ 325 - 277
report-viewer/package-lock.json


+ 10 - 10
report-viewer/package.json

@@ -9,13 +9,13 @@
   },
   "dependencies": {
     "@highlightjs/vue-plugin": "^2.1.0",
-    "chart.js": "^3.7.1",
+    "chart.js": "^3.8.0",
     "chartjs-plugin-datalabels": "^2.0.0",
-    "core-js": "^3.22.5",
+    "core-js": "^3.22.7",
     "gitart-vue-dialog": "^2.0.5",
     "highlight.js": "^11.5.1",
-    "jszip": "^3.9.1",
-    "vue": "^3.2.33",
+    "jszip": "^3.10.0",
+    "vue": "^3.2.36",
     "vue-chart-3": "^3.1.8",
     "vue-draggable-next": "^2.1.1",
     "vue-router": "^4.0.15",
@@ -23,22 +23,22 @@
     "vuex": "^4.0.2"
   },
   "devDependencies": {
-    "@typescript-eslint/eslint-plugin": "^5.23.0",
-    "@typescript-eslint/parser": "^5.23.0",
+    "@typescript-eslint/eslint-plugin": "^5.27.0",
+    "@typescript-eslint/parser": "^5.27.0",
     "@vue/cli-plugin-babel": "~5.0.4",
     "@vue/cli-plugin-eslint": "~5.0.4",
     "@vue/cli-plugin-router": "~5.0.1",
     "@vue/cli-plugin-typescript": "~5.0.4",
     "@vue/cli-service": "~5.0.4",
-    "@vue/compiler-sfc": "^3.2.33",
+    "@vue/compiler-sfc": "^3.2.36",
     "@vue/eslint-config-prettier": "^7.0.0",
     "@vue/eslint-config-typescript": "^10.0.0",
-    "eslint": "^8.15.0",
+    "eslint": "^8.16.0",
     "eslint-plugin-prettier": "^4.0.0",
     "eslint-plugin-vue": "^8.7.1",
-    "lint-staged": "^12.4.1",
+    "lint-staged": "^13.0.0",
     "prettier": "^2.6.2",
-    "typescript": "~4.6.4"
+    "typescript": "~4.7.2"
   },
   "gitHooks": {
     "pre-commit": "lint-staged"

Некоторые файлы не были показаны из-за большого количества измененных файлов