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;
+ }
+}