Browse Source

Merge branch 'master' into core-algorithm-refactoring

# Conflicts:
#	jplag.frontend-utils/src/main/java/de/jplag/TokenPrinter.java
#	jplag.frontend.chars/src/main/java/de/jplag/chars/CharToken.java
#	jplag.frontend.chars/src/main/java/de/jplag/chars/Language.java
#	jplag.frontend.chars/src/main/java/de/jplag/chars/Parser.java
#	jplag.frontend.cpp/src/main/java/de/jplag/cpp/Language.java
#	jplag.frontend.cpp/src/main/java/de/jplag/cpp/Scanner.java
#	jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/CSharpParserAdapter.java
#	jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/Language.java
#	jplag.frontend.java/src/main/java/de/jplag/java/Language.java
#	jplag.frontend.java/src/main/java/de/jplag/java/Parser.java
#	jplag.frontend.kotlin/src/main/java/de/jplag/kotlin/KotlinParserAdapter.java
#	jplag.frontend.kotlin/src/main/java/de/jplag/kotlin/Language.java
#	jplag.frontend.python-3/src/main/java/de/jplag/python3/Language.java
#	jplag.frontend.python-3/src/main/java/de/jplag/python3/Parser.java
#	jplag.frontend.rlang/src/main/java/de/jplag/rlang/Language.java
#	jplag.frontend.rlang/src/main/java/de/jplag/rlang/RParserAdapter.java
#	jplag.frontend.scheme/src/main/java/de/jplag/scheme/Language.java
#	jplag.frontend.scheme/src/main/java/de/jplag/scheme/Parser.java
#	jplag.frontend.text/src/main/java/de/jplag/text/Language.java
#	jplag.frontend.text/src/main/java/de/jplag/text/Parser.java
#	jplag/src/main/java/de/jplag/GreedyStringTiling.java
#	jplag/src/main/java/de/jplag/Match.java
#	jplag/src/main/java/de/jplag/Submission.java
#	jplag/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java
Jan Wittler 4 years ago
parent
commit
ee452364c3
100 changed files with 5811 additions and 566 deletions
  1. 7 12
      .github/workflows/sonarcloud-branch.yml
  2. 79 0
      .github/workflows/sonarcloud-pr.yml
  3. 6 0
      .gitignore
  4. 1 1
      README.md
  5. 19 10
      jplag.cli/pom.xml
  6. 20 9
      jplag.cli/src/main/java/de/jplag/CLI.java
  7. 270 0
      jplag.cli/src/main/java/de/jplag/logger/CollectedLogger.java
  8. 44 0
      jplag.cli/src/main/java/de/jplag/logger/CollectedLoggerFactory.java
  9. 4 0
      jplag.cli/src/main/java/de/jplag/logger/Triple.java
  10. 54 0
      jplag.cli/src/main/java/org/slf4j/impl/StaticLoggerBinder.java
  11. 1 1
      jplag.cli/src/test/java/de/jplag/cli/CommandLineInterfaceTest.java
  12. 105 0
      jplag.endToEndTesting/README.md
  13. 29 0
      jplag.endToEndTesting/pom.xml
  14. 32 0
      jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/constants/TestDirectoryConstants.java
  15. 120 0
      jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/helper/JPlagTestSuiteHelper.java
  16. 33 0
      jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/helper/JsonHelper.java
  17. 52 0
      jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/model/JsonModel.java
  18. 48 0
      jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/model/ResultModel.java
  19. 46 0
      jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/model/TestCaseModel.java
  20. 114 0
      jplag.endToEndTesting/src/test/java/de/jplag/end_to_end_testing/JavaEndToEndTest.java
  21. 36 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo.java
  22. 63 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1.java
  23. 79 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1_2.java
  24. 81 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1_3.java
  25. 71 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1_4.java
  26. 91 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1_5.java
  27. 91 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1_6.java
  28. 38 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo2.java
  29. 44 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo2_5.java
  30. 47 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo3.java
  31. 59 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo3_5.java
  32. 54 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo3_6.java
  33. 33 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo4.java
  34. 40 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo4d1.java
  35. 39 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo4d2.java
  36. 39 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo4d3.java
  37. 44 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo5.java
  38. 45 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo6.java
  39. 33 0
      jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo7.java
  40. 36 0
      jplag.endToEndTesting/src/test/resources/results/JavaResult.json
  41. 42 0
      jplag.frontend-testutils/src/test/java/de/jplag/testutils/FileUtil.java
  42. 0 28
      jplag.frontend-testutils/src/test/java/de/jplag/testutils/TestErrorConsumer.java
  43. 37 0
      jplag.frontend-testutils/src/test/java/de/jplag/testutils/TokenUtils.java
  44. 6 16
      jplag.frontend-utils/src/main/java/de/jplag/AbstractParser.java
  45. 0 20
      jplag.frontend-utils/src/main/java/de/jplag/ErrorConsumer.java
  46. 20 9
      jplag.frontend-utils/src/main/java/de/jplag/Language.java
  47. 35 5
      jplag.frontend-utils/src/main/java/de/jplag/TokenPrinter.java
  48. 0 16
      jplag.frontend.chars/pom.xml
  49. 0 42
      jplag.frontend.chars/src/main/java/de/jplag/chars/CharToken.java
  50. 0 69
      jplag.frontend.chars/src/main/java/de/jplag/chars/Language.java
  51. 0 201
      jplag.frontend.chars/src/main/java/de/jplag/chars/Parser.java
  52. 2 23
      jplag.frontend.cpp/src/main/java/de/jplag/cpp/Language.java
  53. 4 7
      jplag.frontend.cpp/src/main/java/de/jplag/cpp/Scanner.java
  54. 3 5
      jplag.frontend.cpp/src/main/javacc/CPP.jj
  55. 1 1
      jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/CSharpListener.java
  56. 5 8
      jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/CSharpParserAdapter.java
  57. 2 23
      jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/Language.java
  58. 11 7
      jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/grammar/CSharpPreprocessorParserBase.java
  59. 1 3
      jplag.frontend.csharp-6/src/test/java/de/jplag/csharp/MinimalCSharpFrontendTest.java
  60. 43 0
      jplag.frontend.emf-metamodel-dynamic/pom.xml
  61. 29 0
      jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/DynamicMetamodelToken.java
  62. 60 0
      jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/DynamicMetamodelTokenConstants.java
  63. 34 0
      jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/Language.java
  64. 29 0
      jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicEcoreParser.java
  65. 29 0
      jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicMetamodelTokenGenerator.java
  66. 61 0
      jplag.frontend.emf-metamodel-dynamic/src/test/java/de/jplag/emf/dynamic/MinimalDynamicMetamodelTest.java
  67. 16 0
      jplag.frontend.emf-metamodel-dynamic/src/test/resources/de/jplag/models/bookStore.ecore
  68. 38 0
      jplag.frontend.emf-metamodel-dynamic/src/test/resources/de/jplag/models/bookStoreExtended.ecore
  69. 16 0
      jplag.frontend.emf-metamodel-dynamic/src/test/resources/de/jplag/models/bookStoreRenamed.ecore
  70. 39 0
      jplag.frontend.emf-metamodel/pom.xml
  71. 72 0
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/Language.java
  72. 70 0
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/MetamodelToken.java
  73. 35 0
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/MetamodelTokenConstants.java
  74. 87 0
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/parser/EcoreParser.java
  75. 130 0
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/parser/MetamodelTokenGenerator.java
  76. 169 0
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/util/AbstractMetamodelVisitor.java
  77. 75 0
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/util/EMFUtil.java
  78. 90 0
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/util/MetamodelTreeView.java
  79. 66 0
      jplag.frontend.emf-metamodel/src/test/java/de/jplag/emf/MinimalMetamodelTest.java
  80. 16 0
      jplag.frontend.emf-metamodel/src/test/resources/de/jplag/models/bookStore.ecore
  81. 38 0
      jplag.frontend.emf-metamodel/src/test/resources/de/jplag/models/bookStoreExtended.ecore
  82. 16 0
      jplag.frontend.emf-metamodel/src/test/resources/de/jplag/models/bookStoreRenamed.ecore
  83. 29 0
      jplag.frontend.golang/README.md
  84. 45 0
      jplag.frontend.golang/pom.xml
  85. 521 0
      jplag.frontend.golang/src/main/antlr4/de/jplag/golang/grammar/GoLexer.g4
  86. 380 0
      jplag.frontend.golang/src/main/antlr4/de/jplag/golang/grammar/GoParser.g4
  87. 59 0
      jplag.frontend.golang/src/main/java/de/jplag/golang/GoParserAdapter.java
  88. 115 0
      jplag.frontend.golang/src/main/java/de/jplag/golang/GoToken.java
  89. 96 0
      jplag.frontend.golang/src/main/java/de/jplag/golang/GoTokenConstants.java
  90. 535 0
      jplag.frontend.golang/src/main/java/de/jplag/golang/JPlagGoListener.java
  91. 49 0
      jplag.frontend.golang/src/main/java/de/jplag/golang/Language.java
  92. 26 0
      jplag.frontend.golang/src/main/java/de/jplag/golang/grammar/GoParserBase.java
  93. 175 0
      jplag.frontend.golang/src/test/java/de/jplag/golang/GoFrontendTest.java
  94. 20 0
      jplag.frontend.golang/src/test/java/de/jplag/golang/GoTokenTestUtils.java
  95. 167 0
      jplag.frontend.golang/src/test/resources/de/jplag/golang/Complete.go
  96. 1 1
      jplag.frontend.java/src/main/java/de/jplag/java/JavaTokenConstants.java
  97. 6 9
      jplag.frontend.java/src/main/java/de/jplag/java/JavacAdapter.java
  98. 2 23
      jplag.frontend.java/src/main/java/de/jplag/java/Language.java
  99. 3 7
      jplag.frontend.java/src/main/java/de/jplag/java/Parser.java
  100. 8 10
      jplag.frontend.kotlin/src/main/java/de/jplag/kotlin/KotlinParserAdapter.java

+ 7 - 12
.github/workflows/sonarcloud.yml → .github/workflows/sonarcloud-branch.yml

@@ -1,13 +1,9 @@
-name: SonarCloud Scan (triggered by maven.yml)
+name: SonarCloud Scan (master branch)
 
 on:
-  workflow_run:
-    workflows:
-      - Build
-    types:
-      - completed
-
-
+  push:
+    branches:
+      - master
 
   # Allows you to run this workflow manually from the Actions tab
   workflow_dispatch:
@@ -16,13 +12,11 @@ jobs:
   sonar:
     name: SonarCloud Scan
     runs-on: ubuntu-latest
-    if: ${{ github.actor != 'dependabot[bot]' && github.event.workflow_run.conclusion == 'success' }}
+    if: ${{ github.actor != 'dependabot[bot]' }}
 
     steps:
       - uses: actions/checkout@v3
         with:
-          repository: ${{ github.event.workflow_run.head_repository.full_name }}
-          ref: ${{ github.event.workflow_run.head_branch }}
           fetch-depth: 0  # Shallow clones should be disabled for a better relevancy of analysis
       - name: Set up JDK
         uses: actions/setup-java@v3
@@ -42,8 +36,9 @@ jobs:
           path: ~/.m2
           key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
           restore-keys: ${{ runner.os }}-m2
+          
       - name: Build and analyze
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
-        run: mvn -Dsonar.scm.revision=${{ github.event.workflow_run.head_sha }} -Dsonar.pullrequest.key=${{ github.event.workflow_run.pull_requests[0] != null && github.event.workflow_run.pull_requests[0].number }} -Dsonar.pullrequest.branch=${{ github.event.workflow_run.pull_requests[0] != null && github.event.workflow_run.pull_requests[0].head.ref }} -Dsonar.pullrequest.base=${{ github.event.workflow_run.pull_requests[0] != null && github.event.workflow_run.pull_requests[0].base.ref }} -U -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar
+        run: mvn -U -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar

+ 79 - 0
.github/workflows/sonarcloud-pr.yml

@@ -0,0 +1,79 @@
+name: SonarCloud Scan (PRs triggered by maven.yml)
+
+on:
+  workflow_run:
+    workflows:
+      - Build
+    types:
+      - completed
+
+
+
+  # Allows you to run this workflow manually from the Actions tab
+  workflow_dispatch:
+
+jobs:
+  get-info:
+    name: "Get information about the source run"
+    runs-on: ubuntu-latest
+    if: ${{ github.actor != 'dependabot[bot]' && github.event.workflow_run.conclusion == 'success' }}
+    outputs:
+      sourceHeadRepo: ${{ steps.source-run-info.outputs.sourceHeadRepo }}
+      sourceHeadBranch: ${{ steps.source-run-info.outputs.sourceHeadBranch }}
+      sourceHeadSha: ${{ steps.source-run-info.outputs.sourceHeadSha }}
+      mergeCommitSha: ${{ steps.source-run-info.outputs.mergeCommitSha }}
+      targetCommitSha: ${{ steps.source-run-info.outputs.targetCommitSha }}
+      pullRequestNumber: ${{ steps.source-run-info.outputs.pullRequestNumber }}
+      pullRequestLabels: ${{ steps.source-run-info.outputs.pullRequestLabels }}
+      targetBranch: ${{ steps.source-run-info.outputs.targetBranch }}
+      sourceEvent: ${{ steps.source-run-info.outputs.sourceEvent }}
+    steps:
+      - name: "Get information about the origin 'CI' run"
+        uses: potiuk/get-workflow-origin@v1_3
+        id: source-run-info
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          sourceRunId: ${{ github.event.workflow_run.id }}
+
+  sonar:
+    name: SonarCloud Scan
+    runs-on: ubuntu-latest
+    if: ${{ needs.get-info.outputs.sourceEvent == 'pull_request' && github.actor != 'dependabot[bot]' && github.event.workflow_run.conclusion == 'success' }}
+    needs: get-info
+
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          repository: ${{ github.event.workflow_run.head_repository.full_name }}
+          ref: ${{ github.event.workflow_run.head_branch }}
+          fetch-depth: 0  # Shallow clones should be disabled for a better relevancy of analysis
+      - name: Set up JDK
+        uses: actions/setup-java@v3
+        with:
+          distribution: 'temurin'
+          java-version: 17
+
+      - name: Cache SonarCloud packages
+        uses: actions/cache@v3
+        with:
+          path: ~/.sonar/cache
+          key: ${{ runner.os }}-sonar
+          restore-keys: ${{ runner.os }}-sonar
+      - name: Cache Maven packages
+        uses: actions/cache@v3
+        with:
+          path: ~/.m2
+          key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
+          restore-keys: ${{ runner.os }}-m2
+          
+      - name: Build and analyze (PR)
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+        run: >
+          mvn 
+          -Dsonar.scm.revision=${{ github.event.workflow_run.head_sha }} 
+          -Dsonar.pullrequest.key=${{ needs.get-info.outputs.pullRequestNumber }} 
+          -Dsonar.pullrequest.branch=${{ needs.get-info.outputs.sourceHeadBranch }} 
+          -Dsonar.pullrequest.base=${{ needs.get-info.outputs.pullRequestNumber }} 
+          -U -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar

+ 6 - 0
.gitignore

@@ -22,3 +22,9 @@ hs_err_pid*
 .idea/
 *.iml
 .flattened-pom.xml
+
+# VSCode
+.vscode/
+
+# ANTLR
+.antlr/

+ 1 - 1
README.md

