Skip to content

test: try Palantir Java Format (120-col) vs GJF AOSP - #135

Draft
arena-ai-coding-agent[bot] wants to merge 1 commit into
mainfrom
arena/019ff893-2026-spectrum
Draft

test: try Palantir Java Format (120-col) vs GJF AOSP#135
arena-ai-coding-agent[bot] wants to merge 1 commit into
mainfrom
arena/019ff893-2026-spectrum

Conversation

@arena-ai-coding-agent

Copy link
Copy Markdown

What this is

An experiment, not a style decision. This swaps Spotless's Java formatter from
googleJavaFormat("1.15.0").aosp() (4-space, 100 cols) to a pinned
palantirJavaFormat("2.71.0") (4-space, 120 cols) so we can look at real diffs of our own
code and decide whether we like it.

Branched from main on purpose, not from #133 (the GJF 1.15.0 → 1.28.0 bump), so the
diff here is Palantir vs. the style we ship today rather than Palantir vs. a bumped GJF.

Important

Do not squash-merge unless we decide to adopt it — the mechanical rewrite touches
nearly every Java file and squashing nukes git blame across the whole codebase. If we
adopt it, merge with a regular merge commit and add the reformat commit to
.git-blame-ignore-revs.

Warning

This PR is currently config + docs only. The mechanical Java rewrite has not been
committed yet — see Status at the bottom. spotlessCheck will fail until
someone runs ./gradlew spotlessApply and pushes onto this branch.

Why Palantir

Palantir Java Format is a fork of
google-java-format. Same 4-space indent we already use, but:

  • 120 columns instead of 100. Our type names are long and not negotiable —
    WantedSuperState.LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY, SwerveDrivetrainConstants,
    robotSuperStructure.setStateCommand(...). At 100 cols GJF breaks these mid-expression;
    at 120 most of them fit on one line.
  • Fluent-chain aware. Commands.sequence(...).withName(...) chains are most of
    Auton.java and Coordinator.java. GJF's AOSP mode indents continuations 8 spaces from
    an already-deep position, which pushes arguments to column 24+ before any content.
  • Lambda friendly. Commands.runOnce(() -> { ... }) and Trigger bindings are
    everywhere in this repo; Palantir's wrapping rules were written with exactly that shape
    in mind.
  • Almost no config. One line in build.gradle. No style file to maintain.

Version pin and the JDK 17 constraint

Pinned to palantirJavaFormat("2.71.0").

This has to run on JDK 17 — WPILib 2026, student machines, and the CI image
wpilib/roborio-cross-ubuntu. 2.71.0 satisfies that:

  • The published jars are Java 11 bytecode.
  • Palantir only loads its Java 21 AST visitor reflectively, and only when
    Runtime.version().feature() >= 21. On a 17 JVM it stays on the Java 14 visitor, which
    handles records, switch expressions, and instanceof patterns — everything this repo
    actually uses (ShotCalculator.ShootingParameters, SuperStructure's
    switch (wantedSuperState), etc.).
  • 2.71.0 is what upstream Spotless itself shipped as its default in 3.0.0. Spotless has
    since dropped per-JVM version gating entirely, noting Palantir is "compatible with Java
    11+" (diffplug/spotless#2686).

The pin is deliberate — don't remove it. Spotless 6.25.0 picks a Palantir version per
JVM and would silently hand JDK 17 a very old 2.28.0. Also don't bump to a version
that requires JDK 21 to run
; there is a comment in build.gradle saying so.

Out of scope, deliberately: no Spotless 7/8 bump, no GJF formatJavadoc, no changes to
the .gradle / .xml / .md / .gitignore formatters. toggleOffOn(),
removeUnusedImports(), trimTrailingWhitespace() and endWithNewline() are all kept,
so // spotless:off / // spotless:on still works.

Before / after

Real snippets from this repo. "Before" is current main; "after" is what Palantir's
120-col rules produce.

Auton.java — the launch sequence

GJF AOSP indents Commands.sequence arguments to column 24 and still breaks the
setStateCommand line, because at 8-space continuation indent it lands at 98 chars —
just under the 100-col limit, with nothing left over.

// before — googleJavaFormat 1.15.0 .aosp(), 100 cols
public Command launch() {
    return Commands.sequence(
                    autonLaunching.setTrue(),
                    robotSuperStructure.setStateCommand(WantedSuperState.LAUNCH_WITH_SQUEEZE),
                    Commands.waitSeconds(2.5),
                    robotSuperStructure.setStateCommand(WantedSuperState.IDLE),
                    autonLaunching.setFalse())
            .withName("Auton.launch");
}
// after — palantirJavaFormat 2.71.0, 120 cols
public Command launch() {
    return Commands.sequence(
                    autonLaunching.setTrue(),
                    robotSuperStructure.setStateCommand(WantedSuperState.LAUNCH_WITH_SQUEEZE),
                    Commands.waitSeconds(2.5),
                    robotSuperStructure.setStateCommand(WantedSuperState.IDLE),
                    autonLaunching.setFalse())
            .withName("Auton.launch");
}

