Skip to content

Commit 1bf591d

Browse files
timothyspargclaude
andcommitted
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 <noreply@anthropic.com>
1 parent dc6fae8 commit 1bf591d

File tree

38 files changed

+968
-2
lines changed

38 files changed

+968
-2
lines changed

spring-javaformat-cli/pom.xml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<maven.compiler.source>17</maven.compiler.source>
1515
<maven.compiler.target>17</maven.compiler.target>
1616
<java.version>17</java.version>
17+
<jte.version>3.2.3</jte.version>
1718
</properties>
1819

1920
<dependencyManagement>
@@ -73,6 +74,17 @@
7374
<artifactId>plexus-utils</artifactId>
7475
</dependency>
7576

77+
<dependency>
78+
<groupId>gg.jte</groupId>
79+
<artifactId>jte-runtime</artifactId>
80+
<version>${jte.version}</version>
81+
</dependency>
82+
<dependency>
83+
<groupId>gg.jte</groupId>
84+
<artifactId>jte-models</artifactId>
85+
<version>${jte.version}</version>
86+
</dependency>
87+
7688
<!-- Test -->
7789
<dependency>
7890
<groupId>org.springframework.boot</groupId>
@@ -140,6 +152,41 @@
140152
</annotationProcessorPaths>
141153
</configuration>
142154
</plugin>
155+
<plugin>
156+
<groupId>gg.jte</groupId>
157+
<artifactId>jte-maven-plugin</artifactId>
158+
<version>${jte.version}</version>
159+
<configuration>
160+
<sourceDirectory>${project.basedir}/src/main/jte</sourceDirectory>
161+
<contentType>Plain</contentType>
162+
<extensions>
163+
<extension>
164+
<className>gg.jte.models.generator.ModelExtension</className>
165+
<settings>
166+
<interfaceAnnotation>@org.jspecify.annotations.NullMarked</interfaceAnnotation>
167+
<implementationAnnotation>@org.jspecify.annotations.NullMarked</implementationAnnotation>
168+
<language>Java</language>
169+
</settings>
170+
</extension>
171+
</extensions>
172+
</configuration>
173+
<executions>
174+
<execution>
175+
<id>generate</id>
176+
<phase>generate-sources</phase>
177+
<goals>
178+
<goal>generate</goal>
179+
</goals>
180+
</execution>
181+
</executions>
182+
<dependencies>
183+
<dependency>
184+
<groupId>gg.jte</groupId>
185+
<artifactId>jte-models</artifactId>
186+
<version>${jte.version}</version>
187+
</dependency>
188+
</dependencies>
189+
</plugin>
143190
<plugin>
144191
<groupId>org.springframework.experimental</groupId>
145192
<artifactId>spring-aot-maven-plugin</artifactId>

spring-javaformat-cli/src/main/java/io/spring/javaformat/cli/SpringJavaFormatCommand.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import picocli.CommandLine.Model.CommandSpec;
2222
import picocli.CommandLine.Spec;
2323

24+
import io.spring.javaformat.cli.check.CheckCommand;
2425
import io.spring.javaformat.cli.format.ApplyCommand;
2526

2627
/**
@@ -30,7 +31,7 @@
3031
*/
3132
@Component
3233
@Command(name = "spring-javaformat", mixinStandardHelpOptions = true, versionProvider = CliVersionProvider.class,
33-
subcommands = { ApplyCommand.class },
34+
subcommands = { ApplyCommand.class, CheckCommand.class },
3435
description = "Formats and checks Java source files")
3536
@PicocliManaged
3637
class SpringJavaFormatCommand implements Runnable {
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Copyright 2017-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.spring.javaformat.cli.check;
18+
19+
import java.util.Properties;
20+
import java.util.concurrent.Callable;
21+
22+
import org.springframework.stereotype.Component;
23+
import picocli.CommandLine.ArgGroup;
24+
import picocli.CommandLine.Command;
25+
import picocli.CommandLine.Mixin;
26+
import picocli.CommandLine.Model.CommandSpec;
27+
import picocli.CommandLine.Option;
28+
import picocli.CommandLine.ParameterException;
29+
import picocli.CommandLine.Spec;
30+
31+
import io.spring.javaformat.cli.InputOptions;
32+
33+
/**
34+
* Check formatting and checkstyle violations.
35+
*
36+
* @author Tim Sparg
37+
*/
38+
@Component
39+
@Command(name = "check", mixinStandardHelpOptions = true,
40+
description = "Check the codebase for formatting and checkstyle violations")
41+
public class CheckCommand implements Callable<Integer> {
42+
43+
@Spec
44+
private CommandSpec spec;
45+
46+
@Mixin
47+
private InputOptions formatOptions;
48+
49+
@Option(names = { "-t", "--header-type" }, defaultValue = "APACHE2",
50+
description = "Header type (${COMPLETION-CANDIDATES}). Default: ${DEFAULT-VALUE}")
51+
private HeaderType headerType;
52+
53+
@Option(names = { "-c", "--header-copyright-pattern" }, defaultValue = "20\\d\\d(-20\\d\\d|-present)?",
54+
description = "Copyright year regex pattern. Default: ${DEFAULT-VALUE}")
55+
private String headerCopyrightPattern;
56+
57+
@Option(names = { "-f", "--header-file" }, defaultValue = "",
58+
description = "Path to a custom header file. Required when --header-type is 'file'.")
59+
private String headerFile;
60+
61+
@Option(names = { "-r", "--project-root-package" }, defaultValue = "org.springframework",
62+
description = "Root package used for import ordering. Default: ${DEFAULT-VALUE}")
63+
private String projectRootPackage;
64+
65+
@Option(names = { "-s", "--avoid-static-import-excludes" }, split = ",", defaultValue = "",
66+
description = "Repeatable or comma-separated static import patterns to allow.")
67+
private String[] avoidStaticImportExcludes;
68+
69+
@ArgGroup
70+
private RunMode runMode;
71+
72+
private final CheckRunner checkRunner;
73+
74+
private final CheckReportRenderer checkReportRenderer;
75+
76+
@SuppressWarnings("NullAway.Init")
77+
CheckCommand(CheckRunner checkRunner, CheckReportRenderer checkReportRenderer) {
78+
this.checkRunner = checkRunner;
79+
this.checkReportRenderer = checkReportRenderer;
80+
}
81+
82+
@Override
83+
public Integer call() {
84+
CheckRunner.Inputs inputs = createInputs();
85+
CheckReport report = this.checkRunner.run(inputs);
86+
this.checkReportRenderer.render(this.spec.commandLine(), report);
87+
return report.exitCode();
88+
}
89+
90+
private CheckRunner.Inputs createInputs() {
91+
boolean skipCheckstyle = this.runMode != null && this.runMode.skipCheckstyle;
92+
if (!skipCheckstyle && this.headerType == HeaderType.FILE && this.headerFile.isEmpty()) {
93+
throw new ParameterException(this.spec.commandLine(),
94+
"--header-file is required when --header-type is FILE");
95+
}
96+
return new CheckRunner.Inputs(this.formatOptions, buildCheckstyleProperties(),
97+
this.runMode != null && this.runMode.skipFormat, skipCheckstyle);
98+
}
99+
100+
private Properties buildCheckstyleProperties() {
101+
Properties properties = new Properties();
102+
properties.setProperty("headerType", this.headerType.value);
103+
properties.setProperty("headerCopyrightPattern", this.headerCopyrightPattern);
104+
if (!this.headerFile.isEmpty()) {
105+
properties.setProperty("headerFile", this.headerFile);
106+
}
107+
properties.setProperty("projectRootPackage", this.projectRootPackage);
108+
properties.setProperty("avoidStaticImportExcludes", String.join(",",
109+
java.util.Arrays.stream(this.avoidStaticImportExcludes).filter((s) -> !s.isEmpty()).toList()));
110+
return properties;
111+
}
112+
113+
static class RunMode {
114+
115+
@Option(names = { "-S", "--skip-checkstyle" }, defaultValue = "false",
116+
description = "Skip checkstyle checks, only check source formatting")
117+
boolean skipCheckstyle;
118+
119+
@Option(names = { "-F", "--skip-format" }, defaultValue = "false",
120+
description = "Skip source formatting check, only run checkstyle")
121+
boolean skipFormat;
122+
123+
}
124+
125+
enum HeaderType {
126+
127+
APACHE2("apache2"), NONE("none"), UNCHECKED("unchecked"), REGEXP("regexp"), FILE("file");
128+
129+
private final String value;
130+
131+
HeaderType(String value) {
132+
this.value = value;
133+
}
134+
135+
}
136+
137+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2017-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.spring.javaformat.cli.check;
18+
19+
import java.io.File;
20+
import java.util.List;
21+
22+
import com.puppycrawl.tools.checkstyle.api.AuditEvent;
23+
import org.jspecify.annotations.Nullable;
24+
25+
record CheckReport(List<File> formattingProblems, List<AuditEvent> checkstyleViolations, boolean skipFormat,
26+
boolean skipCheckstyle, @Nullable String errorMessage, boolean checkstyleFailure) {
27+
28+
static CheckReport failure(boolean skipFormat, boolean skipCheckstyle, String errorMessage, boolean checkstyleFailure) {
29+
return new CheckReport(List.of(), List.of(), skipFormat, skipCheckstyle, errorMessage, checkstyleFailure);
30+
}
31+
32+
boolean hasViolations() {
33+
return !this.formattingProblems.isEmpty() || !this.checkstyleViolations.isEmpty();
34+
}
35+
36+
boolean hasError() {
37+
return this.errorMessage != null;
38+
}
39+
40+
int exitCode() {
41+
return (hasError() || hasViolations()) ? 1 : 0;
42+
}
43+
44+
boolean combined() {
45+
return !this.skipFormat && !this.skipCheckstyle;
46+
}
47+
48+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2017-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.spring.javaformat.cli.check;
18+
19+
import java.util.Objects;
20+
21+
import gg.jte.generated.precompiled.StaticTemplates;
22+
import gg.jte.generated.precompiled.Templates;
23+
import gg.jte.output.PrintWriterOutput;
24+
import org.springframework.stereotype.Component;
25+
import picocli.CommandLine;
26+
27+
@Component
28+
public class CheckReportRenderer {
29+
30+
private final Templates templates = new StaticTemplates();
31+
32+
void render(CommandLine commandLine, CheckReport report) {
33+
if (report.hasError()) {
34+
renderError(commandLine, report);
35+
}
36+
else if (!report.hasViolations()) {
37+
renderSuccess(commandLine);
38+
}
39+
else if (report.combined()) {
40+
renderCombined(commandLine, report);
41+
}
42+
else {
43+
renderSeparate(commandLine, report);
44+
}
45+
commandLine.getOut().flush();
46+
commandLine.getErr().flush();
47+
}
48+
49+
private void renderError(CommandLine commandLine, CheckReport report) {
50+
this.templates.checkError(Objects.requireNonNull(report.errorMessage()))
51+
.render(new PrintWriterOutput(commandLine.getErr()));
52+
}
53+
54+
private void renderSuccess(CommandLine commandLine) {
55+
this.templates.checkSuccess().render(new PrintWriterOutput(commandLine.getOut()));
56+
}
57+
58+
private void renderCombined(CommandLine commandLine, CheckReport report) {
59+
this.templates.checkCombined(report.formattingProblems(), report.checkstyleViolations())
60+
.render(new PrintWriterOutput(commandLine.getErr()));
61+
}
62+
63+
private void renderSeparate(CommandLine commandLine, CheckReport report) {
64+
if (!report.skipFormat()) {
65+
this.templates.checkFormatting(report.formattingProblems())
66+
.render(new PrintWriterOutput(commandLine.getErr()));
67+
}
68+
if (!report.skipCheckstyle()) {
69+
this.templates.checkCheckstyleDetailed(report.checkstyleViolations())
70+
.render(new PrintWriterOutput(commandLine.getErr()));
71+
}
72+
}
73+
74+
}

0 commit comments

Comments
 (0)