From 2a48175d20c0df40911e31f778613a7f67fe38ff Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Mon, 4 May 2026 19:15:29 -0400 Subject: [PATCH 01/13] Add SuppressWarnings classes with AnnotatedFor methods --- .../annotatedfornullness/AnnotatedForNullness.java | 8 ++++++++ .../annotatedfor/AnnotatedForTest.java | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/checker/tests/nulless-conservative-defaults/annotatedfornullness/AnnotatedForNullness.java b/checker/tests/nulless-conservative-defaults/annotatedfornullness/AnnotatedForNullness.java index b0c4472ec506..86534cb02189 100644 --- a/checker/tests/nulless-conservative-defaults/annotatedfornullness/AnnotatedForNullness.java +++ b/checker/tests/nulless-conservative-defaults/annotatedfornullness/AnnotatedForNullness.java @@ -73,4 +73,12 @@ void bar() { annotatedForNullness(initializedField); annotatedForNullnessAndInitialization(initializedField); } + + @SuppressWarnings("nullness") + class SuppressWarningsClassWithAnnotatedForMethod { + @AnnotatedFor("nullness") + @NonNull Object m() { + return null; + } + } } diff --git a/framework/tests/conservative-defaults/annotatedfor/AnnotatedForTest.java b/framework/tests/conservative-defaults/annotatedfor/AnnotatedForTest.java index b73ea47242b4..967ca6dca4fe 100644 --- a/framework/tests/conservative-defaults/annotatedfor/AnnotatedForTest.java +++ b/framework/tests/conservative-defaults/annotatedfor/AnnotatedForTest.java @@ -247,4 +247,14 @@ static class staticAnnotatedAndWarningsSuppressedClass { so4 = staticUnannotatedMethod(so1); } } + + // Class-level @SuppressWarnings("subtyping") should override a method-level + // @AnnotatedFor("subtyping") + @SuppressWarnings("subtyping") + class SuppressWarningsClassWithAnnotatedForMethod { + @AnnotatedFor("subtyping") + @SubQual Object m(@SuperQual Object p) { + return p; + } + } } From 66e664f497b198e9b1dcc430ea23cdd2aa1f4368 Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Mon, 4 May 2026 19:28:19 -0400 Subject: [PATCH 02/13] Remove redundant check for AnnotatedFor in SourceChecker --- .../org/checkerframework/framework/source/SourceChecker.java | 5 ----- 1 file changed, 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 c3133c10c211..1263e01f931b 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java @@ -2875,11 +2875,6 @@ public boolean shouldSuppressWarnings(Element elt, String errKey) { return true; } } - if (isAnnotatedForThisCheckerOrUpstreamChecker(elt)) { - // Return false immediately. Do NOT check for AnnotatedFor in the - // enclosing elements, because they may not have an @AnnotatedFor. - return false; - } } return false; } From 90c6889df9af476634622fbfdfeeda95be11a55a Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Mon, 4 May 2026 22:31:29 -0400 Subject: [PATCH 03/13] Remove redundant check for AnnotatedFor in SourceChecker --- .../annotatedfornullness/AnnotatedForNullness.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/checker/tests/nulless-conservative-defaults/annotatedfornullness/AnnotatedForNullness.java b/checker/tests/nulless-conservative-defaults/annotatedfornullness/AnnotatedForNullness.java index 86534cb02189..4f7b9aa7da62 100644 --- a/checker/tests/nulless-conservative-defaults/annotatedfornullness/AnnotatedForNullness.java +++ b/checker/tests/nulless-conservative-defaults/annotatedfornullness/AnnotatedForNullness.java @@ -74,6 +74,8 @@ void bar() { annotatedForNullnessAndInitialization(initializedField); } + // Class-level @SuppressWarnings("nullness") should override a method-level + // @AnnotatedFor("nullness") @SuppressWarnings("nullness") class SuppressWarningsClassWithAnnotatedForMethod { @AnnotatedFor("nullness") From 87b149f430341e5f93d7712dda5787c108f331b9 Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Tue, 5 May 2026 15:26:33 -0400 Subject: [PATCH 04/13] Apply suggestions from code review Co-authored-by: Alex Cook <43047600+thisisalexandercook@users.noreply.github.com> --- .../annotatedfornullness/AnnotatedForNullness.java | 4 ++-- .../conservative-defaults/annotatedfor/AnnotatedForTest.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/checker/tests/nulless-conservative-defaults/annotatedfornullness/AnnotatedForNullness.java b/checker/tests/nulless-conservative-defaults/annotatedfornullness/AnnotatedForNullness.java index 4f7b9aa7da62..bd933cf471e4 100644 --- a/checker/tests/nulless-conservative-defaults/annotatedfornullness/AnnotatedForNullness.java +++ b/checker/tests/nulless-conservative-defaults/annotatedfornullness/AnnotatedForNullness.java @@ -74,8 +74,8 @@ void bar() { annotatedForNullnessAndInitialization(initializedField); } - // Class-level @SuppressWarnings("nullness") should override a method-level - // @AnnotatedFor("nullness") + // @SuppressWarnings("nullness") should suppress all nullness diagnostics within the annotated declaration's scope, + // including those in nested @AnnotatedFor("nullness") scopes. @SuppressWarnings("nullness") class SuppressWarningsClassWithAnnotatedForMethod { @AnnotatedFor("nullness") diff --git a/framework/tests/conservative-defaults/annotatedfor/AnnotatedForTest.java b/framework/tests/conservative-defaults/annotatedfor/AnnotatedForTest.java index 967ca6dca4fe..bc655da0ecc3 100644 --- a/framework/tests/conservative-defaults/annotatedfor/AnnotatedForTest.java +++ b/framework/tests/conservative-defaults/annotatedfor/AnnotatedForTest.java @@ -248,8 +248,8 @@ static class staticAnnotatedAndWarningsSuppressedClass { } } - // Class-level @SuppressWarnings("subtyping") should override a method-level - // @AnnotatedFor("subtyping") + // @SuppressWarnings("subtyping") should suppress all subtying diagnostics within the annotated declaration's scope, + // including those in nested @AnnotatedFor("subtyping") scopes. @SuppressWarnings("subtyping") class SuppressWarningsClassWithAnnotatedForMethod { @AnnotatedFor("subtyping") From 19dfafc1254fe44b4b50b50b920d769981410ad8 Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Tue, 5 May 2026 17:23:18 -0400 Subject: [PATCH 05/13] Apply formatter --- .../annotatedfornullness/AnnotatedForNullness.java | 4 ++-- .../conservative-defaults/annotatedfor/AnnotatedForTest.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/checker/tests/nulless-conservative-defaults/annotatedfornullness/AnnotatedForNullness.java b/checker/tests/nulless-conservative-defaults/annotatedfornullness/AnnotatedForNullness.java index bd933cf471e4..81f2dcafb2e4 100644 --- a/checker/tests/nulless-conservative-defaults/annotatedfornullness/AnnotatedForNullness.java +++ b/checker/tests/nulless-conservative-defaults/annotatedfornullness/AnnotatedForNullness.java @@ -74,8 +74,8 @@ void bar() { annotatedForNullnessAndInitialization(initializedField); } - // @SuppressWarnings("nullness") should suppress all nullness diagnostics within the annotated declaration's scope, - // including those in nested @AnnotatedFor("nullness") scopes. + // @SuppressWarnings("nullness") should suppress all nullness diagnostics within the annotated + // declaration's scope, including those in nested @AnnotatedFor("nullness") scopes. @SuppressWarnings("nullness") class SuppressWarningsClassWithAnnotatedForMethod { @AnnotatedFor("nullness") diff --git a/framework/tests/conservative-defaults/annotatedfor/AnnotatedForTest.java b/framework/tests/conservative-defaults/annotatedfor/AnnotatedForTest.java index bc655da0ecc3..c7885f64cc29 100644 --- a/framework/tests/conservative-defaults/annotatedfor/AnnotatedForTest.java +++ b/framework/tests/conservative-defaults/annotatedfor/AnnotatedForTest.java @@ -248,8 +248,8 @@ static class staticAnnotatedAndWarningsSuppressedClass { } } - // @SuppressWarnings("subtyping") should suppress all subtying diagnostics within the annotated declaration's scope, - // including those in nested @AnnotatedFor("subtyping") scopes. + // @SuppressWarnings("subtyping") should suppress all subtying diagnostics within the annotated + // declaration's scope, including those in nested @AnnotatedFor("subtyping") scopes. @SuppressWarnings("subtyping") class SuppressWarningsClassWithAnnotatedForMethod { @AnnotatedFor("subtyping") From ca073e683fa1fff2d915684a111e74bf5d865a3e Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Mon, 11 May 2026 11:46:44 -0400 Subject: [PATCH 06/13] Enhance documentation and logic for @AnnotatedFor and @SuppressWarnings handling --- docs/manual/annotating-libraries.tex | 8 ++- docs/manual/warnings.tex | 7 +- .../framework/source/SourceChecker.java | 68 ++++++++++++------- .../annotatedfor/AnnotatedForTest.java | 2 +- 4 files changed, 55 insertions(+), 30 deletions(-) diff --git a/docs/manual/annotating-libraries.tex b/docs/manual/annotating-libraries.tex index db580a20531c..aa0ba05f7645 100644 --- a/docs/manual/annotating-libraries.tex +++ b/docs/manual/annotating-libraries.tex @@ -384,8 +384,12 @@ conservative defaults (see Section~\ref{defaults-classfile}) for any type use with no explicit user-written annotation, \emph{and} the checker issues no warnings. -The command-line argument \code{-AonlyAnnotatedFor} can be used to suppress errors and warnings outside of the scope of an \<@AnnotatedFor> annotation, -but does not change the default qualifiers for source code (See Section~\ref{aonlyannotatedfor}). +Warnings in code with a relevant \<@AnnotatedFor> annotation can still be suppressed by +an in-scope \<@SuppressWarnings> annotation. +The command-line argument \code{-AonlyAnnotatedFor} can be used to suppress +errors and warnings outside of the scope of an \<@AnnotatedFor> annotation, +but does not change the default qualifiers for source code (see +Section~\ref{aonlyannotatedfor}). \end{sloppypar} The \refqualclass{framework/qual}{AnnotatedFor} annotation, written on a diff --git a/docs/manual/warnings.tex b/docs/manual/warnings.tex index 5fbfacc8c911..773b066750d4 100644 --- a/docs/manual/warnings.tex +++ b/docs/manual/warnings.tex @@ -694,8 +694,11 @@ \sectionAndLabel{\code{-AonlyAnnotatedFor} command-line option}{aonlyannotatedfor} -You can suppress all errors and warnings for code outside of a corresponding \code{@AnnotatedFor} by applying this command-line option. -Note that the \code{@AnnotatedFor} annotation must include the checker's name to enable warnings from that checker. +You can suppress all errors and warnings for code outside of a corresponding +\code{@AnnotatedFor} by applying this command-line option. +Note that the \code{@AnnotatedFor} annotation must include the checker's name to +enable warnings from that checker, except for warnings suppressed by another +mechanism such as \code{@SuppressWarnings}. For example, use \code{@AnnotatedFor("nullness")} for the Nullness Checker. This flag only suppresses warnings, compared to \code{-AuseConservativeDefaultsForUncheckedCode=source}, which also applies conservative defaults for code outside the scope of an \code{@AnnotatedFor} annotation. 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 1263e01f931b..c49a58670d45 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java @@ -2756,6 +2756,12 @@ public boolean shouldSuppressWarnings(Tree tree, String errKey) { * otherwise */ public boolean shouldSuppressWarnings(TreePath path, String errKey) { + if (shouldSuppress(getSuppressWarningsStringsFromOption(), errKey)) { + return true; + } + + boolean foundAnnotatedFor = false; + // iterate through the path; continue until path contains no declarations for (TreePath declPath = TreePathUtil.enclosingDeclarationPath(path); declPath != null; @@ -2764,41 +2770,36 @@ public boolean shouldSuppressWarnings(TreePath path, String errKey) { if (decl instanceof VariableTree) { Element elt = TreeUtils.elementFromDeclaration((VariableTree) decl); - if (shouldSuppressWarnings(elt, errKey)) { + if (shouldSuppressWarningsOnElement(elt, errKey)) { return true; } } else if (decl instanceof MethodTree) { Element elt = TreeUtils.elementFromDeclaration((MethodTree) decl); - if (shouldSuppressWarnings(elt, errKey)) { + if (shouldSuppressWarningsOnElement(elt, errKey)) { return true; } - if (isAnnotatedForThisCheckerOrUpstreamChecker(elt)) { - // Return false immediately. Do NOT check for AnnotatedFor in the enclosing - // elements as the closest AnnotatedFor is already found. - return false; + if (!foundAnnotatedFor && isAnnotatedForThisCheckerOrUpstreamChecker(elt)) { + foundAnnotatedFor = true; } } else if (TreeUtils.classTreeKinds().contains(decl.getKind())) { // A class tree Element elt = TreeUtils.elementFromDeclaration((ClassTree) decl); - if (shouldSuppressWarnings(elt, errKey)) { + if (shouldSuppressWarningsOnElement(elt, errKey)) { return true; } - if (isAnnotatedForThisCheckerOrUpstreamChecker(elt)) { - // Return false immediately. Do NOT check for AnnotatedFor in the enclosing - // elements as the closest AnnotatedFor is already found. - return false; + if (!foundAnnotatedFor && isAnnotatedForThisCheckerOrUpstreamChecker(elt)) { + foundAnnotatedFor = true; } Element packageElement = elt.getEnclosingElement(); if (packageElement != null && packageElement.getKind() == ElementKind.PACKAGE) { - if (shouldSuppressWarnings(packageElement, errKey)) { + if (shouldSuppressWarningsOnElement(packageElement, errKey)) { return true; } - if (isAnnotatedForThisCheckerOrUpstreamChecker(packageElement)) { - // Return false immediately. Do NOT check for AnnotatedFor in the enclosing - // elements as the closest AnnotatedFor is already found. - return false; + if (!foundAnnotatedFor + && isAnnotatedForThisCheckerOrUpstreamChecker(packageElement)) { + foundAnnotatedFor = true; } } } else { @@ -2806,7 +2807,9 @@ public boolean shouldSuppressWarnings(TreePath path, String errKey) { } } - if (useConservativeDefaultsSource || onlyAnnotatedFor) { + if (foundAnnotatedFor) { + return false; + } else if (useConservativeDefaultsSource || onlyAnnotatedFor) { // If we got this far without hitting an @AnnotatedFor and returning // false, we DO suppress the warning. return true; @@ -2865,15 +2868,30 @@ public boolean shouldSuppressWarnings(Element elt, String errKey) { } for (Element currElt = elt; currElt != null; currElt = currElt.getEnclosingElement()) { - SuppressWarnings suppressWarningsAnno = currElt.getAnnotation(SuppressWarnings.class); - if (suppressWarningsAnno != null) { - String[] suppressWarningsStrings = suppressWarningsAnno.value(); - if (shouldSuppress(suppressWarningsStrings, errKey)) { - if (warnUnneededSuppressions) { - elementsWithSuppressedWarnings.add(currElt); - } - return true; + if (shouldSuppressWarningsOnElement(currElt, errKey)) { + return true; + } + } + return false; + } + + /** + * Returns true if the given element has a {@code @SuppressWarnings} annotation that suppresses + * the given error key. + * + * @param elt the element whose annotations to check + * @param errKey the error key the checker is emitting + * @return true if {@code elt} has an corresponding {@code @SuppressWarnings} annotation + */ + private boolean shouldSuppressWarningsOnElement(Element elt, String errKey) { + SuppressWarnings suppressWarningsAnno = elt.getAnnotation(SuppressWarnings.class); + if (suppressWarningsAnno != null) { + String[] suppressWarningsStrings = suppressWarningsAnno.value(); + if (shouldSuppress(suppressWarningsStrings, errKey)) { + if (warnUnneededSuppressions) { + elementsWithSuppressedWarnings.add(elt); } + return true; } } return false; diff --git a/framework/tests/conservative-defaults/annotatedfor/AnnotatedForTest.java b/framework/tests/conservative-defaults/annotatedfor/AnnotatedForTest.java index c7885f64cc29..b44e03eb3650 100644 --- a/framework/tests/conservative-defaults/annotatedfor/AnnotatedForTest.java +++ b/framework/tests/conservative-defaults/annotatedfor/AnnotatedForTest.java @@ -248,7 +248,7 @@ static class staticAnnotatedAndWarningsSuppressedClass { } } - // @SuppressWarnings("subtyping") should suppress all subtying diagnostics within the annotated + // @SuppressWarnings("subtyping") should suppress all subtyping diagnostics within the annotated // declaration's scope, including those in nested @AnnotatedFor("subtyping") scopes. @SuppressWarnings("subtyping") class SuppressWarningsClassWithAnnotatedForMethod { From 5963c8e8580027a88c2406a2fe2e131c5c534969 Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Fri, 19 Jun 2026 13:27:30 -0400 Subject: [PATCH 07/13] Clarify AnnotatedFor warning suppression docs --- docs/manual/annotating-libraries.tex | 4 ++-- docs/manual/warnings.tex | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/manual/annotating-libraries.tex b/docs/manual/annotating-libraries.tex index aa0ba05f7645..ca068587c82a 100644 --- a/docs/manual/annotating-libraries.tex +++ b/docs/manual/annotating-libraries.tex @@ -384,12 +384,12 @@ conservative defaults (see Section~\ref{defaults-classfile}) for any type use with no explicit user-written annotation, \emph{and} the checker issues no warnings. -Warnings in code with a relevant \<@AnnotatedFor> annotation can still be suppressed by -an in-scope \<@SuppressWarnings> annotation. The command-line argument \code{-AonlyAnnotatedFor} can be used to suppress errors and warnings outside of the scope of an \<@AnnotatedFor> annotation, but does not change the default qualifiers for source code (see Section~\ref{aonlyannotatedfor}). +In code with a relevant \<@AnnotatedFor> annotation, warnings can be suppressed +by an in-scope \<@SuppressWarnings> annotation. \end{sloppypar} The \refqualclass{framework/qual}{AnnotatedFor} annotation, written on a diff --git a/docs/manual/warnings.tex b/docs/manual/warnings.tex index 773b066750d4..87b4ada46f9c 100644 --- a/docs/manual/warnings.tex +++ b/docs/manual/warnings.tex @@ -697,8 +697,9 @@ You can suppress all errors and warnings for code outside of a corresponding \code{@AnnotatedFor} by applying this command-line option. Note that the \code{@AnnotatedFor} annotation must include the checker's name to -enable warnings from that checker, except for warnings suppressed by another -mechanism such as \code{@SuppressWarnings}. +enable warnings from that checker. +An explicit \code{@SuppressWarnings} annotation can suppress warnings in +\code{@AnnotatedFor} code. For example, use \code{@AnnotatedFor("nullness")} for the Nullness Checker. This flag only suppresses warnings, compared to \code{-AuseConservativeDefaultsForUncheckedCode=source}, which also applies conservative defaults for code outside the scope of an \code{@AnnotatedFor} annotation. From e747ab5840e1c762400efae55c10288fe53f6b58 Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Fri, 19 Jun 2026 13:51:21 -0400 Subject: [PATCH 08/13] Refine suppression lookup for AnnotatedFor --- .../framework/source/SourceChecker.java | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 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 7851a7e5dff2..3cb1674f7b87 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java @@ -2740,21 +2740,6 @@ private boolean shouldSuppressWarnings(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( - "Checker must provide a SuppressWarnings prefix." - + " SourceChecker#getSuppressWarningsPrefixes was not overridden" - + " correctly."); - } - - if (shouldSuppress(getSuppressWarningsStringsFromOption(), errKey)) { - // If the error key matches a warning string in the -AsuppressWarnings, then suppress - // the warning. - return true; - } - assert this.currentRoot != null : "this.currentRoot == null"; TreePath path = getTreePathCacher().getPath(currentRoot, tree); @@ -2766,6 +2751,11 @@ public boolean shouldSuppressWarnings(Tree tree, String errKey) { * Returns true if the path is within the scope of a @SuppressWarnings annotation, one of whose * values suppresses the checker's warning. * + *

