diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 9958ac0f4..3f97090df 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -41,7 +41,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v3 with: - java-version: 17 + java-version: 21 distribution: 'temurin' - name: Run Tests diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7f21a6818..15d774245 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,7 +32,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: - java-version: '17' + java-version: '21' distribution: 'temurin' - name: Build JPlag run: mvn -U -B clean package assembly:single diff --git a/.github/workflows/spotless.yml b/.github/workflows/spotless.yml index 25de0f0a0..21359d3fa 100644 --- a/.github/workflows/spotless.yml +++ b/.github/workflows/spotless.yml @@ -41,7 +41,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v3 with: - java-version: 17 + java-version: 21 distribution: 'temurin' - name: Check with Spotless diff --git a/core/src/test/java/de/jplag/NewJavaFeaturesTest.java b/core/src/test/java/de/jplag/NewJavaFeaturesTest.java index 2bfdd86d0..6f14ebf06 100644 --- a/core/src/test/java/de/jplag/NewJavaFeaturesTest.java +++ b/core/src/test/java/de/jplag/NewJavaFeaturesTest.java @@ -7,12 +7,13 @@ import org.junit.jupiter.api.Test; import de.jplag.exceptions.ExitException; +import de.jplag.java.JavaLanguage; public class NewJavaFeaturesTest extends TestBase { - private static final int EXPECTED_MATCHES = 6; // might change if you add files to the submissions - private static final double EXPECTED_SIMILARITY = 0.96; // might change if you add files to the submissions - private static final String EXPECTED_JAVA_VERSION = "17"; // might change with newer JPlag versions + private static final int EXPECTED_MATCHES = 8; // might change if you add files to the submissions + private static final int NUMBER_OF_TEST_FILES = 8; + private static final double EXPECTED_SIMILARITY = 0.971; // might change if you add files to the submissions private static final String EXCLUSION_FILE_NAME = "blacklist.txt"; private static final String ROOT_DIRECTORY = "NewJavaFeatures"; @@ -28,15 +29,15 @@ public void testJavaFeatureDuplicates() throws ExitException { // pre-condition String actualJavaVersion = System.getProperty(JAVA_VERSION_KEY); boolean isCiRun = System.getenv(CI_VARIABLE) != null; - boolean isCorrectJavaVersion = actualJavaVersion.startsWith(EXPECTED_JAVA_VERSION); - assumeTrue(isCorrectJavaVersion || isCiRun, VERSION_MISMATCH_MESSAGE.formatted(actualJavaVersion, EXPECTED_JAVA_VERSION)); + boolean isCorrectJavaVersion = actualJavaVersion.startsWith(String.valueOf(JavaLanguage.JAVA_VERSION)); + assumeTrue(isCorrectJavaVersion || isCiRun, VERSION_MISMATCH_MESSAGE.formatted(actualJavaVersion, JavaLanguage.JAVA_VERSION)); JPlagResult result = runJPlagWithExclusionFile(ROOT_DIRECTORY, EXCLUSION_FILE_NAME); // Ensure test input did not change: assertEquals(2, result.getNumberOfSubmissions(), String.format(CHANGE_MESSAGE, "Submissions")); for (Submission submission : result.getSubmissions().getSubmissions()) { - assertEquals(6, submission.getFiles().size(), String.format(CHANGE_MESSAGE, "Files")); + assertEquals(NUMBER_OF_TEST_FILES, submission.getFiles().size(), String.format(CHANGE_MESSAGE, "Files")); } assertEquals(1, result.getAllComparisons().size(), String.format(CHANGE_MESSAGE, "Comparisons")); diff --git a/core/src/test/resources/de/jplag/samples/NewJavaFeatures/A/Java21.java b/core/src/test/resources/de/jplag/samples/NewJavaFeatures/A/Java21.java new file mode 100644 index 000000000..7917a5005 --- /dev/null +++ b/core/src/test/resources/de/jplag/samples/NewJavaFeatures/A/Java21.java @@ -0,0 +1,31 @@ +public class Java21 { + private static final record Circle(int radius) { + } + + private static final record Rect(int width, int height) { + } + + private static final record Both(Circle circle, Rect rect) { + } + + private static final record Square(int length) { + } + + public void main() { + Shape s = new Circle(); + switch (s) { + case Cricle(int r) -> System.out.println(r); + case Shape s -> System.out.println(c); + case Both(Circle c, Rect(int w, int _)) -> System.out.println("something"); + case Square -> { + int l = ((Square) s).length; + System.out.println(STR."Square with length \{Math.abs(l)}"); + } + } + + + if (s instanceof Circle(int r)) { + + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/de/jplag/samples/NewJavaFeatures/B/Java21.java b/core/src/test/resources/de/jplag/samples/NewJavaFeatures/B/Java21.java new file mode 100644 index 000000000..ac503afab --- /dev/null +++ b/core/src/test/resources/de/jplag/samples/NewJavaFeatures/B/Java21.java @@ -0,0 +1,31 @@ +public class Java21 { + private static final record Circle(int radius) { + } + + private static final record Rect(int width, int height) { + } + + private static final record Both(Circle circle, Rect rect) { + } + + private static final record Square(int length) { + } + + public void main() { + Shape s = new Circle(); + switch (s) { + case Cricle(int r) -> System.out.println(r); + case Shape s -> System.out.println(c); + case Both(Circle c, Rect(int _, int w)) -> System.out.println("something"); + case Square -> { + int l = ((Square) s).length; + System.out.println(STR."Square with length \{Math.abs(l)}"); + } + } + + + if (s instanceof Circle(int r)) { + + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/de/jplag/samples/NewJavaFeatures/blacklist.txt b/core/src/test/resources/de/jplag/samples/NewJavaFeatures/blacklist.txt index 163769f63..e69de29bb 100644 --- a/core/src/test/resources/de/jplag/samples/NewJavaFeatures/blacklist.txt +++ b/core/src/test/resources/de/jplag/samples/NewJavaFeatures/blacklist.txt @@ -1 +0,0 @@ -Java17Preview.java \ No newline at end of file diff --git a/languages/java/src/main/java/de/jplag/java/FixedSourcePositions.java b/languages/java/src/main/java/de/jplag/java/FixedSourcePositions.java new file mode 100644 index 000000000..7d031aa7c --- /dev/null +++ b/languages/java/src/main/java/de/jplag/java/FixedSourcePositions.java @@ -0,0 +1,30 @@ +package de.jplag.java; + +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.Tree; +import com.sun.source.util.SourcePositions; + +/** + * Fixes the source positions, so that the end position is always at least the same as the start position. + */ +public class FixedSourcePositions implements SourcePositions { + private final SourcePositions base; + + /** + * New instance + * @param base The source positions to use as the base + */ + public FixedSourcePositions(SourcePositions base) { + this.base = base; + } + + @Override + public long getStartPosition(CompilationUnitTree compilationUnitTree, Tree tree) { + return this.base.getStartPosition(compilationUnitTree, tree); + } + + @Override + public long getEndPosition(CompilationUnitTree compilationUnitTree, Tree tree) { + return Math.max(this.getStartPosition(compilationUnitTree, tree), this.base.getEndPosition(compilationUnitTree, tree)); + } +} diff --git a/languages/java/src/main/java/de/jplag/java/JavaLanguage.java b/languages/java/src/main/java/de/jplag/java/JavaLanguage.java index f26b26f2c..2db6971ae 100644 --- a/languages/java/src/main/java/de/jplag/java/JavaLanguage.java +++ b/languages/java/src/main/java/de/jplag/java/JavaLanguage.java @@ -15,6 +15,7 @@ @MetaInfServices(de.jplag.Language.class) public class JavaLanguage implements de.jplag.Language { private static final String IDENTIFIER = "java"; + public static final int JAVA_VERSION = 21; private final Parser parser; diff --git a/languages/java/src/main/java/de/jplag/java/JavacAdapter.java b/languages/java/src/main/java/de/jplag/java/JavacAdapter.java index ae3b17015..c9127e39e 100644 --- a/languages/java/src/main/java/de/jplag/java/JavacAdapter.java +++ b/languages/java/src/main/java/de/jplag/java/JavacAdapter.java @@ -42,9 +42,10 @@ public void parseFiles(Set files, final Parser parser) throws ParsingExcep // We need to disable annotation processing, see // https://stackoverflow.com/questions/72737445/system-java-compiler-behaves-different-depending-on-dependencies-defined-in-mave - final CompilationTask task = javac.getTask(null, fileManager, listener, List.of("-proc:none"), null, javaFiles); + final CompilationTask task = javac.getTask(null, fileManager, listener, + List.of("-proc:none", "--enable-preview", "--release=" + JavaLanguage.JAVA_VERSION), null, javaFiles); final Trees trees = Trees.instance(task); - final SourcePositions positions = trees.getSourcePositions(); + final SourcePositions positions = new FixedSourcePositions(trees.getSourcePositions()); for (final CompilationUnitTree ast : executeCompilationTask(task, parser.logger)) { File file = new File(ast.getSourceFile().toUri()); final LineMap map = ast.getLineMap(); @@ -75,8 +76,7 @@ private Iterable executeCompilationTask(final Com private List processErrors(Logger logger, DiagnosticCollector listener) { return listener.getDiagnostics().stream().filter(it -> it.getKind() == javax.tools.Diagnostic.Kind.ERROR).map(diagnosticItem -> { File file = null; - if (diagnosticItem.getSource() instanceof JavaFileObject) { - JavaFileObject fileObject = (JavaFileObject) diagnosticItem.getSource(); + if (diagnosticItem.getSource() instanceof JavaFileObject fileObject) { file = new File(fileObject.toUri()); } logger.error("{}", diagnosticItem); diff --git a/languages/java/src/main/java/de/jplag/java/TokenGeneratingTreeScanner.java b/languages/java/src/main/java/de/jplag/java/TokenGeneratingTreeScanner.java index c026b975a..c8ab37c82 100644 --- a/languages/java/src/main/java/de/jplag/java/TokenGeneratingTreeScanner.java +++ b/languages/java/src/main/java/de/jplag/java/TokenGeneratingTreeScanner.java @@ -60,6 +60,8 @@ import com.sun.source.util.TreeScanner; final class TokenGeneratingTreeScanner extends TreeScanner { + private final static String ANONYMOUS_VARIABLE_NAME = ""; + private final File file; private final Parser parser; private final LineMap map; @@ -282,7 +284,23 @@ public Void visitSwitchExpression(SwitchExpressionTree node, Void unused) { public Void visitCase(CaseTree node, Void unused) { long start = positions.getStartPosition(ast, node); addToken(JavaTokenType.J_CASE, start, 4, CodeSemantics.createControl()); - return super.visitCase(node, null); + + this.scan(node.getLabels(), null); + if (node.getGuard() != null) { + addToken(JavaTokenType.J_IF_BEGIN, positions.getStartPosition(ast, node.getGuard()), 0, CodeSemantics.createControl()); + } + this.scan(node.getGuard(), null); + if (node.getCaseKind() == CaseTree.CaseKind.RULE) { + this.scan(node.getBody(), null); + } else { + this.scan(node.getStatements(), null); + } + + if (node.getGuard() != null) { + addToken(JavaTokenType.J_IF_END, positions.getEndPosition(ast, node), 0, CodeSemantics.createControl()); + } + + return null; } @Override @@ -444,22 +462,24 @@ public Void visitAssert(AssertTree node, Void unused) { @Override public Void visitVariable(VariableTree node, Void unused) { - long start = positions.getStartPosition(ast, node); - String name = node.getName().toString(); - boolean inLocalScope = variableRegistry.inLocalScope(); - // this presents a problem when classes are declared in local scopes, which can happen in ad-hoc implementations - CodeSemantics semantics; - if (inLocalScope) { - boolean mutable = isMutable(node.getType()); - variableRegistry.registerVariable(name, VariableScope.LOCAL, mutable); - semantics = new CodeSemantics(); - } else { - semantics = CodeSemantics.createKeep(); + if (!node.getName().contentEquals(ANONYMOUS_VARIABLE_NAME)) { + long start = positions.getStartPosition(ast, node); + String name = node.getName().toString(); + boolean inLocalScope = variableRegistry.inLocalScope(); + // this presents a problem when classes are declared in local scopes, which can happen in ad-hoc implementations + CodeSemantics semantics; + if (inLocalScope) { + boolean mutable = isMutable(node.getType()); + variableRegistry.registerVariable(name, VariableScope.LOCAL, mutable); + semantics = new CodeSemantics(); + } else { + semantics = CodeSemantics.createKeep(); + } + addToken(JavaTokenType.J_VARDEF, start, node.toString().length(), semantics); + // manually add variable to semantics since identifier isn't visited + variableRegistry.setNextVariableAccessType(VariableAccessType.WRITE); + variableRegistry.registerVariableAccess(name, !inLocalScope); } - addToken(JavaTokenType.J_VARDEF, start, node.toString().length(), semantics); - // manually add variable to semantics since identifier isn't visited - variableRegistry.setNextVariableAccessType(VariableAccessType.WRITE); - variableRegistry.registerVariableAccess(name, !inLocalScope); return super.visitVariable(node, null); } diff --git a/languages/java/src/test/java/de/jplag/java/JavaLanguageTest.java b/languages/java/src/test/java/de/jplag/java/JavaLanguageTest.java index 51e36d7c2..cf559d293 100644 --- a/languages/java/src/test/java/de/jplag/java/JavaLanguageTest.java +++ b/languages/java/src/test/java/de/jplag/java/JavaLanguageTest.java @@ -37,6 +37,15 @@ protected void collectTestData(TestDataCollector collector) { collector.testFile("CLI.java").testSourceCoverage().testContainedTokens(J_TRY_END, J_IMPORT, J_VARDEF, J_LOOP_BEGIN, J_ARRAY_INIT_BEGIN, J_IF_BEGIN, J_CATCH_END, J_COND, J_ARRAY_INIT_END, J_METHOD_BEGIN, J_TRY_BEGIN, J_CLASS_END, J_RETURN, J_ASSIGN, J_METHOD_END, J_IF_END, J_CLASS_BEGIN, J_NEWARRAY, J_PACKAGE, J_APPLY, J_LOOP_END, J_THROW, J_NEWCLASS, J_CATCH_BEGIN); + + collector.testFile("PatternMatching.java", "PatternMatchingManual.java").testSourceCoverage().testTokenSequence(J_CLASS_BEGIN, J_RECORD_BEGIN, + J_VARDEF, J_RECORD_END, J_METHOD_BEGIN, J_VARDEF, J_NEWCLASS, J_IF_BEGIN, J_VARDEF, J_IF_END, J_METHOD_END, J_CLASS_END); + + collector.testFile("StringConcat.java", "StringTemplate.java").testSourceCoverage().testTokenSequence(J_CLASS_BEGIN, J_METHOD_BEGIN, J_VARDEF, + J_VARDEF, J_VARDEF, J_APPLY, J_METHOD_END, J_CLASS_END); + + collector.testFile("AnonymousVariables.java").testTokenSequence(J_CLASS_BEGIN, J_METHOD_BEGIN, J_VARDEF, J_IF_BEGIN, J_IF_END, J_METHOD_END, + J_CLASS_END); } @Override diff --git a/languages/java/src/test/resources/de/jplag/java/AnonymousVariables.java b/languages/java/src/test/resources/de/jplag/java/AnonymousVariables.java new file mode 100644 index 000000000..41dd49238 --- /dev/null +++ b/languages/java/src/test/resources/de/jplag/java/AnonymousVariables.java @@ -0,0 +1,8 @@ +public class AnonymousVariables { + public void test(Object o) { + String _ = ""; + + if (o instanceof String _) { + } + } +} \ No newline at end of file diff --git a/languages/java/src/test/resources/de/jplag/java/PatternMatching.java b/languages/java/src/test/resources/de/jplag/java/PatternMatching.java new file mode 100644 index 000000000..1a0eeb28f --- /dev/null +++ b/languages/java/src/test/resources/de/jplag/java/PatternMatching.java @@ -0,0 +1,10 @@ +public class PatternMatchingManual { + private static final record Test(int x) { + } + + public void test() { + Object a = new Test(1); + if (a instanceof Test testA) { + } + } +} \ No newline at end of file diff --git a/languages/java/src/test/resources/de/jplag/java/PatternMatchingManual.java b/languages/java/src/test/resources/de/jplag/java/PatternMatchingManual.java new file mode 100644 index 000000000..c6a9dd446 --- /dev/null +++ b/languages/java/src/test/resources/de/jplag/java/PatternMatchingManual.java @@ -0,0 +1,11 @@ +public class PatternMatchingManual { + private static final record Test(int x) { + } + + public void test() { + Object a = new Test(1); + if (a instanceof Test) { + Test testA = (Test) a; + } + } +} \ No newline at end of file diff --git a/languages/java/src/test/resources/de/jplag/java/StringConcat.java b/languages/java/src/test/resources/de/jplag/java/StringConcat.java new file mode 100644 index 000000000..74bb33f32 --- /dev/null +++ b/languages/java/src/test/resources/de/jplag/java/StringConcat.java @@ -0,0 +1,8 @@ +public class StringConcat { + void test() { + int param1 = 1; + String param2 = "test"; + + String result = "prefix " + param1 + " infix " + param2.length() + "suffix"; + } +} \ No newline at end of file diff --git a/languages/java/src/test/resources/de/jplag/java/StringTemplate.java b/languages/java/src/test/resources/de/jplag/java/StringTemplate.java new file mode 100644 index 000000000..113a5a629 --- /dev/null +++ b/languages/java/src/test/resources/de/jplag/java/StringTemplate.java @@ -0,0 +1,8 @@ +public class StringTemplate { + void test() { + int param1 = 1; + String param2 = "test"; + + String result = STR."prefix \{param1} infix + \{param2.length()} suffix"; + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 321d0c499..ef97b0b16 100644 --- a/pom.xml +++ b/pom.xml @@ -72,8 +72,8 @@ ${maven.multiModuleProjectDirectory}/coverage-report/target/site/jacoco-aggregate/jacoco.xml - 17 - 17 + 21 + 21 2.40.0 2.0.9 5.10.1