From 4c647aacfe1cb43fb5d22d6f2fa0b458c765b137 Mon Sep 17 00:00:00 2001 From: Alex Abashev Date: Fri, 2 Oct 2026 22:23:22 +0300 Subject: [PATCH] Replace derive4j with a sealed interface and records derive4j generated BreakBehaviours for one type, BreakBehaviour: four cases, two of them with a keepIndentWhenInlined flag. BreakBehaviour is now a sealed interface whose cases are records, and the factories the call sites used moved onto it. Level picks the case with a switch over record patterns instead of a Cases visitor and caseOf chains, so the compiler checks that every case is handled; the JSON serializer and LevelDelimitedFlatValueDocVisitor switch the same way. Each case keeps derive4j's toString, which the debug visitor prints, and the JSON keeps its shape. Level.BreakImpl's preferBreakingLastInnerLevel never read the flag and now takes no parameter. open-java-format no longer runs the derive4j processor. The 15,747 files of the JDK 21 sources format exactly as before, and the plugins' tests pass against a native image built from this. --- ...java-format.error-prone-conventions.gradle | 2 +- gradle/libs.versions.toml | 2 - open-java-format/build.gradle | 2 - .../palantir/javaformat/BreakBehaviour.java | 147 ++++++++-------- .../javaformat/LastLevelBreakability.java | 8 +- .../java/com/palantir/javaformat/OpenOp.java | 2 +- .../javaformat/PartialInlineability.java | 2 +- .../com/palantir/javaformat/doc/Level.java | 158 ++++++++++-------- .../LevelDelimitedFlatValueDocVisitor.java | 7 +- .../javaformat/java/JavaInputAstVisitor.java | 50 +++--- 10 files changed, 194 insertions(+), 186 deletions(-) diff --git a/buildSrc/src/main/groovy/open-java-format.error-prone-conventions.gradle b/buildSrc/src/main/groovy/open-java-format.error-prone-conventions.gradle index a573c98b7..9bb1278d3 100644 --- a/buildSrc/src/main/groovy/open-java-format.error-prone-conventions.gradle +++ b/buildSrc/src/main/groovy/open-java-format.error-prone-conventions.gradle @@ -16,7 +16,7 @@ nullaway { tasks.withType(JavaCompile).configureEach { options.errorprone { - // Annotation processor output (derive4j, Immutables) is not ours to fix. + // Annotation processor output (Immutables) is not ours to fix. excludedPaths = '.*/build/generated/.*' } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 253826bfa..4d440a4fc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,8 +26,6 @@ compileTesting = { module = "com.google.testing.compile:compile-testing", versio errorprone-core = { module = "com.google.errorprone:error_prone_core", version = "2.50.0" } errorProneSupport-contrib = { module = "tech.picnic.error-prone-support:error-prone-contrib", version = "0.30.0" } nullaway = { module = "com.uber.nullaway:nullaway", version = "0.14.2" } -derive4j = { module = "org.derive4j:derive4j", version = "1.1.1" } -derive4j-annotation = { module = "org.derive4j:derive4j-annotation", version = "1.1.1" } functionaljava = { module = "org.functionaljava:functionaljava", version = "5.0" } guava = { module = "com.google.guava:guava", version = "33.7.1-jre" } guava-testlib = { module = "com.google.guava:guava-testlib", version = "33.5.0-jre" } diff --git a/open-java-format/build.gradle b/open-java-format/build.gradle index 2f07d2c6d..b16e08fac 100644 --- a/open-java-format/build.gradle +++ b/open-java-format/build.gradle @@ -26,8 +26,6 @@ dependencies { compileOnly variantOf(libs.immutables.value) { classifier('annotations') } annotationProcessor libs.immutables.value - compileOnly libs.derive4j.annotation - annotationProcessor libs.derive4j implementation libs.jsr305 } diff --git a/open-java-format/src/main/java/com/palantir/javaformat/BreakBehaviour.java b/open-java-format/src/main/java/com/palantir/javaformat/BreakBehaviour.java index d2ba639bb..fa2c1b15b 100644 --- a/open-java-format/src/main/java/com/palantir/javaformat/BreakBehaviour.java +++ b/open-java-format/src/main/java/com/palantir/javaformat/BreakBehaviour.java @@ -23,96 +23,99 @@ import com.palantir.javaformat.doc.Doc; import com.palantir.javaformat.doc.Level; import java.io.IOException; -import java.io.UncheckedIOException; -import org.derive4j.ArgOption; -import org.derive4j.Data; -@Data(arguments = ArgOption.checkedNotNull) +/** + * How a {@link Level} is broken when it does not fit on one line. Each case's {@code toString()} is what + * {@link com.palantir.javaformat.doc.LevelDelimitedFlatValueDocVisitor} prints for it. + */ @Immutable @JsonSerialize(using = BreakBehaviour.Json.class) -public abstract class BreakBehaviour { - public interface Cases { +public sealed interface BreakBehaviour { - R breakThisLevel(); + static BreakBehaviour breakThisLevel() { + return new BreakThisLevel(); + } - /** - * If the last level is breakable, prefer breaking it if it will keep the rest of this level on line line. - * - * @param keepIndentWhenInlined whether to keep this level's indent when inlined as a recursive level (when - * reached via a previous `preferBreakingLastInnerLevel` whose breakability was - * {@link LastLevelBreakability#CHECK_INNER}) - */ - R preferBreakingLastInnerLevel(boolean keepIndentWhenInlined); + static BreakBehaviour preferBreakingLastInnerLevel(boolean keepIndentWhenInlined) { + return new PreferBreakingLastInnerLevel(keepIndentWhenInlined); + } - /** - * Attempt to inline the suffix of this level (which must be a {@link Level} and the last doc), recursing into - * the {@link Level} just before the last {@link Level} (if there is such a level) to see if that can be broken - * instead. - * - *

This behaves like {@link #breakThisLevel()} if we couldn't recurse into such an inner level, or if the - * suffix level doesn't fit on the last line. - */ - R inlineSuffix(); + static BreakBehaviour inlineSuffix() { + return new InlineSuffix(); + } - /** - * Break if by doing so all inner levels then fit on a single line. However, don't break if we can fit in the - * {@link Doc docs} up to the first break (which might be nested inside the next doc if it's a {@link Level}), - * in order to prevent exceeding the maxLength accidentally. - */ - R breakOnlyIfInnerLevelsThenFitOnOneLine(boolean keepIndentWhenInlined); + static BreakBehaviour breakOnlyIfInnerLevelsThenFitOnOneLine(boolean keepIndentWhenInlined) { + return new BreakOnlyIfInnerLevelsThenFitOnOneLine(keepIndentWhenInlined); } - public abstract R match(Cases cases); + /** Break this level. */ + record BreakThisLevel() implements BreakBehaviour { + @Override + public String toString() { + return "breakThisLevel()"; + } + } - /** For {@link com.palantir.javaformat.doc.LevelDelimitedFlatValueDocVisitor}. */ - @Override - public abstract String toString(); + /** + * If the last level is breakable, prefer breaking it if it will keep the rest of this level on line line. + * + * @param keepIndentWhenInlined whether to keep this level's indent when inlined as a recursive level (when + * reached via a previous `preferBreakingLastInnerLevel` whose breakability was + * {@link LastLevelBreakability#CHECK_INNER}) + */ + record PreferBreakingLastInnerLevel(boolean keepIndentWhenInlined) implements BreakBehaviour { + @Override + public String toString() { + return "preferBreakingLastInnerLevel(" + keepIndentWhenInlined + ")"; + } + } + + /** + * Attempt to inline the suffix of this level (which must be a {@link Level} and the last doc), recursing into the + * {@link Level} just before the last {@link Level} (if there is such a level) to see if that can be broken instead. + * + *

This behaves like {@link BreakThisLevel} if we couldn't recurse into such an inner level, or if the suffix + * level doesn't fit on the last line. + */ + record InlineSuffix() implements BreakBehaviour { + @Override + public String toString() { + return "inlineSuffix()"; + } + } /** - * This is gross but just wanted to get something working. See https://github.com/derive4j/derive4j/issues/51 for a - * potential better implementation. + * Break if by doing so all inner levels then fit on a single line. However, don't break if we can fit in the + * {@link Doc docs} up to the first break (which might be nested inside the next doc if it's a {@link Level}), in + * order to prevent exceeding the maxLength accidentally. + * + * @param keepIndentWhenInlined whether to keep this level's indent when inlined as a recursive level */ - static class Json extends JsonSerializer { + record BreakOnlyIfInnerLevelsThenFitOnOneLine(boolean keepIndentWhenInlined) implements BreakBehaviour { + @Override + public String toString() { + return "breakOnlyIfInnerLevelsThenFitOnOneLine(" + keepIndentWhenInlined + ")"; + } + } + /** Writes the case as {@code type} and its flag, if it has one. */ + final class Json extends JsonSerializer { @Override public void serialize(BreakBehaviour value, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeStartObject(); - BreakBehaviours.caseOf(value) - .breakThisLevel(() -> { - try { - gen.writeObjectField("type", "breakThisLevel"); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - return null; - }) - .preferBreakingLastInnerLevel(keepIndentWhenInlined -> { - try { - gen.writeObjectField("type", "preferBreakingLastInnerLevel"); - gen.writeObjectField("keepIndentWhenInlined", keepIndentWhenInlined); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - return null; - }) - .inlineSuffix(() -> { - try { - gen.writeObjectField("type", "inlineSuffix"); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - return null; - }) - .breakOnlyIfInnerLevelsThenFitOnOneLine(keepIndentWhenInlined -> { - try { - gen.writeObjectField("type", "breakOnlyIfInnerLevelsThenFitOnOneLine"); - gen.writeObjectField("keepIndentWhenInlined", keepIndentWhenInlined); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - return null; - }); + switch (value) { + case BreakThisLevel() -> gen.writeObjectField("type", "breakThisLevel"); + case PreferBreakingLastInnerLevel(boolean keepIndentWhenInlined) -> { + gen.writeObjectField("type", "preferBreakingLastInnerLevel"); + gen.writeObjectField("keepIndentWhenInlined", keepIndentWhenInlined); + } + case InlineSuffix() -> gen.writeObjectField("type", "inlineSuffix"); + case BreakOnlyIfInnerLevelsThenFitOnOneLine(boolean keepIndentWhenInlined) -> { + gen.writeObjectField("type", "breakOnlyIfInnerLevelsThenFitOnOneLine"); + gen.writeObjectField("keepIndentWhenInlined", keepIndentWhenInlined); + } + } gen.writeEndObject(); } } diff --git a/open-java-format/src/main/java/com/palantir/javaformat/LastLevelBreakability.java b/open-java-format/src/main/java/com/palantir/javaformat/LastLevelBreakability.java index 9b96e53b8..f1e6c0000 100644 --- a/open-java-format/src/main/java/com/palantir/javaformat/LastLevelBreakability.java +++ b/open-java-format/src/main/java/com/palantir/javaformat/LastLevelBreakability.java @@ -20,25 +20,25 @@ /** * How to decide whether to break the last inner level ("this level") of a parent level with - * {@link BreakBehaviour.Cases#preferBreakingLastInnerLevel}. + * {@link BreakBehaviour.PreferBreakingLastInnerLevel}. */ public enum LastLevelBreakability { /** - * Default behaviour. When processing a {@link BreakBehaviour.Cases#preferBreakingLastInnerLevel} chain, if we've + * Default behaviour. When processing a {@link BreakBehaviour.PreferBreakingLastInnerLevel} chain, if we've * arrived at a level with this breakability, then we should abort the chain. */ ABORT, /** * Unconditionally allow ending an inline chain at this level, after which this level may be broken as usual, or a * prefix thereof could be inlined further (if it has the appropriate break behaviour of - * {@link BreakBehaviour.Cases#preferBreakingLastInnerLevel}). This should only be used when you know that the first + * {@link BreakBehaviour.PreferBreakingLastInnerLevel}). This should only be used when you know that the first * non-Level {@link Doc} inside this level, if you flatten it, is a {@link Break}. */ ACCEPT_INLINE_CHAIN, /** * Delegate to the {@link LastLevelBreakability} of _this_ level's last inner level. Typically, this will be true if * this level is not immediately followed by a break (see StartsWithBreakVisitor). Behaves the same as - * {@link #ABORT} if this level is not {@link BreakBehaviour.Cases#preferBreakingLastInnerLevel}. + * {@link #ABORT} if this level is not {@link BreakBehaviour.PreferBreakingLastInnerLevel}. */ CHECK_INNER, /** diff --git a/open-java-format/src/main/java/com/palantir/javaformat/OpenOp.java b/open-java-format/src/main/java/com/palantir/javaformat/OpenOp.java index ba25f077c..0f36e404c 100644 --- a/open-java-format/src/main/java/com/palantir/javaformat/OpenOp.java +++ b/open-java-format/src/main/java/com/palantir/javaformat/OpenOp.java @@ -41,7 +41,7 @@ public abstract class OpenOp extends HasUniqueId implements Op { */ @Default public BreakBehaviour breakBehaviour() { - return BreakBehaviours.breakThisLevel(); + return BreakBehaviour.breakThisLevel(); } /** If it's the last level of its parent, when to inline this level rather than break the parent. */ diff --git a/open-java-format/src/main/java/com/palantir/javaformat/PartialInlineability.java b/open-java-format/src/main/java/com/palantir/javaformat/PartialInlineability.java index 2d300047a..423c67527 100644 --- a/open-java-format/src/main/java/com/palantir/javaformat/PartialInlineability.java +++ b/open-java-format/src/main/java/com/palantir/javaformat/PartialInlineability.java @@ -20,7 +20,7 @@ /** * What are the conditions for a level to be partially inlineable. Partial inlining refers to the behaviour of - * {@link BreakBehaviours#breakOnlyIfInnerLevelsThenFitOnOneLine} where a level is too large to fit on the current line, + * {@link BreakBehaviour.BreakOnlyIfInnerLevelsThenFitOnOneLine} where a level is too large to fit on the current line, * but a prefix thereof is partially inlined onto the current line. * *

Specifically, when inlining a level with the above behaviour, the partial inlineability of its first diff --git a/open-java-format/src/main/java/com/palantir/javaformat/doc/Level.java b/open-java-format/src/main/java/com/palantir/javaformat/doc/Level.java index 4f613f9ed..af9bfcc32 100644 --- a/open-java-format/src/main/java/com/palantir/javaformat/doc/Level.java +++ b/open-java-format/src/main/java/com/palantir/javaformat/doc/Level.java @@ -24,7 +24,10 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Range; import com.palantir.javaformat.BreakBehaviour; -import com.palantir.javaformat.BreakBehaviours; +import com.palantir.javaformat.BreakBehaviour.BreakOnlyIfInnerLevelsThenFitOnOneLine; +import com.palantir.javaformat.BreakBehaviour.BreakThisLevel; +import com.palantir.javaformat.BreakBehaviour.InlineSuffix; +import com.palantir.javaformat.BreakBehaviour.PreferBreakingLastInnerLevel; import com.palantir.javaformat.CommentsHelper; import com.palantir.javaformat.Indent; import com.palantir.javaformat.LastLevelBreakability; @@ -126,7 +129,7 @@ public State computeBreaks(CommentsHelper commentsHelper, int maxWidth, State st .orElseGet(() -> { Obs.LevelNode childLevel = observer.newChildNode(this, state); State newState = - getBreakBehaviour().match(new BreakImpl(commentsHelper, maxWidth, state, childLevel)); + new BreakImpl(commentsHelper, maxWidth, state, childLevel).apply(getBreakBehaviour()); return childLevel.finishLevel(state.updateAfterLevel(newState)); }); @@ -170,7 +173,7 @@ && columnBeforeLastBreak > getColumnLimitBeforeLastBreak().getAsInt()) { return Optional.empty(); } - class BreakImpl implements BreakBehaviour.Cases { + class BreakImpl { private final CommentsHelper commentsHelper; private final int maxWidth; private final State state; @@ -184,17 +187,25 @@ public BreakImpl(CommentsHelper commentsHelper, int maxWidth, State state, Obs.L this.levelNode = levelNode; } + State apply(BreakBehaviour breakBehaviour) { + return switch (breakBehaviour) { + case BreakThisLevel() -> breakThisLevel(); + case PreferBreakingLastInnerLevel unused -> preferBreakingLastInnerLevel(); + case InlineSuffix() -> inlineSuffix(); + case BreakOnlyIfInnerLevelsThenFitOnOneLine(boolean keepIndentWhenInlined) -> + breakOnlyIfInnerLevelsThenFitOnOneLine(keepIndentWhenInlined); + }; + } + private Exploration breakNormally(State state) { return Level.this.breakNormally(state, levelNode, commentsHelper, maxWidth); } - @Override - public State breakThisLevel() { + private State breakThisLevel() { return breakNormally(state).markAccepted(); } - @Override - public State preferBreakingLastInnerLevel(boolean _keepIndentWhenInlined) { + private State preferBreakingLastInnerLevel() { int maxDepth = getMaxDepth(); // Explore both breaking and not breaking if we're in the last LAST_LEVELS_TO_EXPLORE levels. Choose the // better one based on LOC, preferring breaks if the outcome is the same. @@ -229,8 +240,7 @@ public State preferBreakingLastInnerLevel(boolean _keepIndentWhenInlined) { return breakNormally(state).markAccepted(); } - @Override - public State breakOnlyIfInnerLevelsThenFitOnOneLine(boolean keepIndentWhenInlined) { + private State breakOnlyIfInnerLevelsThenFitOnOneLine(boolean keepIndentWhenInlined) { Exploration broken = Level.this.breakNormally(state, levelNode, commentsHelper, maxWidth); Optional maybeInlined = levelNode.maybeExplore( @@ -246,8 +256,7 @@ public State breakOnlyIfInnerLevelsThenFitOnOneLine(boolean keepIndentWhenInline } } - @Override - public State inlineSuffix() { + private State inlineSuffix() { Optional lastLevelBroken = levelNode.maybeExplore( "inlineSuffix", state, @@ -265,7 +274,7 @@ private Exploration breakNormally(State state, LevelNode levelNode, CommentsHelp } /** - * Attempts to perform the {@link BreakBehaviour.Cases#breakOnlyIfInnerLevelsThenFitOnOneLine} logic, returning + * Attempts to perform the {@link BreakOnlyIfInnerLevelsThenFitOnOneLine} logic, returning * empty if it couldn't. Namely, this will only return a state if: *