This overload also accounts for source-position-based suppression from unchecked code: if + * no matching {@code @SuppressWarnings} is found, then warnings outside a relevant {@link + * AnnotatedFor} scope are suppressed when {@code + * -AuseConservativeDefaultsForUncheckedCode=source} or {@code -AonlyAnnotatedFor} is in effect. + * * @param path the TreePath that might be a source of, or related to, a warning * @param errKey the error key the checker is emitting * @return true if no warning should be emitted for the given path because it is contained by a @@ -2773,6 +2763,15 @@ public boolean shouldSuppressWarnings(Tree tree, String errKey) { * otherwise */ public boolean shouldSuppressWarnings(TreePath path, String errKey) { + Collection prefixes = getSuppressWarningsPrefixes(); + if (prefixes.isEmpty() + || (prefixes.contains(SUPPRESS_ALL_PREFIX) && prefixes.size() == 1)) { + throw new BugInCF( + "Checker must provide a SuppressWarnings prefix." + + " SourceChecker#getSuppressWarningsPrefixes was not overridden" + + " correctly."); + } + if (shouldSuppress(getSuppressWarningsStringsFromOption(), errKey)) { return true; } @@ -2787,12 +2786,12 @@ public boolean shouldSuppressWarnings(TreePath path, String errKey) { if (decl instanceof VariableTree) { Element elt = TreeUtils.elementFromDeclaration((VariableTree) decl); - if (shouldSuppressWarningsOnElement(elt, errKey)) { + if (hasSuppressWarningsAnnotationForErrorKey(elt, errKey)) { return true; } } else if (decl instanceof MethodTree) { Element elt = TreeUtils.elementFromDeclaration((MethodTree) decl); - if (shouldSuppressWarningsOnElement(elt, errKey)) { + if (hasSuppressWarningsAnnotationForErrorKey(elt, errKey)) { return true; } @@ -2802,7 +2801,7 @@ public boolean shouldSuppressWarnings(TreePath path, String errKey) { } else if (TreeUtils.classTreeKinds().contains(decl.getKind())) { // A class tree Element elt = TreeUtils.elementFromDeclaration((ClassTree) decl); - if (shouldSuppressWarningsOnElement(elt, errKey)) { + if (hasSuppressWarningsAnnotationForErrorKey(elt, errKey)) { return true; } @@ -2811,7 +2810,7 @@ public boolean shouldSuppressWarnings(TreePath path, String errKey) { } Element packageElement = elt.getEnclosingElement(); if (packageElement != null && packageElement.getKind() == ElementKind.PACKAGE) { - if (shouldSuppressWarningsOnElement(packageElement, errKey)) { + if (hasSuppressWarningsAnnotationForErrorKey(packageElement, errKey)) { return true; } if (!foundAnnotatedFor @@ -2885,7 +2884,7 @@ public boolean shouldSuppressWarnings(Element elt, String errKey) { } for (Element currElt = elt; currElt != null; currElt = currElt.getEnclosingElement()) { - if (shouldSuppressWarningsOnElement(currElt, errKey)) { + if (hasSuppressWarningsAnnotationForErrorKey(currElt, errKey)) { return true; } } @@ -2898,9 +2897,9 @@ public boolean shouldSuppressWarnings(Element elt, String errKey) { * * @param elt the element whose annotations to check * @param errKey the error key the checker is emitting - * @return true if {@code elt} has an corresponding {@code @SuppressWarnings} annotation + * @return true if {@code elt} has a corresponding {@code @SuppressWarnings} annotation */ - private boolean shouldSuppressWarningsOnElement(Element elt, String errKey) { + private boolean hasSuppressWarningsAnnotationForErrorKey(Element elt, String errKey) { SuppressWarnings suppressWarningsAnno = elt.getAnnotation(SuppressWarnings.class); if (suppressWarningsAnno != null) { String[] suppressWarningsStrings = suppressWarningsAnno.value(); From a48161964bb8df82eeb46b1e27d4a91ec121d32f Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Fri, 19 Jun 2026 13:59:55 -0400 Subject: [PATCH 09/13] Clarify SuppressWarnings scope docs --- docs/manual/warnings.tex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/manual/warnings.tex b/docs/manual/warnings.tex index 87b4ada46f9c..f482cea3e4f2 100644 --- a/docs/manual/warnings.tex +++ b/docs/manual/warnings.tex @@ -699,7 +699,8 @@ Note that the \code{@AnnotatedFor} annotation must include the checker's name to enable warnings from that checker. An explicit \code{@SuppressWarnings} annotation can suppress warnings in -\code{@AnnotatedFor} code. +\code{@AnnotatedFor} code and code outside the scope of a corresponding +\code{@AnnotatedFor} annotation. For example, use \code{@AnnotatedFor("nullness")} for the Nullness Checker. This flag only suppresses warnings, compared to \code{-AuseConservativeDefaultsForUncheckedCode=source}, which also applies conservative defaults for code outside the scope of an \code{@AnnotatedFor} annotation. From a82e29003029d16b0f41626b1a3455eb1e8bc550 Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Fri, 19 Jun 2026 14:02:15 -0400 Subject: [PATCH 10/13] Refine AnnotatedFor suppression wording --- docs/manual/annotating-libraries.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manual/annotating-libraries.tex b/docs/manual/annotating-libraries.tex index ca068587c82a..b6936efaf227 100644 --- a/docs/manual/annotating-libraries.tex +++ b/docs/manual/annotating-libraries.tex @@ -389,7 +389,7 @@ but does not change the default qualifiers for source code (see Section~\ref{aonlyannotatedfor}). In code with a relevant \<@AnnotatedFor> annotation, warnings can be suppressed -by an in-scope \<@SuppressWarnings> annotation. +by an explicit \<@SuppressWarnings> annotation. \end{sloppypar} The \refqualclass{framework/qual}{AnnotatedFor} annotation, written on a From 1e400ff5604d90ee5677335efe18317e71ab1e70 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Thu, 16 Jul 2026 23:47:58 -0400 Subject: [PATCH 11/13] Add failing test for shouldSuppressWarnings(Element) issue --- .../test/junit/ElementSuppressionTest.java | 23 ++++++++++++++++ ...lementSuppressionAnnotatedTypeFactory.java | 26 ++++++++++++++++++ .../ElementSuppressionChecker.java | 5 ++++ .../ElementSuppressionVisitor.java | 27 +++++++++++++++++++ .../ElementSuppressionTestCase.java | 22 +++++++++++++++ 5 files changed, 103 insertions(+) create mode 100644 framework/src/test/java/org/checkerframework/framework/test/junit/ElementSuppressionTest.java create mode 100644 framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionAnnotatedTypeFactory.java create mode 100644 framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionChecker.java create mode 100644 framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionVisitor.java create mode 100644 framework/tests/conservative-defaults/elementsuppression/ElementSuppressionTestCase.java diff --git a/framework/src/test/java/org/checkerframework/framework/test/junit/ElementSuppressionTest.java b/framework/src/test/java/org/checkerframework/framework/test/junit/ElementSuppressionTest.java new file mode 100644 index 000000000000..e3cf44a90cf0 --- /dev/null +++ b/framework/src/test/java/org/checkerframework/framework/test/junit/ElementSuppressionTest.java @@ -0,0 +1,23 @@ +package org.checkerframework.framework.test.junit; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +public class ElementSuppressionTest extends CheckerFrameworkPerDirectoryTest { + public ElementSuppressionTest(List testFiles) { + super( + testFiles, + org.checkerframework.framework.testchecker.elementsuppression + .ElementSuppressionChecker.class, + "elementsuppression", + "-AuseConservativeDefaultsForUncheckedCode=source,bytecode"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"conservative-defaults/elementsuppression"}; + } +} diff --git a/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionAnnotatedTypeFactory.java b/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionAnnotatedTypeFactory.java new file mode 100644 index 000000000000..c5ff285da1f0 --- /dev/null +++ b/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionAnnotatedTypeFactory.java @@ -0,0 +1,26 @@ +package org.checkerframework.framework.testchecker.elementsuppression; + +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.testchecker.util.SubQual; +import org.checkerframework.framework.testchecker.util.SuperQual; + +import java.lang.annotation.Annotation; +import java.util.LinkedHashSet; +import java.util.Set; + +public class ElementSuppressionAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { + @SuppressWarnings("this-escape") + public ElementSuppressionAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + this.postInit(); + } + + @Override + protected Set> createSupportedTypeQualifiers() { + Set> qualSet = new LinkedHashSet<>(); + qualSet.add(SubQual.class); + qualSet.add(SuperQual.class); + return qualSet; + } +} diff --git a/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionChecker.java b/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionChecker.java new file mode 100644 index 000000000000..1fa76167d700 --- /dev/null +++ b/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionChecker.java @@ -0,0 +1,5 @@ +package org.checkerframework.framework.testchecker.elementsuppression; + +import org.checkerframework.common.basetype.BaseTypeChecker; + +public class ElementSuppressionChecker extends BaseTypeChecker {} diff --git a/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionVisitor.java b/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionVisitor.java new file mode 100644 index 000000000000..65c0971af90a --- /dev/null +++ b/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionVisitor.java @@ -0,0 +1,27 @@ +package org.checkerframework.framework.testchecker.elementsuppression; + +import com.sun.source.tree.ClassTree; + +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.javacutil.TreeUtils; + +import javax.lang.model.element.Element; + +public class ElementSuppressionVisitor + extends BaseTypeVisitor { + public ElementSuppressionVisitor(BaseTypeChecker checker) { + super(checker); + } + + @Override + public void processClassTree(ClassTree node) { + Element elt = TreeUtils.elementFromDeclaration(node); + if (elt != null && elt.getSimpleName().toString().startsWith("ReportOnMe")) { + // report a custom error on the Element itself! + // "type.invalid" is a built-in error key we can misuse for testing. + checker.reportError(elt, "type.invalid", "mock type", "mock message"); + } + super.processClassTree(node); + } +} diff --git a/framework/tests/conservative-defaults/elementsuppression/ElementSuppressionTestCase.java b/framework/tests/conservative-defaults/elementsuppression/ElementSuppressionTestCase.java new file mode 100644 index 000000000000..85ebba7aad6f --- /dev/null +++ b/framework/tests/conservative-defaults/elementsuppression/ElementSuppressionTestCase.java @@ -0,0 +1,22 @@ +import org.checkerframework.framework.qual.AnnotatedFor; + +public class ElementSuppressionTestCase { + + // Test 1: No suppression, no @AnnotatedFor. + // Under -AuseConservativeDefaultsForUncheckedCode=source, this SHOULD BE SUPPRESSED + // because it is not within an @AnnotatedFor scope! + // But currently, the bug prevents this suppression, so it will EMIT the error, + // which makes the test FAIL if we assert no error, or PASS if we assert error. + // We want a failing test! So we expect NO error, because it should be suppressed. + class ReportOnMe {} + + // Test 2: Inside @AnnotatedFor. Should emit error. + @AnnotatedFor("elementsuppression") + // :: error: (type.invalid) + class ReportOnMe2 {} + + // Test 3: Has @SuppressWarnings. Should suppress. + @SuppressWarnings("elementsuppression") + @AnnotatedFor("elementsuppression") + class ReportOnMe3 {} +} From 19b2e073568640fdfdbe20a2624925ace0202c64 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Thu, 16 Jul 2026 23:48:51 -0400 Subject: [PATCH 12/13] Fix shouldSuppressWarnings(Element) AnnotatedFor behavior and update Javadocs --- .../framework/source/SourceChecker.java | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 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 e43a6b595b4e..e8dc57786b2b 100644 --- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java +++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java @@ -1193,6 +1193,15 @@ public void initChecker() { messageStore = new TreeSet<>(); } + Collection prefixes = getSuppressWarningsPrefixes(); + if (prefixes.isEmpty() + || (prefixes.size() == 1 && prefixes.contains(SUPPRESS_ALL_PREFIX))) { + throw new BugInCF( + "Checker must provide a SuppressWarnings prefix." + + " SourceChecker#getSuppressWarningsPrefixes was not overridden" + + " correctly."); + } + // Validate the lint flags, if they haven't been used already. if (this.activeLints == null) { this.activeLints = createActiveLints(getOptions()); @@ -2822,20 +2831,13 @@ public boolean shouldSuppressWarnings(Tree tree, String errKey) { * * @param path the TreePath that might be a source of, or related to, a warning * @param errKey the error key the checker is emitting - * @return true if no warning should be emitted for the given path because it is contained by a - * declaration with an appropriately-valued {@code @SuppressWarnings} annotation; false + * @return true if no warning should be emitted for the given path, either because it is + * contained by a declaration with an appropriately-valued {@code @SuppressWarnings} + * annotation, because it is suppressed by command-line arguments, or because it is outside + * an {@link AnnotatedFor} scope when source-based conservative defaults are enabled; false * otherwise */ public boolean shouldSuppressWarnings(TreePath path, String errKey) { - Collection prefixes = getSuppressWarningsPrefixes(); - if (prefixes.isEmpty() - || (prefixes.contains(SUPPRESS_ALL_PREFIX) && prefixes.size() == 1)) { - throw new BugInCF( - "Checker must provide a SuppressWarnings prefix." - + " SourceChecker#getSuppressWarningsPrefixes was not overridden" - + " correctly."); - } - if (shouldSuppress(getSuppressWarningsStringsFromOption(), errKey)) { return true; } @@ -2936,10 +2938,17 @@ public boolean useConservativeDefault(String kindOfCode) { * true if the element is within the scope of a @SuppressWarnings annotation, one of whose * values suppresses all the checker's warnings. * + *

This overload also accounts for source-position-based suppression from unchecked code: if + * no matching {@code @SuppressWarnings} is found, then warnings outside a relevant {@link + * AnnotatedFor} scope are suppressed when {@code + * -AuseConservativeDefaultsForUncheckedCode=source} or {@code -AonlyAnnotatedFor} is in effect. + * * @param elt the Element that might be a source of, or related to, a warning * @param errKey the error key the checker is emitting - * @return true if no warning should be emitted for the given Element because it is contained by - * a declaration with an appropriately-valued {@code @SuppressWarnings} annotation; false + * @return true if no warning should be emitted for the given Element, either because it is + * contained by a declaration with an appropriately-valued {@code @SuppressWarnings} + * annotation, because it is suppressed by command-line arguments, or because it is outside + * an {@link AnnotatedFor} scope when source-based conservative defaults are enabled; false * otherwise */ public boolean shouldSuppressWarnings(Element elt, String errKey) { @@ -2947,11 +2956,24 @@ public boolean shouldSuppressWarnings(Element elt, String errKey) { return true; } + boolean foundAnnotatedFor = false; for (Element currElt = elt; currElt != null; currElt = currElt.getEnclosingElement()) { if (hasSuppressWarningsAnnotationForErrorKey(currElt, errKey)) { return true; } + if (!foundAnnotatedFor && isAnnotatedForThisCheckerOrUpstreamChecker(currElt)) { + foundAnnotatedFor = true; + } + } + + if (foundAnnotatedFor) { + return false; + } else if (useConservativeDefaultsSource || onlyAnnotatedFor) { + // If we got this far without hitting an @AnnotatedFor and returning + // false, we DO suppress the warning. + return true; } + return false; } From dd4e56faac3772a88b70d66b5ad43a51c466897d Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Fri, 17 Jul 2026 00:35:42 -0400 Subject: [PATCH 13/13] Document element suppression tests --- .../framework/test/junit/ElementSuppressionTest.java | 8 ++++++++ .../ElementSuppressionAnnotatedTypeFactory.java | 7 +++++++ .../elementsuppression/ElementSuppressionChecker.java | 1 + .../elementsuppression/ElementSuppressionVisitor.java | 7 +++++++ 4 files changed, 23 insertions(+) diff --git a/framework/src/test/java/org/checkerframework/framework/test/junit/ElementSuppressionTest.java b/framework/src/test/java/org/checkerframework/framework/test/junit/ElementSuppressionTest.java index e3cf44a90cf0..8646a863db9f 100644 --- a/framework/src/test/java/org/checkerframework/framework/test/junit/ElementSuppressionTest.java +++ b/framework/src/test/java/org/checkerframework/framework/test/junit/ElementSuppressionTest.java @@ -6,7 +6,12 @@ import java.io.File; import java.util.List; +/** Tests suppression of diagnostics reported on elements. */ public class ElementSuppressionTest extends CheckerFrameworkPerDirectoryTest { + + /** + * @param testFiles the files containing test code, which will be type-checked + */ public ElementSuppressionTest(List testFiles) { super( testFiles, @@ -16,6 +21,9 @@ public ElementSuppressionTest(List testFiles) { "-AuseConservativeDefaultsForUncheckedCode=source,bytecode"); } + /** + * @return the directories containing test files + */ @Parameters public static String[] getTestDirs() { return new String[] {"conservative-defaults/elementsuppression"}; diff --git a/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionAnnotatedTypeFactory.java b/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionAnnotatedTypeFactory.java index c5ff285da1f0..291cce1daebe 100644 --- a/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionAnnotatedTypeFactory.java +++ b/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionAnnotatedTypeFactory.java @@ -9,7 +9,14 @@ import java.util.LinkedHashSet; import java.util.Set; +/** The annotated type factory for {@link ElementSuppressionChecker}. */ public class ElementSuppressionAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { + + /** + * Creates a new ElementSuppressionAnnotatedTypeFactory. + * + * @param checker the checker + */ @SuppressWarnings("this-escape") public ElementSuppressionAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); diff --git a/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionChecker.java b/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionChecker.java index 1fa76167d700..338b0c85ddd0 100644 --- a/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionChecker.java +++ b/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionChecker.java @@ -2,4 +2,5 @@ import org.checkerframework.common.basetype.BaseTypeChecker; +/** A test checker for suppression of diagnostics reported on elements. */ public class ElementSuppressionChecker extends BaseTypeChecker {} diff --git a/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionVisitor.java b/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionVisitor.java index 65c0971af90a..dbcc7426abea 100644 --- a/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionVisitor.java +++ b/framework/src/test/java/org/checkerframework/framework/testchecker/elementsuppression/ElementSuppressionVisitor.java @@ -8,8 +8,15 @@ import javax.lang.model.element.Element; +/** Reports a diagnostic on selected class elements to test element-based suppression. */ public class ElementSuppressionVisitor extends BaseTypeVisitor { + + /** + * Creates a new ElementSuppressionVisitor. + * + * @param checker the checker + */ public ElementSuppressionVisitor(BaseTypeChecker checker) { super(checker); }