Skip to content

Commit 5cc6d72

Browse files
committed
Run the IDEA plugin's formatter on the IDE's JDK and report its failures
1 parent 729ccc0 commit 5cc6d72

5 files changed

Lines changed: 27 additions & 157 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,8 @@ See [open-java-format-eclipse-plugin](./open-java-format-eclipse-plugin).
592592
Upstream [PR 1211](https://github.com/palantir/palantir-java-format/pull/1211) shipped Java 21
593593
support. To use the Java 21 formatting capabilities, ensure that either:
594594

595-
- the Gradle daemon and the IntelliJ Project SDK are set to Java 21
595+
- the Gradle daemon runs on Java 21. The IntelliJ plugin needs no setup: it runs the formatter on
596+
the IDE's own runtime, which is Java 21 or later in every IDE it supports
596597
- or the gradle property `openjavaformat.native.formatter=true` is set. This runs the formatter
597598
as a native image, independent of the Gradle daemon / IntelliJ project JDK version.
598599

‎open-java-format-idea-plugin/src/main/java/com/palantir/javaformat/intellij/FormatterProvider.java‎

Lines changed: 10 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -18,35 +18,23 @@
1818

1919
import com.github.benmanes.caffeine.cache.Caffeine;
2020
import com.github.benmanes.caffeine.cache.LoadingCache;
21-
import com.google.common.annotations.VisibleForTesting;
2221
import com.google.common.base.Preconditions;
2322
import com.intellij.ide.plugins.IdeaPluginDescriptor;
2423
import com.intellij.ide.plugins.PluginManager;
25-
import com.intellij.openapi.application.ApplicationInfo;
2624
import com.intellij.openapi.extensions.PluginId;
2725
import com.intellij.openapi.project.Project;
28-
import com.intellij.openapi.projectRoots.JdkUtil;
29-
import com.intellij.openapi.projectRoots.Sdk;
30-
import com.intellij.openapi.roots.ProjectRootManager;
31-
import com.intellij.openapi.util.BuildNumber;
3226
import com.intellij.openapi.util.SystemInfo;
3327
import com.palantir.javaformat.bootstrap.BootstrappingFormatterService;
3428
import com.palantir.javaformat.bootstrap.NativeImageFormatterService;
3529
import com.palantir.javaformat.java.FormatterService;
3630
import java.io.IOException;
3731
import java.io.UncheckedIOException;
38-
import java.net.MalformedURLException;
3932
import java.net.URI;
40-
import java.net.URL;
41-
import java.net.URLClassLoader;
4233
import java.nio.file.Files;
4334
import java.nio.file.Path;
4435
import java.util.List;
4536
import java.util.Objects;
4637
import java.util.Optional;
47-
import java.util.OptionalInt;
48-
import java.util.ServiceLoader;
49-
import java.util.jar.Attributes.Name;
5038
import java.util.stream.Collectors;
5139
import java.util.stream.Stream;
5240
import org.slf4j.Logger;
@@ -57,7 +45,7 @@ final class FormatterProvider {
5745

5846
static final String PLUGIN_ID = "open-java-format";
5947

60-
// Cache to avoid creating a URLClassloader every time we want to format from IntelliJ
48+
// Cache to avoid resolving the formatter every time we want to format from IntelliJ
6149
private final LoadingCache<FormatterCacheKey, Optional<FormatterService>> implementationCache =
6250
Caffeine.newBuilder().maximumSize(1).build(FormatterProvider::createFormatter);
6351

@@ -70,7 +58,6 @@ static IdeaPluginDescriptor getPluginDescriptor() {
7058
Optional<FormatterService> get(Project project, PalantirJavaFormatSettings settings) {
7159
return implementationCache.get(new FormatterCacheKey(
7260
project,
73-
getSdkVersion(project),
7461
settings.getImplementationClassPath(),
7562
settings.getNativeImageClassPath(),
7663
settings.injectedVersionIsOutdated()));
@@ -82,39 +69,16 @@ private static Optional<FormatterService> createFormatter(FormatterCacheKey cach
8269
log.info("Using the native formatter with classpath: {}", cacheKey.nativeImageClassPath.get());
8370
return Optional.of(new NativeImageFormatterService(Path.of(cacheKey.nativeImageClassPath.get())));
8471
}
85-
if (cacheKey.jdkMajorVersion.isEmpty()) {
86-
return Optional.empty();
87-
}
8872

89-
int jdkMajorVersion = cacheKey.jdkMajorVersion.getAsInt();
73+
// The formatter runs in a JVM of its own, started with the "--add-exports" it needs to reach javac.
74+
// That JVM is the IDE's own runtime, never the project SDK: every IDE this plugin supports runs on Java 21
75+
// or later, which the formatter needs, while a project SDK can be any version.
76+
int jdkMajorVersion = Runtime.version().feature();
77+
Path jdkPath = Path.of(System.getProperty("java.home"), "bin", SystemInfo.isWindows ? "java.exe" : "java");
9078
List<Path> implementationClasspath =
9179
getImplementationUrls(cacheKey.implementationClassPath, cacheKey.useBundledImplementation);
92-
93-
// When running with JDK 15+ or using newer language features, we use the bootstrapping formatter which injects
94-
// required "--add-exports" args.
95-
if (useBootstrappingFormatter(
96-
jdkMajorVersion, ApplicationInfo.getInstance().getBuild())) {
97-
Path jdkPath = getJdkPath(cacheKey.project);
98-
log.info("Using bootstrapping formatter with jdk version {} and path: {}", jdkMajorVersion, jdkPath);
99-
return Optional.of(new BootstrappingFormatterService(jdkPath, jdkMajorVersion, implementationClasspath));
100-
}
101-
102-
// Use "in-process" formatter service
103-
log.info("Using in-process formatter for jdk version {}", jdkMajorVersion);
104-
URL[] implementationUrls = toUrlsUnchecked(implementationClasspath);
105-
ClassLoader classLoader = new URLClassLoader(implementationUrls, FormatterService.class.getClassLoader());
106-
return ServiceLoader.load(FormatterService.class, classLoader).findFirst();
107-
}
108-
109-
/**
110-
* When projects use JDK 15+ as their SDK, they might use newer language features which are only supported by the
111-
* bootstrapping formatter.
112-
* Separately, starting from 2022.2 (branch number '222'), Intellij now runs with JDK 17 which also requires the
113-
* bootstrapping formatter. See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html for
114-
* how the build number is formatted.
115-
*/
116-
private static boolean useBootstrappingFormatter(int jdkMajorVersion, BuildNumber buildNumber) {
117-
return jdkMajorVersion >= 15 || buildNumber.getBaselineVersion() >= 222;
80+
log.info("Using bootstrapping formatter with jdk version {} and path: {}", jdkMajorVersion, jdkPath);
81+
return Optional.of(new BootstrappingFormatterService(jdkPath, jdkMajorVersion, implementationClasspath));
11882
}
11983

12084
private static List<Path> getProvidedImplementationUrls(List<URI> implementationClasspath) {
@@ -149,68 +113,6 @@ private static List<Path> getImplementationUrls(
149113
});
150114
}
151115

152-
private static Path getJdkPath(Project project) {
153-
return getProjectJdk(project)
154-
.map(Sdk::getHomePath)
155-
.map(Path::of)
156-
.map(sdkHome -> sdkHome.resolve("bin").resolve("java" + (SystemInfo.isWindows ? ".exe" : "")))
157-
.filter(Files::exists)
158-
.orElseThrow(() ->
159-
new IllegalStateException("Could not determine JDK path for project: " + project.getName()));
160-
}
161-
162-
private static OptionalInt getSdkVersion(Project project) {
163-
return getProjectJdk(project)
164-
.map(FormatterProvider::parseSdkJavaVersion)
165-
.orElseThrow(() ->
166-
// This is not that common as our Gradle infrastructure should setup an SDK, but it does
167-
// happen on occassion and it manifests as the plugin ceasing to format. Give a slight
168-
// nudge for the manual remediation.
169-
new IllegalStateException("Could not determine SDK version for project: " + project.getName()
170-
+ ". Ensure you have an SDK set in "
171-
+ "'Project Structure' -> 'Project Settings' -> 'Project' -> 'SDK'"));
172-
}
173-
174-
private static OptionalInt parseSdkJavaVersion(Sdk sdk) {
175-
// Parses the actual version out of "SDK#getVersionString" which returns 'java version "15"'
176-
// or 'openjdk version "15.0.2"'.
177-
@SuppressWarnings("for-rollout:deprecation")
178-
String version = Preconditions.checkNotNull(
179-
JdkUtil.getJdkMainAttribute(sdk, Name.IMPLEMENTATION_VERSION), "JDK version is null");
180-
return parseSdkJavaVersion(version);
181-
}
182-
183-
@SuppressWarnings("for-rollout:Slf4jLogsafeArgs")
184-
@VisibleForTesting
185-
static OptionalInt parseSdkJavaVersion(String version) {
186-
int indexOfVersionDelimiter = version.indexOf('.');
187-
String normalizedVersion =
188-
indexOfVersionDelimiter >= 0 ? version.substring(0, indexOfVersionDelimiter) : version;
189-
normalizedVersion = normalizedVersion.replaceAll("-ea", "");
190-
try {
191-
return OptionalInt.of(Integer.parseInt(normalizedVersion));
192-
} catch (NumberFormatException e) {
193-
log.error("Could not parse sdk version: {}", version, e);
194-
return OptionalInt.empty();
195-
}
196-
}
197-
198-
private static Optional<Sdk> getProjectJdk(Project project) {
199-
return Optional.ofNullable(ProjectRootManager.getInstance(project).getProjectSdk());
200-
}
201-
202-
private static URL[] toUrlsUnchecked(List<Path> paths) {
203-
return paths.stream()
204-
.map(path -> {
205-
try {
206-
return path.toUri().toURL();
207-
} catch (IllegalArgumentException | MalformedURLException e) {
208-
throw new RuntimeException("Couldn't convert Path to URL: " + path, e);
209-
}
210-
})
211-
.toArray(URL[]::new);
212-
}
213-
214116
private static List<Path> listDirAsUrlsUnchecked(Path dir) {
215117
try (Stream<Path> list = Files.list(dir)) {
216118
return list.collect(Collectors.toList());
@@ -221,19 +123,16 @@ private static List<Path> listDirAsUrlsUnchecked(Path dir) {
221123

222124
private static final class FormatterCacheKey {
223125
private final Project project;
224-
private final OptionalInt jdkMajorVersion;
225126
private final Optional<List<URI>> implementationClassPath;
226127
private final Optional<URI> nativeImageClassPath;
227128
private final boolean useBundledImplementation;
228129

229130
FormatterCacheKey(
230131
Project project,
231-
OptionalInt jdkMajorVersion,
232132
Optional<List<URI>> implementationClassPath,
233133
Optional<URI> nativeImageClassPath,
234134
boolean useBundledImplementation) {
235135
this.project = project;
236-
this.jdkMajorVersion = jdkMajorVersion;
237136
this.implementationClassPath = implementationClassPath;
238137
this.nativeImageClassPath = nativeImageClassPath;
239138
this.useBundledImplementation = useBundledImplementation;
@@ -248,17 +147,15 @@ public boolean equals(Object o) {
248147
return false;
249148
}
250149
FormatterCacheKey that = (FormatterCacheKey) o;
251-
return Objects.equals(jdkMajorVersion, that.jdkMajorVersion)
252-
&& useBundledImplementation == that.useBundledImplementation
150+
return useBundledImplementation == that.useBundledImplementation
253151
&& Objects.equals(project, that.project)
254152
&& Objects.equals(implementationClassPath, that.implementationClassPath)
255153
&& Objects.equals(nativeImageClassPath, that.nativeImageClassPath);
256154
}
257155

258156
@Override
259157
public int hashCode() {
260-
return Objects.hash(
261-
project, jdkMajorVersion, implementationClassPath, nativeImageClassPath, useBundledImplementation);
158+
return Objects.hash(project, implementationClassPath, nativeImageClassPath, useBundledImplementation);
262159
}
263160
}
264161
}

‎open-java-format-idea-plugin/src/main/java/com/palantir/javaformat/intellij/Notifications.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,8 @@ class Notifications {
2525
static String parsingErrorMessage(String filename) {
2626
return "open-java-format failed. Does " + filename + " have syntax errors?";
2727
}
28+
29+
static String formatterFailedMessage(String filename) {
30+
return "open-java-format could not run the formatter on " + filename + ". The details are in idea.log.";
31+
}
2832
}

‎open-java-format-idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatFormattingService.java‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.intellij.ide.highlighter.JavaFileType;
2626
import com.intellij.ide.impl.TrustedProjects;
2727
import com.intellij.openapi.diagnostic.Logger;
28+
import com.intellij.openapi.progress.ProcessCanceledException;
2829
import com.intellij.openapi.project.Project;
2930
import com.intellij.openapi.util.NlsSafe;
3031
import com.intellij.openapi.util.TextRange;
@@ -134,6 +135,16 @@ public void run() {
134135
Notifications.PARSING_ERROR_TITLE,
135136
Notifications.parsingErrorMessage(
136137
request.getContext().getContainingFile().getName()));
138+
} catch (ProcessCanceledException e) {
139+
throw e;
140+
} catch (RuntimeException e) {
141+
// Typically the formatter's JVM failing to start or exiting with an error. Left alone, the IDE
142+
// swallows it and Reformat looks as if it did nothing.
143+
logger.warn("open-java-format could not run the formatter", e);
144+
request.onError(
145+
Notifications.GENERIC_ERROR_NOTIFICATION_GROUP,
146+
Notifications.formatterFailedMessage(
147+
request.getContext().getContainingFile().getName()));
137148
}
138149
}
139150

‎open-java-format-idea-plugin/src/test/java/com/palantir/javaformat/intellij/FormatterProviderTest.java‎

Lines changed: 0 additions & 43 deletions
This file was deleted.

0 commit comments

Comments
 (0)