diff --git a/kotlin-analysis-api/src/main/kotlin/com/intellij/util/lang/JavaVersion.java b/kotlin-analysis-api/src/main/kotlin/com/intellij/util/lang/JavaVersion.java new file mode 100644 index 0000000000..86bd53c7b7 --- /dev/null +++ b/kotlin-analysis-api/src/main/kotlin/com/intellij/util/lang/JavaVersion.java @@ -0,0 +1,334 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.util.lang; + +import com.intellij.ReviseWhenPortedToJDK; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * NB: copied from intellij to workaround KT-73967. + * + *

Can be removed as soon as Kotlin depends on intellij of at least 243.23654.3.

+ * + * + * + *

A class representing a version of some Java platform - e.g. the runtime the class is loaded into, or some installed JRE.

+ * + *

Based on JEP 322 "Time-Based Release Versioning" (Java 10+), but also supports JEP 223 + * "New Version-String Scheme" (Java 9), as well as earlier version's formats.

+ * + *

See {@link #parse(String)} for examples of supported version strings.

+ * + * @implNote the class is used in bootstrap - please use only JDK API + */ +@SuppressWarnings("DuplicatedCode") +public final class JavaVersion implements Comparable { + /** + * The major version. + * Corresponds to the first number of the 9+ format (9.0.1) / the second number of the 1.x format (1.8.0_60). + */ + public final int feature; + + /** + * The minor version. + * Corresponds to the second number of the 9+ format (9.0.1) / the third number of 1.x the format (1.8.0_60). + * Was used in version strings prior to 1.5, in newer strings is always {@code 0}. + */ + public final int minor; + + /** + * The patch version. + * Corresponds to the third number of the 9+ format (9.0.1) / the number after an underscore of the 1.x format (1.8.0_60). + */ + public final int update; + + /** + * The build number. + * Corresponds to a number prefixed by the "plus" sign in the 9+ format (9.0.1+7) / + * by "-b" string in the 1.x format (1.8.0_60-b12). + */ + public final int build; + + /** + * {@code true} if the platform is an early access release, {@code false} otherwise (or when not known). + */ + public final boolean ea; + + private JavaVersion(int feature, int minor, int update, int build, boolean ea) { + this.feature = feature; + this.minor = minor; + this.update = update; + this.build = build; + this.ea = ea; + } + + @Override + public int compareTo(@NotNull JavaVersion o) { + int diff = feature - o.feature; + if (diff != 0) return diff; + diff = minor - o.minor; + if (diff != 0) return diff; + diff = update - o.update; + if (diff != 0) return diff; + diff = build - o.build; + if (diff != 0) return diff; + return (ea ? 0 : 1) - (o.ea ? 0 : 1); + } + + public boolean isAtLeast(int feature) { + return this.feature >= feature; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof JavaVersion)) return false; + JavaVersion other = (JavaVersion)o; + return feature == other.feature && + minor == other.minor && + update == other.update && + build == other.build && + ea == other.ea; + } + + @Override + public int hashCode() { + int hash = feature; + hash = 31 * hash + minor; + hash = 31 * hash + update; + hash = 31 * hash + build; + hash = 31 * hash + (ea ? 1231 : 1237); + return hash; + } + + /** + * @return feature version string, e.g. 1.8 or 11 + */ + public @NotNull String toFeatureString() { + return formatVersionTo(true, true); + } + + /** + * @return feature, minor and update components of the version string, e.g. + * 1.8.0_242 or 11.0.5 + */ + public @NotNull String toFeatureMinorUpdateString() { + return formatVersionTo(false, true); + } + + @Override + public String toString() { + return formatVersionTo(false, false); + } + + private String formatVersionTo(boolean upToFeature, boolean upToUpdate) { + StringBuilder sb = new StringBuilder(); + if (feature > 8) { + sb.append(feature); + if (!upToFeature) { + if (minor > 0 || update > 0) sb.append('.').append(minor); + if (update > 0) sb.append('.').append(update); + if (!upToUpdate) { + if (ea) sb.append("-ea"); + if (build > 0) sb.append('+').append(build); + } + } + } + else { + sb.append("1.").append(feature); + if (!upToFeature) { + if (minor > 0 || update > 0 || ea || build > 0) sb.append('.').append(minor); + if (update > 0) sb.append('_').append(update); + if (!upToUpdate) { + if (ea) sb.append("-ea"); + if (build > 0) sb.append("-b").append(build); + } + } + } + return sb.toString(); + } + + /** + * Composes a version object out of given parameters. + * + * @throws IllegalArgumentException when any of the numbers is negative + */ + public static @NotNull JavaVersion compose(int feature, int minor, int update, int build, boolean ea) throws IllegalArgumentException { + if (feature < 0) throw new IllegalArgumentException(); + if (minor < 0) throw new IllegalArgumentException(); + if (update < 0) throw new IllegalArgumentException(); + if (build < 0) throw new IllegalArgumentException(); + return new JavaVersion(feature, minor, update, build, ea); + } + + public static @NotNull JavaVersion compose(int feature) { + return compose(feature, 0, 0, 0, false); + } + + private static JavaVersion current; + + /** + * Returns the version of a Java runtime the class is loaded into. + * The method attempts to parse {@code "java.runtime.version"} system property first (usually, it is more complete), + * and falls back to {@code "java.version"} if the former is invalid or differs in {@link #feature} or {@link #minor} numbers. + */ + public static @NotNull JavaVersion current() { + if (current == null) { + JavaVersion fallback = parse(System.getProperty("java.version")); + JavaVersion rt = rtVersion(); + if (rt == null) { + try { rt = parse(System.getProperty("java.runtime.version")); } + catch (Throwable ignored) { } + } + current = rt != null && rt.feature == fallback.feature && rt.minor == fallback.minor ? rt : fallback; + } + return current; + } + + /** + * Attempts to use Runtime.version() method available since Java 9. + */ + @ReviseWhenPortedToJDK("9") + private static @Nullable JavaVersion rtVersion() { + try { + Object version = Runtime.class.getMethod("version").invoke(null); + int major = (Integer)version.getClass().getMethod("major").invoke(version); + int minor = (Integer)version.getClass().getMethod("minor").invoke(version); + int security = (Integer)version.getClass().getMethod("security").invoke(version); + Object buildOpt = version.getClass().getMethod("build").invoke(version); + int build = (Integer)buildOpt.getClass().getMethod("orElse", Object.class).invoke(buildOpt, Integer.valueOf(0)); + Object preOpt = version.getClass().getMethod("pre").invoke(version); + boolean ea = (Boolean)preOpt.getClass().getMethod("isPresent").invoke(preOpt); + return new JavaVersion(major, minor, security, build, ea); + } + catch (Throwable ignored) { + return null; + } + } + + private static final int MAX_ACCEPTED_VERSION = 50; // sanity check + + /** + *

Parses a Java version string.

+ * + *

Supports various sources, including (but not limited to):
+ * - {@code "java.*version"} system properties (a version number without any decoration)
+ * - values of Java compiler -source/-target/--release options ("$MAJOR", "1.$MAJOR")
+ * - output of "{@code java -version}" (usually "java version \"$VERSION\"")
+ * - a second line of the above command (something like to "Java(TM) SE Runtime Environment (build $VERSION)")
+ * - output of "{@code java --full-version}" ("java $VERSION")
+ * - a line of "release" file ("JAVA_VERSION=\"$VERSION\"")

+ * + *

See com.intellij.util.lang.JavaVersionTest for examples.

+ * + * @throws IllegalArgumentException if failed to recognize the number. + */ + public static @NotNull JavaVersion parse(@NotNull String versionString) throws IllegalArgumentException { + // trimming + String str = versionString.trim(); + Map trimmingMap = new HashMap<>(); // "substring to detect" to "substring from which to trim" + trimmingMap.put("Runtime Environment", "(build "); + trimmingMap.put("OpenJ9", "version "); + trimmingMap.put("GraalVM", "Java "); + for (String keyToDetect : trimmingMap.keySet()) { + if (str.contains(keyToDetect)) { + int p = str.indexOf(trimmingMap.get(keyToDetect)); + if (p > 0) str = str.substring(p); + } + } + + // partitioning + List numbers = new ArrayList<>(), separators = new ArrayList<>(); + int length = str.length(), p = 0; + boolean number = false; + while (p < length) { + int start = p; + while (p < length && Character.isDigit(str.charAt(p)) == number) p++; + String part = str.substring(start, p); + (number ? numbers : separators).add(part); + number = !number; + } + + // parsing + if (!numbers.isEmpty() && !separators.isEmpty()) { + try { + int feature = Integer.parseInt(numbers.get(0)), minor = 0, update = 0, build = 0; + boolean ea = false; + + if (feature >= 5 && feature < MAX_ACCEPTED_VERSION) { + // Java 9+; Java 5+ (short format) + p = 1; + while (p < separators.size() && ".".equals(separators.get(p))) p++; + if (p > 1 && numbers.size() > 2) { + minor = Integer.parseInt(numbers.get(1)); + update = Integer.parseInt(numbers.get(2)); + } + if (p < separators.size()) { + String s = separators.get(p); + if (s != null && !s.isEmpty() && s.charAt(0) == '-') { + ea = startsWithWord(s, "-ea") || startsWithWord(s, "-internal"); + if (p < numbers.size() && s.charAt(s.length() - 1) == '+') { + build = Integer.parseInt(numbers.get(p)); + } + p++; + } + if (build == 0 && p < separators.size() && p < numbers.size() && "+".equals(separators.get(p))) { + build = Integer.parseInt(numbers.get(p)); + } + } + return new JavaVersion(feature, minor, update, build, ea); + } + else if (feature == 1 && numbers.size() > 1 && separators.size() > 1 && ".".equals(separators.get(1))) { + // Java 1.0 .. 1.4; Java 5+ (prefixed format) + feature = Integer.parseInt(numbers.get(1)); + if (feature <= MAX_ACCEPTED_VERSION) { + if (numbers.size() > 2 && separators.size() > 2 && ".".equals(separators.get(2))) { + minor = Integer.parseInt(numbers.get(2)); + if (numbers.size() > 3 && separators.size() > 3 && "_".equals(separators.get(3))) { + update = Integer.parseInt(numbers.get(3)); + if (separators.size() > 4) { + String s = separators.get(4); + if (s != null && !s.isEmpty() && s.charAt(0) == '-') { + ea = startsWithWord(s, "-ea") || startsWithWord(s, "-internal"); + } + p = 4; + while (p < separators.size() && !separators.get(p).endsWith("-b")) p++; + if (p < numbers.size()) { + build = Integer.parseInt(numbers.get(p)); + } + } + } + } + return new JavaVersion(feature, minor, update, build, ea); + } + } + } + catch (NumberFormatException ignored) { } + } + + throw new IllegalArgumentException(versionString); + } + + private static boolean startsWithWord(String s, String word) { + return s.startsWith(word) && (s.length() == word.length() || !Character.isLetterOrDigit(s.charAt(word.length()))); + } + + /** + * A safe version of {@link #parse(String)} - returns {@code null} when unable to parse a version string. + */ + public static @Nullable JavaVersion tryParse(String versionString) { + if (versionString != null) { + try { + return parse(versionString); + } + catch (IllegalArgumentException ignored) { } + } + + return null; + } +}