@@ -48,7 +48,7 @@ Usage: JPlag [ options ] [ <root-dir> ... ] [ -new <new-dir> ... ] [ -old <old-d
 
 named arguments:
   -h, --help       show this help message and exit
-  -l               {java,python3,cpp,csharp,rlang,kotlin,char,text,scheme} Select the language to parse the submissions (default: java)
+  -l               {java,python3,cpp,csharp,golang,kotlin,rlang,scala,text,scheme,emf-metamodel,emf-metamodel-dynamic} Select the language to parse the submissions (default: java)
   -bc BC           Path of the directory containing the base code (common framework used in all submissions)
   -v               {quiet,long} Verbosity of the logging (default: quiet)
   -d               Debug parser. Non-parsable files will be stored (default: false)

+ 19 - 10
jplag.cli/pom.xml

@@ -12,10 +12,6 @@
             <groupId>de.jplag</groupId>
             <artifactId>jplag</artifactId>
         </dependency>
-        <dependency>
-            <groupId>de.jplag</groupId>
-            <artifactId>jplag</artifactId>
-        </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>frontend-utils</artifactId>
@@ -42,27 +38,40 @@
         </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
-            <artifactId>rlang</artifactId>
+            <artifactId>golang</artifactId>
         </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>kotlin</artifactId>
         </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>rlang</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>scala</artifactId>
+        </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>scheme</artifactId>
         </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
-            <artifactId>chars</artifactId>
+            <artifactId>emf-metamodel</artifactId>
         </dependency>
         <dependency>
-            <groupId>net.sourceforge.argparse4j</groupId>
-            <artifactId>argparse4j</artifactId>
+            <groupId>de.jplag</groupId>
+            <artifactId>emf-metamodel-dynamic</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>endToEndTesting</artifactId>
         </dependency>
+
         <dependency>
-            <groupId>org.slf4j</groupId>
-            <artifactId>slf4j-simple</artifactId>
+            <groupId>net.sourceforge.argparse4j</groupId>
+            <artifactId>argparse4j</artifactId>
         </dependency>
     </dependencies>
     <build>

+ 20 - 9
jplag.cli/src/main/java/de/jplag/CLI.java

@@ -2,6 +2,7 @@ package de.jplag;
 
 import static de.jplag.CommandLineArgument.*;
 
+import java.security.SecureRandom;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -12,6 +13,7 @@ import net.sourceforge.argparse4j.inf.ArgumentParser;
 import net.sourceforge.argparse4j.inf.ArgumentParserException;
 import net.sourceforge.argparse4j.inf.Namespace;
 
+import org.slf4j.ILoggerFactory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -20,22 +22,24 @@ import de.jplag.clustering.ClusteringOptions;
 import de.jplag.clustering.Preprocessing;
 import de.jplag.clustering.algorithm.InterClusterSimilarity;
 import de.jplag.exceptions.ExitException;
+import de.jplag.logger.CollectedLoggerFactory;
 import de.jplag.options.JPlagOptions;
 import de.jplag.options.LanguageOption;
 import de.jplag.options.SimilarityMetric;
 import de.jplag.options.Verbosity;
-import de.jplag.reporting.JsonReport;
-import de.jplag.reporting.Report;
+import de.jplag.reporting.reportobject.ReportObjectFactory;
 import de.jplag.strategy.ComparisonMode;
 
 /**
  * Command line interface class, allows using via command line.
  * @see CLI#main(String[])
  */
-public class CLI {
+public final class CLI {
 
     private static final Logger logger = LoggerFactory.getLogger(CLI.class);
 
+    private static final Random RANDOM = new SecureRandom();
+
     private static final String CREDITS = "Created by IPD Tichy, Guido Malpohl, and others. JPlag logo designed by Sandro Koch. Currently maintained by Sebastian Hahner and Timur Saglam.";
 
     private static final String[] DESCRIPTIONS = {"Detecting Software Plagiarism", "Software-Archaeological Playground", "Since 1996",
@@ -60,14 +64,22 @@ public class CLI {
             JPlag program = new JPlag(options);
             logger.info("JPlag initialized");
             JPlagResult result = program.run();
-            Report report = new JsonReport();
-            report.saveReport(result, arguments.getString(RESULT_FOLDER.flagWithoutDash()));
+            ReportObjectFactory.createAndSaveReport(result, arguments.getString(RESULT_FOLDER.flagWithoutDash()));
+
         } catch (ExitException exception) {
             logger.error(exception.getMessage(), exception);
+            finalizeLogger();
             System.exit(1);
         }
     }
 
+    private static void finalizeLogger() {
+        ILoggerFactory factory = LoggerFactory.getILoggerFactory();
+        if (!(factory instanceof CollectedLoggerFactory collectedLoggerFactory))
+            return;
+        collectedLoggerFactory.finalizeInstances();
+    }
+
     /**
      * Creates the command line interface and initializes the argument parser.
      */
@@ -164,15 +176,14 @@ public class CLI {
     }
 
     private String generateDescription() {
-        var randomDescription = DESCRIPTIONS[new Random().nextInt(DESCRIPTIONS.length)];
-        return String.format("JPlag - %s" + System.lineSeparator() + CREDITS, randomDescription);
+        var randomDescription = DESCRIPTIONS[RANDOM.nextInt(DESCRIPTIONS.length)];
+        return String.format("JPlag - %s%n%s", CREDITS, randomDescription);
     }
 
     private void addAllMultiValueArgument(List<List<String>> argumentValues, List<String> destinationRootDirectories) {
         if (argumentValues == null) {
             return;
         }
-
-        argumentValues.stream().forEach(value -> destinationRootDirectories.addAll(value));
+        argumentValues.forEach(destinationRootDirectories::addAll);
     }
 }

+ 270 - 0
jplag.cli/src/main/java/de/jplag/logger/CollectedLogger.java

@@ -0,0 +1,270 @@
+package de.jplag.logger;
+
+import java.io.PrintStream;
+import java.io.Serial;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.concurrent.ConcurrentLinkedDeque;
+
+import org.slf4j.helpers.FormattingTuple;
+import org.slf4j.helpers.MarkerIgnoringBase;
+import org.slf4j.helpers.MessageFormatter;
+import org.slf4j.spi.LocationAwareLogger;
+
+/**
+ * This logger is able to collect errors and print them at the end. Mainly adopted from org.slf4j.impl.SimpleLogger
+ * @author Dominik Fuchss
+ */
+public final class CollectedLogger extends MarkerIgnoringBase {
+
+    @Serial
+    private static final long serialVersionUID = -1278670638921140275L;
+
+    private static final int LOG_LEVEL_TRACE = LocationAwareLogger.TRACE_INT;
+    private static final int LOG_LEVEL_DEBUG = LocationAwareLogger.DEBUG_INT;
+    private static final int LOG_LEVEL_INFO = LocationAwareLogger.INFO_INT;
+    private static final int LOG_LEVEL_WARN = LocationAwareLogger.WARN_INT;
+    private static final int LOG_LEVEL_ERROR = LocationAwareLogger.ERROR_INT;
+
+    private final int currentLogLevel = LOG_LEVEL_INFO;
+
+    /**
+     * The short name of this simple log instance
+     */
+    private transient String shortLogName = null;
+
+    /**
+     * Indicator whether finalization is in progress.
+     * @see #printAllErrorsForLogger()
+     */
+    private transient boolean isFinalizing = false;
+
+    private final transient SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-hh:mm:ss_SSS");
+
+    private final ConcurrentLinkedDeque<Triple<String, Throwable, Date>> allErrors = new ConcurrentLinkedDeque<>();
+
+    CollectedLogger(String name) {
+        this.name = name;
+    }
+
+    private void log(int level, String message, Throwable throwable) {
+        log(level, message, throwable, null);
+    }
+
+    private void log(int level, String message, Throwable throwable, Date timeOfError) {
+        if (!isLevelEnabled(level)) {
+            return;
+        }
+
+        if (level == LOG_LEVEL_ERROR && !isFinalizing) {
+            // Buffer errors for the final output
+            allErrors.add(new Triple<>(message, throwable, new Date()));
+            return;
+        }
+
+        StringBuilder builder = new StringBuilder(32);
+
+        // Append date-time
+        builder.append(dateFormat.format(timeOfError == null ? new Date() : timeOfError)).append(' ');
+
+        // Append current thread name
+        builder.append('[').append(Thread.currentThread().getName()).append("] ");
+        // Append current Level
+        builder.append('[').append(renderLevel(level)).append(']').append(' ');
+
+        // Append the name of the log instance
+        if (shortLogName == null)
+            shortLogName = computeShortName();
+        builder.append(shortLogName).append(" - ");
+        // Append the message
+        builder.append(message);
+
+        write(builder, throwable);
+    }
+
+    void printAllErrorsForLogger() {
+        this.isFinalizing = true;
+        // Copy errors to prevent infinite recursion
+        var errors = new ArrayList<>(this.allErrors);
+        if (errors.isEmpty())
+            return;
+
+        this.allErrors.removeAll(errors);
+
+        info("Summary of all Errors:");
+        errors.forEach(error -> log(LOG_LEVEL_ERROR, error.first(), error.second(), error.third()));
+        isFinalizing = false;
+    }
+
+    @SuppressWarnings("java:S106")
+    void write(StringBuilder buf, Throwable throwable) {
+        PrintStream targetStream = System.out;
+
+        targetStream.println(buf.toString());
+        writeThrowable(throwable, targetStream);
+        targetStream.flush();
+    }
+
+    private void writeThrowable(Throwable throwable, PrintStream targetStream) {
+        if (throwable != null) {
+            throwable.printStackTrace(targetStream);
+        }
+    }
+
+    private String computeShortName() {
+        return name.substring(name.lastIndexOf(".") + 1);
+    }
+
+    private boolean isLevelEnabled(int logLevel) {
+        return logLevel >= currentLogLevel;
+    }
+
+    private String renderLevel(int level) {
+        return switch (level) {
+            case LOG_LEVEL_TRACE -> "TRACE";
+            case LOG_LEVEL_DEBUG -> "DEBUG";
+            case LOG_LEVEL_INFO -> "INFO";
+            case LOG_LEVEL_WARN -> "WARN";
+            case LOG_LEVEL_ERROR -> "ERROR";
+            default -> throw new IllegalStateException("Unrecognized level [" + level + "]");
+        };
+    }
+
+    public boolean isTraceEnabled() {
+        return isLevelEnabled(LOG_LEVEL_TRACE);
+    }
+
+    @Override
+    public void trace(String message) {
+        log(LOG_LEVEL_TRACE, message, null);
+    }
+
+    public void trace(String format, Object param1) {
+        formatAndLog(LOG_LEVEL_TRACE, format, param1, null);
+    }
+
+    public void trace(String format, Object param1, Object param2) {
+        formatAndLog(LOG_LEVEL_TRACE, format, param1, param2);
+    }
+
+    public void trace(String format, Object... argArray) {
+        formatAndLog(LOG_LEVEL_TRACE, format, argArray);
+    }
+
+    public void trace(String message, Throwable t) {
+        log(LOG_LEVEL_TRACE, message, t);
+    }
+
+    public boolean isDebugEnabled() {
+        return isLevelEnabled(LOG_LEVEL_DEBUG);
+    }
+
+    public void debug(String message) {
+        log(LOG_LEVEL_DEBUG, message, null);
+    }
+
+    public void debug(String format, Object param1) {
+        formatAndLog(LOG_LEVEL_DEBUG, format, param1, null);
+    }
+
+    public void debug(String format, Object param1, Object param2) {
+        formatAndLog(LOG_LEVEL_DEBUG, format, param1, param2);
+    }
+
+    public void debug(String format, Object... argArray) {
+        formatAndLog(LOG_LEVEL_DEBUG, format, argArray);
+    }
+
+    public void debug(String message, Throwable throwable) {
+        log(LOG_LEVEL_DEBUG, message, throwable);
+    }
+
+    public boolean isInfoEnabled() {
+        return isLevelEnabled(LOG_LEVEL_INFO);
+    }
+
+    public void info(String message) {
+        log(LOG_LEVEL_INFO, message, null);
+    }
+
+    public void info(String format, Object arg) {
+        formatAndLog(LOG_LEVEL_INFO, format, arg, null);
+    }
+
+    public void info(String format, Object arg1, Object arg2) {
+        formatAndLog(LOG_LEVEL_INFO, format, arg1, arg2);
+    }
+
+    public void info(String format, Object... argArray) {
+        formatAndLog(LOG_LEVEL_INFO, format, argArray);
+    }
+
+    public void info(String message, Throwable throwable) {
+        log(LOG_LEVEL_INFO, message, throwable);
+    }
+
+    public boolean isWarnEnabled() {
+        return isLevelEnabled(LOG_LEVEL_WARN);
+    }
+
+    public void warn(String message) {
+        log(LOG_LEVEL_WARN, message, null);
+    }
+
+    public void warn(String format, Object arg) {
+        formatAndLog(LOG_LEVEL_WARN, format, arg, null);
+    }
+
+    public void warn(String format, Object arg1, Object arg2) {
+        formatAndLog(LOG_LEVEL_WARN, format, arg1, arg2);
+    }
+
+    public void warn(String format, Object... argArray) {
+        formatAndLog(LOG_LEVEL_WARN, format, argArray);
+    }
+
+    public void warn(String message, Throwable throwable) {
+        log(LOG_LEVEL_WARN, message, throwable);
+    }
+
+    public boolean isErrorEnabled() {
+        return isLevelEnabled(LOG_LEVEL_ERROR);
+    }
+
+    public void error(String message) {
+        log(LOG_LEVEL_ERROR, message, null);
+    }
+
+    public void error(String format, Object arg) {
+        formatAndLog(LOG_LEVEL_ERROR, format, arg, null);
+    }
+
+    public void error(String format, Object arg1, Object arg2) {
+        formatAndLog(LOG_LEVEL_ERROR, format, arg1, arg2);
+    }
+
+    public void error(String format, Object... argArray) {
+        formatAndLog(LOG_LEVEL_ERROR, format, argArray);
+    }
+
+    public void error(String message, Throwable throwable) {
+        log(LOG_LEVEL_ERROR, message, throwable);
+    }
+
+    private void formatAndLog(int level, String format, Object arg1, Object arg2) {
+        if (!isLevelEnabled(level)) {
+            return;
+        }
+        FormattingTuple formattingTuple = MessageFormatter.format(format, arg1, arg2);
+        log(level, formattingTuple.getMessage(), formattingTuple.getThrowable());
+    }
+
+    private void formatAndLog(int level, String format, Object... arguments) {
+        if (!isLevelEnabled(level)) {
+            return;
+        }
+        FormattingTuple formattingTuple = MessageFormatter.arrayFormat(format, arguments);
+        log(level, formattingTuple.getMessage(), formattingTuple.getThrowable());
+    }
+}

+ 44 - 0
jplag.cli/src/main/java/de/jplag/logger/CollectedLoggerFactory.java

@@ -0,0 +1,44 @@
+package de.jplag.logger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.slf4j.ILoggerFactory;
+import org.slf4j.Logger;
+
+/**
+ * The factory for {@link CollectedLogger}.
+ * @author Dominik Fuchss
+ */
+public final class CollectedLoggerFactory implements ILoggerFactory {
+
+    private final ConcurrentMap<String, CollectedLogger> loggerMap;
+
+    public CollectedLoggerFactory() {
+        loggerMap = new ConcurrentHashMap<>();
+    }
+
+    /**
+     * Return an appropriate {@link CollectedLogger} instance by name.
+     */
+    public Logger getLogger(String name) {
+        CollectedLogger simpleLogger = loggerMap.get(name);
+        if (simpleLogger != null) {
+            return simpleLogger;
+        } else {
+            CollectedLogger newInstance = new CollectedLogger(name);
+            Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
+            return oldInstance == null ? newInstance : oldInstance;
+        }
+    }
+
+    /**
+     * Print all errors of existing instances of {@link CollectedLogger}.
+     */
+    public void finalizeInstances() {
+        List<CollectedLogger> copy = new ArrayList<>(loggerMap.values());
+        copy.forEach(CollectedLogger::printAllErrorsForLogger);
+    }
+}

+ 4 - 0
jplag.cli/src/main/java/de/jplag/logger/Triple.java

@@ -0,0 +1,4 @@
+package de.jplag.logger;
+
+public record Triple<A, B, C> (A first, B second, C third) {
+}

+ 54 - 0
jplag.cli/src/main/java/org/slf4j/impl/StaticLoggerBinder.java

@@ -0,0 +1,54 @@
+package org.slf4j.impl;
+
+import org.slf4j.ILoggerFactory;
+import org.slf4j.LoggerFactory;
+import org.slf4j.spi.LoggerFactoryBinder;
+
+import de.jplag.logger.CollectedLoggerFactory;
+
+/**
+ * The binding of {@link LoggerFactory} class with an actual instance of {@link ILoggerFactory} is performed using
+ * information returned by this class. The class has been adopted from SimpleLogger.
+ * @author Dominik Fuchss
+ */
+public class StaticLoggerBinder implements LoggerFactoryBinder {
+
+    /**
+     * The unique instance of this class.
+     */
+    private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
+
+    /**
+     * Return the singleton of this class.
+     * @return the StaticLoggerBinder singleton
+     */
+    public static StaticLoggerBinder getSingleton() {
+        return SINGLETON;
+    }
+
+    /**
+     * Declare the version of the SLF4J API this implementation is compiled against. The value of this field is modified
+     * with each major release.
+     */
+    // to avoid constant folding by the compiler, this field must *not* be final
+    public static String REQUESTED_API_VERSION = "1.6.99"; // !final
+
+    private static final String loggerFactoryClassStr = CollectedLoggerFactory.class.getName();
+
+    /**
+     * The ILoggerFactory instance returned by the {@link #getLoggerFactory} method should always be the same object
+     */
+    private final ILoggerFactory loggerFactory;
+
+    private StaticLoggerBinder() {
+        loggerFactory = new CollectedLoggerFactory();
+    }
+
+    public ILoggerFactory getLoggerFactory() {
+        return loggerFactory;
+    }
+
+    public String getLoggerFactoryClassStr() {
+        return loggerFactoryClassStr;
+    }
+}

+ 1 - 1
jplag.cli/src/test/java/de/jplag/cli/CommandLineInterfaceTest.java

@@ -17,7 +17,7 @@ import de.jplag.options.JPlagOptions;
  * line arguments, thus these tests do not test the functionality of the options during the comparison.
  * @author Timur Saglam
  */
-public class CommandLineInterfaceTest {
+public abstract class CommandLineInterfaceTest {
     protected static final String CURRENT_DIRECTORY = ".";
     protected static final float DELTA = 0.0001f;
 

+ 105 - 0
jplag.endToEndTesting/README.md

@@ -0,0 +1,105 @@
+# JPlag - End To End Testing
+With the help of the end-to-end module, changes to the detection of JPlag are to be tested.
+With the help of elaborated plagiarisms, which have been worked out from suggestions in the literature on the topic of "plagiarism detection and avoidance", a wide range of detectable change can be covered. The selected plagiarisms are the decisive factor here as to whether a change in recognition can be perceived. 
+
+### References
+These elaborations provide basic ideas on how a modification of the plagiarized source code can look like or be adapted.
+These code adaptations refer to a wide range of changes starting from
+adding/removing comments to architectural changes in the deliverables.
+
+The following elaborations were used to be able to create the plagiarisms with the largest coverage:
+- [Mossad: defeating software plagiarism detection](https://dl.acm.org/doi/abs/10.1145/3428206 "Mossad: defeating software plagiarism detection")
+- [Detecting Source Code Plagiarism on Introductory Programming Course Assignments Using a Bytecode Approach - Oscar Karnalim](https://ieeexplore.ieee.org/abstract/document/7910274 "Detecting Source Code Plagiarism on Introductory Programming Course Assignments Using a Bytecode Approach - Oscar Karnalim")
+- [Detecting Disguised Plagiarism - Hatem A. Mahmoud](https://arxiv.org/abs/1711.02149 "Detecting Disguised Plagiarism - Hatem A. Mahmoud")
+- [Instructor-centric source code plagiarism detection and plagiarism corpus](https://dl.acm.org/doi/abs/10.1145/2325296.2325328 "Instructor-centric source code plagiarism detection and plagiarism corpus")
+
+### Steps Towards Plagiarism
+The following changes were applied to sample tasks to create test cases:
+<ul type="1">
+	<li>Inserting comments or empty lines (normalization level)</li>
+	<li>Changing variable names or function names (normalization level)</li>
+	<li>Insertion of unnecessary or changed code lines (token generation)</li>
+	<li>Changing the program flow (token generation) (statments and functions must be independent from each other)</li>
+		<ul>
+			<li>Variable decleration at the beginning of the program</li>
+			<li>Combining declerations of variables</li>
+			<li>Reuse of the same variable for other functions</li>
+		</ul>
+	<li>Changing control structures</li>
+		<ul>
+			<li>for(...) to while(...)</li>
+			<li>if(...) to switch-case</li>
+		</ul>
+	<li>Modification of expressions</li>
+		<ul>
+			<li>(X < Y) to !(X >= Y) and ++x to x = x + 1</li>
+		</ul>
+	<li>Splitting and merging statements</li>
+		<ul>
+			<li>x = getSomeValue(); y = x- z; to y = (getSomeValue() - Z;</li>
+		</ul>
+	<li>Inserting unnecessary casts</li>
+</ul>
+
+More detailed information about the create as well as about the subject to the issue can be found in the issue [Develop an end-to-end testing strategy](https://github.com/jplag/JPlag/issues/193 "Develop an end-to-end testing strategy").
+
+**The changes listed above have been developed and evaluated for purely scientific purposes and are not intended to be used for plagiarism in the public or private domain.**
+
+Software is according to [§ 2 of the copyright law](https://www.gesetze-im-internet.de/urhg/__2.html "§ 2 of the copyright law") a protected work which may not be plagiarized. 
+
+### JPlag - End To End TestSuite Structure
+The construction of an end to end test is done with the help of the JPlag api. 
+
+``` java 
+[...]
+    /**
+     * This method creates the necessary results as well as models for a test run and summarizes them for a comparison.
+     * @param testClassNames Plagiarized classes names in the resource directorie which are needed for the test
+     * @param testIdentifier name of the testId to load and identify the stored results
+     * @throws IOException is thrown in case of problems with copying the plagiarism classes
+     * @throws ExitException in case the plagiarism detection with JPlag is preemptively terminated would be of the test.
+     */
+    private void runJPlagTestSuite(String[] testClassNames, int testIdentifier) throws IOException, ExitException {
+        String functionName = StackWalker.getInstance().walk(stream -> stream.skip(1).findFirst().get()).getMethodName();
+        TestCaseModel testCaseModel = jplagTestSuiteHelper.createNewTestCase(testClassNames, functionName);
+        JPlagResult jplagResult = new JPlag(testCaseModel.getJPlagOptionsFromCurrentModel()).run();
+
+        for (JPlagComparison jPlagComparison : jplagResult.getAllComparisons()) {
+            assertEquals(testCaseModel.getCurrentJsonModel().getResultModelById(testIdentifier).getResultSimilarity(), jPlagComparison.similarity(),
+                    "The JPlag results [similarity] do not match the stored values!");
+        }
+    }
+[...]
+```
+The created plagiarisms are copied with the JPlagTestSuiteHelper into temporary directories, which can then be checked with JPlag. 
+
+In order to be able to distinguish in which domain of the recognition changes have occurred, fine granular test cases are used. These are composed of the changes already mentioned above. The plagiarism is compared with the original delivery and thus it is possible to detect and test small sections of the recognition. 
+
+The values compared so far to detect a match are limited to the similarity of the JPlag scan. It is already planned to expand the comparative values in order to have more indications of a change. 
+
+The Current Discussion is already taking place and can be found at [End to end testing - "comparative values"](https://github.com/jplag/JPlag/issues/548 "End to end testing - \"comparative values\""). 
+
+The current JPlag scans will be compared with the stored ones.
+This was done by storing the data in a *.json file which is read at the beginning of each test run. 
+
+``` json  
+{
+[...]
+    "function_name": "normalizationLevelTest",
+    "test_results": [
+      {
+        "result_similarity": 100,
+        "test_identifier": 0
+      },
+      {
+        "result_similarity": 100,
+        "test_identifier": 1
+      },
+      {
+        "result_similarity": 100,
+        "test_identifier": 2
+      }
+    ]
+  }
+[...]
+```

+ 29 - 0
jplag.endToEndTesting/pom.xml

@@ -0,0 +1,29 @@
+<?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 https://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>endToEndTesting</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>jplag</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>frontend-testutils</artifactId>
+            <version>${revision}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.13.3</version>
+        </dependency>
+    </dependencies>
+</project>

+ 32 - 0
jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/constants/TestDirectoryConstants.java

@@ -0,0 +1,32 @@
+package de.jplag.end_to_end_testing.constants;
+
+import java.nio.file.Path;
+
+/**
+ * All constant values that are needed in the test cases or helper classes.
+ */
+public final class TestDirectoryConstants {
+
+    private TestDirectoryConstants() {
+        // private constructor to prevent instantiation
+    }
+
+    /**
+     * Name for the folder to copy the submissions to in order to test them with JPlag
+     */
+    public static final String TEMPORARY_DIRECTORY_NAME = "testing-directory-submission";
+    /**
+     * Create the complete path to the submission files. Here the temporary system path is extended with the
+     * "TEMPORARY_DIRECTORY_NAME", which is predefined in this class.
+     */
+    public static final String TEMPORARY_SUBMISSION_DIRECTORY_NAME = Path.of("target", TEMPORARY_DIRECTORY_NAME).toString();
+
+    /**
+     * Base path to the created plagiarism and the main file located in the project resources.
+     */
+    public static final Path BASE_PATH_TO_JAVA_RESOURCES_SORTALGO = Path.of("src", "test", "resources", "java", "sortAlgo");
+    /**
+     * Base path to the saved results of the previous tests in a *.json file
+     */
+    public static final Path BASE_PATH_TO_JAVA_RESULT_JSON = Path.of("src", "test", "resources", "results", "JavaResult.json");
+}

+ 120 - 0
jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/helper/JPlagTestSuiteHelper.java

@@ -0,0 +1,120 @@
+package de.jplag.end_to_end_testing.helper;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Arrays;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.jplag.end_to_end_testing.constants.TestDirectoryConstants;
+import de.jplag.end_to_end_testing.model.JsonModel;
+import de.jplag.end_to_end_testing.model.TestCaseModel;
+import de.jplag.options.LanguageOption;
+
+/**
+ * This helper class deals with creating the test cases as well as copying and deleting for the end-to-end tests. The
+ * required plagiarisms are copied from the resource folder to a temporary location, thus creating a folder structure
+ * that can be tested by JPlag. Models are instantiated here and required information for the tests is loaded.
+ */
+public class JPlagTestSuiteHelper {
+
+    private static final Logger logger = LoggerFactory.getLogger(JPlagTestSuiteHelper.class);
+
+    private String[] resourceNames;
+    private List<JsonModel> resultModel;
+    private LanguageOption languageOption;
+
+    /**
+     * Helper class for the endToEnd tests. In this class the necessary resources are loaded, prepared and copied for the
+     * tests based on the passed parameters. An instance of this class loads all necessary paths and properties for a test
+     * run with the specified language
+     * @param languageOption for loading language-specific resources
+     * @throws IOException is thrown for all problems that may occur while parsing the json file. This includes both reading
+     * and parsing problems.
+     */
+    public JPlagTestSuiteHelper(LanguageOption languageOption) throws IOException {
+        this.languageOption = languageOption;
+        this.resourceNames = new File(TestDirectoryConstants.BASE_PATH_TO_JAVA_RESOURCES_SORTALGO.toString()).list();
+
+        this.resultModel = JsonHelper.getResultModelFromPath();
+        logger.debug("temp path at [{}]", TestDirectoryConstants.TEMPORARY_SUBMISSION_DIRECTORY_NAME);
+    }
+
+    /**
+     * creates all necessary folder paths and objects for a test run. Also searches for the stored previous results of the
+     * test in order to compare them with the current results.
+     * @param classNames Array of class names with language specific extension to be prepared for a test.
+     * @return comparison results saved for the test
+     * @throws IOException Exception can be thrown in cases that involve reading, copying or locating files.
+     */
+    public TestCaseModel createNewTestCase(String[] classNames, String functionName) throws IOException {
+        createNewTestCaseDirectory(classNames);
+        JsonModel resultJsonModel = resultModel.stream().filter(jsonModel -> functionName.equals(jsonModel.getFunctionName())).findAny().orElse(null);
+        return new TestCaseModel(TestDirectoryConstants.TEMPORARY_SUBMISSION_DIRECTORY_NAME, resultJsonModel, languageOption);
+    }
+
+    /**
+     * The copied data should be deleted after instance closure
+     * @throws IOException if an I/O error occurs
+     */
+    public void clear() throws IOException {
+        logger.debug("Class instance was cleaned!");
+        deleteCopiedFiles(new File(TestDirectoryConstants.TEMPORARY_SUBMISSION_DIRECTORY_NAME));
+    }
+
+    /**
+     * Copies the passed filenames to a temporary path to use them in the tests
+     * @throws IOException Exception can be thrown in cases that involve reading, copying or locating files.
+     */
+    private void createNewTestCaseDirectory(String[] classNames) throws IOException {
+        // before copying files to the test path, check if all files are in the resource
+        // directory
+        for (String className : classNames) {
+            if (!Arrays.asList(resourceNames).contains(className)) {
+                throw new FileNotFoundException(String.format("The specified class could not be found! [%s]", className));
+            }
+        }
+        // Copy the resources data to the temporary path
+        for (int counter = 0; counter < classNames.length; counter++) {
+            Path originalPath = Path.of(TestDirectoryConstants.BASE_PATH_TO_JAVA_RESOURCES_SORTALGO.toString(), classNames[counter]);
+            Path copiePath = Path.of(TestDirectoryConstants.TEMPORARY_SUBMISSION_DIRECTORY_NAME,
+                    TestDirectoryConstants.TEMPORARY_DIRECTORY_NAME + (counter + 1), classNames[counter]);
+
+            File directory = new File(copiePath.toString());
+            if (!directory.exists()) {
+                directory.mkdirs();
+            }
+
+            Files.copy(originalPath, copiePath, StandardCopyOption.REPLACE_EXISTING);
+            logger.debug("Copy file from [{}] to [{}]", originalPath, copiePath);
+        }
+    }
+
+    /**
+     * Delete directory with including files
+     * @param file Path to a folder or file to be deleted. This happens recursively to the path
+     * @throws IOException if an I/O error occurs
+     */
+    private void deleteCopiedFiles(File folder) throws IOException {
+        File[] files = folder.listFiles();
+        if (files != null) { // some JVMs return null for empty dirs
+            for (File file : files) {
+                if (file.isDirectory()) {
+                    deleteCopiedFiles(file);
+                } else {
+                    Files.delete(file.toPath());
+                    logger.debug("Delete file in folder: [{}]", file);
+                }
+            }
+        }
+        Files.delete(folder.toPath());
+        logger.debug("Delete folder: [{}]", folder);
+    }
+
+}

+ 33 - 0
jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/helper/JsonHelper.java

@@ -0,0 +1,33 @@
+package de.jplag.end_to_end_testing.helper;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import de.jplag.end_to_end_testing.constants.TestDirectoryConstants;
+import de.jplag.end_to_end_testing.model.JsonModel;
+
+/**
+ * Helper class for serializing and creating all json dependent events.
+ */
+public final class JsonHelper {
+
+    /**
+     * private constructor to prevent instantiation
+     */
+    private JsonHelper() {
+        // For Serialization
+    }
+
+    /**
+     * Parsing the old results in the json file as a list from ResultJsonModel.
+     * @return list of saved results for the test cases
+     * @throws IOException is thrown for all problems that may occur while parsing the json file. This includes both reading
+     * and parsing problems.
+     */
+    public static List<JsonModel> getResultModelFromPath() throws IOException {
+        return Arrays.asList(new ObjectMapper().readValue(TestDirectoryConstants.BASE_PATH_TO_JAVA_RESULT_JSON.toFile(), JsonModel[].class));
+    }
+}

+ 52 - 0
jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/model/JsonModel.java

@@ -0,0 +1,52 @@
+package de.jplag.end_to_end_testing.model;
+
+import java.util.Arrays;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The ResultJsonModel is the java object for the JavaResult.json file. The object contains all the necessary
+ * information for the comparisons in the test cases between old and new results, which have been stored in the
+ * JavaResult.json file.
+ */
+public class JsonModel {
+    @JsonProperty("function_name")
+    private String functionName;
+    @JsonProperty("test_results")
+    private ResultModel[] results;
+
+    /**
+     * Constructor for the JsonModel. The model is the serialization of the Json file in the form of a Java object.
+     * @param functionName the function name for the associated test results. Used as identifier to search results for the
+     * test cases.
+     * @param results Collection of the results that are in the current json file for certain tests
+     */
+    public JsonModel(String functionName, ResultModel[] results) {
+        this.functionName = functionName;
+        this.results = results;
+    }
+
+    /**
+     * empty constructor in case the serialization contains an empty object to prevent throwing exceptions. this constructor
+     * was necessary for serialization with the Jackson parse extension
+     */
+    public JsonModel() {
+        // For Serialization
+    }
+
+    /**
+     * @return the name of the currently used function stored in json
+     */
+    public String getFunctionName() {
+        return functionName;
+    }
+
+    /**
+     * returns the results for the function that have been stored for the given id.
+     * @param identifier for the comparative values
+     * @return associated comparison values that have been assigned to the identifier
+     */
+    public ResultModel getResultModelById(Integer identifier) {
+        return Arrays.asList(results).stream().filter(resultModel -> identifier.equals(resultModel.getTestId())).findAny().orElse(null);
+    }
+}

+ 48 - 0
jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/model/ResultModel.java

@@ -0,0 +1,48 @@
+package de.jplag.end_to_end_testing.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The ResultModel contains all information and comparison values that are important for a test and that could be parsed
+ * from the json file.
+ */
+public class ResultModel {
+
+    @JsonProperty("result_similarity")
+    private float resultSimilarity;
+    @JsonProperty("test_identifier")
+    private int testIdentifier;
+
+    /**
+     * Constructor for the ResultModel. The model is the serialization of the Json file in the form of a Java object.
+     * @param resultSimilarity stored comparative value of the similarity
+     * @param testIdentifier specifies which associated test results are needed for a test case and are therefore associated
+     * with this Id.
+     */
+    public ResultModel(float resultSimilarity, int testIdentifier) {
+        this.resultSimilarity = resultSimilarity;
+        this.testIdentifier = testIdentifier;
+    }
+
+    /**
+     * empty constructor in case the serialization contains an empty object to prevent throwing exceptions. this constructor
+     * was necessary for serialization with the Jackson parse extension
+     */
+    public ResultModel() {
+        // For Serialization
+    }
+
+    /**
+     * @return Identifier assigned to the result
+     */
+    public int getTestId() {
+        return testIdentifier;
+    }
+
+    /**
+     * @return of the comparative similarity
+     */
+    public float getResultSimilarity() {
+        return resultSimilarity;
+    }
+}

+ 46 - 0
jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/model/TestCaseModel.java

@@ -0,0 +1,46 @@
+package de.jplag.end_to_end_testing.model;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import de.jplag.options.JPlagOptions;
+import de.jplag.options.LanguageOption;
+
+/**
+ * Median class for trading the results and the paths used in the tests. Also the JPlagOptions used for a test run are
+ * created here.
+ */
+public class TestCaseModel {
+
+    private JsonModel jsonModel;
+    private LanguageOption languageOption;
+    private String submissionFolderPath;
+
+    /**
+     * Constructor for creating the instance
+     * @param submissionFolderPath path to the plagiarism folder structure to be tested using Jplag
+     * @param jsonModel the parsed result model needed for the test to be able to match it later in the tests.
+     * @param languageOption the language selection in which the tests take place
+     */
+    public TestCaseModel(String submissionFolderPath, JsonModel jsonModel, LanguageOption languageOption) {
+        this.submissionFolderPath = submissionFolderPath;
+        this.jsonModel = jsonModel;
+        this.languageOption = languageOption;
+    }
+
+    /**
+     * returns the, for the test, current result model that has been persisted in the saved list.
+     * @return ResultJsonModel loaded for the test
+     */
+    public JsonModel getCurrentJsonModel() {
+        return jsonModel;
+    }
+
+    /**
+     * creates the JPlag options for the test run from the values passed in the constructor.
+     * @return current JPlag options for the created object
+     */
+    public JPlagOptions getJPlagOptionsFromCurrentModel() {
+        return new JPlagOptions(new ArrayList<>(Arrays.asList(submissionFolderPath)), new ArrayList<>(), languageOption);
+    }
+}

+ 114 - 0
jplag.endToEndTesting/src/test/java/de/jplag/end_to_end_testing/JavaEndToEndTest.java

@@ -0,0 +1,114 @@
+package de.jplag.end_to_end_testing;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import de.jplag.JPlag;
+import de.jplag.JPlagComparison;
+import de.jplag.JPlagResult;
+import de.jplag.end_to_end_testing.constants.TestDirectoryConstants;
+import de.jplag.end_to_end_testing.helper.JPlagTestSuiteHelper;
+import de.jplag.end_to_end_testing.model.TestCaseModel;
+import de.jplag.exceptions.ExitException;
+import de.jplag.options.LanguageOption;
+
+/**
+ * Main test class for end-to-end testing in the Java language. The test cases aim to detect changes in the detection of
+ * plagiarism in the Java language and to be able to roughly categorize them. The plagiarism is compared with the
+ * original class. The results are compared with the results from previous tests and changes are detected.
+ */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class JavaEndToEndTest {
+
+    private JPlagTestSuiteHelper jplagTestSuiteHelper;
+
+    @BeforeAll
+    public void setUp() throws IOException {
+        jplagTestSuiteHelper = new JPlagTestSuiteHelper(LanguageOption.JAVA);
+        assertTrue(TestDirectoryConstants.BASE_PATH_TO_JAVA_RESOURCES_SORTALGO.toFile().exists(), "Could not find base directory!");
+        assertTrue(TestDirectoryConstants.BASE_PATH_TO_JAVA_RESULT_JSON.toFile().isFile(), "Could not find java result json!");
+    }
+
+    @AfterEach
+    public void teardown() throws IOException {
+        // after close the created directories are deleted
+        jplagTestSuiteHelper.clear();
+    }
+
+    /**
+     * Test cases created for the normalization level
+     * @return the classes to be tested with the corresponding identifier
+     */
+    private static Stream<Arguments> normalizationLevelTestArguments() {
+        return Stream.of(Arguments.of((Object) new String[] {"SortAlgo.java", "SortAlgo1.java"}, 0),
+                Arguments.of((Object) new String[] {"SortAlgo.java", "SortAlgo2.java"}, 1),
+                Arguments.of((Object) new String[] {"SortAlgo1.java", "SortAlgo2.java"}, 2));
+    }
+
+    /**
+     * Test cases for the token generation level
+     * @return the classes to be tested with the corresponding identifier
+     */
+    private static Stream<Arguments> tokenGenerationLevelTestArguments() {
+        return Stream.of(Arguments.of((Object) new String[] {"SortAlgo.java", "SortAlgo3.java"}, 0),
+                Arguments.of((Object) new String[] {"SortAlgo.java", "SortAlgo4.java"}, 1),
+                Arguments.of((Object) new String[] {"SortAlgo.java", "SortAlgo4d1.java"}, 2));
+    }
+
+    /**
+     * Inserting comments or empty lines (normalization level) -> id = 0 Changing variable names or function names
+     * (normalization level) -> id = 1 Inserting comments or empty lines (normalization level) -> id = 2
+     * @param testClassNames Plagiarized classes names in the resource directorie which are needed for the test
+     * @param testIdentifier name of the testId to load and identify the stored results
+     * @throws IOException is thrown in case of problems with copying the plagiarism classes
+     * @throws ExitException in case the plagiarism detection with JPlag is preemptively terminated would be of the test.
+     */
+    @ParameterizedTest
+    @MethodSource("normalizationLevelTestArguments")
+    void normalizationLevelTest(String[] testClassNames, int testIdentifier) throws IOException, ExitException {
+        runJPlagTestSuite(testClassNames, testIdentifier);
+    }
+
+    /**
+     * Insertion of unnecessary or changed code lines (token generation) -> id = 0 Changing the program flow (token
+     * generation) (statements and functions must be independent from each other) -> id = 1 Variable declaration at the
+     * beginning of the program (Detecting Source Code Plagiarism [...]) -> id = 2
+     * @param testClassNames Plagiarized classes names in the resource directorie which are needed for the test
+     * @param testIdentifier name of the testId to load and identify the stored results
+     * @throws IOException is thrown in case of problems with copying the plagiarism classes
+     * @throws ExitException in case the plagiarism detection with JPlag is preemptively terminated would be of the test.
+     */
+    @ParameterizedTest
+    @MethodSource("tokenGenerationLevelTestArguments")
+    void tokenGenerationLevelTest(String[] testClassNames, int testIdentifier) throws IOException, ExitException {
+        runJPlagTestSuite(testClassNames, testIdentifier);
+    }
+
+    /**
+     * This method creates the necessary results as well as models for a test run and summarizes them for a comparison.
+     * @param testClassNames Plagiarized classes names in the resource directorie which are needed for the test
+     * @param testIdentifier name of the testId to load and identify the stored results
+     * @throws IOException is thrown in case of problems with copying the plagiarism classes
+     * @throws ExitException in case the plagiarism detection with JPlag is preemptively terminated would be of the test.
+     */
+    private void runJPlagTestSuite(String[] testClassNames, int testIdentifier) throws IOException, ExitException {
+        String functionName = StackWalker.getInstance().walk(stream -> stream.skip(1).findFirst().get()).getMethodName();
+        TestCaseModel testCaseModel = jplagTestSuiteHelper.createNewTestCase(testClassNames, functionName);
+        JPlagResult jplagResult = new JPlag(testCaseModel.getJPlagOptionsFromCurrentModel()).run();
+
+        for (JPlagComparison jPlagComparison : jplagResult.getAllComparisons()) {
+            assertEquals(testCaseModel.getCurrentJsonModel().getResultModelById(testIdentifier).getResultSimilarity(), jPlagComparison.similarity(),
+                    "The JPlag results [similarity] do not match the stored values!");
+        }
+    }
+}

+ 36 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo.java

@@ -0,0 +1,36 @@
+package SortAlgos;
+public class SortAlgo {
+
+	public void BubbleSortRecursion(Integer arr[], int n) {
+		if (n == 1) 
+		{
+			return;
+		}
+
+		for (int i = 0; i < n - 1; i++)
+		{
+			if (arr[i] > arr[i + 1]) 
+			{ 
+				swap(arr, i , i+1);
+			}
+		}
+		BubbleSortRecursion(arr, n - 1);
+	}
+
+	public void BubbleSortWithoutRecursion(Integer arr[]) {
+		for(int i = arr.length; i > 1 ; i--) {
+			for(int innerCounter = 0; innerCounter < arr.length-1; innerCounter++)
+			{
+				if (arr[innerCounter] > arr[innerCounter + 1]) {
+					swap(arr, innerCounter, (innerCounter + 1));
+				}
+			}
+		}
+	}
+
+	private final <T> void swap(T[] arr, int i, int j) {
+		T t = arr[i];
+		arr[i] = arr[j];
+		arr[j] = t;
+	}
+}

+ 63 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1.java

@@ -0,0 +1,63 @@
+package SortAlgos;
+
+/*
+ * Unnecessary comment
+ * Unnecessary comment
+ * Unnecessary comment
+ * */
+public class SortAlgo1 {
+	//Unnecessary comment
+	public void BubbleSortRecursion(Integer arr[], int n) {
+		
+		if (n == 1) 
+		{
+			
+			return;
+			
+		}
+
+		for (int i = 0; i < n - 1; i++)
+		{
+			
+			if (arr[i] > arr[i + 1]) 
+				
+			{ 
+				
+				swap(arr, i , i+1);
+				
+			}
+		}
+		
+		BubbleSortRecursion(arr, n - 1);
+		
+	}
+	
+/*
+	
+	Unnecessary comment
+	*/
+	public void BubbleSortWithoutRecursion(Integer arr[]) {
+		//Unnecessary comment
+		for(int i = arr.length; i > 1 ; i--) {
+			
+			for(int innerCounter = 0; innerCounter < arr.length-1; innerCounter++)
+				
+			{
+				
+				if (arr[innerCounter] > arr[innerCounter + 1]) {
+					swap(arr, innerCounter, (innerCounter + 1));
+					
+				}
+			}
+		}
+	}
+
+	////Unnecessary comment
+	private final <T> void swap(T[] arr, int i, int j) {
+		T t = arr[i];
+		
+		arr[i] = arr[j];
+		
+		arr[j] = t;
+	}
+}

+ 79 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1_2.java

@@ -0,0 +1,79 @@
+package SortAlgos;
+
+public class SortAlgo1_2 {
+	//Unnecessary comment
+	public void ChangeFunctionName(Integer changeVariableArray[], int counter) {
+		
+		/*  
+		 * Comment
+		 * */
+		if (counter == 1) 
+		{
+			return;
+		}
+
+		for (int i = 0; i < counter - 1; i++)
+		{
+			
+			
+			if (changeVariableArray[i] > changeVariableArray[i + 1]) 
+			{ 
+				
+				
+				paws(changeVariableArray, i , i+1);
+				
+				
+			}
+		}
+		ChangeFunctionName(changeVariableArray, counter - 1);
+	}
+		
+	/*
+		
+		Unnecessary comment
+		*/
+	public void ChangeFunctionNameWithoutRecursion(Integer arr[]) {
+		//Unnecessary comment
+		for(int otherVariable = arr.length; otherVariable > 1 ; otherVariable--) {
+			
+			
+			for(int counter = 0; counter < arr.length-1; counter++)
+				
+			{
+				if (arr[counter] > arr[counter + 1]) {
+					paws(arr, counter, (counter + 1));
+					
+				}
+				
+			}
+		}
+	}
+		public void BubbleSortWithoutRecursion(Integer arr[]) {
+			//Unnecessary comment
+			for(int i = arr.length; i > 1 ; i--) {
+				
+				for(int innerCounter = 0; innerCounter < arr.length-1; innerCounter++)
+					
+				{
+					
+					if (arr[innerCounter] > arr[innerCounter + 1]) {
+						paws(arr, innerCounter, (innerCounter + 1));
+						
+					}
+					
+				}
+			}
+		}
+
+		////Unnecessary comment
+		private final <T> void paws(T[] arr, int i, int j) {
+			T t = arr[i];
+			//Comment
+			arr[i] = arr[j];
+			//Comment
+			arr[j] = t;
+		}
+}
+
+
+//Comment

+ 81 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1_3.java

@@ -0,0 +1,81 @@
+package SortAlgos;
+
+public class SortAlgo1_3 {
+	
+	private boolean _notUsedBool = false;
+	private int _thisIsNotUsed = 0;
+//Unnecessary comment
+	public void BubbleSortRecursion(Integer arr[], int n) {
+		if (_thisIsNotUsed == 0 && !_notUsedBool)
+			if (n == 1) {
+				return;
+			}
+		/*
+		 * Unnecessary comment
+		 * */
+
+		var tempVar = 0;
+		//Comment
+		var tempVar1 = tempVar + 1;
+		//Comment
+		var tempVar2 = tempVar1 + 1;
+		
+		for (int i = 0; i < n - 1; i++) {
+			
+			/*
+			 * Comment
+			 * Comment
+			 * */
+			if (arr[i] > arr[i + 1]) {
+				
+				
+				swap(arr, i, i + 1);
+				
+			}
+		}
+		
+		
+		//Unnecessary comment
+		BubbleSortRecursion(arr, n - 1);
+	}
+/*
+ * Unnecessary comment
+ * Unnecessary comment
+ * */
+	public void BubbleSortWithoutRecursion(Integer arr[]) {
+		//Unnecessary comment
+		var tempVar = 0;
+		var tempVar1 = tempVar + 1;
+		tempVar1 = tempVar1 + 1;
+		tempVar = tempVar1 *1;
+		
+		for (int i = arr.length; i > 1; i--) {
+			//Comment
+			for (int innerCounter = 0; innerCounter < arr.length - 1; innerCounter++) {
+				if (arr[innerCounter] > arr[innerCounter + 1]) {
+					
+					swap(arr, innerCounter, (innerCounter + 1));
+					
+					
+					
+				}
+			}
+		}
+	}
+
+	private final <T> void swap(T[] arr, int i, int j) {
+		var tempVar1 = 0;
+		var tempVar2 = tempVar1 + 1;
+		tempVar2 = tempVar2 + 1;
+		
+		//Unnecessary comment
+		
+		
+		if (true) {
+			T t = arr[i];
+			arr[i] = arr[j];
+			arr[j] = t;
+			tempVar2 = 0;
+		}
+	}
+}

+ 71 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1_4.java

@@ -0,0 +1,71 @@
+package SortAlgos;
+/*
+ * Unnecessary comment
+ * Unnecessary comment
+ * Unnecessary comment
+ * */
+public class SortAlgo1_4 {
+	
+	/*
+	
+	Unnecessary comment
+	*/
+	public void BubbleSortWithoutRecursion(Integer arr[]) {
+		//Unnecessary comment
+		for(int i = arr.length; i > 1 ; i--) {
+			
+			for(int innerCounter = 0; innerCounter < arr.length-1; innerCounter++)
+				
+			{
+				
+				if (arr[innerCounter] > arr[innerCounter + 1]) {
+					swap(arr, innerCounter, (innerCounter + 1));
+					
+				}
+			}
+		}
+	}
+	
+
+	////Unnecessary comment
+	private final <T> void swap(T[] arr, int i, int j) {
+		T t = arr[i];
+		
+		arr[i] = arr[j];
+		
+		arr[j] = t;
+	}
+	
+	//Unnecessary comment
+		public void BubbleSortRecursion(Integer arr[], int n) {
+			
+
+			for (int i = 0; i < n - 1; i++)
+			{
+				
+				if (arr[i] > arr[i + 1]) 
+					
+				{ 
+					
+					swap(arr, i , i+1);
+					
+				}
+			}
+			
+			
+			if (n == 2) {
+				
+				return;
+				
+			} else {
+				
+				
+				BubbleSortRecursion(arr, n - 1);
+				
+			}
+			
+		}
+		
+	
+
+}

+ 91 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1_5.java

@@ -0,0 +1,91 @@
+package SortAlgos;
+/*
+ * Unnecessary comment
+ * Unnecessary comment
+ * Unnecessary comment
+ * */
+public class SortAlgo1_5 {
+	
+	public void BubbleSortRecursion(Integer arr[], int n) {
+		
+		switch (n) {
+		
+		case 1:
+			return;
+			
+			
+			
+		}
+	////Unnecessary comment
+	////Unnecessary comment
+	////Unnecessary comment
+		/*
+		 * 
+		 * 
+		 * 
+		 * */
+		int i = 0;
+		
+		while(i < n-1)
+			
+		{
+			var tempBool = arr[i] > arr[i + 1];
+			
+			if (tempBool) {
+				
+				swap(arr, i, i + 1);
+				
+			}
+			
+			i++;
+		}
+		
+		BubbleSortRecursion(arr, n - 1);
+		
+		
+	}
+////Unnecessary comment
+	public void BubbleSortWithoutRecursion(Integer arr[]) {
+		
+		
+		int i = arr.length;
+		
+	////Unnecessary comment
+		while(i > 1)
+		{
+		////Unnecessary comment
+			int innerCounter = 0;
+			
+			
+			while(innerCounter < arr.length -1)
+			////Unnecessary comment
+			{
+				if (arr[innerCounter] > arr[innerCounter + 1]) {
+					
+					
+					swap(arr, innerCounter, (innerCounter + 1));
+					
+				}
+				
+				
+				innerCounter++;
+				
+				
+			}
+			i--;
+		}
+	}
+////Unnecessary comment
+	private final <T> void swap(T[] arr, int i, int j) {
+		
+	////Unnecessary comment////Unnecessary comment
+		T t = arr[i];
+		
+		
+	////Unnecessary comment
+		arr[i] = arr[j];
+		
+	////Unnecessary comment
+		arr[j] = t;
+	}
+}

+ 91 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo1_6.java

@@ -0,0 +1,91 @@
+package SortAlgos;
+
+/*
+ * Unnecessary comment
+ * Unnecessary comment
+ * Unnecessary comment
+ * */
+
+
+public class SortAlgo1_6 {
+	
+	//
+	//
+	//
+	//
+	public void BubbleSortRecursion(Integer arr[], int n) {
+		/*
+		 * Unnecessary comment
+		 * Unnecessary comment
+		 * Unnecessary comment
+		 * */
+		if (n != 1) 
+		{
+			//
+			
+			for (int i = 0; !(i >= (n - 1));)
+			{
+				
+				
+				if (!(arr[i] <= arr[i + 1])) 
+				{ 
+					
+					
+					swap(arr, i , i+1);
+				}
+				
+				
+				i = i + 1;
+			}
+			
+			
+			BubbleSortRecursion(arr, n - 1);
+		}
+		else
+			
+			
+		{
+			return;
+		}
+	}
+
+	/*
+	 * Unnecessary comment
+	 * Unnecessary comment
+	 * Unnecessary comment
+	 * */
+	public void BubbleSortWithoutRecursion(Integer arr[]) {
+		
+		for(int i = arr.length; !(i < 1) ; i--) {
+			
+			////Comment
+			//Comment
+			//Comment
+			
+			for(int innerCounter = 0; innerCounter < arr.length-1; innerCounter++)
+			{
+				
+				//Comment
+				if (!(arr[innerCounter] <= arr[innerCounter + 1])) {
+					
+					
+					swap(arr, innerCounter, (innerCounter + 1));
+					
+					    
+				}
+			}
+		}
+	}
+
+	private final <T> void swap(T[] arr, int i, int j) {
+		
+		T t = arr[i];
+		
+		
+		arr[i] = arr[j];
+		
+		arr[j] = t;
+		
+		
+	}
+}

+ 38 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo2.java

@@ -0,0 +1,38 @@
+package SortAlgos;
+
+
+public class SortAlgo2 {
+
+	public void ChangeFunctionName(Integer changeVariableArray[], int counter) {
+		if (counter == 1) 
+		{
+			return;
+		}
+
+		for (int i = 0; i < counter - 1; i++)
+		{
+			if (changeVariableArray[i] > changeVariableArray[i + 1]) 
+			{ 
+				paws(changeVariableArray, i , i+1);
+			}
+		}
+		ChangeFunctionName(changeVariableArray, counter - 1);
+	}
+
+	public void ChangeFunctionNameWithoutRecursion(Integer arr[]) {
+		for(int otherVariable = arr.length; otherVariable > 1 ; otherVariable--) {
+			for(int counter = 0; counter < arr.length-1; counter++)
+			{
+				if (arr[counter] > arr[counter + 1]) {
+					paws(arr, counter, (counter + 1));
+				}
+			}
+		}
+	}
+
+	private final <T> void paws(T[] otherArr, int i, int j) {
+		T t = otherArr[i];
+		otherArr[i] = otherArr[j];
+		otherArr[j] = t;
+	}
+}

+ 44 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo2_5.java

@@ -0,0 +1,44 @@
+package SortAlgos;
+
+public class SortAlgo2_5 {
+	public void ChangeFunctionName(Integer changeVariableArray[], int counter) {
+		switch (counter) {
+		case 1:
+			return;
+		}
+
+		int i = 0;
+		while(i < counter-1)
+		{
+			var tempBool = changeVariableArray[i] > changeVariableArray[i + 1];
+			if (tempBool) {
+				paws(changeVariableArray, i, i + 1);
+			}
+			i++;
+		}
+		
+		ChangeFunctionName(changeVariableArray, counter - 1);
+	}
+
+	public void ChangeFunctionNameWithoutRecursion(Integer arr[]) {
+		int otherVariable = arr.length;
+		while(otherVariable > 1)
+		{
+			int innerCounter = 0;
+			while(innerCounter < arr.length -1)
+			{
+				if (arr[innerCounter] > arr[innerCounter + 1]) {
+					paws(arr, innerCounter, (innerCounter + 1));
+				}
+				innerCounter++;
+			}
+			otherVariable--;
+		}
+	}
+
+	private final <T> void paws(T[] otherArr, int i, int j) {
+		T t = otherArr[i];
+		otherArr[i] = otherArr[j];
+		otherArr[j] = t;
+	}
+}

+ 47 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo3.java

@@ -0,0 +1,47 @@
+package SortAlgos;
+
+public class SortAlgo3 {
+
+	private int _thisIsNotUsed = 0;
+
+	public void BubbleSortRecursion(Integer arr[], int n) {
+		if (_thisIsNotUsed == 0)
+			if (n == 1) {
+				return;
+			}
+
+		var tempVar = 0;
+		tempVar ++;
+		tempVar = tempVar -1;
+		
+		for (int i = 0; i < n - 1; i++) {
+			if (arr[i] > arr[i + 1]) {
+				swap(arr, i, i + 1);
+			}
+		}
+		BubbleSortRecursion(arr, n - 1);
+	}
+
+	public void BubbleSortWithoutRecursion(Integer arr[]) {
+		var tempVar = 0;
+		for (int i = arr.length; i > 1; i--) {
+			for (int innerCounter = 0; innerCounter < arr.length - 1; innerCounter++) {
+				if (arr[innerCounter] > arr[innerCounter + 1]) {
+					swap(arr, innerCounter, (innerCounter + 1));
+				}
+			}
+		}
+	}
+
+	private final <T> void swap(T[] arr, int i, int j) {
+		var tempVar1 = 0;
+		if (true) {
+			T t = arr[i];
+			arr[i] = arr[j];
+			arr[j] = t;
+			var tempVar2 = 0;
+			tempVar2++;
+			tempVar2 = tempVar2 + 1;
+		}
+	}
+}

+ 59 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo3_5.java

@@ -0,0 +1,59 @@
+package SortAlgos;
+
+public class SortAlgo3_5 {
+	private int _thisIsNotUsed = 0;
+
+	public void BubbleSortRecursion(Integer arr[], int n) {
+		if (_thisIsNotUsed == 0)
+			switch (n) {
+			case 1:
+				return;
+			}
+
+		var tempVar = 0;
+		tempVar++;
+		tempVar = tempVar - 1;
+
+		int i = 0;
+		while(i < n-1)
+		{
+			var tempBool = arr[i] > arr[i + 1];
+			if (tempBool) {
+				swap(arr, i, i + 1);
+			}
+			i++;
+		}
+		
+		BubbleSortRecursion(arr, n - 1);
+	}
+
+	public void BubbleSortWithoutRecursion(Integer arr[]) {
+		int i = arr.length;
+		var tempVar = 0;
+		
+		while(i > 1)
+		{
+			int innerCounter = 0;
+			while(innerCounter < arr.length -1)
+			{
+				if (arr[innerCounter] > arr[innerCounter + 1]) {
+					swap(arr, innerCounter, (innerCounter + 1));
+				}
+				innerCounter++;
+			}
+			i--;
+		}
+	}
+
+	private final <T> void swap(T[] arr, int i, int j) {
+		var tempVar1 = 0;
+		if (true) {
+			T t = arr[i];
+			var tempVar2 = 0;
+			arr[i] = arr[j];
+			arr[j] = t;
+			tempVar2++;
+			tempVar2 = tempVar2 + 1;
+		}
+	}
+}

+ 54 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo3_6.java

@@ -0,0 +1,54 @@
+package SortAlgos;
+
+public class SortAlgo3_6 {
+	private int _thisIsNotUsed = 0;
+
+	public void BubbleSortRecursion(Integer arr[], int n) {
+		if (_thisIsNotUsed == 0) {
+			if (n != 1) {
+
+				var tempVar = 0;
+				tempVar++;
+				tempVar = tempVar - 1;
+
+				for (int i = 0; !(i >= (n - 1));)
+				{
+					if (!(arr[i] <= arr[i + 1])) 
+					{ 
+						swap(arr, i , i+1);
+					}
+					i = i + 1;
+				}
+				
+				BubbleSortRecursion(arr, n - 1);
+			} else {
+				return;
+			}
+
+		}
+	}
+
+	public void BubbleSortWithoutRecursion(Integer arr[]) {
+		var tempVar = 0;
+		for(int i = arr.length; !(i < 1) ; i--) {
+			for(int innerCounter = 0; innerCounter < arr.length-1; innerCounter++)
+			{
+				if (!(arr[innerCounter] <= arr[innerCounter + 1])) {
+					swap(arr, innerCounter, (innerCounter + 1));
+				}
+			}
+		}
+	}
+
+	private final <T> void swap(T[] arr, int i, int j) {
+		var tempVar1 = 0;
+		if (true) {
+			T t = arr[i];
+			arr[i] = arr[j];
+			arr[j] = t;
+			var tempVar2 = 0;
+			tempVar2++;
+			tempVar2 = tempVar2 + 1;
+		}
+	}
+}

+ 33 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo4.java

@@ -0,0 +1,33 @@
+package SortAlgos;
+
+public class SortAlgo4 {
+	public void BubbleSortWithoutRecursion(Integer arr[]) {
+		for (int i = arr.length; i > 1; i--) {
+			for (int innerCounter = 0; innerCounter < arr.length - 1; innerCounter++) {
+				if (arr[innerCounter] > arr[innerCounter + 1]) {
+					swap(arr, innerCounter, (innerCounter + 1));
+				}
+			}
+		}
+	}
+
+	public void BubbleSortRecursion(Integer arr[], int n) {
+		for (int i = 0; i < n - 1; i++) {
+			if (arr[i] > arr[i + 1]) {
+				swap(arr, i, i + 1);
+			}
+		}
+
+		if (n == 2) {
+			return;
+		} else {
+			BubbleSortRecursion(arr, n - 1);
+		}
+	}
+
+	private final <T> void swap(T[] arr, int i, int j) {
+		T t = arr[i];
+		arr[i] = arr[j];
+		arr[j] = t;
+	}
+}

+ 40 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo4d1.java

@@ -0,0 +1,40 @@
+public class SortAlgo2 {
+	private int firstCounter;
+	private int arrayLenght;
+	private int swapVarI;
+	private int swapVarJ;
+
+	public void BubbleSortRecursion(Integer arr[], int n) {
+		firstCounter = n;
+		if (firstCounter == 1) {
+			return;
+		}
+
+		for (int i = 0; i < firstCounter - 1; i++) {
+			if (arr[i] > arr[i + 1]) {
+				swap(arr, i, i + 1);
+			}
+		}
+		BubbleSortRecursion(arr, firstCounter - 1);
+	}
+
+	public void BubbleSortWithoutRecursion(Integer arr[]) {
+		arrayLenght = arr.length;
+		for (int i = arrayLenght; i > 1; i--) {
+			for (int innerCounter = 0; innerCounter < arrayLenght - 1; innerCounter++) {
+				if (arr[innerCounter] > arr[innerCounter + 1]) {
+					swap(arr, innerCounter, (innerCounter + 1));
+				}
+			}
+		}
+	}
+
+	private final <T> void swap(T[] arr, int i, int j) {
+		swapVarI = i;
+		swapVarJ = j;
+
+		T t = arr[swapVarI];
+		arr[swapVarI] = arr[swapVarJ];
+		arr[swapVarJ] = t;
+	}
+}

+ 39 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo4d2.java

@@ -0,0 +1,39 @@
+package SortAlgos;
+
+public class SortAlgo4d2 {
+	private int firstCounter,swapVarJ,arrayLenght ,swapVarI;
+
+	public void BubbleSortRecursion(Integer arr[], int n) {
+		firstCounter = n;
+		if (firstCounter == 1) {
+			return;
+		}
+
+		for (int i = 0; i < firstCounter - 1; i++) {
+			if (arr[i] > arr[i + 1]) {
+				swap(arr, i, i + 1);
+			}
+		}
+		BubbleSortRecursion(arr, firstCounter - 1);
+	}
+
+	public void BubbleSortWithoutRecursion(Integer arr[]) {
+		arrayLenght = arr.length;
+		for (int i = arrayLenght; i > 1; i--) {
+			for (int innerCounter = 0; innerCounter < arrayLenght - 1; innerCounter++) {
+				if (arr[innerCounter] > arr[innerCounter + 1]) {
+					swap(arr, innerCounter, (innerCounter + 1));
+				}
+			}
+		}
+	}
+
+	private final <T> void swap(T[] arr, int i, int j) {
+		swapVarI = i;
+		swapVarJ = j;
+
+		T t = arr[swapVarI];
+		arr[swapVarI] = arr[swapVarJ];
+		arr[swapVarJ] = t;
+	}
+}

+ 39 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo4d3.java

@@ -0,0 +1,39 @@
+package SortAlgos;
+
+public class SortAlgo4d3 {
+	private int firstCounterAndArrayLenghtAndswapVarJ ,swapVarI;
+
+	public void BubbleSortRecursion(Integer arr[], int n) {
+		firstCounterAndArrayLenghtAndswapVarJ = n;
+		if (firstCounterAndArrayLenghtAndswapVarJ == 1) {
+			return;
+		}
+
+		for (int i = 0; i < firstCounterAndArrayLenghtAndswapVarJ - 1; i++) {
+			if (arr[i] > arr[i + 1]) {
+				swap(arr, i, i + 1);
+			}
+		}
+		BubbleSortRecursion(arr, firstCounterAndArrayLenghtAndswapVarJ - 1);
+	}
+
+	public void BubbleSortWithoutRecursion(Integer arr[]) {
+		firstCounterAndArrayLenghtAndswapVarJ = arr.length;
+		for (int i = firstCounterAndArrayLenghtAndswapVarJ; i > 1; i--) {
+			for (int innerCounter = 0; innerCounter < firstCounterAndArrayLenghtAndswapVarJ - 1; innerCounter++) {
+				if (arr[innerCounter] > arr[innerCounter + 1]) {
+					swap(arr, innerCounter, (innerCounter + 1));
+				}
+			}
+		}
+	}
+
+	private final <T> void swap(T[] arr, int i, int j) {
+		swapVarI = i;
+		firstCounterAndArrayLenghtAndswapVarJ = j;
+
+		T t = arr[swapVarI];
+		arr[swapVarI] = arr[firstCounterAndArrayLenghtAndswapVarJ];
+		arr[firstCounterAndArrayLenghtAndswapVarJ] = t;
+	}
+}

+ 44 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo5.java

@@ -0,0 +1,44 @@
+package SortAlgos;
+
+public class SortAlgo5 {
+	public void BubbleSortRecursion(Integer arr[], int n) {
+		switch (n) {
+		case 1:
+			return;
+		}
+
+		int i = 0;
+		while(i < n-1)
+		{
+			var tempBool = arr[i] > arr[i + 1];
+			if (tempBool) {
+				swap(arr, i, i + 1);
+			}
+			i++;
+		}
+		
+		BubbleSortRecursion(arr, n - 1);
+	}
+
+	public void BubbleSortWithoutRecursion(Integer arr[]) {
+		int i = arr.length;
+		while(i > 1)
+		{
+			int innerCounter = 0;
+			while(innerCounter < arr.length -1)
+			{
+				if (arr[innerCounter] > arr[innerCounter + 1]) {
+					swap(arr, innerCounter, (innerCounter + 1));
+				}
+				innerCounter++;
+			}
+			i--;
+		}
+	}
+
+	private final <T> void swap(T[] arr, int i, int j) {
+		T t = arr[i];
+		arr[i] = arr[j];
+		arr[j] = t;
+	}
+}

+ 45 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo6.java

@@ -0,0 +1,45 @@
+package SortAlgos;
+
+public class SortAlgo6 {
+	public void BubbleSortRecursion(Integer arr[], int n) {
+		if (n != 1) 
+		{
+			for (int i = 0; !(i >= (n - 1));)
+			{
+				if (!(arr[i] <= arr[i + 1])) 
+				{ 
+					swap(arr, i , i+1);
+				}
+				i = i + 1;
+			}
+			BubbleSortRecursion(arr, n - 1);
+		}
+		else
+		{
+			return;
+		}
+	}
+
+	public void BubbleSortWithoutRecursion(Integer arr[]) {
+		for(int i = arr.length; !(i < 1) ; i--) {
+			for(int innerCounter = 0; innerCounter < arr.length-1; innerCounter++)
+			{
+				if (!(arr[innerCounter] <= arr[innerCounter + 1])) {
+					swap(arr, innerCounter, (innerCounter + 1));
+				}
+			}
+		}
+	}
+
+	private final <T> void swap(T[] arr, int i, int j) {
+		var tempVar1 = 0;
+		if (true) {
+			T t = arr[i];
+			arr[i] = arr[j];
+			arr[j] = t;
+			var tempVar2 = 0;
+			tempVar2++;
+			tempVar2 = tempVar2 + 1;
+		}
+	}
+}

+ 33 - 0
jplag.endToEndTesting/src/test/resources/java/sortAlgo/SortAlgo7.java

@@ -0,0 +1,33 @@
+package SortAlgos;
+/*
+ * No able changes for (Splitting and merging statements)
+ * */
+public class SortAlgo7 {
+	public void BubbleSortRecursion(Integer arr[], int n) {
+		if (n == 1)
+			return;
+
+		for (int i = 0; i < n - 1; i++) {
+			if (arr[i] > arr[i + 1]) {
+				swap(arr, i, i + 1);
+			}
+		}
+		BubbleSortRecursion(arr, n - 1);
+	}
+
+	public void BubbleSortWithoutRecursion(Integer arr[]) {
+		for (int i = arr.length; i > 1; i--) {
+			for (int innerCounter = 0; innerCounter < arr.length - 1; innerCounter++) {
+				if (arr[innerCounter] > arr[innerCounter + 1]) {
+					swap(arr, innerCounter, (innerCounter + 1));
+				}
+			}
+		}
+	}
+
+	private final <T> void swap(T[] arr, int i, int j) {
+		T t = arr[i];
+		arr[i] = arr[j];
+		arr[j] = t;
+	}
+}

+ 36 - 0
jplag.endToEndTesting/src/test/resources/results/JavaResult.json

@@ -0,0 +1,36 @@
+[
+  {
+    "function_name": "normalizationLevelTest",
+    "test_results": [
+      {
+        "result_similarity": 100,
+        "test_identifier": 0
+      },
+      {
+        "result_similarity": 100,
+        "test_identifier": 1
+      },
+      {
+        "result_similarity": 100,
+        "test_identifier": 2
+      }
+    ]
+  },
+  {
+    "function_name": "tokenGenerationLevelTest",
+    "test_results": [
+      {
+        "result_similarity": 64.0,
+        "test_identifier": 0
+      },
+      {
+        "result_similarity": 73.04348,
+        "test_identifier": 1
+      },
+      {
+        "result_similarity": 73.94958,
+        "test_identifier": 2
+      }
+    ]
+  }
+]

+ 42 - 0
jplag.frontend-testutils/src/test/java/de/jplag/testutils/FileUtil.java

@@ -0,0 +1,42 @@
+package de.jplag.testutils;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.util.Arrays;
+
+/**
+ * Test utility regarding files and directories.
+ */
+public final class FileUtil {
+
+    private static final String CONTENT_NOT_FOUND = "Directory contents not as expected";
+    private static final String DIRECTORY_NOT_FOUND = "Could not find directory: ";
+
+    private FileUtil() {
+        // private constructor to prevent instantiation
+    }
+
+    /**
+     * Checks if a directory exists and contains the expected files.
+     * @param directory is the directory to check.
+     * @param expectedFilesSorted is the sorted list (a to z) of expected file names.
+     */
+    public static void assertDirectory(File directory, String[] expectedFilesSorted) {
+        assertTrue(directory.exists(), DIRECTORY_NOT_FOUND + directory);
+        String[] filesInDirectory = directory.list();
+        Arrays.sort(filesInDirectory);
+        assertArrayEquals(expectedFilesSorted, filesInDirectory, CONTENT_NOT_FOUND);
+    }
+
+    /**
+     * Clears all files from a directory that whose names end in a certain suffix.
+     * @param directory is the target directory.
+     * @param suffix is the file suffix of the files to clear.
+     */
+    public static void clearFiles(File directory, String suffix) {
+        Arrays.stream(directory.listFiles()).filter(file -> file.getName().endsWith(suffix)).forEach(File::delete);
+    }
+
+}

+ 0 - 28
jplag.frontend-testutils/src/test/java/de/jplag/testutils/TestErrorConsumer.java

@@ -1,28 +0,0 @@
-package de.jplag.testutils;
-
-import static org.junit.jupiter.api.Assertions.fail;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import de.jplag.ErrorConsumer;
-
-/**
- * Mock error consumer that fails the test case on error occurrence.
- * @author Timur Saglam
- */
-public class TestErrorConsumer implements ErrorConsumer {
-    private final Logger logger = LoggerFactory.getLogger("JPlag-Test");
-
-    @Override
-    public void addError(String errorMessage) {
-        logger.error(errorMessage);
-        fail(errorMessage);
-    }
-
-    @Override
-    public void print(String message, String longMessage) {
-        logger.info(message);
-    }
-
-}

+ 37 - 0
jplag.frontend-testutils/src/test/java/de/jplag/testutils/TokenUtils.java

@@ -0,0 +1,37 @@
+package de.jplag.testutils;
+
+import java.util.List;
+import java.util.stream.StreamSupport;
+
+import de.jplag.Token;
+import de.jplag.TokenList;
+
+public final class TokenUtils {
+
+    private TokenUtils() {
+        // private constructor to prevent instantiation
+    }
+
+    /**
+     * Returns the type of all tokens in a {@link TokenList} that belong to a file.
+     * @param tokens is the {@link TokenList}.
+     * @param name is the name of the target file.
+     * @return the immutable list of token types.
+     */
+    public static List<Integer> tokenTypesByFile(TokenList tokenList, String name) {
+        var tokens = tokensByFile(tokenList, name);
+        return tokens.stream().map(Token::getType).toList();
+    }
+
+    /**
+     * Returns the tokens in a {@link TokenList} that belong to a file.
+     * @param tokens is the {@link TokenList}.
+     * @param name is the name of the target file.
+     * @return the immutable list of tokens.
+     */
+    public static List<Token> tokensByFile(TokenList tokenList, String name) {
+        var tokens = StreamSupport.stream(tokenList.allTokens().spliterator(), false);
+        return tokens.filter(it -> it.getFile().startsWith(name)).toList();
+    }
+
+}

+ 6 - 16
jplag.frontend-utils/src/main/java/de/jplag/AbstractParser.java

@@ -1,15 +1,18 @@
 package de.jplag;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 /**
  * Abstract parser class. Counts errors and manages an error consumer.
  * @author Emeric Kwemou
  */
 public abstract class AbstractParser {
-    protected ErrorConsumer errorConsumer;
     protected int errors = 0;
+    public final Logger logger;
 
-    public AbstractParser(ErrorConsumer errorConsumer) {
-        this.errorConsumer = errorConsumer;
+    protected AbstractParser() {
+        this.logger = LoggerFactory.getLogger(this.getClass());
     }
 
     /**
@@ -18,17 +21,4 @@ public abstract class AbstractParser {
     public boolean hasErrors() {
         return errors != 0;
     }
-
-    /**
-     * @return the error consumer that collects and prints errors.
-     */
-    public ErrorConsumer getErrorConsumer() {
-        return errorConsumer;
-    }
-
-    /**
-     * Setter for the error consumer that collects and prints errors.
-     * @param errorConsumer is the consumer to set.
-     */
-
 }

+ 0 - 20
jplag.frontend-utils/src/main/java/de/jplag/ErrorConsumer.java

@@ -1,20 +0,0 @@
-package de.jplag;
-
-/**
- * Consumes errors from a language parser.
- */
-public interface ErrorConsumer {
-
-    /**
-     * Adds an error to the error stack. The error is only printed directly when using the corresponding verbosity option.
-     * @param errorMessage is the error message.
-     */
-    void addError(String errorMessage);
-
-    /**
-     * Prints a message depending on the verbosity.
-     * @param message is the message for the normal mode.
-     * @param longMessage is the message for verbose mode.
-     */
-    void print(String message, String longMessage);
-}

+ 20 - 9
jplag.frontend-utils/src/main/java/de/jplag/Language.java

@@ -9,7 +9,7 @@ import java.util.List;
 public interface Language {
 
     /**
-     * Suffixes for the files containing code of the language.
+     * Suffixes for the files containing code of the language. An empty array means all suffixes are valid.
      */
     String[] suffixes();
 
@@ -42,22 +42,33 @@ public interface Language {
     boolean hasErrors();
 
     /**
-     * Determines whether the parser provide column information.
+     * Determines whether the parser provide column information. If that is the case, line and column indices are used
+     * instead of a single token index.
      */
-    boolean supportsColumns();
+    default boolean supportsColumns() {
+        return true;
+    }
 
     /**
-     * Determines whether JPlag should use a fixed-width font in its reports.
+     * Determines whether a fixed-width font should be used to display that language.
      */
-    boolean isPreformatted();
+    default boolean isPreformatted() {
+        return true;
+    }
 
     /**
-     * Determines whether tokens from the scanner are indexed.
+     * Indicates whether the input files (code) should be used as representation in the report, or different files that form
+     * a view on the input files.
      */
-    boolean usesIndex();
+    default boolean useViewFiles() {
+        return false;
+    }
 
     /**
-     * Number of defined tokens in the scanner of the language.
+     * If the language uses representation files, this method returns the suffix used for the representation files.
      */
-    int numberOfTokens();
+    default String viewFileSuffix() {
+        return "";
+    }
+
 }

+ 35 - 5
jplag.frontend-utils/src/main/java/de/jplag/TokenPrinter.java

@@ -1,15 +1,16 @@
 package de.jplag;
 
-import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toMap;
 
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -41,6 +42,19 @@ public final class TokenPrinter {
         // Utility class, no public constructor.
     }
 
+    /**
+     * Creates a string representation of a set of files line by line and adds the tokens under the lines.
+     * @param tokens is the set of tokens parsed from the files.
+     * @param directory is the common directory of the files.
+     * @param fileNames is a collection of the file names.
+     * @param suffix is the optional view file suffix.
+     * @return the string representation.
+     */
+    public static String printTokens(List<Token> tokens, File directory, Collection<String> fileNames, Optional<String> suffix) {
+        Collection<File> files = fileNames.stream().map(name -> new File(directory, name)).toList();
+        return printTokens(tokens, files, directory, suffix);
+    }
+
     /**
      * Creates a string representation of a set of files line by line and adds the tokens under the lines.
      * @param tokens is the set of tokens parsed from the files.
@@ -49,8 +63,8 @@ public final class TokenPrinter {
      * @return the string representation.
      */
     public static String printTokens(List<Token> tokens, File directory, Collection<String> fileNames) {
-        Collection<File> files = fileNames.stream().map(name -> new File(directory, name)).collect(toList());
-        return printTokens(tokens, files, directory);
+        Collection<File> files = fileNames.stream().map(name -> new File(directory, name)).toList();
+        return printTokens(tokens, files, directory, Optional.empty());
     }
 
     /**
@@ -60,6 +74,17 @@ public final class TokenPrinter {
      * @return the string representation.
      */
     public static String printTokens(List<Token> tokens, Collection<File> files, File root) {
+        return printTokens(tokens, files, root, Optional.empty());
+    }
+
+    /**
+     * Creates a string representation of a collection of files line by line and adds the tokens under the lines.
+     * @param tokens is the set of tokens parsed from the files.
+     * @param files are the parsed files.
+     * @param suffix is the optional view file suffix.
+     * @return the string representation.
+     */
+    public static String printTokens(List<Token> tokens, Collection<File> files, File root, Optional<String> suffix) {
         Map<String, List<String>> filesToLines = readFiles(files);
         StringBuilder builder = new StringBuilder();
         int lineIndex = 0;
@@ -84,6 +109,11 @@ public final class TokenPrinter {
                 columnIndex = 1;
 
                 String fileName = token.getFile().isEmpty() ? root.getName() : token.getFile();
+                fileName = fileName + suffix.orElse("");
+                if (!filesToLines.containsKey(fileName)) {
+                    throw new IllegalStateException("Cannot find file " + fileName + " in " + filesToLines.keySet());
+                }
+
                 currentLine = filesToLines.get(fileName).get(lineIndex - 1);
 
                 appendCodeLinePrefix(builder, lineIndex);
@@ -91,8 +121,6 @@ public final class TokenPrinter {
                 appendTokenLinePrefix(builder, lineIndex);
             }
 
-            assert currentLine != null;
-
             // New line if already past token index:
             if (columnIndex > token.getColumn()) {
                 appendTokenLinePrefix(builder, lineIndex);
@@ -190,6 +218,8 @@ public final class TokenPrinter {
     private static List<String> linesFromFile(File file) {
         try {
             return Files.readAllLines(file.toPath());
+        } catch (NoSuchFileException exception) {
+            logger.error("File does not exist, thus no tokens are printed: " + file.getAbsolutePath());
         } catch (IOException exception) {
             logger.error("Cannot read " + file.getAbsolutePath() + ":", exception);
         }

+ 0 - 16
jplag.frontend.chars/pom.xml

@@ -1,16 +0,0 @@
-<?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>chars</artifactId>
-    <dependencies>
-        <dependency>
-            <groupId>de.jplag</groupId>
-            <artifactId>frontend-utils</artifactId>
-        </dependency>
-    </dependencies>
-</project>

+ 0 - 42
jplag.frontend.chars/src/main/java/de/jplag/chars/CharToken.java

@@ -1,42 +0,0 @@
-package de.jplag.chars;
-
-import de.jplag.Token;
-
-public class CharToken extends Token {
-
-    private final int index;
-    private final Character character;
-
-    public CharToken(int type, String file, int index, Character character, Parser parser) {
-        super(type, file, -1);
-        this.index = index;
-        this.character = character;
-    }
-
-    public CharToken(int type, String file, Parser parser) {
-        this(type, file, -1, null, parser);
-    }
-
-    @Override
-    public int getLine() {
-        return index; // so that the result-index is sorted correctly
-    }
-
-    @Override
-    public int getIndex() {
-        return index;
-    }
-
-    @Override
-    public void setColumn(int column) {
-        // Do nothing!
-    }
-
-    @Override
-    protected String type2string() {
-        if (character == null) {
-            return "FILE_END";
-        }
-        return character.toString();
-    }
-}

+ 0 - 69
jplag.frontend.chars/src/main/java/de/jplag/chars/Language.java

@@ -1,69 +0,0 @@
-package de.jplag.chars;
-
-import java.io.File;
-import java.util.List;
-
-import de.jplag.ErrorConsumer;
-import de.jplag.Token;
-
-/*
- * read in text files as characters
- */
-public class Language implements de.jplag.Language {
-
-    private final Parser parser;
-
-    public Language(ErrorConsumer program) {
-        parser = new Parser(program);
-    }
-
-    @Override
-    public String[] suffixes() {
-        return new String[] {".TXT", ".txt", ".ASC", ".asc", ".TEX", ".tex"};
-    }
-
-    @Override
-    public String getName() {
-        return "Character Parser";
-    }
-
-    @Override
-    public String getShortName() {
-        return "char";
-    }
-
-    @Override
-    public int minimumTokenMatch() {
-        return 10;
-    }
-
-    @Override
-    public List<Token> parse(File dir, String[] files) {
-        return this.parser.parse(dir, files);
-    }
-
-    @Override
-    public boolean hasErrors() {
-        return this.parser.hasErrors();
-    }
-
-    @Override
-    public boolean supportsColumns() {
-        return false;
-    }
-
-    @Override
-    public boolean isPreformatted() {
-        return false;
-    }
-
-    @Override
-    public boolean usesIndex() {
-        return true;
-    }
-
-    @Override
-    public int numberOfTokens() {
-        return 36;
-    }
-}

+ 0 - 201
jplag.frontend.chars/src/main/java/de/jplag/chars/Parser.java

@@ -1,201 +0,0 @@
-package de.jplag.chars;
-
-import java.io.File;
-import java.io.FileReader;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.List;
-
-import de.jplag.AbstractParser;
-import de.jplag.ErrorConsumer;
-import de.jplag.Token;
-import de.jplag.TokenConstants;
-
-public class Parser extends AbstractParser {
-
-    private List<Token> tokens;
-
-    /**
-     * Creates the parser.
-     * @param errorConsumer is the consumer for any occurring errors.
-     */
-    public Parser(ErrorConsumer errorConsumer) {
-        super(errorConsumer);
-    }
-
-    public List<Token> parse(File directory, String[] files) {
-        tokens = new ArrayList<>();
-        errors = 0;
-        for (String file : files) {
-            getErrorConsumer().print(null, "Parsing file " + file);
-            if (!parseFile(directory, file))
-                errors++;
-            tokens.add(new CharToken(TokenConstants.FILE_END, file, this));
-        }
-        if (errors == 0)
-            errorConsumer.print(null, "OK");
-        else
-            errorConsumer.print(null, errors + " ERROR" + (errors > 1 ? "S" : ""));
-
-        return tokens;
-    }
-
-    private boolean parseFile(File dir, String file) {
-        char[] buffer = new char[4096];
-        int type;
-        int length;
-        int offset = 0;
-
-        try {
-            FileReader reader = new FileReader(new File(dir, file), StandardCharsets.UTF_8);
-
-            do {
-                length = reader.read(buffer);
-
-                for (int i = 0; i < length; i++) {
-                    if (buffer[i] <= 127 && (type = mapping[buffer[i]]) > 1) {
-                        tokens.add(new CharToken(type, file, offset + i, buffer[i], this));
-                    }
-                }
-                offset += length;
-            } while (length != -1);
-
-            // close file
-            reader.close();
-        } catch (Exception e) {
-            e.printStackTrace();
-            return false;
-        }
-        return true;
-    }
-
-    private final int[] mapping = {-1, // 0 (nul)
-            -1, // 1 (soh)
-            -1, // 2 (stx)
-            -1, // 3 (etx)
-            -1, // 4 (eot)
-            -1, // 5 (enq)
-            -1, // 6 (ack)
-            -1, // 7 (bel)
-            -1, // 8 (bs)
-            -1, // 9 (ht)
-            -1, // 10 (nl)
-            -1, // 11 (vt)
-            -1, // 12 (np)
-            -1, // 13 (cr)
-            -1, // 14 (so)
-            -1, // 15 (si)
-            -1, // 16 (dle)
-            -1, // 17 (dc1)
-            -1, // 18 (dc2)
-            -1, // 19 (dc3)
-            -1, // 20 (dc4)
-            -1, // 21 (nak)
-            -1, // 22 (syn)
-            -1, // 23 (etb)
-            -1, // 24 (can)
-            -1, // 25 (em)
-            -1, // 26 (sub)
-            -1, // 27 (esc)
-            -1, // 28 (fs)
-            -1, // 29 (gs)
-            -1, // 30 (rs)
-            -1, // 31 (us)
-            -1, // 32 (sp)
-            -1, // 33 !
-            -1, // 34 "
-            -1, // 35 #
-            -1, // 36 $
-            -1, // 37 %
-            -1, // 38 &
-            -1, // 39 '
-            -1, // 40 (
-            -1, // 41 )
-            -1, // 42 *
-            -1, // 43 +
-            -1, // 44 ,
-            -1, // 45 -
-            -1, // 46 .
-            -1, // 47 /
-            27, // 48 0
-            28, // 49 1
-            29, // 50 2
-            30, // 51 3
-            31, // 52 4
-            32, // 53 5
-            33, // 54 6
-            34, // 55 7
-            35, // 56 8
-            36, // 57 9
-            -1, // 58 :
-            -1, // 59 ;
-            -1, // 60 <
-            -1, // 61 =
-            -1, // 62 >
-            -1, // 63 ?
-            -1, // 64 @
-            2, // 65 A
-            3, // 66 B
-            4, // 67 C
-            5, // 68 D
-            6, // 69 E
-            7, // 70 F
-            8, // 71 G
-            9, // 72 H
-            10, // 73 I
-            11, // 74 J
-            12, // 75 K
-            13, // 76 L
-            14, // 77 M
-            15, // 78 N
-            16, // 79 O
-            17, // 80 P
-            18, // 81 Q
-            19, // 82 R
-            20, // 83 S
-            21, // 84 T
-            22, // 85 U
-            23, // 86 V
-            24, // 87 W
-            25, // 88 X
-            26, // 89 Y
-            27, // 90 Z
-            -1, // 91 [
-            -1, // 92 \
-            -1, // 93 ]
-            -1, // 94 ^
-            -1, // 95 _
-            -1, // 96 `
-            2, // 97 a
-            3, // 98 b
-            4, // 99 c
-            5, // 100 d
-            6, // 101 e
-            7, // 102 f
-            8, // 103 g
-            9, // 104 h
-            10, // 105 i
-            11, // 106 j
-            12, // 107 k
-            13, // 108 l
-            14, // 109 m
-            15, // 110 n
-            16, // 111 o
-            17, // 112 p
-            18, // 113 q
-            19, // 114 r
-            20, // 115 s
-            21, // 116 t
-            22, // 117 u
-            23, // 118 v
-            24, // 119 w
-            25, // 120 x
-            26, // 121 y
-            27, // 122 z
-            -1, // 123 {
-            -1, // 124 |
-            -1, // 125 }
-            -1, // 126 ~
-            -1, // 127 (del)
-    };
-}

+ 2 - 23
jplag.frontend.cpp/src/main/java/de/jplag/cpp/Language.java

@@ -3,14 +3,13 @@ package de.jplag.cpp;
 import java.io.File;
 import java.util.List;
 
-import de.jplag.ErrorConsumer;
 import de.jplag.Token;
 
 public class Language implements de.jplag.Language {
     private final Scanner scanner; // cpp code is scanned not parsed
 
-    public Language(ErrorConsumer errorConsumer) {
-        scanner = new Scanner(errorConsumer);
+    public Language() {
+        scanner = new Scanner();
     }
 
     @Override
@@ -33,21 +32,6 @@ public class Language implements de.jplag.Language {
         return 12;
     }
 
-    @Override
-    public boolean supportsColumns() {
-        return true;
-    }
-
-    @Override
-    public boolean isPreformatted() {
-        return true;
-    }
-
-    @Override
-    public boolean usesIndex() {
-        return false;
-    }
-
     @Override
     public List<Token> parse(File dir, String[] files) {
         return this.scanner.scan(dir, files);
@@ -57,9 +41,4 @@ public class Language implements de.jplag.Language {
     public boolean hasErrors() {
         return this.scanner.hasErrors();
     }
-
-    @Override
-    public int numberOfTokens() {
-        return CPPTokenConstants.NUM_DIFF_TOKENS;
-    }
 }

+ 4 - 7
jplag.frontend.cpp/src/main/java/de/jplag/cpp/Scanner.java

@@ -5,8 +5,6 @@ import java.util.ArrayList;
 import java.util.List;
 
 import de.jplag.AbstractParser;
-import de.jplag.ErrorConsumer;
-import de.jplag.TokenConstants;
 
 public class Scanner extends AbstractParser {
     private String currentFile;
@@ -15,10 +13,9 @@ public class Scanner extends AbstractParser {
 
     /**
      * Creates the parser.
-     * @param errorConsumer is the consumer for any occurring errors.
      */
-    public Scanner(ErrorConsumer errorConsumer) {
-        super(errorConsumer);
+    public Scanner() {
+        super();
     }
 
     public List<de.jplag.Token> scan(File directory, String[] files) {
@@ -26,11 +23,11 @@ public class Scanner extends AbstractParser {
         errors = 0;
         for (String currentFile : files) {
             this.currentFile = currentFile;
-            getErrorConsumer().print(null, "Scanning file " + currentFile);
+            logger.trace("Scanning file {}", currentFile);
             if (!CPPScanner.scanFile(directory, currentFile, this)) {
                 errors++;
             }
-            tokens.add(new CPPToken(TokenConstants.FILE_END, currentFile));
+            tokens.add(new CPPToken(CPPTokenConstants.FILE_END, currentFile));
         }
         return tokens;
     }

+ 3 - 5
jplag.frontend.cpp/src/main/javacc/CPP.jj

@@ -19,10 +19,8 @@ import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.InputStream;
 
-import de.jplag.cpp.NewlineStream;
-
 public class CPPScanner implements CPPTokenConstants {
-    private static Scanner delegatingScanner;
+    private Scanner delegatingScanner;
 
     public static boolean scanFile(File dir, String fileName, Scanner delegatingScanner) {
         CPPScanner scanner;
@@ -37,10 +35,10 @@ public class CPPScanner implements CPPTokenConstants {
         try {
             scanner.scan();
         } catch (ParseException e) {
-            delegatingScanner.getErrorConsumer().addError("  Parsing Error in '" + fileName + "':\n  " + e.getMessage());
+            delegatingScanner.logger.error("Parsing Error in '" + fileName + "': " + e.getMessage());
             return false;
         } catch (TokenMgrException e) {
-            delegatingScanner.getErrorConsumer().addError("  Scanning Error in '" + fileName + "':\n  " + e.getMessage());
+            delegatingScanner.logger.error("Scanning Error in '" + fileName + "': " + e.getMessage());
             return false;
         }
         return true;

+ 1 - 1
jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/CSharpListener.java

@@ -215,7 +215,7 @@ public class CSharpListener extends CSharpParserBaseListener implements CSharpTo
 
     @Override
     public void enterUsingStatement(UsingStatementContext context) {
-        transformToken(USING, null);
+        transformToken(USING, context.getStart());
         super.enterUsingStatement(context);
     }
 

+ 5 - 8
jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/CSharpParserAdapter.java

@@ -13,9 +13,7 @@ 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.Token;
-import de.jplag.TokenConstants;
 import de.jplag.csharp.grammar.CSharpLexer;
 import de.jplag.csharp.grammar.CSharpParser;
 
@@ -30,10 +28,9 @@ public class CSharpParserAdapter extends AbstractParser {
 
     /**
      * Creates the parser adapter.
-     * @param errorConsumer is the consumer for any occurring errors.
      */
-    public CSharpParserAdapter(ErrorConsumer errorConsumer) {
-        super(errorConsumer);
+    public CSharpParserAdapter() {
+        super();
     }
 
     /**
@@ -49,14 +46,14 @@ public class CSharpParserAdapter extends AbstractParser {
             if (!parseFile(directory, fileName)) {
                 errors++;
             }
-            tokens.add(new CSharpToken(TokenConstants.FILE_END, fileName, -1, -1, -1));
+            tokens.add(new CSharpToken(CSharpTokenConstants.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);) {
+        try (FileInputStream inputStream = new FileInputStream(file)) {
             currentFile = fileName;
 
             // create a lexer, a parser and a buffer between them.
@@ -74,7 +71,7 @@ public class CSharpParserAdapter extends AbstractParser {
                 treeWalker.walk(new CSharpListener(this), parseTree);
             }
         } catch (IOException exception) {
-            getErrorConsumer().addError("Parsing Error in '" + fileName + "':" + File.separator + exception.toString());
+            logger.error("Parsing Error in '" + fileName + "':" + File.separator + exception, exception);
             return false;
         }
         return true;

+ 2 - 23
jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/Language.java

@@ -4,7 +4,6 @@ import java.io.File;
 import java.util.Arrays;
 import java.util.List;
 
-import de.jplag.ErrorConsumer;
 import de.jplag.Token;
 
 /**
@@ -19,8 +18,8 @@ public class Language implements de.jplag.Language {
 
     private final CSharpParserAdapter parser;
 
-    public Language(ErrorConsumer program) {
-        parser = new CSharpParserAdapter(program);
+    public Language() {
+        parser = new CSharpParserAdapter();
     }
 
     @Override
@@ -52,24 +51,4 @@ public class Language implements de.jplag.Language {
     public boolean hasErrors() {
         return parser.hasErrors();
     }
-
-    @Override
-    public boolean supportsColumns() {
-        return true;
-    }
-
-    @Override
-    public boolean isPreformatted() {
-        return true;
-    }
-
-    @Override
-    public boolean usesIndex() {
-        return false;
-    }
-
-    @Override
-    public int numberOfTokens() {
-        return CSharpTokenConstants.NUM_DIFF_TOKENS;
-    }
 }

+ 11 - 7
jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/grammar/CSharpPreprocessorParserBase.java

@@ -1,14 +1,18 @@
 package de.jplag.csharp.grammar;
 
 import java.util.HashSet;
+import java.util.Objects;
 import java.util.Stack;
 
-import org.antlr.v4.runtime.*;
+import org.antlr.v4.runtime.Parser;
+import org.antlr.v4.runtime.ParserRuleContext;
+import org.antlr.v4.runtime.TokenStream;
 
 /**
  * This class was taken from <a href="https://github.com/antlr/grammars-v4/tree/master/csharp">antlr/grammars-v4</a>. It
  * was originally written by Ken Domino. Note that this class is licensed under Eclipse Public License - v 1.0.
  */
+@SuppressWarnings("java:S100")
 abstract class CSharpPreprocessorParserBase extends Parser {
     protected CSharpPreprocessorParserBase(TokenStream input) {
         super(input);
@@ -16,7 +20,7 @@ abstract class CSharpPreprocessorParserBase extends Parser {
         ConditionalSymbols.add("DEBUG");
     }
 
-    Stack<Boolean> conditions = new Stack<Boolean>();
+    Stack<Boolean> conditions = new Stack<>();
     public HashSet<String> ConditionalSymbols = new HashSet<String>();
 
     protected Boolean AllConditions() {
@@ -51,7 +55,7 @@ abstract class CSharpPreprocessorParserBase extends Parser {
     protected void OnPreprocessorDirectiveElif() {
         ParserRuleContext c = this._ctx;
         CSharpPreprocessorParser.PreprocessorConditionalContext d = (CSharpPreprocessorParser.PreprocessorConditionalContext) c;
-        if (!conditions.peek()) {
+        if (Boolean.FALSE.equals(conditions.peek())) {
             conditions.pop();
             d.value = d.expr.value.equals("true") && AllConditions();
             conditions.push(d.expr.value.equals("true"));
@@ -63,9 +67,9 @@ abstract class CSharpPreprocessorParserBase extends Parser {
     protected void OnPreprocessorDirectiveElse() {
         ParserRuleContext c = this._ctx;
         CSharpPreprocessorParser.PreprocessorConditionalContext d = (CSharpPreprocessorParser.PreprocessorConditionalContext) c;
-        if (!conditions.peek()) {
+        if (Boolean.FALSE.equals(conditions.peek())) {
             conditions.pop();
-            d.value = true && AllConditions();
+            d.value = AllConditions();
             conditions.push(true);
         } else {
             d.value = false;
@@ -154,13 +158,13 @@ abstract class CSharpPreprocessorParserBase extends Parser {
     protected void OnPreprocessorExpressionConditionalEq() {
         ParserRuleContext c = this._ctx;
         CSharpPreprocessorParser.Preprocessor_expressionContext d = (CSharpPreprocessorParser.Preprocessor_expressionContext) c;
-        d.value = (d.expr1.value == d.expr2.value ? "true" : "false");
+        d.value = (Objects.equals(d.expr1.value, d.expr2.value) ? "true" : "false");
     }
 
     protected void OnPreprocessorExpressionConditionalNe() {
         ParserRuleContext c = this._ctx;
         CSharpPreprocessorParser.Preprocessor_expressionContext d = (CSharpPreprocessorParser.Preprocessor_expressionContext) c;
-        d.value = (d.expr1.value != d.expr2.value ? "true" : "false");
+        d.value = (!Objects.equals(d.expr1.value, d.expr2.value) ? "true" : "false");
     }
 
     protected void OnPreprocessorExpressionConditionalAnd() {

+ 1 - 3
jplag.frontend.csharp-6/src/test/java/de/jplag/csharp/MinimalCSharpFrontendTest.java

@@ -19,7 +19,6 @@ import org.slf4j.LoggerFactory;
 import de.jplag.Token;
 import de.jplag.TokenConstants;
 import de.jplag.TokenPrinter;
-import de.jplag.testutils.TestErrorConsumer;
 
 class MinimalCSharpFrontendTest {
     private final Logger logger = LoggerFactory.getLogger("JPlag-Test");
@@ -32,8 +31,7 @@ class MinimalCSharpFrontendTest {
 
     @BeforeEach
     public void setUp() {
-        TestErrorConsumer consumer = new TestErrorConsumer();
-        frontend = new Language(consumer);
+        frontend = new Language();
         baseDirectory = BASE_PATH.toFile();
         assertTrue(baseDirectory.exists(), "Could not find base directory!");
     }

+ 43 - 0
jplag.frontend.emf-metamodel-dynamic/pom.xml

@@ -0,0 +1,43 @@
+<?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>emf-metamodel-dynamic</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>frontend-utils</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>emf-metamodel</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>frontend-testutils</artifactId>
+            <version>${revision}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.emf</groupId>
+            <artifactId>org.eclipse.emf.ecore</artifactId>
+            <version>${emf.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.emf</groupId>
+            <artifactId>org.eclipse.emf.common</artifactId>
+            <version>${emf.ecore.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.emf</groupId>
+            <artifactId>org.eclipse.emf.ecore.xmi</artifactId>
+            <version>${emf.ecore.xmi.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 29 - 0
jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/DynamicMetamodelToken.java

@@ -0,0 +1,29 @@
+package de.jplag.emf.dynamic;
+
+import org.eclipse.emf.ecore.EObject;
+
+import de.jplag.emf.MetamodelToken;
+
+/**
+ * EMF metamodel token which uses dynamically created tokens.
+ * @author Timur Saglam
+ */
+public class DynamicMetamodelToken extends MetamodelToken {
+
+    public DynamicMetamodelToken(int type, String file, EObject eObject) {
+        super(type, file, eObject);
+    }
+
+    public DynamicMetamodelToken(int type, String file) {
+        super(type, file);
+    }
+
+    @Override
+    protected String type2string() {
+        if (type < DynamicMetamodelTokenConstants.TOKEN_TYPE_START) {
+            return super.type2string();
+        }
+        return DynamicMetamodelTokenConstants.getTokenString(type);
+    }
+
+}

+ 60 - 0
jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/DynamicMetamodelTokenConstants.java

@@ -0,0 +1,60 @@
+package de.jplag.emf.dynamic;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.emf.ecore.EClass;
+
+import de.jplag.TokenConstants;
+
+/**
+ * Utility class for the dynamic creation of token constants. Replaces a handcrafted token set.
+ * @author Timur Saglam
+ */
+public class DynamicMetamodelTokenConstants implements TokenConstants {
+    private static final String UNKNOWN_TOKEN = "<Unknown>";
+
+    public static final int TOKEN_TYPE_START = 2;
+
+    private static Map<EClass, Integer> eClassToTokenType = new HashMap<>();
+    private static Map<Integer, EClass> tokenTypeToEClass = new HashMap<>();
+    private static int tokenTypeIndex = TOKEN_TYPE_START;
+
+    private DynamicMetamodelTokenConstants() {
+        // private constructor for non-instantiability.
+    }
+
+    public static int getTokenType(EClass eClass) {
+        if (eClassToTokenType.containsKey(eClass)) {
+            return eClassToTokenType.get(eClass);
+        }
+        Integer tokenType = tokenTypeIndex++;
+        eClassToTokenType.put(eClass, tokenType);
+        tokenTypeToEClass.put(tokenType, eClass);
+        return tokenType;
+    }
+
+    public static String getTokenString(int tokenType) {
+        if (tokenTypeToEClass.containsKey(tokenType)) {
+            return tokenTypeToEClass.get(tokenType).getName();
+        }
+        return UNKNOWN_TOKEN;
+    }
+
+    /**
+     * Returns the current size of the dynamic token set.
+     * @return the number of known tokens.
+     */
+    public static int getNumberOfTokens() {
+        return eClassToTokenType.size();
+    }
+
+    /**
+     * @return a read only view on the collection of token strings.
+     */
+    public static Collection<String> getTokenStrings() {
+        return eClassToTokenType.keySet().stream().map(EClass::getName).toList();
+    }
+
+}

+ 34 - 0
jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/Language.java

@@ -0,0 +1,34 @@
+package de.jplag.emf.dynamic;
+
+import de.jplag.emf.dynamic.parser.DynamicEcoreParser;
+
+/**
+ * Language for EMF metamodels from the Eclipse Modeling Framework (EMF). This language is based on a dynamically
+ * created token set instead of a hand-picked one.
+ * @author Timur Saglam
+ */
+public class Language extends de.jplag.emf.Language {
+    private static final String NAME = "EMF metamodels (dynamically created token set)";
+    private static final String SHORT_NAME = "EMF metamodel (dynamic)";
+
+    private static final int DEFAULT_MIN_TOKEN_MATCH = 10;
+
+    public Language() {
+        super(new DynamicEcoreParser());
+    }
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+
+    @Override
+    public String getShortName() {
+        return SHORT_NAME;
+    }
+
+    @Override
+    public int minimumTokenMatch() {
+        return DEFAULT_MIN_TOKEN_MATCH;
+    }
+}

+ 29 - 0
jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicEcoreParser.java

@@ -0,0 +1,29 @@
+package de.jplag.emf.dynamic.parser;
+
+import org.eclipse.emf.ecore.EObject;
+
+import de.jplag.emf.MetamodelToken;
+import de.jplag.emf.dynamic.DynamicMetamodelToken;
+import de.jplag.emf.parser.EcoreParser;
+import de.jplag.emf.util.AbstractMetamodelVisitor;
+
+/**
+ * Parser for EMF metamodels based on dynamically created tokens.
+ * @author Timur Saglam
+ */
+public class DynamicEcoreParser extends EcoreParser {
+
+    private static final String NO_PREFIX = "";
+
+    @Override
+    protected AbstractMetamodelVisitor createMetamodelVisitor() {
+        return new DynamicMetamodelTokenGenerator(this);
+    }
+
+    @Override
+    public void addToken(int type, EObject source) {
+        MetamodelToken token = new DynamicMetamodelToken(type, currentFile, source);
+        treeView.addToken(token, visitor.getCurrentTreeDepth(), NO_PREFIX);
+        tokens.addToken(token);
+    }
+}

+ 29 - 0
jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicMetamodelTokenGenerator.java

@@ -0,0 +1,29 @@
+package de.jplag.emf.dynamic.parser;
+
+import org.eclipse.emf.ecore.EObject;
+
+import de.jplag.emf.dynamic.DynamicMetamodelTokenConstants;
+import de.jplag.emf.util.AbstractMetamodelVisitor;
+
+/**
+ * Visits a metamodel containment tree and extracts the relevant token based on a dynamically created token set.
+ * @author Timur Saglam
+ */
+public class DynamicMetamodelTokenGenerator extends AbstractMetamodelVisitor {
+    private final DynamicEcoreParser parser;
+
+    /**
+     * Creates the visitor.
+     * @param parser is the parser which receives the generated tokens.
+     */
+    public DynamicMetamodelTokenGenerator(DynamicEcoreParser parser) {
+        super(false);
+        this.parser = parser;
+    }
+
+    @Override
+    protected void visitEObject(EObject eObject) {
+        int tokenType = DynamicMetamodelTokenConstants.getTokenType(eObject.eClass());
+        parser.addToken(tokenType, eObject);
+    }
+}

+ 61 - 0
jplag.frontend.emf-metamodel-dynamic/src/test/java/de/jplag/emf/dynamic/MinimalDynamicMetamodelTest.java

@@ -0,0 +1,61 @@
+package de.jplag.emf.dynamic;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertIterableEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.jplag.TokenList;
+import de.jplag.TokenPrinter;
+import de.jplag.testutils.FileUtil;
+import de.jplag.testutils.TokenUtils;
+
+class MinimalDynamicMetamodelTest {
+    private final Logger logger = LoggerFactory.getLogger("JPlag-Test");
+
+    private static final Path BASE_PATH = Path.of("src", "test", "resources", "de", "jplag", "models");
+    private static final String[] TEST_SUBJECTS = {"bookStore.ecore", "bookStoreExtended.ecore", "bookStoreRenamed.ecore"};
+
+    private de.jplag.Language frontend;
+    private File baseDirectory;
+
+    @BeforeEach
+    public void setUp() {
+        frontend = new Language();
+        baseDirectory = BASE_PATH.toFile();
+        FileUtil.assertDirectory(baseDirectory, TEST_SUBJECTS);
+    }
+
+    @Test
+    void testBookstoreMetamodels() {
+        TokenList result = frontend.parse(baseDirectory, TEST_SUBJECTS);
+        List<String> treeViewFiles = Arrays.stream(TEST_SUBJECTS).map(it -> it + Language.VIEW_FILE_SUFFIX).toList();
+        logger.debug(TokenPrinter.printTokens(result, baseDirectory, treeViewFiles, Optional.of(Language.VIEW_FILE_SUFFIX)));
+        logger.info(("Dynamic token set: " + DynamicMetamodelTokenConstants.getTokenStrings()));
+        logger.info("parsed tokens: " + result.allTokens().toString());
+        assertEquals(7, DynamicMetamodelTokenConstants.getTokenStrings().size());
+        assertEquals(64, result.size());
+
+        var bookstoreTokens = TokenUtils.tokenTypesByFile(result, TEST_SUBJECTS[0]);
+        var bookstoreRenamedTokens = TokenUtils.tokenTypesByFile(result, TEST_SUBJECTS[2]);
+        var bookstoreExtendedTokens = TokenUtils.tokenTypesByFile(result, TEST_SUBJECTS[1]);
+        assertTrue(bookstoreTokens.size() < bookstoreExtendedTokens.size());
+        assertIterableEquals(bookstoreTokens, bookstoreRenamedTokens);
+    }
+
+    @AfterEach
+    public void tearDown() {
+        FileUtil.clearFiles(new File(BASE_PATH.toString()), Language.VIEW_FILE_SUFFIX);
+    }
+}

+ 16 - 0
jplag.frontend.emf-metamodel-dynamic/src/test/resources/de/jplag/models/bookStore.ecore

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="ASCII"?>
+<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="BookStorePackage" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore"
+    nsPrefix="bookStore">
+  <eClassifiers xsi:type="ecore:EClass" name="BookStore">
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="owner" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="location" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EReference" name="books" upperBound="-1"
+        eType="#//Book" containment="true"/>
+  </eClassifiers>
+  <eClassifiers xsi:type="ecore:EClass" name="Book">
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="isbn" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EInt"
+        iD="true"/>
+  </eClassifiers>
+</ecore:EPackage>

+ 38 - 0
jplag.frontend.emf-metamodel-dynamic/src/test/resources/de/jplag/models/bookStoreExtended.ecore

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="ASCII"?>
+<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="BookStorePackage" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore"
+    nsPrefix="bookStore">
+  <eSubpackages name="store" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore/store"
+      nsPrefix="store">
+    <eClassifiers xsi:type="ecore:EClass" name="BookStore">
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="location" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+      <eStructuralFeatures xsi:type="ecore:EReference" name="books" upperBound="-1"
+          eType="#//store/Book" containment="true"/>
+      <eStructuralFeatures xsi:type="ecore:EReference" name="owner" eType="#//person/Person"/>
+    </eClassifiers>
+    <eClassifiers xsi:type="ecore:EClass" name="Book">
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="isbn" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EInt"
+          iD="true"/>
+      <eStructuralFeatures xsi:type="ecore:EReference" name="author" eType="#//person/Author"/>
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="genre" eType="#//store/Genre"/>
+    </eClassifiers>
+    <eClassifiers xsi:type="ecore:EEnum" name="Genre">
+      <eLiterals name="NOVEL"/>
+      <eLiterals name="COOKBOOK" value="1"/>
+      <eLiterals name="BIOGRAPHY" value="3"/>
+      <eLiterals name="TEXTBOOK" value="4"/>
+    </eClassifiers>
+  </eSubpackages>
+  <eSubpackages name="person" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore/person"
+      nsPrefix="person">
+    <eClassifiers xsi:type="ecore:EClass" name="Author" eSuperTypes="#//person/Person">
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="isStageName" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EBoolean"/>
+    </eClassifiers>
+    <eClassifiers xsi:type="ecore:EClass" name="Person">
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="firstName" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="lastName" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    </eClassifiers>
+  </eSubpackages>
+</ecore:EPackage>

+ 16 - 0
jplag.frontend.emf-metamodel-dynamic/src/test/resources/de/jplag/models/bookStoreRenamed.ecore

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="ASCII"?>
+<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="BookStorePackage" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore"
+    nsPrefix="bookStore">
+  <eClassifiers xsi:type="ecore:EClass" name="StoreThatSellsBooks">
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="nameOfOwner" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="city" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EReference" name="soldItems" upperBound="-1"
+        eType="#//BookStoreItem" containment="true"/>
+  </eClassifiers>
+  <eClassifiers xsi:type="ecore:EClass" name="BookStoreItem">
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="title" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="identifier" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EInt"
+        iD="true"/>
+  </eClassifiers>
+</ecore:EPackage>

+ 39 - 0
jplag.frontend.emf-metamodel/pom.xml

@@ -0,0 +1,39 @@
+<?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>emf-metamodel</artifactId>
+
+    <dependencies>
+        <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>
+        <dependency>
+            <groupId>org.eclipse.emf</groupId>
+            <artifactId>org.eclipse.emf.ecore</artifactId>
+            <version>${emf.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.emf</groupId>
+            <artifactId>org.eclipse.emf.common</artifactId>
+            <version>${emf.ecore.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.emf</groupId>
+            <artifactId>org.eclipse.emf.ecore.xmi</artifactId>
+            <version>${emf.ecore.xmi.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 72 - 0
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/Language.java

@@ -0,0 +1,72 @@
+package de.jplag.emf;
+
+import java.io.File;
+import java.util.Arrays;
+
+import org.eclipse.emf.ecore.EcorePackage;
+
+import de.jplag.TokenList;
+import de.jplag.emf.parser.EcoreParser;
+
+/**
+ * Language for EMF metamodels from the Eclipse Modeling Framework (EMF).
+ * @author Timur Saglam
+ */
+public class Language implements de.jplag.Language {
+    public static final String VIEW_FILE_SUFFIX = ".TreeView";
+    public static final String FILE_ENDING = "." + EcorePackage.eNAME;
+
+    private static final String NAME = "EMF metamodel";
+    private static final String SHORT_NAME = "EMF metamodel";
+    private static final int DEFAULT_MIN_TOKEN_MATCH = 6;
+
+    protected final EcoreParser parser;
+
+    public Language() {
+        this(new EcoreParser());
+    }
+
+    protected Language(EcoreParser parser) {
+        this.parser = parser;
+    }
+
+    @Override
+    public String[] suffixes() {
+        return new String[] {FILE_ENDING};
+    }
+
+    @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 dir, String[] files) {
+        return parser.parse(dir, Arrays.asList(files));
+    }
+
+    @Override
+    public boolean hasErrors() {
+        return parser.hasErrors();
+    }
+
+    @Override
+    public boolean useViewFiles() {
+        return true;
+    }
+
+    @Override
+    public String viewFileSuffix() {
+        return VIEW_FILE_SUFFIX;
+    }
+}

+ 70 - 0
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/MetamodelToken.java

@@ -0,0 +1,70 @@
+package de.jplag.emf;
+
+import java.util.Optional;
+
+import org.eclipse.emf.ecore.EObject;
+
+import de.jplag.Token;
+import de.jplag.TokenConstants;
+
+/**
+ * EMF metamodel token.
+ * @author Timur Saglam
+ */
+public class MetamodelToken extends Token implements MetamodelTokenConstants {
+
+    private final Optional<EObject> eObject;
+
+    /**
+     * Creates an Ecore metamodel token that corresponds to an EObject.
+     * @param type is the corresponding ID of the {@link TokenConstants}.
+     * @param file is the name of the source model file.
+     * @param eObject is the corresponding eObject in the model from which this token was extracted.
+     */
+    public MetamodelToken(int type, String file, EObject eObject) {
+        super(type, file, -1);
+        this.eObject = Optional.of(eObject);
+    }
+
+    /**
+     * Creates an Ecore metamodel token.
+     * @param type is the corresponding ID of the {@link TokenConstants}.
+     * @param file is the name of the source model file.
+     */
+    public MetamodelToken(int type, String file) {
+        super(type, file, -1);
+        this.eObject = Optional.empty();
+    }
+
+    /**
+     * @return the optional corresponding EObject of the token.
+     */
+    public Optional<EObject> getEObject() {
+        return eObject;
+    }
+
+    @Override
+    protected String type2string() {
+        return switch (type) {
+            case PACKAGE -> "EPackage";
+            case ANNOTATION -> "EAnnotation";
+            case CLASS -> "EClass";
+            case DATATYPE -> "EDatatype";
+            case ENUM -> "EEnum";
+            case ENUM_LITERAL -> "EEnumLiteral";
+            case OPERATION -> "EOperation";
+            case REFERENCE -> "EReference";
+            case ATTRIBUTE -> "EAttribute";
+            case PARAMETER -> "EParameter";
+            case INTERFACE -> "EClass (Interface)";
+            case SUPER_TYPE -> "ESuperType";
+            case ID_ATTRIBUTE -> "EAttribute (ID)";
+            case CONTAINMENT -> "EReference (Containment)";
+            case ABSTRACT_CLASS -> "EClass (Abstract)";
+            case RETURN_TYPE -> "EClassifier (Return Type)";
+            case THROWS_DECLARATION -> "EClassifier (Exception)";
+            case FILE_END -> "(EOF)";
+            default -> "<UNKNOWN" + type + ">";
+        };
+    }
+}

+ 35 - 0
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/MetamodelTokenConstants.java

@@ -0,0 +1,35 @@
+package de.jplag.emf;
+
+import de.jplag.TokenConstants;
+
+/**
+ * Ecore meta-metamodel token constants. They define which tokens are extracted from a metamodel.
+ * @author Timur Saglam
+ */
+public interface MetamodelTokenConstants extends TokenConstants {
+
+    int PACKAGE = 2;
+    int ANNOTATION = 3;
+    int CLASS = 4;
+    int DATATYPE = 5;
+    int ENUM = 6;
+    int ENUM_LITERAL = 7;
+    int OPERATION = 8;
+    int REFERENCE = 9;
+    int ATTRIBUTE = 10;
+    int PARAMETER = 11;
+    int INTERFACE = 12;
+    int SUPER_TYPE = 13;
+    int ID_ATTRIBUTE = 14;
+    int CONTAINMENT = 15;
+    int ABSTRACT_CLASS = 16;
+    int RETURN_TYPE = 17;
+    int THROWS_DECLARATION = 18;
+    int TYPE_PARAMETER = 19;
+    int BOUND = 20;
+
+    /*
+     * Number of token constants:
+     */
+    int NUM_DIFF_TOKENS = 21;
+}

+ 87 - 0
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/parser/EcoreParser.java

@@ -0,0 +1,87 @@
+package de.jplag.emf.parser;
+
+import java.io.File;
+import java.util.List;
+
+import org.eclipse.emf.ecore.EObject;
+
+import de.jplag.AbstractParser;
+import de.jplag.TokenConstants;
+import de.jplag.TokenList;
+import de.jplag.emf.Language;
+import de.jplag.emf.MetamodelToken;
+import de.jplag.emf.util.AbstractMetamodelVisitor;
+import de.jplag.emf.util.EMFUtil;
+import de.jplag.emf.util.MetamodelTreeView;
+
+/**
+ * Parser for EMF metamodels.
+ * @author Timur Saglam
+ */
+public class EcoreParser extends AbstractParser {
+    protected TokenList tokens;
+    protected String currentFile;
+    protected MetamodelTreeView treeView;
+    protected AbstractMetamodelVisitor visitor;
+
+    /**
+     * Creates the parser.
+     */
+    public EcoreParser() {
+        EMFUtil.registerEcoreExtension();
+    }
+
+    /**
+     * Parses all tokens form a list of files.
+     * @param directory is the base directory.
+     * @param fileNames is the list of file names.
+     * @return the list of parsed tokens.
+     */
+    public TokenList parse(File directory, List<String> fileNames) {
+        errors = 0;
+        tokens = new TokenList();
+        for (String fileName : fileNames) {
+            currentFile = fileName;
+            String filePath = fileName.isEmpty() ? directory.toString() : directory.toString() + File.separator + fileName;
+            parseModelFile(filePath);
+        }
+        return tokens;
+    }
+
+    /**
+     * Loads a metamodel from a file and parses it.
+     * @param filePath is the path to the metamodel file.
+     */
+    protected void parseModelFile(String filePath) {
+        treeView = new MetamodelTreeView(filePath);
+        List<EObject> model = EMFUtil.loadModel(filePath);
+        if (model == null) {
+            errors++;
+        } else {
+            for (EObject root : model) {
+                visitor = createMetamodelVisitor();
+                visitor.visit(root);
+            }
+            tokens.addToken(new MetamodelToken(TokenConstants.FILE_END, currentFile));
+            treeView.writeToFile(Language.VIEW_FILE_SUFFIX);
+        }
+    }
+
+    /**
+     * Extension point for subclasses to employ different token generators.
+     * @return a token generating metamodel visitor.
+     */
+    protected AbstractMetamodelVisitor createMetamodelVisitor() {
+        return new MetamodelTokenGenerator(this);
+    }
+
+    public void addToken(int type, EObject source, String prefix) {
+        MetamodelToken token = new MetamodelToken(type, currentFile, source);
+        treeView.addToken(token, visitor.getCurrentTreeDepth(), prefix);
+        tokens.addToken(token);
+    }
+
+    public void addToken(int type, EObject source) {
+        addToken(type, source, "");
+    }
+}

+ 130 - 0
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/parser/MetamodelTokenGenerator.java

@@ -0,0 +1,130 @@
+package de.jplag.emf.parser;
+
+import static de.jplag.emf.MetamodelTokenConstants.ABSTRACT_CLASS;
+import static de.jplag.emf.MetamodelTokenConstants.ANNOTATION;
+import static de.jplag.emf.MetamodelTokenConstants.ATTRIBUTE;
+import static de.jplag.emf.MetamodelTokenConstants.BOUND;
+import static de.jplag.emf.MetamodelTokenConstants.CLASS;
+import static de.jplag.emf.MetamodelTokenConstants.CONTAINMENT;
+import static de.jplag.emf.MetamodelTokenConstants.DATATYPE;
+import static de.jplag.emf.MetamodelTokenConstants.ENUM;
+import static de.jplag.emf.MetamodelTokenConstants.ENUM_LITERAL;
+import static de.jplag.emf.MetamodelTokenConstants.ID_ATTRIBUTE;
+import static de.jplag.emf.MetamodelTokenConstants.INTERFACE;
+import static de.jplag.emf.MetamodelTokenConstants.OPERATION;
+import static de.jplag.emf.MetamodelTokenConstants.PACKAGE;
+import static de.jplag.emf.MetamodelTokenConstants.PARAMETER;
+import static de.jplag.emf.MetamodelTokenConstants.REFERENCE;
+import static de.jplag.emf.MetamodelTokenConstants.RETURN_TYPE;
+import static de.jplag.emf.MetamodelTokenConstants.SUPER_TYPE;
+import static de.jplag.emf.MetamodelTokenConstants.THROWS_DECLARATION;
+import static de.jplag.emf.MetamodelTokenConstants.TYPE_PARAMETER;
+
+import org.eclipse.emf.ecore.EAnnotation;
+import org.eclipse.emf.ecore.EAttribute;
+import org.eclipse.emf.ecore.EClass;
+import org.eclipse.emf.ecore.EDataType;
+import org.eclipse.emf.ecore.EEnum;
+import org.eclipse.emf.ecore.EEnumLiteral;
+import org.eclipse.emf.ecore.EOperation;
+import org.eclipse.emf.ecore.EPackage;
+import org.eclipse.emf.ecore.EParameter;
+import org.eclipse.emf.ecore.EReference;
+import org.eclipse.emf.ecore.ETypeParameter;
+
+import de.jplag.emf.util.AbstractMetamodelVisitor;
+
+/**
+ * Visits a metamodel containment tree and extracts the relevant token.
+ * @author Timur Saglam
+ */
+public class MetamodelTokenGenerator extends AbstractMetamodelVisitor {
+    private EcoreParser parser;
+
+    /**
+     * Creates the visitor.
+     * @param parser is the parser which receives the generated tokens.
+     */
+    public MetamodelTokenGenerator(EcoreParser parser) {
+        super(true);
+        this.parser = parser;
+    }
+
+    @Override
+    protected void visitEAnnotation(EAnnotation eAnnotation) {
+        parser.addToken(ANNOTATION, eAnnotation);
+    }
+
+    @Override
+    protected void visitEAttribute(EAttribute eAttribute) {
+        if (eAttribute.isID()) {
+            parser.addToken(ID_ATTRIBUTE, eAttribute);
+        } else {
+            parser.addToken(ATTRIBUTE, eAttribute);
+        }
+    }
+
+    @Override
+    protected void visitEClass(EClass eClass) {
+        if (eClass.isInterface()) {
+            parser.addToken(INTERFACE, eClass);
+        } else if (eClass.isAbstract()) {
+            parser.addToken(ABSTRACT_CLASS, eClass);
+        } else {
+            parser.addToken(CLASS, eClass);
+        }
+        eClass.getESuperTypes().forEach(it -> parser.addToken(SUPER_TYPE, it, " extends "));
+    }
+
+    @Override
+    protected void visitEDataType(EDataType eDataType) {
+        if (!(eDataType instanceof EEnum)) {
+            parser.addToken(DATATYPE, eDataType);
+        }
+    }
+
+    @Override
+    protected void visitEEnum(EEnum eEnum) {
+        parser.addToken(ENUM, eEnum);
+    }
+
+    @Override
+    protected void visitEEnumLiteral(EEnumLiteral eEnumLiteral) {
+        parser.addToken(ENUM_LITERAL, eEnumLiteral);
+    }
+
+    @Override
+    protected void visitEOperation(EOperation eOperation) {
+        parser.addToken(OPERATION, eOperation);
+        if (eOperation.getEType() != null) {
+            parser.addToken(RETURN_TYPE, eOperation);
+        }
+        eOperation.getEExceptions().forEach(it -> parser.addToken(THROWS_DECLARATION, it));
+    }
+
+    @Override
+    protected void visitEPackage(EPackage ePackage) {
+        parser.addToken(PACKAGE, ePackage);
+    }
+
+    @Override
+    protected void visitEParameter(EParameter eParameter) {
+        parser.addToken(PARAMETER, eParameter);
+    }
+
+    @Override
+    protected void visitEReference(EReference eReference) {
+        if (eReference.isContainment()) {
+            parser.addToken(CONTAINMENT, eReference);
+        } else {
+            parser.addToken(REFERENCE, eReference);
+        }
+    }
+
+    @Override
+    protected void visitETypeParameter(ETypeParameter eTypeParameter) {
+        parser.addToken(TYPE_PARAMETER, eTypeParameter);
+        eTypeParameter.getEBounds().forEach(it -> parser.addToken(BOUND, it));
+    }
+
+}

+ 169 - 0
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/util/AbstractMetamodelVisitor.java

@@ -0,0 +1,169 @@
+package de.jplag.emf.util;
+
+import java.util.ArrayList;
+
+import org.eclipse.emf.ecore.EAnnotation;
+import org.eclipse.emf.ecore.EAttribute;
+import org.eclipse.emf.ecore.EClass;
+import org.eclipse.emf.ecore.EClassifier;
+import org.eclipse.emf.ecore.EDataType;
+import org.eclipse.emf.ecore.EEnum;
+import org.eclipse.emf.ecore.EEnumLiteral;
+import org.eclipse.emf.ecore.EFactory;
+import org.eclipse.emf.ecore.EGenericType;
+import org.eclipse.emf.ecore.ENamedElement;
+import org.eclipse.emf.ecore.EObject;
+import org.eclipse.emf.ecore.EOperation;
+import org.eclipse.emf.ecore.EPackage;
+import org.eclipse.emf.ecore.EParameter;
+import org.eclipse.emf.ecore.EReference;
+import org.eclipse.emf.ecore.EStructuralFeature;
+import org.eclipse.emf.ecore.ETypeParameter;
+
+/**
+ * Visitor for the containment tree of an EMF Metamodel.
+ * @author Timur Saglam
+ */
+public abstract class AbstractMetamodelVisitor {
+
+    private final boolean sortContainmentsByType;
+
+    protected AbstractMetamodelVisitor(boolean sortContainmentsByType) {
+        this.sortContainmentsByType = sortContainmentsByType;
+    }
+
+    private int currentTreeDepth;
+
+    /**
+     * Returns the current depth in the containment tree from the starting point.
+     * @return the depth in tree node levels.
+     */
+    public int getCurrentTreeDepth() {
+        return currentTreeDepth;
+    }
+
+    /**
+     * Visits an EObject and all nodes in the containment tree below. Note that multiple visitor method may be called for a
+     * single element. For example <code>visitEClass()</code> and <code>visitEObject()</code>.
+     * @param eObject is the EObject to visit.
+     */
+    public final void visit(EObject eObject) {
+        visitEObject(eObject);
+        if (eObject instanceof EPackage ePackage) {
+            visitEPackage(ePackage);
+        }
+        if (eObject instanceof EAnnotation eAnnotation) {
+            visitEAnnotation(eAnnotation);
+        }
+        if (eObject instanceof EClassifier eClassifier) {
+            visitEClassifier(eClassifier);
+        }
+        if (eObject instanceof EClass eClass) {
+            visitEClass(eClass);
+        }
+        if (eObject instanceof EDataType eDataType) {
+            visitEDataType(eDataType);
+        }
+        if (eObject instanceof EStructuralFeature eStructuralFeature) {
+            visitEStructuralFeature(eStructuralFeature);
+        }
+        if (eObject instanceof EAttribute eAttribute) {
+            visitEAttribute(eAttribute);
+        }
+        if (eObject instanceof EReference eReference) {
+            visitEReference(eReference);
+        }
+        if (eObject instanceof EOperation eOperation) {
+            visitEOperation(eOperation);
+        }
+        if (eObject instanceof EEnum eEnum) {
+            visitEEnum(eEnum);
+        }
+        if (eObject instanceof EEnumLiteral eEnumLiteral) {
+            visitEEnumLiteral(eEnumLiteral);
+        }
+        if (eObject instanceof EFactory eFactory) {
+            visitEFactory(eFactory);
+        }
+        if (eObject instanceof EParameter eParameter) {
+            visitEParameter(eParameter);
+        }
+        if (eObject instanceof EGenericType eGenericType) {
+            visitEGenericType(eGenericType);
+        }
+        if (eObject instanceof ETypeParameter eTypeParameter) {
+            visitETypeParameter(eTypeParameter);
+        }
+        if (eObject instanceof ENamedElement eNamedElement) {
+            visitENamedElement(eNamedElement);
+        }
+
+        var children = new ArrayList<>(eObject.eContents());
+        if (sortContainmentsByType) {
+            children.sort((first, second) -> first.eClass().getName().compareTo(second.eClass().getName()));
+        }
+
+        currentTreeDepth++;
+        for (EObject child : children) {
+            visit(child);
+        }
+        currentTreeDepth--;
+    }
+
+    protected void visitENamedElement(ENamedElement eNamedElement) {
+    }
+
+    /**
+     * Visit method that gets called for all annotation nodes.
+     * @param eAnnotation is the node that is visited.
+     */
+    protected void visitEAnnotation(EAnnotation eAnnotation) {
+    }
+
+    protected void visitEAttribute(EAttribute eAttribute) {
+    }
+
+    protected void visitEClass(EClass eClass) {
+    }
+
+    protected void visitEClassifier(EClassifier eClassifier) {
+    }
+
+    protected void visitEDataType(EDataType eDataType) {
+    }
+
+    protected void visitEEnum(EEnum eEnum) {
+    }
+
+    protected void visitEEnumLiteral(EEnumLiteral eEnumLiteral) {
+    }
+
+    protected void visitEFactory(EFactory eFactory) {
+    }
+
+    protected void visitEGenericType(EGenericType eGenericType) {
+    }
+
+    protected void visitEObject(EObject eObject) {
+
+    }
+
+    protected void visitEOperation(EOperation eOperation) {
+    }
+
+    protected void visitEPackage(EPackage ePackage) {
+    }
+
+    protected void visitEParameter(EParameter eParameter) {
+    }
+
+    protected void visitEReference(EReference eReference) {
+    }
+
+    protected void visitEStructuralFeature(EStructuralFeature eStructuralFeature) {
+    }
+
+    protected void visitETypeParameter(ETypeParameter eTypeParameter) {
+    }
+
+}

+ 75 - 0
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/util/EMFUtil.java

@@ -0,0 +1,75 @@
+package de.jplag.emf.util;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.emf.common.util.URI;
+import org.eclipse.emf.common.util.WrappedException;
+import org.eclipse.emf.ecore.EObject;
+import org.eclipse.emf.ecore.EPackage;
+import org.eclipse.emf.ecore.EcorePackage;
+import org.eclipse.emf.ecore.resource.Resource;
+import org.eclipse.emf.ecore.resource.ResourceSet;
+import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
+import org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility class for EMF specific functionality.
+ * @author Timur Saglam
+ */
+public final class EMFUtil {
+
+    private static final Logger logger = LoggerFactory.getLogger(EMFUtil.class);
+
+    private EMFUtil() {
+        // private constructor for non-instantiability.
+    }
+
+    /**
+     * Registers a file extension in the EMF registry for a XMI resource factory.
+     * @param extension is the file extension string including a dot. Use "*" for any extension.
+     */
+    public static void registerModelExtension(String extension) {
+        final Resource.Factory.Registry registry = Resource.Factory.Registry.INSTANCE;
+        final Map<String, Object> extensionMap = registry.getExtensionToFactoryMap();
+        extensionMap.put(extension, new XMIResourceFactoryImpl());
+    }
+
+    /**
+     * Register the Ecore file extension in the EMF registry.
+     */
+    public static void registerEcoreExtension() {
+        EcorePackage.eINSTANCE.eClass();
+        registerModelExtension(EcorePackage.eNAME);
+    }
+
+    /**
+     * Registers a collection of EPackages in the EPackage registry via their URIs.
+     * @param ePackages are the EPackages to register.
+     */
+    public static void registerEPackageURIs(Collection<EPackage> ePackages) {
+        ePackages.forEach(it -> EPackage.Registry.INSTANCE.put(it.getNsURI(), it));
+    }
+
+    /**
+     * Loads a model or metamodel from a absolute file path.
+     * @param filePath is the absolute path to the (meta)model.
+     * @return the content of the loaded (meta)model resource or null if it could not be loaded.
+     */
+    public static List<EObject> loadModel(String filePath) {
+        final ResourceSet resourceSet = new ResourceSetImpl();
+        try {
+            final Resource resource = resourceSet.getResource(URI.createFileURI(filePath), true);
+            return resource.getContents();
+        } catch (WrappedException exception) {
+            String name = new File(filePath).getName();
+            logger.error("Could not load " + name + ": " + exception.getCause().getMessage());
+        }
+        return null;
+    }
+
+}

+ 90 - 0
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/util/MetamodelTreeView.java

@@ -0,0 +1,90 @@
+package de.jplag.emf.util;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+import org.eclipse.emf.ecore.ENamedElement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.jplag.emf.MetamodelToken;
+
+/**
+ * Simplistic tree view representation of an EMF metamodel.
+ * @author Timur Saglam
+ */
+public class MetamodelTreeView {
+    private final String filePath;
+    private int lineIndex;
+    private int columnIndex;
+    private final StringBuilder viewBuilder;
+    private static final String INDENTATION = "  ";
+    public final Logger logger;
+
+    /**
+     * Creates a tree view for a metamodel.
+     * @param filePath is the path to the file where the metamodel is persisted.
+     */
+    public MetamodelTreeView(String filePath) {
+        this.filePath = filePath;
+        logger = LoggerFactory.getLogger(this.getClass());
+        viewBuilder = new StringBuilder();
+    }
+
+    /**
+     * Adds a token to the view, thus adding the index information to the token.
+     * @param token is the token to add.
+     * @param treeDepth is the current containment tree depth, required for the indentation.
+     */
+    public void addToken(MetamodelToken token, int treeDepth, String prefix) {
+        token.getEObject().ifPresent(it -> {
+            if (prefix.isEmpty() && treeDepth > 0) {
+                lineIndex++;
+                columnIndex = 0;
+                viewBuilder.append(System.lineSeparator());
+            }
+
+            String tokenText = token.toString();
+            if (it instanceof ENamedElement element) {
+                tokenText = element.getName() + " : " + tokenText;
+            }
+            token.setLength(tokenText.length());
+
+            if (prefix.isEmpty()) {
+                for (int i = 0; i < treeDepth; i++) {
+                    viewBuilder.append(INDENTATION);
+                    columnIndex += INDENTATION.length();
+                }
+                viewBuilder.append(tokenText);
+            } else {
+                viewBuilder.append(prefix + tokenText);
+                columnIndex += prefix.length();
+            }
+
+            token.setLine(lineIndex + 1);
+            token.setColumn(columnIndex + 1);
+
+            columnIndex += tokenText.length();
+
+        });
+    }
+
+    /**
+     * Writes the tree view into a file.
+     * @param suffix is the suffix of the file to be written.
+     */
+    public void writeToFile(String suffix) {
+        File treeViewFile = new File(filePath + suffix);
+        try (BufferedWriter writer = new BufferedWriter(new FileWriter(treeViewFile));) {
+            if (!treeViewFile.createNewFile()) {
+                logger.warn("Overwriting tree view file: {}", treeViewFile);
+            }
+            writer.append(viewBuilder.toString());
+        } catch (IOException exception) {
+            logger.error("Could not write tree view file!", exception);
+        }
+    }
+
+}

+ 66 - 0
jplag.frontend.emf-metamodel/src/test/java/de/jplag/emf/MinimalMetamodelTest.java

@@ -0,0 +1,66 @@
+package de.jplag.emf;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertIterableEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.jplag.TokenList;
+import de.jplag.TokenPrinter;
+import de.jplag.testutils.FileUtil;
+import de.jplag.testutils.TokenUtils;
+
+class MinimalMetamodelTest {
+    private final Logger logger = LoggerFactory.getLogger("JPlag-Test");
+
+    private static final Path BASE_PATH = Path.of("src", "test", "resources", "de", "jplag", "models");
+    private static final String[] TEST_SUBJECTS = {"bookStore.ecore", "bookStoreExtended.ecore", "bookStoreRenamed.ecore"};
+
+    private de.jplag.Language frontend;
+    private File baseDirectory;
+
+    @BeforeEach
+    public void setUp() {
+        frontend = new Language();
+        baseDirectory = BASE_PATH.toFile();
+        FileUtil.assertDirectory(baseDirectory, TEST_SUBJECTS);
+    }
+
+    @Test
+    void testBookstoreMetamodels() {
+        TokenList result = frontend.parse(baseDirectory, TEST_SUBJECTS);
+        List<String> treeViewFiles = Arrays.stream(TEST_SUBJECTS).map(it -> it + Language.VIEW_FILE_SUFFIX).toList();
+
+        logger.debug(TokenPrinter.printTokens(result, baseDirectory, treeViewFiles, Optional.of(Language.VIEW_FILE_SUFFIX)));
+        Field[] fields = MetamodelTokenConstants.class.getFields();
+        var constants = Arrays.stream(fields).map(Field::getName).filter(it -> !it.equals("NUM_DIFF_TOKENS")).toList();
+        logger.info(("Handcrafted token set: " + constants));
+        logger.info("Parsed tokens: " + result.allTokens().toString());
+        assertEquals(21, constants.size());
+        assertEquals(43, result.size());
+
+        var bookstoreTokens = TokenUtils.tokenTypesByFile(result, TEST_SUBJECTS[0]);
+        var bookstoreRenamedTokens = TokenUtils.tokenTypesByFile(result, TEST_SUBJECTS[2]);
+        var bookstoreExtendedTokens = TokenUtils.tokenTypesByFile(result, TEST_SUBJECTS[1]);
+        assertTrue(bookstoreTokens.size() < bookstoreExtendedTokens.size());
+        assertIterableEquals(bookstoreTokens, bookstoreRenamedTokens);
+    }
+
+    @AfterEach
+    public void tearDown() {
+        FileUtil.clearFiles(new File(BASE_PATH.toString()), Language.VIEW_FILE_SUFFIX);
+    }
+
+}

+ 16 - 0
jplag.frontend.emf-metamodel/src/test/resources/de/jplag/models/bookStore.ecore

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="ASCII"?>
+<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="BookStorePackage" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore"
+    nsPrefix="bookStore">
+  <eClassifiers xsi:type="ecore:EClass" name="BookStore">
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="owner" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="location" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EReference" name="books" upperBound="-1"
+        eType="#//Book" containment="true"/>
+  </eClassifiers>
+  <eClassifiers xsi:type="ecore:EClass" name="Book">
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="isbn" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EInt"
+        iD="true"/>
+  </eClassifiers>
+</ecore:EPackage>

+ 38 - 0
jplag.frontend.emf-metamodel/src/test/resources/de/jplag/models/bookStoreExtended.ecore

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="ASCII"?>
+<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="BookStorePackage" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore"
+    nsPrefix="bookStore">
+  <eSubpackages name="store" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore/store"
+      nsPrefix="store">
+    <eClassifiers xsi:type="ecore:EClass" name="BookStore">
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="location" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+      <eStructuralFeatures xsi:type="ecore:EReference" name="books" upperBound="-1"
+          eType="#//store/Book" containment="true"/>
+      <eStructuralFeatures xsi:type="ecore:EReference" name="owner" eType="#//person/Person"/>
+    </eClassifiers>
+    <eClassifiers xsi:type="ecore:EClass" name="Book">
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="isbn" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EInt"
+          iD="true"/>
+      <eStructuralFeatures xsi:type="ecore:EReference" name="author" eType="#//person/Author"/>
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="genre" eType="#//store/Genre"/>
+    </eClassifiers>
+    <eClassifiers xsi:type="ecore:EEnum" name="Genre">
+      <eLiterals name="NOVEL"/>
+      <eLiterals name="COOKBOOK" value="1"/>
+      <eLiterals name="BIOGRAPHY" value="3"/>
+      <eLiterals name="TEXTBOOK" value="4"/>
+    </eClassifiers>
+  </eSubpackages>
+  <eSubpackages name="person" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore/person"
+      nsPrefix="person">
+    <eClassifiers xsi:type="ecore:EClass" name="Author" eSuperTypes="#//person/Person">
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="isStageName" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EBoolean"/>
+    </eClassifiers>
+    <eClassifiers xsi:type="ecore:EClass" name="Person">
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="firstName" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="lastName" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    </eClassifiers>
+  </eSubpackages>
+</ecore:EPackage>

+ 16 - 0
jplag.frontend.emf-metamodel/src/test/resources/de/jplag/models/bookStoreRenamed.ecore

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="ASCII"?>
+<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="BookStorePackage" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore"
+    nsPrefix="bookStore">
+  <eClassifiers xsi:type="ecore:EClass" name="StoreThatSellsBooks">
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="nameOfOwner" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="city" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EReference" name="soldItems" upperBound="-1"
+        eType="#//BookStoreItem" containment="true"/>
+  </eClassifiers>
+  <eClassifiers xsi:type="ecore:EClass" name="BookStoreItem">
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="title" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="identifier" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EInt"
+        iD="true"/>
+  </eClassifiers>
+</ecore:EPackage>

+ 29 - 0
jplag.frontend.golang/README.md

@@ -0,0 +1,29 @@
+# JPlag Go language frontend
+
+The JPlag Go frontend allows the use of JPlag with submissions in Go. <br>
+It is based on the [Golang ANTLR4 grammar](https://github.com/antlr/grammars-v4/tree/master/golang), licensed under BSD-3.
+
+### Go specification compatibility
+
+The underlying grammar definition does not specify which version of Go it is built on. This is what we know:
+ - Number literal prefixes, a feature of go1.13, are included.
+ - Generics, a feature of go1.18, are _not_ included.
+ - Between go1.13 and go1.18, there were no changes to the syntax. So, the grammar should be fully compatible with go1.17, released in mid-2021.
+
+If the grammar is updated to a more recent<a href="#footnote-1"><sup>1</sup></a> syntax definition, this module should surely be updated as well.
+
+### Token Extraction
+
+The choice of tokens is intended to be similar to the Java or C# frontends. Specifically, among others, it includes a range of nesting structures (class and method declarations, control flow expressions) as well as variable declaration, object creation, assignment, and control flow altering keywords. <br>
+Blocks are distinguished by their context, i.e. there are separate `TokenConstants` for `if` blocks, `for` blocks, class bodies, method bodies, array constructors, and the like.
+
+More syntactic elements of Go may turn out to be helpful to include in the future, especially those that are newly introduced.
+
+### Usage
+
+To use the Go frontend, add the `-l golang` flag in the CLI, or use a `JPlagOption` object set to `LanguageOption.GO_LANG` 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).
+
+<br>
+
+#### Footnotes
+<section id="footnote-1"><sup>1 </sup>The grammar files are taken from grammar-v4, with the most recent modifiactions in <a href="https://github.com/antlr/grammars-v4/tree/51ecccf87b75e96177287367b96cfa99e9f304b8/golang">commit 51ecccf</a> from April 2022.</section>

+ 45 - 0
jplag.frontend.golang/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>golang</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>frontend-utils</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.antlr</groupId>
+            <artifactId>antlr4-runtime</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>

+ 521 - 0
jplag.frontend.golang/src/main/antlr4/de/jplag/golang/grammar/GoLexer.g4

@@ -0,0 +1,521 @@
+/*
+ [The "BSD licence"]
+ Copyright (c) 2017 Sasa Coh, Michał Błotniak
+ Copyright (c) 2019 Ivan Kochurkin, kvanttt@gmail.com, Positive Technologies
+ Copyright (c) 2019 Dmitry Rassadin, flipparassa@gmail.com, Positive Technologies
+ Copyright (c) 2021 Martin Mirchev, mirchevmartin2203@gmail.com
+ 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.
+*/
+
+/*
+ * A Go grammar for ANTLR 4 derived from the Go Language Specification
+ * https://golang.org/ref/spec
+ */
+
+lexer grammar GoLexer;
+
+// Keywords
+
+BREAK                  : 'break' -> mode(NLSEMI);
+DEFAULT                : 'default';
+FUNC                   : 'func';
+INTERFACE              : 'interface';
+SELECT                 : 'select';
+CASE                   : 'case';
+DEFER                  : 'defer';
+GO                     : 'go';
+MAP                    : 'map';
+STRUCT                 : 'struct';
+CHAN                   : 'chan';
+ELSE                   : 'else';
+GOTO                   : 'goto';
+PACKAGE                : 'package';
+SWITCH                 : 'switch';
+CONST                  : 'const';
+FALLTHROUGH            : 'fallthrough' -> mode(NLSEMI);
+IF                     : 'if';
+RANGE                  : 'range';
+TYPE                   : 'type';
+CONTINUE               : 'continue' -> mode(NLSEMI);
+FOR                    : 'for';
+IMPORT                 : 'import';
+RETURN                 : 'return' -> mode(NLSEMI);
+VAR                    : 'var';
+
+NIL_LIT                : 'nil' -> mode(NLSEMI);
+
+IDENTIFIER             : LETTER (LETTER | UNICODE_DIGIT)* -> mode(NLSEMI);
+
+// Punctuation
+
+L_PAREN                : '(';
+R_PAREN                : ')' -> mode(NLSEMI);
+L_CURLY                : '{';
+R_CURLY                : '}' -> mode(NLSEMI);
+L_BRACKET              : '[';
+R_BRACKET              : ']' -> mode(NLSEMI);
+ASSIGN                 : '=';
+COMMA                  : ',';
+SEMI                   : ';';
+COLON                  : ':';
+DOT                    : '.';
+PLUS_PLUS              : '++' -> mode(NLSEMI);
+MINUS_MINUS            : '--' -> mode(NLSEMI);
+DECLARE_ASSIGN         : ':=';
+ELLIPSIS               : '...';
+
+// Logical
+
+LOGICAL_OR             : '||';
+LOGICAL_AND            : '&&';
+
+// Relation operators
+
+EQUALS                 : '==';
+NOT_EQUALS             : '!=';
+LESS                   : '<';
+LESS_OR_EQUALS         : '<=';
+GREATER                : '>';
+GREATER_OR_EQUALS      : '>=';
+
+// Arithmetic operators
+
+OR                     : '|';
+DIV                    : '/';
+MOD                    : '%';
+LSHIFT                 : '<<';
+RSHIFT                 : '>>';
+BIT_CLEAR              : '&^';
+
+// Unary operators
+
+EXCLAMATION            : '!';
+
+// Mixed operators
+
+PLUS                   : '+';
+MINUS                  : '-';
+CARET                  : '^';
+STAR                   : '*';
+AMPERSAND              : '&';
+RECEIVE                : '<-';
+
+// Number literals
+
+DECIMAL_LIT            : ('0' | [1-9] ('_'? [0-9])*) -> mode(NLSEMI);
+BINARY_LIT             : '0' [bB] ('_'? BIN_DIGIT)+ -> mode(NLSEMI);
+OCTAL_LIT              : '0' [oO]? ('_'? OCTAL_DIGIT)+ -> mode(NLSEMI);
+HEX_LIT                : '0' [xX]  ('_'? HEX_DIGIT)+ -> mode(NLSEMI);
+
+
+FLOAT_LIT : (DECIMAL_FLOAT_LIT | HEX_FLOAT_LIT) -> mode(NLSEMI);
+
+DECIMAL_FLOAT_LIT      : DECIMALS ('.' DECIMALS? EXPONENT? | EXPONENT)
+                       | '.' DECIMALS EXPONENT?
+                       ;
+
+HEX_FLOAT_LIT          : '0' [xX] HEX_MANTISSA HEX_EXPONENT
+                       ;
+
+fragment HEX_MANTISSA  : ('_'? HEX_DIGIT)+ ('.' ( '_'? HEX_DIGIT )*)?
+                       | '.' HEX_DIGIT ('_'? HEX_DIGIT)*;
+
+fragment HEX_EXPONENT  : [pP] [+-]? DECIMALS;
+
+
+IMAGINARY_LIT          : (DECIMAL_LIT | BINARY_LIT |  OCTAL_LIT | HEX_LIT | FLOAT_LIT) 'i' -> mode(NLSEMI);
+
+// Rune literals
+
+fragment RUNE               : '\'' (UNICODE_VALUE | BYTE_VALUE) '\'';//: '\'' (~[\n\\] | ESCAPED_VALUE) '\'';
+
+RUNE_LIT                : RUNE -> mode(NLSEMI);
+
+
+
+BYTE_VALUE : OCTAL_BYTE_VALUE | HEX_BYTE_VALUE;
+
+OCTAL_BYTE_VALUE: '\\' OCTAL_DIGIT OCTAL_DIGIT OCTAL_DIGIT;
+
+HEX_BYTE_VALUE: '\\' 'x'  HEX_DIGIT HEX_DIGIT;
+
+LITTLE_U_VALUE: '\\' 'u' HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT;
+
+BIG_U_VALUE: '\\' 'U' HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT;
+
+// String literals
+
+RAW_STRING_LIT         : '`' ~'`'*                      '`' -> mode(NLSEMI);
+INTERPRETED_STRING_LIT : '"' (~["\\] | ESCAPED_VALUE)*  '"' -> mode(NLSEMI);
+
+// Hidden tokens
+
+WS                     : [ \t]+             -> channel(HIDDEN);
+COMMENT                : '/*' .*? '*/'      -> channel(HIDDEN);
+TERMINATOR             : [\r\n]+            -> channel(HIDDEN);
+LINE_COMMENT           : '//' ~[\r\n]*      -> channel(HIDDEN);
+
+fragment UNICODE_VALUE: ~[\r\n'] | LITTLE_U_VALUE | BIG_U_VALUE | ESCAPED_VALUE;
+
+// Fragments
+
+fragment ESCAPED_VALUE
+    : '\\' ('u' HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT
+           | 'U' HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT
+           | [abfnrtv\\'"]
+           | OCTAL_DIGIT OCTAL_DIGIT OCTAL_DIGIT
+           | 'x' HEX_DIGIT HEX_DIGIT)
+    ;
+
+fragment DECIMALS
+    : [0-9] ('_'? [0-9])*
+    ;
+
+fragment OCTAL_DIGIT
+    : [0-7]
+    ;
+
+fragment HEX_DIGIT
+    : [0-9a-fA-F]
+    ;
+
+fragment BIN_DIGIT
+    : [01]
+    ;
+
+fragment EXPONENT
+    : [eE] [+-]? DECIMALS
+    ;
+
+fragment LETTER
+    : UNICODE_LETTER
+    | '_'
+    ;
+
+fragment UNICODE_DIGIT
+    : [\p{Nd}]
+
+    /*  [\u0030-\u0039]
+    | [\u0660-\u0669]
+    | [\u06F0-\u06F9]
+    | [\u0966-\u096F]
+    | [\u09E6-\u09EF]
+    | [\u0A66-\u0A6F]
+    | [\u0AE6-\u0AEF]
+    | [\u0B66-\u0B6F]
+    | [\u0BE7-\u0BEF]
+    | [\u0C66-\u0C6F]
+    | [\u0CE6-\u0CEF]
+    | [\u0D66-\u0D6F]
+    | [\u0E50-\u0E59]
+    | [\u0ED0-\u0ED9]
+    | [\u0F20-\u0F29]
+    | [\u1040-\u1049]
+    | [\u1369-\u1371]
+    | [\u17E0-\u17E9]
+    | [\u1810-\u1819]
+    | [\uFF10-\uFF19]*/
+    ;
+
+fragment UNICODE_LETTER
+    : [\p{L}]
+    /*  [\u0041-\u005A]
+    | [\u0061-\u007A]
+    | [\u00AA]
+    | [\u00B5]
+    | [\u00BA]
+    | [\u00C0-\u00D6]
+    | [\u00D8-\u00F6]
+    | [\u00F8-\u021F]
+    | [\u0222-\u0233]
+    | [\u0250-\u02AD]
+    | [\u02B0-\u02B8]
+    | [\u02BB-\u02C1]
+    | [\u02D0-\u02D1]
+    | [\u02E0-\u02E4]
+    | [\u02EE]
+    | [\u037A]
+    | [\u0386]
+    | [\u0388-\u038A]
+    | [\u038C]
+    | [\u038E-\u03A1]
+    | [\u03A3-\u03CE]
+    | [\u03D0-\u03D7]
+    | [\u03DA-\u03F3]
+    | [\u0400-\u0481]
+    | [\u048C-\u04C4]
+    | [\u04C7-\u04C8]
+    | [\u04CB-\u04CC]
+    | [\u04D0-\u04F5]
+    | [\u04F8-\u04F9]
+    | [\u0531-\u0556]
+    | [\u0559]
+    | [\u0561-\u0587]
+    | [\u05D0-\u05EA]
+    | [\u05F0-\u05F2]
+    | [\u0621-\u063A]
+    | [\u0640-\u064A]
+    | [\u0671-\u06D3]
+    | [\u06D5]
+    | [\u06E5-\u06E6]
+    | [\u06FA-\u06FC]
+    | [\u0710]
+    | [\u0712-\u072C]
+    | [\u0780-\u07A5]
+    | [\u0905-\u0939]
+    | [\u093D]
+    | [\u0950]
+    | [\u0958-\u0961]
+    | [\u0985-\u098C]
+    | [\u098F-\u0990]
+    | [\u0993-\u09A8]
+    | [\u09AA-\u09B0]
+    | [\u09B2]
+    | [\u09B6-\u09B9]
+    | [\u09DC-\u09DD]
+    | [\u09DF-\u09E1]
+    | [\u09F0-\u09F1]
+    | [\u0A05-\u0A0A]
+    | [\u0A0F-\u0A10]
+    | [\u0A13-\u0A28]
+    | [\u0A2A-\u0A30]
+    | [\u0A32-\u0A33]
+    | [\u0A35-\u0A36]
+    | [\u0A38-\u0A39]
+    | [\u0A59-\u0A5C]
+    | [\u0A5E]
+    | [\u0A72-\u0A74]
+    | [\u0A85-\u0A8B]
+    | [\u0A8D]
+    | [\u0A8F-\u0A91]
+    | [\u0A93-\u0AA8]
+    | [\u0AAA-\u0AB0]
+    | [\u0AB2-\u0AB3]
+    | [\u0AB5-\u0AB9]
+    | [\u0ABD]
+    | [\u0AD0]
+    | [\u0AE0]
+    | [\u0B05-\u0B0C]
+    | [\u0B0F-\u0B10]
+    | [\u0B13-\u0B28]
+    | [\u0B2A-\u0B30]
+    | [\u0B32-\u0B33]
+    | [\u0B36-\u0B39]
+    | [\u0B3D]
+    | [\u0B5C-\u0B5D]
+    | [\u0B5F-\u0B61]
+    | [\u0B85-\u0B8A]
+    | [\u0B8E-\u0B90]
+    | [\u0B92-\u0B95]
+    | [\u0B99-\u0B9A]
+    | [\u0B9C]
+    | [\u0B9E-\u0B9F]
+    | [\u0BA3-\u0BA4]
+    | [\u0BA8-\u0BAA]
+    | [\u0BAE-\u0BB5]
+    | [\u0BB7-\u0BB9]
+    | [\u0C05-\u0C0C]
+    | [\u0C0E-\u0C10]
+    | [\u0C12-\u0C28]
+    | [\u0C2A-\u0C33]
+    | [\u0C35-\u0C39]
+    | [\u0C60-\u0C61]
+    | [\u0C85-\u0C8C]
+    | [\u0C8E-\u0C90]
+    | [\u0C92-\u0CA8]
+    | [\u0CAA-\u0CB3]
+    | [\u0CB5-\u0CB9]
+    | [\u0CDE]
+    | [\u0CE0-\u0CE1]
+    | [\u0D05-\u0D0C]
+    | [\u0D0E-\u0D10]
+    | [\u0D12-\u0D28]
+    | [\u0D2A-\u0D39]
+    | [\u0D60-\u0D61]
+    | [\u0D85-\u0D96]
+    | [\u0D9A-\u0DB1]
+    | [\u0DB3-\u0DBB]
+    | [\u0DBD]
+    | [\u0DC0-\u0DC6]
+    | [\u0E01-\u0E30]
+    | [\u0E32-\u0E33]
+    | [\u0E40-\u0E46]
+    | [\u0E81-\u0E82]
+    | [\u0E84]
+    | [\u0E87-\u0E88]
+    | [\u0E8A]
+    | [\u0E8D]
+    | [\u0E94-\u0E97]
+    | [\u0E99-\u0E9F]
+    | [\u0EA1-\u0EA3]
+    | [\u0EA5]
+    | [\u0EA7]
+    | [\u0EAA-\u0EAB]
+    | [\u0EAD-\u0EB0]
+    | [\u0EB2-\u0EB3]
+    | [\u0EBD-\u0EC4]
+    | [\u0EC6]
+    | [\u0EDC-\u0EDD]
+    | [\u0F00]
+    | [\u0F40-\u0F6A]
+    | [\u0F88-\u0F8B]
+    | [\u1000-\u1021]
+    | [\u1023-\u1027]
+    | [\u1029-\u102A]
+    | [\u1050-\u1055]
+    | [\u10A0-\u10C5]
+    | [\u10D0-\u10F6]
+    | [\u1100-\u1159]
+    | [\u115F-\u11A2]
+    | [\u11A8-\u11F9]
+    | [\u1200-\u1206]
+    | [\u1208-\u1246]
+    | [\u1248]
+    | [\u124A-\u124D]
+    | [\u1250-\u1256]
+    | [\u1258]
+    | [\u125A-\u125D]
+    | [\u1260-\u1286]
+    | [\u1288]
+    | [\u128A-\u128D]
+    | [\u1290-\u12AE]
+    | [\u12B0]
+    | [\u12B2-\u12B5]
+    | [\u12B8-\u12BE]
+    | [\u12C0]
+    | [\u12C2-\u12C5]
+    | [\u12C8-\u12CE]
+    | [\u12D0-\u12D6]
+    | [\u12D8-\u12EE]
+    | [\u12F0-\u130E]
+    | [\u1310]
+    | [\u1312-\u1315]
+    | [\u1318-\u131E]
+    | [\u1320-\u1346]
+    | [\u1348-\u135A]
+    | [\u13A0-\u13B0]
+    | [\u13B1-\u13F4]
+    | [\u1401-\u1676]
+    | [\u1681-\u169A]
+    | [\u16A0-\u16EA]
+    | [\u1780-\u17B3]
+    | [\u1820-\u1877]
+    | [\u1880-\u18A8]
+    | [\u1E00-\u1E9B]
+    | [\u1EA0-\u1EE0]
+    | [\u1EE1-\u1EF9]
+    | [\u1F00-\u1F15]
+    | [\u1F18-\u1F1D]
+    | [\u1F20-\u1F39]
+    | [\u1F3A-\u1F45]
+    | [\u1F48-\u1F4D]
+    | [\u1F50-\u1F57]
+    | [\u1F59]
+    | [\u1F5B]
+    | [\u1F5D]
+    | [\u1F5F-\u1F7D]
+    | [\u1F80-\u1FB4]
+    | [\u1FB6-\u1FBC]
+    | [\u1FBE]
+    | [\u1FC2-\u1FC4]
+    | [\u1FC6-\u1FCC]
+    | [\u1FD0-\u1FD3]
+    | [\u1FD6-\u1FDB]
+    | [\u1FE0-\u1FEC]
+    | [\u1FF2-\u1FF4]
+    | [\u1FF6-\u1FFC]
+    | [\u207F]
+    | [\u2102]
+    | [\u2107]
+    | [\u210A-\u2113]
+    | [\u2115]
+    | [\u2119-\u211D]
+    | [\u2124]
+    | [\u2126]
+    | [\u2128]
+    | [\u212A-\u212D]
+    | [\u212F-\u2131]
+    | [\u2133-\u2139]
+    | [\u2160-\u2183]
+    | [\u3005-\u3007]
+    | [\u3021-\u3029]
+    | [\u3031-\u3035]
+    | [\u3038-\u303A]
+    | [\u3041-\u3094]
+    | [\u309D-\u309E]
+    | [\u30A1-\u30FA]
+    | [\u30FC-\u30FE]
+    | [\u3105-\u312C]
+    | [\u3131-\u318E]
+    | [\u31A0-\u31B7]
+    | [\u3400]
+    | [\u4DB5]
+    | [\u4E00]
+    | [\u9FA5]
+    | [\uA000-\uA48C]
+    | [\uAC00]
+    | [\uD7A3]
+    | [\uF900-\uFA2D]
+    | [\uFB00-\uFB06]
+    | [\uFB13-\uFB17]
+    | [\uFB1D]
+    | [\uFB1F-\uFB28]
+    | [\uFB2A-\uFB36]
+    | [\uFB38-\uFB3C]
+    | [\uFB3E]
+    | [\uFB40-\uFB41]
+    | [\uFB43-\uFB44]
+    | [\uFB46-\uFBB1]
+    | [\uFBD3-\uFD3D]
+    | [\uFD50-\uFD8F]
+    | [\uFD92-\uFDC7]
+    | [\uFDF0-\uFDFB]
+    | [\uFE70-\uFE72]
+    | [\uFE74]
+    | [\uFE76-\uFEFC]
+    | [\uFF21-\uFF3A]
+    | [\uFF41-\uFF5A]
+    | [\uFF66-\uFFBE]
+    | [\uFFC2-\uFFC7]
+    | [\uFFCA-\uFFCF]
+    | [\uFFD2-\uFFD7]
+    | [\uFFDA-\uFFDC]
+    */
+    ;
+
+
+mode NLSEMI;
+
+
+// Treat whitespace as normal
+WS_NLSEMI                     : [ \t]+             -> channel(HIDDEN);
+// Ignore any comments that only span one line
+COMMENT_NLSEMI                : '/*' ~[\r\n]*? '*/'      -> channel(HIDDEN);
+LINE_COMMENT_NLSEMI : '//' ~[\r\n]*      -> channel(HIDDEN);
+// Emit an EOS token for any newlines, semicolon, multiline comments or the EOF and 
+//return to normal lexing
+EOS:              ([\r\n]+ | ';' | '/*' .*? '*/' | EOF)            -> mode(DEFAULT_MODE);
+// Did not find an EOS, so go back to normal lexing
+OTHER: -> mode(DEFAULT_MODE), channel(HIDDEN);

+ 380 - 0
jplag.frontend.golang/src/main/antlr4/de/jplag/golang/grammar/GoParser.g4

@@ -0,0 +1,380 @@
+/*
+ [The "BSD licence"] Copyright (c) 2017 Sasa Coh, Michał Błotniak Copyright (c) 2019 Ivan Kochurkin,
+ kvanttt@gmail.com, Positive Technologies Copyright (c) 2019 Dmitry Rassadin,
+ flipparassa@gmail.com,Positive Technologies All rights reserved. Copyright (c) 2021 Martin Mirchev,
+ mirchevmartin2203@gmail.com
+
+ 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.
+ */
+
+/*
+ * A Go grammar for ANTLR 4 derived from the Go Language Specification https://golang.org/ref/spec
+ */
+
+parser grammar GoParser;
+
+options {
+	tokenVocab = GoLexer;
+	superClass = GoParserBase;
+}
+
+sourceFile:
+	packageClause eos (importDecl eos)* (
+		(functionDecl | methodDecl | declaration) eos
+	)* EOF;
+
+packageClause: PACKAGE packageName = IDENTIFIER;
+
+importDecl:
+	IMPORT (importSpec | L_PAREN (importSpec eos)* R_PAREN);
+
+importSpec: alias = (DOT | IDENTIFIER)? importPath;
+
+importPath: string_;
+
+declaration: constDecl | typeDecl | varDecl;
+
+constDecl: CONST (constSpec | L_PAREN (constSpec eos)* R_PAREN);
+
+constSpec: identifierList (type_? ASSIGN expressionList)?;
+
+identifierList: IDENTIFIER (COMMA IDENTIFIER)*;
+
+expressionList: expression (COMMA expression)*;
+
+typeDecl: TYPE (typeSpec | L_PAREN (typeSpec eos)* R_PAREN);
+
+typeSpec: IDENTIFIER ASSIGN? type_;
+
+// Function declarations
+
+functionDecl: FUNC IDENTIFIER (signature block?);
+
+methodDecl: FUNC receiver IDENTIFIER ( signature block?);
+
+receiver: parameters;
+
+varDecl: VAR (varSpec | L_PAREN (varSpec eos)* R_PAREN);
+
+varSpec:
+	identifierList (
+		type_ (ASSIGN expressionList)?
+		| ASSIGN expressionList
+	);
+
+block: L_CURLY statementList? R_CURLY;
+
+statementList: ((SEMI? | EOS? | {closingBracket()}?) statement eos)+;
+
+statement:
+	declaration
+	| labeledStmt
+	| simpleStmt
+	| goStmt
+	| returnStmt
+	| breakStmt
+	| continueStmt
+	| gotoStmt
+	| fallthroughStmt
+	| block
+	| ifStmt
+	| switchStmt
+	| selectStmt
+	| forStmt
+	| deferStmt;
+
+simpleStmt:
+	sendStmt
+	| incDecStmt
+	| assignment
+	| expressionStmt
+	| shortVarDecl;
+
+expressionStmt: expression;
+
+sendStmt: channel = expression RECEIVE expression;
+
+incDecStmt: expression (PLUS_PLUS | MINUS_MINUS);
+
+assignment: expressionList assign_op expressionList;
+
+assign_op: (
+		PLUS
+		| MINUS
+		| OR
+		| CARET
+		| STAR
+		| DIV
+		| MOD
+		| LSHIFT
+		| RSHIFT
+		| AMPERSAND
+		| BIT_CLEAR
+	)? ASSIGN;
+
+shortVarDecl: identifierList DECLARE_ASSIGN expressionList;
+
+emptyStmt: EOS | SEMI;
+
+labeledStmt: IDENTIFIER COLON statement?;
+
+returnStmt: RETURN expressionList?;
+
+breakStmt: BREAK IDENTIFIER?;
+
+continueStmt: CONTINUE IDENTIFIER?;
+
+gotoStmt: GOTO IDENTIFIER;
+
+fallthroughStmt: FALLTHROUGH;
+
+deferStmt: DEFER expression;
+
+ifStmt:
+	IF ( expression
+			| eos expression
+			| simpleStmt eos expression
+			) block (
+		ELSE (ifStmt | block)
+	)?;
+
+switchStmt: exprSwitchStmt | typeSwitchStmt;
+
+exprSwitchStmt:
+	SWITCH (expression?
+					| simpleStmt? eos expression?
+					) L_CURLY exprCaseClause* R_CURLY;
+
+exprCaseClause: exprSwitchCase COLON statementList?;
+
+exprSwitchCase: CASE expressionList | DEFAULT;
+
+typeSwitchStmt:
+	SWITCH ( typeSwitchGuard
+					| eos typeSwitchGuard
+					| simpleStmt eos typeSwitchGuard)
+					 L_CURLY typeCaseClause* R_CURLY;
+
+typeSwitchGuard: (IDENTIFIER DECLARE_ASSIGN)? primaryExpr DOT L_PAREN TYPE R_PAREN;
+
+typeCaseClause: typeSwitchCase COLON statementList?;
+
+typeSwitchCase: CASE typeList | DEFAULT;
+
+typeList: (type_ | NIL_LIT) (COMMA (type_ | NIL_LIT))*;
+
+selectStmt: SELECT L_CURLY commClause* R_CURLY;
+
+commClause: commCase COLON statementList?;
+
+commCase: CASE (sendStmt | recvStmt) | DEFAULT;
+
+recvStmt: (expressionList ASSIGN | identifierList DECLARE_ASSIGN)? recvExpr = expression;
+
+forStmt: FOR (expression? | forClause | rangeClause?) block;
+
+forClause:
+	initStmt = simpleStmt? eos expression? eos postStmt = simpleStmt?;
+
+rangeClause: (
+		expressionList ASSIGN
+		| identifierList DECLARE_ASSIGN
+	)? RANGE expression;
+
+goStmt: GO expression;
+
+type_: typeName | typeLit | L_PAREN type_ R_PAREN;
+
+typeName: qualifiedIdent | IDENTIFIER;
+
+typeLit:
+	arrayType
+	| structType
+	| pointerType
+	| functionType
+	| interfaceType
+	| sliceType
+	| mapType
+	| channelType;
+
+arrayType: L_BRACKET arrayLength R_BRACKET elementType;
+
+arrayLength: expression;
+
+elementType: type_;
+
+pointerType: STAR type_;
+
+interfaceType:
+	INTERFACE L_CURLY ((methodSpec | typeName) eos)* R_CURLY;
+
+sliceType: L_BRACKET R_BRACKET elementType;
+
+// It's possible to replace `type` with more restricted typeLit list and also pay attention to nil maps
+mapType: MAP L_BRACKET type_ R_BRACKET elementType;
+
+channelType: (CHAN | CHAN RECEIVE | RECEIVE CHAN) elementType;
+
+methodSpec:
+	IDENTIFIER parameters result
+	| IDENTIFIER parameters;
+
+functionType: FUNC signature;
+
+signature:
+	parameters result
+	| parameters;
+
+result: parameters | type_;
+
+parameters:
+	L_PAREN (parameterDecl (COMMA parameterDecl)* COMMA?)? R_PAREN;
+
+parameterDecl: identifierList? ELLIPSIS? type_;
+
+expression:
+	primaryExpr
+	| unary_op = (
+		PLUS
+		| MINUS
+		| EXCLAMATION
+		| CARET
+		| STAR
+		| AMPERSAND
+		| RECEIVE
+	) expression
+	| expression mul_op = (
+		STAR
+		| DIV
+		| MOD
+		| LSHIFT
+		| RSHIFT
+		| AMPERSAND
+		| BIT_CLEAR
+	) expression
+	| expression add_op = (PLUS | MINUS | OR | CARET) expression
+	| expression rel_op = (
+		EQUALS
+		| NOT_EQUALS
+		| LESS
+		| LESS_OR_EQUALS
+		| GREATER
+		| GREATER_OR_EQUALS
+	) expression
+	| expression LOGICAL_AND expression
+	| expression LOGICAL_OR expression;
+
+primaryExpr:
+	operand
+	| conversion
+	| methodExpr
+	| primaryExpr (
+		(DOT IDENTIFIER)
+		| index
+		| slice_
+		| typeAssertion
+		| arguments
+	);
+
+
+conversion: nonNamedType L_PAREN expression COMMA? R_PAREN;
+
+nonNamedType: typeLit | L_PAREN nonNamedType R_PAREN;
+
+operand: literal | operandName | L_PAREN expression R_PAREN;
+
+literal: basicLit | compositeLit | functionLit;
+
+basicLit:
+	NIL_LIT
+	| integer
+	| string_
+	| FLOAT_LIT;
+
+integer:
+	DECIMAL_LIT
+	| BINARY_LIT
+	| OCTAL_LIT
+	| HEX_LIT
+	| IMAGINARY_LIT
+	| RUNE_LIT;
+
+operandName: IDENTIFIER;
+
+qualifiedIdent: IDENTIFIER DOT IDENTIFIER;
+
+compositeLit: literalType literalValue;
+
+literalType:
+	structType
+	| arrayType
+	| L_BRACKET ELLIPSIS R_BRACKET elementType
+	| sliceType
+	| mapType
+	| typeName;
+
+literalValue: L_CURLY (elementList COMMA?)? R_CURLY;
+
+elementList: keyedElement (COMMA keyedElement)*;
+
+keyedElement: (key COLON)? element;
+
+key: expression | literalValue;
+
+element: expression | literalValue;
+
+structType: STRUCT L_CURLY (fieldDecl eos)* R_CURLY;
+
+fieldDecl: (
+		identifierList type_
+		| embeddedField
+	) tag = string_?;
+
+string_: RAW_STRING_LIT | INTERPRETED_STRING_LIT;
+
+embeddedField: STAR? typeName;
+
+functionLit: FUNC signature block; // function
+
+index: L_BRACKET expression R_BRACKET;
+
+slice_:
+	L_BRACKET (
+		expression? COLON expression?
+		| expression? COLON expression COLON expression
+	) R_BRACKET;
+
+typeAssertion: DOT L_PAREN type_ R_PAREN;
+
+arguments:
+	L_PAREN (
+		(expressionList | nonNamedType (COMMA expressionList)?) ELLIPSIS? COMMA?
+	)? R_PAREN;
+
+methodExpr: nonNamedType DOT IDENTIFIER;
+
+//receiverType: typeName | '(' ('*' typeName | receiverType) ')';
+
+receiverType: type_;
+
+eos:
+	SEMI
+	| EOF
+	| EOS
+	| {closingBracket()}?
+	;

+ 59 - 0
jplag.frontend.golang/src/main/java/de/jplag/golang/GoParserAdapter.java

@@ -0,0 +1,59 @@
+package de.jplag.golang;
+
+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.TokenList;
+import de.jplag.golang.grammar.GoLexer;
+import de.jplag.golang.grammar.GoParser;
+
+public class GoParserAdapter extends AbstractParser {
+    private String currentFile;
+    private TokenList tokens;
+
+    public TokenList parse(File directory, String[] fileNames) {
+        tokens = new TokenList();
+        for (String file : fileNames) {
+            if (!parseFile(directory, file)) {
+                errors++;
+            }
+        }
+        return tokens;
+    }
+
+    private boolean parseFile(File directory, String fileName) {
+        File file = new File(directory, fileName);
+        try (FileInputStream inputStream = new FileInputStream(file)) {
+            currentFile = fileName;
+
+            GoLexer lexer = new GoLexer(CharStreams.fromStream(inputStream));
+            CommonTokenStream tokenStream = new CommonTokenStream(lexer);
+            GoParser parser = new GoParser(tokenStream);
+
+            ParserRuleContext entryContext = parser.sourceFile();
+            ParseTreeWalker treeWalker = new ParseTreeWalker();
+
+            JPlagGoListener listener = new JPlagGoListener(this);
+            for (int i = 0; i < entryContext.getChildCount(); i++) {
+                ParseTree parseTree = entryContext.getChild(i);
+                treeWalker.walk(listener, parseTree);
+            }
+        } catch (IOException exception) {
+            logger.error("Parsing Error in '%s':".formatted(fileName), exception);
+            return false;
+        }
+        return true;
+    }
+
+    public void addToken(int tokenType, int line, int column, int length) {
+        tokens.addToken(new GoToken(tokenType, currentFile, line, column, length));
+    }
+}

+ 115 - 0
jplag.frontend.golang/src/main/java/de/jplag/golang/GoToken.java

@@ -0,0 +1,115 @@
+package de.jplag.golang;
+
+import static de.jplag.golang.GoTokenConstants.*;
+
+import de.jplag.Token;
+
+public class GoToken extends Token {
+
+    public GoToken(int type, String file, int line, int column, int length) {
+        super(type, file, line, column, length);
+    }
+
+    @Override
+    protected String type2string() {
+        return switch (type) {
+            case FILE_END -> "<EOF>";
+            case SEPARATOR_TOKEN -> "---------";
+
+            case PACKAGE -> "PACKAGE";
+            case IMPORT_CLAUSE -> "IMPORT";
+            case IMPORT_CLAUSE_BEGIN -> "IMPORT(";
+            case IMPORT_CLAUSE_END -> ")IMPORT";
+            case IMPORT_DECLARATION -> "IMPORT_DECL";
+
+            case ARRAY_BODY_BEGIN -> "ARRAY{";
+            case ARRAY_BODY_END -> "}ARRAY";
+
+            case STRUCT_DECLARATION -> "STRUCT";
+            case STRUCT_BODY_BEGIN -> "STRUCT{";
+            case STRUCT_BODY_END -> "}STRUCT";
+
+            case INTERFACE_DECLARATION -> "INTERFACE";
+            case INTERFACE_BLOCK_BEGIN -> "INTERFACE{";
+            case INTERFACE_BLOCK_END -> "}INTERFACE";
+
+            case INTERFACE_METHOD -> "INTERFACE_METHOD";
+            case TYPE_CONSTRAINT -> "TYPE_CONSTRAINT";
+            case TYPE_ASSERTION -> "TYPE_ASSERTION";
+
+            case MAP_BODY_BEGIN -> "MAP{";
+            case MAP_BODY_END -> "}MAP";
+
+            case SLICE_BODY_BEGIN -> "SLICE{";
+            case SLICE_BODY_END -> "}SLICE";
+
+            case NAMED_TYPE_BODY_BEGIN -> "CTYPE{";
+            case NAMED_TYPE_BODY_END -> "}CTYPE";
+
+            case MEMBER_DECLARATION -> "FIELD";
+
+            // Functions and Methods
+
+            case FUNCTION_DECLARATION -> "FUNC";
+            case RECEIVER -> "RECEIVER";
+            case FUNCTION_PARAMETER -> "PARAM";
+            case FUNCTION_BODY_BEGIN -> "FUNC{";
+            case FUNCTION_BODY_END -> "}FUNC";
+
+            // Control flow statements
+
+            case IF_STATEMENT -> "IF";
+
+            case IF_BLOCK_BEGIN -> "IF{";
+            case IF_BLOCK_END -> "}IF";
+            case ELSE_BLOCK_BEGIN -> "ELSE{";
+            case ELSE_BLOCK_END -> "}ELSE";
+            case FOR_STATEMENT -> "FOR";
+            case FOR_BLOCK_BEGIN -> "FOR{";
+            case FOR_BLOCK_END -> "}FOR";
+            case SWITCH_STATEMENT -> "SWITCH";
+            case SWITCH_BLOCK_BEGIN -> "SWITCH{";
+            case SWITCH_BLOCK_END -> "}SWITCH";
+            case SWITCH_CASE -> "CASE";
+            case SELECT_STATEMENT -> "SELECT";
+            case SELECT_BLOCK_BEGIN -> "SELECT{";
+            case SELECT_BLOCK_END -> "}SELECT";
+            case CASE_BLOCK_BEGIN -> "CASE{";
+            case CASE_BLOCK_END -> "}CASE";
+
+            // Statements
+
+            case VARIABLE_DECLARATION -> "VAR_DECL";
+            case FUNCTION_LITERAL -> "FUNC_LIT";
+            case ASSIGNMENT -> "ASSIGN";
+            case SEND_STATEMENT -> "SEND";
+            case RECEIVE_STATEMENT -> "RECV";
+            case INVOCATION -> "INVOC";
+            case ARGUMENT -> "ARG";
+            case STATEMENT_BLOCK_BEGIN -> "INNER{";
+            case STATEMENT_BLOCK_END -> "}INNER";
+
+            // Object Creation
+
+            case ARRAY_ELEMENT -> "ARRAY_ELEM";
+            case MAP_ELEMENT -> "MAP_ELEM";
+            case SLICE_ELEMENT -> "SLICE_ELEM";
+            case NAMED_TYPE_ELEMENT -> "CTYPE_ELEM";
+            case ARRAY_CONSTRUCTOR -> "ARRAY()";
+            case SLICE_CONSTRUCTOR -> "SLICE()";
+            case MAP_CONSTRUCTOR -> "MAP()";
+            case NAMED_TYPE_CONSTRUCTOR -> "CTYPE()";
+
+            // Control Flow Keywords
+
+            case RETURN -> "RETURN";
+            case BREAK -> "BREAK";
+            case CONTINUE -> "CONTINUE";
+            case FALLTHROUGH -> "FALLTHROUGH";
+            case GOTO -> "GOTO";
+            case GO -> "GO";
+            case DEFER -> "DEFER";
+            default -> "<UNKNOWN%d>".formatted(type);
+        };
+    }
+}

+ 96 - 0
jplag.frontend.golang/src/main/java/de/jplag/golang/GoTokenConstants.java

@@ -0,0 +1,96 @@
+package de.jplag.golang;
+
+import de.jplag.TokenConstants;
+
+public interface GoTokenConstants extends TokenConstants {
+
+    // TOP LEVEL STRUCTURES
+
+    int PACKAGE = 2;
+    int IMPORT_CLAUSE = 3;
+    int IMPORT_CLAUSE_BEGIN = 4;
+    int IMPORT_CLAUSE_END = 5;
+    int IMPORT_DECLARATION = 6;
+    int ARRAY_BODY_BEGIN = 7;
+    int ARRAY_BODY_END = 8;
+    int STRUCT_DECLARATION = 9;
+    int STRUCT_BODY_BEGIN = 10;
+    int STRUCT_BODY_END = 11;
+    int INTERFACE_DECLARATION = 12;
+    int INTERFACE_BLOCK_BEGIN = 13;
+    int INTERFACE_BLOCK_END = 14;
+    int INTERFACE_METHOD = 15;
+    int TYPE_CONSTRAINT = 16;
+    int TYPE_ASSERTION = 17;
+    int MAP_BODY_BEGIN = 18;
+    int MAP_BODY_END = 19;
+    int SLICE_BODY_BEGIN = 20;
+    int SLICE_BODY_END = 21;
+    int NAMED_TYPE_BODY_BEGIN = 22;
+    int NAMED_TYPE_BODY_END = 23;
+    int MEMBER_DECLARATION = 24;
+
+    // FUNCTIONS AND METHODS
+
+    int FUNCTION_DECLARATION = 25;
+    int RECEIVER = 26;
+    int FUNCTION_PARAMETER = 27;
+    int FUNCTION_BODY_BEGIN = 28;
+    int FUNCTION_BODY_END = 29;
+
+    // CONTROL FLOW STATEMENTS
+
+    int IF_STATEMENT = 30;
+    int IF_BLOCK_BEGIN = 31;
+    int IF_BLOCK_END = 32;
+    int ELSE_BLOCK_BEGIN = 33;
+    int ELSE_BLOCK_END = 34;
+    int FOR_STATEMENT = 35;
+    int FOR_BLOCK_BEGIN = 36;
+    int FOR_BLOCK_END = 37;
+    int SWITCH_STATEMENT = 38;
+    int SWITCH_BLOCK_BEGIN = 39;
+    int SWITCH_BLOCK_END = 40;
+    int SWITCH_CASE = 41;
+    int SELECT_STATEMENT = 42;
+    int SELECT_BLOCK_BEGIN = 43;
+    int SELECT_BLOCK_END = 44;
+    int CASE_BLOCK_BEGIN = 45;
+    int CASE_BLOCK_END = 46;
+
+    // STATEMENTS
+
+    int VARIABLE_DECLARATION = 47;
+    int FUNCTION_LITERAL = 48;
+    int ASSIGNMENT = 49;
+    int SEND_STATEMENT = 50;
+    int RECEIVE_STATEMENT = 51;
+    int INVOCATION = 52;
+    int ARGUMENT = 53;
+    int STATEMENT_BLOCK_BEGIN = 54;
+    int STATEMENT_BLOCK_END = 55;
+
+    // OBJECT CREATION
+
+    int ARRAY_ELEMENT = 56;
+    int MAP_ELEMENT = 57;
+    int SLICE_ELEMENT = 58;
+    int NAMED_TYPE_ELEMENT = 59;
+    int ARRAY_CONSTRUCTOR = 60;
+    int MAP_CONSTRUCTOR = 61;
+    int SLICE_CONSTRUCTOR = 62;
+    int NAMED_TYPE_CONSTRUCTOR = 63;
+
+    // CONTROL FLOW KEYWORDS
+
+    int RETURN = 64;
+    int BREAK = 65;
+    int CONTINUE = 66;
+    int FALLTHROUGH = 67;
+    int GOTO = 68;
+    int GO = 69;
+    int DEFER = 70;
+
+    int NUM_DIFF_TOKENS = 71;
+
+}

+ 535 - 0
jplag.frontend.golang/src/main/java/de/jplag/golang/JPlagGoListener.java

@@ -0,0 +1,535 @@
+package de.jplag.golang;
+
+import static de.jplag.golang.GoTokenConstants.*;
+
+import java.util.*;
+
+import org.antlr.v4.runtime.Token;
+import org.antlr.v4.runtime.tree.TerminalNode;
+
+import de.jplag.golang.grammar.GoParser;
+import de.jplag.golang.grammar.GoParserBaseListener;
+
+public class JPlagGoListener extends GoParserBaseListener {
+
+    public static final int NONE = -1;
+    private final GoParserAdapter parserAdapter;
+    private final Deque<GoBlockContext> blockContexts;
+
+    public JPlagGoListener(GoParserAdapter parserAdapter) {
+        this.parserAdapter = parserAdapter;
+        blockContexts = new LinkedList<>();
+    }
+
+    /**
+     * Passes a token of the given tokenType to the parserAdapter, representing the grammar's token given by token.
+     * @param tokenType the custom token type that occurred.
+     * @param token the corresponding grammar's token
+     */
+    private void transformToken(int tokenType, Token token) {
+        parserAdapter.addToken(tokenType, token.getLine(), token.getCharPositionInLine() + 1, token.getText().length());
+    }
+
+    /**
+     * Passes a token of the given tokenType to the parserAdapter, representing the current grammatical context given by
+     * start and end.
+     * @param tokenType the custom token type that occurred.
+     * @param start the first Token of the context
+     * @param end the last Token of the context
+     */
+    private void transformToken(int tokenType, Token start, Token end) {
+        parserAdapter.addToken(tokenType, start.getLine(), start.getCharPositionInLine() + 1, end.getStopIndex() - start.getStartIndex() + 1);
+    }
+
+    private void enterContext(GoBlockContext context) {
+        blockContexts.push(context);
+    }
+
+    private void expectAndLeave(GoBlockContext... contexts) {
+        GoBlockContext topContext = blockContexts.pop();
+        assert Arrays.stream(contexts).anyMatch(context -> context == topContext);
+    }
+
+    private GoBlockContext getCurrentContext() {
+        return blockContexts.peek();
+    }
+
+    /* TOP LEVEL STRUCTURES */
+
+    @Override
+    public void enterPackageClause(GoParser.PackageClauseContext context) {
+        transformToken(PACKAGE, context.getStart(), context.getStop());
+        super.enterPackageClause(context);
+    }
+
+    @Override
+    public void enterImportDecl(GoParser.ImportDeclContext context) {
+        transformToken(IMPORT_DECLARATION, context.getStart());
+
+        // if the children contain TerminalNodes, then it must be '(' and ')'
+        Optional<TerminalNode> listStart = context.children.stream().filter(TerminalNode.class::isInstance).map(TerminalNode.class::cast).findFirst();
+        listStart.ifPresent(lParenTree -> transformToken(IMPORT_CLAUSE_BEGIN, lParenTree.getSymbol()));
+
+        super.enterImportDecl(context);
+    }
+
+    @Override
+    public void exitImportDecl(GoParser.ImportDeclContext context) {
+        if (context.getStop().getText().equals(")")) {
+            transformToken(IMPORT_CLAUSE_END, context.getStop());
+        }
+        super.exitImportDecl(context);
+    }
+
+    @Override
+    public void enterImportSpec(GoParser.ImportSpecContext context) {
+        transformToken(IMPORT_CLAUSE, context.getStart(), context.getStop());
+        super.enterImportSpec(context);
+    }
+
+    /* INTERFACE */
+
+    @Override
+    public void enterInterfaceType(GoParser.InterfaceTypeContext context) {
+        transformToken(INTERFACE_DECLARATION, context.getStart());
+        enterContext(GoBlockContext.INTERFACE_BODY);
+        super.enterInterfaceType(context);
+    }
+
+    @Override
+    public void exitInterfaceType(GoParser.InterfaceTypeContext context) {
+        expectAndLeave(GoBlockContext.INTERFACE_BODY);
+        super.exitInterfaceType(context);
+    }
+
+    /* STRUCT */
+
+    @Override
+    public void enterStructType(GoParser.StructTypeContext context) {
+        transformToken(STRUCT_DECLARATION, context.getStart());
+        enterContext(GoBlockContext.STRUCT_BODY);
+        super.enterStructType(context);
+    }
+
+    @Override
+    public void exitStructType(GoParser.StructTypeContext context) {
+        expectAndLeave(GoBlockContext.STRUCT_BODY);
+        super.exitStructType(context);
+    }
+
+    @Override
+    public void enterFieldDecl(GoParser.FieldDeclContext context) {
+        transformToken(MEMBER_DECLARATION, context.getStart(), context.getStop());
+        super.enterFieldDecl(context);
+    }
+
+    /* FUNCTION */
+
+    @Override
+    public void enterFunctionDecl(GoParser.FunctionDeclContext context) {
+        transformToken(FUNCTION_DECLARATION, context.getStart());
+        enterContext(GoBlockContext.FUNCTION_BODY);
+        super.enterFunctionDecl(context);
+    }
+
+    @Override
+    public void exitFunctionDecl(GoParser.FunctionDeclContext context) {
+        expectAndLeave(GoBlockContext.FUNCTION_BODY);
+        super.exitFunctionDecl(context);
+    }
+
+    @Override
+    public void enterMethodDecl(GoParser.MethodDeclContext context) {
+        transformToken(FUNCTION_DECLARATION, context.getStart());
+        enterContext(GoBlockContext.FUNCTION_BODY);
+        super.enterMethodDecl(context);
+    }
+
+    @Override
+    public void exitMethodDecl(GoParser.MethodDeclContext context) {
+        expectAndLeave(GoBlockContext.FUNCTION_BODY);
+        super.exitMethodDecl(context);
+    }
+
+    @Override
+    public void enterParameterDecl(GoParser.ParameterDeclContext context) {
+        if (context.parent.parent instanceof GoParser.ReceiverContext) {
+            transformToken(RECEIVER, context.getStart(), context.getStop());
+        } else {
+            transformToken(FUNCTION_PARAMETER, context.getStart(), context.getStop());
+        }
+        super.enterParameterDecl(context);
+    }
+
+    /* CONTROL FLOW STATEMENTS */
+
+    @Override
+    public void enterIfStmt(GoParser.IfStmtContext context) {
+        transformToken(IF_STATEMENT, context.getStart());
+        enterContext(GoBlockContext.IF_BLOCK);
+        super.enterIfStmt(context);
+    }
+
+    @Override
+    public void exitIfStmt(GoParser.IfStmtContext context) {
+        expectAndLeave(GoBlockContext.IF_BLOCK, GoBlockContext.ELSE_BLOCK);
+        super.exitIfStmt(context);
+    }
+
+    @Override
+    public void enterForStmt(GoParser.ForStmtContext context) {
+        transformToken(FOR_STATEMENT, context.getStart());
+        enterContext(GoBlockContext.FOR_BLOCK);
+        super.enterForStmt(context);
+    }
+
+    @Override
+    public void exitForStmt(GoParser.ForStmtContext context) {
+        expectAndLeave(GoBlockContext.FOR_BLOCK);
+        super.exitForStmt(context);
+    }
+
+    @Override
+    public void enterSwitchStmt(GoParser.SwitchStmtContext context) {
+        transformToken(SWITCH_STATEMENT, context.getStart());
+        enterContext(GoBlockContext.SWITCH_BLOCK);
+        super.enterSwitchStmt(context);
+    }
+
+    @Override
+    public void exitSwitchStmt(GoParser.SwitchStmtContext context) {
+        expectAndLeave(GoBlockContext.SWITCH_BLOCK);
+        super.exitSwitchStmt(context);
+    }
+
+    @Override
+    public void enterExprCaseClause(GoParser.ExprCaseClauseContext context) {
+        transformToken(SWITCH_CASE, context.getStart());
+        var caseBlock = context.getChild(GoParser.StatementListContext.class, 0);
+        if (caseBlock != null) {
+            enterContext(GoBlockContext.CASE_BLOCK);
+            transformToken(CASE_BLOCK_BEGIN, caseBlock.getStart());
+        }
+        super.enterExprCaseClause(context);
+    }
+
+    @Override
+    public void exitExprCaseClause(GoParser.ExprCaseClauseContext context) {
+        if (getCurrentContext() == GoBlockContext.CASE_BLOCK) {
+            transformToken(CASE_BLOCK_END, context.getStop());
+            expectAndLeave(GoBlockContext.CASE_BLOCK);
+        }
+        super.exitExprCaseClause(context);
+    }
+
+    @Override
+    public void enterTypeCaseClause(GoParser.TypeCaseClauseContext context) {
+        transformToken(SWITCH_CASE, context.getStart());
+        var caseBlock = context.getChild(GoParser.StatementListContext.class, 0);
+        if (caseBlock != null) {
+            enterContext(GoBlockContext.CASE_BLOCK);
+            transformToken(CASE_BLOCK_BEGIN, caseBlock.getStart());
+        }
+        super.enterTypeCaseClause(context);
+    }
+
+    @Override
+    public void exitTypeCaseClause(GoParser.TypeCaseClauseContext context) {
+        if (getCurrentContext() == GoBlockContext.CASE_BLOCK) {
+            transformToken(CASE_BLOCK_END, context.getStop());
+            expectAndLeave(GoBlockContext.CASE_BLOCK);
+        }
+        super.exitTypeCaseClause(context);
+    }
+
+    @Override
+    public void enterSelectStmt(GoParser.SelectStmtContext context) {
+        transformToken(SELECT_STATEMENT, context.getStart());
+        enterContext(GoBlockContext.SELECT_CONTEXT);
+        super.enterSelectStmt(context);
+    }
+
+    @Override
+    public void exitSelectStmt(GoParser.SelectStmtContext context) {
+        expectAndLeave(GoBlockContext.SELECT_CONTEXT);
+        super.exitSelectStmt(context);
+    }
+
+    @Override
+    public void enterCommCase(GoParser.CommCaseContext context) {
+        transformToken(SWITCH_CASE, context.getStart());
+        var caseBlock = context.getChild(GoParser.StatementListContext.class, 0);
+        if (caseBlock != null) {
+            enterContext(GoBlockContext.CASE_BLOCK);
+            transformToken(CASE_BLOCK_BEGIN, caseBlock.getStart());
+        }
+        super.enterCommCase(context);
+    }
+
+    @Override
+    public void exitCommCase(GoParser.CommCaseContext context) {
+        if (getCurrentContext() == GoBlockContext.CASE_BLOCK) {
+            transformToken(CASE_BLOCK_END, context.getStop());
+            expectAndLeave(GoBlockContext.CASE_BLOCK);
+        }
+        super.exitCommCase(context);
+    }
+
+    /* STATEMENTS */
+
+    @Override
+    public void enterVarDecl(GoParser.VarDeclContext context) {
+        transformToken(VARIABLE_DECLARATION, context.getStart(), context.getStop());
+        super.enterVarDecl(context);
+    }
+
+    @Override
+    public void enterConstSpec(GoParser.ConstSpecContext context) {
+        transformToken(VARIABLE_DECLARATION, context.getStart());
+        super.enterConstSpec(context);
+    }
+
+    @Override
+    public void enterFunctionLit(GoParser.FunctionLitContext context) {
+        transformToken(FUNCTION_LITERAL, context.getStart());
+        enterContext(GoBlockContext.FUNCTION_BODY);
+        super.enterFunctionLit(context);
+    }
+
+    @Override
+    public void exitFunctionLit(GoParser.FunctionLitContext context) {
+        expectAndLeave(GoBlockContext.FUNCTION_BODY);
+        super.exitFunctionLit(context);
+    }
+
+    @Override
+    public void enterAssignment(GoParser.AssignmentContext context) {
+        transformToken(ASSIGNMENT, context.getStart(), context.getStop());
+        super.enterAssignment(context);
+    }
+
+    @Override
+    public void enterShortVarDecl(GoParser.ShortVarDeclContext context) {
+        transformToken(VARIABLE_DECLARATION, context.getStart());
+        transformToken(ASSIGNMENT, context.getStart());
+        super.enterShortVarDecl(context);
+    }
+
+    @Override
+    public void enterArguments(GoParser.ArgumentsContext context) {
+        transformToken(INVOCATION, context.getStart(), context.getStop());
+
+        // Arguments consist of ExpressionLists, which consist of Expressions
+        // Get all Expressions of all ExpressionLists in this ArgumentsContext
+        context.getRuleContexts(GoParser.ExpressionListContext.class).stream()
+                .flatMap(child -> child.getRuleContexts(GoParser.ExpressionContext.class).stream())
+                .forEachOrdered(arg -> transformToken(ARGUMENT, arg.getStart(), arg.getStop()));
+        super.enterArguments(context);
+    }
+
+    @Override
+    public void enterStatement(GoParser.StatementContext context) {
+        enterContext(GoBlockContext.STATEMENT_BLOCK);
+        super.enterStatement(context);
+    }
+
+    @Override
+    public void exitStatement(GoParser.StatementContext context) {
+        expectAndLeave(GoBlockContext.STATEMENT_BLOCK);
+        super.exitStatement(context);
+    }
+
+    /* OBJECT CREATION */
+
+    @Override
+    public void enterKeyedElement(GoParser.KeyedElementContext context) {
+        int tokenType = getCurrentContext().getElement();
+        transformToken(tokenType, context.getStart(), context.getStop());
+
+        super.enterKeyedElement(context);
+    }
+
+    @Override
+    public void enterArrayType(GoParser.ArrayTypeContext context) {
+        // otherwise, it is just a type expression
+        if (context.parent.parent instanceof GoParser.CompositeLitContext) {
+            enterContext(GoBlockContext.ARRAY_BODY);
+            transformToken(ARRAY_CONSTRUCTOR, context.getStart(), context.getStop());
+        }
+        super.enterArrayType(context);
+    }
+
+    @Override
+    public void enterSliceType(GoParser.SliceTypeContext context) {
+        // otherwise, it is just a type expression
+        if (context.parent.parent instanceof GoParser.CompositeLitContext) {
+            enterContext(GoBlockContext.SLICE_BODY);
+            transformToken(SLICE_CONSTRUCTOR, context.getStart(), context.getStop());
+        }
+        super.enterSliceType(context);
+    }
+
+    @Override
+    public void exitCompositeLit(GoParser.CompositeLitContext context) {
+        expectAndLeave(GoBlockContext.MAP_BODY, GoBlockContext.SLICE_BODY, GoBlockContext.ARRAY_BODY, GoBlockContext.NAMED_TYPE_BODY);
+        super.exitCompositeLit(context);
+    }
+
+    @Override
+    public void enterMapType(GoParser.MapTypeContext context) {
+        // otherwise, it is just a type expression
+        if (context.parent.parent instanceof GoParser.CompositeLitContext) {
+            enterContext(GoBlockContext.MAP_BODY);
+            transformToken(MAP_CONSTRUCTOR, context.getStart(), context.getStop());
+        }
+        super.enterMapType(context);
+    }
+
+    @Override
+    public void enterTypeName(GoParser.TypeNameContext context) {
+        if (context.parent.parent instanceof GoParser.CompositeLitContext) {
+            transformToken(NAMED_TYPE_CONSTRUCTOR, context.getStart());
+            enterContext(GoBlockContext.NAMED_TYPE_BODY);
+        } else if (context.parent instanceof GoParser.InterfaceTypeContext) {
+            transformToken(TYPE_CONSTRAINT, context.getStart(), context.getStop());
+        }
+        super.enterTypeName(context);
+    }
+
+    @Override
+    public void enterTypeAssertion(GoParser.TypeAssertionContext context) {
+        transformToken(TYPE_ASSERTION, context.getStart(), context.getStop());
+        super.enterTypeAssertion(context);
+    }
+
+    @Override
+    public void enterMethodSpec(GoParser.MethodSpecContext context) {
+        transformToken(INTERFACE_METHOD, context.getStart(), context.getStop());
+        super.enterMethodSpec(context);
+    }
+
+    /* CONTROL FLOW KEYWORDS */
+
+    @Override
+    public void enterReturnStmt(GoParser.ReturnStmtContext context) {
+        transformToken(RETURN, context.getStart(), context.getStop());
+        super.enterReturnStmt(context);
+    }
+
+    @Override
+    public void enterBreakStmt(GoParser.BreakStmtContext context) {
+        transformToken(BREAK, context.getStart(), context.getStop());
+        super.enterBreakStmt(context);
+    }
+
+    @Override
+    public void enterContinueStmt(GoParser.ContinueStmtContext context) {
+        transformToken(CONTINUE, context.getStart(), context.getStop());
+        super.enterContinueStmt(context);
+    }
+
+    @Override
+    public void enterFallthroughStmt(GoParser.FallthroughStmtContext context) {
+        transformToken(FALLTHROUGH, context.getStart(), context.getStop());
+        super.enterFallthroughStmt(context);
+    }
+
+    @Override
+    public void enterGotoStmt(GoParser.GotoStmtContext context) {
+        transformToken(GOTO, context.getStart(), context.getStop());
+        super.enterGotoStmt(context);
+    }
+
+    @Override
+    public void enterGoStmt(GoParser.GoStmtContext context) {
+        transformToken(GO, context.getStart(), context.getStop());
+        super.enterGoStmt(context);
+    }
+
+    @Override
+    public void enterDeferStmt(GoParser.DeferStmtContext context) {
+        transformToken(DEFER, context.getStart(), context.getStop());
+        super.enterDeferStmt(context);
+    }
+
+    @Override
+    public void enterSendStmt(GoParser.SendStmtContext ctx) {
+        transformToken(SEND_STATEMENT, ctx.getStart(), ctx.getStop());
+        super.enterSendStmt(ctx);
+    }
+
+    @Override
+    public void enterRecvStmt(GoParser.RecvStmtContext ctx) {
+        transformToken(RECEIVE_STATEMENT, ctx.getStart(), ctx.getStop());
+        super.enterRecvStmt(ctx);
+    }
+
+    @Override
+    public void visitTerminal(TerminalNode node) {
+        Token token = node.getSymbol();
+        switch (token.getText()) {
+            case "else" -> {
+                expectAndLeave(GoBlockContext.IF_BLOCK);
+                enterContext(GoBlockContext.ELSE_BLOCK);
+            }
+            case "{" -> transformToken(getCurrentContext().getBegin(), token);
+            case "}" -> transformToken(getCurrentContext().getEnd(), token);
+            case "<EOF>" -> {
+                if (node.getParent() instanceof GoParser.SourceFileContext) {
+                    transformToken(FILE_END, token);
+                }
+            }
+            default -> {
+                // do nothing.
+            }
+        }
+        super.visitTerminal(node);
+    }
+
+    /**
+     * This enumeration provides sets of information regarding different types of nesting structures in Go. Each element is
+     * a tuple of a token for the beginning of a block, the end of the block, and optionally, for the elements contained.
+     * <p>
+     * As the Go parser does not differentiate between different kinds of blocks, a stack of these GoBlockContexts is
+     * required to be able to assign the correct token types for each block.
+     */
+    private enum GoBlockContext {
+        ARRAY_BODY(ARRAY_BODY_BEGIN, ARRAY_BODY_END, ARRAY_ELEMENT),
+        STRUCT_BODY(STRUCT_BODY_BEGIN, STRUCT_BODY_END, MEMBER_DECLARATION),
+        MAP_BODY(MAP_BODY_BEGIN, MAP_BODY_END, MAP_ELEMENT),
+        SLICE_BODY(SLICE_BODY_BEGIN, SLICE_BODY_END, SLICE_ELEMENT),
+        NAMED_TYPE_BODY(NAMED_TYPE_BODY_BEGIN, NAMED_TYPE_BODY_END, NAMED_TYPE_ELEMENT),
+        FUNCTION_BODY(FUNCTION_BODY_BEGIN, FUNCTION_BODY_END, NONE),
+
+        IF_BLOCK(IF_BLOCK_BEGIN, IF_BLOCK_END, NONE),
+        ELSE_BLOCK(ELSE_BLOCK_BEGIN, ELSE_BLOCK_END, NONE),
+        FOR_BLOCK(FOR_BLOCK_BEGIN, FOR_BLOCK_END, NONE),
+        SWITCH_BLOCK(SWITCH_BLOCK_BEGIN, SWITCH_BLOCK_END, NONE),
+        SELECT_CONTEXT(SELECT_BLOCK_BEGIN, SELECT_BLOCK_END, NONE),
+        STATEMENT_BLOCK(STATEMENT_BLOCK_BEGIN, STATEMENT_BLOCK_END, NONE),
+        CASE_BLOCK(CASE_BLOCK_BEGIN, CASE_BLOCK_END, NONE),
+        INTERFACE_BODY(INTERFACE_BLOCK_BEGIN, INTERFACE_BLOCK_END, NONE);
+
+        private final int beginTokenType;
+        private final int endTokenType;
+        private final int elementTokenType;
+
+        GoBlockContext(int beginTokenType, int endTokenType, int elementTokenType) {
+            this.beginTokenType = beginTokenType;
+            this.endTokenType = endTokenType;
+            this.elementTokenType = elementTokenType;
+        }
+
+        int getBegin() {
+            return this.beginTokenType;
+        }
+
+        int getEnd() {
+            return this.endTokenType;
+        }
+
+        public int getElement() {
+            return this.elementTokenType;
+        }
+    }
+}

+ 49 - 0
jplag.frontend.golang/src/main/java/de/jplag/golang/Language.java

@@ -0,0 +1,49 @@
+package de.jplag.golang;
+
+import java.io.File;
+
+import de.jplag.TokenList;
+
+public class Language implements de.jplag.Language {
+
+    private static final String NAME = "Go Parser";
+    private static final String SHORT_NAME = "Go";
+    private static final int DEFAULT_MIN_TOKEN_MATCH = 8;
+    private static final String[] FILE_EXTENSIONS = {".go"};
+    private final GoParserAdapter parserAdapter;
+
+    public Language() {
+        this.parserAdapter = new GoParserAdapter();
+    }
+
+    @Override
+    public String[] suffixes() {
+        return FILE_EXTENSIONS;
+    }
+
+    @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();
+    }
+
+}

+ 26 - 0
jplag.frontend.golang/src/main/java/de/jplag/golang/grammar/GoParserBase.java

@@ -0,0 +1,26 @@
+package de.jplag.golang.grammar;
+
+import org.antlr.v4.runtime.BufferedTokenStream;
+import org.antlr.v4.runtime.Parser;
+import org.antlr.v4.runtime.TokenStream;
+
+/**
+ * All parser methods that used in grammar (p, prev, notLineTerminator, etc.) should start with lower case char similar
+ * to parser rules. Taken from <a href="https://github.com/antlr/grammars-v4/tree/master/golang">the ANTLR4 Project
+ * grammars repository</a>. Licenced under BSD-3.
+ */
+public abstract class GoParserBase extends Parser {
+    protected GoParserBase(TokenStream input) {
+        super(input);
+    }
+
+    /**
+     * Returns true if the current Token is a closing bracket (")" or "}")
+     */
+    protected boolean closingBracket() {
+        BufferedTokenStream stream = (BufferedTokenStream) _input;
+        int prevTokenType = stream.LA(1);
+
+        return prevTokenType == GoParser.R_CURLY || prevTokenType == GoParser.R_PAREN;
+    }
+}

+ 175 - 0
jplag.frontend.golang/src/test/java/de/jplag/golang/GoFrontendTest.java

@@ -0,0 +1,175 @@
+package de.jplag.golang;
+
+import static de.jplag.golang.GoTokenTestUtils.getDummyToken;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.OptionalInt;
+import java.util.stream.Collectors;
+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;
+
+class GoFrontendTest {
+    /**
+     * 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.go";
+
+    /**
+     * Regular expression that describes lines consisting only of whitespace and optionally a line comment.
+     */
+    private static final String EMPTY_OR_SINGLE_LINE_COMMENT = "\\s*(//.*|/\\*.*\\*/)?";
+
+    /**
+     * Regular expression that describes lines containing the start of a multiline comment and no code before it.
+     */
+    private static final String DELIMITED_COMMENT_START = "\\s*/\\*(?:(?!\\*/).)*$";
+
+    /**
+     * Regular expression that describes lines containing the end of a multiline comment and no more code after that.
+     */
+    private static final String DELIMITED_COMMENT_END = ".*\\*/\\s*$";
+
+    private final Logger logger = LoggerFactory.getLogger("GoLang frontend test");
+    private final String[] testFiles = new String[] {COMPLETE_TEST_FILE};
+    private final File testFileLocation = Path.of("src", "test", "resources", "de", "jplag", "golang").toFile();
+    private Language language;
+
+    @BeforeEach
+    void setup() {
+        language = new Language();
+    }
+
+    @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 every type of GoToken has a Sting representation associated to it.
+     */
+    @Test
+    void testTokenToString() {
+        var missingTokens = IntStream.range(0, GoTokenConstants.NUM_DIFF_TOKENS).mapToObj(GoTokenTestUtils::getDummyToken)
+                .filter(token -> token.type2string().contains("UNKNOWN")).toList();
+
+        if (!missingTokens.isEmpty()) {
+            var typeList = missingTokens.stream().map(Token::getType).map(Object::toString).collect(Collectors.joining(", "));
+            fail("Found token types with no string representation: %s".formatted(typeList));
+        }
+
+    }
+
+    /**
+     * 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);
+
+        List<String> lines = null;
+        try {
+            lines = Files.readAllLines(testFile.toPath());
+        } catch (IOException exception) {
+            logger.info("Error while reading test file %s".formatted(fileName), exception);
+            fail();
+        }
+
+        // All lines that contain code
+        var codeLines = getCodeLines(lines);
+        // All lines that contain a token
+        var tokenLines = IntStream.range(0, tokens.size()).mapToObj(tokens::getToken).mapToInt(Token::getLine).distinct().boxed().toList();
+
+        if (codeLines.size() > tokenLines.size()) {
+            List<Integer> missedLinesIndices = new ArrayList<>(codeLines);
+            missedLinesIndices.removeAll(tokenLines);
+            var missedLines = missedLinesIndices.stream().map(Object::toString).collect(Collectors.joining(", "));
+            if (!missedLines.isBlank()) {
+                fail("Found lines in file '%s' that are not represented in the token list. \n\tMissed lines: %s".formatted(fileName, missedLines));
+            }
+        }
+        OptionalInt differingLine = IntStream.range(0, codeLines.size())
+                .dropWhile(index -> Objects.equals(codeLines.get(index), tokenLines.get(index))).findAny();
+        differingLine.ifPresent(
+                i -> fail("Not all lines of code in '%s' are represented in tokens, starting with line %d.".formatted(fileName, codeLines.get(i))));
+    }
+
+    /**
+     * Gets the line numbers of lines containing actual code, omitting empty lines and comment lines.
+     * @param lines lines of a code file
+     * @return an array of the line numbers of code lines
+     */
+    private List<Integer> getCodeLines(List<String> lines) {
+        // This boxed boolean can be accessed from within the lambda method below
+        var state = new Object() {
+            boolean insideComment = false;
+        };
+
+        var codeLines = IntStream.rangeClosed(1, lines.size()).sequential().filter(idx -> {
+            String line = lines.get(idx - 1);
+            if (line.matches(EMPTY_OR_SINGLE_LINE_COMMENT)) {
+                return false;
+            } else if (line.matches(DELIMITED_COMMENT_START)) {
+                state.insideComment = true;
+                return false;
+            } else if (state.insideComment) {
+                // This fails if code follows after '*/'. If the code is formatted well, this should not happen.
+                if (line.matches(DELIMITED_COMMENT_END)) {
+                    state.insideComment = false;
+                }
+                return false;
+            }
+            return true;
+        });
+
+        return codeLines.boxed().toList();
+
+    }
+
+    /**
+     * 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().boxed().toList();
+
+        // Exclude SEPARATOR_TOKEN, as it does not occur
+        var missingTokenTypes = IntStream.range(0, GoTokenConstants.NUM_DIFF_TOKENS).filter(i -> i != TokenConstants.SEPARATOR_TOKEN).boxed()
+                .collect(Collectors.toList());
+        missingTokenTypes.removeAll(foundTokens);
+
+        if (!missingTokenTypes.isEmpty()) {
+            String missingTypesList = missingTokenTypes.stream().map(type -> getDummyToken(type).type2string()).collect(Collectors.joining(", "));
+            fail("Some Token types were not found in the complete code example '%s': %s.".formatted(fileName, missingTypesList));
+        }
+
+    }
+
+}

+ 20 - 0
jplag.frontend.golang/src/test/java/de/jplag/golang/GoTokenTestUtils.java

@@ -0,0 +1,20 @@
+package de.jplag.golang;
+
+public class GoTokenTestUtils {
+
+    private static final int NOT_SET = -1;
+    private static final String EMPTY_STRING = "";
+
+    private GoTokenTestUtils() {
+        // Utility class, should not be instantiated.
+    }
+
+    public static GoToken getDummyToken(int type) {
+        return new GoToken(type, EMPTY_STRING, NOT_SET, NOT_SET, NOT_SET);
+    }
+
+    public static GoToken getDummyToken(int type, String fileName) {
+        return new GoToken(type, fileName, NOT_SET, NOT_SET, NOT_SET);
+    }
+
+}

+ 167 - 0
jplag.frontend.golang/src/test/resources/de/jplag/golang/Complete.go

@@ -0,0 +1,167 @@
+package goExample
+
+import (
+	"fmt"
+	"strings"
+)
+
+type myStruct struct {
+	int number
+	string name
+
+	int	[]data
+}
+
+func (this myStruct) init(number int, name string, data []int) {
+	this.number = number
+	this.name = name
+	this.data = data
+}
+
+func (this myStruct) print() {
+	const (intro = "This is %s, myStruct number %d, which contains %d elements."; separator = "-------------------")
+
+	fmt.Printf(intro, this.name, this.number, len(this.data))
+	fmt.Println(separator)
+}
+
+func getNth(structs []myStruct, index i) myStruct {
+	if i < len(structs) {
+		return structs[i]
+	}
+
+	return nil
+}
+
+func printAll(structs []myStruct) {
+	for i := range structs {
+		var element = getNth(structs, i)
+		element.print()
+	}
+}
+
+func deferredFunction() {
+	fmt.println("This is printed even after panic.")
+}
+
+func panicExample() {
+	defer defferedFunction()
+	panic("This operation does not work!")
+}
+
+func switchExample(int dividend, int divisor) {
+	switch {
+		case divisor == 1: return dividend
+		case divisor == 0: panicExample()
+		case dividend == 0: return 0
+		default: return dividend / divisor
+	}
+}
+
+var isEven = map[int]bool{
+	0: true,
+	1: false,
+	2: true,
+	3: false,
+}
+
+func checkIsEven(value int) bool {
+	f := func(value int) bool {
+		if value >= 0 && value <= 3 {
+			return isEven[value]
+		} else {
+			return value % 2 == 0
+		}
+	}
+
+	return f(value)
+}
+
+func sliceExample() []int {
+	return []int{1, 2, 3}
+}
+
+func fallthroughExample(value int) {
+	switch {
+		case value > 10:
+			fmt.Println("Big number!")
+			fallthrough
+		case value > 100:
+			fmt.Println("Very big number!")
+	}
+}
+
+func createCType() {
+	var element = myStruct{
+		number: 1,
+		name: "myElement",
+		data: [5]int{1, 2, 3, 4, 5},
+	}
+
+}
+
+func createArray() *ast.File {
+	var array = [3]int { nil, nil, nil }
+	return array
+}
+
+func selectExample() int {
+	select {
+		case signal1 := <-ch1:
+			fmt.Println("Haha! ", signal1)
+			return signal1
+		case signal2 := <-ch2:
+			return signal2
+		default: return -1
+	}
+}
+
+func innerBlock() {
+	fmt.Println("Outer")
+	{
+		fmt.Println("Inner")
+	}
+}
+
+func continueExample() {
+	begin: for true {
+		if 1 > 0 {
+			continue;
+		}
+		break;
+	}
+	goto begin
+}
+
+func channelExample(ch chan<- int, value int) {
+	ch <- value
+}
+
+func goExample() {
+	ch := make(chan<- int)
+	go innerBlock(ch, 42)
+}
+
+type InterfaceExample interface {
+	f1(arg int) string
+	f2(arg string) int
+	MySuperType
+}
+
+type MySuperType interface {
+	superMethod()
+}
+
+type Implementation struct {}
+
+func (imp Implementation) f1(arg int) string {
+	return "A"
+}
+
+func (imp Implementation) f2(arg string) int {
+	return 1
+}
+
+func typeAssertion(InterfaceExample i) {
+	fmt.Println(i.(Implementation))
+}

+ 1 - 1
jplag.frontend.java/src/main/java/de/jplag/java/JavaTokenConstants.java

@@ -84,5 +84,5 @@ public interface JavaTokenConstants extends TokenConstants {
     int J_RECORD_BEGIN = 68;
     int J_RECORD_END = 69;
 
-    final static int NUM_DIFF_TOKENS = 70;	    // @formatter:on
+    int NUM_DIFF_TOKENS = 70;	    // @formatter:on
 }

+ 6 - 9
jplag.frontend.java/src/main/java/de/jplag/java/JavacAdapter.java

@@ -6,12 +6,10 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Paths;
 import java.util.Collections;
 
-import javax.tools.Diagnostic;
-import javax.tools.DiagnosticCollector;
-import javax.tools.JavaCompiler;
+import javax.tools.*;
 import javax.tools.JavaCompiler.CompilationTask;
-import javax.tools.StandardJavaFileManager;
-import javax.tools.ToolProvider;
+
+import org.slf4j.Logger;
 
 import com.sun.source.tree.CompilationUnitTree;
 import com.sun.source.tree.LineMap;
@@ -19,7 +17,6 @@ import com.sun.source.util.JavacTask;
 import com.sun.source.util.SourcePositions;
 import com.sun.source.util.Trees;
 
-import de.jplag.ErrorConsumer;
 import de.jplag.TokenConstants;
 
 public class JavacAdapter {
@@ -39,7 +36,7 @@ public class JavacAdapter {
             ast.accept(new TokenGeneratingTreeScanner(filename, parser, map, positions, ast), null);
             parser.add(TokenConstants.FILE_END, filename, 1, -1, -1);
         }
-        return processErrors(parser.getErrorConsumer(), listener);
+        return processErrors(parser.logger, listener);
     }
 
     private Iterable<? extends CompilationUnitTree> executeCompilationTask(final CompilationTask task) {
@@ -60,11 +57,11 @@ public class JavacAdapter {
         }
     }
 
-    private int processErrors(final ErrorConsumer consumer, DiagnosticCollector<Object> listener) {
+    private int processErrors(Logger logger, DiagnosticCollector<Object> listener) {
         int errors = 0;
         for (Diagnostic<?> diagnosticItem : listener.getDiagnostics()) {
             if (diagnosticItem.getKind() == javax.tools.Diagnostic.Kind.ERROR) {
-                consumer.addError(diagnosticItem.toString());
+                logger.error("{}", diagnosticItem);
                 errors++;
             }
         }

+ 2 - 23
jplag.frontend.java/src/main/java/de/jplag/java/Language.java

@@ -3,7 +3,6 @@ package de.jplag.java;
 import java.io.File;
 import java.util.List;
 
-import de.jplag.ErrorConsumer;
 import de.jplag.Token;
 
 /**
@@ -12,8 +11,8 @@ import de.jplag.Token;
 public class Language implements de.jplag.Language {
     private final Parser parser;
 
-    public Language(ErrorConsumer errorConsumer) {
-        parser = new Parser(errorConsumer);
+    public Language() {
+        parser = new Parser();
     }
 
     @Override
@@ -36,26 +35,6 @@ public class Language implements de.jplag.Language {
         return 9;
     }
 
-    @Override
-    public boolean supportsColumns() {
-        return true;
-    }
-
-    @Override
-    public boolean isPreformatted() {
-        return true;
-    }
-
-    @Override
-    public boolean usesIndex() {
-        return false;
-    }
-
-    @Override
-    public int numberOfTokens() {
-        return JavaTokenConstants.NUM_DIFF_TOKENS;
-    }
-
     @Override
     public List<Token> parse(File directory, String[] files) {
         return this.parser.parse(directory, files);

+ 3 - 7
jplag.frontend.java/src/main/java/de/jplag/java/Parser.java

@@ -1,14 +1,11 @@
 package de.jplag.java;
 
-import static java.util.stream.Collectors.toList;
-
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
 import de.jplag.AbstractParser;
-import de.jplag.ErrorConsumer;
 import de.jplag.Token;
 
 public class Parser extends AbstractParser {
@@ -16,16 +13,15 @@ public class Parser extends AbstractParser {
 
     /**
      * Creates the parser.
-     * @param errorConsumer is the consumer for any occurring errors.
      */
-    public Parser(ErrorConsumer errorConsumer) {
-        super(errorConsumer);
+    public Parser() {
+        super();
     }
 
     public List<Token> parse(File directory, String[] files) {
         tokens = new ArrayList<>();
         errors = 0;
-        var pathedFiles = Arrays.stream(files).map(it -> new File(directory, it)).collect(toList());
+        var pathedFiles = Arrays.stream(files).map(it -> new File(directory, it)).toList();
         errors += new JavacAdapter().parseFiles(directory, pathedFiles, this);
         return tokens;
     }

+ 8 - 10
jplag.frontend.kotlin/src/main/java/de/jplag/kotlin/KotlinParserAdapter.java

@@ -13,9 +13,8 @@ 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.Token;
 import de.jplag.TokenConstants;
+import de.jplag.Token;
 import de.jplag.kotlin.grammar.KotlinLexer;
 import de.jplag.kotlin.grammar.KotlinParser;
 
@@ -27,17 +26,16 @@ public class KotlinParserAdapter extends AbstractParser {
 
     /**
      * Creates the KotlinParserAdapter
-     * @param consumer the ErrorConsumer that parser errors are passed on to.
      */
-    public KotlinParserAdapter(ErrorConsumer consumer) {
-        super(consumer);
+    public KotlinParserAdapter() {
+        super();
     }
 
     /**
-     * Parsers a list of files into a single list of {@link Token}s.
+     * 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 list containing all tokens of all files.
+     * @return a {@link TokenList} containing all tokens of all files.
      */
     public List<Token> parse(File directory, String[] fileNames) {
         tokens = new ArrayList<>();
@@ -68,15 +66,15 @@ public class KotlinParserAdapter extends AbstractParser {
                 treeWalker.walk(listener, parseTree);
             }
         } catch (IOException exception) {
-            getErrorConsumer().addError("Parsing Error in '%s': %s%s".formatted(fileName, File.separator, exception));
+            logger.error("Parsing Error in '{}': {}{}", fileName, File.separator, exception);
             return false;
         }
         return true;
     }
 
     /**
-     * Adds a new {@link Token} to the current token list.
-     * @param tokenType the type of the new {@link Token}
+     * Adds a new {@link de.jplag.Token} to the current {@link TokenList}.
+     * @param tokenType the type of the new {@link de.jplag.Token}
      * @param line the line of the Token in the current file
      * @param column the start column of the Token in the line
      * @param length the length of the Token

Some files were not shown because too many files changed in this diff