From 73de83d9dddcd4ac8f3c017ed0fb03ad9ed0fe60 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 27 May 2026 17:02:42 -0700 Subject: [PATCH 01/10] Improve efficiency of "unneeded.suppression" warning --- .../framework/source/SourceChecker.java | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java index 96feb1d6a233..9062fbec4c87 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java @@ -455,9 +455,6 @@ public abstract class SourceChecker extends AbstractTypeProcessor implements Opt /** The SuppressWarnings prefix that will suppress warnings for all checkers. */ public static final String SUPPRESS_ALL_PREFIX = "allcheckers"; - /** The message key emitted when an unused warning suppression is found. */ - public static final @CompilerMessageKey String UNNEEDED_SUPPRESSION_KEY = "unneeded.suppression"; - /** File name of the localized messages. */ protected static final String MSGS_FILE = "messages.properties"; @@ -2492,11 +2489,11 @@ protected void warnUnneededSuppressions() { * * @param elementsSuppress elements with a {@code @SuppressWarnings} that actually suppressed a * warning - * @param prefixes the SuppressWarnings prefixes that suppress all warnings from this checker + * @param checkerPrefixes the SuppressWarnings prefixes associated with this checker * @param allErrorKeys all error keys that can be issued by this checker */ protected void warnUnneededSuppressions( - Set elementsSuppress, Set prefixes, Set allErrorKeys) { + Set elementsSuppress, Set checkerPrefixes, Set allErrorKeys) { for (Tree tree : getVisitor().treesWithSuppressWarnings) { Element elt = TreeUtils.elementFromTree(tree); // TODO: This test is too coarse. The fact that this @SuppressWarnings suppressed @@ -2504,7 +2501,7 @@ protected void warnUnneededSuppressions( if (elementsSuppress.contains(elt)) { continue; } - // tree has a @SuppressWarnings annotation that didn't suppress any warnings. + // `tree` has a @SuppressWarnings annotation that didn't suppress any warnings. SuppressWarnings suppressAnno = elt.getAnnotation(SuppressWarnings.class); String[] suppressWarningsStrings = suppressAnno.value(); for (String suppressWarningsString : suppressWarningsStrings) { @@ -2512,12 +2509,17 @@ protected void warnUnneededSuppressions( && warnUnneededSuppressionsExceptions.matcher(suppressWarningsString).find(0)) { continue; } - for (String prefix : prefixes) { - if (suppressWarningsString.equals(prefix) - || (suppressWarningsString.startsWith(prefix + ":") - && !suppressWarningsString.equals(prefix + ":" + UNNEEDED_SUPPRESSION_KEY))) { - reportUnneededSuppression(tree, suppressWarningsString); - break; // Don't report the same warning string more than once. + if (checkerPrefixes.contains(suppressWarningsString)) { + reportUnneededSuppression(tree, suppressWarningsString); + } else { + int colonPos = suppressWarningsString.indexOf(":"); + if (colonPos != -1) { + String warningPrefix = suppressWarningsString.substring(0, colonPos); + if (checkerPrefixes.contains(warningPrefix) + && !suppressWarningsString.regionMatches( + colonPos, ":unneeded.suppression", 0, ":unneeded.suppression".length())) { + reportUnneededSuppression(tree, suppressWarningsString); + } } } } @@ -2535,7 +2537,7 @@ private void reportUnneededSuppression(Tree tree, String suppressWarningsString) report( swTree, Diagnostic.Kind.MANDATORY_WARNING, - SourceChecker.UNNEEDED_SUPPRESSION_KEY, + "unneeded.suppression", "\"" + suppressWarningsString + "\"", getClass().getSimpleName()); } @@ -2820,7 +2822,7 @@ private boolean shouldSuppress( if (prefixes.contains(currentSuppressWarningsInEffect)) { // The value in the @SuppressWarnings is exactly a prefix. // Suppress the warning unless its message key is "unneeded.suppression". - boolean result = !currentSuppressWarningsInEffect.equals(UNNEEDED_SUPPRESSION_KEY); + boolean result = !currentSuppressWarningsInEffect.equals("unneeded.suppression"); return result; } else if (requirePrefixInWarningSuppressions) { // A prefix is required, but this SuppressWarnings string does not have a @@ -2829,7 +2831,7 @@ private boolean shouldSuppress( } else if (currentSuppressWarningsInEffect.equals(SUPPRESS_ALL_MESSAGE_KEY)) { // Prefixes aren't required and the SuppressWarnings string is "all". // Suppress the warning unless its message key is "unneeded.suppression". - boolean result = !currentSuppressWarningsInEffect.equals(UNNEEDED_SUPPRESSION_KEY); + boolean result = !currentSuppressWarningsInEffect.equals("unneeded.suppression"); return result; } // The currentSuppressWarningsInEffect is not a prefix or a prefix:message-key, so From 990b6a6b82fc652d7409bf8aa401c3f115c1ae40 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 27 May 2026 17:06:14 -0700 Subject: [PATCH 02/10] Tweak documentation --- .../checkerframework/framework/source/SourceChecker.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java index 9062fbec4c87..5582b5f61727 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java @@ -2483,9 +2483,9 @@ protected void warnUnneededSuppressions() { } /** - * Issues a warning about any {@code @SuppressWarnings} string that didn't suppress a warning, but - * starts with one of the given prefixes (checker names). Does nothing if the string doesn't start - * with a checker name. + * Issues a warning about any {@code @SuppressWarnings} string that didn't suppress a warning. Has + * an effect only if the string is one of the given prefixes (checker names) or starts with one + * followed by a colon. * * @param elementsSuppress elements with a {@code @SuppressWarnings} that actually suppressed a * warning From 7cedddc817210dcba131241afba1c8d7e5a2d786 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 27 May 2026 17:42:58 -0700 Subject: [PATCH 03/10] Code review changes --- .../framework/source/SourceChecker.java | 58 +++++++++---------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java index 5582b5f61727..cd5c1598cdc7 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java @@ -449,9 +449,6 @@ public abstract class SourceChecker extends AbstractTypeProcessor implements Opt // TODO A checker should export itself through a separate interface, and maybe have an interface // for all the methods for which it's safe to override. - /** The message key that will suppress all warnings (it matches any message key). */ - public static final String SUPPRESS_ALL_MESSAGE_KEY = "all"; - /** The SuppressWarnings prefix that will suppress warnings for all checkers. */ public static final String SUPPRESS_ALL_PREFIX = "allcheckers"; @@ -2469,15 +2466,13 @@ protected void warnUnneededSuppressions() { this.elementsWithSuppressedWarnings.clear(); Set prefixes = new HashSet<>(getSuppressWarningsPrefixes()); - Set errorKeys = new HashSet<>(messagesProperties.stringPropertyNames()); for (SourceChecker subChecker : subcheckers) { allElementsWithSuppressedWarnings.addAll(subChecker.elementsWithSuppressedWarnings); subChecker.elementsWithSuppressedWarnings.clear(); prefixes.addAll(subChecker.getSuppressWarningsPrefixes()); - errorKeys.addAll(subChecker.messagesProperties.stringPropertyNames()); subChecker.getVisitor().treesWithSuppressWarnings.clear(); } - warnUnneededSuppressions(allElementsWithSuppressedWarnings, prefixes, errorKeys); + warnUnneededSuppressions(allElementsWithSuppressedWarnings, prefixes); getVisitor().treesWithSuppressWarnings.clear(); } @@ -2490,10 +2485,9 @@ protected void warnUnneededSuppressions() { * @param elementsSuppress elements with a {@code @SuppressWarnings} that actually suppressed a * warning * @param checkerPrefixes the SuppressWarnings prefixes associated with this checker - * @param allErrorKeys all error keys that can be issued by this checker */ protected void warnUnneededSuppressions( - Set elementsSuppress, Set checkerPrefixes, Set allErrorKeys) { + Set elementsSuppress, Set checkerPrefixes) { for (Tree tree : getVisitor().treesWithSuppressWarnings) { Element elt = TreeUtils.elementFromTree(tree); // TODO: This test is too coarse. The fact that this @SuppressWarnings suppressed @@ -2515,10 +2509,14 @@ protected void warnUnneededSuppressions( int colonPos = suppressWarningsString.indexOf(":"); if (colonPos != -1) { String warningPrefix = suppressWarningsString.substring(0, colonPos); - if (checkerPrefixes.contains(warningPrefix) - && !suppressWarningsString.regionMatches( - colonPos, ":unneeded.suppression", 0, ":unneeded.suppression".length())) { - reportUnneededSuppression(tree, suppressWarningsString); + if (checkerPrefixes.contains(warningPrefix)) { + // Test whether the error key is "unneeded.suppression", without creating a String. + boolean isUnneededSuppression = + suppressWarningsString.length() == colonPos + 1 + "unneeded.suppression".length() + && suppressWarningsString.endsWith("unneeded.suppression"); + if (!isUnneededSuppression) { + reportUnneededSuppression(tree, suppressWarningsString); + } } } } @@ -2815,28 +2813,28 @@ private boolean shouldSuppress( for (String currentSuppressWarningsInEffect : suppressWarningsInEffect) { int colonPos = currentSuppressWarningsInEffect.indexOf(':'); - String messageKeyInSuppressWarningsString; + String messageKeyInEffect; if (colonPos == -1) { - // The SuppressWarnings string has no colon, so it is not of the form + // `currentSuppressWarningsInEffect` has no colon, so it is not of the form // prefix:partial-message-key. if (prefixes.contains(currentSuppressWarningsInEffect)) { // The value in the @SuppressWarnings is exactly a prefix. // Suppress the warning unless its message key is "unneeded.suppression". - boolean result = !currentSuppressWarningsInEffect.equals("unneeded.suppression"); + boolean result = !messageKey.equals("unneeded.suppression"); return result; } else if (requirePrefixInWarningSuppressions) { // A prefix is required, but this SuppressWarnings string does not have a - // prefix; check the next SuppressWarnings string. + // prefix. continue; - } else if (currentSuppressWarningsInEffect.equals(SUPPRESS_ALL_MESSAGE_KEY)) { + } else if (currentSuppressWarningsInEffect.equals("all")) { // Prefixes aren't required and the SuppressWarnings string is "all". // Suppress the warning unless its message key is "unneeded.suppression". - boolean result = !currentSuppressWarningsInEffect.equals("unneeded.suppression"); + boolean result = !messageKey.equals("unneeded.suppression"); return result; } - // The currentSuppressWarningsInEffect is not a prefix or a prefix:message-key, so + // The currentSuppressWarningsInEffect is not a checker name, so // it might be a message key. - messageKeyInSuppressWarningsString = currentSuppressWarningsInEffect; + messageKeyInEffect = currentSuppressWarningsInEffect; } else { // The SuppressWarnings string has a colon; that is, it has a prefix. String currentSuppressWarningsPrefix = @@ -2846,12 +2844,11 @@ private boolean shouldSuppress( // this checker. Proceed to the next SuppressWarnings string. continue; } - messageKeyInSuppressWarningsString = - currentSuppressWarningsInEffect.substring(colonPos + 1); + messageKeyInEffect = currentSuppressWarningsInEffect.substring(colonPos + 1); } // Check if the message key in the warning suppression is part of the message key that // the checker is emitting. - if (messageKeyMatches(messageKey, messageKeyInSuppressWarningsString)) { + if (messageKeyMatches(messageKey, messageKeyInEffect)) { return true; } } @@ -2866,16 +2863,15 @@ private boolean shouldSuppress( * * @param messageKey the message key of the error that is being emitted, without any "checker:" * prefix - * @param messageKeyInSuppressWarningsString the message key in a {@code @SuppressWarnings} - * annotation + * @param messageKeyInEffect the message key in a {@code @SuppressWarnings} annotation that is + * currently in effect * @return true if the arguments match */ - protected boolean messageKeyMatches( - String messageKey, String messageKeyInSuppressWarningsString) { - return messageKey.equals(messageKeyInSuppressWarningsString) - || messageKey.startsWith(messageKeyInSuppressWarningsString + ".") - || messageKey.endsWith("." + messageKeyInSuppressWarningsString) - || messageKey.contains("." + messageKeyInSuppressWarningsString + "."); + protected boolean messageKeyMatches(String messageKey, String messageKeyInEffect) { + return messageKey.equals(messageKeyInEffect) + || messageKey.startsWith(messageKeyInEffect + ".") + || messageKey.endsWith("." + messageKeyInEffect) + || messageKey.contains("." + messageKeyInEffect + "."); } /** From ee084d77c0ac313b64be1a03b0c83677c8c20990 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 27 May 2026 18:22:27 -0700 Subject: [PATCH 04/10] Remove unnecessary `@SuppressWarnings` --- .../checkerframework/dataflow/analysis/AbstractAnalysis.java | 1 - .../dataflow/cfg/builder/CFGTranslationPhaseThree.java | 1 - .../main/java/org/checkerframework/javacutil/ElementUtils.java | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AbstractAnalysis.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AbstractAnalysis.java index 4743a3962a23..1cbb5a206d2e 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AbstractAnalysis.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AbstractAnalysis.java @@ -497,7 +497,6 @@ protected TransferResult callTransferFunction( } transferInput.node = node; setCurrentNode(node); - @SuppressWarnings("nullness") // CF bug: "INFERENCE FAILED" TransferResult transferResult = node.accept(transferFunction, transferInput); setCurrentNode(null); if (node instanceof AssignmentNode assignment) { diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseThree.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseThree.java index 1ebc14092052..1d49d92d057c 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseThree.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseThree.java @@ -65,7 +65,6 @@ protected static interface PredecessorHolder { * not allowed to read or modify {@code cfg} after the call to {@code process} any more. * @return the resulting control flow graph */ - @SuppressWarnings("nullness") // TODO: successors public static ControlFlowGraph process(ControlFlowGraph cfg) { Set blocks = cfg.getAllBlocks(); Set removedBlocks = new HashSet<>(); diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/ElementUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/ElementUtils.java index 48f40fa0a79d..8790e65735fb 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/ElementUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/ElementUtils.java @@ -1021,7 +1021,7 @@ public static ElementKind getKindRecordAsClass(Element elt) { * @deprecated use {@link TypeElement#getRecordComponents} */ @Deprecated(forRemoval = true, since = "4.0.0") - @SuppressWarnings({"unchecked", "nullness"}) // because of cast from reflection + @SuppressWarnings("unchecked") public static List getRecordComponents(TypeElement element) { return element.getRecordComponents(); } From c6bc342a5052bae087378fef0b16ccd7d75fdf84 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 27 May 2026 19:24:01 -0700 Subject: [PATCH 05/10] Handle "allcheckers" as well as "all" --- .../framework/source/SourceChecker.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java index cd5c1598cdc7..074fcfa0ef33 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java @@ -2826,8 +2826,9 @@ private boolean shouldSuppress( // A prefix is required, but this SuppressWarnings string does not have a // prefix. continue; - } else if (currentSuppressWarningsInEffect.equals("all")) { - // Prefixes aren't required and the SuppressWarnings string is "all". + } else if (currentSuppressWarningsInEffect.equals("all") + || currentSuppressWarningsInEffect.equals("allcheckers")) { + // Prefixes aren't required and the SuppressWarnings string is "all" or "allcheckers". // Suppress the warning unless its message key is "unneeded.suppression". boolean result = !messageKey.equals("unneeded.suppression"); return result; @@ -2839,9 +2840,10 @@ private boolean shouldSuppress( // The SuppressWarnings string has a colon; that is, it has a prefix. String currentSuppressWarningsPrefix = currentSuppressWarningsInEffect.substring(0, colonPos); - if (!prefixes.contains(currentSuppressWarningsPrefix)) { - // The prefix of this SuppressWarnings string is a not a prefix supported by - // this checker. Proceed to the next SuppressWarnings string. + if (currentSuppressWarningsPrefix.equals("allcheckers") + || !prefixes.contains(currentSuppressWarningsPrefix)) { + // The prefix of this SuppressWarnings string is "allcheckers" or is not a prefix + // supported by this checker. Proceed to the next SuppressWarnings string. continue; } messageKeyInEffect = currentSuppressWarningsInEffect.substring(colonPos + 1); From 83391f6a00a36045e0ab5e65b18a9873096c4a07 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 27 May 2026 20:19:46 -0700 Subject: [PATCH 06/10] Update jtreg tests --- checker/jtreg/index/UnneededSuppressionsTest.out | 3 +-- checker/jtreg/nullness/issue2318/RequireCheckerPrefix.1.out | 5 ++++- checker/jtreg/nullness/issue2318/RequireCheckerPrefix.2.out | 5 ++++- .../checkerframework/framework/source/SourceChecker.java | 6 ++++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/checker/jtreg/index/UnneededSuppressionsTest.out b/checker/jtreg/index/UnneededSuppressionsTest.out index a26e0d3e5475..4b432deba062 100644 --- a/checker/jtreg/index/UnneededSuppressionsTest.out +++ b/checker/jtreg/index/UnneededSuppressionsTest.out @@ -1,5 +1,4 @@ UnneededSuppressionsTest.java:22:3: compiler.warn.proc.messager: [unneeded.suppression] warning suppression "lowerbound" is not used by IndexChecker UnneededSuppressionsTest.java:24:5: compiler.warn.proc.messager: [unneeded.suppression] warning suppression "upperbound:assignment" is not used by IndexChecker UnneededSuppressionsTest.java:39:3: compiler.warn.proc.messager: [unneeded.suppression] warning suppression "index:foo.bar.baz" is not used by IndexChecker -UnneededSuppressionsTest.java:42:3: compiler.warn.proc.messager: [unneeded.suppression] warning suppression "allcheckers:purity.not.deterministic.call" is not used by IndexChecker -4 warnings +3 warnings diff --git a/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.1.out b/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.1.out index 03a059588cb5..87e2ac2ce3c7 100644 --- a/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.1.out +++ b/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.1.out @@ -1,10 +1,13 @@ RequireCheckerPrefix.java:19:25: compiler.err.proc.messager: [nullness:assignment] incompatible types in assignment. found : @Initialized @Nullable Object required: @UnknownInitialization @NonNull Object +RequireCheckerPrefix.java:21:25: compiler.err.proc.messager: [nullness:assignment] incompatible types in assignment. +found : @Initialized @Nullable Object +required: @UnknownInitialization @NonNull Object RequireCheckerPrefix.java:24:25: compiler.err.proc.messager: [nullness:assignment] incompatible types in assignment. found : @Initialized @Nullable Object required: @UnknownInitialization @NonNull Object RequireCheckerPrefix.java:27:25: compiler.err.proc.messager: [nullness:assignment] incompatible types in assignment. found : @Initialized @Nullable Object required: @UnknownInitialization @NonNull Object -3 errors +4 errors diff --git a/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.2.out b/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.2.out index f72ddfe2a3ee..eab01bacf1a8 100644 --- a/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.2.out +++ b/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.2.out @@ -1,4 +1,7 @@ RequireCheckerPrefix.java:19:25: compiler.err.proc.messager: [assignment] incompatible types in assignment. found : @Initialized @Nullable Object required: @UnknownInitialization @NonNull Object -1 error +RequireCheckerPrefix.java:21:25: compiler.err.proc.messager: [assignment] incompatible types in assignment. +found : @Initialized @Nullable Object +required: @UnknownInitialization @NonNull Object +2 errors diff --git a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java index 074fcfa0ef33..b57d4f3ee1e3 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java @@ -2503,13 +2503,15 @@ protected void warnUnneededSuppressions( && warnUnneededSuppressionsExceptions.matcher(suppressWarningsString).find(0)) { continue; } - if (checkerPrefixes.contains(suppressWarningsString)) { + if (suppressWarningsString.equals("allcheckers")) { + // Do nothing. + } else if (checkerPrefixes.contains(suppressWarningsString)) { reportUnneededSuppression(tree, suppressWarningsString); } else { int colonPos = suppressWarningsString.indexOf(":"); if (colonPos != -1) { String warningPrefix = suppressWarningsString.substring(0, colonPos); - if (checkerPrefixes.contains(warningPrefix)) { + if (!warningPrefix.equals("allcheckers") && checkerPrefixes.contains(warningPrefix)) { // Test whether the error key is "unneeded.suppression", without creating a String. boolean isUnneededSuppression = suppressWarningsString.length() == colonPos + 1 + "unneeded.suppression".length() From b545b1d431c12849f69914158da116d17e5f4310 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 27 May 2026 22:19:33 -0700 Subject: [PATCH 07/10] Undo undesirable change --- .../framework/source/SourceChecker.java | 8 +++----- framework/tests/all-systems/PuritySubstring.java | 13 +++++++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 framework/tests/all-systems/PuritySubstring.java diff --git a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java index b57d4f3ee1e3..0c4c62b5aa6b 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java @@ -2607,7 +2607,6 @@ private boolean shouldSuppressWarnings(@Nullable Object src, String errKey) { * otherwise */ public boolean shouldSuppressWarnings(Tree tree, String errKey) { - Collection prefixes = getSuppressWarningsPrefixes(); if (prefixes.isEmpty() || (prefixes.contains(SUPPRESS_ALL_PREFIX) && prefixes.size() == 1)) { throw new BugInCF( @@ -2842,10 +2841,9 @@ private boolean shouldSuppress( // The SuppressWarnings string has a colon; that is, it has a prefix. String currentSuppressWarningsPrefix = currentSuppressWarningsInEffect.substring(0, colonPos); - if (currentSuppressWarningsPrefix.equals("allcheckers") - || !prefixes.contains(currentSuppressWarningsPrefix)) { - // The prefix of this SuppressWarnings string is "allcheckers" or is not a prefix - // supported by this checker. Proceed to the next SuppressWarnings string. + if (!prefixes.contains(currentSuppressWarningsPrefix)) { + // The prefix of this SuppressWarnings string is is not a prefix supported + // by this checker. Proceed to the next SuppressWarnings string. continue; } messageKeyInEffect = currentSuppressWarningsInEffect.substring(colonPos + 1); diff --git a/framework/tests/all-systems/PuritySubstring.java b/framework/tests/all-systems/PuritySubstring.java new file mode 100644 index 000000000000..7e5237339a90 --- /dev/null +++ b/framework/tests/all-systems/PuritySubstring.java @@ -0,0 +1,13 @@ +import org.checkerframework.dataflow.qual.SideEffectFree; + +public class PuritySubstring { + + @SuppressWarnings({"allcheckers:purity", "lock"}) // local StringBuilder + @SideEffectFree + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("hello"); + return sb.toString(); + } +} From 27b40dfc033668f10d13cdc05e472dad7e72eaca Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Wed, 27 May 2026 23:05:26 -0700 Subject: [PATCH 08/10] Adjust jtreg goal --- checker/jtreg/nullness/issue2318/RequireCheckerPrefix.1.out | 5 +---- checker/jtreg/nullness/issue2318/RequireCheckerPrefix.2.out | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.1.out b/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.1.out index 87e2ac2ce3c7..03a059588cb5 100644 --- a/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.1.out +++ b/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.1.out @@ -1,13 +1,10 @@ RequireCheckerPrefix.java:19:25: compiler.err.proc.messager: [nullness:assignment] incompatible types in assignment. found : @Initialized @Nullable Object required: @UnknownInitialization @NonNull Object -RequireCheckerPrefix.java:21:25: compiler.err.proc.messager: [nullness:assignment] incompatible types in assignment. -found : @Initialized @Nullable Object -required: @UnknownInitialization @NonNull Object RequireCheckerPrefix.java:24:25: compiler.err.proc.messager: [nullness:assignment] incompatible types in assignment. found : @Initialized @Nullable Object required: @UnknownInitialization @NonNull Object RequireCheckerPrefix.java:27:25: compiler.err.proc.messager: [nullness:assignment] incompatible types in assignment. found : @Initialized @Nullable Object required: @UnknownInitialization @NonNull Object -4 errors +3 errors diff --git a/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.2.out b/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.2.out index eab01bacf1a8..f72ddfe2a3ee 100644 --- a/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.2.out +++ b/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.2.out @@ -1,7 +1,4 @@ RequireCheckerPrefix.java:19:25: compiler.err.proc.messager: [assignment] incompatible types in assignment. found : @Initialized @Nullable Object required: @UnknownInitialization @NonNull Object -RequireCheckerPrefix.java:21:25: compiler.err.proc.messager: [assignment] incompatible types in assignment. -found : @Initialized @Nullable Object -required: @UnknownInitialization @NonNull Object -2 errors +1 error From b7e2316e35f978631e86b1b838e01414c9b90c36 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 28 May 2026 08:15:08 -0700 Subject: [PATCH 09/10] Grammar --- docs/developer/new-contributor-projects.html-old | 2 +- .../org/checkerframework/framework/source/SourceChecker.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developer/new-contributor-projects.html-old b/docs/developer/new-contributor-projects.html-old index 97e49338d671..392212247b05 100644 --- a/docs/developer/new-contributor-projects.html-old +++ b/docs/developer/new-contributor-projects.html-old @@ -671,7 +671,7 @@ The goal of this project is to build a tool to check uses of Optional and run it
  • Is it possible to build a tool that enforces good style in using Optional?
  • -
  • How much effort is is for programmers to use such a tool? +
  • How much effort is it for programmers to use such a tool?
  • Does real-world code obey the rules about use of Optional, or not?
  • diff --git a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java index 0c4c62b5aa6b..0c916c813f30 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java @@ -2842,7 +2842,7 @@ private boolean shouldSuppress( String currentSuppressWarningsPrefix = currentSuppressWarningsInEffect.substring(0, colonPos); if (!prefixes.contains(currentSuppressWarningsPrefix)) { - // The prefix of this SuppressWarnings string is is not a prefix supported + // The prefix of this SuppressWarnings string is not a prefix supported // by this checker. Proceed to the next SuppressWarnings string. continue; } From 17a068138cedc5cc2ec776bb3037f0e7d811d289 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Thu, 28 May 2026 11:24:32 -0700 Subject: [PATCH 10/10] Documentation --- docs/manual/introduction.tex | 4 ++- docs/manual/warnings.tex | 28 ++++++++++--------- .../framework/source/SourceChecker.java | 13 +++++++-- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/docs/manual/introduction.tex b/docs/manual/introduction.tex index 026b395ea95a..926f15d87438 100644 --- a/docs/manual/introduction.tex +++ b/docs/manual/introduction.tex @@ -707,9 +707,11 @@ \item \<-AshowSuppressWarningsStrings> With each warning, show all possible strings to suppress that warning. \item \<-AwarnUnneededSuppressions> - Issue a warning if a \<@SuppressWarnings> did not suppress a warning + Issue a \ warning for each \<@SuppressWarnings> + that did not suppress a warning issued by the checker. This only warns about \<@SuppressWarnings> strings that contain a checker name + other than \<"allcheckers"> (for syntax, Section~\ref{suppresswarnings-annotation-syntax}). The \<-ArequirePrefixInWarningSuppressions> command-line argument ensures that all \<@SuppressWarnings> strings contain a checker name. diff --git a/docs/manual/warnings.tex b/docs/manual/warnings.tex index 5fda87f0a5fd..d1d4a4208bb0 100644 --- a/docs/manual/warnings.tex +++ b/docs/manual/warnings.tex @@ -73,8 +73,9 @@ \noindent The rest of this chapter explains these mechanisms in turn. -You can use the \code{-AwarnUnneededSuppressions} command-line option to issue a -warning if a \code{@SuppressWarnings} did not suppress any warnings issued by the current checker. +You can use the \code{-AwarnUnneededSuppressions} command-line option to +issue a warning for each \code{@SuppressWarnings} that does not suppress +any warnings issued by the current checker. \sectionAndLabel{\code{@SuppressWarnings} annotation}{suppresswarnings-annotation} @@ -150,11 +151,9 @@ The checkername \<"allcheckers"> means all checkers. Using this is not recommended, except for messages common to all checkers such as -purity-related messages when using \<-AcheckPurityAnnotations>. If you use -\<"allcheckers">, you run some checker that does not issue any warnings, -and you supply the \<-AwarnUnneededSuppressions> command-line argument, then -the Checker Framework will issue an \ -warning. +purity-related messages when using \<-AcheckPurityAnnotations>. +The Checker Framework never issues an \ warning +about a \<@SuppressWarnings> whose checkername is \<"allcheckers">. The special messagekey ``\'' means to suppress all warnings. @@ -351,12 +350,15 @@ \end{Verbatim} \noindent -Please report false positive warnings, then reference them in your warning suppressions. -This permits the Checker Framework maintainers to know about the -problem, it helps them with prioritization (by knowing how often in your -codebase a particular issue arises), and it enables you to know when an -issue has been fixed (though the \<-AwarnUnneededSuppressions> command-line -option also serves the latter purpose). +When you encounter a false positive warning, please upvote the +corresponding issue on the +\href{https://github.com/typetools/checker-framework/issues}{issue +tracker}, or create a new issue. This helps the Checker Framework +maintainers to prioritize their work. +If you reference the issue URL in your warning suppression, then you can +later follow the URL to check whether an issue has been fixed (though the +\<-AwarnUnneededSuppressions> command-line option also serves this +purpose). \sectionAndLabel{\code{@AssumeAssertion} string in an \ message}{assumeassertion} diff --git a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java index 0c916c813f30..77d538edda3c 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java @@ -2520,6 +2520,12 @@ protected void warnUnneededSuppressions( reportUnneededSuppression(tree, suppressWarningsString); } } + } else { + // The annotation might be of the form `@SuppressWarnings("generic.argument")`. + // However, per the manual, "This only warns about @SuppressWarnings strings that + // contain a checker name". Thus, `@SuppressWarnings("generic.argument")` is treated + // the same as `@SuppressWarnings("allcheckers:generic.argument")` in that no warning is + // ever issued about it. } } } @@ -2778,8 +2784,8 @@ public boolean shouldSuppressWarnings(@Nullable Element elt, String errKey) { * with a message key that contains "generic.argument". * * - * {@code "allcheckers"} is a prefix that suppresses a warning from any checker. {@code "all"} is - * a partial-message-key that suppresses a warning with any message key. + * {@code "allcheckers"} is a prefix/checkername that suppresses a warning from any checker. + * {@code "all"} is a partial-message-key that suppresses a warning with any message key. * *

    If the {@code -ArequirePrefixInWarningSuppressions} command-line argument was supplied, then * {@code "partial-message-key"} has no effect; {@code "prefix"} and {@code @@ -2829,7 +2835,8 @@ private boolean shouldSuppress( continue; } else if (currentSuppressWarningsInEffect.equals("all") || currentSuppressWarningsInEffect.equals("allcheckers")) { - // Prefixes aren't required and the SuppressWarnings string is "all" or "allcheckers". + // Prefixes aren't required and the whole SuppressWarnings string is + // the messagekey "all" or the checkername "allcheckers". // Suppress the warning unless its message key is "unneeded.suppression". boolean result = !messageKey.equals("unneeded.suppression"); return result;