JDK-8356043 : Add lint warnings for unnecessary warning suppression
  • Type: CSR
  • Component: tools
  • Sub-Component: javac
  • Priority: P4
  • Status: Closed
  • Resolution: Approved
  • Fix Versions: 26
  • Submitted: 2025-05-01
  • Updated: 2025-09-15
  • Resolved: 2025-09-15
Related Reports
CSR :  
Relates :  
Description
Summary
-------

Add a new compiler warning that warns about unnecessary warning suppressions in `@SuppressWarnings` annotations.

Problem
-------

The Java compiler emits warnings to alert developers about code that appears incorrect or imprudent. It is then up to the developer to apply human judgement to decide whether to change the code, ignore the warning, or suppress the warning.

For lint warnings (which is most of them), suppression is accomplished at a per-declaration level by `@SuppressWarnings` annotations and globally by the `-Xlint:-category` command line flag.

There are a couple of problems with this mechanism. The first problem is that `@SuppressWarnings` and `-Xlint:-category` flags are very blunt instruments. In fact, the latter is _maximally_ blunt: it covers the entire compilation. The former is still quite blunt, because while affected warnings occur at specific lines of code, the `@SuppressWarnings` annotation always applies to an entire class, method, or variable declaration.

This imprecision is a problem because that excessive coverage creates opportunity for false negatives. That is, warnings other than the particular one being targeted could be suppressed unwittingly, creating an opportunity for uncaught bugs.

`@SuppressWarnings` annotations are sometimes added even when they're never actually needed, i.e., proactively as the code is written, because the developer thinks they _might_ be needed and wants to potentially save one compile & debug cycle. But the compiler provides the same feedback (i.e., none) whether or not there was actually any warning there, so this type of mistake is rarely caught.

The second problem is that once a warning is suppressed, the affected code can continue to evolve, but in practice the continued need for the suppression is rarely, if ever, reviewed. This is due mostly to the "out of sight, out of mind" effect, but also even for a developer diligent enough to track this, doing such a review would require a tedious manual, per-warning audit. This also creates an opportunity for false negatives, i.e., more uncaught bugs.

Perhaps most frustrating, the compiler _by definition_ has all the information needed to detect unnecessary warning suppression, yet there's no way for developers to automatically extract or apply this information. If the compiler could detect and report unnecessary warning suppression, developers could ensure their warning suppression is as narrow as it can possibly be.

An analysis of the JDK reveals over 400 unnecessary suppressions currently being applied, so this problem is not theoretical.

Solution
--------

The solution is add the ability for the compiler to generate an "unnecessary suppression" warning when a lint category is being suppressed unnecessarily.

This CSR proposes to warn only about unnecesssary warning suppression via `@SuppressWarnings` annotations; unnecessary suppression via the `-Xlint:-category` flag is left for future study.

*Definition of Unnecessary Suppression*

First, it is important to be precise about the meaning of "unnecessary suppression". A _suppression_ is the occurrence of a lint category within a `@SuppressWarnings` annotation. Such a suppression is _unnecessary_ if recompiling with (a) that suppression removed and (b) that lint category enabled would _not_ result in the generation of new warning(s) in that lint category in any of the code covered by the suppression.

Condition (b) is because we want the determination of whether a suppression is unnecessary to be a function of the code itself, not the details of how a particular compiler run is configured. Therefore, the determination is always made as if the corresponding category were enabled. That is, disabling all lint categories doesn't suddenly make all suppressions unnecessary.

When it comes to reporting unnecessary suppressions, ideally the compiler will be perfectly accurate, but if not, a crucial observation is that false positives are much worse than false negatives: a false negative is simply equivalent to the current behavior, while a false positive could put a developer in a situation where it would be impossible to ever build without warnings (short of removing code). Therefore, it is critical that false positives be avoided. When given a choice, the implementation should err on the side of false negatives.

*New Lint Category*

A new `"suppression"` lint category will be added to allow developers to control whether this new warning is enabled using the usual mechanism. Even though the new warning relates to the suppression of other warnings, it is still just a normal compiler warning with an associated lint category that can be controlled using the normal mechanisms, i.e., `@SuppressWarnings` annotations and `-Xlint` flags.

By default, the new `"suppression"` lint category will be disabled: the `-Xlint` flag must be used to enable it, via `-Xlint:all` or `-Xlint:suppression`.

*Existing Lint Categories*

A few lint categories don't support suppression via `@SuppressWarnings`: these are `"classfile"`, `"incubating"`, `"options"`, `"output-file-clash"`, `"path"`, and `"processing"`. Therefore, occurrences of (for example) `@SuppressWarnings("classfile")` have no effect and are therefore always unnecessary. When unnecessary suppression warnings are enabled, such occurrences will always trigger a warning.

On the other hand, warnings will never be generated for unrecognized lint categories, e.g. `@SuppressWarnings("blah")`. This is consistent with the JLS recommendation for compilers to ignore such values. This includes `doclint` keys, which are not lint categories.

*Nested Annotations*

The determination of whether a suppression is unnecessary is unambiguous except possibly in the scenario where a warning is suppressed twice at two different levels, for example:
```
@SuppressWarnings("divzero")
public class A {

    @SuppressWarnings("divzero")
    public int m() {
        return 1/0;
    }
}
```

Clearly one of the `@SuppressWarnings` annotations is unnecessary, but which one? The implementation always assumes that the innermost annotation is the "real" one doing the suppression, and therefore any outer annotations are the "unnecessary" ones. In the above example, the outer annotation on `class A` would generate an unnecessary suppression warning. This behavior is consistent with the principle that warning suppression should be apply as narrowly as possible: removing the annotation indicated by the warning will ensure that this property is maintained.

*Self Reference*

What is the meaning of `@SuppressWarnings("suppression")`? Although self-referential, this annotation is both valid and useful. It means no unnecessary suppression warnings (i.e., the warnings in lint category `"suppression"`) will arise from that annotation, or from any annotation contained by the corresponding declaration.

For example, no unnecessary suppression warning would be generated by this code:
```
@SuppressWarnings("suppression")
public class A {

    @SuppressWarnings("unchecked")
    public abstract int m();
}
```
or by this code:
```
@SuppressWarnings({ "suppression", "unchecked" })
public class A {

    public abstract int m();
}
```

As mentioned above, `@SuppressWarnings("suppression")` applies equally to itself. So no unnecessary suppression warning would be generated by this code either:
```
@SuppressWarnings("suppression")
public class A { }
```
Put another way, an unnecessary suppression warning can never be generated for the `"suppression"` lint category.

*Different Compiler Versions*

When the same code is compiled under two different compiler versions, a warning in lint category `"serial"` (for example) may occur under compiler version X but not compiler version Y. This means that a `@SuppressWarnings("serial")` that suppresses the warning in version X would be unnecessary in version Y, and therefore could possibly generate an unnecessary suppression warning.

This is perfectly normal situation: Just as we address the `"serial"` warning occuring under X but not Y by adding `@SuppressWarnings("serial")`, we address the `"suppression"` warning occuring under Y but not X by adding `@SuppressWarnings("suppression")` (in practice, these two annotations would likely merge as `@SuppressWarnings({ "serial", "suppression" })`).

This answer should always be safe: When we combine the facts that (a) compilers are required to ignore unrecognized strings in `@SuppressWarnings` annotations and (b) unnecessary suppression warnings can never be generated for the `"suppression"` lint category, it follows that adding `@SuppressWarnings("suppression")` can never cause any _new_ warnings under any compiler version.

Specification
-------------

The output of `javac --help-lint` changes as follows:
```
--- x0	2025-05-01 14:38:06.521664879 -0500
+++ x1	2025-05-01 14:38:30.359932368 -0500
@@ -167,6 +167,8 @@
                          Also warn about other suspect declarations in Serializable and Externalizable classes and interfaces.
     static               Warn about accessing a static member using an instance.
     strictfp             Warn about unnecessary use of the strictfp modifier.
+    suppression          Warn about recognized @SuppressWarnings values that don't actually suppress any warnings.
     synchronization      Warn about synchronization attempts on instances of value-based classes.
     text-blocks          Warn about inconsistent white space characters in text block indentation.
     this-escape          Warn when a constructor invokes a method that could be overriden in an external subclass.
```

The warning for unnecessary suppression via `@SuppressWarnings` looks like this:
```
    $ javac -Xlint:suppression Test.java
    Test.java:10: warning: [suppression] unnecessary warning suppression: "divzero"
        @SuppressWarnings("divzero")
        ^
    1 warning
```

If multiple categories in the same annotation are being warned about, the warning looks like this:
```
    $ javac -Xlint:suppression Test.java
    Test.java:10: warning: [suppression] unnecessary warning suppression: "rawtypes", "unchecked"
        @SuppressWarnings({ "rawtypes", "unchecked" })
        ^
    1 warning
```

Comments
Moving to Approved now that the CSR has a reviewer.
15-09-2025

[~jlahoda] thanks for taking a look.
15-09-2025

Looks reasonable to me.
15-09-2025

[~vromero] or [~jlahoda] can you review this CSR? Thanks. Moving to Provisional until at least one `javac`-area reviewer.
11-09-2025

Thanks for the confirmation [~acobbs]. I believe these policies in total address my concerns about source compatibility when `-Xlint:all -Werror` is used on different compilers or different versions of the same compiler.
10-09-2025

Hi [~darcy], > Please confirm Yes to both. The key property that make this true is that adding `@SuppressWarnings("suppression")` can never increase the number of warnings (of any kind) that are generated: * If JDK $N does not understand `"suppression"`, then it will simply ignore it - the number of warnings does not change * If JDK $N does understand `"suppression"`, then the number of warnings can only decrease Here's a more complete "story". Let $M be the first JDK version that supports `"suppression`". * It used to be true that adding `@SuppressWarnings("foobar")` could never increase the number of JDK $N warnings for any $N * But when $N ≥ $M, that's no longer true: the `"foobar"` could now be flagged as unnecessary, generating a new warning * But this new corner case can always be addressed by adding `@SuppressWarnings("suppression")` * And doing so can never jeopardize compilation when $N < $M because the `"suppression"` will simply be ignored
09-09-2025

Okay -- another round of discussion to check some particular scenarios. As some of the early email traffic on this topic indicated, a particular use-case I'm interest in is having `-Xlint:all -Werror` work as part of the JDK build. For a given JDK release $N, a subset of the sources in JDK $N might be compiled with JDK ($N-1), JDK $N, or JDK ($N-2) -- the last of these occurring at the start of a new release. So if there is a new warning category added "not-in-old-release", a declaration like @SuppressWarning("not-in-old-release") Foo myFooField = ... should compile find under any of JDK $N, $N-1, or $N-2. And if the set of warning for an existing categories expands or contracts -- say due to a bug fix -- than @SuppressWarning({"lint-category", "suppression"}) Bar myBarField = ... will likewise compile under any of JDK $N, $N-1, or $N-2. Please confirm
08-09-2025

Hi [~darcy], Let's tackle #2 and #3 first... #2: > Attempt to compile the same code under `-Xlint:all -Werror` under two versions of `javac` where the set of supported suppress warnings strings differ. This should work as the unrecognized option would be ignored in the `javac` version that doesn't support it. Correct... A minor subtlety is that there are now two levels of "ignoring" at work here when the compiler encounters an unknown lint category string like `@SuppressWarnings("nonexistent")`. First level - As usual, the compiler won't try to suppress a category of warnings that it knows nothing about, obviously, and it won't complain about not recognizing `nonexistent`. No change here. Second level - Even though this annotation is actually not suppressing any warnings, the `suppression` warning logic will never _complain_ about it because category `nonexistent` is unknown. Put another way, the new warning only warns about _recognized_ lint categories in a `@SuppressWarnings` annotation that fail to suppress any warnings. #3: > Attempt to compile the same code under `-Xlint:all -Werror` under two versions of `javac` where the set of warnings generated for a given category is different (e.g. the expansion of serial warnings). For the code that did _not_ warn in only one compiler version, it would have to have `suppression` added to any `@SuppressWarning` carve-outs. Correct. This is just a more loopy version of what happens with any other warning category. For example, if a new type of `serial` warning were added, code that compiles cleaning in compiler version X might start generating a new warning in compiler version X + 1. In that situation you would need to add a `@SuppressWarnings("serial")` somewhere to restore the previous clean compile. Similarly, in the situation you describe, the same action is required: A new warning has popped up in the lint category `suppression`, so you need to add a `@SuppressWarnings("suppression")` somewhere to restore the previous clean compile. #1: > Attempt to compile the same code under `-Xlint:all -Werror` under `javac` and another compiler. Any suppression categories _not_ recognized by `javac` will be ignored. Correct, and the same is true for the other compiler of course. And that includes `suppression` of course, if the other compiler doesn't recognize it. > The `javac` compiler warning in _fewer_ locations than the other compiler for the same warning category could be handled with a `@SuppressWarning("suppression")` annotation on the affected code. Correct. This is a variant of #3. If for some compiler version, it warns in fewer locations, then either (a) the compiler doesn't support `suppression`, so there's no problem and nothing to fix, or (b) the compiler supports `suppression`, in which case it will generate a new `suppression` warning about the unnecessary `@SuppressWarnings` annotation string, and so a new `@SuppressWarnings("suppression")` annotation somewhere - or adding `"suppression"` to the existing annotation - will fix it. Thanks for your good questions :)
06-09-2025

Okay; let's work through some scenarios and to see how well this proposal supports them: * Attempt to compile the same code under `-Xlint:all -Werror` under `javac` and another compiler. Any suppression categories _not_ recognized by `javac` will be ignored. The `javac` compiler warning in _fewer_ locations than the other compiler for the same warning category could be handled with a `@SuppressWarning("suppression")` annotation on the affected code. * Attempt to compile the same code under `-Xlint:all -Werror` under two versions of `javac` where the set of supported suppress warnings strings differ. This should work as the unrecognized option would be ignored in the `javac` version that doesn't support it. * Attempt to compile the same code under `-Xlint:all -Werror` under two versions of `javac` where the set of warnings generated for a given category is different (e.g. the expansion of serial warnings). For the code that did _not_ warn in only one compiler version, it would have to have `suppression` added to any `@SuppressWarning` carve-outs. [~acobbs], are the above scenarios correct? Moving to Provisional.
05-09-2025

Updated to remove suppression tracking for suppression via `-Xlint:-foo` flags.
10-05-2025

> If synchronization is superseded by identity for the same category of checks, it could be challenging to get the same code base "meta-lint clean" on multiple compiler versions. As long as "synchronization" and "identity" point to the same `LintCategory` (as they will), then this won't be a problem - suppression tracking is based on the category not the option string or chosen alias. > Depending on how suppression is handled, there are also hazards for compiling under... True, but as [~mcimadamore] pointed out this is true for any newly introduced warning - and so the usual solution applies - i.e., you would add `@SuppressWarnings("suppression")` if the new warning starts popping up and you don't have time to fix it (or if you have time, you fix it and move on). Now I agree with you that there could be some different/new issues that pop up with the warning about unnecessary `-Xlint:-foo` flags. I think now we should just side-step those by only warning about suppression via `@SuppressWarnings` in this first version. We can wait until later to worry about adding support for warnings about suppression via `-Xlint:-foo`. Your thoughts?
09-05-2025

PS See JDK-8356539 for potential complications of a `suppression` type policy. If `synchronization` is superseded by `identity` for the same category of checks, it could be challenging to get the same code base "meta-lint clean" on multiple compiler versions. Depending on how suppression is handled, there are also hazards for compiling under `$JDK_N/bin/javac -X:lint:all -Werror ..` compared to `$JDK_(N+k)/bin/javac -X:lint:all -Werror --release $N ...`
09-05-2025

FYI I also asked for additional input on the `compiler-dev` mailing list [here][1]. [1]: https://mail.openjdk.org/pipermail/compiler-dev/2025-May/030223.html
07-05-2025

I think this warning will be valuable but your points make sense and they argue for not including `suppression` in `-Xlint:all`. Let's explore what that means: either (a) it's still a lint category but excepted from `all`, or (b) it's not a lint category anymore. The downside of (a) is that "all" no longer really mans "all", which might be confusing. The downside of (b) is that you lose the ability to say `@SuppressWarnings("suppression")` which is likely to be helpful in those very situations you allude to (e.g., compiling with different JDK versions). In addition, we would have to create some new flag like `-warn-unnecessary-suppression` or whatever. I think (a) is the better compromise. This exception could be reverted in the future if the warning was sufficiently popular. Until then, if you want all lint categories enabled *including* `suppression`, you would have to make that explicit via `-Xlint:all,suppression`. The javac help and man page would of course need to document which categories were excepted from `-Xlint:all`.
06-05-2025

A significant subset of the JDK code base is compiled under "-Xlint:all -Werror". I think this is a strong, simple property that is valuable to maintain. I don't know how many other nontrivial code bases follow suit. As alluded to the previous mailing list discussion, different Java compilers accept different sets of suppression strings, including different versions of `javac`, and different versions of `javac` and have semantically different checks for the same lint warning. Additionally, some warnings like deprecation and dependent on the version of JDK library being compiled against so a suppression of deprecation could be needed in some contexts but not others. Getting code to compile under `-Xlint:al -Werror` is already challenging and I don't think the build system trade offs to bundling extraneous suppression detection to the lint mechanism are advantageous. Those sort of checks feel to me more appropriate for an out-of-lint-band / opt-in mechanism.
06-05-2025

Hi [~acobbs]. I support code being clear of `javac` lint warnings (cf JEP 212: Resolve Lint and Doclint Warnings, JDK-8042878) and I think this proposal does address a gap in the platform capabilities to be able to get code clear of "meta-lint" conditions or to be not just "perfect" with respect to warnings but be "pluperfect." However, I do have reservations about how this mechanism is proposed to be implemented so I'm moving the CSR back to Draft. I'd like us to reconsider functionality in this area early in the JDK 26 timeline. That should give sufficient time to develop some alternatives and work through implications for more complicated compilation scenarios.
06-05-2025

[~acobbs], acknowledged. The CSR process is getting lots of submissions at the moment ahead of JDK 25 rampdown 1.
05-05-2025

[~darcy], the associated PR is still in draft and I'm not sure it will get done for JDK 25 but I wanted to go ahead and get this CSR in the pipeline in case it is. Thanks.
05-05-2025