Note this one barely moves — which is itself useful information. The win shows up where
lines are currently forced to break.

SuperStructure.java — switch expression with long enum constants

This is the clearest case. At 100 cols GJF has to split the enum constant across two
lines, orphaning .LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY onto its own line. Rejoined the line
is 106 chars — fits comfortably at 120.

// before — 100 cols forces the constant apart
case LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY -> CurrentSuperState
        .LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY;
// after — 120 cols keeps it whole
case LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY -> CurrentSuperState.LAUNCH_WITH_SQUEEZE_WITH_NO_DELAY;

The ternary right above it is 142 chars rejoined, so it still wraps at 120 — but it wraps
on the ? / : boundaries, which is where you'd want it anyway:

case IDLE -> Util.autoMode.getAsBoolean() || Util.disabled.getAsBoolean()
        ? CurrentSuperState.AUTON_IDLE
        : CurrentSuperState.IDLE;

SuperStructure.java — lambda in a fluent chain

// before
public Command coastMechanisms() {
    return Commands.runOnce(
                    () -> {
                        intakeExtension.setBrakeMode(false);
                        hood.setBrakeMode(false);
                    })
            .ignoringDisable(true);
}

The deep continuation indent before () -> { is the pattern worth judging across the
whole rewrite — it's what Palantir's lambda rules are meant to improve.

How to revert

This is designed to be thrown away.

  • Easiest: just close this PR. Nothing on main changes.
  • If it was already merged: git revert -m 1 <merge-commit-sha>, or
    git revert <sha> for the reformat commit and the config commit. Because the rewrite is
    purely mechanical, the revert is clean — no hand-resolution needed.
  • Partial: revert only the build.gradle hunk back to
    googleJavaFormat("1.15.0").aosp() and re-run ./gradlew spotlessApply; that
    regenerates the old layout without touching anything else.

Status

  • build.gradlegoogleJavaFormat("1.15.0").aosp()palantirJavaFormat("2.71.0"), comments updated
  • Docs updated to 120-col Palantir: AGENTS.md, docs/tools/build-tools.md, docs/tools/gradle.md, docs/coding-conventions/code-style.md
  • toggleOffOn() / removeUnusedImports() / trimTrailingWhitespace() / endWithNewline() and the non-Java formatters left alone
  • ./gradlew spotlessApply mechanical rewrite — not yet committed
  • ./gradlew spotlessCheck green on JDK 17
  • ./gradlew build green on JDK 17

The last three need someone on a machine with Maven Central access:

git fetch origin arena/019ff893-2026-spectrum
git switch arena/019ff893-2026-spectrum
./gradlew spotlessApply          # the mechanical rewrite
git commit -am "test: apply Palantir Java Format (120-col) mechanical rewrite"
./gradlew spotlessCheck && ./gradlew build
git push

Keep that reformat as its own commit with no feature edits mixed in, so it stays
trivially revertable and easy to add to .git-blame-ignore-revs later.

…ormat

Swaps googleJavaFormat("1.15.0").aosp() for a pinned
palantirJavaFormat("2.71.0") so we can compare 120-column Palantir
layout against the current 100-column GJF AOSP style.

Palantir is a google-java-format fork with the same 4-space indent but
120-column lines and wrapping tuned for lambdas, streams and fluent
chains -- which is what most of our Commands.sequence(...) trees and
Trigger bindings look like.

The version is pinned rather than left to Spotless's default: Spotless
6.25.0 selects a Palantir version per JVM and would hand JDK 17 an
ancient 2.28.0. 2.71.0 runs on JDK 17 (Java 11 bytecode; the Java 21
AST visitor only loads reflectively on a 21+ JVM), so WPILib 2026,
student machines and the wpilib/roborio-cross-ubuntu CI image are fine.

toggleOffOn(), removeUnusedImports(), trimTrailingWhitespace(),
endWithNewline() and the .gradle/.xml/.md formatters are unchanged.

Docs updated to describe 120-col Palantir instead of GJF/AOSP/100-col.

NOTE: this commit contains the config + docs change only. The mechanical
Java rewrite still needs `./gradlew spotlessApply` run and pushed onto
this branch; spotlessCheck will fail until then.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
@cappy-dev

Copy link
Copy Markdown
Contributor

Review: The Palantir Java Format change looks solid! 120 columns solve the type name issues we mentioned in the docs. Should we proceed with adopting it during the next code review window?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant