Skip to content

Keep java.lang imports that disambiguate a simple name - #174

Merged
abashev merged 2 commits into
21.X.Xfrom
fix/keep-used-java-lang-import
Oct 30, 2026
Merged

abashev merged 2 commits into
21.X.Xfrom
fix/keep-used-java-lang-import

Conversation

@abashev

@abashev abashev commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #149.

What was wrong

Removing unused imports dropped every import from java.lang, used or not. A single-type import shadows a type with the same simple name declared in the file's own package, and one brought in by an import on demand (JLS 6.4.1). So a used import java.lang.Byte; can be the only thing that binds Byte to the JDK class, and removing it makes the name ambiguous or binds it to the other class:

package com.company.project.bar;

import com.company.project.foo.*; // also declares a Byte
import java.lang.Byte;

public class Test {
    Byte foo() {
        return null;
    }
}

Until now the java.lang.Byte import was removed. I checked this with a com.company.project.foo.Byte class next to the file: the input compiles, but the output of 21.X.X fails with reference to Byte is ambiguous. Now the import stays, because Byte is used. Unused java.lang imports and import java.lang.*; are still removed, and so are imports from the file's own package, which can never disambiguate anything.

The change

The commit is palantir#1778 by Zayan Khan, cherry-picked with -x (c9d7b75), with the author and the message kept. Upstream closed the PR unmerged; it fixes palantir#1334. Its CLAUDE.md and changelog commits are left out. The only conflict was in RemoveUnusedImportsTest, whose table has more rows here; upstream's rows come after ours.

Tests

The first commit adds upstream's five rows to RemoveUnusedImportsTest and changes the existing row whose output now keeps import java.lang.Foo;.

The second commit is ours. It moves the tests of unused import removal into case files under test-cases/imports/, one file per test, named after what it checks:

  • 19 of the table's 23 rows and the one test of RemoveUnusedImportsCaseLabelsTest became cases. A case runs the whole formatter, so its after block is the row's expected output, formatted.
  • Four rows stay in RemoveUnusedImportsTest as plain tests with text blocks:

With the RemoveUnusedImports of 21.X.X, four cases fail: each loses a java.lang import it uses. With the fix all of them pass.

  • FormatterCasesTest: 887 → 907
  • RemoveUnusedImportsTest: 18 → 4 (the module import is skipped on JDK 21; on JDK 25 all four run)
  • RemoveUnusedImportsCaseLabelsTest: removed, its test is now a case
  • open-java-format: 1408 → 1413 tests
  • ./gradlew test --rerun passes for all modules (Gradle plugin 51, IntelliJ plugin 10), and the 907 cases also pass on JDK 25

Effect on output

Of the 15,747 files of the JDK 21 sources, 13 format differently. Each of them keeps a java.lang import that it uses (String, Integer, Character, RuntimePermission and others), and a second run changes none of them. In two of these files a comment that belongs to such an import no longer ends up somewhere else:

 // java import
 //
+import java.lang.Comparable; // to be substituted for jdk1.1.x
 import java.util.Set;
-// to be substituted for jdk1.1.x

That is javax/management/openmbean/OpenMBeanParameterInfo.java. In java/util/prefs/Preferences.java, the comment // These imports needed only as a workaround for a JavaDoc bug stays with its import, where before it was left alone after the import block.

Because this changes output, it counts toward the second number of the version.

ZayanKhan-12 and others added 2 commits October 30, 2026 13:23
`RemoveUnusedImports` deleted every import qualified by `java.lang`
unconditionally. That is unsound: per JLS 6.4.1 a single-type-import
declaration shadows both types declared elsewhere in the current package
and types brought in by an import-on-demand, so `import java.lang.Byte;`
can be the only thing binding `Byte` to the JDK class.

Removing it either makes the reference ambiguous:

    error: reference to Byte is ambiguous
      both class com.company.project.foo.Byte in com.company.project.foo
      and class java.lang.Byte in java.lang match

or silently rebinds the name to a same-package class, which then fails to
compile against the wrong type.

`RemoveUnusedImports` analyses one compilation unit with no classpath and
no symbol resolution, so it cannot prove that no clashing type exists.
Only remove a `java.lang` single-type-import when its simple name is not
referenced at all.

Unchanged: `import java.lang.*;` is still removed (java.lang is implicitly
imported on demand), unused `java.lang` imports are still removed, and
imports of same-package types are still removed -- package members already
shadow import-on-demand declarations, so those can never be disambiguating.

One existing test case encoded the old behaviour and has been updated.

Fixes palantir#1334

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
(cherry picked from commit c9d7b75)
RemoveUnusedImportsTest was a table inherited from google-java-format:
each row an array of input lines and one of output lines, joined with
\n and run through RemoveUnusedImports alone. 19 of its 23 rows are now
cases under test-cases/imports, one file each, named after what it
checks, and so is the one test of RemoveUnusedImportsCaseLabelsTest. A
case runs the whole formatter, so its after block is the row's
expected output, formatted: for every row the formatted input and the
formatted expectation are the same text, and a second run changes
nothing. One row's input was formatted already, so its case lays the
same code out another way and has something to format.

Four rows stay in RemoveUnusedImportsTest, as plain tests with text
blocks. The three of #37 cannot be cases: a case fixes the imports
before it formats, and formatting takes a doubled blank line away by
itself, while the command line formats first. The module import parses
from JDK 23 on, and the cases run on JDK 21 as well.

With the RemoveUnusedImports of 21.X.X, the same four cases fail as
the rows did: the ones with a used import from java.lang.

Tests on JDK 21: FormatterCasesTest 887 -> 907, RemoveUnusedImportsTest
23 -> 4 (the module import skipped), RemoveUnusedImportsCaseLabelsTest
1 -> gone; 1413 tests in the module, as before. On JDK 25 the four
tests of RemoveUnusedImportsTest all run, and the 907 cases pass.
@abashev
abashev merged commit 05b43c8 into 21.X.X Oct 30, 2026
17 checks passed
@abashev
abashev deleted the fix/keep-used-java-lang-import branch October 30, 2026 18:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants