diff --git a/build.gradle b/build.gradle index d2704fca0..45bc8c121 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,6 @@ buildscript { classpath 'com.palantir.gradle.plugintesting:gradle-plugin-testing:0.6.0' classpath 'com.palantir.javaformat:gradle-palantir-java-format:2.52.0' classpath 'com.palantir.gradle.revapi:gradle-revapi:1.8.0' - classpath 'com.palantir.javaformat:gradle-palantir-java-format:2.52.0' classpath 'com.palantir.suppressible-error-prone:gradle-suppressible-error-prone:2.4.0' classpath 'gradle.plugin.org.inferred:gradle-processors:3.7.0' classpath 'me.champeau.jmh:jmh-gradle-plugin:0.7.2' diff --git a/changelog/2.52.0/pr-1194.v2.yml b/changelog/@unreleased/pr-1194.v2.yml similarity index 100% rename from changelog/2.52.0/pr-1194.v2.yml rename to changelog/@unreleased/pr-1194.v2.yml diff --git a/gradle-palantir-java-format/build.gradle b/gradle-palantir-java-format/build.gradle index 0cbe5f941..b3716395a 100644 --- a/gradle-palantir-java-format/build.gradle +++ b/gradle-palantir-java-format/build.gradle @@ -74,15 +74,38 @@ configurations { canBeConsumed = false canBeResolved = true } + + formatterNativeImage { + canBeConsumed = false + canBeResolved = true + } + + } dependencies { impl project(':palantir-java-format') + formatterNativeImage(project(':palantir-java-format-native')) { + attributes { + attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, project.objects.named(LibraryElements, 'nativeImage')) + } + } + +} + +tasks.register("copyNativeImage", Copy.class) { + from(configurations.formatterNativeImage) + rename { fileName -> + String.format("%s.bin", fileName) + } + into("$buildDir/nativeImage") } def writeImplClasspath = tasks.register("writeImplClasspath") { + inputs.files(tasks.named("copyNativeImage")) doLast { file("$buildDir/impl.classpath").text = configurations.impl.asPath + file("$buildDir/nativeImage.path").text = tasks.named("copyNativeImage").get().getOutputs().getFiles().getSingleFile().listFiles()[0].getAbsolutePath() } } diff --git a/gradle-palantir-java-format/src/main/groovy/com/palantir/javaformat/gradle/ConfigureJavaFormatterXml.groovy b/gradle-palantir-java-format/src/main/groovy/com/palantir/javaformat/gradle/ConfigureJavaFormatterXml.groovy index 90fe0711d..ae06573ca 100644 --- a/gradle-palantir-java-format/src/main/groovy/com/palantir/javaformat/gradle/ConfigureJavaFormatterXml.groovy +++ b/gradle-palantir-java-format/src/main/groovy/com/palantir/javaformat/gradle/ConfigureJavaFormatterXml.groovy @@ -17,7 +17,8 @@ package com.palantir.javaformat.gradle class ConfigureJavaFormatterXml { - static void configureJavaFormat(Node rootNode, List uris) { + + static void configureJavaFormat(Node rootNode, List uris, Optional nativeImageUri) { def settings = matchOrCreateChild(rootNode, 'component', [name: 'PalantirJavaFormatSettings']) // enable matchOrCreateChild(settings, 'option', [name: 'enabled']).attributes().put('value', 'true') @@ -28,6 +29,10 @@ class ConfigureJavaFormatterXml { uris.forEach { URI uri -> listItems.appendNode('option', [value: uri]) } + // configure nativeImageClasspath + nativeImageUri.ifPresent { URI uri -> + matchOrCreateChild(settings, 'option', [name: 'nativeImageClassPath']).attributes().put('value', uri) + } } static void configureExternalDependencies(Node rootNode) { diff --git a/gradle-palantir-java-format/src/main/groovy/com/palantir/javaformat/gradle/PalantirJavaFormatIdeaPlugin.java b/gradle-palantir-java-format/src/main/groovy/com/palantir/javaformat/gradle/PalantirJavaFormatIdeaPlugin.java index 9a78f2077..43c454b80 100644 --- a/gradle-palantir-java-format/src/main/groovy/com/palantir/javaformat/gradle/PalantirJavaFormatIdeaPlugin.java +++ b/gradle-palantir-java-format/src/main/groovy/com/palantir/javaformat/gradle/PalantirJavaFormatIdeaPlugin.java @@ -29,6 +29,7 @@ import java.net.URI; import java.nio.charset.Charset; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import java.util.stream.Collectors; import javax.xml.parsers.ParserConfigurationException; @@ -47,23 +48,35 @@ public void apply(Project rootProject) { "May only apply com.palantir.java-format-idea to the root project"); rootProject.getPlugins().apply(PalantirJavaFormatProviderPlugin.class); - rootProject.getPluginManager().withPlugin("idea", ideaPlugin -> { Configuration implConfiguration = rootProject.getConfigurations().getByName(PalantirJavaFormatProviderPlugin.CONFIGURATION_NAME); - configureLegacyIdea(rootProject, implConfiguration); - configureIntelliJImport(rootProject, implConfiguration); + Optional nativeImplConfiguration = maybeGetNativeImplConfiguration(rootProject); + + configureLegacyIdea(rootProject, implConfiguration, nativeImplConfiguration); + configureIntelliJImport(rootProject, implConfiguration, nativeImplConfiguration); }); } - private static void configureLegacyIdea(Project project, Configuration implConfiguration) { + private static Optional maybeGetNativeImplConfiguration(Project rootProject) { + return NativeImageFormatProviderPlugin.shouldUseNativeImage(rootProject) + ? Optional.of(rootProject + .getConfigurations() + .getByName(NativeImageFormatProviderPlugin.NATIVE_CONFIGURATION_NAME)) + : Optional.empty(); + } + + private static void configureLegacyIdea( + Project project, Configuration implConfiguration, Optional nativeImplConfiguration) { IdeaModel ideaModel = project.getExtensions().getByType(IdeaModel.class); ideaModel.getProject().getIpr().withXml(xmlProvider -> { // this block is lazy List uris = implConfiguration.getFiles().stream().map(File::toURI).collect(Collectors.toList()); - ConfigureJavaFormatterXml.configureJavaFormat(xmlProvider.asNode(), uris); + Optional nativeUri = + nativeImplConfiguration.map(conf -> conf.getSingleFile().toURI()); + ConfigureJavaFormatterXml.configureJavaFormat(xmlProvider.asNode(), uris, nativeUri); ConfigureJavaFormatterXml.configureExternalDependencies(xmlProvider.asNode()); }); @@ -72,7 +85,8 @@ private static void configureLegacyIdea(Project project, Configuration implConfi }); } - private static void configureIntelliJImport(Project project, Configuration implConfiguration) { + private static void configureIntelliJImport( + Project project, Configuration implConfiguration, Optional nativeImplConfiguration) { // Note: we tried using 'org.jetbrains.gradle.plugin.idea-ext' and afterSync triggers, but these are currently // very hard to manage as the tasks feel disconnected from the Sync operation, and you can't remove them once // you've added them. For that reason, we accept that we have to resolve this configuration at @@ -84,9 +98,12 @@ private static void configureIntelliJImport(Project project, Configuration implC List uris = implConfiguration.getFiles().stream().map(File::toURI).collect(Collectors.toList()); + Optional nativeImageUri = + nativeImplConfiguration.map(conf -> conf.getSingleFile().toURI()); + createOrUpdateIdeaXmlFile( project.file(".idea/palantir-java-format.xml"), - node -> ConfigureJavaFormatterXml.configureJavaFormat(node, uris)); + node -> ConfigureJavaFormatterXml.configureJavaFormat(node, uris, nativeImageUri)); createOrUpdateIdeaXmlFile( project.file(".idea/externalDependencies.xml"), node -> ConfigureJavaFormatterXml.configureExternalDependencies(node)); @@ -95,7 +112,7 @@ private static void configureIntelliJImport(Project project, Configuration implC // Still configure legacy idea if using intellij import updateIdeaXmlFileIfExists(project.file(project.getName() + ".ipr"), node -> { - ConfigureJavaFormatterXml.configureJavaFormat(node, uris); + ConfigureJavaFormatterXml.configureJavaFormat(node, uris, nativeImageUri); ConfigureJavaFormatterXml.configureExternalDependencies(node); }); updateIdeaXmlFileIfExists(project.file(project.getName() + ".iws"), node -> { diff --git a/gradle-palantir-java-format/src/main/groovy/com/palantir/javaformat/gradle/PalantirJavaFormatPlugin.java b/gradle-palantir-java-format/src/main/groovy/com/palantir/javaformat/gradle/PalantirJavaFormatPlugin.java index 140fc8f45..ae5bbe025 100644 --- a/gradle-palantir-java-format/src/main/groovy/com/palantir/javaformat/gradle/PalantirJavaFormatPlugin.java +++ b/gradle-palantir-java-format/src/main/groovy/com/palantir/javaformat/gradle/PalantirJavaFormatPlugin.java @@ -16,11 +16,19 @@ package com.palantir.javaformat.gradle; +import com.palantir.javaformat.bootstrap.NativeImageFormatterService; import com.palantir.javaformat.java.FormatterService; +import java.io.File; import java.io.IOException; import org.gradle.api.DefaultTask; import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.TaskAction; public final class PalantirJavaFormatPlugin implements Plugin { @@ -28,19 +36,38 @@ public final class PalantirJavaFormatPlugin implements Plugin { @Override public void apply(Project project) { project.getRootProject().getPlugins().apply(PalantirJavaFormatProviderPlugin.class); + project.getRootProject().getPlugins().apply(NativeImageFormatProviderPlugin.class); project.getRootProject().getPlugins().apply(PalantirJavaFormatIdeaPlugin.class); project.getPlugins().apply(PalantirJavaFormatSpotlessPlugin.class); project.getPlugins().withId("java", p -> { - project.getTasks().register("formatDiff", FormatDiffTask.class); // TODO(dfox): in the future we may want to offer a simple 'format' task so people don't need to use // spotless to try out our formatter + project.getTasks().register("formatDiff", FormatDiffTask.class, task -> { + if (NativeImageFormatProviderPlugin.shouldUseNativeImage(project)) { + task.getNativeImage().fileProvider(getNativeImplConfiguration(project)); + } + }); }); } - public static class FormatDiffTask extends DefaultTask { + private static Provider getNativeImplConfiguration(Project project) { + return (project.getRootProject() + .getConfigurations() + .named(NativeImageFormatProviderPlugin.NATIVE_CONFIGURATION_NAME) + .map(FileCollection::getSingleFile)); + } + + public abstract static class FormatDiffTask extends DefaultTask { + + private static Logger log = Logging.getLogger(FormatDiffTask.class); + + @org.gradle.api.tasks.Optional + @InputFile + abstract RegularFileProperty getNativeImage(); + public FormatDiffTask() { setDescription("Format only chunks of files that appear in git diff"); setGroup("Formatting"); @@ -48,10 +75,19 @@ public FormatDiffTask() { @TaskAction public final void formatDiff() throws IOException, InterruptedException { - JavaFormatExtension extension = - getProject().getRootProject().getExtensions().getByType(JavaFormatExtension.class); - FormatterService formatterService = extension.serviceLoad(); - FormatDiff.formatDiff(getProject().getProjectDir().toPath(), formatterService); + if (getNativeImage().isPresent()) { + log.info("Using the native-image to format"); + FormatDiff.formatDiff( + getProject().getProjectDir().toPath(), + new NativeImageFormatterService( + getNativeImage().get().getAsFile().toPath())); + } else { + log.info("Using legacy java formatter"); + JavaFormatExtension extension = + getProject().getRootProject().getExtensions().getByType(JavaFormatExtension.class); + FormatterService formatterService = extension.serviceLoad(); + FormatDiff.formatDiff(getProject().getProjectDir().toPath(), formatterService); + } } } } diff --git a/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/ExecutableTransform.java b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/ExecutableTransform.java new file mode 100644 index 000000000..2d61c98c2 --- /dev/null +++ b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/ExecutableTransform.java @@ -0,0 +1,77 @@ +/* + * (c) Copyright 2025 Palantir Technologies Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.javaformat.gradle; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.gradle.api.artifacts.transform.InputArtifact; +import org.gradle.api.artifacts.transform.TransformAction; +import org.gradle.api.artifacts.transform.TransformOutputs; +import org.gradle.api.artifacts.transform.TransformParameters; +import org.gradle.api.file.FileSystemLocation; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; +import org.gradle.api.provider.Provider; + +/** + * A transform that makes the input artifact executable. + * + *

This is useful for native-image executables, which need to be executable in order to run. + */ +public abstract class ExecutableTransform implements TransformAction { + + private static Logger logger = Logging.getLogger(ExecutableTransform.class); + + @InputArtifact + public abstract Provider getInputArtifact(); + + @Override + public void transform(TransformOutputs outputs) { + File inputFile = getInputArtifact().get().getAsFile(); + File outputFile = outputs.file(inputFile.getName() + ".executable"); + try { + Files.copy(inputFile.toPath(), outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + makeFileExecutable(outputFile.toPath()); + } catch (IOException e) { + throw new RuntimeException(String.format("Failed to create executable file %s", outputFile.toPath()), e); + } + } + + private static void makeFileExecutable(Path pathToExe) { + try { + Set existingPermissions = Files.getPosixFilePermissions(pathToExe); + Files.setPosixFilePermissions( + pathToExe, + Stream.concat( + existingPermissions.stream(), + Stream.of( + PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.OTHERS_EXECUTE)) + .collect(Collectors.toSet())); + } catch (IOException e) { + throw new RuntimeException("Failed to set execute permissions on native-image", e); + } + } +} diff --git a/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/NativeImageFormatProviderPlugin.java b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/NativeImageFormatProviderPlugin.java new file mode 100644 index 000000000..bacb3a16a --- /dev/null +++ b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/NativeImageFormatProviderPlugin.java @@ -0,0 +1,93 @@ +/* + * (c) Copyright 2025 Palantir Technologies Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.javaformat.gradle; + +import com.google.common.base.Preconditions; +import com.palantir.platform.Architecture; +import com.palantir.platform.OperatingSystem; +import java.util.Optional; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.type.ArtifactTypeDefinition; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; + +public final class NativeImageFormatProviderPlugin implements Plugin { + + private static final Logger log = Logging.getLogger(NativeImageFormatProviderPlugin.class); + static final String NATIVE_CONFIGURATION_NAME = "palantirJavaFormatNative"; + + @Override + public void apply(Project rootProject) { + Preconditions.checkState( + rootProject == rootProject.getRootProject(), + "May only apply com.palantir.java-format-provider to the root project"); + + if (!shouldUseNativeImage(rootProject)) { + log.info("Skipping native image configuration as it is not supported on this platform"); + return; + } + String implementationVersion = JavaFormatExtension.class.getPackage().getImplementationVersion(); + OperatingSystem operatingSystem = OperatingSystem.get(); + rootProject.getConfigurations().register(NATIVE_CONFIGURATION_NAME, conf -> { + conf.setDescription("Internal configuration for resolving the palantir-java-format native image"); + conf.setVisible(false); + conf.setCanBeConsumed(false); + conf.setCanBeResolved(true); + conf.defaultDependencies(deps -> { + deps.add(rootProject + .getDependencies() + .create(String.format( + "com.palantir.javaformat:palantir-java-format-native:%s:nativeImage-%s_%s@%s", + implementationVersion, + operatingSystem.uiName(), + Architecture.get().uiName(), + getExtension(operatingSystem)))); + }); + conf.getAttributes().attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "executable-nativeImage"); + }); + rootProject.getDependencies().registerTransform(ExecutableTransform.class, transformSpec -> { + transformSpec + .getFrom() + .attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, getExtension(operatingSystem)); + transformSpec.getTo().attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "executable-nativeImage"); + }); + } + + public static boolean shouldUseNativeImage(Project project) { + return isNativeImageSupported() && isNativeFlagEnabled(project); + } + + private static boolean isNativeImageSupported() { + OperatingSystem os = OperatingSystem.get(); + return os.equals(OperatingSystem.LINUX_GLIBC) + || (os.equals(OperatingSystem.MACOS) && Architecture.get().equals(Architecture.AARCH64)); + } + + private static boolean isNativeFlagEnabled(Project project) { + return Optional.ofNullable(project.findProperty("palantir.native.formatter")) + .map(value -> Boolean.parseBoolean((String) value)) + .orElse(false); + } + + static String getExtension(OperatingSystem operatingSystem) { + if (operatingSystem.equals(OperatingSystem.WINDOWS)) { + return "exe"; + } + return "bin"; + } +} diff --git a/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/PalantirJavaFormatProviderPlugin.java b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/PalantirJavaFormatProviderPlugin.java index 3fdc94d7e..f84d92279 100644 --- a/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/PalantirJavaFormatProviderPlugin.java +++ b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/PalantirJavaFormatProviderPlugin.java @@ -17,7 +17,6 @@ package com.palantir.javaformat.gradle; import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableMap; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; @@ -32,19 +31,18 @@ public void apply(Project rootProject) { rootProject == rootProject.getRootProject(), "May only apply com.palantir.java-format-provider to the root project"); + rootProject.getPluginManager().apply(NativeImageFormatProviderPlugin.class); + Configuration configuration = rootProject.getConfigurations().create(CONFIGURATION_NAME, conf -> { conf.setDescription("Internal configuration for resolving the palantir-java-format implementation"); conf.setVisible(false); conf.setCanBeConsumed(false); - conf.defaultDependencies(deps -> { deps.add(rootProject .getDependencies() - .create(ImmutableMap.of( - "group", "com.palantir.javaformat", - "name", "palantir-java-format", - "version", - JavaFormatExtension.class.getPackage().getImplementationVersion()))); + .create(String.format( + "com.palantir.javaformat:palantir-java-format:%s", + JavaFormatExtension.class.getPackage().getImplementationVersion()))); }); }); diff --git a/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPlugin.java b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPlugin.java index 03f92f69c..d091163af 100644 --- a/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPlugin.java +++ b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/PalantirJavaFormatSpotlessPlugin.java @@ -31,8 +31,7 @@ public void apply(Project project) { project.getPluginManager().withPlugin("java", _javaPlugin -> { SPOTLESS_PLUGINS.forEach( spotlessPluginId -> project.getPluginManager().withPlugin(spotlessPluginId, _spotlessPlugin -> { - SpotlessInterop.addSpotlessJavaStep( - project, PalantirJavaFormatProviderPlugin.CONFIGURATION_NAME); + SpotlessInterop.addSpotlessJavaStep(project); })); }); } diff --git a/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/SpotlessInterop.java b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/SpotlessInterop.java index dbae64583..96ca27bac 100644 --- a/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/SpotlessInterop.java +++ b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/SpotlessInterop.java @@ -16,20 +16,39 @@ package com.palantir.javaformat.gradle; import com.diffplug.gradle.spotless.SpotlessExtension; +import com.diffplug.spotless.FormatterStep; +import com.palantir.javaformat.gradle.spotless.NativePalantirJavaFormatStep; import com.palantir.javaformat.gradle.spotless.PalantirJavaFormatStep; import org.gradle.api.Project; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; /** * Class that exists only to encapsulate accessing spotless classes, so that Gradle can generate a decorated class for * {@link com.palantir.javaformat.gradle.PalantirJavaFormatSpotlessPlugin} even if spotless is not on the classpath. */ final class SpotlessInterop { + private static Logger logger = Logging.getLogger(SpotlessInterop.class); + private SpotlessInterop() {} - static void addSpotlessJavaStep(Project project, String configurationName) { + static void addSpotlessJavaStep(Project project) { SpotlessExtension spotlessExtension = project.getExtensions().getByType(SpotlessExtension.class); - spotlessExtension.java(java -> java.addStep(PalantirJavaFormatStep.create( - project.getRootProject().getConfigurations().getByName(configurationName), - project.getRootProject().getExtensions().getByType(JavaFormatExtension.class)))); + spotlessExtension.java(java -> java.addStep(addSpotlessJavaFormatStep(project))); + } + + static FormatterStep addSpotlessJavaFormatStep(Project project) { + if (NativeImageFormatProviderPlugin.shouldUseNativeImage(project)) { + logger.info("Using the native-image palantir-java-formatter"); + return NativePalantirJavaFormatStep.create(project.getRootProject() + .getConfigurations() + .getByName(NativeImageFormatProviderPlugin.NATIVE_CONFIGURATION_NAME)); + } + logger.info("Using the legacy palantir-java-formatter"); + return PalantirJavaFormatStep.create( + project.getRootProject() + .getConfigurations() + .getByName(PalantirJavaFormatProviderPlugin.CONFIGURATION_NAME), + project.getRootProject().getExtensions().getByType(JavaFormatExtension.class)); } } diff --git a/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/spotless/NativePalantirJavaFormatStep.java b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/spotless/NativePalantirJavaFormatStep.java new file mode 100644 index 000000000..cc92dd1d3 --- /dev/null +++ b/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/spotless/NativePalantirJavaFormatStep.java @@ -0,0 +1,72 @@ +/* + * (c) Copyright 2025 Palantir Technologies Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.javaformat.gradle.spotless; + +import com.diffplug.spotless.FileSignature; +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ProcessRunner; +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; + +public final class NativePalantirJavaFormatStep { + private static Logger logger = Logging.getLogger(NativePalantirJavaFormatStep.class); + + private NativePalantirJavaFormatStep() {} + + private static final String NAME = "palantir-java-format"; + + /** Creates a step which formats everything - code, import order, and unused imports. */ + public static FormatterStep create(Configuration configuration) { + return FormatterStep.createLazy( + NAME, + () -> { + File execFile = configuration.getSingleFile(); + logger.info("Using native-image at {}", configuration.getSingleFile()); + return new State(FileSignature.signAsSet(execFile)); + }, + State::createFormat); + } + + static class State implements Serializable { + private static final long serialVersionUID = 1L; + + final FileSignature pathToExe; + + State(FileSignature pathToExe) { + this.pathToExe = pathToExe; + } + + String format(ProcessRunner runner, String input) throws IOException, InterruptedException { + List argumentsWithPathToExe = + List.of(pathToExe.getOnlyFile().getAbsolutePath(), "--palantir", "-"); + return runner.exec(input.getBytes(StandardCharsets.UTF_8), argumentsWithPathToExe) + .assertExitZero(StandardCharsets.UTF_8); + } + + FormatterFunc.Closeable createFormat() { + ProcessRunner runner = new ProcessRunner(); + return FormatterFunc.Closeable.of(runner, this::format); + } + } +} diff --git a/gradle-palantir-java-format/src/test/groovy/com/palantir/javaformat/gradle/ConfigureJavaFormatterXmlTest.groovy b/gradle-palantir-java-format/src/test/groovy/com/palantir/javaformat/gradle/ConfigureJavaFormatterXmlTest.groovy index 91c0faed5..464ae378f 100644 --- a/gradle-palantir-java-format/src/test/groovy/com/palantir/javaformat/gradle/ConfigureJavaFormatterXmlTest.groovy +++ b/gradle-palantir-java-format/src/test/groovy/com/palantir/javaformat/gradle/ConfigureJavaFormatterXmlTest.groovy @@ -58,6 +58,7 @@ class ConfigureJavaFormatterXmlTest extends Specification { + +

- + @@ -18,7 +18,7 @@ - + @@ -67,6 +67,22 @@ + + + + + + + + + + + + + + + + diff --git a/idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatConfigurable.java b/idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatConfigurable.java index 0dd857bb9..abd2b899a 100644 --- a/idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatConfigurable.java +++ b/idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatConfigurable.java @@ -21,11 +21,7 @@ import com.intellij.openapi.options.SearchableConfigurable; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.ComboBox; -import com.intellij.uiDesigner.core.GridConstraints; -import com.intellij.uiDesigner.core.GridLayoutManager; -import com.intellij.uiDesigner.core.Spacer; import com.palantir.javaformat.intellij.PalantirJavaFormatSettings.EnabledState; -import java.awt.Insets; import javax.annotation.Nullable; import javax.swing.JCheckBox; import javax.swing.JComboBox; @@ -44,6 +40,7 @@ class PalantirJavaFormatConfigurable extends BaseConfigurable implements Searcha @SuppressWarnings("for-rollout:RawTypes") private JComboBox styleComboBox; + private JLabel isUsingNativeImage; private JLabel formatterVersion; private JLabel pluginVersion; @@ -103,6 +100,7 @@ public void reset() { styleComboBox.setSelectedItem(UiFormatterStyle.convert(settings.getStyle())); pluginVersion.setText(settings.getImplementationVersion().orElse("unknown")); formatterVersion.setText(getFormatterVersionText(settings)); + isUsingNativeImage.setText(isUsingNativeImage(settings)); } @Override @@ -115,6 +113,14 @@ public boolean isModified() { @Override public void disposeUIResources() {} + private static String isUsingNativeImage(PalantirJavaFormatSettings settings) { + if (settings.getNativeImageClassPath().isPresent()) { + return "Native image formatter (`palantir.native.formatter` gradle property is enabled)"; + } else { + return "(Default setup) Java-based formatter"; + } + } + private static String getFormatterVersionText(PalantirJavaFormatSettings settings) { String suffix = settings.injectedVersionIsOutdated() ? " (using bundled)" : " (using injected)"; return settings.computeFormatterVersion().orElse("(bundled)") + suffix; @@ -123,171 +129,4 @@ private static String getFormatterVersionText(PalantirJavaFormatSettings setting private void createUIComponents() { styleComboBox = new ComboBox<>(UiFormatterStyle.values()); } - - { - // GUI initializer generated by IntelliJ IDEA GUI Designer - // >>> IMPORTANT!! <<< - // DO NOT EDIT OR ADD ANY CODE HERE! - $$$setupUI$$$(); - } - - /** - * Method generated by IntelliJ IDEA GUI Designer >>> IMPORTANT!! <<< DO NOT edit this method OR call it in your - * code! - * - * @noinspection ALL - */ - @SuppressWarnings("for-rollout:InvalidBlockTag") - private void $$$setupUI$$$() { - createUIComponents(); - panel = new JPanel(); - panel.setLayout(new GridLayoutManager(5, 2, new Insets(0, 0, 0, 0), -1, -1)); - enable = new JCheckBox(); - enable.setText("Enable palantir-java-format"); - panel.add( - enable, - new GridConstraints( - 0, - 0, - 1, - 2, - GridConstraints.ANCHOR_WEST, - GridConstraints.FILL_NONE, - GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, - GridConstraints.SIZEPOLICY_FIXED, - null, - null, - null, - 0, - false)); - final Spacer spacer1 = new Spacer(); - panel.add( - spacer1, - new GridConstraints( - 4, - 0, - 1, - 2, - GridConstraints.ANCHOR_CENTER, - GridConstraints.FILL_VERTICAL, - 1, - GridConstraints.SIZEPOLICY_WANT_GROW, - null, - null, - null, - 0, - false)); - final JLabel label1 = new JLabel(); - label1.setText("Code style"); - panel.add( - label1, - new GridConstraints( - 1, - 0, - 1, - 1, - GridConstraints.ANCHOR_WEST, - GridConstraints.FILL_NONE, - GridConstraints.SIZEPOLICY_FIXED, - GridConstraints.SIZEPOLICY_FIXED, - null, - null, - null, - 0, - false)); - panel.add( - styleComboBox, - new GridConstraints( - 1, - 1, - 1, - 1, - GridConstraints.ANCHOR_WEST, - GridConstraints.FILL_HORIZONTAL, - GridConstraints.SIZEPOLICY_CAN_GROW, - GridConstraints.SIZEPOLICY_FIXED, - null, - null, - null, - 1, - false)); - final JLabel label2 = new JLabel(); - label2.setText("Implementation version"); - panel.add( - label2, - new GridConstraints( - 3, - 0, - 1, - 1, - GridConstraints.ANCHOR_WEST, - GridConstraints.FILL_NONE, - GridConstraints.SIZEPOLICY_FIXED, - GridConstraints.SIZEPOLICY_FIXED, - null, - null, - null, - 0, - false)); - formatterVersion = new JLabel(); - formatterVersion.setText("what version are we running with?"); - panel.add( - formatterVersion, - new GridConstraints( - 3, - 1, - 1, - 1, - GridConstraints.ANCHOR_CENTER, - GridConstraints.FILL_HORIZONTAL, - GridConstraints.SIZEPOLICY_CAN_GROW, - GridConstraints.SIZEPOLICY_FIXED, - null, - null, - null, - 1, - false)); - final JLabel label3 = new JLabel(); - label3.setText("Plugin version"); - panel.add( - label3, - new GridConstraints( - 2, - 0, - 1, - 1, - GridConstraints.ANCHOR_WEST, - GridConstraints.FILL_NONE, - GridConstraints.SIZEPOLICY_FIXED, - GridConstraints.SIZEPOLICY_FIXED, - null, - null, - null, - 0, - false)); - pluginVersion = new JLabel(); - pluginVersion.setText("plugin version"); - panel.add( - pluginVersion, - new GridConstraints( - 2, - 1, - 1, - 1, - GridConstraints.ANCHOR_WEST, - GridConstraints.FILL_NONE, - GridConstraints.SIZEPOLICY_FIXED, - GridConstraints.SIZEPOLICY_FIXED, - null, - null, - null, - 1, - false)); - } - - /** @noinspection ALL */ - @SuppressWarnings({"for-rollout:InvalidBlockTag", "for-rollout:MissingSummary"}) - public JComponent $$$getRootComponent$$$() { - return panel; - } } diff --git a/idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatSettings.java b/idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatSettings.java index a4eec5867..026a58a25 100644 --- a/idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatSettings.java +++ b/idea-plugin/src/main/java/com/palantir/javaformat/intellij/PalantirJavaFormatSettings.java @@ -38,7 +38,7 @@ @State( name = "PalantirJavaFormatSettings", storages = {@Storage("palantir-java-format.xml")}) -class PalantirJavaFormatSettings implements PersistentStateComponent { +public class PalantirJavaFormatSettings implements PersistentStateComponent { @SuppressWarnings("for-rollout:SameNameButDifferent") private State state = new State(); @@ -91,6 +91,13 @@ Optional> getImplementationClassPath() { return state.implementationClassPath; } + /** + * The path to the formatter nativeImage. + */ + Optional getNativeImageClassPath() { + return state.nativeImageClassPath; + } + boolean injectedVersionIsOutdated() { Optional formatterVersion = computeFormatterVersion(); Optional implementationVersion = OrderableSlsVersion.safeValueOf( @@ -139,6 +146,7 @@ static class State { private EnabledState enabled = EnabledState.UNKNOWN; private Optional> implementationClassPath = Optional.empty(); + private Optional nativeImageClassPath = Optional.empty(); public JavaFormatterOptions.Style style = JavaFormatterOptions.Style.PALANTIR; @@ -154,6 +162,14 @@ public List getImplementationClassPath() { .orElse(null); } + public void setNativeImageClassPath(@Nullable String value) { + nativeImageClassPath = Optional.ofNullable(value).map(URI::create); + } + + public String getNativeImageClassPath() { + return nativeImageClassPath.map(URI::toString).orElse(null); + } + // enabled used to be a boolean so we use bean property methods for backwards compatibility public void setEnabled(@Nullable String enabledStr) { if (enabledStr == null) { @@ -182,8 +198,10 @@ public String toString() { return "PalantirJavaFormatSettings{" + "enabled=" + enabled - + ", formatterPath=" + + ", implementationClassPath=" + implementationClassPath + + ", nativeImageClassPath=" + + nativeImageClassPath + ", style=" + style + '}'; diff --git a/palantir-java-format-jdk-bootstrap/build.gradle b/palantir-java-format-jdk-bootstrap/build.gradle index 9348a2823..46c334611 100644 --- a/palantir-java-format-jdk-bootstrap/build.gradle +++ b/palantir-java-format-jdk-bootstrap/build.gradle @@ -1,7 +1,13 @@ +import me.champeau.jmh.JMHTask + apply plugin: 'java-library' apply plugin: 'com.palantir.external-publish-jar' apply plugin: 'com.palantir.revapi' +configurations { + formatterNativeImage +} + dependencies { annotationProcessor "org.immutables:value" @@ -14,4 +20,15 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter' testImplementation 'org.assertj:assertj-core' testImplementation 'com.fasterxml.jackson.core:jackson-databind' + formatterNativeImage(project(':palantir-java-format-native')) { + attributes { + attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, project.objects.named(LibraryElements, 'nativeImage')) + } + } } + +tasks.named('test', Test.class) { + inputs.files(configurations.named('formatterNativeImage')) + environment.put('NATIVE_IMAGE_CLASSPATH', configurations.formatterNativeImage.asPath) +} + diff --git a/palantir-java-format-jdk-bootstrap/src/main/java/com/palantir/javaformat/bootstrap/BootstrappingFormatterService.java b/palantir-java-format-jdk-bootstrap/src/main/java/com/palantir/javaformat/bootstrap/BootstrappingFormatterService.java index 5c88f5876..cf5caf897 100644 --- a/palantir-java-format-jdk-bootstrap/src/main/java/com/palantir/javaformat/bootstrap/BootstrappingFormatterService.java +++ b/palantir-java-format-jdk-bootstrap/src/main/java/com/palantir/javaformat/bootstrap/BootstrappingFormatterService.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.guava.GuavaModule; import com.google.common.base.Joiner; -import com.google.common.collect.BoundType; import com.google.common.collect.ImmutableList; import com.google.common.collect.Range; import com.palantir.javaformat.java.FormatterException; @@ -85,9 +84,7 @@ private ImmutableList getFormatReplacementsInternal(String input, C .withJvmArgsForVersion(jdkMajorVersion) .implementationClasspath(implementationClassPath) .outputReplacements(true) - .characterRanges(ranges.stream() - .map(BootstrappingFormatterService::toStringRange) - .collect(Collectors.toList())) + .characterRanges(ranges.stream().map(RangeUtils::toStringRange).collect(Collectors.toList())) .build(); Optional output = FormatterCommandRunner.runWithStdin(command.toArgs(), input); @@ -107,28 +104,18 @@ private String runFormatterCommand(String input) throws IOException { return FormatterCommandRunner.runWithStdin(command.toArgs(), input).orElse(input); } - /** Returns a range representation as parsed by "com.palantir.javaformat.java.CommandLineOptionsParser". */ - private static String toStringRange(Range range) { - int lower = range.lowerBoundType() == BoundType.CLOSED ? range.lowerEndpoint() : range.lowerEndpoint() + 1; - int higher = range.upperBoundType() == BoundType.CLOSED ? range.upperEndpoint() : range.upperEndpoint() - 1; - if (lower == higher) { - return String.valueOf(lower); - } - return String.format("%s:%s", lower, higher); - } - @Value.Immutable interface FormatterCliArgs { + List characterRanges(); + + boolean outputReplacements(); + Path jdkPath(); List implementationClasspath(); - List characterRanges(); - List jvmArgs(); - boolean outputReplacements(); - default List toArgs() { ImmutableList.Builder args = ImmutableList.builder() .add(jdkPath().toAbsolutePath().toString()) diff --git a/palantir-java-format-jdk-bootstrap/src/main/java/com/palantir/javaformat/bootstrap/NativeImageFormatterService.java b/palantir-java-format-jdk-bootstrap/src/main/java/com/palantir/javaformat/bootstrap/NativeImageFormatterService.java new file mode 100644 index 000000000..368cd9baa --- /dev/null +++ b/palantir-java-format-jdk-bootstrap/src/main/java/com/palantir/javaformat/bootstrap/NativeImageFormatterService.java @@ -0,0 +1,125 @@ +/* + * (c) Copyright 2025 Palantir Technologies Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.javaformat.bootstrap; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.guava.GuavaModule; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Range; +import com.palantir.javaformat.java.FormatterService; +import com.palantir.javaformat.java.Replacement; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.immutables.value.Value; + +public class NativeImageFormatterService implements FormatterService { + private static final ObjectMapper MAPPER = + JsonMapper.builder().addModule(new GuavaModule()).build(); + private final Path nativeImagePath; + + public NativeImageFormatterService(Path nativeImagePath) { + this.nativeImagePath = nativeImagePath; + } + + @Override + public ImmutableList getFormatReplacements(String input, Collection> ranges) { + try { + FormatterNativeImageArgs command = FormatterNativeImageArgs.builder() + .nativeImagePath(nativeImagePath) + .outputReplacements(true) + .characterRanges( + ranges.stream().map(RangeUtils::toStringRange).collect(Collectors.toList())) + .build(); + + Optional output = FormatterCommandRunner.runWithStdin(command.toArgs(), input); + if (output.isEmpty() || output.get().isEmpty()) { + return ImmutableList.of(); + } + return MAPPER.readValue(output.get(), new TypeReference<>() {}); + } catch (IOException e) { + throw new RuntimeException("Error running the native image command", e); + } + } + + @Override + public String formatSourceReflowStringsAndFixImports(String input) { + try { + return runFormatterCommand(input); + } catch (IOException e) { + throw new RuntimeException("Error running the native image command", e); + } + } + + @Override + public String fixImports(String input) { + try { + return runFormatterCommand(input); + } catch (IOException e) { + throw new RuntimeException("Error running the native image command", e); + } + } + + private String runFormatterCommand(String input) throws IOException { + FormatterNativeImageArgs command = FormatterNativeImageArgs.builder() + .nativeImagePath(nativeImagePath) + .outputReplacements(false) + .build(); + return FormatterCommandRunner.runWithStdin(command.toArgs(), input).orElse(input); + } + + @Value.Immutable + interface FormatterNativeImageArgs { + + List characterRanges(); + + boolean outputReplacements(); + + Path nativeImagePath(); + + default List toArgs() { + ImmutableList.Builder args = ImmutableList.builder() + .add(nativeImagePath().toAbsolutePath().toString()); + + if (!characterRanges().isEmpty()) { + args.add("--character-ranges", Joiner.on(',').join(characterRanges())); + } + if (outputReplacements()) { + args.add("--output-replacements"); + } + + return args + // Use palantir style + .add("--palantir") + // Trailing "-" enables formatting stdin -> stdout + .add("-") + .build(); + } + + static FormatterNativeImageArgs.Builder builder() { + return new FormatterNativeImageArgs.Builder(); + } + + final class Builder extends ImmutableFormatterNativeImageArgs.Builder {} + } +} diff --git a/palantir-java-format-jdk-bootstrap/src/main/java/com/palantir/javaformat/bootstrap/RangeUtils.java b/palantir-java-format-jdk-bootstrap/src/main/java/com/palantir/javaformat/bootstrap/RangeUtils.java new file mode 100644 index 000000000..e6be7b854 --- /dev/null +++ b/palantir-java-format-jdk-bootstrap/src/main/java/com/palantir/javaformat/bootstrap/RangeUtils.java @@ -0,0 +1,35 @@ +/* + * (c) Copyright 2025 Palantir Technologies Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.javaformat.bootstrap; + +import com.google.common.collect.BoundType; +import com.google.common.collect.Range; + +public final class RangeUtils { + + /** Returns a range representation as parsed by "com.palantir.javaformat.java.CommandLineOptionsParser". */ + public static String toStringRange(Range range) { + int lower = range.lowerBoundType() == BoundType.CLOSED ? range.lowerEndpoint() : range.lowerEndpoint() + 1; + int higher = range.upperBoundType() == BoundType.CLOSED ? range.upperEndpoint() : range.upperEndpoint() - 1; + if (lower == higher) { + return String.valueOf(lower); + } + return String.format("%s:%s", lower, higher); + } + + private RangeUtils() {} +} diff --git a/palantir-java-format-jdk-bootstrap/src/test/java/com/palantir/javaformat/bootstrap/BootstrappingFormatterServiceTest.java b/palantir-java-format-jdk-bootstrap/src/test/java/com/palantir/javaformat/bootstrap/FormatterServicesTest.java similarity index 69% rename from palantir-java-format-jdk-bootstrap/src/test/java/com/palantir/javaformat/bootstrap/BootstrappingFormatterServiceTest.java rename to palantir-java-format-jdk-bootstrap/src/test/java/com/palantir/javaformat/bootstrap/FormatterServicesTest.java index 3986adeca..bb34a69fb 100644 --- a/palantir-java-format-jdk-bootstrap/src/test/java/com/palantir/javaformat/bootstrap/BootstrappingFormatterServiceTest.java +++ b/palantir-java-format-jdk-bootstrap/src/test/java/com/palantir/javaformat/bootstrap/FormatterServicesTest.java @@ -22,6 +22,8 @@ import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Range; +import com.palantir.javaformat.java.FormatterException; +import com.palantir.javaformat.java.FormatterService; import com.palantir.javaformat.java.Replacement; import java.net.URI; import java.nio.file.Files; @@ -29,34 +31,39 @@ import java.util.List; import java.util.Locale; import java.util.stream.Collectors; -import org.junit.jupiter.api.Test; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; -final class BootstrappingFormatterServiceTest { +final class FormatterServicesTest { - @Test - void can_format_file_with_replacement() { + @ParameterizedTest + @MethodSource("getFormatters") + void can_format_file_with_replacement(FormatterService formatterService) throws FormatterException { String input = getTestResourceContent("format.input"); String expectedOutput = getTestResourceContent("format.output"); ImmutableList replacements = - getFormatter().getFormatReplacements(input, List.of(Range.open(0, input.length()))); + formatterService.getFormatReplacements(input, List.of(Range.open(0, input.length()))); assertThat(replacements).hasSize(1); assertThat(replacements.get(0).getReplacementString()).isEqualTo(expectedOutput); } - @Test - void can_format_full_file() { + @ParameterizedTest + @MethodSource("getFormatters") + void can_format_full_file(FormatterService formatterService) throws FormatterException { String input = getTestResourceContent("format.input"); String expectedOutput = getTestResourceContent("format.output"); - String formatted = getFormatter().formatSourceReflowStringsAndFixImports(input); + String formatted = formatterService.formatSourceReflowStringsAndFixImports(input); assertThat(formatted).isEqualTo(expectedOutput); } - @Test - void can_format_large_input_file() { + @ParameterizedTest + @MethodSource("getFormatters") + void can_format_large_input_file(FormatterService formatterService) throws FormatterException { String input = "class A {\n" + " void f(){\n" + " System.out.println(\"Test output\");\n".repeat(2000) @@ -64,16 +71,19 @@ void can_format_large_input_file() { + "}\n"; ImmutableList replacements = - getFormatter().getFormatReplacements(input, List.of(Range.open(0, input.length()))); + formatterService.getFormatReplacements(input, List.of(Range.open(0, input.length()))); assertThat(replacements).singleElement().satisfies(replacement -> { assertThat(replacement.getReplacementString()).startsWith("class A {\n void f() {"); }); } - private BootstrappingFormatterService getFormatter() { - return new BootstrappingFormatterService( - javaBinPath(), Runtime.version().feature(), getClasspath()); + private static Stream getFormatters() { + return Stream.of( + new BootstrappingFormatterService( + javaBinPath(), Runtime.version().feature(), getClasspath()), + new NativeImageFormatterService( + Path.of(System.getenv("NATIVE_IMAGE_CLASSPATH").toString()))); } private String getTestResourceContent(String resourceName) {