diff --git a/its/ruling/src/test/resources/expected/eclipse-jetty-similar-to-main/java-S5738.json b/its/ruling/src/test/resources/expected/eclipse-jetty-similar-to-main/java-S5738.json deleted file mode 100644 index 0277d1f2890..00000000000 --- a/its/ruling/src/test/resources/expected/eclipse-jetty-similar-to-main/java-S5738.json +++ /dev/null @@ -1,9 +0,0 @@ -{ -"eclipse-jetty:jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java": [ -212, -320, -2509, -2518, -3041 -] -} diff --git a/its/ruling/src/test/resources/expected/eclipse-jetty/java-S5738.json b/its/ruling/src/test/resources/expected/eclipse-jetty/java-S5738.json deleted file mode 100644 index e14b0890cc3..00000000000 --- a/its/ruling/src/test/resources/expected/eclipse-jetty/java-S5738.json +++ /dev/null @@ -1,28 +0,0 @@ -{ -"eclipse-jetty:jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java": [ -212, -320, -2509, -2518, -3041 -], -"eclipse-jetty:jetty-slf4j-impl/src/main/java/org/eclipse/jetty/logging/JettyLoggerConfiguration.java": [ -169 -], -"eclipse-jetty:jetty-util/src/main/java/org/eclipse/jetty/util/MemoryUtils.java": [ -37 -], -"eclipse-jetty:jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java": [ -680 -], -"eclipse-jetty:jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java": [ -198, -216 -], -"eclipse-jetty:jetty-util/src/test/java/org/eclipse/jetty/util/thread/QueuedThreadPoolTest.java": [ -730 -], -"eclipse-jetty:jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java": [ -1761 -] -} diff --git a/its/ruling/src/test/resources/expected/guava/java-S2387.json b/its/ruling/src/test/resources/expected/guava/java-S2387.json deleted file mode 100644 index b2438d3406c..00000000000 --- a/its/ruling/src/test/resources/expected/guava/java-S2387.json +++ /dev/null @@ -1,17 +0,0 @@ -{ -"guava:src/com/google/common/cache/LocalCache.java": [ -1370, -4230, -4239 -], -"guava:src/com/google/common/collect/MapMakerInternalMap.java": [ -1241, -1499, -3566, -3574 -], -"guava:src/com/google/common/collect/Maps.java": [ -3384, -3395 -] -} diff --git a/its/ruling/src/test/resources/expected/guava/java-S3551.json b/its/ruling/src/test/resources/expected/guava/java-S3551.json new file mode 100644 index 00000000000..07f09f6cc06 --- /dev/null +++ b/its/ruling/src/test/resources/expected/guava/java-S3551.json @@ -0,0 +1,6 @@ +{ +"guava:src/com/google/common/hash/HashingInputStream.java": [ +88, +95 +] +} diff --git a/its/ruling/src/test/resources/expected/guava/java-S5738.json b/its/ruling/src/test/resources/expected/guava/java-S5738.json index 1aff35a77db..df812fefdce 100644 --- a/its/ruling/src/test/resources/expected/guava/java-S5738.json +++ b/its/ruling/src/test/resources/expected/guava/java-S5738.json @@ -1,15 +1,7 @@ { -"guava:src/com/google/common/base/Throwables.java": [ -337, -367, -370 -], "guava:src/com/google/common/cache/Striped64.java": [ 321 ], -"guava:src/com/google/common/io/FileBackedOutputStream.java": [ -103 -], "guava:src/com/google/common/primitives/UnsignedBytes.java": [ 364 ], diff --git a/java-checks-test-sources/default/src/main/java/checks/CollectorsToListCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/CollectorsToListCheckSample.java index f6e4636a7b6..6b10c9139c8 100644 --- a/java-checks-test-sources/default/src/main/java/checks/CollectorsToListCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/CollectorsToListCheckSample.java @@ -123,22 +123,22 @@ void compliant() { List list6 = Stream.of("B", "C", "D") .collect(Collectors.toList()); // Compliant, list6 needs to be mutable - list6.addFirst("A"); + list6.add(0, "A"); List list7 = Stream.of("A", "B", "C") .collect(Collectors.toList()); // Compliant, list7 needs to be mutable - list7.addLast("D"); + list7.add("D"); List list8 = Stream.of("A", "B", "C") .collect(Collectors.toList()); // Compliant, list8 needs to be mutable - list8.removeFirst(); + list8.remove(0); List list9 = Stream.of("A", "B", "C") .collect(Collectors.toList()); // Compliant, list9 needs to be mutable - list9.removeLast(); + list9.remove("C"); } void addX() { diff --git a/java-checks-test-sources/default/src/main/java/checks/CollectorsToListCheckSampleWithoutSemantic.java b/java-checks-test-sources/default/src/main/java/checks/CollectorsToListCheckSampleWithoutSemantic.java index a5a3514ecd7..cec32cebcd0 100644 --- a/java-checks-test-sources/default/src/main/java/checks/CollectorsToListCheckSampleWithoutSemantic.java +++ b/java-checks-test-sources/default/src/main/java/checks/CollectorsToListCheckSampleWithoutSemantic.java @@ -118,22 +118,22 @@ void compliant() { List list6 = Stream.of("B", "C", "D") .collect(Collectors.toList()); // Compliant, list6 needs to be mutable - list6.addFirst("A"); + list6.add(0, "A"); List list7 = Stream.of("A", "B", "C") .collect(Collectors.toList()); // Compliant, list7 needs to be mutable - list7.addLast("D"); + list7.add("D"); List list8 = Stream.of("A", "B", "C") .collect(Collectors.toList()); // Compliant, list8 needs to be mutable - list8.removeFirst(); + list8.remove(0); List list9 = Stream.of("A", "B", "C") .collect(Collectors.toList()); // Compliant, list9 needs to be mutable - list9.removeLast(); + list9.remove("C"); } void addX() { diff --git a/java-checks-test-sources/default/src/main/java/checks/CollectorsToListCheckSequencedSample.java b/java-checks-test-sources/default/src/main/java/checks/CollectorsToListCheckSequencedSample.java new file mode 100644 index 00000000000..e5621b3cad9 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/CollectorsToListCheckSequencedSample.java @@ -0,0 +1,38 @@ +package checks; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class CollectorsToListCheckSequencedSample { + + void noncompliant() { + List list = Stream.of("A", "B", "C") + .collect(Collectors.toList()); // Noncompliant {{Replace this usage of 'Stream.collect(Collectors.toList())' with 'Stream.toList()' and ensure that the list is unmodified.}} +// ^^^^^^^^^^^^^^^^^^^ + + list.getFirst(); + } + + void compliant() { + List addFirst = Stream.of("B", "C", "D") + .collect(Collectors.toList()); // Compliant, addFirst needs to be mutable + + addFirst.addFirst("A"); + + List addLast = Stream.of("A", "B", "C") + .collect(Collectors.toList()); // Compliant, addLast needs to be mutable + + addLast.addLast("D"); + + List removeFirst = Stream.of("A", "B", "C") + .collect(Collectors.toList()); // Compliant, removeFirst needs to be mutable + + removeFirst.removeFirst(); + + List removeLast = Stream.of("A", "B", "C") + .collect(Collectors.toList()); // Compliant, removeLast needs to be mutable + + removeLast.removeLast(); + } +} diff --git a/java-checks-test-sources/default/src/main/java/checks/StringIsBlankCheckSample_java10.java b/java-checks-test-sources/default/src/main/java/checks/StringIsBlankCheckSample_java10.java index cece611f17c..449d8a5d8f5 100644 --- a/java-checks-test-sources/default/src/main/java/checks/StringIsBlankCheckSample_java10.java +++ b/java-checks-test-sources/default/src/main/java/checks/StringIsBlankCheckSample_java10.java @@ -4,16 +4,7 @@ public class StringIsBlankCheckSample_java10 { public boolean sample(String s) { boolean b; - // `String.isBlank()` does not exist before Java 11, so these stay emptiness checks on the trimmed string - - b = s.strip().length() == 0; // Noncompliant {{Use "isEmpty()" to check whether a "String" is empty or not.}} [[quickfixes=qf1]] - // fix@qf1 {{Replace with "isEmpty()"}} - // edit@qf1 [[sc=19;ec=32]]{{isEmpty()}} - - b = s.strip().length() > 0; // Noncompliant {{Use "isEmpty()" to check whether a "String" is empty or not.}} [[quickfixes=qf2]] - // fix@qf2 {{Replace with "isEmpty()"}} - // edit@qf2 [[sc=9;ec=9]]{{!}} - // edit@qf2 [[sc=19;ec=31]]{{isEmpty()}} + // `String.isBlank()` does not exist before Java 11, so trimmed strings are plain emptiness checks b = s.trim().length() == 0; // Noncompliant {{Use "isEmpty()" to check whether a "String" is empty or not.}} [[quickfixes=qf3]] // fix@qf3 {{Replace with "isEmpty()"}} diff --git a/java-checks/src/test/java/org/sonar/java/checks/CollectorsToListCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/CollectorsToListCheckTest.java index 5df09c6fd2c..d6875606d0c 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/CollectorsToListCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/CollectorsToListCheckTest.java @@ -28,6 +28,15 @@ void test() { .verifyIssues(); } + @Test + void test_sequenced_collection_mutators() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/CollectorsToListCheckSequencedSample.java")) + .withCheck(new CollectorsToListCheck()) + .withJavaVersion(21) + .verifyIssues(); + } + @Test void test_no_issues_for_older_versions() { createCheckVerifier() diff --git a/java-checks/src/test/java/org/sonar/java/checks/RunFinalizersCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/RunFinalizersCheckTest.java index 8442bd53f08..568039fa06d 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/RunFinalizersCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/RunFinalizersCheckTest.java @@ -47,7 +47,8 @@ void test() { .withJavaVersion(11) .verifyNoIssues(); } else { - // No issue raised starting JDK 11 as the related APIs were removed from JDK and cannot be resolved + // No issue raised by default starting JDK 11 as the related APIs were removed from JDK and cannot be resolved, + // unless the analyzed Java version is 10, for which the APIs of that release are visible CheckVerifier.newVerifier() .onFile(nonCompilingTestSourcesPath("checks/RunFinalizersCheckNoIssue.java")) .withCheck(new RunFinalizersCheck()) @@ -58,10 +59,10 @@ void test() { .withJavaVersion(javaVersion) .verifyNoIssues(); CheckVerifier.newVerifier() - .onFile(nonCompilingTestSourcesPath("checks/RunFinalizersCheckNoIssue.java")) + .onFile(nonCompilingTestSourcesPath("checks/RunFinalizersCheckSample.java")) .withCheck(new RunFinalizersCheck()) .withJavaVersion(10) - .verifyNoIssues(); + .verifyIssues(); } } diff --git a/java-frontend/pom.xml b/java-frontend/pom.xml index 016dbbde0b9..060cc981c4a 100644 --- a/java-frontend/pom.xml +++ b/java-frontend/pom.xml @@ -21,7 +21,7 @@ ${project.groupId} jdt-package - 1.9.0.1840 + 1.9.0.12280 diff --git a/java-frontend/src/main/java/org/sonar/java/model/JParserConfig.java b/java-frontend/src/main/java/org/sonar/java/model/JParserConfig.java index 79cffd79a90..cd81d499c33 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JParserConfig.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JParserConfig.java @@ -16,25 +16,13 @@ */ package org.sonar.java.model; -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; -import java.util.function.BooleanSupplier; -import java.util.stream.StreamSupport; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.FileASTRequestor; +import org.eclipse.jdt.core.dom.SystemReleaseClasspath; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.batch.fs.InputFile; @@ -47,6 +35,21 @@ import org.sonarsource.analyzer.commons.ProgressReport; import org.sonarsource.performance.measure.PerformanceMeasure; +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.BooleanSupplier; +import java.util.stream.StreamSupport; + public abstract class JParserConfig { public static final JavaVersion MAXIMUM_SUPPORTED_JAVA_VERSION = new JavaVersionImpl(JavaVersionImpl.MAX_SUPPORTED, true); @@ -54,7 +57,9 @@ public abstract class JParserConfig { private static final Logger LOG = LoggerFactory.getLogger(JParserConfig.class); private static final String MAXIMUM_ECJ_WARNINGS = "42000"; - private static final Set JRE_JARS = new HashSet<>(Arrays.asList("rt.jar", "jrt-fs.jar", "android.jar")); + private static final String JRT_FS_JAR = "jrt-fs.jar"; + private static final Set NON_MODULAR_JRE_JAR = new HashSet<>(Arrays.asList("rt.jar", "android.jar")); + private static final Set JRE_JARS = new HashSet<>(Arrays.asList("rt.jar", JRT_FS_JAR, "android.jar")); final JavaVersion javaVersion; final List classpath; @@ -67,7 +72,7 @@ private JParserConfig(JavaVersion javaVersion, List classpath, boolean sho } public abstract void parse(Iterable inputFiles, BooleanSupplier isCanceled, - AnalysisProgress analysisProgress, BiConsumer action); + AnalysisProgress analysisProgress, BiConsumer action); public enum Mode { BATCH(Batch::new), @@ -128,14 +133,17 @@ public ASTParser astParser() { JProblem.Type.compilerOptions() .forEach(option -> options.put(option, "warning")); + options.put(SystemReleaseClasspath.OPTION_RELEASE, javaVersion.effectiveJavaVersionAsString()); + List environment = useReleaseApiOfConfiguredJdk(options); + astParser.setCompilerOptions(options); - boolean includeRunningVMBootclasspath = classpath.stream() + boolean includeRunningVMBootclasspath = environment.stream() .noneMatch(f -> JRE_JARS.contains(f.getName())); - astParser.setEnvironment(classpath.stream() + astParser.setEnvironment(environment.stream() .map(File::getAbsolutePath) - .toArray(String[]::new), new String[] {}, new String[] {}, includeRunningVMBootclasspath); + .toArray(String[]::new), new String[]{}, new String[]{}, includeRunningVMBootclasspath); astParser.setResolveBindings(true); astParser.setBindingsRecovery(true); @@ -143,6 +151,53 @@ public ASTParser astParser() { return astParser; } + /** + * When the classpath carries the runtime image of a modular JDK (the {@code lib/jrt-fs.jar} added for + * {@code sonar.java.jdkHome}), ECJ would see the whole API of that JDK whatever the analyzed Java version is. + * If the requested release is older than that JDK, the image is removed from the classpath and the JDK is + * registered in {@code options} as the provider of the release API, which {@link SystemReleaseClasspath} then + * serves from its {@code ct.sym}. + * + *

Nothing changes when another JDK-like entry is present ({@code rt.jar}, {@code android.jar}), since it is + * the actual platform to analyze against, or when the release cannot be served by that JDK. + * + * @param options the compiler options, to which the JDK home is added when the release API is used + * @return the classpath to give to ECJ + */ + @VisibleForTesting + List useReleaseApiOfConfiguredJdk(Map options) { + boolean hasOtherPlatformJar = classpath.stream() + .anyMatch(f -> NON_MODULAR_JRE_JAR.contains(f.getName())); + if (hasOtherPlatformJar) { + return classpath; + } + for (File runtimeImage : classpath) { + Optional jdkHome = jdkHomeOfRuntimeImage(runtimeImage); + if (jdkHome.isPresent()) { + Map candidate = new HashMap<>(options); + candidate.put(SystemReleaseClasspath.OPTION_JAVA_HOME, jdkHome.get().getAbsolutePath()); + if (SystemReleaseClasspath.isApplicable(candidate)) { + options.put(SystemReleaseClasspath.OPTION_JAVA_HOME, jdkHome.get().getAbsolutePath()); + return classpath.stream().filter(f -> !f.equals(runtimeImage)).toList(); + } + if (LOG.isDebugEnabled()) { + LOG.debug("The API of Java {} is not served from the JDK '{}': using the API of that JDK as is.", + javaVersion.effectiveJavaVersionAsString(), jdkHome.get()); + } + return classpath; + } + } + return classpath; + } + + private static Optional jdkHomeOfRuntimeImage(File file) { + File lib = file.getParentFile(); + if (JRT_FS_JAR.equals(file.getName()) && lib != null && "lib".equals(lib.getName())) { + return Optional.ofNullable(lib.getParentFile()); + } + return Optional.empty(); + } + @VisibleForTesting static class Batch extends JParserConfig { @@ -152,7 +207,7 @@ static class Batch extends JParserConfig { @Override public void parse(Iterable inputFiles, BooleanSupplier isCanceled, - AnalysisProgress analysisProgress, BiConsumer action) { + AnalysisProgress analysisProgress, BiConsumer action) { List sourceFilePaths = new ArrayList<>(); Set notYetAnalyzedFiles = new LinkedHashSet<>(); @@ -230,7 +285,7 @@ private FileByFile(JavaVersion javaVersion, List classpath, boolean should @Override public void parse(Iterable inputFiles, BooleanSupplier isCanceled, - AnalysisProgress analysisProgress, BiConsumer action) { + AnalysisProgress analysisProgress, BiConsumer action) { boolean successfullyCompleted = false; boolean cancelled = false; @@ -280,6 +335,7 @@ private static void parse(ASTParser astParser, InputFile inputFile, JavaVersion static boolean shouldEnablePreviewFlag(JavaVersion currentVersion) { return currentVersion.arePreviewFeaturesEnabled(); } + @FunctionalInterface public interface ParserConfigConstructor { JParserConfig apply(JavaVersion version, List files, Boolean shouldIgnoreUnnamedModuleForSplitPackage); diff --git a/java-frontend/src/test/java/org/sonar/java/model/JParserConfigTest.java b/java-frontend/src/test/java/org/sonar/java/model/JParserConfigTest.java index 78d488f74b7..1aaa3b75dd6 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JParserConfigTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JParserConfigTest.java @@ -16,9 +16,18 @@ */ package org.sonar.java.model; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.eclipse.jdt.core.dom.SystemReleaseClasspath; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.io.TempDir; import org.slf4j.event.Level; import org.sonar.java.testing.ThreadLocalLogTester; @@ -29,6 +38,9 @@ class JParserConfigTest { @RegisterExtension public ThreadLocalLogTester logTester = new ThreadLocalLogTester().setLevel(Level.INFO); + @TempDir + Path temp; + @Test void should_enable_preview() { assertThat(shouldEnablePreviewFlag(new JavaVersionImpl())).isFalse(); @@ -55,4 +67,137 @@ void a_debug_message_is_logged_when_shouldIgnoreUnnamedModuleForSplitPackage_is_ JParserConfig.Mode.BATCH.create(new JavaVersionImpl(17), Collections.emptyList(), true); assertThat(logTester.logs()).containsExactly("The Java analyzer will ignore the unnamed module for split packages."); } + + @Test + void runtime_image_of_a_jdk_is_replaced_by_its_release_api_when_the_release_is_older() throws IOException { + File runtimeImage = runtimeImageOf(fakeJdk("jdk21", "21.0.2", true)); + File library = new File("lib.jar"); + Map options = releaseOptions("11"); + + List environment = config("11", List.of(library, runtimeImage)).useReleaseApiOfConfiguredJdk(options); + + assertThat(environment).containsExactly(library); + assertThat(options).containsEntry(SystemReleaseClasspath.OPTION_JAVA_HOME, runtimeImage.getParentFile().getParent()); + } + + @Test + void the_other_entries_of_the_classpath_are_kept_in_order() throws IOException { + File runtimeImage = runtimeImageOf(fakeJdk("jdk21", "21.0.2", true)); + List classpath = List.of(new File("a.jar"), runtimeImage, new File("b.jar"), new File("target/classes")); + + List environment = config("11", classpath).useReleaseApiOfConfiguredJdk(releaseOptions("11")); + + assertThat(environment).containsExactly(new File("a.jar"), new File("b.jar"), new File("target/classes")); + } + + @Test + void runtime_image_is_kept_when_the_release_is_not_older_than_the_jdk() throws IOException { + File runtimeImage = runtimeImageOf(fakeJdk("jdk21", "21.0.2", true)); + List classpath = List.of(runtimeImage); + + assertKept("21", classpath); + assertKept("22", classpath); + assertKept("26", classpath); + } + + @Test + void runtime_image_is_kept_when_the_release_is_below_9() throws IOException { + List classpath = List.of(runtimeImageOf(fakeJdk("jdk21", "21.0.2", true))); + + assertKept("8", classpath); + assertKept("7", classpath); + } + + @Test + void runtime_image_is_kept_when_the_jdk_cannot_serve_the_release() throws IOException { + assertKept("11", List.of(runtimeImageOf(fakeJdk("without-ct-sym", "21.0.2", false)))); + + Path withoutReleaseFile = Files.createDirectories(temp.resolve("without-release-file/lib")); + Files.createFile(withoutReleaseFile.resolve("ct.sym")); + assertKept("11", List.of(runtimeImageOf(withoutReleaseFile.getParent()))); + } + + @Test + void a_debug_message_explains_why_the_release_api_is_not_used() throws IOException { + logTester.setLevel(Level.DEBUG); + File runtimeImage = runtimeImageOf(fakeJdk("jdk21", "21.0.2", true)); + + config("21", List.of(runtimeImage)).useReleaseApiOfConfiguredJdk(releaseOptions("21")); + + assertThat(logTester.logs(Level.DEBUG)).hasSize(1); + assertThat(logTester.logs(Level.DEBUG).get(0)).contains("Java 21").contains(runtimeImage.getParentFile().getParent()); + } + + @Test + void runtime_image_is_kept_when_a_platform_jar_is_on_the_classpath() throws IOException { + File runtimeImage = runtimeImageOf(fakeJdk("jdk21", "21.0.2", true)); + + assertKept("11", List.of(new File("jdk8/jre/lib/rt.jar"), runtimeImage)); + assertKept("11", List.of(runtimeImage, new File("android-sdk/platforms/android-34/android.jar"))); + } + + @Test + void rt_jar_alone_is_not_modified() { + assertKept("11", List.of(new File("jdk8/jre/lib/rt.jar"))); + } + + @Test + void jar_named_as_runtime_image_outside_of_a_lib_folder_is_not_considered_as_a_jdk() throws IOException { + Path home = Files.createDirectories(temp.resolve("not-a-jdk")); + Files.createFile(home.resolve("jrt-fs.jar")); + + assertKept("11", List.of(home.resolve("jrt-fs.jar").toFile())); + } + + @Test + void classpath_without_jdk_is_not_modified() { + Map options = releaseOptions("11"); + List classpath = List.of(new File("a.jar"), new File("b.jar")); + + assertThat(config("11", classpath).useReleaseApiOfConfiguredJdk(options)).isSameAs(classpath); + assertThat(options).doesNotContainKey(SystemReleaseClasspath.OPTION_JAVA_HOME); + } + + @Test + void empty_classpath_is_not_modified() { + assertKept("11", List.of()); + } + + private void assertKept(String release, List classpath) { + Map options = releaseOptions(release); + + List environment = config(release, classpath).useReleaseApiOfConfiguredJdk(options); + + assertThat(environment).isSameAs(classpath); + assertThat(options).doesNotContainKey(SystemReleaseClasspath.OPTION_JAVA_HOME); + } + + private static JParserConfig config(String release, List classpath) { + return JParserConfig.Mode.FILE_BY_FILE.create(JavaVersionImpl.fromString(release), classpath); + } + + private static Map releaseOptions(String release) { + Map options = new HashMap<>(); + options.put(SystemReleaseClasspath.OPTION_RELEASE, release); + return options; + } + + private static File runtimeImageOf(Path jdkHome) throws IOException { + Path image = jdkHome.resolve("lib/jrt-fs.jar"); + Files.createDirectories(image.getParent()); + if (!Files.exists(image)) { + Files.createFile(image); + } + return image.toFile(); + } + + private Path fakeJdk(String name, String version, boolean withCtSym) throws IOException { + Path home = Files.createDirectories(temp.resolve(name)); + Path lib = Files.createDirectories(home.resolve("lib")); + Files.writeString(home.resolve("release"), "JAVA_VERSION=\"" + version + "\"\n"); + if (withCtSym) { + Files.createFile(lib.resolve("ct.sym")); + } + return home; + } } diff --git a/java-frontend/src/test/java/org/sonar/java/model/JParserTest.java b/java-frontend/src/test/java/org/sonar/java/model/JParserTest.java index 57d8dfeb19b..4aea4c7eded 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JParserTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JParserTest.java @@ -38,6 +38,7 @@ import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; +import java.util.stream.Collectors; import org.apache.commons.io.FileUtils; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.jdt.core.JavaCore; @@ -91,6 +92,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doReturn; @@ -111,6 +113,79 @@ class JParserTest { @RegisterExtension public ThreadLocalLogTester logTester = new ThreadLocalLogTester().setLevel(Level.DEBUG); + @Test + void should_only_see_the_jdk_api_of_the_requested_release() { + assumeRunningJdkCanServeOlderReleases(); + String source = """ + class Example { + java.util.jar.Pack200 removedInJdk14; + java.lang.Record addedInJdk16; + } + """; + Set undefinedAt11 = undefinedTypes("11", source); + assertThat(undefinedAt11).containsExactly("java.lang.Record cannot be resolved to a type"); + assertThat(undefinedTypes("17", source)).containsExactly("java.util.jar.Pack200 cannot be resolved to a type"); + } + + @Test + void should_use_the_release_api_of_the_jdk_provided_in_the_classpath() { + File runtimeImage = new File(System.getProperty("java.home"), "lib/jrt-fs.jar"); + assumeTrue(runtimeImage.isFile()); + assumeRunningJdkCanServeOlderReleases(); + String source = """ + class Example { + java.util.jar.Pack200 removedInJdk14; + java.lang.Record addedInJdk16; + } + """; + List classpath = List.of(runtimeImage); + int jdkVersion = Runtime.version().feature(); + + assertThat(undefinedTypes("11", source, classpath)).containsExactly("java.lang.Record cannot be resolved to a type"); + assertThat(undefinedTypes(Integer.toString(jdkVersion), source, classpath)).containsExactly("java.util.jar.Pack200 cannot be resolved to a type"); + assertThat(undefinedTypes(Integer.toString(jdkVersion + 1), source, classpath)).containsExactly("java.util.jar.Pack200 cannot be resolved to a type"); + } + + @Test + void should_only_see_the_jdk_api_of_the_requested_release_in_batch_mode(@TempDir Path temp) throws Exception { + assumeRunningJdkCanServeOlderReleases(); + Path file = Files.writeString(temp.resolve("Example.java"), """ + class Example { + java.util.jar.Pack200 removedInJdk14; + java.lang.Record addedInJdk16; + } + """); + List> undefinedTypes = new ArrayList<>(); + + BATCH + .create(JavaVersionImpl.fromString("11"), List.of()) + .parse(Set.of(TestUtils.inputFile(file.toFile())), () -> false, new AnalysisProgress(1), (inputFile, result) -> { + try { + undefinedTypes.add(result.get().sema.undefinedTypes.stream().map(Object::toString).collect(Collectors.toSet())); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }); + + assertThat(undefinedTypes).containsExactly(Set.of("java.lang.Record cannot be resolved to a type")); + } + + private static void assumeRunningJdkCanServeOlderReleases() { + assumeTrue(new File(System.getProperty("java.home"), "lib/ct.sym").isFile(), "the running JVM has no lib/ct.sym"); + assumeTrue(Runtime.version().feature() > 17, "the running JVM must be newer than Java 17"); + } + + private static Set undefinedTypes(String release, String source) { + return undefinedTypes(release, source, List.of()); + } + + private static Set undefinedTypes(String release, String source, List classpath) { + ASTParser astParser = FILE_BY_FILE.create(JavaVersionImpl.fromString(release), classpath).astParser(); + return JParser.parse(astParser, release, "Example", source).sema.undefinedTypes.stream() + .map(Object::toString) + .collect(Collectors.toSet()); + } + @Test void should_throw_RecognitionException_in_case_of_syntax_error() { // Note that without check for syntax errors will cause IndexOutOfBoundsException