From 4b687344ff21df3033ff437424a59445f5dc7a67 Mon Sep 17 00:00:00 2001 From: Tim Sparg <6872586+timothysparg@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:14:08 +0100 Subject: [PATCH 01/10] Add spring-javaformat-cli module Introduces a new Spring Boot CLI module built on picocli. The module targets Java 17 and includes Error Prone and NullAway for compile-time null-safety enforcement via jspecify @NullMarked annotations. Co-Authored-By: Claude Sonnet 4.6 --- .mvn/jvm.config | 12 + pom.xml | 30 +++ spring-javaformat-cli/pom.xml | 211 ++++++++++++++++++ .../cli/SpringJavaFormatCliApplication.java | 41 ++++ .../spring/javaformat/cli/package-info.java | 25 +++ .../src/main/resources/application.properties | 4 + 6 files changed, 323 insertions(+) create mode 100644 spring-javaformat-cli/pom.xml create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/SpringJavaFormatCliApplication.java create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/package-info.java create mode 100644 spring-javaformat-cli/src/main/resources/application.properties diff --git a/.mvn/jvm.config b/.mvn/jvm.config index 629555a1..d1e0726a 100644 --- a/.mvn/jvm.config +++ b/.mvn/jvm.config @@ -1,2 +1,14 @@ -Xmx1536m -Dtycho.disableP2Mirrors=true +-Djdk.xml.maxGeneralEntitySizeLimit=0 +-Djdk.xml.totalEntitySizeLimit=0 +--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED +--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED +--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED +--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED diff --git a/pom.xml b/pom.xml index 57d056c6..b04aa8d3 100644 --- a/pom.xml +++ b/pom.xml @@ -48,8 +48,12 @@ 3.6.28 5.8.1 3.21.0-GA + 1.0.0 1.2 + 4.7.7 3.5.1 + 2.7.18 + 0.12.2 1.16.0 1.16.0 3.0.3 @@ -181,6 +185,16 @@ maven-source-plugin 3.2.1 + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + org.springframework.experimental + spring-aot-maven-plugin + ${spring-native.version} + org.apache.maven.plugins maven-javadoc-plugin @@ -495,6 +509,11 @@ log4j-slf4j-impl ${log4j.version} + + org.jspecify + jspecify + ${jspecify.version} + org.junit junit-bom @@ -558,11 +577,21 @@ picocontainer ${picocontainer.version} + + info.picocli + picocli-spring-boot-starter + ${picocli.version} + org.codehaus.plexus plexus-utils ${plexus-utils.version} + + org.springframework.experimental + spring-native + ${spring-native.version} + org.testcontainers testcontainers @@ -595,6 +624,7 @@ spring-javaformat + spring-javaformat-cli spring-javaformat-eclipse spring-javaformat-gradle spring-javaformat-intellij-idea diff --git a/spring-javaformat-cli/pom.xml b/spring-javaformat-cli/pom.xml new file mode 100644 index 00000000..a33724cf --- /dev/null +++ b/spring-javaformat-cli/pom.xml @@ -0,0 +1,211 @@ + + + 4.0.0 + + io.spring.javaformat + spring-javaformat-build + 0.0.48-SNAPSHOT + + spring-javaformat-cli + Spring JavaFormat CLI + + ${basedir}/.. + 17 + 17 + 17 + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + org.jspecify + jspecify + + + io.spring.javaformat + spring-javaformat-formatter + ${project.version} + + + io.spring.javaformat + spring-javaformat-formatter-eclipse-runtime + ${project.version} + + + * + * + + + + + io.spring.javaformat + spring-javaformat-config + ${project.version} + + + io.spring.javaformat + spring-javaformat-checkstyle + ${project.version} + + + org.springframework.experimental + spring-native + + + info.picocli + picocli-spring-boot-starter + + + org.codehaus.plexus + plexus-utils + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + ${java.version} + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + -XDcompilePolicy=simple + -XDaddTypeAnnotationsToSymbol=true + + -XDshould-stop.ifError=FLOW + -Xplugin:ErrorProne -XepDisableWarningsInGeneratedCode -Xep:NullAway:ERROR -XepOpt:NullAway:JSpecifyMode=true -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:ExternalInitAnnotations=io.spring.javaformat.cli.PicocliManaged + -Aproject=${project.groupId}/${project.artifactId} + + + + info.picocli + picocli-codegen + 4.7.7 + + + + com.google.errorprone + error_prone_core + 2.42.0 + + + + com.uber.nullaway + nullaway + 0.12.11 + + + + + + org.springframework.experimental + spring-aot-maven-plugin + + + generate + + generate + + + + + + org.springframework.boot + spring-boot-maven-plugin + + exec + + + + + repackage + + + + + + + + + + native + + + + org.graalvm.buildtools + native-maven-plugin + 0.9.28 + true + + + build-native + package + + build + + + + + io.spring.javaformat.cli.SpringJavaFormatCliApplication + + --no-fallback + --allow-incomplete-classpath + --initialize-at-build-time=org.springframework.core.annotation.RepeatableContainers,org.springframework.core.annotation.RepeatableContainers$NoRepeatableContainers,org.springframework.core.annotation.TypeMappedAnnotations + --initialize-at-build-time=io.spring.javaformat.eclipse.jdt.jdk17.internal.compiler + + + + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/SpringJavaFormatCliApplication.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/SpringJavaFormatCliApplication.java new file mode 100644 index 00000000..ee8e3294 --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/SpringJavaFormatCliApplication.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli; + +import org.springframework.boot.Banner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Main application for the Spring JavaFormat CLI. + * + * @author Tim Sparg + */ +@SpringBootApplication +public class SpringJavaFormatCliApplication { + + protected SpringJavaFormatCliApplication() { + } + + public static void main(String[] args) { + SpringApplication application = new SpringApplication(SpringJavaFormatCliApplication.class); + application.setBannerMode(Banner.Mode.OFF); + application.setLogStartupInfo(false); + System.exit(SpringApplication.exit(application.run(args))); + } + +} diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/package-info.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/package-info.java new file mode 100644 index 00000000..7abe78d9 --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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. + */ + +/** + * Spring JavaFormat CLI — command-line tool for applying and validating Java formatting. + * + * @author Tim Sparg + */ +@NullMarked +package io.spring.javaformat.cli; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-javaformat-cli/src/main/resources/application.properties b/spring-javaformat-cli/src/main/resources/application.properties new file mode 100644 index 00000000..891e4df1 --- /dev/null +++ b/spring-javaformat-cli/src/main/resources/application.properties @@ -0,0 +1,4 @@ +spring.main.web-application-type=none +logging.level.root=ERROR +logging.level.io.spring.javaformat=ERROR +logging.level.org.springframework=ERROR From 2c3c9e3334041e67d825cf2d1c83b3a47dc931fa Mon Sep 17 00:00:00 2001 From: Tim Sparg <6872586+timothysparg@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:16:03 +0100 Subject: [PATCH 02/10] Add picocli command infrastructure Introduces the root picocli command (SpringJavaFormatCommand), the Spring CommandLineRunner that executes it (PicocliRunner), a version provider (CliVersionProvider), and the PicocliManaged annotation used to suppress NullAway field-initialization warnings for picocli-injected fields. Co-Authored-By: Claude Sonnet 4.6 --- spring-javaformat-cli/pom.xml | 16 +++++ .../javaformat/cli/CliVersionProvider.java | 57 +++++++++++++++++ .../spring/javaformat/cli/PicocliManaged.java | 41 +++++++++++++ .../spring/javaformat/cli/PicocliRunner.java | 54 ++++++++++++++++ .../cli/SpringJavaFormatCommand.java | 43 +++++++++++++ .../spring/javaformat/cli/version.properties | 1 + .../SpringJavaFormatCliApplicationTests.java | 61 +++++++++++++++++++ 7 files changed, 273 insertions(+) create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/CliVersionProvider.java create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/PicocliManaged.java create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/PicocliRunner.java create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/SpringJavaFormatCommand.java create mode 100644 spring-javaformat-cli/src/main/resources/io/spring/javaformat/cli/version.properties create mode 100644 spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/SpringJavaFormatCliApplicationTests.java diff --git a/spring-javaformat-cli/pom.xml b/spring-javaformat-cli/pom.xml index a33724cf..c6014d6b 100644 --- a/spring-javaformat-cli/pom.xml +++ b/spring-javaformat-cli/pom.xml @@ -82,6 +82,22 @@ + + + src/main/resources + true + + io/spring/javaformat/cli/version.properties + + + + src/main/resources + false + + io/spring/javaformat/cli/version.properties + + + org.apache.maven.plugins diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/CliVersionProvider.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/CliVersionProvider.java new file mode 100644 index 00000000..7259b1fd --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/CliVersionProvider.java @@ -0,0 +1,57 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import picocli.CommandLine.Help.Ansi; +import picocli.CommandLine.IVersionProvider; + +/** + * Provides CLI version text for picocli version help. + * + * @author Tim Sparg + */ +public class CliVersionProvider implements IVersionProvider { + + @Override + public String[] getVersion() { + String version = readVersion(); + return new String[] { Ansi.AUTO.string("@|bold spring-javaformat|@ @|cyan " + version + "|@") }; + } + + private String readVersion() { + try (InputStream in = CliVersionProvider.class + .getResourceAsStream("/io/spring/javaformat/cli/version.properties")) { + if (in != null) { + Properties properties = new Properties(); + properties.load(in); + String version = properties.getProperty("version"); + if (version != null && !version.isBlank()) { + return version; + } + } + } + catch (IOException ex) { + // fall through to unknown + } + return "unknown"; + } + +} diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/PicocliManaged.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/PicocliManaged.java new file mode 100644 index 00000000..6535c00f --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/PicocliManaged.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a class whose fields are initialized by picocli via field injection rather than + * via a constructor. This covers {@code @Spec}, {@code @Mixin}, and {@code @Option} + * fields with a {@code defaultValue}, all of which picocli sets before invoking the + * command. + * + *

+ * This annotation is registered with NullAway as an {@code ExternalInitAnnotation}, + * suppressing "field not initialized" errors for the annotated class without requiring + * per-class {@code @SuppressWarnings("NullAway.Init")}. + * + * @author Tim Sparg + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface PicocliManaged { + +} diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/PicocliRunner.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/PicocliRunner.java new file mode 100644 index 00000000..88343625 --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/PicocliRunner.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.ExitCodeGenerator; +import org.springframework.stereotype.Component; +import picocli.CommandLine; +import picocli.CommandLine.IFactory; + +/** + * Runs the picocli command line. + * + * @author Tim Sparg + */ +@Component +class PicocliRunner implements CommandLineRunner, ExitCodeGenerator { + + private final SpringJavaFormatCommand command; + + private final IFactory factory; + + private int exitCode; + + PicocliRunner(SpringJavaFormatCommand command, IFactory factory) { + this.command = command; + this.factory = factory; + } + + @Override + public void run(String... args) { + this.exitCode = new CommandLine(this.command, this.factory).execute(args); + } + + @Override + public int getExitCode() { + return this.exitCode; + } + +} diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/SpringJavaFormatCommand.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/SpringJavaFormatCommand.java new file mode 100644 index 00000000..b41bec0d --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/SpringJavaFormatCommand.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli; + +import org.springframework.stereotype.Component; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Spec; + +/** + * Root picocli command. + * + * @author Tim Sparg + */ +@Component +@Command(name = "spring-javaformat", mixinStandardHelpOptions = true, versionProvider = CliVersionProvider.class, + description = "Formats and checks Java source files") +@PicocliManaged +class SpringJavaFormatCommand implements Runnable { + + @Spec + private CommandSpec spec; + + @Override + public void run() { + this.spec.commandLine().usage(System.out); + } + +} diff --git a/spring-javaformat-cli/src/main/resources/io/spring/javaformat/cli/version.properties b/spring-javaformat-cli/src/main/resources/io/spring/javaformat/cli/version.properties new file mode 100644 index 00000000..a9ca5c52 --- /dev/null +++ b/spring-javaformat-cli/src/main/resources/io/spring/javaformat/cli/version.properties @@ -0,0 +1 @@ +version=@project.version@ diff --git a/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/SpringJavaFormatCliApplicationTests.java b/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/SpringJavaFormatCliApplicationTests.java new file mode 100644 index 00000000..9b1fa087 --- /dev/null +++ b/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/SpringJavaFormatCliApplicationTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import picocli.CommandLine; +import picocli.CommandLine.IFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Smoke test — verifies the Spring application context loads successfully. + * + * @author Tim Sparg + */ +@SpringBootTest(webEnvironment = WebEnvironment.NONE) +class SpringJavaFormatCliApplicationTests { + + @Autowired + private SpringJavaFormatCommand command; + + @Autowired + private IFactory factory; + + @Test + void helpIsPrinted() { + StringWriter out = new StringWriter(); + int exitCode = new CommandLine(this.command, this.factory).setOut(new PrintWriter(out)).execute("--help"); + assertThat(exitCode).isZero(); + assertThat(out.toString()).contains("Usage: spring-javaformat"); + } + + @Test + void versionIsPrinted() { + StringWriter out = new StringWriter(); + int exitCode = new CommandLine(this.command, this.factory).setOut(new PrintWriter(out)).execute("--version"); + assertThat(exitCode).isZero(); + assertThat(out.toString()).contains("spring-javaformat").containsPattern("\\S"); + } + +} From e22e812c4b8df37827f234be9b7e7e2ec60ccd8b Mon Sep 17 00:00:00 2001 From: Tim Sparg <6872586+timothysparg@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:18:01 +0100 Subject: [PATCH 03/10] Add file scanning infrastructure Introduces InputOptions (shared picocli mixin for path, includes, excludes, encoding, and line-separator), FileScanner (directory scanning using plexus DirectoryScanner), FileFormatterFactory (factory for creating a FileFormatter from a base directory), and ScanConfiguration (Spring configuration wiring the factory bean). Co-Authored-By: Claude Sonnet 4.6 --- .../spring/javaformat/cli/InputOptions.java | 113 ++++++++++++ .../cli/scan/FileFormatterFactory.java | 33 ++++ .../javaformat/cli/scan/FileScanner.java | 51 ++++++ .../cli/scan/ScanConfiguration.java | 40 +++++ .../javaformat/cli/scan/package-info.java | 25 +++ .../javaformat/cli/InputOptionsTests.java | 167 ++++++++++++++++++ 6 files changed, 429 insertions(+) create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/InputOptions.java create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/scan/FileFormatterFactory.java create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/scan/FileScanner.java create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/scan/ScanConfiguration.java create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/scan/package-info.java create mode 100644 spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/InputOptionsTests.java diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/InputOptions.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/InputOptions.java new file mode 100644 index 00000000..932be8fa --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/InputOptions.java @@ -0,0 +1,113 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli; + +import java.io.File; +import java.nio.charset.Charset; +import java.nio.file.FileSystems; + +import org.jspecify.annotations.Nullable; +import picocli.CommandLine.ITypeConverter; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.TypeConversionException; + +/** + * Shared options for commands that scan and format files. + * + * @author Tim Sparg + */ +@PicocliManaged +public class InputOptions { + + /** Base directory to scan. */ + @Parameters(index = "0", arity = "0..1", defaultValue = ".", paramLabel = "PATH", + description = "Base directory to scan. Default: ${DEFAULT-VALUE}", converter = DirectoryConverter.class) + public File path; + + /** Glob patterns for files to include. */ + @Option(names = { "-i", "--includes" }, split = ",", defaultValue = "**/*.java", + description = "Repeatable or comma-separated include patterns. Default: ${DEFAULT-VALUE}", + converter = GlobConverter.class) + public String[] includes; + + /** Glob patterns for files to exclude. */ + @Option(names = { "-x", "--excludes" }, split = ",", + defaultValue = "**/target/**,**/generated-sources/**,**/generated-test-sources/**", + description = "Repeatable or comma-separated exclude patterns. Default: ${DEFAULT-VALUE}", + converter = GlobConverter.class) + public String[] excludes; + + /** File encoding to use when reading and writing files. */ + @Option(names = { "-e", "--encoding" }, defaultValue = "UTF-8", + description = "File encoding. Default: ${DEFAULT-VALUE}") + public Charset encoding; + + @Option(names = { "-l", "--line-separator" }, + description = "Line separator (${COMPLETION-CANDIDATES}). If not specified, the existing line separator in each file is preserved.") + @Nullable + LineSeparator lineSeparator; + + @Nullable + public String resolveLineSeparator() { + return (this.lineSeparator != null) ? this.lineSeparator.value : null; + } + + enum LineSeparator { + + CR("\r"), LF("\n"), CRLF("\r\n"); + + private final String value; + + LineSeparator(String value) { + this.value = value; + } + + } + + static final class GlobConverter implements ITypeConverter { + + @Override + public String convert(String value) throws TypeConversionException { + try { + FileSystems.getDefault().getPathMatcher("glob:" + value); + } + catch (IllegalArgumentException ex) { + throw new TypeConversionException("Invalid glob pattern '" + value + "': " + ex.getMessage()); + } + return value; + } + + } + + static final class DirectoryConverter implements ITypeConverter { + + @Override + public File convert(String value) throws TypeConversionException { + File dir = new File(value).getAbsoluteFile(); + if (!dir.exists()) { + throw new TypeConversionException("Directory does not exist: " + dir); + } + if (!dir.isDirectory()) { + throw new TypeConversionException("Not a directory: " + dir); + } + return dir; + } + + } + +} diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/scan/FileFormatterFactory.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/scan/FileFormatterFactory.java new file mode 100644 index 00000000..4b61b8a3 --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/scan/FileFormatterFactory.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli.scan; + +import java.io.File; + +import io.spring.javaformat.formatter.FileFormatter; + +/** + * Factory for creating a {@link FileFormatter} from a base directory. + * + * @author Tim Sparg + */ +@FunctionalInterface +public interface FileFormatterFactory { + + FileFormatter create(File basedir); + +} diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/scan/FileScanner.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/scan/FileScanner.java new file mode 100644 index 00000000..f3c4c1da --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/scan/FileScanner.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli.scan; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.codehaus.plexus.util.DirectoryScanner; +import org.springframework.stereotype.Component; + +import io.spring.javaformat.cli.InputOptions; + +/** + * Scans files from a directory using include and exclude patterns. + * + * @author Tim Sparg + */ +@Component +public class FileScanner { + + public List scan(InputOptions options) { + DirectoryScanner scanner = new DirectoryScanner(); + scanner.setBasedir(options.path); + scanner.setIncludes(options.includes); + scanner.setExcludes(options.excludes); + scanner.addDefaultExcludes(); + scanner.setCaseSensitive(false); + scanner.setFollowSymlinks(false); + scanner.scan(); + return Arrays.stream(scanner.getIncludedFiles()) + .map((name) -> new File(options.path, name)) + .collect(Collectors.toList()); + } + +} diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/scan/ScanConfiguration.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/scan/ScanConfiguration.java new file mode 100644 index 00000000..609d6001 --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/scan/ScanConfiguration.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli.scan; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.spring.javaformat.config.JavaFormatConfig; +import io.spring.javaformat.formatter.FileFormatter; + + + +/** + * Spring configuration for the scan sub-system. + * + * @author Tim Sparg + */ +@Configuration +class ScanConfiguration { + + @Bean + FileFormatterFactory fileFormatterFactory() { + return (basedir) -> new FileFormatter(JavaFormatConfig.findFrom(basedir)); + } + +} diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/scan/package-info.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/scan/package-info.java new file mode 100644 index 00000000..7a2838e2 --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/scan/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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. + */ + +/** + * File scanning and formatting infrastructure shared across commands. + * + * @author Tim Sparg + */ +@NullMarked +package io.spring.javaformat.cli.scan; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/InputOptionsTests.java b/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/InputOptionsTests.java new file mode 100644 index 00000000..37ae8c79 --- /dev/null +++ b/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/InputOptionsTests.java @@ -0,0 +1,167 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.concurrent.Callable; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link InputOptions} — plain picocli unit tests with no Spring context. + * + * @author Tim Sparg + */ +class InputOptionsTests { + + private TestCommand command; + + private CommandLine commandLine; + + @BeforeEach + void setUp() { + this.command = new TestCommand(); + this.commandLine = new CommandLine(this.command); + } + + @Nested + class PathOption { + + @Test + void acceptsExistingDirectory(@TempDir Path tempDir) { + InputOptionsTests.this.commandLine.parseArgs(tempDir.toString()); + assertThat(InputOptionsTests.this.command.options.path).isEqualTo(tempDir.toFile().getAbsoluteFile()); + } + + @Test + void rejectsNonExistentDirectory() { + assertThatExceptionOfType(CommandLine.ParameterException.class) + .isThrownBy(() -> InputOptionsTests.this.commandLine.parseArgs( + "/nonexistent/path/that/does/not/exist")); + } + + @Test + void rejectsFile(@TempDir Path tempDir) throws IOException { + File file = tempDir.resolve("test.txt").toFile(); + file.createNewFile(); + assertThatExceptionOfType(CommandLine.ParameterException.class) + .isThrownBy(() -> InputOptionsTests.this.commandLine.parseArgs(file.getAbsolutePath())); + } + + } + + @Nested + class EncodingOption { + + @Test + void acceptsValidCharset() { + InputOptionsTests.this.commandLine.parseArgs("--encoding", "ISO-8859-1"); + assertThat(InputOptionsTests.this.command.options.encoding).isEqualTo(Charset.forName("ISO-8859-1")); + } + + @Test + void rejectsInvalidCharset() { + assertThatExceptionOfType(CommandLine.ParameterException.class) + .isThrownBy(() -> InputOptionsTests.this.commandLine.parseArgs("--encoding", "NOT-A-VALID-CHARSET")); + } + + } + + @Nested + class IncludesOption { + + @Test + void acceptsValidGlobPattern() { + InputOptionsTests.this.commandLine.parseArgs("--includes", "**/*.groovy"); + assertThat(InputOptionsTests.this.command.options.includes).containsExactly("**/*.groovy"); + } + + @Test + void acceptsMultiplePatternsSplitByComma() { + InputOptionsTests.this.commandLine.parseArgs("--includes", "**/*.java,**/*.kt"); + assertThat(InputOptionsTests.this.command.options.includes).containsExactly("**/*.java", "**/*.kt"); + } + + @Test + void rejectsInvalidGlobPattern() { + assertThatExceptionOfType(CommandLine.ParameterException.class) + .isThrownBy(() -> InputOptionsTests.this.commandLine.parseArgs("--includes", "[invalid")); + } + + } + + @Nested + class ExcludesOption { + + @Test + void acceptsValidGlobPattern() { + InputOptionsTests.this.commandLine.parseArgs("--excludes", "**/build/**"); + assertThat(InputOptionsTests.this.command.options.excludes).containsExactly("**/build/**"); + } + + @Test + void rejectsInvalidGlobPattern() { + assertThatExceptionOfType(CommandLine.ParameterException.class) + .isThrownBy(() -> InputOptionsTests.this.commandLine.parseArgs("--excludes", "[invalid")); + } + + } + + @Nested + class LineSeparatorOption { + + @Test + void acceptsCr() { + InputOptionsTests.this.commandLine.parseArgs("--line-separator", "CR"); + assertThat(InputOptionsTests.this.command.options.resolveLineSeparator()).isEqualTo("\r"); + } + + @Test + void rejectsInvalidValue() { + assertThatExceptionOfType(CommandLine.ParameterException.class) + .isThrownBy(() -> InputOptionsTests.this.commandLine.parseArgs("--line-separator", "INVALID")); + } + + } + + @PicocliManaged + @Command(name = "test") + private static class TestCommand implements Callable { + + @Mixin + InputOptions options; + + @Override + public Integer call() { + return 0; + } + + } + +} From dc6fae8275b4a8c934e73df048d9dbc824d22619 Mon Sep 17 00:00:00 2001 From: Tim Sparg <6872586+timothysparg@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:19:37 +0100 Subject: [PATCH 04/10] Add apply command Introduces the apply subcommand which formats Java source files in place. FormattingService wraps the FileFormatter and takes a pre-scanned file list so the directory scan is not repeated. Includes integration tests covering default formatting, encoding, includes/excludes filtering, and springjavaformatconfig discovery. Co-Authored-By: Claude Sonnet 4.6 --- .../cli/SpringJavaFormatCommand.java | 3 + .../javaformat/cli/format/ApplyCommand.java | 99 +++++++++++++++ .../cli/format/FormattingService.java | 58 +++++++++ .../javaformat/cli/format/package-info.java | 25 ++++ .../cli/AbstractCommandIntegrationTests.java | 69 +++++++++++ .../cli/ApplyCommandIntegrationTests.java | 113 ++++++++++++++++++ .../fixtures/format/default/Main.java | 4 + .../format/default/excluded/Excluded.java | 4 + .../fixtures/format/default/notes.txt | 1 + .../fixtures/format/latin1/Main.java | 5 + .../format/latin1/excluded/Excluded.java | 4 + .../fixtures/format/latin1/notes.txt | 1 + .../spaces-config/.springjavaformatconfig | 1 + .../fixtures/format/spaces-config/Main.java | 4 + 14 files changed, 391 insertions(+) create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/format/ApplyCommand.java create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/format/FormattingService.java create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/format/package-info.java create mode 100644 spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/AbstractCommandIntegrationTests.java create mode 100644 spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/ApplyCommandIntegrationTests.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/format/default/Main.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/format/default/excluded/Excluded.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/format/default/notes.txt create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/format/latin1/Main.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/format/latin1/excluded/Excluded.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/format/latin1/notes.txt create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/format/spaces-config/.springjavaformatconfig create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/format/spaces-config/Main.java diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/SpringJavaFormatCommand.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/SpringJavaFormatCommand.java index b41bec0d..16b9e1a3 100644 --- a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/SpringJavaFormatCommand.java +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/SpringJavaFormatCommand.java @@ -21,6 +21,8 @@ import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Spec; +import io.spring.javaformat.cli.format.ApplyCommand; + /** * Root picocli command. * @@ -28,6 +30,7 @@ */ @Component @Command(name = "spring-javaformat", mixinStandardHelpOptions = true, versionProvider = CliVersionProvider.class, + subcommands = { ApplyCommand.class }, description = "Formats and checks Java source files") @PicocliManaged class SpringJavaFormatCommand implements Runnable { diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/format/ApplyCommand.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/format/ApplyCommand.java new file mode 100644 index 00000000..7f027e7b --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/format/ApplyCommand.java @@ -0,0 +1,99 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli.format; + +import java.io.File; +import java.io.PrintWriter; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.Callable; + +import org.springframework.stereotype.Component; +import picocli.CommandLine.Command; +import picocli.CommandLine.Help.Ansi; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Spec; + +import io.spring.javaformat.cli.InputOptions; +import io.spring.javaformat.formatter.FileEdit; +import io.spring.javaformat.formatter.FileFormatterException; + +/** + * Apply formatting command. + * + * @author Tim Sparg + */ +@Component +@Command(name = "apply", mixinStandardHelpOptions = true, description = "Apply formatting to the codebase") +public class ApplyCommand implements Callable { + + private static final Path CWD = Paths.get("").toAbsolutePath(); + + @Spec + private CommandSpec spec; + + @Mixin + private InputOptions options; + + private final FormattingService formattingService; + + @SuppressWarnings("NullAway.Init") + ApplyCommand(FormattingService formattingService) { + this.formattingService = formattingService; + } + + @Override + public Integer call() { + try { + this.formattingService.format(this.options).filter(FileEdit::hasEdits).forEach(this::save); + return 0; + } + catch (FileFormatterException ex) { + err().println(ansi( + "@|bold,red error:|@ unable to format file " + relativize(ex.getFile()) + ": " + ex.getMessage())); + return 1; + } + } + + private PrintWriter err() { + return this.spec.commandLine().getErr(); + } + + private PrintWriter out() { + return this.spec.commandLine().getOut(); + } + + private void save(FileEdit edit) { + out().println(ansi("@|bold,green formatted|@ " + relativize(edit.getFile()))); + edit.save(); + } + + private String ansi(String markup) { + return Ansi.AUTO.string(markup); + } + + private static String relativize(File file) { + try { + return CWD.relativize(file.toPath().toAbsolutePath()).toString(); + } + catch (IllegalArgumentException ex) { + return file.getAbsolutePath(); + } + } + +} diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/format/FormattingService.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/format/FormattingService.java new file mode 100644 index 00000000..601d0b94 --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/format/FormattingService.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli.format; + +import java.io.File; +import java.util.List; +import java.util.stream.Stream; + +import org.springframework.stereotype.Component; + +import io.spring.javaformat.cli.InputOptions; +import io.spring.javaformat.cli.scan.FileFormatterFactory; +import io.spring.javaformat.cli.scan.FileScanner; +import io.spring.javaformat.formatter.FileEdit; +import io.spring.javaformat.formatter.FileFormatter; +import io.spring.javaformat.formatter.FileFormatterException; + +/** + * Applies formatting to scanned files. + * + * @author Tim Sparg + */ +@Component +public class FormattingService { + + private final FileFormatterFactory fileFormatterFactory; + + private final FileScanner fileScanner; + + FormattingService(FileFormatterFactory fileFormatterFactory, FileScanner fileScanner) { + this.fileFormatterFactory = fileFormatterFactory; + this.fileScanner = fileScanner; + } + + public Stream format(InputOptions options) throws FileFormatterException { + return format(this.fileScanner.scan(options), options); + } + + public Stream format(List files, InputOptions options) throws FileFormatterException { + FileFormatter formatter = this.fileFormatterFactory.create(options.path); + return formatter.formatFiles(files, options.encoding, options.resolveLineSeparator()); + } + +} diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/format/package-info.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/format/package-info.java new file mode 100644 index 00000000..73e21b93 --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/format/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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. + */ + +/** + * Apply formatting command. + * + * @author Tim Sparg + */ +@NullMarked +package io.spring.javaformat.cli.format; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/AbstractCommandIntegrationTests.java b/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/AbstractCommandIntegrationTests.java new file mode 100644 index 00000000..17e67689 --- /dev/null +++ b/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/AbstractCommandIntegrationTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.Callable; +import java.util.stream.Stream; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import picocli.CommandLine; +import picocli.CommandLine.IFactory; + +/** + * Base class for command integration tests. + * + * @author Tim Sparg + */ +@SpringBootTest(webEnvironment = WebEnvironment.NONE) +abstract class AbstractCommandIntegrationTests { + + @Autowired + IFactory factory; + + abstract Callable command(); + + int execute(StringWriter out, StringWriter err, String... args) { + return new CommandLine(command(), this.factory).setOut(new PrintWriter(out)) + .setErr(new PrintWriter(err)) + .execute(args); + } + + void copyFixture(Path fixturesDir, String name, Path target) throws IOException { + Path fixtureDir = fixturesDir.resolve(name); + try (Stream files = Files.walk(fixtureDir)) { + files.filter(Files::isRegularFile).forEach((source) -> { + try { + Path dest = target.resolve(fixtureDir.relativize(source)); + Files.createDirectories(dest.getParent()); + Files.copy(source, dest); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); + } + } + +} diff --git a/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/ApplyCommandIntegrationTests.java b/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/ApplyCommandIntegrationTests.java new file mode 100644 index 00000000..b4083564 --- /dev/null +++ b/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/ApplyCommandIntegrationTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli; + +import java.io.StringWriter; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.beans.factory.annotation.Autowired; + +import io.spring.javaformat.cli.format.ApplyCommand; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ApplyCommand}. + * + * @author Tim Sparg + */ +class ApplyCommandIntegrationTests extends AbstractCommandIntegrationTests { + + private static final Path FIXTURES_DIR = Path.of("src/test/resources/fixtures/format"); + + @Autowired + ApplyCommand applyCommand; + + @Override + ApplyCommand command() { + return this.applyCommand; + } + + @Test + void defaultEncodingFormatsFile(@TempDir Path tempDir) throws Exception { + copyFixture(FIXTURES_DIR, "default", tempDir); + String before = Files.readString(tempDir.resolve("Main.java")); + + execute(new StringWriter(), new StringWriter(), tempDir.toString()); + + assertThat(Files.readString(tempDir.resolve("Main.java"))).isNotEqualTo(before); + } + + @Test + void nonDefaultEncodingFormatsFile(@TempDir Path tempDir) throws Exception { + Charset latin1 = Charset.forName("ISO-8859-1"); + copyFixture(FIXTURES_DIR, "latin1", tempDir); + String before = Files.readString(tempDir.resolve("Main.java"), latin1); + + execute(new StringWriter(), new StringWriter(), "--encoding", "ISO-8859-1", tempDir.toString()); + + String after = Files.readString(tempDir.resolve("Main.java"), latin1); + assertThat(after).isNotEqualTo(before); + assertThat(after).contains("é"); + } + + @Test + void includesOnlyFormatsMatchingFiles(@TempDir Path tempDir) throws Exception { + copyFixture(FIXTURES_DIR, "default", tempDir); + String mainBefore = readFile(tempDir, "Main.java"); + String noteBefore = Files.readString(tempDir.resolve("notes.txt")); + String excludedBefore = readFile(tempDir, "excluded/Excluded.java"); + + execute(new StringWriter(), new StringWriter(), "--includes", "**/Main.java", tempDir.toString()); + + assertThat(readFile(tempDir, "Main.java")).isNotEqualTo(mainBefore); + assertThat(Files.readString(tempDir.resolve("notes.txt"))).isEqualTo(noteBefore); + assertThat(readFile(tempDir, "excluded/Excluded.java")).isEqualTo(excludedBefore); + } + + @Test + void excludesSkipsExcludedFiles(@TempDir Path tempDir) throws Exception { + copyFixture(FIXTURES_DIR, "default", tempDir); + String mainBefore = readFile(tempDir, "Main.java"); + String excludedBefore = readFile(tempDir, "excluded/Excluded.java"); + + execute(new StringWriter(), new StringWriter(), "--excludes", "excluded/**", tempDir.toString()); + + assertThat(readFile(tempDir, "Main.java")).isNotEqualTo(mainBefore); + assertThat(readFile(tempDir, "excluded/Excluded.java")).isEqualTo(excludedBefore); + } + + @Test + void springJavaFormatConfigChangesIndentationStyle(@TempDir Path tempDir) throws Exception { + copyFixture(FIXTURES_DIR, "spaces-config", tempDir); + + execute(new StringWriter(), new StringWriter(), tempDir.toString()); + + String formatted = readFile(tempDir, "Main.java"); + assertThat(formatted).contains(" void method() {"); + assertThat(formatted).doesNotContain("\tvoid method() {"); + } + + private String readFile(Path dir, String path) throws Exception { + return Files.readString(dir.resolve(path)); + } + +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/format/default/Main.java b/spring-javaformat-cli/src/test/resources/fixtures/format/default/Main.java new file mode 100644 index 00000000..273a084c --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/format/default/Main.java @@ -0,0 +1,4 @@ +class Unformatted { + void method() { + } +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/format/default/excluded/Excluded.java b/spring-javaformat-cli/src/test/resources/fixtures/format/default/excluded/Excluded.java new file mode 100644 index 00000000..273a084c --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/format/default/excluded/Excluded.java @@ -0,0 +1,4 @@ +class Unformatted { + void method() { + } +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/format/default/notes.txt b/spring-javaformat-cli/src/test/resources/fixtures/format/default/notes.txt new file mode 100644 index 00000000..cf88e040 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/format/default/notes.txt @@ -0,0 +1 @@ +some notes diff --git a/spring-javaformat-cli/src/test/resources/fixtures/format/latin1/Main.java b/spring-javaformat-cli/src/test/resources/fixtures/format/latin1/Main.java new file mode 100644 index 00000000..493cd23b --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/format/latin1/Main.java @@ -0,0 +1,5 @@ +// é comment +class Unformatted { + void method() { + } +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/format/latin1/excluded/Excluded.java b/spring-javaformat-cli/src/test/resources/fixtures/format/latin1/excluded/Excluded.java new file mode 100644 index 00000000..273a084c --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/format/latin1/excluded/Excluded.java @@ -0,0 +1,4 @@ +class Unformatted { + void method() { + } +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/format/latin1/notes.txt b/spring-javaformat-cli/src/test/resources/fixtures/format/latin1/notes.txt new file mode 100644 index 00000000..cf88e040 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/format/latin1/notes.txt @@ -0,0 +1 @@ +some notes diff --git a/spring-javaformat-cli/src/test/resources/fixtures/format/spaces-config/.springjavaformatconfig b/spring-javaformat-cli/src/test/resources/fixtures/format/spaces-config/.springjavaformatconfig new file mode 100644 index 00000000..881903b2 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/format/spaces-config/.springjavaformatconfig @@ -0,0 +1 @@ +indentation-style=spaces diff --git a/spring-javaformat-cli/src/test/resources/fixtures/format/spaces-config/Main.java b/spring-javaformat-cli/src/test/resources/fixtures/format/spaces-config/Main.java new file mode 100644 index 00000000..8548401d --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/format/spaces-config/Main.java @@ -0,0 +1,4 @@ +class Test { +void method() { +} +} From 1bf591d9c9168c7f53f5d8af3e28c7b7411411ff Mon Sep 17 00:00:00 2001 From: Tim Sparg <6872586+timothysparg@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:22:38 +0100 Subject: [PATCH 05/10] Add check command with JTE output rendering Introduces the check subcommand which validates source formatting and Spring checkstyle rules. Output is rendered via JTE templates (checkSuccess, checkError, checkFormatting, checkCheckstyleDetailed, checkCombined) with ANSI colour styling. CheckRunner scans files once and shares the list between the formatter and CheckstyleService. Includes integration tests for formatting, checkstyle, combined output, header types, import order, and includes/excludes filtering. Co-Authored-By: Claude Sonnet 4.6 --- spring-javaformat-cli/pom.xml | 47 ++++++ .../cli/SpringJavaFormatCommand.java | 3 +- .../javaformat/cli/check/CheckCommand.java | 137 ++++++++++++++++ .../javaformat/cli/check/CheckReport.java | 48 ++++++ .../cli/check/CheckReportRenderer.java | 74 +++++++++ .../javaformat/cli/check/CheckRunner.java | 78 +++++++++ .../cli/check/CheckstyleService.java | 116 ++++++++++++++ .../javaformat/cli/check/RenderHelper.java | 106 ++++++++++++ .../javaformat/cli/check/package-info.java | 25 +++ .../src/main/jte/checkCheckstyleDetailed.jte | 11 ++ .../src/main/jte/checkCombined.jte | 11 ++ .../src/main/jte/checkError.jte | 3 + .../src/main/jte/checkFormatting.jte | 8 + .../src/main/jte/checkSuccess.jte | 2 + .../javaformat/cli/check/checkstyle.xml | 13 ++ .../cli/CheckCommandIntegrationTests.java | 151 ++++++++++++++++++ .../SpringJavaFormatCliApplicationTests.java | 2 +- .../check/apache2-header-missing/Main.java | 5 + .../apache2-header-missing/package-info.java | 4 + .../fixtures/check/combined/Main.java | 6 + .../fixtures/check/custom-header/Main.java | 7 + .../fixtures/check/custom-header/header.txt | 1 + .../check/custom-header/package-info.java | 6 + .../check/excludes-filter/WithStarImport.java | 5 + .../excluded/ExcludedWithStarImport.java | 5 + .../check/includes-filter/WithStarImport.java | 5 + .../subdir/ExcludedFromIncludes.java | 5 + .../fixtures/check/no-header-none/Main.java | 5 + .../check/no-header-none/package-info.java | 4 + .../check/non-spring-import-order/Main.java | 17 ++ .../non-spring-import-order/package-info.java | 4 + .../check/spring-import-order/Main.java | 17 ++ .../spring-import-order/package-info.java | 4 + .../check/star-import/WithStarImport.java | 5 + .../check/static-import-excluded/Main.java | 11 ++ .../static-import-excluded/package-info.java | 4 + .../check/static-import-violation/Main.java | 11 ++ .../static-import-violation/package-info.java | 4 + 38 files changed, 968 insertions(+), 2 deletions(-) create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckCommand.java create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckReport.java create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckReportRenderer.java create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckRunner.java create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckstyleService.java create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/RenderHelper.java create mode 100644 spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/package-info.java create mode 100644 spring-javaformat-cli/src/main/jte/checkCheckstyleDetailed.jte create mode 100644 spring-javaformat-cli/src/main/jte/checkCombined.jte create mode 100644 spring-javaformat-cli/src/main/jte/checkError.jte create mode 100644 spring-javaformat-cli/src/main/jte/checkFormatting.jte create mode 100644 spring-javaformat-cli/src/main/jte/checkSuccess.jte create mode 100644 spring-javaformat-cli/src/main/resources/io/spring/javaformat/cli/check/checkstyle.xml create mode 100644 spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/CheckCommandIntegrationTests.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/apache2-header-missing/Main.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/apache2-header-missing/package-info.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/combined/Main.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/custom-header/Main.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/custom-header/header.txt create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/custom-header/package-info.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/excludes-filter/WithStarImport.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/excludes-filter/excluded/ExcludedWithStarImport.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/includes-filter/WithStarImport.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/includes-filter/subdir/ExcludedFromIncludes.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/no-header-none/Main.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/no-header-none/package-info.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/non-spring-import-order/Main.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/non-spring-import-order/package-info.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/spring-import-order/Main.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/spring-import-order/package-info.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/star-import/WithStarImport.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/static-import-excluded/Main.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/static-import-excluded/package-info.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/static-import-violation/Main.java create mode 100644 spring-javaformat-cli/src/test/resources/fixtures/check/static-import-violation/package-info.java diff --git a/spring-javaformat-cli/pom.xml b/spring-javaformat-cli/pom.xml index c6014d6b..1b9f5dcc 100644 --- a/spring-javaformat-cli/pom.xml +++ b/spring-javaformat-cli/pom.xml @@ -14,6 +14,7 @@ 17 17 17 + 3.2.3 @@ -73,6 +74,17 @@ plexus-utils + + gg.jte + jte-runtime + ${jte.version} + + + gg.jte + jte-models + ${jte.version} + + org.springframework.boot @@ -140,6 +152,41 @@ + + gg.jte + jte-maven-plugin + ${jte.version} + + ${project.basedir}/src/main/jte + Plain + + + gg.jte.models.generator.ModelExtension + + @org.jspecify.annotations.NullMarked + @org.jspecify.annotations.NullMarked + Java + + + + + + + generate + generate-sources + + generate + + + + + + gg.jte + jte-models + ${jte.version} + + + org.springframework.experimental spring-aot-maven-plugin diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/SpringJavaFormatCommand.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/SpringJavaFormatCommand.java index 16b9e1a3..fd206a69 100644 --- a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/SpringJavaFormatCommand.java +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/SpringJavaFormatCommand.java @@ -21,6 +21,7 @@ import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Spec; +import io.spring.javaformat.cli.check.CheckCommand; import io.spring.javaformat.cli.format.ApplyCommand; /** @@ -30,7 +31,7 @@ */ @Component @Command(name = "spring-javaformat", mixinStandardHelpOptions = true, versionProvider = CliVersionProvider.class, - subcommands = { ApplyCommand.class }, + subcommands = { ApplyCommand.class, CheckCommand.class }, description = "Formats and checks Java source files") @PicocliManaged class SpringJavaFormatCommand implements Runnable { diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckCommand.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckCommand.java new file mode 100644 index 00000000..7dff6fc3 --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckCommand.java @@ -0,0 +1,137 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli.check; + +import java.util.Properties; +import java.util.concurrent.Callable; + +import org.springframework.stereotype.Component; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.Spec; + +import io.spring.javaformat.cli.InputOptions; + +/** + * Check formatting and checkstyle violations. + * + * @author Tim Sparg + */ +@Component +@Command(name = "check", mixinStandardHelpOptions = true, + description = "Check the codebase for formatting and checkstyle violations") +public class CheckCommand implements Callable { + + @Spec + private CommandSpec spec; + + @Mixin + private InputOptions formatOptions; + + @Option(names = { "-t", "--header-type" }, defaultValue = "APACHE2", + description = "Header type (${COMPLETION-CANDIDATES}). Default: ${DEFAULT-VALUE}") + private HeaderType headerType; + + @Option(names = { "-c", "--header-copyright-pattern" }, defaultValue = "20\\d\\d(-20\\d\\d|-present)?", + description = "Copyright year regex pattern. Default: ${DEFAULT-VALUE}") + private String headerCopyrightPattern; + + @Option(names = { "-f", "--header-file" }, defaultValue = "", + description = "Path to a custom header file. Required when --header-type is 'file'.") + private String headerFile; + + @Option(names = { "-r", "--project-root-package" }, defaultValue = "org.springframework", + description = "Root package used for import ordering. Default: ${DEFAULT-VALUE}") + private String projectRootPackage; + + @Option(names = { "-s", "--avoid-static-import-excludes" }, split = ",", defaultValue = "", + description = "Repeatable or comma-separated static import patterns to allow.") + private String[] avoidStaticImportExcludes; + + @ArgGroup + private RunMode runMode; + + private final CheckRunner checkRunner; + + private final CheckReportRenderer checkReportRenderer; + + @SuppressWarnings("NullAway.Init") + CheckCommand(CheckRunner checkRunner, CheckReportRenderer checkReportRenderer) { + this.checkRunner = checkRunner; + this.checkReportRenderer = checkReportRenderer; + } + + @Override + public Integer call() { + CheckRunner.Inputs inputs = createInputs(); + CheckReport report = this.checkRunner.run(inputs); + this.checkReportRenderer.render(this.spec.commandLine(), report); + return report.exitCode(); + } + + private CheckRunner.Inputs createInputs() { + boolean skipCheckstyle = this.runMode != null && this.runMode.skipCheckstyle; + if (!skipCheckstyle && this.headerType == HeaderType.FILE && this.headerFile.isEmpty()) { + throw new ParameterException(this.spec.commandLine(), + "--header-file is required when --header-type is FILE"); + } + return new CheckRunner.Inputs(this.formatOptions, buildCheckstyleProperties(), + this.runMode != null && this.runMode.skipFormat, skipCheckstyle); + } + + private Properties buildCheckstyleProperties() { + Properties properties = new Properties(); + properties.setProperty("headerType", this.headerType.value); + properties.setProperty("headerCopyrightPattern", this.headerCopyrightPattern); + if (!this.headerFile.isEmpty()) { + properties.setProperty("headerFile", this.headerFile); + } + properties.setProperty("projectRootPackage", this.projectRootPackage); + properties.setProperty("avoidStaticImportExcludes", String.join(",", + java.util.Arrays.stream(this.avoidStaticImportExcludes).filter((s) -> !s.isEmpty()).toList())); + return properties; + } + + static class RunMode { + + @Option(names = { "-S", "--skip-checkstyle" }, defaultValue = "false", + description = "Skip checkstyle checks, only check source formatting") + boolean skipCheckstyle; + + @Option(names = { "-F", "--skip-format" }, defaultValue = "false", + description = "Skip source formatting check, only run checkstyle") + boolean skipFormat; + + } + + enum HeaderType { + + APACHE2("apache2"), NONE("none"), UNCHECKED("unchecked"), REGEXP("regexp"), FILE("file"); + + private final String value; + + HeaderType(String value) { + this.value = value; + } + + } + +} diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckReport.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckReport.java new file mode 100644 index 00000000..c0dd41f1 --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckReport.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli.check; + +import java.io.File; +import java.util.List; + +import com.puppycrawl.tools.checkstyle.api.AuditEvent; +import org.jspecify.annotations.Nullable; + +record CheckReport(List formattingProblems, List checkstyleViolations, boolean skipFormat, + boolean skipCheckstyle, @Nullable String errorMessage, boolean checkstyleFailure) { + + static CheckReport failure(boolean skipFormat, boolean skipCheckstyle, String errorMessage, boolean checkstyleFailure) { + return new CheckReport(List.of(), List.of(), skipFormat, skipCheckstyle, errorMessage, checkstyleFailure); + } + + boolean hasViolations() { + return !this.formattingProblems.isEmpty() || !this.checkstyleViolations.isEmpty(); + } + + boolean hasError() { + return this.errorMessage != null; + } + + int exitCode() { + return (hasError() || hasViolations()) ? 1 : 0; + } + + boolean combined() { + return !this.skipFormat && !this.skipCheckstyle; + } + +} diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckReportRenderer.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckReportRenderer.java new file mode 100644 index 00000000..f56d36b8 --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckReportRenderer.java @@ -0,0 +1,74 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli.check; + +import java.util.Objects; + +import gg.jte.generated.precompiled.StaticTemplates; +import gg.jte.generated.precompiled.Templates; +import gg.jte.output.PrintWriterOutput; +import org.springframework.stereotype.Component; +import picocli.CommandLine; + +@Component +public class CheckReportRenderer { + + private final Templates templates = new StaticTemplates(); + + void render(CommandLine commandLine, CheckReport report) { + if (report.hasError()) { + renderError(commandLine, report); + } + else if (!report.hasViolations()) { + renderSuccess(commandLine); + } + else if (report.combined()) { + renderCombined(commandLine, report); + } + else { + renderSeparate(commandLine, report); + } + commandLine.getOut().flush(); + commandLine.getErr().flush(); + } + + private void renderError(CommandLine commandLine, CheckReport report) { + this.templates.checkError(Objects.requireNonNull(report.errorMessage())) + .render(new PrintWriterOutput(commandLine.getErr())); + } + + private void renderSuccess(CommandLine commandLine) { + this.templates.checkSuccess().render(new PrintWriterOutput(commandLine.getOut())); + } + + private void renderCombined(CommandLine commandLine, CheckReport report) { + this.templates.checkCombined(report.formattingProblems(), report.checkstyleViolations()) + .render(new PrintWriterOutput(commandLine.getErr())); + } + + private void renderSeparate(CommandLine commandLine, CheckReport report) { + if (!report.skipFormat()) { + this.templates.checkFormatting(report.formattingProblems()) + .render(new PrintWriterOutput(commandLine.getErr())); + } + if (!report.skipCheckstyle()) { + this.templates.checkCheckstyleDetailed(report.checkstyleViolations()) + .render(new PrintWriterOutput(commandLine.getErr())); + } + } + +} diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckRunner.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckRunner.java new file mode 100644 index 00000000..38dff656 --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckRunner.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli.check; + +import java.io.File; +import java.util.List; +import java.util.Properties; +import java.util.stream.Collectors; + +import com.puppycrawl.tools.checkstyle.api.AuditEvent; +import com.puppycrawl.tools.checkstyle.api.CheckstyleException; +import org.springframework.stereotype.Component; + +import io.spring.javaformat.cli.InputOptions; +import io.spring.javaformat.cli.format.FormattingService; +import io.spring.javaformat.cli.scan.FileScanner; +import io.spring.javaformat.formatter.FileEdit; + +@Component +public class CheckRunner { + + private final FormattingService formattingService; + + private final CheckstyleService checkstyleService; + + private final FileScanner fileScanner; + + CheckRunner(FormattingService formattingService, CheckstyleService checkstyleService, FileScanner fileScanner) { + this.formattingService = formattingService; + this.checkstyleService = checkstyleService; + this.fileScanner = fileScanner; + } + + CheckReport run(Inputs inputs) { + try { + List files = this.fileScanner.scan(inputs.formatOptions()); + List formattingProblems = inputs.skipFormat() ? List.of() : collectFormattingProblems(files, inputs); + List checkstyleViolations = inputs.skipCheckstyle() ? List.of() + : this.checkstyleService.run(files, inputs.checkstyleProperties()); + return new CheckReport(formattingProblems, checkstyleViolations, inputs.skipFormat(), inputs.skipCheckstyle(), + null, false); + } + catch (CheckstyleException ex) { + return CheckReport.failure(inputs.skipFormat(), inputs.skipCheckstyle(), + "unable to run checkstyle: " + ex.getMessage(), true); + } + catch (Exception ex) { + return CheckReport.failure(inputs.skipFormat(), inputs.skipCheckstyle(), + "unable to check formatting: " + ex.getMessage(), false); + } + } + + private List collectFormattingProblems(List files, Inputs inputs) throws Exception { + return this.formattingService.format(files, inputs.formatOptions()) + .filter(FileEdit::hasEdits) + .map(FileEdit::getFile) + .collect(Collectors.toList()); + } + + record Inputs(InputOptions formatOptions, Properties checkstyleProperties, + boolean skipFormat, boolean skipCheckstyle) { + } + +} diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckstyleService.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckstyleService.java new file mode 100644 index 00000000..4dbfc718 --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/CheckstyleService.java @@ -0,0 +1,116 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli.check; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import com.puppycrawl.tools.checkstyle.Checker; +import com.puppycrawl.tools.checkstyle.ConfigurationLoader; +import com.puppycrawl.tools.checkstyle.ConfigurationLoader.IgnoredModulesOptions; +import com.puppycrawl.tools.checkstyle.ModuleFactory; +import com.puppycrawl.tools.checkstyle.PackageObjectFactory; +import com.puppycrawl.tools.checkstyle.PropertiesExpander; +import com.puppycrawl.tools.checkstyle.ThreadModeSettings; +import com.puppycrawl.tools.checkstyle.api.AuditEvent; +import com.puppycrawl.tools.checkstyle.api.AuditListener; +import com.puppycrawl.tools.checkstyle.api.CheckstyleException; +import com.puppycrawl.tools.checkstyle.api.Configuration; +import com.puppycrawl.tools.checkstyle.api.RootModule; +import com.puppycrawl.tools.checkstyle.api.SeverityLevel; +import org.springframework.stereotype.Component; +import org.xml.sax.InputSource; + +/** + * Runs Spring checkstyle checks against a list of files. + * + * @author Tim Sparg + */ +@Component +class CheckstyleService { + + List run(List files, Properties properties) throws CheckstyleException { + try (InputStream is = getClass().getResourceAsStream("checkstyle.xml")) { + return run(files, is, properties); + } + catch (Exception ex) { + throw new CheckstyleException("Failed to load checkstyle configuration", ex); + } + } + + private List run(List files, InputStream configStream, Properties properties) + throws CheckstyleException { + Configuration config = ConfigurationLoader.loadConfiguration(new InputSource(configStream), + new PropertiesExpander(properties), IgnoredModulesOptions.EXECUTE, + ThreadModeSettings.SINGLE_THREAD_MODE_INSTANCE); + ClassLoader cl = getClass().getClassLoader(); + ModuleFactory factory = new PackageObjectFactory(Checker.class.getPackage().getName(), cl); + RootModule rootModule = (RootModule) factory.createModule(config.getName()); + rootModule.setModuleClassLoader(cl); + rootModule.configure(config); + CollectingAuditListener listener = new CollectingAuditListener(); + rootModule.addListener(listener); + try { + rootModule.process(files); + } + finally { + rootModule.destroy(); + } + return listener.getViolations(); + } + + private static final class CollectingAuditListener implements AuditListener { + + private final List violations = new ArrayList<>(); + + @Override + public void auditStarted(AuditEvent event) { + } + + @Override + public void auditFinished(AuditEvent event) { + } + + @Override + public void fileStarted(AuditEvent event) { + } + + @Override + public void fileFinished(AuditEvent event) { + } + + @Override + public void addError(AuditEvent event) { + if (event.getSeverityLevel() != SeverityLevel.IGNORE) { + this.violations.add(event); + } + } + + @Override + public void addException(AuditEvent event, Throwable throwable) { + } + + List getViolations() { + return this.violations; + } + + } + +} diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/RenderHelper.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/RenderHelper.java new file mode 100644 index 00000000..645c66f6 --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/RenderHelper.java @@ -0,0 +1,106 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli.check; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.puppycrawl.tools.checkstyle.api.AuditEvent; +import picocli.CommandLine.Help.Ansi; + +public final class RenderHelper { + + private static final Path CWD = Paths.get("").toAbsolutePath(); + + private RenderHelper() { + } + + public static String plural(long count, String noun) { + return count + " " + noun + (count == 1 ? "" : "s"); + } + + public static String styledPath(File file) { + return Ansi.AUTO.string("@|cyan " + relativize(file) + "|@"); + } + + public static String styledPath(String path) { + return Ansi.AUTO.string("@|cyan " + path + "|@"); + } + + public static String styledLineNumber(int line) { + return Ansi.AUTO.string("@|bold " + line + "|@"); + } + + public static String styledMessage(String message) { + return Ansi.AUTO.string("@|faint " + message + "|@"); + } + + public static String success(String message) { + return Ansi.AUTO.string("@|bold,green " + message + "|@"); + } + + public static String error(String message) { + return Ansi.AUTO.string("@|bold,red error:|@ " + message); + } + + public static String formattingHeader(int count) { + return "\n" + Ansi.AUTO.string("@|bold,underline,red Formatting violations found|@") + " in " + + plural(count, "file") + ":"; + } + + public static String checkstyleHeader(List violations) { + long fileCount = violations.stream().map(AuditEvent::getFileName).distinct().count(); + return "\n" + Ansi.AUTO.string("@|bold,underline,red Checkstyle violations found|@") + " in " + + plural(fileCount, "file") + " (" + plural(violations.size(), "violation") + "):"; + } + + public static String combinedHeader(int count) { + return "\n" + Ansi.AUTO.string("@|bold,underline,red Violations found|@") + " in " + plural(count, "file") + + ":"; + } + + public static List combinedLines(List formattingProblems, List checkstyleViolations) { + return Stream + .concat(formattingProblems.stream().map((f) -> Map.entry(relativize(f), "format")), + checkstyleViolations.stream() + .map((v) -> Map.entry(relativize(new File(v.getFileName())), "checkstyle"))) + .collect(Collectors.groupingBy(Map.Entry::getKey, LinkedHashMap::new, + Collectors.mapping(Map.Entry::getValue, Collectors.toCollection(LinkedHashSet::new)))) + .entrySet() + .stream() + .map((e) -> styledPath(e.getKey()) + " " + + Ansi.AUTO.string(e.getValue().stream().collect(Collectors.joining(", ", "@|faint [", "]|@")))) + .toList(); + } + + public static String relativize(File file) { + try { + return CWD.relativize(file.toPath().toAbsolutePath()).toString(); + } + catch (IllegalArgumentException ex) { + return file.getAbsolutePath(); + } + } + +} diff --git a/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/package-info.java b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/package-info.java new file mode 100644 index 00000000..3dedef66 --- /dev/null +++ b/spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/check/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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. + */ + +/** + * Checkstyle-based check command. + * + * @author Tim Sparg + */ +@NullMarked +package io.spring.javaformat.cli.check; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-javaformat-cli/src/main/jte/checkCheckstyleDetailed.jte b/spring-javaformat-cli/src/main/jte/checkCheckstyleDetailed.jte new file mode 100644 index 00000000..dd1ea810 --- /dev/null +++ b/spring-javaformat-cli/src/main/jte/checkCheckstyleDetailed.jte @@ -0,0 +1,11 @@ +@import com.puppycrawl.tools.checkstyle.api.AuditEvent +@import java.io.File +@import java.util.List +@import static io.spring.javaformat.cli.check.RenderHelper.checkstyleHeader +@import static io.spring.javaformat.cli.check.RenderHelper.styledLineNumber +@import static io.spring.javaformat.cli.check.RenderHelper.styledMessage +@import static io.spring.javaformat.cli.check.RenderHelper.styledPath +@param List violations +${checkstyleHeader(violations)} +@for(AuditEvent violation : violations) ${styledPath(new File(violation.getFileName()))}:${styledLineNumber(violation.getLine())}: ${styledMessage(violation.getMessage())} +@endfor \ No newline at end of file diff --git a/spring-javaformat-cli/src/main/jte/checkCombined.jte b/spring-javaformat-cli/src/main/jte/checkCombined.jte new file mode 100644 index 00000000..e1587a3a --- /dev/null +++ b/spring-javaformat-cli/src/main/jte/checkCombined.jte @@ -0,0 +1,11 @@ +@import com.puppycrawl.tools.checkstyle.api.AuditEvent +@import java.io.File +@import java.util.List +@import static io.spring.javaformat.cli.check.RenderHelper.combinedHeader +@import static io.spring.javaformat.cli.check.RenderHelper.combinedLines +@param List formattingProblems +@param List checkstyleViolations +!{List lines = combinedLines(formattingProblems, checkstyleViolations);} +${combinedHeader(lines.size())} +@for(String line : lines) ${line} +@endfor \ No newline at end of file diff --git a/spring-javaformat-cli/src/main/jte/checkError.jte b/spring-javaformat-cli/src/main/jte/checkError.jte new file mode 100644 index 00000000..363ae27a --- /dev/null +++ b/spring-javaformat-cli/src/main/jte/checkError.jte @@ -0,0 +1,3 @@ +@import static io.spring.javaformat.cli.check.RenderHelper.error +@param String message +${error(message)} diff --git a/spring-javaformat-cli/src/main/jte/checkFormatting.jte b/spring-javaformat-cli/src/main/jte/checkFormatting.jte new file mode 100644 index 00000000..78c56093 --- /dev/null +++ b/spring-javaformat-cli/src/main/jte/checkFormatting.jte @@ -0,0 +1,8 @@ +@import java.io.File +@import java.util.List +@import static io.spring.javaformat.cli.check.RenderHelper.formattingHeader +@import static io.spring.javaformat.cli.check.RenderHelper.styledPath +@param List files +${formattingHeader(files.size())} +@for(File file : files) ${styledPath(file)} +@endfor \ No newline at end of file diff --git a/spring-javaformat-cli/src/main/jte/checkSuccess.jte b/spring-javaformat-cli/src/main/jte/checkSuccess.jte new file mode 100644 index 00000000..39c2ad9c --- /dev/null +++ b/spring-javaformat-cli/src/main/jte/checkSuccess.jte @@ -0,0 +1,2 @@ +@import static io.spring.javaformat.cli.check.RenderHelper.success +${success("All checks passed.")} diff --git a/spring-javaformat-cli/src/main/resources/io/spring/javaformat/cli/check/checkstyle.xml b/spring-javaformat-cli/src/main/resources/io/spring/javaformat/cli/check/checkstyle.xml new file mode 100644 index 00000000..8f957724 --- /dev/null +++ b/spring-javaformat-cli/src/main/resources/io/spring/javaformat/cli/check/checkstyle.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/CheckCommandIntegrationTests.java b/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/CheckCommandIntegrationTests.java new file mode 100644 index 00000000..5c7c243e --- /dev/null +++ b/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/CheckCommandIntegrationTests.java @@ -0,0 +1,151 @@ +/* + * Copyright 2017-present the original author or authors. + * + * 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 + * + * https://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 io.spring.javaformat.cli; + +import java.io.StringWriter; +import java.nio.file.Path; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.beans.factory.annotation.Autowired; + +import io.spring.javaformat.cli.check.CheckCommand; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link CheckCommand}. + * + * @author Tim Sparg + */ +class CheckCommandIntegrationTests extends AbstractCommandIntegrationTests { + + private static final Path CHECK_FIXTURES_DIR = Path.of("src/test/resources/fixtures/check"); + + private static final Path FORMAT_FIXTURES_DIR = Path.of("src/test/resources/fixtures/format"); + + @Autowired + CheckCommand checkCommand; + + @Override + CheckCommand command() { + return this.checkCommand; + } + + @Nested + class SkipFormat { + + @Test + void violationsAreReported(@TempDir Path tempDir) throws Exception { + copyFixture(CHECK_FIXTURES_DIR, "star-import", tempDir); + + StringWriter err = new StringWriter(); + int exitCode = execute(new StringWriter(), err, "--skip-format", "--project-root-package", + "com.example", tempDir.toString()); + + assertThat(exitCode).isEqualTo(1); + assertThat(err.toString()).contains("WithStarImport.java"); + } + + @Test + void includesOnlyChecksMatchingFiles(@TempDir Path tempDir) throws Exception { + copyFixture(CHECK_FIXTURES_DIR, "includes-filter", tempDir); + + StringWriter err = new StringWriter(); + int exitCode = execute(new StringWriter(), err, "--skip-format", "--project-root-package", + "com.example", "--includes", "**/WithStarImport.java", tempDir.toString()); + + assertThat(exitCode).isEqualTo(1); + assertThat(err.toString()).contains("WithStarImport.java"); + assertThat(err.toString()).doesNotContain("ExcludedFromIncludes.java"); + } + + @Test + void excludesSkipsExcludedFiles(@TempDir Path tempDir) throws Exception { + copyFixture(CHECK_FIXTURES_DIR, "excludes-filter", tempDir); + + StringWriter err = new StringWriter(); + int exitCode = execute(new StringWriter(), err, "--skip-format", "--project-root-package", + "com.example", "--excludes", "excluded/**", tempDir.toString()); + + assertThat(exitCode).isEqualTo(1); + assertThat(err.toString()).contains("WithStarImport.java"); + assertThat(err.toString()).doesNotContain("ExcludedWithStarImport.java"); + } + + @Test + void customHeaderFileIsUsed(@TempDir Path tempDir) throws Exception { + copyFixture(CHECK_FIXTURES_DIR, "custom-header", tempDir); + + int exitCode = execute(new StringWriter(), new StringWriter(), "--skip-format", "--header-type", "FILE", + "--header-file", + tempDir.resolve("header.txt").toString(), tempDir.toString()); + + assertThat(exitCode).isEqualTo(0); + } + + } + + @Nested + class SkipCheckstyle { + + @Test + void violationsAreReported(@TempDir Path tempDir) throws Exception { + copyFixture(FORMAT_FIXTURES_DIR, "default", tempDir); + + StringWriter err = new StringWriter(); + int exitCode = execute(new StringWriter(), err, "--skip-checkstyle", tempDir.toString()); + + assertThat(exitCode).isEqualTo(1); + assertThat(err.toString()).contains("Main.java"); + } + + @Test + void nonDefaultEncodingDetectsViolations(@TempDir Path tempDir) throws Exception { + copyFixture(FORMAT_FIXTURES_DIR, "latin1", tempDir); + + StringWriter err = new StringWriter(); + int exitCode = execute(new StringWriter(), err, "--skip-checkstyle", "--encoding", "ISO-8859-1", + tempDir.toString()); + + assertThat(exitCode).isEqualTo(1); + assertThat(err.toString()).contains("Main.java"); + } + + } + + @Nested + class Combined { + + @Test + void groupsViolationsByFile(@TempDir Path tempDir) throws Exception { + copyFixture(CHECK_FIXTURES_DIR, "combined", tempDir); + + StringWriter err = new StringWriter(); + int exitCode = execute(new StringWriter(), err, "--header-type", "UNCHECKED", tempDir.toString()); + + assertThat(exitCode).isEqualTo(1); + assertThat(err.toString()).contains("Violations found in 1 file"); + assertThat(err.toString()).contains("Main.java").contains("format, checkstyle"); + assertThat(err.toString()).doesNotContain("Checkstyle violations found"); + assertThat(err.toString()).doesNotContain(":4:"); + } + + } + +} diff --git a/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/SpringJavaFormatCliApplicationTests.java b/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/SpringJavaFormatCliApplicationTests.java index 9b1fa087..098d1b7a 100644 --- a/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/SpringJavaFormatCliApplicationTests.java +++ b/spring-javaformat-cli/src/test/java/io/spring/javaformat/cli/SpringJavaFormatCliApplicationTests.java @@ -47,7 +47,7 @@ void helpIsPrinted() { StringWriter out = new StringWriter(); int exitCode = new CommandLine(this.command, this.factory).setOut(new PrintWriter(out)).execute("--help"); assertThat(exitCode).isZero(); - assertThat(out.toString()).contains("Usage: spring-javaformat"); + assertThat(out.toString()).contains("Usage: spring-javaformat").contains("apply").contains("check"); } @Test diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/apache2-header-missing/Main.java b/spring-javaformat-cli/src/test/resources/fixtures/check/apache2-header-missing/Main.java new file mode 100644 index 00000000..4c57793b --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/apache2-header-missing/Main.java @@ -0,0 +1,5 @@ +package test; + +class Main { + +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/apache2-header-missing/package-info.java b/spring-javaformat-cli/src/test/resources/fixtures/check/apache2-header-missing/package-info.java new file mode 100644 index 00000000..e5de2528 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/apache2-header-missing/package-info.java @@ -0,0 +1,4 @@ +/** + * Test package. + */ +package test; diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/combined/Main.java b/spring-javaformat-cli/src/test/resources/fixtures/check/combined/Main.java new file mode 100644 index 00000000..18782fe2 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/combined/Main.java @@ -0,0 +1,6 @@ +import java.util.*; + +class Unformatted { + void method() { + } +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/custom-header/Main.java b/spring-javaformat-cli/src/test/resources/fixtures/check/custom-header/Main.java new file mode 100644 index 00000000..297a12d8 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/custom-header/Main.java @@ -0,0 +1,7 @@ +// My custom header. + +package test; + +class Main { + +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/custom-header/header.txt b/spring-javaformat-cli/src/test/resources/fixtures/check/custom-header/header.txt new file mode 100644 index 00000000..ec8bf038 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/custom-header/header.txt @@ -0,0 +1 @@ +// My custom header. diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/custom-header/package-info.java b/spring-javaformat-cli/src/test/resources/fixtures/check/custom-header/package-info.java new file mode 100644 index 00000000..f1c8ced0 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/custom-header/package-info.java @@ -0,0 +1,6 @@ +// My custom header. + +/** + * Test package. + */ +package test; diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/excludes-filter/WithStarImport.java b/spring-javaformat-cli/src/test/resources/fixtures/check/excludes-filter/WithStarImport.java new file mode 100644 index 00000000..50c566f4 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/excludes-filter/WithStarImport.java @@ -0,0 +1,5 @@ +import java.util.*; + +class WithStarImport { + +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/excludes-filter/excluded/ExcludedWithStarImport.java b/spring-javaformat-cli/src/test/resources/fixtures/check/excludes-filter/excluded/ExcludedWithStarImport.java new file mode 100644 index 00000000..445fdb6c --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/excludes-filter/excluded/ExcludedWithStarImport.java @@ -0,0 +1,5 @@ +import java.util.*; + +class ExcludedWithStarImport { + +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/includes-filter/WithStarImport.java b/spring-javaformat-cli/src/test/resources/fixtures/check/includes-filter/WithStarImport.java new file mode 100644 index 00000000..50c566f4 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/includes-filter/WithStarImport.java @@ -0,0 +1,5 @@ +import java.util.*; + +class WithStarImport { + +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/includes-filter/subdir/ExcludedFromIncludes.java b/spring-javaformat-cli/src/test/resources/fixtures/check/includes-filter/subdir/ExcludedFromIncludes.java new file mode 100644 index 00000000..8c58a237 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/includes-filter/subdir/ExcludedFromIncludes.java @@ -0,0 +1,5 @@ +import java.util.*; + +class ExcludedFromIncludes { + +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/no-header-none/Main.java b/spring-javaformat-cli/src/test/resources/fixtures/check/no-header-none/Main.java new file mode 100644 index 00000000..4c57793b --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/no-header-none/Main.java @@ -0,0 +1,5 @@ +package test; + +class Main { + +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/no-header-none/package-info.java b/spring-javaformat-cli/src/test/resources/fixtures/check/no-header-none/package-info.java new file mode 100644 index 00000000..e5de2528 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/no-header-none/package-info.java @@ -0,0 +1,4 @@ +/** + * Test package. + */ +package test; diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/non-spring-import-order/Main.java b/spring-javaformat-cli/src/test/resources/fixtures/check/non-spring-import-order/Main.java new file mode 100644 index 00000000..b8498119 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/non-spring-import-order/Main.java @@ -0,0 +1,17 @@ +package test; + +import java.util.List; + +import com.example.Foo; + +import org.springframework.Bar; + +class Main { + + List list; + + Foo foo; + + Bar bar; + +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/non-spring-import-order/package-info.java b/spring-javaformat-cli/src/test/resources/fixtures/check/non-spring-import-order/package-info.java new file mode 100644 index 00000000..e5de2528 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/non-spring-import-order/package-info.java @@ -0,0 +1,4 @@ +/** + * Test package. + */ +package test; diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/spring-import-order/Main.java b/spring-javaformat-cli/src/test/resources/fixtures/check/spring-import-order/Main.java new file mode 100644 index 00000000..b8498119 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/spring-import-order/Main.java @@ -0,0 +1,17 @@ +package test; + +import java.util.List; + +import com.example.Foo; + +import org.springframework.Bar; + +class Main { + + List list; + + Foo foo; + + Bar bar; + +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/spring-import-order/package-info.java b/spring-javaformat-cli/src/test/resources/fixtures/check/spring-import-order/package-info.java new file mode 100644 index 00000000..e5de2528 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/spring-import-order/package-info.java @@ -0,0 +1,4 @@ +/** + * Test package. + */ +package test; diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/star-import/WithStarImport.java b/spring-javaformat-cli/src/test/resources/fixtures/check/star-import/WithStarImport.java new file mode 100644 index 00000000..50c566f4 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/star-import/WithStarImport.java @@ -0,0 +1,5 @@ +import java.util.*; + +class WithStarImport { + +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/static-import-excluded/Main.java b/spring-javaformat-cli/src/test/resources/fixtures/check/static-import-excluded/Main.java new file mode 100644 index 00000000..5f4c9af9 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/static-import-excluded/Main.java @@ -0,0 +1,11 @@ +package test; + +import java.util.List; + +import static java.util.Collections.emptyList; + +class Main { + + List items = emptyList(); + +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/static-import-excluded/package-info.java b/spring-javaformat-cli/src/test/resources/fixtures/check/static-import-excluded/package-info.java new file mode 100644 index 00000000..e5de2528 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/static-import-excluded/package-info.java @@ -0,0 +1,4 @@ +/** + * Test package. + */ +package test; diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/static-import-violation/Main.java b/spring-javaformat-cli/src/test/resources/fixtures/check/static-import-violation/Main.java new file mode 100644 index 00000000..5f4c9af9 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/static-import-violation/Main.java @@ -0,0 +1,11 @@ +package test; + +import java.util.List; + +import static java.util.Collections.emptyList; + +class Main { + + List items = emptyList(); + +} diff --git a/spring-javaformat-cli/src/test/resources/fixtures/check/static-import-violation/package-info.java b/spring-javaformat-cli/src/test/resources/fixtures/check/static-import-violation/package-info.java new file mode 100644 index 00000000..e5de2528 --- /dev/null +++ b/spring-javaformat-cli/src/test/resources/fixtures/check/static-import-violation/package-info.java @@ -0,0 +1,4 @@ +/** + * Test package. + */ +package test; From df583060a1dce92628c3259cdc12e5e45111469a Mon Sep 17 00:00:00 2001 From: Tim Sparg <6872586+timothysparg@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:23:51 +0100 Subject: [PATCH 06/10] Add native image support Adds GraalVM native image configuration: reflect-config.json registers all Checkstyle API classes, Spring JavaFormat checks, and Eclipse JDT types needed at runtime; resource-config.json registers the checkstyle configuration and formatter preference files. Co-Authored-By: Claude Sonnet 4.6 --- .../spring-javaformat-cli/reflect-config.json | 433 ++++++++++++++++++ .../resource-config.json | 15 + 2 files changed, 448 insertions(+) create mode 100644 spring-javaformat-cli/src/main/resources/META-INF/native-image/io.spring.javaformat/spring-javaformat-cli/reflect-config.json create mode 100644 spring-javaformat-cli/src/main/resources/META-INF/native-image/io.spring.javaformat/spring-javaformat-cli/resource-config.json diff --git a/spring-javaformat-cli/src/main/resources/META-INF/native-image/io.spring.javaformat/spring-javaformat-cli/reflect-config.json b/spring-javaformat-cli/src/main/resources/META-INF/native-image/io.spring.javaformat/spring-javaformat-cli/reflect-config.json new file mode 100644 index 00000000..dd0f8e1c --- /dev/null +++ b/spring-javaformat-cli/src/main/resources/META-INF/native-image/io.spring.javaformat/spring-javaformat-cli/reflect-config.json @@ -0,0 +1,433 @@ +[ + {"name": "java.util.Date[]", "unsafeAllocated": true}, + {"name": "java.util.Calendar[]", "unsafeAllocated": true}, + {"name": "java.sql.Date[]", "unsafeAllocated": true}, + {"name": "java.sql.Time[]", "unsafeAllocated": true}, + {"name": "java.sql.Timestamp[]", "unsafeAllocated": true}, + {"name": "java.math.BigDecimal[]", "unsafeAllocated": true}, + {"name": "java.math.BigInteger[]", "unsafeAllocated": true}, + {"name": "java.net.URI[]", "unsafeAllocated": true}, + {"name": "java.net.URL[]", "unsafeAllocated": true}, + {"name": "java.time.LocalDate[]", "unsafeAllocated": true}, + {"name": "java.time.LocalTime[]", "unsafeAllocated": true}, + {"name": "java.time.LocalDateTime[]", "unsafeAllocated": true}, + {"name": "java.time.ZonedDateTime[]", "unsafeAllocated": true}, + {"name": "java.time.OffsetDateTime[]", "unsafeAllocated": true}, + {"name": "java.time.OffsetTime[]", "unsafeAllocated": true}, + {"name": "java.time.Instant[]", "unsafeAllocated": true}, + {"name": "java.time.Duration[]", "unsafeAllocated": true}, + {"name": "java.time.Period[]", "unsafeAllocated": true}, + {"name": "java.util.UUID[]", "unsafeAllocated": true}, + {"name": "java.lang.Boolean[]", "unsafeAllocated": true}, + {"name": "java.lang.Integer[]", "unsafeAllocated": true}, + {"name": "java.lang.Long[]", "unsafeAllocated": true}, + {"name": "java.lang.Double[]", "unsafeAllocated": true}, + {"name": "java.lang.Float[]", "unsafeAllocated": true}, + {"name": "java.lang.Byte[]", "unsafeAllocated": true}, + {"name": "java.lang.Short[]", "unsafeAllocated": true}, + {"name": "java.lang.Character[]", "unsafeAllocated": true}, + {"name": "java.lang.Number[]", "unsafeAllocated": true}, + {"name": "java.lang.String[]", "unsafeAllocated": true}, + { + "name": "io.spring.javaformat.eclipse.jdt.jdk17.internal.formatter.DefaultCodeFormatter", + "fields": [ + {"name": "astRoot"}, + {"name": "tokenManager"} + ] + }, + { + "name": "com.puppycrawl.tools.checkstyle.api.TokenTypes", + "allDeclaredFields": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.api.AutomaticBean", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.api.AbstractViolationReporter", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.api.AbstractCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.Checker", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.TreeWalker", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.SpringChecks", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.check.SpringHeaderCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.check.SpringTestFileNameCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.check.SpringAnnotationLocationCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.check.SpringAnnotationAttributeConciseValueCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.check.SpringHideUtilityClassConstructor", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.check.SpringImportOrderCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.check.SpringNoWhitespaceBeforeCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.check.SpringAvoidStaticImportCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.check.SpringLambdaCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.check.SpringTernaryCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.check.SpringCatchCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.check.SpringJavadocCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.check.SpringLeadingWhitespaceCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.check.SpringMethodOrderCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.check.SpringMethodVisibilityCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.check.SpringNullabilityCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "io.spring.javaformat.checkstyle.check.SpringParenPadCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.NewlineAtEndOfFileCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocPackageCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.annotation.MissingOverrideCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.annotation.MissingDeprecatedCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.annotation.PackageAnnotationCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.blocks.EmptyBlockCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.blocks.LeftCurlyCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.blocks.RightCurlyCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.blocks.NeedBracesCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.blocks.AvoidNestedBlocksCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.design.FinalClassCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.design.InterfaceIsTypeCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.design.MutableExceptionCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.design.InnerTypeLastCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.design.OneTopLevelClassCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.coding.CovariantEqualsCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.coding.EmptyStatementCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.coding.EqualsHashCodeCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.coding.InnerAssignmentCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.coding.SimplifyBooleanExpressionCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.coding.SimplifyBooleanReturnCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.coding.StringLiteralEqualityCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.coding.NestedForDepthCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.coding.NestedIfDepthCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.coding.NestedTryDepthCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.coding.MultipleVariableDeclarationsCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.coding.RequireThisCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.coding.OneStatementPerLineCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.coding.UnnecessarySemicolonInEnumerationCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.imports.AvoidStarImportCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.imports.RedundantImportCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTypeCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocMethodCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocVariableCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocStyleCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.javadoc.NonEmptyAtclauseDescriptionCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTagContinuationIndentationCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.javadoc.AtclauseOrderCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.indentation.CommentsIndentationCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.UpperEllCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.ArrayTypeStyleCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.OuterTypeFilenameCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.modifier.RedundantModifierCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.modifier.ModifierOrderCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.regexp.RegexpCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.whitespace.GenericWhitespaceCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.whitespace.MethodParamPadCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.whitespace.NoWhitespaceAfterCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.whitespace.TypecastParenPadCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.whitespace.WhitespaceAfterCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + }, + { + "name": "com.puppycrawl.tools.checkstyle.checks.whitespace.WhitespaceAroundCheck", + "allDeclaredConstructors": true, + "allDeclaredMethods": true + } +] diff --git a/spring-javaformat-cli/src/main/resources/META-INF/native-image/io.spring.javaformat/spring-javaformat-cli/resource-config.json b/spring-javaformat-cli/src/main/resources/META-INF/native-image/io.spring.javaformat/spring-javaformat-cli/resource-config.json new file mode 100644 index 00000000..2f8356f8 --- /dev/null +++ b/spring-javaformat-cli/src/main/resources/META-INF/native-image/io.spring.javaformat/spring-javaformat-cli/resource-config.json @@ -0,0 +1,15 @@ +{ + "resources": [ + {"pattern": "io/spring/javaformat/cli/version.properties"}, + {"pattern": "io/spring/javaformat/formatter/eclipse/formatter.prefs"}, + {"pattern": "io/spring/javaformat/eclipse/jdt/jdk17/.*\\.rsc"}, + {"pattern": "io/spring/javaformat/eclipse/jdt/jdk17/.*\\.props"}, + {"pattern": "io/spring/javaformat/eclipse/jdt/jdk17/.*\\.properties"}, + {"pattern": "io/spring/javaformat/cli/check/checkstyle.xml"}, + {"pattern": "io/spring/javaformat/checkstyle/.*\\.xml"}, + {"pattern": "io/spring/javaformat/checkstyle/.*\\.txt"}, + {"pattern": "io/spring/javaformat/checkstyle/.*\\.properties"}, + {"pattern": "com/puppycrawl/tools/checkstyle/.*\\.dtd"}, + {"pattern": "com/puppycrawl/tools/checkstyle/.*\\.properties"} + ] +} From cfedd1eb445fcf647c38ce39f23b46e5f83b21ed Mon Sep 17 00:00:00 2001 From: Tim Sparg <6872586+timothysparg@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:24:35 +0100 Subject: [PATCH 07/10] Add CI/CD workflows for native image builds and releases - Add build-cli-native workflow: 4-platform matrix (linux-amd64, linux-arm64, windows-amd64, macos-arm64); windows-arm64 excluded as GraalVM/Liberica NIK has no native Windows ARM64 support - Clear MAVEN_OPTS in native build step to prevent jvm.config options (heap size, --add-exports) propagating to the native-image subprocess - Use pwsh shell on Windows for native build to invoke mvnw.cmd - Add prepare-cli-native-release workflow: creates draft GitHub release, builds native binaries, uploads as release assets; pass --repo to gh release upload as the job has no checkout step for git context - Add setup-native composite action using Liberica NIK JDK 17 --- .../actions/create-github-release/action.yml | 4 +- .github/actions/setup-native/action.yml | 15 +++++ .github/workflows/build-cli-native.yml | 64 +++++++++++++++++++ .github/workflows/build-pull-request.yml | 11 ++++ .../workflows/prepare-cli-native-release.yml | 45 +++++++++++++ .github/workflows/promote.yml | 2 +- .github/workflows/release.yml | 14 +++- .github/workflows/rollback.yml | 7 ++ spring-javaformat-cli/.sdkmanrc | 3 + 9 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 .github/actions/setup-native/action.yml create mode 100644 .github/workflows/build-cli-native.yml create mode 100644 .github/workflows/prepare-cli-native-release.yml create mode 100644 spring-javaformat-cli/.sdkmanrc diff --git a/.github/actions/create-github-release/action.yml b/.github/actions/create-github-release/action.yml index 1d72cd3a..4a28df82 100644 --- a/.github/actions/create-github-release/action.yml +++ b/.github/actions/create-github-release/action.yml @@ -14,8 +14,8 @@ runs: milestone: ${{ inputs.milestone }} token: ${{ inputs.token }} config-file: .github/actions/create-github-release/changelog-generator.yml - - name: Create GitHub Release + - name: Publish GitHub Release env: GITHUB_TOKEN: ${{ inputs.token }} shell: bash - run: gh release create ${{ format('v{0}', inputs.milestone) }} --notes-file changelog.md + run: gh release edit ${{ format('v{0}', inputs.milestone) }} --draft=false --notes-file changelog.md diff --git a/.github/actions/setup-native/action.yml b/.github/actions/setup-native/action.yml new file mode 100644 index 00000000..ea166083 --- /dev/null +++ b/.github/actions/setup-native/action.yml @@ -0,0 +1,15 @@ +name: 'Setup Native' +runs: + using: composite + steps: + - name: Set Up GraalVM + uses: graalvm/setup-graalvm@v1 + with: + distribution: liberica + java-version: '17' + cache: maven + components: native-image + github-token: ${{ github.token }} + - name: Disable Java Problem Matcher + shell: bash + run: echo "::remove-matcher owner=java::" diff --git a/.github/workflows/build-cli-native.yml b/.github/workflows/build-cli-native.yml new file mode 100644 index 00000000..0c553f1f --- /dev/null +++ b/.github/workflows/build-cli-native.yml @@ -0,0 +1,64 @@ +name: Build CLI Native +on: + workflow_call: + workflow_dispatch: +permissions: + contents: read +jobs: + build: + name: Build (${{ matrix.name }}) + runs-on: ${{ matrix.runs-on }} + strategy: + fail-fast: false + matrix: + include: + - name: linux-amd64 + os: linux + arch: amd64 + runs-on: ubuntu-latest + - name: linux-arm64 + os: linux + arch: arm64 + runs-on: ubuntu-24.04-arm + - name: windows-amd64 + os: windows + arch: amd64 + runs-on: windows-latest + - name: macos-arm64 + os: macos + arch: arm64 + runs-on: macos-14 + steps: + - name: Check Out + uses: actions/checkout@v4 + - name: Set Up + uses: ./.github/actions/setup-native + - name: Build + if: matrix.os != 'windows' + shell: bash + env: + MAVEN_OPTS: "" + run: ./mvnw -pl spring-javaformat-cli -am -Pnative package --batch-mode --no-transfer-progress --update-snapshots + - name: Build + if: matrix.os == 'windows' + shell: pwsh + env: + MAVEN_OPTS: "" + run: .\mvnw.cmd -pl spring-javaformat-cli -am -Pnative package --batch-mode --no-transfer-progress --update-snapshots + - name: Prepare Artifact + shell: bash + run: | + binary="spring-javaformat-cli/target/spring-javaformat-cli" + extension="" + if [[ "${{ matrix.os }}" == "windows" ]]; then + binary="${binary}.exe" + extension=".exe" + fi + release_name="spring-javaformat-${{ matrix.os }}-${{ matrix.arch }}${extension}" + mkdir -p native-artifacts + cp "${binary}" "native-artifacts/${release_name}" + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: spring-javaformat-${{ matrix.os }}-${{ matrix.arch }} + path: native-artifacts/* diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index 9e085b69..62bbb8bd 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -14,3 +14,14 @@ jobs: uses: ./.github/actions/setup - name: Build run: ./mvnw clean install --batch-mode --no-transfer-progress --update-snapshots + build-cli-native: + name: Build CLI Native + runs-on: ubuntu-latest + if: ${{ github.repository == 'spring-io/spring-javaformat' }} + steps: + - name: Check Out + uses: actions/checkout@v4 + - name: Set Up Native + uses: ./.github/actions/setup-native + - name: Build CLI Native + run: ./mvnw -pl spring-javaformat-cli -am -Pnative package --batch-mode --no-transfer-progress --update-snapshots diff --git a/.github/workflows/prepare-cli-native-release.yml b/.github/workflows/prepare-cli-native-release.yml new file mode 100644 index 00000000..4b1c4624 --- /dev/null +++ b/.github/workflows/prepare-cli-native-release.yml @@ -0,0 +1,45 @@ +name: Prepare CLI Native Release +on: + workflow_call: + inputs: + version: + type: string + required: true + secrets: + gh-token: + required: true +permissions: + contents: write +jobs: + create-draft-release: + name: Create Draft GitHub Release + runs-on: ubuntu-latest + steps: + - name: Check Out + uses: actions/checkout@v4 + - name: Create Draft GitHub Release + env: + GH_TOKEN: ${{ secrets.gh-token }} + shell: bash + run: gh release create ${{ format('v{0}', inputs.version) }} --draft --verify-tag --notes "Draft release" + build: + name: Build + needs: create-draft-release + uses: ./.github/workflows/build-cli-native.yml + upload-assets: + name: Upload Assets + needs: + - create-draft-release + - build + runs-on: ubuntu-latest + steps: + - name: Download Native Artifacts + uses: actions/download-artifact@v4 + with: + pattern: spring-javaformat-* + path: native-artifacts + merge-multiple: true + - name: Upload Assets to Draft Release + env: + GH_TOKEN: ${{ secrets.gh-token }} + run: gh release upload ${{ format('v{0}', inputs.version) }} native-artifacts/* --clobber --repo ${{ github.repository }} diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index e1faac58..f8cd5c3d 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -85,7 +85,7 @@ jobs: build-number: ${{ inputs.build-number }} artifactory-username: ${{ secrets.ARTIFACTORY_USERNAME }} artifactory-password: ${{ secrets.ARTIFACTORY_PASSWORD }} - - name: Create GitHub Release + - name: Publish GitHub Release uses: ./.github/actions/create-github-release with: milestone: ${{ inputs.version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c80a4091..cedb980b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,8 @@ on: description: Environment type: environment required: true +permissions: + contents: write jobs: stage: name: Stage @@ -49,9 +51,19 @@ jobs: outputs: release-version: ${{ steps.deduce-versions.outputs.release-version }} release-build-number: ${{ github.run_number }} + prepare-cli-native-release: + name: Prepare CLI Native Release + needs: stage + uses: ./.github/workflows/prepare-cli-native-release.yml + with: + version: ${{ needs.stage.outputs.release-version }} + secrets: + gh-token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} promote: name: Promote - needs: stage + needs: + - stage + - prepare-cli-native-release uses: ./.github/workflows/promote.yml with: environment: ${{ inputs.environment }} diff --git a/.github/workflows/rollback.yml b/.github/workflows/rollback.yml index 43e04a4d..7780e177 100644 --- a/.github/workflows/rollback.yml +++ b/.github/workflows/rollback.yml @@ -39,3 +39,10 @@ jobs: build_name=${{ format('spring-javaformat-{0}', inputs.version)}} build_number=${{ inputs.build-number }} jf rt delete --build ${build_name}/${build_number} + - name: Delete Draft GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + run: | + if gh release view ${{ format('v{0}', inputs.version) }} > /dev/null 2>&1; then + gh release delete ${{ format('v{0}', inputs.version) }} --yes --cleanup-tag + fi diff --git a/spring-javaformat-cli/.sdkmanrc b/spring-javaformat-cli/.sdkmanrc new file mode 100644 index 00000000..431b64cb --- /dev/null +++ b/spring-javaformat-cli/.sdkmanrc @@ -0,0 +1,3 @@ +# Enable auto-env through the sdkman_auto_env config +# Add key=value pairs of SDKs to use below +java=23.0.11.r17-nik From 0938f805d923ecc515182853e47a6521539935be Mon Sep 17 00:00:00 2001 From: Tim Sparg <6872586+timothysparg@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:24:57 +0100 Subject: [PATCH 08/10] Document the CLI Adds CLI usage documentation to the README covering installation via SDKMAN, the apply and check commands, and available options. Co-Authored-By: Claude Sonnet 4.6 --- README.adoc | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/README.adoc b/README.adoc index 907d9a2c..49a2beaf 100644 --- a/README.adoc +++ b/README.adoc @@ -211,6 +211,53 @@ springJavaFormat { +=== CLI +The CLI provides formatting and checking support without requiring Maven or Gradle integration. + +WARNING: The CLI is experimental and not yet considered a stable interface. + +==== Download +Native binaries for Linux, Windows, and macOS are published as assets on the corresponding https://github.com/spring-io/spring-javaformat/releases[GitHub release]. + +==== Usage +[source,shell] +---- +spring-javaformat --help +---- + +The CLI provides the following commands: + +* `apply` formats Java source files in place +* `validate` checks formatting without modifying files +* `check` runs Spring style checks + +==== Examples +Format sources in the current project: + +[source,shell] +---- +spring-javaformat apply . +---- + +Validate formatting: + +[source,shell] +---- +spring-javaformat validate . +---- + +Run style checks: + +[source,shell] +---- +spring-javaformat check . +---- + +==== Configuration +The `apply` and `validate` commands honor `.springjavaformatconfig`. + + + === Java 8 Support By default, the formatter requires Java 17. If you are working on an older project, you can use a variation of the formatter based off Eclipse 2021-03 (the latest Eclipse JDT version built with Java 8). From 58ae10cf34e00d5064cc56f2d3075d8587c7efae Mon Sep 17 00:00:00 2001 From: Tim Sparg <6872586+timothysparg@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:09:03 +0100 Subject: [PATCH 09/10] Make CI workflows testable in timothysparg/spring-javaformat fork - build-and-deploy-snapshot: move repo guard to Deploy step so Build always runs in the fork (already applied by linter) - build-pull-request: extend repo guard to allow timothysparg fork - release: guard Deploy to Staging step; fall back to github.token for gh-token so draft release creation works without GH_ACTIONS_REPO_TOKEN - promote: guard all external-service steps (JFrog, Maven Central, Eclipse update site) with spring-io repo check; fall back to github.token for Publish GitHub Release Co-Authored-By: Claude Sonnet 4.6 --- .../create-github-release/changelog-generator.yml | 2 +- .github/workflows/build-and-deploy-snapshot.yml | 4 ++-- .github/workflows/build-pull-request.yml | 4 ++-- .github/workflows/prepare-cli-native-release.yml | 4 ++-- .github/workflows/promote.yml | 12 +++++++----- .github/workflows/release.yml | 3 ++- 6 files changed, 16 insertions(+), 13 deletions(-) diff --git a/.github/actions/create-github-release/changelog-generator.yml b/.github/actions/create-github-release/changelog-generator.yml index 2ce74a09..bd848fb0 100644 --- a/.github/actions/create-github-release/changelog-generator.yml +++ b/.github/actions/create-github-release/changelog-generator.yml @@ -1,2 +1,2 @@ changelog: - repository: spring-io/spring-javaformat + repository: timothysparg/spring-javaformat diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index d269a903..bd7fade6 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -2,14 +2,13 @@ name: Build and Deploy Snapshot on: push: branches: - - main + - test-ci concurrency: group: ${{ github.workflow }}-${{ github.ref }} jobs: build-and-deploy-snapshot: name: Build and Deploy Snapshot runs-on: ubuntu-latest - if: ${{ github.repository == 'spring-io/spring-javaformat' }} steps: - name: Check Out uses: actions/checkout@v4 @@ -18,6 +17,7 @@ jobs: - name: Build run: ./mvnw clean deploy --batch-mode --no-transfer-progress --update-snapshots -DaltDeploymentRepository=distribution::file://$(pwd)/distribution-repository - name: Deploy + if: ${{ github.repository == 'spring-io/spring-javaformat' }} uses: spring-io/artifactory-deploy-action@v0.0.1 with: folder: 'distribution-repository' diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index 62bbb8bd..ca20b1ec 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -6,7 +6,7 @@ jobs: build: name: Build Pull Request runs-on: ubuntu-latest - if: ${{ github.repository == 'spring-io/spring-javaformat' }} + if: ${{ github.repository == 'timothysparg/spring-javaformat' || github.repository == 'spring-io/spring-javaformat' }} steps: - name: Check Out uses: actions/checkout@v4 @@ -17,7 +17,7 @@ jobs: build-cli-native: name: Build CLI Native runs-on: ubuntu-latest - if: ${{ github.repository == 'spring-io/spring-javaformat' }} + if: ${{ github.repository == 'timothysparg/spring-javaformat' || github.repository == 'spring-io/spring-javaformat' }} steps: - name: Check Out uses: actions/checkout@v4 diff --git a/.github/workflows/prepare-cli-native-release.yml b/.github/workflows/prepare-cli-native-release.yml index 4b1c4624..0348d8e4 100644 --- a/.github/workflows/prepare-cli-native-release.yml +++ b/.github/workflows/prepare-cli-native-release.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v4 - name: Create Draft GitHub Release env: - GH_TOKEN: ${{ secrets.gh-token }} + GH_TOKEN: ${{ secrets.gh-token || github.token }} shell: bash run: gh release create ${{ format('v{0}', inputs.version) }} --draft --verify-tag --notes "Draft release" build: @@ -41,5 +41,5 @@ jobs: merge-multiple: true - name: Upload Assets to Draft Release env: - GH_TOKEN: ${{ secrets.gh-token }} + GH_TOKEN: ${{ secrets.gh-token || github.token }} run: gh release upload ${{ format('v{0}', inputs.version) }} native-artifacts/* --clobber --repo ${{ github.repository }} diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index f8cd5c3d..ffe17b56 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -36,6 +36,7 @@ jobs: - name: Check Out uses: actions/checkout@v4 - name: Set Up JFrog CLI + if: ${{ github.repository == 'spring-io/spring-javaformat' }} uses: jfrog/setup-jfrog-cli@ff5cb544114ffc152db9cea1cd3d5978d5074946 # v4.5.11 env: JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} @@ -50,10 +51,10 @@ jobs: fi echo "status-code=${status_code}" >> $GITHUB_OUTPUT - name: Download Release Artifacts - if: ${{ steps.check-sync-status.outputs.status-code == '404' }} + if: ${{ github.repository == 'spring-io/spring-javaformat' && steps.check-sync-status.outputs.status-code == '404' }} run: jf rt download --spec ./.github/artifacts.spec --spec-vars 'buildName=${{ format('spring-javaformat-{0}', inputs.version) }};buildNumber=${{ inputs.build-number }}' - name: Sync to Maven Central - if: ${{ steps.check-sync-status.outputs.status-code == '404' }} + if: ${{ github.repository == 'spring-io/spring-javaformat' && steps.check-sync-status.outputs.status-code == '404' }} uses: spring-io/nexus-sync-action@v0.0.1 with: username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} @@ -65,7 +66,7 @@ jobs: release: true generate-checksums: true - name: Await Maven Central Sync - if: ${{ steps.check-sync-status.outputs.status-code == '404' }} + if: ${{ github.repository == 'spring-io/spring-javaformat' && steps.check-sync-status.outputs.status-code == '404' }} run: | url=${{ format('https://repo.maven.apache.org/maven2/io/spring/javaformat/spring-javaformat/{0}/spring-javaformat-{0}.pom', inputs.version) }} echo "Waiting for $url" @@ -76,9 +77,10 @@ jobs: done echo "$url is available" - name: Promote Build - if: ${{ steps.check-sync-status.outputs.status-code == '404' }} + if: ${{ github.repository == 'spring-io/spring-javaformat' && steps.check-sync-status.outputs.status-code == '404' }} run: jfrog rt build-promote ${{ format('spring-javaformat-{0}', inputs.version)}} ${{ inputs.build-number }} libs-release-local - name: Publish Eclipse Update Site + if: ${{ github.repository == 'spring-io/spring-javaformat' }} uses: ./.github/actions/publish-eclipse-update-site with: version: ${{ inputs.version }} @@ -89,4 +91,4 @@ jobs: uses: ./.github/actions/create-github-release with: milestone: ${{ inputs.version }} - token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN || github.token }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cedb980b..7383718d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,6 +36,7 @@ jobs: release-version: ${{ steps.deduce-versions.outputs.release-version }} next-version: ${{ steps.deduce-versions.outputs.next-version }} - name: Deploy to Staging + if: ${{ github.repository == 'spring-io/spring-javaformat' }} uses: spring-io/artifactory-deploy-action@v0.0.1 with: folder: distribution-repository @@ -58,7 +59,7 @@ jobs: with: version: ${{ needs.stage.outputs.release-version }} secrets: - gh-token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + gh-token: ${{ secrets.GH_ACTIONS_REPO_TOKEN || github.token }} promote: name: Promote needs: From d6a94bfe9513ada2108caccbeccc025ecad91f91 Mon Sep 17 00:00:00 2001 From: Tim Sparg <6872586+timothysparg@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:16:52 +0100 Subject: [PATCH 10/10] Remove extra blank lines in README Co-Authored-By: Claude Sonnet 4.6 --- README.adoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.adoc b/README.adoc index 49a2beaf..b1a98f77 100644 --- a/README.adoc +++ b/README.adoc @@ -2,8 +2,6 @@ :checkstyle-version: 9.3 == Spring Java Format - - === What is This? A set of plugins that can be applied to any Java project to provide a consistent "`Spring`" style. The set currently consists of: