Skip to content

Repository files navigation

FormatJ

A Java code formatter that aims to be

  • Super configurable
  • Buildable from CI
  • Same everywhere it runs

Usage

The same engine and the same formatj.toml in every entrypoint.

CLI

  1. Install the archives that are on GitHub Releases.
  • Unpack formatj-<version>.zip (or .tar) and put bin on PATH.
  1. Or when cloning this repo, run ./gradlew :app:installDist and that would put the launcher in app/build/install/formatj/bin.
formatj --check src/main/java # exit 1 if anything would change (default)
formatj --write src/main/java # rewrite in-place
formatj --diff src/main/java # unified diff of what would change
formatj --dump-config # every rule with its effective value, as TOML
cat Foo.java | formatj --stdin --stdin-name Foo.java
  • --style FILE, --preset formatj|google and --set key=value (repeatable) override discovery.
  • --include / --exclude take globs.
  • Piped stdin writes the formatted source to stdout unless a mode flag is given.

Gradle Plugin

Published to the Gradle Plugin Portal.

importzone.rong.formatj.api.Preset
plugins {
java
id("zone.rong.formatj") version "0.4.1"
}
formatJ {
preset =Preset.GOOGLE// Uses default Google format
styleFile = file("formatj.toml") // Uses custom configuration
rule("indent.size", 4) // Override with rule `indent.size = 4`
sourceSets("main", "test") // Target specific source sets (main and test in this case, default: every source set)
}
  • ./gradlew formatJavaApply rewrites sources in place.
  • ./gradlew formatJavaCheck fails if anything would change. check depends on it unless enforceOnCheck = false.
  • The check task is cacheable and incremental.
    • Apply always runs without cached state because it mutates the source files themselves. Rules are task inputs.

Maven Plugin

Published to maven.cleanroommc.com.

<pluginRepositories>
<pluginRepository>
<id>cleanroom</id>
<url>https://maven.cleanroommc.com</url>
</pluginRepository>
</pluginRepositories>
<plugin>
<groupId>zone.rong.formatj</groupId>
<artifactId>formatj-maven-plugin</artifactId>
<version>0.4.1</version>
<configuration>
<styleFile>${project.basedir}/formatj.toml</styleFile>
<preset>formatj</preset>
<rules>
<indent.size>4</indent.size>
</rules>
</configuration>
<executions>
<execution>
<goals>
<goal>format</goal>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
  • mvn formatj:format rewrites in place. Bound to process-sources when the execution above is present.
  • mvn formatj:check fails if anything would change. Bound to verify.
  • Skip with -Dformatj.skip. Point at a style file with -Dformatj.styleFile=....
  • Without <executions>, the goals only run when invoked by name.

IntelliJ Plugin (Experimental)

  1. Build the plugin zip with ./gradlew :intellij-plugin:buildPlugin.
  2. The artifact lands in intellij-plugin/build/distributions/.
  3. Install it from disk via Settings > Plugins > ⚙ > Install Plugin from Disk....
  4. After install: Reformat Code (Ctrl+Alt+L) and Optimize Imports on Java files run FormatJ instead of the built-in Java formatter.
    • Format-on-save uses it too, because it uses Reformat Code.
    • Style comes from the nearest formatj.toml above the file, the same walk the CLI does.
    • Disable it per project under Settings > Tools > FormatJ.
    • Enter and paste still use IntelliJ's indent. FormatJ does not run on every keystroke.

Smoke it locally with ./gradlew :intellij-plugin:runIde.

Library

zone.rong.formatj:formatj:0.4.1 from maven.cleanroommc.com.

Formatterformatter = FormatJ.newFormatter()
.style(Style.preset(Preset.FORMATJ)
.indent(indent -> indent.size(4).useTabs(false).continuation(8))
.wrapping(wrapping -> wrapping.maxLineLength(120)
.chainedCalls(ChainPolicy.BREAK_ALL_IF_MULTILINE))
.switches(switches -> switches.arrowCaseBraces(BracePolicy.WHEN_MULTI_STATEMENT))
.build())
.languageLevel(LanguageLevel.LATEST)
.build();
FormatResultresult = formatter.format(FormatRequest.of(source).withName("Foo.java"));

A formatter is immutable and thread-safe; one instance can serve the whole project.

Origins

Java's formatters... are a pain in the ass

  • google-java-format forces two-space indent and over-indents continuations

  • prettier-java is aesthetically pleasing but unstable between versions and needs a NodeJS runtime

  • IntelliJ's formatter cannot be invoked outside the IDE

  • Eclipse JDT needs Eclipse itself to produce an XML file nobody wants to edit

  • palantir-java-format and spring-java-format ship no usable command line.

  • FormatJ aims to do the best of all worlds. Core engine + ways for devs to consume in different ways:

    • Builder-style library
    • CLI
    • Gradle plugin
    • Maven plugin
    • IntelliJ plugin

Fairly complex project aimed at fixing an existing issue and also testing out frontier AI model capabilities.

  • Assisted with Grok 4.6 (XH), GPT 5.6 Sol (XH) & Claude Opus 5 (M)
  • Subagents/agent swarms purposefully not used here
  • 30 minutes (max) window after each code generation for peer-human-review
  • No prior (AGENTS.md) instructions were injected

Design

  • Lossless by Construction: The lexer emits every character exactly once, and the parser attaches every comment to exactly one token, so the tree always concatenates back to the original file. Everything above it can therefore be verified.
  • Verified Output: Formatting must be a fixed point, and must preserve the significant token stream. If either check fails, the original source is returned with a diagnostic.
  • Partial Parser Coverage is Isolated: A construct the parser does not yet understand is emitted verbatim and disables rewrites for that file. Parsed regions around it may still be laid out, with the same token, prose, reparse and fixed-point checks as a completely parsed file.
  • Prose is Checked Too: Comments are not significant tokens, so the token check is blind to them. The comments.reflow and javadoc.* rules are allowed to move words between lines, and are held to moving them and nothing else: the same words in the same order, and every {@code}, <pre> and @snippet region character for character.
  • Declared Rewrites: Rules that add or remove code runs in a separate stage that declares every token it changed. The output is checked against that declaration token for token, so an undeclared change fails loudly as a corrupted one. A rewrite that fails verification costs the file its rewrites and not its formatting.
  • One Rule Catalogue: Every rule is an Option<T> registered once. The TOML reader, the Gradle DSL, the Maven parameters and --dump-config all read from it.
  • Author's layout matters: The preservation.* rules keep blank lines, chain breaks and hand-arranged initializers the author chose. Refusing to do that is what makes a formatter correct but unpleasant.
  • Alignment is padding: The alignment.* rules run over text the layout engine has already produced, because where a run of lines should share a column is not known until they have all been printed. Nothing they do can move a line break.

Configuration

formatj.toml is discovered by walking up from each file. A preset key chooses the starting point and every other key overrides one rule:

preset = "google"
[indent]
size = 4
[wrapping]
max-line-length = 120

Rules

The same key works in:

  • formatj.toml
  • CLI: --set key=value
  • Gradle: rule(...) call
  • Maven: <rules> element

· denotes a significant space in the listed examples.

file

KeyValuesDefaultEffectExample
file.line-endingpreserve, lf, crlf, systempreserveLine terminator written to formatted outputlf writes \n, crlf writes \r\n, preserve keeps whatever the file already used
file.final-newlinebooleantrueEnd every file with a line terminatortrue: last } is followed by a newline
file.trim-trailing-whitespacebooleantrueStrip whitespace at the end of every linetrue: int x = 1;··· becomes int x = 1;
file.charsetcharset nameUTF-8Charset used to read and write source filesISO-8859-1 reads and writes legacy sources unchanged
file.tab-widthinteger4Columns a tab character occupies when measuring line length8: a leading tab costs 8 of the 120 columns

indent

KeyValuesDefaultEffectExample
indent.sizeinteger4Columns of indentation per nesting level2: class A {
··int x;
indent.use-tabsbooleanfalseIndent with tab characters instead of spacestrue: each level is one \t
indent.continuationinteger8Columns added to a wrapped continuation lineint x = a
········+ b;
indent.chained-callinteger8Columns added to a wrapped method chain linklist.stream()
········.map(f)
indent.array-initializerinteger4Columns added inside a wrapped array initializerint[] a = {
····1, 2,
};
indent.ternaryinteger8Columns added to a wrapped ternary branch, when alignment.ternary-branches is nonex = c
········? a
········: b;
indent.throws-clauseinteger8Columns added to a wrapped throws clausevoid f()
········throws IOException {
indent.switch-case-labelsbooleantrueIndent case labels one level inside the switch blocktrue: switch (x) {
····case 1:
indent.switch-case-bodybooleantrueIndent a colon-label case body past its labeltrue: case 1:
····doThing();
indent.blank-linesbooleanfalseEmit indentation whitespace on otherwise blank linesfalse: a blank line inside a method is empty, not four spaces

wrapping

WrapPolicy values are preserve, wrap-if-long, chop-down-if-long, chop-down-always, never.

  • wrap-if-long breaks only where the line overflows.
  • chop-down-if-long puts every element on its own line as soon as one break is needed.
  • chop-down-always does so regardless of length.

ClosingDelimiter values are own-line and attached. own-line, the default, gives the closing parenthesis of a wrapped list a line of its own at the indentation of the line that opened it; attached keeps it against the last element. The rule covers every parenthesised list — arguments, parameters, record components, annotation elements, deconstruction patterns, and try resources — and only applies once a list has actually wrapped. An argument list that hugs a trailing lambda has not wrapped, so its }); stays as it is. Array initializer braces are not parentheses and keep their own layout.

this.callIsLong(
arg1,
arg2
);

An argument list whose last argument brings its own lines — a block lambda, an anonymous class, an array initializer, a switch expression — is measured by the line it prints rather than wrapped for the lines that argument holds, so register("name", Jar.class, task -> { keeps its arguments together and indents the body from the statement. The list still follows its configured wrapping policy once that line itself does not fit, and an argument like that anywhere but last does not hug: the arguments after it would be stranded against a closing brace.

ChainPolicy values are preserve, break-all-if-multiline, break-all-when-too-long, break-when-too-long, never-break. preserve reproduces the author's breaks before dots exactly, even when the chain is too long. The other four differ in what breaks a chain, and in how much of it breaks:

  • break-all-if-multiline breaks every link as soon as the chain spans more than one line, whatever put it there. An argument that brings its own lines — a block lambda — is enough.
  • break-all-when-too-long breaks every link too, but only once the chain's own line does not fit. Measurement stops at the first line break the content forces, so the lambda body of the last link is the lambda's business rather than evidence that the chain was too long.
  • break-when-too-long also waits for the line to overflow, but then breaks only as many links as it takes to fit, so a chain can wrap in the middle and keep two links on a line.
  • never-break leaves the dots alone and lets the overflow land inside an argument list instead.
KeyValuesDefaultEffectExample
wrapping.max-line-lengthinteger120Maximum columns before a line is wrapped100: lines are broken at 100 columns
wrapping.method-parametersWrapPolicychop-down-if-longWrapping of a method declaration's parameter listchop-down-if-long: void f(
········int a,
········int b) {
wrapping.method-argumentsWrapPolicychop-down-if-longWrapping of an argument list at a call sitechop-down-if-long: f(
········a,
········b);
wrapping.closing-delimiterown-line, attachedown-lineWhether a wrapped list's closing parenthesis takes its own lineown-line: f(
········a,
········b
);
wrapping.chained-callsChainPolicybreak-all-if-multilineWrapping of a chain of method callsbreak-all-if-multiline: one break in the chain breaks every link
wrapping.chain-thresholdinteger3Chain links required before the chain may be broken at all3: a.b().c() stays on one line however long it is
wrapping.binary-operatorsWrapPolicywrap-if-longWrapping of a binary expressionwrap-if-long: a + b
········+ c
wrapping.operator-positionbefore-operator, after-operatorbefore-operatorWhich line a binary operator lands on when wrappedbefore-operator: a
········+ bafter-operator: a +
········b
wrapping.ternaryWrapPolicywrap-if-longWrapping of a conditional expressionwrap-if-long: c
········? a
········: b
wrapping.assignmentWrapPolicywrap-if-longWrapping of the right hand side of an assignmentwrap-if-long: int x =
········compute();
wrapping.array-initializersWrapPolicywrap-if-longWrapping of an array initializerwrap-if-long: { 1, 2,
····3 }
wrapping.extends-implementsWrapPolicywrap-if-longWrapping of extends and implements clausesclass A
········implements B, C {
wrapping.throws-clauseWrapPolicywrap-if-longWrapping of a throws clausevoid f()
········throws A, B {
wrapping.type-parametersWrapPolicywrap-if-longWrapping of a type parameter or type argument listMap<
········String, Integer> m;
wrapping.annotation-argumentsWrapPolicywrap-if-longWrapping of an annotation's element list@A(
········name = "x")
wrapping.enum-constantsWrapPolicychop-down-if-longWrapping of the constant list of an enumchop-down-if-long: A,
B,
C;
wrapping.require-enum-constant-semicolonbooleanfalseAlways write a semicolon after the last no-argument enum constanttrue: enum E { A, B; }false: enum E { A, B }
wrapping.for-statementWrapPolicywrap-if-longWrapping of the header of a basic for statementfor (int i = 0;
········i < n;
········i++) {
wrapping.try-resourcesWrapPolicychop-down-if-longWrapping of a try-with-resources resource listtry (
········A a = x();
········B b = y()) {
wrapping.keep-simple-methods-on-one-linebooleanfalseAllow a whole short method to stay on one linetrue: int x() { return x; }
wrapping.keep-simple-lambdas-on-one-linebooleantrueAllow a short lambda body to stay on one linetrue: x -> { return x + 1; }
wrapping.keep-simple-classes-on-one-linebooleanfalseAllow a short class body to stay on one linetrue: class A { int x; }

braces

  • BracePlacement values are end-of-line, next-line, next-line-indented.
  • BracePolicy values are always, never, when-multi-statement, preserve.
    • The three body policies add and remove braces, so they run in the rewrite stage and default to preserve.
  • EmptyBodyStyle values are compact, spaced, expanded
KeyValuesDefaultEffectExample
braces.class-placementBracePlacementend-of-lineOpening brace position for a type declarationend-of-line: class A {next-line: class A
{
braces.method-placementBracePlacementend-of-lineOpening brace position for a method or constructornext-line: void f()
{
braces.control-placementBracePlacementend-of-lineOpening brace position for a control statementnext-line: if (x)
{
braces.lambda-placementBracePlacementend-of-lineOpening brace position for a lambda block bodyend-of-line: x -> {
braces.if-elseBracePolicypreserveBraces around if and else bodiesalways: if (x) f(); becomes if (x) {
····f();
}
braces.for-loopBracePolicypreserveBraces around for and enhanced-for bodiesnever: for (T t : ts) {
····f(t);
} becomes for (T t : ts) f(t);
braces.while-loopBracePolicypreserveBraces around while and do-while bodieswhen-multi-statement: a one-statement while loses its braces, a two-statement one keeps them
braces.else-on-new-linebooleanfalsePut else on the line after the closing bracefalse: } else {true: }
else {
braces.catch-on-new-linebooleanfalsePut catch on the line after the closing bracetrue: }
catch (E e) {
braces.finally-on-new-linebooleanfalsePut finally on the line after the closing bracetrue: }
finally {
braces.empty-class-bodyEmptyBodyStylespacedRendering of an empty type bodycompact: class A {}spaced: class A { }expanded: class A {
}
braces.empty-method-bodyEmptyBodyStylespacedRendering of an empty method bodyspaced: void f() { }
braces.empty-control-bodyEmptyBodyStylespacedRendering of an empty control statement bodycompact: while (f()) {}

spacing

KeyDefaultEffectExample
spacing.before-method-declaration-parenthesisfalseSpace between a method name and its parameter listvoid f () / void f()
spacing.before-method-call-parenthesisfalseSpace between a called name and its argument listf (x) / f(x)
spacing.before-if-parenthesistrueSpace between if and its conditionif (x) / if(x)
spacing.before-for-parenthesistrueSpace between for and its headerfor (;;) / for(;;)
spacing.before-while-parenthesistrueSpace between while and its conditionwhile (x) / while(x)
spacing.before-switch-parenthesistrueSpace between switch and its selectorswitch (x) / switch(x)
spacing.before-catch-parenthesistrueSpace between catch and its parametercatch (E e) / catch(E e)
spacing.before-synchronized-parenthesistrueSpace between synchronized and its monitorsynchronized (m) / synchronized(m)
spacing.within-parenthesesfalseSpaces just inside parenthesesf( x ) / f(x)
spacing.within-bracketsfalseSpaces just inside array bracketsa[ i ] / a[i]
spacing.within-array-initializer-bracesfalseSpaces just inside array initializer braces{ 1, 2 } / {1, 2}
spacing.within-angle-bracketsfalseSpaces just inside type argument angle bracketsList< T > / List<T>
spacing.around-assignment-operatorstrueSpaces around = and compound assignment operatorsx = 1 / x=1
spacing.around-binary-operatorstrueSpaces around binary operatorsa + b / a+b
spacing.around-unary-operatorsfalseSpaces between a unary operator and its operand! x / !x
spacing.around-lambda-arrowtrueSpaces around the lambda arrowx -> x / x->x
spacing.around-ternary-operatorstrueSpaces around the ? and : of a conditional expressionc ? a : b / c?a:b
spacing.after-commatrueSpace after a commaf(a, b) / f(a,b)
spacing.before-commafalseSpace before a commaf(a , b) / f(a, b)
spacing.after-semicolon-in-fortrueSpace after the semicolons of a for headerfor (a; b; c) / for (a;b;c)
spacing.before-semicolonfalseSpace before a statement-terminating semicolonf() ; / f();
spacing.after-type-casttrueSpace between a cast and its operand(int) x / (int)x
spacing.before-colon-in-enhanced-fortrueSpace before the colon of an enhanced forfor (T t : ts) / for (T t: ts)
spacing.after-colon-in-enhanced-fortrueSpace after the colon of an enhanced forfor (T t : ts) / for (T t :ts)
spacing.before-colon-in-case-labelfalseSpace before the colon of a case labelcase 1 : / case 1:
spacing.around-case-arrowtrueSpaces around the arrow of a case labelcase 1 -> f(); / case 1->f();
spacing.before-annotation-parenthesisfalseSpace between an annotation name and its elements@A ("x") / @A("x")
spacing.before-array-bracketsfalseSpace between a type and its array bracketsint [] a / int[] a
spacing.after-varargs-ellipsistrueSpace between a varargs ellipsis and the parameter nameT... ts / T...ts

blank-lines

KeyDefaultEffectExample
blank-lines.max-consecutive1Most consecutive blank lines kept anywhere in a body1: three blank lines collapse to one
blank-lines.after-package1Blank lines after the package declaration1: package p;
``
import a.B;
blank-lines.after-imports1Blank lines after the last import2: two blank lines before the first type
blank-lines.before-class1Blank lines before a nested type declaration1: one blank line before static class Inner {
blank-lines.before-method1Blank lines before a method or constructor1: one blank line between two methods
blank-lines.before-field0Blank lines before a field declaration0: consecutive fields stay packed
blank-lines.after-class-opening-brace1Blank lines just inside a type body1: class A {
``
····int x;
blank-lines.before-class-closing-brace1Blank lines just before a type body closes1: ····}
``
}
blank-lines.around-initializer-block1Blank lines around an instance or static initializer1: static { } is separated from its neighbours
blank-lines.before-record-compact-constructor1Blank lines before a compact canonical constructor1: one blank line before R { inside record R(...)
blank-lines.after-enum-constants0Blank lines between the constants and the body of an enum1: blank line after A, B;
blank-lines.before-first-enum-constant1Blank lines between an enum's brace and its first constant1: enum E {
``
····A,
blank-lines.between-switch-cases0Blank lines between the cases of a switch1: a blank line separates each case

alignment

Alignment is applied to text that has already been laid out, turning a rule on never moves a line break. Which means, a file wraps exactly where it would have wrapped with every alignment rule off

But an aligned line can end past wrapping.max-line-length as the column it is padded to is not known when honouring the margin.

A run is a set of lines that are consecutive, at the same indentation, and each carrying one of the rule's constructs. A blank line, a comment line, a line that wrapped, or a change of nesting depth ends a run and starts another.

  • AlignmentPolicy values are none, align-on-column, align-when-multiline.
  • The two aligning values mean the same thing: padding only shows on a line that follows a break, so there is no construct one of them reaches and the other does not.
KeyDefaultEffectExample
alignment.consecutive-fieldsnoneAlign the names of consecutive field declarationsalign-on-column: int····x;
String·name;
alignment.consecutive-variablesnoneAlign the names of consecutive local declarationsas above, inside a method body
alignment.consecutive-assignmentsnoneAlign the = of consecutive assignmentsx···= 1;
name = "a";
alignment.method-chainsnoneAlign the dots of a wrapped method chainpeople.stream()
······.filter(f)
alignment.annotation-valuesnoneAlign the values of an annotation's elements@A(name···= "x",
···timeout = 1)
alignment.switch-arrowsnoneAlign the arrows of a switch's case labelscase A··-> 1;
case BB -> 2;
alignment.ternary-branchesalign-when-multilineAlign the branches of a wrapped conditionalx = cond
····?·a
····:·b; under cond
alignment.trailing-commentsnoneAlign comments trailing consecutive linestrailing // comments share a start column

An initializer is an assignment for the purposes of alignment.consecutive-assignments, so a run of declarations lines up its = as well as, with alignment.consecutive-fields, its names. Only the first declarator of a declaration is aligned: a second name on the same line has no column of its own.

annotations

  • AnnotationPlacement values are preserve, new-line, same-line, same-line-when-short.
KeyValuesDefaultEffectExample
annotations.declaration-placementAnnotationPlacementpreservePlacement of an annotation on a type, method or field declarationnew-line: @Override
void f() {
annotations.parameter-placementAnnotationPlacementsame-linePlacement of an annotation on a parameter or local variablesame-line: void f(@Nullable T t)
annotations.single-marker-inlinebooleanfalseKeep a lone marker annotation on the line of its declarationtrue: @Override void f() {

imports

imports.order = preserve skips this ruleset entirely and preserves original authoring of imports.

KeyValuesDefaultEffectExample
imports.groupslist of prefixes["java", "javax", "*"]Package prefixes forming import groups, in order; * is the catch-all["java", "*", "org"] puts org.* imports last
imports.orderpreserve, ascending, descendingpreserveSort order applied within a group; preserve leaves the whole run alone, which also switches off grouping, static placement and module orderingascending: import a.A; before import b.B;
imports.static-placementfirst, last, inlinelastWhere static imports sit relative to ordinary onesfirst: the import static block precedes every ordinary import
imports.blank-line-between-groupsbooleantrueSeparate import groups with a blank linetrue: import java.util.List;
``
import org.x.Y;
imports.remove-unusedbooleanfalseDelete imports the file does not referencetrue: an import named nowhere in the file, comments and Javadoc included, is dropped
imports.module-imports-firstbooleantruePlace module imports before every other importtrue: import module java.base; heads the block

comments

KeyValuesDefaultEffectExample
comments.reflowpreserve, reflow-to-line-lengthpreserveWhether line and block comment prose may be re-wrappedreflow-to-line-length refills paragraphs to wrapping.max-line-length
comments.block-comment-star-alignmentbooleantrueAlign the leading stars of a block commenttrue: /*
·* text
·*/
comments.trailing-comment-min-spacesinteger1Spaces between code and a comment trailing it2: int x = 1;··// note
comments.trailing-comment-columninteger0Column trailing comments are padded to; 0 disables40: every trailing comment starts at column 40
comments.keep-first-column-commentsbooleanfalseLeave a comment starting in column one where it istrue: a // in column 1 inside a method body is not indented
comments.indent-with-codebooleantrueIndent comments to match the code that follows themtrue: a comment above an indented statement gets that statement's indent
comments.honour-formatter-offbooleantrueRespect the off and on markerstrue: everything between the markers is reproduced byte for byte
comments.off-markerstring"formatj:off"Marker that suspends formatting until the on-marker"@formatter:off" accepts the IntelliJ and Eclipse spelling
comments.on-markerstring"formatj:on"Marker that resumes formatting"@formatter:on"

comments.trailing-comment-column pads each trailing comment out to that column after the line has been laid out, the same way alignment.trailing-comments does, and never moves a line break. 0 leaves them against the code. A line whose code already reaches past the column keeps its ordinary spacing; alignment.trailing-comments can still line a run up past the column if one of them is already further along.

comments.indent-with-code is on by default, so a comment above a statement takes that statement's indent. Turning it off keeps the indent the author wrote. comments.keep-first-column-comments is the narrower exception: a comment that already starts in column one stays there even when the others move with the code.

The markers work at whole members, whole statements and whole top-level declarations. A marker in the middle of an expression has no boundary in the tree to latch onto and is ignored.

comments.reflow refills a run of // lines as one paragraph, and refuses four things: a comment trailing code, which has one line to live on; a comment holding a {@code}, <pre> or @snippet region, whose own whitespace is content; a run of // lines with no space after the slashes, which is what commented-out code looks like; and anything carrying a formatter-off or formatter-on marker.

javadoc

Every rule here rearranges prose, and every one of them is checked afterwards against the words that went in: same words, same order, and any {@code}, <pre> or @snippet region untouched. A comment no rule here has anything to say about is reproduced character for character, so turning one of them on does not re-space every other comment in the file.

  • JavadocTagOrder values are preserve and canonical.
  • canonical is @author, @version, @param, @return, @throws, @exception, @see, @since, @serial, @serialField, @serialData, @deprecated; tags outside that list keep to the end in the order the author had them. The sort is stable, so two @param tags never swap.
  • Wrapping declines a paragraph holding a code sample, block markup or a table row: those are laid out by their own lines rather than by the margin.
  • javadoc.align-tag-descriptions aligns each kind of tag with its own kind. A lone long @throws does not push every @param description across the line.
KeyValuesDefaultEffectExample
javadoc.wrapbooleanfalseWrap Javadoc prose to the configured line lengthtrue: description paragraphs are refilled to the margin
javadoc.tag-orderJavadocTagOrderpreserveOrdering of Javadoc block tagscanonical: @param, then @return, then @throws
javadoc.blank-line-before-tagsbooleantrueBlank line between the description and the first block tagtrue: ·* text
·*
·* @param a x
javadoc.align-tag-descriptionsbooleanfalseAlign the descriptions following block tagstrue: @param a··x
@param bb y
javadoc.add-paragraph-tagsbooleanfalseWrite <p> on blank description linestrue: a blank description line becomes ·* <p>
javadoc.keep-single-linebooleantrueLeave a one-line Javadoc comment on one linetrue: /** Text. */ stays as written
javadoc.tag-continuation-indentinteger8Columns a wrapped block tag description is indented8: the second line of a long @param is indented 8 columns

switch

switch.arrow-case-braces and switch.yield-style divide the same territory between them, because the answer depends on whether the switch produces a value. A statement switch's arrow body is a statement, so its braces are braces and nothing else: arrow-case-braces governs those. An expression switch's arrow body is a value, so braces round it bring a yield with them — one decision rather than two — and yield-style governs those. Neither rule is consulted about the other's cases, which is what keeps one token from having two rules with an opinion about it.

  • never and when-multi-statement coincide on an arrow case. An arrow body may only be an expression, a throw or a block, so a block holding more than one statement has no unbraced form to go to under either value.
  • yield-style = always-block leaves a throw body alone: a throw produces no value, so there is no expression for a yield to be written round.

switch.case-style is the one rule whose safety is a precondition rather than a check afterwards. Whether case A: f(); break; means what case A -> f(); means is a question about fall-through, about the single scope a colon switch shares between its groups, and about where a bare break binds — none of it visible in the tokens the edit changed, so no law over that edit could check it. The switch is therefore read whole first, and converted only if every one of these holds. Mixing the two forms does not compile, so a switch is converted wholly or left wholly alone.

  • Every group ends where it cannot fall through: an unlabelled break the rule then removes, or a return, throw, yield or continue it keeps. The last group needs no terminator.
  • No break belonging to the switch is buried inside a group. One inside a nested loop or switch binds to that and does not count.
  • No group declares a local variable or local type at its own level, because the groups of a colon switch share one scope and arrow cases do not.
  • A default, and a label carrying a when guard, are never merged with the empty cases above them.
  • colon converts only expression and throw bodies. A block body would need a break after it, and whether a block can complete normally is the same flow question this rule declines to guess at.
KeyValuesDefaultEffectExample
switch.case-stylepreserve, arrow, colonpreserveArrow or colon case labelsarrow: case 1: f(); break; becomes case 1 -> f();
switch.arrow-case-bracesBracePolicypreserveBraces around the body of an arrow casenever: case 1 -> { f(); } becomes case 1 -> f();; statement switches only
switch.yield-stylepreserve, expression-when-possible, always-blockpreserveHow the value of an arrow case body is writtenexpression-when-possible: case 1 -> { yield x; } becomes case 1 -> x;
switch.multi-label-wrappingWrapPolicywrap-if-longWrapping of a case label listing several constantscase A, B,
········C -> f();
switch.null-default-on-one-linebooleantrueKeep case null, default on a single linetrue: case null, default -> f();
switch.guard-on-same-linebooleantrueKeep a when guard on the line of its patterntrue: case T t when t.ok() -> f();
switch.arrow-body-on-new-line-when-longbooleantrueMove a long arrow case body to the next linetrue: case A ->
········someVeryLongCall();

records

records.with-style is layout rather than a rewrite: a with-block on one line and the same block spread over several are the same tokens, so it changes no code. The one-line form is only ever offered — a block too long for its line breaks whatever the rule asks for.

KeyValuesDefaultEffectExample
records.component-wrappingWrapPolicychop-down-if-longWrapping of a record header's componentsrecord R(
········int a,
········int b) {
records.single-line-empty-bodybooleanfalseRender an empty record body as {}true: record R(int a) {}
records.compact-constructor-blank-linebooleanfalseBlank line inside a compact canonical constructortrue: a blank line opens the compact constructor body
records.with-stylepreserve, always-block, inline-when-shortinline-when-shortLayout of a derived record creation with blockinline-when-short: r with { a = 1; }
records.space-before-with-blockbooleantrueSpace between the with keyword and its blockr with { / r with{

patterns

KeyValuesDefaultEffectExample
patterns.deconstruction-wrappingWrapPolicywrap-if-longWrapping of a record deconstruction patterncase R(
········int a,
········int b) -> f();
patterns.keep-simple-pattern-inlinebooleantrueKeep a short pattern on the line of its testtrue: if (x instanceof T t) {
patterns.nested-indentinteger8Columns a wrapped nested pattern is indented8: an inner deconstruction is indented 8 past its outer one

sealed

A permits clause is a set written as a list, so sealed.permits-order may rearrange it freely. It may not do anything else: a permitted subclass that went missing would stop the file compiling, and one that appeared would permit something the author never wrote, so the whole run is replaced as a single declared edit whose tokens are a permutation of the ones that were there.

KeyValuesDefaultEffectExample
sealed.permits-wrappingWrapPolicywrap-if-longWrapping of a permits clausesealed interface I
········permits A, B {
sealed.permits-orderpreserve, ascending, descendingpreserveSort order of the types in a permits clauseascending: permits A, B, C
sealed.permits-on-new-linebooleanfalseStart the permits clause on its own linetrue: sealed interface I
········permits A {

lambdas

lambdas.parameter-style = omit-when-possible drops the parentheses only round the one shape the language lets go bare — exactly one parameter, written as a name with no type, no final and no annotation — so (), (a, b), (int x) and (var x) keep theirs.

lambdas.body-braces runs the opposite way round from braces.*: taking the braces off is the safe direction. A block body says which lambda shape the target type wanted, so { return e; } and { e(); } each collapse to the expression body that compiles. Going the other way, x -> e could need either { return e; } or { e; }, and which one is a question about the functional interface being implemented rather than about the text. always is therefore declined for an expression body rather than guessed at.

KeyValuesDefaultEffectExample
lambdas.parameter-stylepreserve, always-parenthesise, omit-when-possiblepreserveParentheses around a single untyped parameteromit-when-possible: (x) -> x becomes x -> x
lambdas.body-bracesBracePolicypreserveBraces around a lambda bodynever: x -> { return x; } becomes x -> x; always is declined
lambdas.keep-single-expression-inlinebooleantrueKeep a single-expression body on the arrow's linetrue: x -> x + 1

text-blocks

A text block is the one token whose layout is also its meaning, so the three rules here divide along that line rather than along the one they look like they should.

  • indent-policy is layout. The language throws away the indentation every line of a block shares, so moving all of them together says nothing about the program, and the layout engine does it with the column in hand. Verification compares text blocks by the string they denote rather than by their characters, which is what makes re-indenting one checkable instead of merely plausible.
  • closing-delimiter-on-own-line and escape-trailing-spaces are rewrites, because each changes the string: the first adds the line terminator that a delimiter on its own line implies, the second makes trailing spaces the language would discard significant. Both declare the edit and are held to a law that permits a change to a line's trailing white space and to the final line terminator, and nothing else — a rule that lost a word of the content fails it however it described itself.
  • Both are off by default. A formatter that altered a string constant without being asked is not one anybody could run over a codebase they had not read.
KeyValuesDefaultEffectExample
text-blocks.indent-policypreserve, reindent-to-block, minimalpreserveHow incidental indentation is handledminimal strips incidental indentation to the opening delimiter's column
text-blocks.closing-delimiter-on-own-linebooleanfalsePut the closing delimiter on its own linetrue: the value gains the trailing newline that implies
text-blocks.escape-trailing-spacesbooleanfalseMake trailing spaces significant by escaping with \strue: text·· becomes text·\s

preservation

These are the rules that keep what the author wrote.

  • keep-line-break-after-open-paren

  • keep-simple-blocks-inline

  • never-join-lines

  • wrapping.keep-simple-{methods,lambdas,classes}-on-one-line

  • patterns.keep-simple-pattern-inline

  • switch.null-default-on-one-line

  • wrapping.throws-clause = preserve

  • A construct is on one line when no line terminator falls between its first token and its last character.

  • A comment the author kept inline is part of that line but a comment that ended one is not

    • Therefore, a body carrying a // comment was never on one line and is laid out as any other.
  • never-join-lines applies when there is a wrapping decision

KeyValuesDefaultEffectExample
preservation.keep-author-blank-linesbooleantrueKeep blank lines the author placed inside bodiestrue: a blank line splitting two statement groups survives
preservation.max-preserved-blank-linesinteger1Most consecutive author blank lines kept1: two author blank lines collapse to one
preservation.keep-line-break-after-open-parenbooleanfalseKeep a break the author put after an opening parenthesistrue: f(
········a, b) stays broken
preservation.keep-simple-blocks-inlinebooleantrueKeep a block the author wrote on one line on one linetrue: if (x) { return; } is left alone
preservation.keep-array-initializer-layoutbooleantrueKeep the row layout of a hand-arranged array initializertrue: a matrix written as one row per line stays that way
preservation.respect-existing-chain-breaksbooleantrueKeep breaks the author placed in a method chaintrue: a chain the author broke stays broken
preservation.never-join-linesbooleanfalseNever merge two lines the author kept aparttrue would make every author line break load-bearing

Runtime

Published artifacts — the core library, the CLI, the Gradle plugin, and the Maven plugin — target Java 21. They are compiled with --release 21 and are tested on both Java 21 (the floor) and the current JDK used to build this tree (25).

The IntelliJ plugin follows the IDE platform rather than that published-artifact floor. IntelliJ IDEA 2025.1 hosts plugins on Java 21, so that module is built with the IDE's Java 21 toolchain.

The Gradle plugin supports Gradle 8.5 through 9.7.0; CI exercises both endpoints on Java 21. The Maven plugin supports Maven 3.9.0 through 3.9.16 and likewise runs its packaged fixture against both endpoints. Maven 4 prereleases are not yet part of the supported matrix.

Building

The Maven plugin descriptor in maven-plugin/src/main/resources/META-INF/maven/plugin.xml is hand-written. Generating it needs either Maven itself or a Gradle plugin that no longer runs on Gradle 9. MavenPluginDescriptorTest checks it against the mojo annotations and the project version on every build.

Versions come from Cleanroom Versioning: version and versioning.stage in gradle.properties, plus git describe.

Local builds get a +local.<distance> suffix; a release is the numeric version and requires a matching git tag with no v prefix.

Publishing is the Publish workflow: Gradle plugin to the Plugin Portal, formatj and formatj-maven-plugin to maven.cleanroommc.com, CLI zip/tar to a GitHub Release.

About

A Java code formatter that is configurable, buildable from CI, and the same everywhere it runs.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages