Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
import org.gradle.api.Plugin;
import org.gradle.api.Project;

/**
* Adds the spotless steps
* The formatters are lazily loaded
*/
public class PalantirJavaFormatSpotlessPlugin implements Plugin<Project> {
// The spotless gradle plugin got renamed to 'com.diffplug.spotless' at version 5.0.0
private static final ImmutableList<String> SPOTLESS_PLUGINS =
Expand All @@ -34,5 +38,11 @@ public void apply(Project project) {
SpotlessInterop.addSpotlessJavaStep(project);
}));
});

// assertFormattersNotOnClasspath();
}

public void assertFormattersNotOnClasspath() {
SpotlessInterop.assertFormattersNotOnClasspath();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,18 @@ static FormatterStep addSpotlessJavaFormatStep(Project project) {
return NativePalantirJavaFormatStep.create(project.getRootProject()
.getConfigurations()
.getByName(NativeImageFormatProviderPlugin.NATIVE_CONFIGURATION_NAME));
} else {
logger.info("Using the Java-based formatter {}", JavaVersion.current());
return PalantirJavaFormatStep.create(
project.getRootProject()
.getConfigurations()
.getByName(PalantirJavaFormatProviderPlugin.CONFIGURATION_NAME),
project.getRootProject().getExtensions().getByType(JavaFormatExtension.class));
}
logger.info("Using the Java-based formatter {}", JavaVersion.current());
return PalantirJavaFormatStep.create(
project.getRootProject()
.getConfigurations()
.getByName(PalantirJavaFormatProviderPlugin.CONFIGURATION_NAME),
project.getRootProject().getExtensions().getByType(JavaFormatExtension.class));
}

static void assertFormattersNotOnClasspath() {
// TODO(okelvin): is this needed for the native-image formatter as well?
PalantirJavaFormatStep.assertClassIsNotLoadable();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,42 +25,37 @@
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.function.Supplier;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;

public final class NativePalantirJavaFormatStep {
@SuppressWarnings("for-rollout:NonFinalStaticField")
private static Logger logger = Logging.getLogger(NativePalantirJavaFormatStep.class);
private static final 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);
return FormatterStep.createLazy(NAME, () -> new State(configuration::getSingleFile), State::createFormat);
}

static class State implements Serializable {
private static final long serialVersionUID = 1L;

final FileSignature pathToExe;
private final transient Supplier<File> execSupplier;

State(FileSignature pathToExe) {
this.pathToExe = pathToExe;
State(Supplier<File> supplier) {
this.execSupplier = supplier;
}

String format(ProcessRunner runner, String input) throws IOException, InterruptedException {
List<String> argumentsWithPathToExe =
List.of(pathToExe.getOnlyFile().getAbsolutePath(), "--palantir", "-");
File execFile = execSupplier.get();
logger.info("Using native-image at {}", execFile);
File signedFile = FileSignature.signAsSet(execFile).getOnlyFile();
List<String> argumentsWithPathToExe = List.of(signedFile.getAbsolutePath(), "--palantir", "-");
return runner.exec(input.getBytes(StandardCharsets.UTF_8), argumentsWithPathToExe)
.assertExitZero(StandardCharsets.UTF_8);
}
Expand All @@ -70,4 +65,4 @@ FormatterFunc.Closeable createFormat() {
return FormatterFunc.Closeable.of(runner, this::format);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ private PalantirJavaFormatStep() {}

/** Creates a step which formats everything - code, import order, and unused imports. */
public static FormatterStep create(Configuration palantirJavaFormat, JavaFormatExtension extension) {
ensureImplementationNotDirectlyLoadable();
assertClassIsNotLoadable();
Supplier<FormatterService> memoizedService = extension::serviceLoad;
return FormatterStep.createLazy(
NAME, () -> new State(palantirJavaFormat.getFiles(), memoizedService), State::createFormat);
NAME, () -> new State(palantirJavaFormat::getFiles, memoizedService), State::createFormat);
}

static final class State implements Serializable {
Expand All @@ -51,30 +51,41 @@ static final class State implements Serializable {

// Kept for state serialization purposes.
@SuppressWarnings({"unused", "FieldCanBeLocal"})
private final FileSignature jarsSignature;
private FileSignature jarsSignature;

private final transient Supplier<Iterable<File>> jarsSupplier;

// Transient as this is not serializable.
private final transient Supplier<FormatterService> memoizedFormatter;

/**
* Build a cacheable state for spotless from the given jars, that uses the given {@link FormatterService}.
*
* @param jars The jars that contain the palantir-java-format implementation. This is only used for caching and
* @param jarsSupplier Supplies the jars that contain the palantir-java-format implementation. This is only used for caching and
* up-to-dateness purposes.
*/
State(Iterable<File> jars, Supplier<FormatterService> memoizedFormatter) throws IOException {
this.jarsSignature = FileSignature.signAsSet(jars);
State(Supplier<Iterable<File>> jarsSupplier, Supplier<FormatterService> memoizedFormatter) {
this.jarsSupplier = jarsSupplier;
this.memoizedFormatter = memoizedFormatter;
}

@SuppressWarnings("NullableProblems")
FormatterFunc createFormat() {
return memoizedFormatter.get()::formatSourceReflowStringsAndFixImports;
return input -> {
try {
// Only resolve the jars and compute the signature at execution time!
Iterable<File> jars = jarsSupplier.get();
this.jarsSignature = FileSignature.signAsSet(jars);
return memoizedFormatter.get().formatSourceReflowStringsAndFixImports(input);
} catch (IOException e) {
throw new RuntimeException(e);
}
};
}
}

@SuppressWarnings("for-rollout:ThrowSpecificExceptions")
private static void ensureImplementationNotDirectlyLoadable() {
public static void assertClassIsNotLoadable() {
try {
PalantirJavaFormatStep.class.getClassLoader().loadClass(IMPL_CLASS);
} catch (ClassNotFoundException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* (c) Copyright 2019 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 nebula.test.IntegrationTestKitSpec


class FormatterLazinessTest extends IntegrationTestKitSpec {
private static final CLASSPATH_FILE = new File("build/impl.classpath").absolutePath

private GradlewExecutor executor

def setup() {
definePluginOutsideOfPluginBlock = true
keepFiles = true
executor = new GradlewExecutor(projectDir)
}

def "formatter is not loaded even if spotlessJava is realized lazily"() {
// language=Gradle
buildFile << """
buildscript {
repositories {
maven {
url 'https://artifactory.palantir.build/artifactory/release-jar'
metadataSources { mavenPom(); ignoreGradleMetadataRedirection() }
}
}

dependencies {
classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.22.0'
}
}

allprojects {
apply plugin: 'java'
apply plugin: 'com.diffplug.spotless'
apply plugin: 'com.palantir.java-format'

repositories {
maven { url 'https://artifactory.palantir.build/artifactory/release-jar' }
}
}

project.getTasks().getByName("spotlessJava")
""".stripIndent(true)

file("versions.props")
file("versions.lock")

runTasks('wrapper')

buildFile << """
dependencies {
palantirJavaFormat files(file("${CLASSPATH_FILE}").text.split(':'))
}
""".stripIndent()

when:
def result = executor.runGradlewTasks('--info')

then:
assert result.success
println(result.standardOutput)
result.standardOutput.contains('BUILD SUCCESSFUL')
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* (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 nebula.test.functional.internal.classpath.ClasspathAddingInitScriptBuilder
import org.gradle.internal.impldep.org.eclipse.jgit.annotations.NonNull

import java.nio.charset.StandardCharsets
import java.nio.file.Path
import java.util.concurrent.TimeUnit
import java.util.stream.Collectors
import java.util.stream.Stream

/**
* IntegrationTestKitSpec currently [loads more than what it needs into the classpath](https://github.com/nebula-plugins/nebula-test/blob/c5d3af9004898276bde5c68da492c6b0b4c5facc/src/main/groovy/nebula/test/IntegrationTestKitBase.groovy#L136)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not valid javadoc (yet - it is in java 23). Can you use the proper links and what not. Not doing a proper review, was just curious what was going on

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep gotcha.

* This means the Formatter is loaded onto the classpath eagerly, [erroneously](https://github.com/palantir/palantir-java-format/blob/00b08d2f471d66382d6c4cd2d05f56b6bb546ad3/gradle-palantir-java-format/src/main/java/com/palantir/javaformat/gradle/spotless/PalantirJavaFormatStep.java#L83).
* As a workaround, let's use the classpath produced by Gradle Test Kit in plugin-under-test-metadata.properties
* This classpath only contains the dependencies required by the plugin, as well as the plugin itself.
* This means no more eager loading of Formatters onto the classpath, no more complaints from PalantirJavaFormatStep
*/
class GradlewExecutor {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure this is used by the existing spotless test

private File projectDir

GradlewExecutor(@NonNull File projectDir) {
this.projectDir = projectDir
}

private static Iterable<File> getBuildPluginClasspathInjector() {
return getPluginClasspathInjector(Path.of("../gradle-palantir-java-format/build/pluginUnderTestMetadata/plugin-under-test-metadata.properties"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try and get this from a system property, so you're inherently relying the on the structure of your build. Then in your gradle file, ensure that the task that will run this test, has it as a system property.

If you were to make the change to nebula, I imagine this is what you'd want to do. i.e. if the system property is there, then use that, if not then fallback to whatever mechanism they currently have.

}

private static Iterable<File> getPluginClasspathInjector(Path path) {
File propertiesFile = path.toFile()
Properties properties = new Properties()
propertiesFile.withInputStream { inputStream ->
properties.load(inputStream)
}
String classpath = properties.getProperty('implementation-classpath')
return classpath.split(File.pathSeparator).collect { new File(it) }
}

GradlewExecutionResult runGradlewTasks(String... tasks) {
ProcessBuilder processBuilder = getProcessBuilder(tasks)
Process process = processBuilder.start()
String output = readAllInput(process.getInputStream())
process.waitFor(1, TimeUnit.MINUTES)
GradlewExecutionResult result = new GradlewExecutionResult(process.exitValue(), output)
return result
Comment on lines +61 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: return directly

}

final class GradlewExecutionResult {

final Boolean success
final String standardOutput
final Throwable failure

GradlewExecutionResult(int exitValue, String output) {
this.success = exitValue == 0
this.standardOutput = output
this.failure = exitValue != 0 ? new RuntimeException(String.format("Build failed with exitCode %s", exitValue)) : null
}
}

private static String readAllInput(InputStream inputStream) {
try (Stream<String> lines =
new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines()) {
return lines.collect(Collectors.joining("\n"));
}
}

private ProcessBuilder getProcessBuilder(String... tasks) {
File initScript = new File(projectDir, "init.gradle")
ClasspathAddingInitScriptBuilder.build(initScript, getBuildPluginClasspathInjector().toList())
List<String> arguments = ["./gradlew", "--init-script", initScript.toPath().toString()]
Arrays.asList(tasks).forEach(arguments::add)
ProcessBuilder processBuilder = new ProcessBuilder()
.command(arguments)
.directory(projectDir)
.redirectErrorStream(true)
return processBuilder
Comment on lines +90 to +94

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return directly

}
}