Skip to content

Measure the whole type of a cast before inlining a chain onto it - #78

Merged
abashev merged 2 commits into
mainfrom
cast-type-without-breaks
Sep 25, 2026
Merged

abashev merged 2 commits into
mainfrom
cast-type-without-breaks

Conversation

@abashev

@abashev abashev commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

Closes #60.

The bug

In sun.rmi.transport.tcp.TCPTransport the cast (PrivilegedAction<String>) came out cut between < and String, with the lambda it applies to on a third line, while the call around it stayed on one line:

    static final Log tcpLog = Log.getLog(
            "sun.rmi.transport.tcp", "tcp", LogStream.parseLevel(AccessController.doPrivileged((PrivilegedAction<
                            String>)
                    () -> System.getProperty("sun.rmi.transport.tcp.logLevel"))));

Why

The arguments of getLog are inlined when a prefix of the last one fits, and CountWidthUntilBreakVisitor measures that prefix up to the first break inside the last level. visitTypeCast declares its level ACCEPT_INLINE_CHAIN, which means that first break is the one before the operand. The first break was the one visitParameterizedType emits after <, so the prefix ended at (PrivilegedAction<, fit, and the type then had to take that break because String>) did not fit behind it. A wildcard bound (? extends List<Integer>), an intersection type (Runnable & Serializable) and an annotated type (@Nullable String) offer breaks of their own in the same place and split the same way.

The fix

The cast's level gets PartialInlineability.IF_FIRST_LEVEL_FITS, what visitDotWithPrefix uses for the same purpose: the visitor counts the whole first inner level, which is the rest of the type up to the break before the operand, when it fits. A chain is inlined onto a cast only if the type fits behind it. Here it does not, the chain is not inlined, and the arguments break one per line, as they do today when the type is not parameterized:

    static final Log tcpLog = Log.getLog(
            "sun.rmi.transport.tcp",
            "tcp",
            LogStream.parseLevel(AccessController.doPrivileged(
                    (PrivilegedAction<String>) () -> System.getProperty("sun.rmi.transport.tcp.logLevel"))));

The type stays breakable for a cast that does not fit on a line at all: laying the type out without breaks, the first option in #60, gave the same result here but turned the 231-character cast of the golden C into a 254-column line, so it was dropped.

Checked

  • The golden ojf-issue-60-cast-type-in-inline-chain holds the statement above, the same cast one call shallower (which fit and does not change), and a method reference, two type arguments, a wildcard bound, a qualified name, an intersection type and an annotated type in the cast. It fails on main and passes with the change, and its output is stable on a second run.
  • ./gradlew :open-java-format:test on JDK 21: 1545 tests, all green, the goldens C and B23749160 among them.
  • Of the 15,747 files of the JDK 21 sources, that one statement in TCPTransport is the only one that formats differently, as A cast breaks inside its type when the call around it stays on one line #60 predicted; the 203 lambdas and 17 method references cast to a generic type elsewhere in the JDK are unchanged.

Version

A bug fix that changes the output of one shape, which the README allows in 2.x. The Migrate page on the website lists such fixes and should get a line for this one with the release that carries it.

In sun.rmi.transport.tcp.TCPTransport the cast (PrivilegedAction<String>)
came out cut between "<" and "String", with the lambda it applies to on a
third line, while the call around it stayed on one line (#60):

    static final Log tcpLog = Log.getLog(
            "sun.rmi.transport.tcp", "tcp", LogStream.parseLevel(AccessController.doPrivileged((PrivilegedAction<
                            String>)
                    () -> System.getProperty("sun.rmi.transport.tcp.logLevel"))));

The arguments of getLog are inlined when a prefix of the last one fits,
and CountWidthUntilBreakVisitor measures that prefix up to the first
break inside the last level. visitTypeCast declares its level
ACCEPT_INLINE_CHAIN, which means that first break is the one before the
operand. The first break was the one visitParameterizedType emits after
"<", so the prefix ended at "(PrivilegedAction<", fit, and the type had
to take the break because "String>)" did not fit behind it. A wildcard
bound, an intersection type and an annotated type offer breaks of their
own in the same place and split the same way.

The cast's level now has PartialInlineability.IF_FIRST_LEVEL_FITS, what
visitDotWithPrefix uses for the same purpose: the visitor counts the
whole first inner level, which is the rest of the type up to the break
before the operand, when it fits. A chain is inlined onto a cast only if
the type fits behind it. Here it does not, the chain is not inlined, and
the arguments break one per line, as they do when the type is not
parameterized. The type stays breakable for a cast that does not fit on
a line at all, so the golden C keeps its output.

The golden holds the statement above, the same cast one call shallower,
which fit and does not change, and a method reference, two type
arguments, a wildcard bound, a qualified name, an intersection type and
an annotated type in the cast. Of the 15,747 files of the JDK 21
sources, that one statement in TCPTransport is the only one that
formats differently, and its new output is stable.
The input was the wanted output with the arguments joined onto one line,
so the golden checked little besides the chain. Each statement now
arrives in a different shape: the first one as the broken output the fix
removes, the others on one line, or split at the "::", inside the type
arguments, after "&" or before the lambda, with stray spaces around "<",
",", "::", "->" and "[ ]". The expected output does not change, and main
still formats the input differently.
abashev added a commit to openjavaformat/docs that referenced this pull request Sep 25, 2026
openjavaformat/open-java-format#78 stops splitting a cast to a
parameterized type inside its type when an inlined call chain has no
room for the type; the call breaks its arguments one per line instead.
It is a bug fix that changes the output of one shape, which the 2.x
line allows, and Migrate lists every such fix. The two fixes of 2.98.0.3
become a list, the cast fix joins it marked as not yet released, and the
corpus check says what it changes: one statement of the JDK 21 sources,
in sun.rmi.transport.tcp.TCPTransport.
@abashev
abashev merged commit a427d59 into main Sep 25, 2026
15 checks passed
@abashev
abashev deleted the cast-type-without-breaks branch September 25, 2026 12:52
abashev added a commit to openjavaformat/docs that referenced this pull request Sep 25, 2026
2.98.0.4 is out on GitHub, Maven Central and the Gradle Plugin Portal,
so the pages now install it. It carries the three output changes that
Migrate listed as not yet in a release: the cast fix
(openjavaformat/open-java-format#78), the switch expression on the
declaration's line (openjavaformat/open-java-format#76) and the /***
banner that no longer counts as javadoc
(openjavaformat/open-java-format#74). Each now says it applies since
2.98.0.4, and the corpus paragraph no longer calls them unreleased.

The GitHub Action page keeps 2.98.0.3 as the default of the action's
version input, because the action itself still defaults to it.
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.

A cast breaks inside its type when the call around it stays on one line

1 participant