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

38 files changed

Lines changed: 968 additions & 2 deletions

File tree

‎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
importpicocli.CommandLine.Model.CommandSpec;
2222
importpicocli.CommandLine.Spec;
2323

24+
importio.spring.javaformat.cli.check.CheckCommand;
2425
importio.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
classSpringJavaFormatCommandimplementsRunnable {
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+
packageio.spring.javaformat.cli.check;
18+
19+
importjava.util.Properties;
20+
importjava.util.concurrent.Callable;
21+
22+
importorg.springframework.stereotype.Component;
23+
importpicocli.CommandLine.ArgGroup;
24+
importpicocli.CommandLine.Command;
25+
importpicocli.CommandLine.Mixin;
26+
importpicocli.CommandLine.Model.CommandSpec;
27+
importpicocli.CommandLine.Option;
28+
importpicocli.CommandLine.ParameterException;
29+
importpicocli.CommandLine.Spec;
30+
31+
importio.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+
publicclassCheckCommandimplementsCallable<Integer> {
42+
43+
@Spec
44+
privateCommandSpecspec;
45+
46+
@Mixin
47+
privateInputOptionsformatOptions;
48+
49+
@Option(names = { "-t", "--header-type" }, defaultValue = "APACHE2",
50+
description = "Header type (${COMPLETION-CANDIDATES}). Default: ${DEFAULT-VALUE}")
51+
privateHeaderTypeheaderType;
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+
privateStringheaderCopyrightPattern;
56+
57+
@Option(names = { "-f", "--header-file" }, defaultValue = "",
58+
description = "Path to a custom header file. Required when --header-type is 'file'.")
59+
privateStringheaderFile;
60+
61+
@Option(names = { "-r", "--project-root-package" }, defaultValue = "org.springframework",
62+
description = "Root package used for import ordering. Default: ${DEFAULT-VALUE}")
63+
privateStringprojectRootPackage;
64+
65+
@Option(names = { "-s", "--avoid-static-import-excludes" }, split = ",", defaultValue = "",
66+
description = "Repeatable or comma-separated static import patterns to allow.")
67+
privateString[] avoidStaticImportExcludes;
68+
69+
@ArgGroup
70+
privateRunModerunMode;
71+
72+
privatefinalCheckRunnercheckRunner;
73+
74+
privatefinalCheckReportRenderercheckReportRenderer;
75+
76+
@SuppressWarnings("NullAway.Init")
77+
CheckCommand(CheckRunnercheckRunner, CheckReportRenderercheckReportRenderer) {
78+
this.checkRunner = checkRunner;
79+
this.checkReportRenderer = checkReportRenderer;
80+
}
81+
82+
@Override
83+
publicIntegercall() {
84+
CheckRunner.Inputsinputs = createInputs();
85+
CheckReportreport = this.checkRunner.run(inputs);
86+
this.checkReportRenderer.render(this.spec.commandLine(), report);
87+
returnreport.exitCode();
88+
}
89+
90+
privateCheckRunner.InputscreateInputs() {
91+
booleanskipCheckstyle = this.runMode != null && this.runMode.skipCheckstyle;
92+
if (!skipCheckstyle && this.headerType == HeaderType.FILE && this.headerFile.isEmpty()) {
93+
thrownewParameterException(this.spec.commandLine(),
94+
"--header-file is required when --header-type is FILE");
95+
}
96+
returnnewCheckRunner.Inputs(this.formatOptions, buildCheckstyleProperties(),
97+
this.runMode != null && this.runMode.skipFormat, skipCheckstyle);
98+
}
99+
100+
privatePropertiesbuildCheckstyleProperties() {
101+
Propertiesproperties = newProperties();
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+
returnproperties;
111+
}
112+
113+
staticclassRunMode {
114+
115+
@Option(names = { "-S", "--skip-checkstyle" }, defaultValue = "false",
116+
description = "Skip checkstyle checks, only check source formatting")
117+
booleanskipCheckstyle;
118+
119+
@Option(names = { "-F", "--skip-format" }, defaultValue = "false",
120+
description = "Skip source formatting check, only run checkstyle")
121+
booleanskipFormat;
122+
123+
}
124+
125+
enumHeaderType {
126+
127+
APACHE2("apache2"), NONE("none"), UNCHECKED("unchecked"), REGEXP("regexp"), FILE("file");
128+
129+
privatefinalStringvalue;
130+
131+
HeaderType(Stringvalue) {
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+
packageio.spring.javaformat.cli.check;
18+
19+
importjava.io.File;
20+
importjava.util.List;
21+
22+
importcom.puppycrawl.tools.checkstyle.api.AuditEvent;
23+
importorg.jspecify.annotations.Nullable;
24+
25+
recordCheckReport(List<File> formattingProblems, List<AuditEvent> checkstyleViolations, booleanskipFormat,
26+
booleanskipCheckstyle, @NullableStringerrorMessage, booleancheckstyleFailure) {
27+
28+
staticCheckReportfailure(booleanskipFormat, booleanskipCheckstyle, StringerrorMessage, booleancheckstyleFailure) {
29+
returnnewCheckReport(List.of(), List.of(), skipFormat, skipCheckstyle, errorMessage, checkstyleFailure);
30+
}
31+
32+
booleanhasViolations() {
33+
return !this.formattingProblems.isEmpty() || !this.checkstyleViolations.isEmpty();
34+
}
35+
36+
booleanhasError() {
37+
returnthis.errorMessage != null;
38+
}
39+
40+
intexitCode() {
41+
return (hasError() || hasViolations()) ? 1 : 0;
42+
}
43+
44+
booleancombined() {
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+
packageio.spring.javaformat.cli.check;
18+
19+
importjava.util.Objects;
20+
21+
importgg.jte.generated.precompiled.StaticTemplates;
22+
importgg.jte.generated.precompiled.Templates;
23+
importgg.jte.output.PrintWriterOutput;
24+
importorg.springframework.stereotype.Component;
25+
importpicocli.CommandLine;
26+
27+
@Component
28+
publicclassCheckReportRenderer {
29+
30+
privatefinalTemplatestemplates = newStaticTemplates();
31+
32+
voidrender(CommandLinecommandLine, CheckReportreport) {
33+
if (report.hasError()) {
34+
renderError(commandLine, report);
35+
}
36+
elseif (!report.hasViolations()) {
37+
renderSuccess(commandLine);
38+
}
39+
elseif (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+
privatevoidrenderError(CommandLinecommandLine, CheckReportreport) {
50+
this.templates.checkError(Objects.requireNonNull(report.errorMessage()))
51+
.render(newPrintWriterOutput(commandLine.getErr()));
52+
}
53+
54+
privatevoidrenderSuccess(CommandLinecommandLine) {
55+
this.templates.checkSuccess().render(newPrintWriterOutput(commandLine.getOut()));
56+
}
57+
58+
privatevoidrenderCombined(CommandLinecommandLine, CheckReportreport) {
59+
this.templates.checkCombined(report.formattingProblems(), report.checkstyleViolations())
60+
.render(newPrintWriterOutput(commandLine.getErr()));
61+
}
62+
63+
privatevoidrenderSeparate(CommandLinecommandLine, CheckReportreport) {
64+
if (!report.skipFormat()) {
65+
this.templates.checkFormatting(report.formattingProblems())
66+
.render(newPrintWriterOutput(commandLine.getErr()));
67+
}
68+
if (!report.skipCheckstyle()) {
69+
this.templates.checkCheckstyleDetailed(report.checkstyleViolations())
70+
.render(newPrintWriterOutput(commandLine.getErr()));
71+
}
72+
}
73+
74+
}

0 commit comments

Comments
 (0)