From dd3e16ff38f6e98d2f8846de397254bb8cf115a1 Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Tue, 1 Sep 2026 10:27:09 -0400 Subject: [PATCH 1/9] Support package annotation subpackage opt-outs --- .../common/util/report/qual/ReportUse.java | 18 +++- .../framework/qual/AnnotatedFor.java | 11 ++ .../framework/qual/HasQualifierParameter.java | 15 ++- .../tainting/hqpoptin/InNestedSubpackage.java | 9 ++ .../tests/tainting/hqpoptin/InSubpackage.java | 9 ++ .../tests/tainting/hqpoptin/package-info.java | 7 ++ .../hqpoptout/InNestedSubpackage.java | 9 ++ .../tests/tainting/hqpoptout/InPackage.java | 9 ++ .../tainting/hqpoptout/InSubpackage.java | 10 ++ .../tainting/hqpoptout/package-info.java | 7 ++ docs/CHANGELOG.md | 15 +++ docs/manual/advanced-features.tex | 4 +- docs/manual/annotating-libraries.tex | 11 ++ docs/manual/generics.tex | 6 ++ .../common/basetype/BaseTypeChecker.java | 64 +++++++++-- .../common/util/report/ReportVisitor.java | 53 +++++++-- .../framework/type/AnnotatedTypeFactory.java | 101 ++++++++++++++++-- .../subpkgoptin/InNestedSubpackage.java | 13 +++ .../annotatedfor/subpkgoptin/InPackage.java | 11 ++ .../subpkgoptin/InSubpackage.java | 13 +++ .../subpkgoptin/package-info.java | 7 ++ .../subpkgoptout/InNestedSubpackage.java | 11 ++ .../annotatedfor/subpkgoptout/InPackage.java | 13 +++ .../subpkgoptout/InSubpackage.java | 12 +++ .../subpkgoptout/package-info.java | 7 ++ .../report/reportuseoptin/InInnerPackage.java | 4 + .../reportuseoptin/InNestedSubpackage.java | 4 + .../tests/report/reportuseoptin/Uses.java | 9 ++ .../report/reportuseoptin/package-info.java | 4 + .../reportuseoptout/InNestedSubpackage.java | 3 + .../report/reportuseoptout/InPackage.java | 4 + .../tests/report/reportuseoptout/Uses.java | 8 ++ .../report/reportuseoptout/package-info.java | 4 + 33 files changed, 457 insertions(+), 28 deletions(-) create mode 100644 checker/tests/tainting/hqpoptin/InNestedSubpackage.java create mode 100644 checker/tests/tainting/hqpoptin/InSubpackage.java create mode 100644 checker/tests/tainting/hqpoptin/package-info.java create mode 100644 checker/tests/tainting/hqpoptout/InNestedSubpackage.java create mode 100644 checker/tests/tainting/hqpoptout/InPackage.java create mode 100644 checker/tests/tainting/hqpoptout/InSubpackage.java create mode 100644 checker/tests/tainting/hqpoptout/package-info.java create mode 100644 framework/tests/conservative-defaults/annotatedfor/subpkgoptin/InNestedSubpackage.java create mode 100644 framework/tests/conservative-defaults/annotatedfor/subpkgoptin/InPackage.java create mode 100644 framework/tests/conservative-defaults/annotatedfor/subpkgoptin/InSubpackage.java create mode 100644 framework/tests/conservative-defaults/annotatedfor/subpkgoptin/package-info.java create mode 100644 framework/tests/conservative-defaults/annotatedfor/subpkgoptout/InNestedSubpackage.java create mode 100644 framework/tests/conservative-defaults/annotatedfor/subpkgoptout/InPackage.java create mode 100644 framework/tests/conservative-defaults/annotatedfor/subpkgoptout/InSubpackage.java create mode 100644 framework/tests/conservative-defaults/annotatedfor/subpkgoptout/package-info.java create mode 100644 framework/tests/report/reportuseoptin/InInnerPackage.java create mode 100644 framework/tests/report/reportuseoptin/InNestedSubpackage.java create mode 100644 framework/tests/report/reportuseoptin/Uses.java create mode 100644 framework/tests/report/reportuseoptin/package-info.java create mode 100644 framework/tests/report/reportuseoptout/InNestedSubpackage.java create mode 100644 framework/tests/report/reportuseoptout/InPackage.java create mode 100644 framework/tests/report/reportuseoptout/Uses.java create mode 100644 framework/tests/report/reportuseoptout/package-info.java diff --git a/checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportUse.java b/checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportUse.java index 91b7f6465297..b9077e2a0058 100644 --- a/checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportUse.java +++ b/checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportUse.java @@ -6,8 +6,22 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** Report all uses of a type that has this annotation. Can also be used on a package. */ +/** + * Report all uses of a type that has this annotation. Can also be used on a package. + * + *

When written on a package, {@code @ReportUse} applies to that package and its subpackages by + * default. Set {@link #applyToSubpackages()} to false to limit it to the package itself; doing so + * does not block an applicable {@code @ReportUse} on an enclosing package. + */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PACKAGE, ElementType.TYPE}) -public @interface ReportUse {} +public @interface ReportUse { + + /** + * When used on a package, whether this annotation should also apply to subpackages. + * + * @return whether this annotation should be inherited by subpackages + */ + boolean applyToSubpackages() default true; +} diff --git a/checker-qual/src/main/java/org/checkerframework/framework/qual/AnnotatedFor.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/AnnotatedFor.java index 48c4f7ab47f7..05da2f4138ba 100644 --- a/checker-qual/src/main/java/org/checkerframework/framework/qual/AnnotatedFor.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/AnnotatedFor.java @@ -23,6 +23,10 @@ * warnings. However, a class with a relevant {@code @AnnotatedFor} annotation is always defaulted * normally (typically using the CLIMB-to-top rule), and typechecking warnings are issued. * + *

An {@code @AnnotatedFor} on a package also applies to subpackages, unless the {@code + * applyToSubpackages} field is set to false. Setting it to false does not block an applicable + * {@code @AnnotatedFor} on an enclosing package. + * * @checker_framework.manual #compiling-libraries Compiling partially-annotated libraries */ @Documented @@ -39,4 +43,11 @@ * @checker_framework.manual #shorthand-for-checkers Short names for built-in checkers */ String[] value(); + + /** + * When used on a package, whether this annotation should also apply to subpackages. + * + * @return whether this annotation should be inherited by subpackages + */ + boolean applyToSubpackages() default true; } diff --git a/checker-qual/src/main/java/org/checkerframework/framework/qual/HasQualifierParameter.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/HasQualifierParameter.java index cd59052d2f50..bef795ba87e4 100644 --- a/checker-qual/src/main/java/org/checkerframework/framework/qual/HasQualifierParameter.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/HasQualifierParameter.java @@ -63,9 +63,11 @@ *

Written on a package

* *

When {@code @HasQualifierParameter} is written on a package, it is equivalent to writing that - * annotation on each class in the package or in a sub-package. It can be disabled on a specific - * class and its subclasses by writing {@code @NoQualifierParameter} on that class. This annotation - * may not be written on the same class as {@code NoQualifierParameter} for the same hierarchy. + * annotation on each class in the package or in a sub-package. Set the {@code applyToSubpackages} + * field to false to limit it to the package itself; doing so does not block an applicable + * {@code @HasQualifierParameter} on an enclosing package. It can be disabled on a specific class + * and its subclasses by writing {@code @NoQualifierParameter} on that class. This annotation may + * not be written on the same class as {@code NoQualifierParameter} for the same hierarchy. * * @see NoQualifierParameter */ @@ -81,4 +83,11 @@ * @return the value */ Class[] value(); + + /** + * When used on a package, whether this annotation should also apply to subpackages. + * + * @return whether this annotation should be inherited by subpackages + */ + boolean applyToSubpackages() default true; } diff --git a/checker/tests/tainting/hqpoptin/InNestedSubpackage.java b/checker/tests/tainting/hqpoptin/InNestedSubpackage.java new file mode 100644 index 000000000000..163ab18c4e5f --- /dev/null +++ b/checker/tests/tainting/hqpoptin/InNestedSubpackage.java @@ -0,0 +1,9 @@ +package hqpoptin.sub.nested; + +import org.checkerframework.checker.tainting.qual.PolyTainted; + +// applyToSubpackages defaults to true, so the qualifier parameter reaches transitively nested +// subpackages. +public class InNestedSubpackage { + @PolyTainted int field; +} diff --git a/checker/tests/tainting/hqpoptin/InSubpackage.java b/checker/tests/tainting/hqpoptin/InSubpackage.java new file mode 100644 index 000000000000..79218845208f --- /dev/null +++ b/checker/tests/tainting/hqpoptin/InSubpackage.java @@ -0,0 +1,9 @@ +package hqpoptin.sub; + +import org.checkerframework.checker.tainting.qual.PolyTainted; + +// applyToSubpackages defaults to true, so this class inherits the qualifier parameter from package +// hqpoptin and the polymorphic qualifier is allowed. +public class InSubpackage { + @PolyTainted int field; +} diff --git a/checker/tests/tainting/hqpoptin/package-info.java b/checker/tests/tainting/hqpoptin/package-info.java new file mode 100644 index 000000000000..1f825895f366 --- /dev/null +++ b/checker/tests/tainting/hqpoptin/package-info.java @@ -0,0 +1,7 @@ +// This file and the class beside it are deliberately in one directory so that the package +// annotation and the subpackage class are compiled together. +@HasQualifierParameter(Tainted.class) +package hqpoptin; + +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.framework.qual.HasQualifierParameter; diff --git a/checker/tests/tainting/hqpoptout/InNestedSubpackage.java b/checker/tests/tainting/hqpoptout/InNestedSubpackage.java new file mode 100644 index 000000000000..677c77af5c2c --- /dev/null +++ b/checker/tests/tainting/hqpoptout/InNestedSubpackage.java @@ -0,0 +1,9 @@ +package hqpoptout.sub.nested; + +import org.checkerframework.checker.tainting.qual.PolyTainted; + +// The enclosing package opts out of all subpackages, including transitively nested ones. +public class InNestedSubpackage { + // :: error: (invalid.polymorphic.qualifier.use) + @PolyTainted int field; +} diff --git a/checker/tests/tainting/hqpoptout/InPackage.java b/checker/tests/tainting/hqpoptout/InPackage.java new file mode 100644 index 000000000000..71bf437a24e0 --- /dev/null +++ b/checker/tests/tainting/hqpoptout/InPackage.java @@ -0,0 +1,9 @@ +package hqpoptout; + +import org.checkerframework.checker.tainting.qual.PolyTainted; + +// The package's @HasQualifierParameter still covers the package itself, so a polymorphic +// qualifier may be written on this field. +public class InPackage { + @PolyTainted int field; +} diff --git a/checker/tests/tainting/hqpoptout/InSubpackage.java b/checker/tests/tainting/hqpoptout/InSubpackage.java new file mode 100644 index 000000000000..c30b98a2cf79 --- /dev/null +++ b/checker/tests/tainting/hqpoptout/InSubpackage.java @@ -0,0 +1,10 @@ +package hqpoptout.sub; + +import org.checkerframework.checker.tainting.qual.PolyTainted; + +// Package hqpoptout sets applyToSubpackages=false, so this class has no qualifier parameter and +// the polymorphic qualifier is rejected. +public class InSubpackage { + // :: error: (invalid.polymorphic.qualifier.use) + @PolyTainted int field; +} diff --git a/checker/tests/tainting/hqpoptout/package-info.java b/checker/tests/tainting/hqpoptout/package-info.java new file mode 100644 index 000000000000..0344f9494897 --- /dev/null +++ b/checker/tests/tainting/hqpoptout/package-info.java @@ -0,0 +1,7 @@ +// This file and the classes beside it are deliberately in one directory so that the package +// annotation and the subpackage class are compiled together. +@HasQualifierParameter(value = Tainted.class, applyToSubpackages = false) +package hqpoptout; + +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.framework.qual.HasQualifierParameter; diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 63142b582161..fcdb0de4f14b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,13 @@ Version 3.49.5-eisop2 (June ?, 2026) **User-visible changes:** +`AnnotatedFor`, `HasQualifierParameter`, and `ReportUse` support the new +`applyToSubpackages` annotation element, which decides whether an annotation +written on a package also applies to that package's subpackages. To preserve +the current behavior the default is `true`. `DefaultQualifier` already had this +element. When an older `checker-qual` artifact lacks the element, package +annotations retain their previous behavior and apply to subpackages. + The Checker Framework now issues an `annotation.on.supertype` error when an annotation supported by the checker is written as a main annotation on the superclass or interface in an `extends` or `implements` clause. Annotations on the supertype's type arguments remain permitted. A checker @@ -341,6 +348,14 @@ which `BaseTypeChecker` implements with a cache. **Implementation details:** +New method `AnnotatedTypeFactory.doesAnnotatedForApplyToSubpackages(AnnotationMirror)` +reports whether an `@AnnotatedFor` written on a package also applies to that +package's subpackages. Code that walks up the package chain to find a package +annotation should gate the steps to enclosing packages on the annotation's +`applyToSubpackages` element rather than propagating unconditionally; the +annotated package itself is always in scope. An absent element, as in a +`checker-qual` that predates it, is treated as true for compatibility. + `AnnotatedIntersectionType.summarizeBounds` computes the summary described above, reading each bound's qualifier, explicit or defaulted, uniformly, and folding diff --git a/docs/manual/advanced-features.tex b/docs/manual/advanced-features.tex index 6cefdfb98fb9..e197375a0714 100644 --- a/docs/manual/advanced-features.tex +++ b/docs/manual/advanced-features.tex @@ -322,7 +322,9 @@ If \code{@DefaultQualifier}[\code{s}] is placed on a package (via the \ file), then it applies to the given package \emph{and} -all subpackages. +all subpackages. To limit a default to the package it is written on, set +the \ element to false, as in +\<@DefaultQualifier(value = NonNull.class, applyToSubpackages = false)>. % This is slightly at odds with Java's treatment of packages of different % names as essentially unrelated, but is more intuitive and useful. diff --git a/docs/manual/annotating-libraries.tex b/docs/manual/annotating-libraries.tex index b6ce89dc8fd8..e6c84bc03334 100644 --- a/docs/manual/annotating-libraries.tex +++ b/docs/manual/annotating-libraries.tex @@ -436,6 +436,17 @@ any annotations, but that you examined the source code and verified that all appropriate annotations are present. +\<@AnnotatedFor> may also be written on a package, via the +\ file, in which case it applies to the given package +\emph{and} all subpackages. To limit it to the package it is written on, +set the \ element to false, as in +\<@AnnotatedFor(value = "nullness", applyToSubpackages = false)>. +This limits only that annotation; it does not block an applicable +\<@AnnotatedFor> on an enclosing package. +Because \<@AnnotatedFor> has source retention, a package annotation only +affects compilation units that are compiled together with its +\ file. + \begin{sloppypar} Whenever you compile a class using the Checker Framework, including when using the \<-AuseConservativeDefaultsForUncheckedCode=source,bytecode> command-line diff --git a/docs/manual/generics.tex b/docs/manual/generics.tex index e517bd34224b..cc54a4a8a37e 100644 --- a/docs/manual/generics.tex +++ b/docs/manual/generics.tex @@ -1051,6 +1051,12 @@ package mypackage; \end{Verbatim} +To limit \<@HasQualifierParameter> to the package it is written on, set the +\ element to false, as in +\<@HasQualifierParameter(value = Tainted.class, applyToSubpackages = false)>. +This limits only that annotation; it does not block an applicable +\<@HasQualifierParameter> on an enclosing package. + When using \<@HasQualifierParameter> on a package, it's possible to disable it for a specific class using \refqualclass{framework/qual}{NoQualifierParameter}. Writing this on a class indicates it has no class qualifier parameter and diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeChecker.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeChecker.java index ae058151e952..a39273fe93c0 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeChecker.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeChecker.java @@ -81,6 +81,15 @@ public abstract class BaseTypeChecker extends SourceChecker { private final IdentityHashMap elementAnnotatedForThisCheckerOrUpstreamCache = new IdentityHashMap<>(); + /** + * A mapping from a package to whether an {@code @AnnotatedFor} on that package or an enclosing + * package applies to this checker or an upstream checker and to subpackages. This differs from + * {@link #elementAnnotatedForThisCheckerOrUpstreamCache} because an {@code @AnnotatedFor} that + * opts out of subpackages still covers its own package. + */ + private final IdentityHashMap annotatedForReachesSubpackagesCache = + new IdentityHashMap<>(); + /** An array containing just {@code BaseTypeChecker.class}. */ protected static Class[] baseTypeCheckerClassArray = new Class[] {BaseTypeChecker.class}; @@ -345,21 +354,58 @@ public boolean isElementAnnotatedForThisCheckerOrUpstreamChecker(@Nullable Eleme && atypeFactory.doesAnnotatedForApplyToThisChecker(annotatedFor); if (!elementAnnotatedForThisChecker) { - Element parent; if (elt.getKind() == ElementKind.PACKAGE) { - parent = - ElementUtils.parentPackage( - (PackageElement) elt, atypeFactory.getElementUtils()); + // A package is covered by an enclosing package only if that package's + // @AnnotatedFor applies to subpackages. + elementAnnotatedForThisChecker = + doesAnnotatedForReachSubpackages( + ElementUtils.parentPackage( + (PackageElement) elt, atypeFactory.getElementUtils())); } else { - parent = elt.getEnclosingElement(); - } - - if (parent != null && isElementAnnotatedForThisCheckerOrUpstreamChecker(parent)) { - elementAnnotatedForThisChecker = true; + // A non-package element is inside its enclosing element rather than in a + // subpackage of it, so applyToSubpackages does not apply to this step. + Element parent = elt.getEnclosingElement(); + elementAnnotatedForThisChecker = + parent != null && isElementAnnotatedForThisCheckerOrUpstreamChecker(parent); } } elementAnnotatedForThisCheckerOrUpstreamCache.put(elt, elementAnnotatedForThisChecker); return elementAnnotatedForThisChecker; } + + /** + * Returns true if a subpackage of {@code pkg} would be covered by an {@code @AnnotatedFor} on + * {@code pkg} or an enclosing package. + * + * @param pkg a package, or null + * @return true if {@code pkg} or an enclosing package has an {@code @AnnotatedFor} for this + * checker or an upstream checker that applies to subpackages + */ + private boolean doesAnnotatedForReachSubpackages(@Nullable PackageElement pkg) { + if (pkg == null) { + return false; + } + + Boolean cached = annotatedForReachesSubpackagesCache.get(pkg); + if (cached != null) { + return cached; + } + + AnnotatedTypeFactory atypeFactory = getTypeFactory(); + AnnotationMirror annotatedFor = atypeFactory.getDeclAnnotation(pkg, AnnotatedFor.class); + boolean result = + annotatedFor != null + && atypeFactory.doesAnnotatedForApplyToThisChecker(annotatedFor) + && atypeFactory.doesAnnotatedForApplyToSubpackages(annotatedFor); + if (!result) { + // A package that opts out of subpackages does not block an outer package that opts in. + result = + doesAnnotatedForReachSubpackages( + ElementUtils.parentPackage(pkg, atypeFactory.getElementUtils())); + } + + annotatedForReachesSubpackagesCache.put(pkg, result); + return result; + } } diff --git a/framework/src/main/java/org/checkerframework/common/util/report/ReportVisitor.java b/framework/src/main/java/org/checkerframework/common/util/report/ReportVisitor.java index 0b34c8acb5b4..061c5464effd 100644 --- a/framework/src/main/java/org/checkerframework/common/util/report/ReportVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/util/report/ReportVisitor.java @@ -29,6 +29,7 @@ import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.util.AnnotatedTypes; +import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; @@ -37,12 +38,14 @@ import java.util.Locale; import java.util.Map; +import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic; /** The visitor for the Report Checker. */ public class ReportVisitor extends BaseTypeVisitor { @@ -53,9 +56,30 @@ public class ReportVisitor extends BaseTypeVisitor { /** The modifiers that should be reported; may be null. */ private final @Nullable EnumSet modifiers; + /** + * The {@link ReportUse#applyToSubpackages()} element, or null if the checker-qual version on + * the classpath predates that element. + */ + private final @Nullable ExecutableElement reportUseApplyToSubpackagesElement; + public ReportVisitor(BaseTypeChecker checker) { super(checker); + reportUseApplyToSubpackagesElement = + TreeUtils.getMethodOrNull( + ReportUse.class, + "applyToSubpackages", + 0, + checker.getProcessingEnvironment()); + if (reportUseApplyToSubpackagesElement == null) { + checker.message( + Diagnostic.Kind.NOTE, + "The @ReportUse annotation on the classpath does not define the" + + " applyToSubpackages element; package annotations will apply to" + + " subpackages. Use the EISOP checker-qual artifact to control this" + + " behavior."); + } + EnumSet treeKindsTmp = EnumSet.noneOf(Tree.Kind.class); for (String treeKind : checker.getStringsOption("reportTreeKinds", ',')) { treeKindsTmp.add(Tree.Kind.valueOf(treeKind.toUpperCase(Locale.ROOT))); @@ -88,8 +112,12 @@ public Void scan(Tree tree, Void p) { */ private void checkReportUse(Tree tree, Element member) { Element loop = member; + boolean isParentPackage = false; while (loop != null) { - boolean report = this.atypeFactory.getDeclAnnotation(loop, ReportUse.class) != null; + AnnotationMirror reportUse = this.atypeFactory.getDeclAnnotation(loop, ReportUse.class); + boolean report = + reportUse != null + && (!isParentPackage || doesReportUseApplyToSubpackages(reportUse)); if (report) { checker.reportError( tree, @@ -100,17 +128,30 @@ private void checkReportUse(Tree tree, Element member) { ElementUtils.getQualifiedName(member), member.getKind()); break; - } else { - if (loop.getKind() == ElementKind.PACKAGE) { - loop = ElementUtils.parentPackage((PackageElement) loop, elements); - continue; - } + } else if (loop.getKind() == ElementKind.PACKAGE) { + loop = ElementUtils.parentPackage((PackageElement) loop, elements); + isParentPackage = true; + continue; } // Package will always be the last iteration. loop = loop.getEnclosingElement(); } } + /** + * Returns whether the given package-level {@link ReportUse} annotation applies to subpackages. + * + * @param reportUse a {@link ReportUse} annotation + * @return whether {@code reportUse} applies to subpackages + */ + private boolean doesReportUseApplyToSubpackages(AnnotationMirror reportUse) { + // A checker-qual that predates the element gives no way to opt out, so preserve the old + // behavior and treat package annotations as applying to subpackages. + return reportUseApplyToSubpackagesElement == null + || AnnotationUtils.getElementValue( + reportUse, reportUseApplyToSubpackagesElement, Boolean.class, true); + } + /* Would we want this? Seems redundant, as all uses of the imported * package should already be reported. * Also, how do we get an element for the import? diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 92992a67cd0d..3c5b190fd079 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -140,6 +140,7 @@ import javax.lang.model.type.TypeVariable; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; +import javax.tools.Diagnostic; /** * The methods of this class take an element or AST node, and return the annotated type as an {@link @@ -206,6 +207,13 @@ public class AnnotatedTypeFactory implements AnnotationProvider { /** The AnnotatedFor.value argument/element. */ protected final ExecutableElement annotatedForValueElement; + /** + * The AnnotatedFor.applyToSubpackages() field/element. Null if the version of + * {@code @AnnotatedFor} on the classpath predates this element, in which case an + * {@code @AnnotatedFor} on a package always applies to subpackages. + */ + protected final @Nullable ExecutableElement annotatedForApplyToSubpackagesElement; + /** The EnsuresQualifier.expression field/element. */ protected final ExecutableElement ensuresQualifierExpressionElement; @@ -230,6 +238,13 @@ public class AnnotatedTypeFactory implements AnnotationProvider { /** The HasQualifierParameter.value field/element. */ protected final ExecutableElement hasQualifierParameterValueElement; + /** + * The HasQualifierParameter.applyToSubpackages() field/element. Null if the version of + * {@code @HasQualifierParameter} on the classpath predates this element, in which case a + * {@code @HasQualifierParameter} on a package always applies to subpackages. + */ + protected final @Nullable ExecutableElement hasQualifierParameterApplyToSubpackagesElement; + /** The MethodVal.className argument/element. */ public final ExecutableElement methodValClassNameElement; @@ -800,6 +815,17 @@ public AnnotatedTypeFactory(BaseTypeChecker checker) { annotatedForValueElement = TreeUtils.getMethod(AnnotatedFor.class, "value", 0, processingEnv); + annotatedForApplyToSubpackagesElement = + TreeUtils.getMethodOrNull( + AnnotatedFor.class, "applyToSubpackages", 0, processingEnv); + if (annotatedForApplyToSubpackagesElement == null) { + checker.message( + Diagnostic.Kind.NOTE, + "The @AnnotatedFor annotation on the classpath does not define the" + + " applyToSubpackages element; package annotations will apply to" + + " subpackages. Use the EISOP checker-qual artifact to control this" + + " behavior."); + } ensuresQualifierExpressionElement = TreeUtils.getMethod(EnsuresQualifier.class, "expression", 0, processingEnv); ensuresQualifierListValueElement = @@ -816,6 +842,17 @@ public AnnotatedTypeFactory(BaseTypeChecker checker) { TreeUtils.getMethod(FieldInvariant.class, "qualifier", 0, processingEnv); hasQualifierParameterValueElement = TreeUtils.getMethod(HasQualifierParameter.class, "value", 0, processingEnv); + hasQualifierParameterApplyToSubpackagesElement = + TreeUtils.getMethodOrNull( + HasQualifierParameter.class, "applyToSubpackages", 0, processingEnv); + if (hasQualifierParameterApplyToSubpackagesElement == null) { + checker.message( + Diagnostic.Kind.NOTE, + "The @HasQualifierParameter annotation on the classpath does not define the" + + " applyToSubpackages element; package annotations will apply to" + + " subpackages. Use the EISOP checker-qual artifact to control this" + + " behavior."); + } methodValClassNameElement = TreeUtils.getMethod(MethodVal.class, "className", 0, processingEnv); methodValMethodNameElement = @@ -5481,17 +5518,24 @@ public AnnotationMirrorSet getQualifierParameterHierarchies(Element element) { element, HasQualifierParameter.class, hasQualifierParameterValueElement)); AnnotationMirrorSet hasQualifierParameterTops = new AnnotationMirrorSet(); PackageElement packageElement = ElementUtils.enclosingPackage(element); - - // Traverse all packages containing this element. - while (packageElement != null) { - AnnotationMirrorSet packageDefaultTops = + if (packageElement != null) { + // The type's own package always applies; applyToSubpackages does not govern this step. + hasQualifierParameterTops.addAll( getSupportedAnnotationsInElementAnnotation( packageElement, HasQualifierParameter.class, - hasQualifierParameterValueElement); - hasQualifierParameterTops.addAll(packageDefaultTops); - + hasQualifierParameterValueElement)); packageElement = ElementUtils.parentPackage(packageElement, elements); + while (packageElement != null) { + if (doesHasQualifierParameterApplyToSubpackages(packageElement)) { + hasQualifierParameterTops.addAll( + getSupportedAnnotationsInElementAnnotation( + packageElement, + HasQualifierParameter.class, + hasQualifierParameterValueElement)); + } + packageElement = ElementUtils.parentPackage(packageElement, elements); + } } AnnotationMirrorSet noQualifierParamClasses = @@ -5506,6 +5550,30 @@ public AnnotationMirrorSet getQualifierParameterHierarchies(Element element) { return found; } + /** + * Returns whether {@code packageElement}'s {@code @HasQualifierParameter} applies to + * subpackages. + * + * @param packageElement a package + * @return true if {@code packageElement} is annotated with a {@code @HasQualifierParameter} + * that applies to subpackages + */ + private boolean doesHasQualifierParameterApplyToSubpackages(PackageElement packageElement) { + AnnotationMirror hasQualifierParameter = + getDeclAnnotation(packageElement, HasQualifierParameter.class); + if (hasQualifierParameter == null) { + return false; + } + // A checker-qual that predates the applyToSubpackages element gives no way to opt out, so + // treat the annotation as applying to subpackages. + return hasQualifierParameterApplyToSubpackagesElement == null + || AnnotationUtils.getElementValue( + hasQualifierParameter, + hasQualifierParameterApplyToSubpackagesElement, + Boolean.class, + true); + } + /** * Returns a set of supported annotation mirrors corresponding to the annotation classes listed * in the value element of an annotation with class {@code annoClass} on {@code element}. @@ -6852,6 +6920,25 @@ protected void makeConditionConsistentWithOtherMethod( } */ + /** + * Does {@code annotatedForAnno}, which is an {@link + * org.checkerframework.framework.qual.AnnotatedFor} annotation written on a package, also apply + * to subpackages of that package? + * + * @param annotatedForAnno an {@link AnnotatedFor} annotation written on a package + * @return whether {@code annotatedForAnno} applies to subpackages + */ + public boolean doesAnnotatedForApplyToSubpackages(AnnotationMirror annotatedForAnno) { + // A checker-qual that predates the applyToSubpackages element gives no way to opt out, so + // treat the annotation as applying to subpackages. + return annotatedForApplyToSubpackagesElement == null + || AnnotationUtils.getElementValue( + annotatedForAnno, + annotatedForApplyToSubpackagesElement, + Boolean.class, + true); + } + /** * Does {@code annotatedForAnno}, which is an {@link * org.checkerframework.framework.qual.AnnotatedFor} annotation, apply to this checker? diff --git a/framework/tests/conservative-defaults/annotatedfor/subpkgoptin/InNestedSubpackage.java b/framework/tests/conservative-defaults/annotatedfor/subpkgoptin/InNestedSubpackage.java new file mode 100644 index 000000000000..a7c93a2db4f5 --- /dev/null +++ b/framework/tests/conservative-defaults/annotatedfor/subpkgoptin/InNestedSubpackage.java @@ -0,0 +1,13 @@ +package afoptin.sub.nested; + +import org.checkerframework.framework.testchecker.util.SubQual; +import org.checkerframework.framework.testchecker.util.SuperQual; + +// applyToSubpackages defaults to true, so package afoptin's @AnnotatedFor reaches transitively +// nested subpackages. +public class InNestedSubpackage { + void m() { + // :: error: (assignment.type.incompatible) + @SubQual Object o = new @SuperQual Object(); + } +} diff --git a/framework/tests/conservative-defaults/annotatedfor/subpkgoptin/InPackage.java b/framework/tests/conservative-defaults/annotatedfor/subpkgoptin/InPackage.java new file mode 100644 index 000000000000..c917ca9431c8 --- /dev/null +++ b/framework/tests/conservative-defaults/annotatedfor/subpkgoptin/InPackage.java @@ -0,0 +1,11 @@ +package afoptin; + +import org.checkerframework.framework.testchecker.util.SubQual; +import org.checkerframework.framework.testchecker.util.SuperQual; + +public class InPackage { + void m() { + // :: error: (assignment.type.incompatible) + @SubQual Object o = new @SuperQual Object(); + } +} diff --git a/framework/tests/conservative-defaults/annotatedfor/subpkgoptin/InSubpackage.java b/framework/tests/conservative-defaults/annotatedfor/subpkgoptin/InSubpackage.java new file mode 100644 index 000000000000..d39320f29a62 --- /dev/null +++ b/framework/tests/conservative-defaults/annotatedfor/subpkgoptin/InSubpackage.java @@ -0,0 +1,13 @@ +package afoptin.sub; + +import org.checkerframework.framework.testchecker.util.SubQual; +import org.checkerframework.framework.testchecker.util.SuperQual; + +// applyToSubpackages defaults to true, so package afoptin's @AnnotatedFor reaches this subpackage +// and its subtyping warnings are issued. +public class InSubpackage { + void m() { + // :: error: (assignment.type.incompatible) + @SubQual Object o = new @SuperQual Object(); + } +} diff --git a/framework/tests/conservative-defaults/annotatedfor/subpkgoptin/package-info.java b/framework/tests/conservative-defaults/annotatedfor/subpkgoptin/package-info.java new file mode 100644 index 000000000000..26873e01fba4 --- /dev/null +++ b/framework/tests/conservative-defaults/annotatedfor/subpkgoptin/package-info.java @@ -0,0 +1,7 @@ +// This file and the classes beside it are deliberately in one directory: @AnnotatedFor is +// source-retention, so a package annotation only reaches other compilation units when its +// package-info is compiled in the same run. +@AnnotatedFor("subtyping") +package afoptin; + +import org.checkerframework.framework.qual.AnnotatedFor; diff --git a/framework/tests/conservative-defaults/annotatedfor/subpkgoptout/InNestedSubpackage.java b/framework/tests/conservative-defaults/annotatedfor/subpkgoptout/InNestedSubpackage.java new file mode 100644 index 000000000000..6782bbc5fbab --- /dev/null +++ b/framework/tests/conservative-defaults/annotatedfor/subpkgoptout/InNestedSubpackage.java @@ -0,0 +1,11 @@ +package afoptout.sub.nested; + +import org.checkerframework.framework.testchecker.util.SubQual; +import org.checkerframework.framework.testchecker.util.SuperQual; + +// The enclosing package opts out of all subpackages, including transitively nested ones. +public class InNestedSubpackage { + void m() { + @SubQual Object o = new @SuperQual Object(); + } +} diff --git a/framework/tests/conservative-defaults/annotatedfor/subpkgoptout/InPackage.java b/framework/tests/conservative-defaults/annotatedfor/subpkgoptout/InPackage.java new file mode 100644 index 000000000000..d53dccf3fc8b --- /dev/null +++ b/framework/tests/conservative-defaults/annotatedfor/subpkgoptout/InPackage.java @@ -0,0 +1,13 @@ +package afoptout; + +import org.checkerframework.framework.testchecker.util.SubQual; +import org.checkerframework.framework.testchecker.util.SuperQual; + +// Opting out of subpackages does not opt the annotated package itself out, so this code is in an +// @AnnotatedFor scope and its subtyping warnings are issued. +public class InPackage { + void m() { + // :: error: (assignment.type.incompatible) + @SubQual Object o = new @SuperQual Object(); + } +} diff --git a/framework/tests/conservative-defaults/annotatedfor/subpkgoptout/InSubpackage.java b/framework/tests/conservative-defaults/annotatedfor/subpkgoptout/InSubpackage.java new file mode 100644 index 000000000000..d5e36dbf249d --- /dev/null +++ b/framework/tests/conservative-defaults/annotatedfor/subpkgoptout/InSubpackage.java @@ -0,0 +1,12 @@ +package afoptout.sub; + +import org.checkerframework.framework.testchecker.util.SubQual; +import org.checkerframework.framework.testchecker.util.SuperQual; + +// Package afoptout sets applyToSubpackages=false, so this code is outside any @AnnotatedFor scope +// and conservative defaults suppress its warnings. No error is expected below. +public class InSubpackage { + void m() { + @SubQual Object o = new @SuperQual Object(); + } +} diff --git a/framework/tests/conservative-defaults/annotatedfor/subpkgoptout/package-info.java b/framework/tests/conservative-defaults/annotatedfor/subpkgoptout/package-info.java new file mode 100644 index 000000000000..60679e76ec01 --- /dev/null +++ b/framework/tests/conservative-defaults/annotatedfor/subpkgoptout/package-info.java @@ -0,0 +1,7 @@ +// This file and the classes beside it are deliberately in one directory: @AnnotatedFor is +// source-retention, so a package annotation only reaches other compilation units when its +// package-info is compiled in the same run. +@AnnotatedFor(value = "subtyping", applyToSubpackages = false) +package afoptout; + +import org.checkerframework.framework.qual.AnnotatedFor; diff --git a/framework/tests/report/reportuseoptin/InInnerPackage.java b/framework/tests/report/reportuseoptin/InInnerPackage.java new file mode 100644 index 000000000000..5f70a5af9a13 --- /dev/null +++ b/framework/tests/report/reportuseoptin/InInnerPackage.java @@ -0,0 +1,4 @@ +package reportuseoptin.inner; + +// :: error: (usage) +public class InInnerPackage {} diff --git a/framework/tests/report/reportuseoptin/InNestedSubpackage.java b/framework/tests/report/reportuseoptin/InNestedSubpackage.java new file mode 100644 index 000000000000..76bd897d808a --- /dev/null +++ b/framework/tests/report/reportuseoptin/InNestedSubpackage.java @@ -0,0 +1,4 @@ +package reportuseoptin.inner.nested; + +// :: error: (usage) +public class InNestedSubpackage {} diff --git a/framework/tests/report/reportuseoptin/Uses.java b/framework/tests/report/reportuseoptin/Uses.java new file mode 100644 index 000000000000..ddb59bcb8c04 --- /dev/null +++ b/framework/tests/report/reportuseoptin/Uses.java @@ -0,0 +1,9 @@ +public class Uses { + // The default true reaches a direct subpackage. + // :: error: (usage) + reportuseoptin.inner.InInnerPackage inInnerPackage; + + // The default true also reaches transitively nested subpackages. + // :: error: (usage) + reportuseoptin.inner.nested.InNestedSubpackage inNestedSubpackage; +} diff --git a/framework/tests/report/reportuseoptin/package-info.java b/framework/tests/report/reportuseoptin/package-info.java new file mode 100644 index 000000000000..a0df72eb4043 --- /dev/null +++ b/framework/tests/report/reportuseoptin/package-info.java @@ -0,0 +1,4 @@ +@ReportUse +package reportuseoptin; + +import org.checkerframework.common.util.report.qual.ReportUse; diff --git a/framework/tests/report/reportuseoptout/InNestedSubpackage.java b/framework/tests/report/reportuseoptout/InNestedSubpackage.java new file mode 100644 index 000000000000..5611760761d2 --- /dev/null +++ b/framework/tests/report/reportuseoptout/InNestedSubpackage.java @@ -0,0 +1,3 @@ +package reportuseoptout.sub.nested; + +public class InNestedSubpackage {} diff --git a/framework/tests/report/reportuseoptout/InPackage.java b/framework/tests/report/reportuseoptout/InPackage.java new file mode 100644 index 000000000000..ad9e2b80f4a3 --- /dev/null +++ b/framework/tests/report/reportuseoptout/InPackage.java @@ -0,0 +1,4 @@ +package reportuseoptout; + +// :: error: (usage) +public class InPackage {} diff --git a/framework/tests/report/reportuseoptout/Uses.java b/framework/tests/report/reportuseoptout/Uses.java new file mode 100644 index 000000000000..b6b318e12900 --- /dev/null +++ b/framework/tests/report/reportuseoptout/Uses.java @@ -0,0 +1,8 @@ +public class Uses { + // The annotation always covers the package on which it is written. + // :: error: (usage) + reportuseoptout.InPackage inPackage; + + // applyToSubpackages=false also excludes transitively nested packages. + reportuseoptout.sub.nested.InNestedSubpackage inNestedSubpackage; +} diff --git a/framework/tests/report/reportuseoptout/package-info.java b/framework/tests/report/reportuseoptout/package-info.java new file mode 100644 index 000000000000..6decbad4bf5e --- /dev/null +++ b/framework/tests/report/reportuseoptout/package-info.java @@ -0,0 +1,4 @@ +@ReportUse(applyToSubpackages = false) +package reportuseoptout; + +import org.checkerframework.common.util.report.qual.ReportUse; From 9c5cf9d8b34a0bb545836282c179dae3a574902b Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Tue, 1 Sep 2026 10:50:41 -0400 Subject: [PATCH 2/9] Trigger CI From fbf3c99621e8415b78ce5cc8ad6c822646649666 Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Tue, 1 Sep 2026 11:30:20 -0400 Subject: [PATCH 3/9] Document ReportVisitor constructor --- .../checkerframework/common/util/report/ReportVisitor.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/framework/src/main/java/org/checkerframework/common/util/report/ReportVisitor.java b/framework/src/main/java/org/checkerframework/common/util/report/ReportVisitor.java index 061c5464effd..02e9a3a7f37d 100644 --- a/framework/src/main/java/org/checkerframework/common/util/report/ReportVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/util/report/ReportVisitor.java @@ -62,6 +62,11 @@ public class ReportVisitor extends BaseTypeVisitor { */ private final @Nullable ExecutableElement reportUseApplyToSubpackagesElement; + /** + * Creates a ReportVisitor. + * + * @param checker the checker + */ public ReportVisitor(BaseTypeChecker checker) { super(checker); From 81f505fb72b2def453c1591bf575467250408361 Mon Sep 17 00:00:00 2001 From: Aosen Xiong Date: Tue, 1 Sep 2026 17:37:08 -0400 Subject: [PATCH 4/9] Simplify qualifier parameter package traversal --- .../framework/type/AnnotatedTypeFactory.java | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 3c5b190fd079..89b5f9f03e27 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -5518,24 +5518,20 @@ public AnnotationMirrorSet getQualifierParameterHierarchies(Element element) { element, HasQualifierParameter.class, hasQualifierParameterValueElement)); AnnotationMirrorSet hasQualifierParameterTops = new AnnotationMirrorSet(); PackageElement packageElement = ElementUtils.enclosingPackage(element); - if (packageElement != null) { - // The type's own package always applies; applyToSubpackages does not govern this step. - hasQualifierParameterTops.addAll( - getSupportedAnnotationsInElementAnnotation( - packageElement, - HasQualifierParameter.class, - hasQualifierParameterValueElement)); - packageElement = ElementUtils.parentPackage(packageElement, elements); - while (packageElement != null) { - if (doesHasQualifierParameterApplyToSubpackages(packageElement)) { - hasQualifierParameterTops.addAll( - getSupportedAnnotationsInElementAnnotation( - packageElement, - HasQualifierParameter.class, - hasQualifierParameterValueElement)); - } - packageElement = ElementUtils.parentPackage(packageElement, elements); + // Traverse all packages containing this element. The element's own package always applies; + // an outer package applies only if its annotation applies to subpackages. + boolean isOwnPackage = true; + while (packageElement != null) { + if (isOwnPackage || doesHasQualifierParameterApplyToSubpackages(packageElement)) { + AnnotationMirrorSet packageDefaultTops = + getSupportedAnnotationsInElementAnnotation( + packageElement, + HasQualifierParameter.class, + hasQualifierParameterValueElement); + hasQualifierParameterTops.addAll(packageDefaultTops); } + packageElement = ElementUtils.parentPackage(packageElement, elements); + isOwnPackage = false; } AnnotationMirrorSet noQualifierParamClasses = From eff37dcffd685261f07e3fc73052f802969745a1 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Sat, 5 Sep 2026 10:52:55 -0400 Subject: [PATCH 5/9] Review: drop the classpath NOTEs, share one helper, halve a lookup The three NOTEs about a checker-qual without applyToSubpackages cannot be acted on: if the annotation on the classpath has no such element, no user code can set it and still compile, so the message reports a situation the user cannot be in and cannot fix. They also fire per type factory, so a compound checker prints several per compilation. Remove them; the fields' Javadoc already records the fallback. Fold the three copies of the fallback logic into one static AnnotatedTypeFactory.appliesToSubpackages(AnnotationMirror, ExecutableElement). getQualifierParameterHierarchies called getDeclAnnotation twice per enclosing package: once to decide whether the annotation applies to subpackages, once inside getSupportedAnnotationsInElementAnnotation. Fetch the annotation once and pass it to a new overload. The method is called per type declaration and per cast and is not cached, and getDeclAnnotation is a measured hot path. Also: state what the BaseTypeChecker cache holds and why it is separate from the existing one, and make checkReportUse's package walk say which steps the subpackage rule applies to. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019jLjeEW1F1aE2E3ax7EWqV --- docs/CHANGELOG.md | 26 +++--- .../common/basetype/BaseTypeChecker.java | 34 ++++--- .../common/util/report/ReportVisitor.java | 55 ++++-------- .../framework/type/AnnotatedTypeFactory.java | 88 ++++++++----------- 4 files changed, 83 insertions(+), 120 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index cc71c9dc5c56..2862ea7d47f3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -43,12 +43,12 @@ The shaded jars no longer contain a `module-info.class` or jsr305's Recognition of `javax.annotation.Nullable`, `@Nonnull` and `@CheckForNull` in user code is unaffected. -`AnnotatedFor`, `HasQualifierParameter`, and `ReportUse` support the new -`applyToSubpackages` annotation element, which decides whether an annotation -written on a package also applies to that package's subpackages. To preserve -the current behavior the default is `true`. `DefaultQualifier` already had this -element. When an older `checker-qual` artifact lacks the element, package -annotations retain their previous behavior and apply to subpackages. +`AnnotatedFor`, `HasQualifierParameter`, and `ReportUse` gain the +`applyToSubpackages` element that `DefaultQualifier` already had. It says whether +an annotation written on a package also applies to that package's subpackages, +and defaults to `true`, so existing code is unaffected. Setting it to false limits +only that annotation; an applicable annotation on an enclosing package still +applies. The Checker Framework now issues an `annotation.on.supertype` error when an annotation supported by the checker is written as a main annotation on the superclass or interface in an `extends` or @@ -393,13 +393,13 @@ which `BaseTypeChecker` implements with a cache. **Implementation details:** -New method `AnnotatedTypeFactory.doesAnnotatedForApplyToSubpackages(AnnotationMirror)` -reports whether an `@AnnotatedFor` written on a package also applies to that -package's subpackages. Code that walks up the package chain to find a package -annotation should gate the steps to enclosing packages on the annotation's -`applyToSubpackages` element rather than propagating unconditionally; the -annotated package itself is always in scope. An absent element, as in a -`checker-qual` that predates it, is treated as true for compatibility. +Code that walks up the package chain looking for a package annotation must now gate +each step to an enclosing package on that annotation's `applyToSubpackages` element; +the annotated package itself is always in scope. `AnnotatedTypeFactory` has two new +methods for this: the static `appliesToSubpackages(AnnotationMirror, ExecutableElement)`, +and `doesAnnotatedForApplyToSubpackages(AnnotationMirror)` for `@AnnotatedFor`. A null +element, as in a `checker-qual` that predates it, is treated as true, so a package +annotation from such an artifact applies to subpackages as it always did. `AnnotatedIntersectionType.summarizeBounds` computes the summary described above, reading each bound's qualifier, explicit or defaulted, uniformly, diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeChecker.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeChecker.java index a39273fe93c0..e7e55c7a964b 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeChecker.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeChecker.java @@ -82,10 +82,11 @@ public abstract class BaseTypeChecker extends SourceChecker { new IdentityHashMap<>(); /** - * A mapping from a package to whether an {@code @AnnotatedFor} on that package or an enclosing - * package applies to this checker or an upstream checker and to subpackages. This differs from - * {@link #elementAnnotatedForThisCheckerOrUpstreamCache} because an {@code @AnnotatedFor} that - * opts out of subpackages still covers its own package. + * A mapping from a package to whether that package's subpackages are covered by an + * {@code @AnnotatedFor} for this checker or an upstream checker, written on it or on an + * enclosing package. Separate from {@link #elementAnnotatedForThisCheckerOrUpstreamCache} + * because an {@code @AnnotatedFor} that opts out of subpackages still covers its own package, + * so the two answers differ for the same package. */ private final IdentityHashMap annotatedForReachesSubpackagesCache = new IdentityHashMap<>(); @@ -375,12 +376,13 @@ public boolean isElementAnnotatedForThisCheckerOrUpstreamChecker(@Nullable Eleme } /** - * Returns true if a subpackage of {@code pkg} would be covered by an {@code @AnnotatedFor} on - * {@code pkg} or an enclosing package. + * Returns true if the subpackages of {@code pkg} are covered by an {@code @AnnotatedFor} for + * this checker or an upstream checker. Such an annotation may be written on {@code pkg} itself + * or on any enclosing package: a package that opts out of subpackages does not shield its own + * subpackages from an enclosing package that opts in. * - * @param pkg a package, or null - * @return true if {@code pkg} or an enclosing package has an {@code @AnnotatedFor} for this - * checker or an upstream checker that applies to subpackages + * @param pkg a package, or null for no package + * @return true if an {@code @AnnotatedFor} covers the subpackages of {@code pkg} */ private boolean doesAnnotatedForReachSubpackages(@Nullable PackageElement pkg) { if (pkg == null) { @@ -395,15 +397,11 @@ private boolean doesAnnotatedForReachSubpackages(@Nullable PackageElement pkg) { AnnotatedTypeFactory atypeFactory = getTypeFactory(); AnnotationMirror annotatedFor = atypeFactory.getDeclAnnotation(pkg, AnnotatedFor.class); boolean result = - annotatedFor != null - && atypeFactory.doesAnnotatedForApplyToThisChecker(annotatedFor) - && atypeFactory.doesAnnotatedForApplyToSubpackages(annotatedFor); - if (!result) { - // A package that opts out of subpackages does not block an outer package that opts in. - result = - doesAnnotatedForReachSubpackages( - ElementUtils.parentPackage(pkg, atypeFactory.getElementUtils())); - } + (annotatedFor != null + && atypeFactory.doesAnnotatedForApplyToThisChecker(annotatedFor) + && atypeFactory.doesAnnotatedForApplyToSubpackages(annotatedFor)) + || doesAnnotatedForReachSubpackages( + ElementUtils.parentPackage(pkg, atypeFactory.getElementUtils())); annotatedForReachesSubpackagesCache.put(pkg, result); return result; diff --git a/framework/src/main/java/org/checkerframework/common/util/report/ReportVisitor.java b/framework/src/main/java/org/checkerframework/common/util/report/ReportVisitor.java index 02e9a3a7f37d..6d63096da3fb 100644 --- a/framework/src/main/java/org/checkerframework/common/util/report/ReportVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/util/report/ReportVisitor.java @@ -29,7 +29,6 @@ import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.util.AnnotatedTypes; -import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; @@ -45,7 +44,6 @@ import javax.lang.model.element.Modifier; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; -import javax.tools.Diagnostic; /** The visitor for the Report Checker. */ public class ReportVisitor extends BaseTypeVisitor { @@ -76,15 +74,6 @@ public ReportVisitor(BaseTypeChecker checker) { "applyToSubpackages", 0, checker.getProcessingEnvironment()); - if (reportUseApplyToSubpackagesElement == null) { - checker.message( - Diagnostic.Kind.NOTE, - "The @ReportUse annotation on the classpath does not define the" - + " applyToSubpackages element; package annotations will apply to" - + " subpackages. Use the EISOP checker-qual artifact to control this" - + " behavior."); - } - EnumSet treeKindsTmp = EnumSet.noneOf(Tree.Kind.class); for (String treeKind : checker.getStringsOption("reportTreeKinds", ',')) { treeKindsTmp.add(Tree.Kind.valueOf(treeKind.toUpperCase(Locale.ROOT))); @@ -116,14 +105,16 @@ public Void scan(Tree tree, Void p) { * @param member the element from which to start looking */ private void checkReportUse(Tree tree, Element member) { - Element loop = member; - boolean isParentPackage = false; - while (loop != null) { + // Once the walk moves from a package to its parent, an annotation applies only if it + // applies to subpackages. Everything before that -- the member, its enclosing types, and + // its own package -- is covered by an annotation written on it. + boolean inEnclosingPackage = false; + for (Element loop = member; loop != null; ) { AnnotationMirror reportUse = this.atypeFactory.getDeclAnnotation(loop, ReportUse.class); - boolean report = - reportUse != null - && (!isParentPackage || doesReportUseApplyToSubpackages(reportUse)); - if (report) { + if (reportUse != null + && (!inEnclosingPackage + || AnnotatedTypeFactory.appliesToSubpackages( + reportUse, reportUseApplyToSubpackagesElement))) { checker.reportError( tree, "usage", @@ -132,31 +123,19 @@ private void checkReportUse(Tree tree, Element member) { loop.getKind(), ElementUtils.getQualifiedName(member), member.getKind()); - break; - } else if (loop.getKind() == ElementKind.PACKAGE) { + return; + } + if (loop.getKind() == ElementKind.PACKAGE) { loop = ElementUtils.parentPackage((PackageElement) loop, elements); - isParentPackage = true; - continue; + inEnclosingPackage = true; + } else { + // The enclosing element of a top-level type is its package, so once the walk + // reaches a package it stays in packages until it runs out. + loop = loop.getEnclosingElement(); } - // Package will always be the last iteration. - loop = loop.getEnclosingElement(); } } - /** - * Returns whether the given package-level {@link ReportUse} annotation applies to subpackages. - * - * @param reportUse a {@link ReportUse} annotation - * @return whether {@code reportUse} applies to subpackages - */ - private boolean doesReportUseApplyToSubpackages(AnnotationMirror reportUse) { - // A checker-qual that predates the element gives no way to opt out, so preserve the old - // behavior and treat package annotations as applying to subpackages. - return reportUseApplyToSubpackagesElement == null - || AnnotationUtils.getElementValue( - reportUse, reportUseApplyToSubpackagesElement, Boolean.class, true); - } - /* Would we want this? Seems redundant, as all uses of the imported * package should already be reported. * Also, how do we get an element for the import? diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index 89b5f9f03e27..bc2cd98d3406 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -140,7 +140,6 @@ import javax.lang.model.type.TypeVariable; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; -import javax.tools.Diagnostic; /** * The methods of this class take an element or AST node, and return the annotated type as an {@link @@ -818,14 +817,6 @@ public AnnotatedTypeFactory(BaseTypeChecker checker) { annotatedForApplyToSubpackagesElement = TreeUtils.getMethodOrNull( AnnotatedFor.class, "applyToSubpackages", 0, processingEnv); - if (annotatedForApplyToSubpackagesElement == null) { - checker.message( - Diagnostic.Kind.NOTE, - "The @AnnotatedFor annotation on the classpath does not define the" - + " applyToSubpackages element; package annotations will apply to" - + " subpackages. Use the EISOP checker-qual artifact to control this" - + " behavior."); - } ensuresQualifierExpressionElement = TreeUtils.getMethod(EnsuresQualifier.class, "expression", 0, processingEnv); ensuresQualifierListValueElement = @@ -845,14 +836,6 @@ public AnnotatedTypeFactory(BaseTypeChecker checker) { hasQualifierParameterApplyToSubpackagesElement = TreeUtils.getMethodOrNull( HasQualifierParameter.class, "applyToSubpackages", 0, processingEnv); - if (hasQualifierParameterApplyToSubpackagesElement == null) { - checker.message( - Diagnostic.Kind.NOTE, - "The @HasQualifierParameter annotation on the classpath does not define the" - + " applyToSubpackages element; package annotations will apply to" - + " subpackages. Use the EISOP checker-qual artifact to control this" - + " behavior."); - } methodValClassNameElement = TreeUtils.getMethod(MethodVal.class, "className", 0, processingEnv); methodValMethodNameElement = @@ -5518,17 +5501,20 @@ public AnnotationMirrorSet getQualifierParameterHierarchies(Element element) { element, HasQualifierParameter.class, hasQualifierParameterValueElement)); AnnotationMirrorSet hasQualifierParameterTops = new AnnotationMirrorSet(); PackageElement packageElement = ElementUtils.enclosingPackage(element); - // Traverse all packages containing this element. The element's own package always applies; - // an outer package applies only if its annotation applies to subpackages. + // Traverse all packages containing this element. The element's own package always + // applies; an enclosing package applies only if its annotation applies to subpackages. boolean isOwnPackage = true; while (packageElement != null) { - if (isOwnPackage || doesHasQualifierParameterApplyToSubpackages(packageElement)) { - AnnotationMirrorSet packageDefaultTops = - getSupportedAnnotationsInElementAnnotation( - packageElement, - HasQualifierParameter.class, - hasQualifierParameterValueElement); - hasQualifierParameterTops.addAll(packageDefaultTops); + AnnotationMirror hasQualifierParameter = + getDeclAnnotation(packageElement, HasQualifierParameter.class); + if (hasQualifierParameter != null + && (isOwnPackage + || appliesToSubpackages( + hasQualifierParameter, + hasQualifierParameterApplyToSubpackagesElement))) { + hasQualifierParameterTops.addAll( + getSupportedAnnotationsInAnnotation( + hasQualifierParameter, hasQualifierParameterValueElement)); } packageElement = ElementUtils.parentPackage(packageElement, elements); isOwnPackage = false; @@ -5547,27 +5533,21 @@ public AnnotationMirrorSet getQualifierParameterHierarchies(Element element) { } /** - * Returns whether {@code packageElement}'s {@code @HasQualifierParameter} applies to + * Returns whether an annotation written on a package also applies to that package's * subpackages. * - * @param packageElement a package - * @return true if {@code packageElement} is annotated with a {@code @HasQualifierParameter} - * that applies to subpackages + * @param anno an annotation written on a package + * @param applyToSubpackagesElement {@code anno}'s {@code applyToSubpackages} element, or null + * if the {@code checker-qual} on the classpath predates that element + * @return true if {@code anno} applies to subpackages */ - private boolean doesHasQualifierParameterApplyToSubpackages(PackageElement packageElement) { - AnnotationMirror hasQualifierParameter = - getDeclAnnotation(packageElement, HasQualifierParameter.class); - if (hasQualifierParameter == null) { - return false; - } - // A checker-qual that predates the applyToSubpackages element gives no way to opt out, so - // treat the annotation as applying to subpackages. - return hasQualifierParameterApplyToSubpackagesElement == null + public static boolean appliesToSubpackages( + AnnotationMirror anno, @Nullable ExecutableElement applyToSubpackagesElement) { + // A checker-qual without the element gives no way to opt out, so an annotation from it + // applies to subpackages, as it always did. + return applyToSubpackagesElement == null || AnnotationUtils.getElementValue( - hasQualifierParameter, - hasQualifierParameterApplyToSubpackagesElement, - Boolean.class, - true); + anno, applyToSubpackagesElement, Boolean.class, true); } /** @@ -5596,7 +5576,20 @@ private AnnotationMirrorSet getSupportedAnnotationsInElementAnnotation( if (annotation == null) { return AnnotationMirrorSet.emptySet(); } + return getSupportedAnnotationsInAnnotation(annotation, valueElement); + } + /** + * Returns the supported annotation mirrors named by {@code valueElement} of {@code annotation}. + * The same as {@link #getSupportedAnnotationsInElementAnnotation}, for a caller that already + * holds the annotation. + * + * @param annotation an annotation whose {@code valueElement} names annotation classes + * @param valueElement the element of {@code annotation} whose value is a list of classes + * @return the supported annotations named by {@code valueElement} + */ + private AnnotationMirrorSet getSupportedAnnotationsInAnnotation( + AnnotationMirror annotation, ExecutableElement valueElement) { AnnotationMirrorSet found = new AnnotationMirrorSet(); List<@CanonicalName Name> qualClasses = AnnotationUtils.getElementValueClassNames(annotation, valueElement); @@ -6925,14 +6918,7 @@ protected void makeConditionConsistentWithOtherMethod( * @return whether {@code annotatedForAnno} applies to subpackages */ public boolean doesAnnotatedForApplyToSubpackages(AnnotationMirror annotatedForAnno) { - // A checker-qual that predates the applyToSubpackages element gives no way to opt out, so - // treat the annotation as applying to subpackages. - return annotatedForApplyToSubpackagesElement == null - || AnnotationUtils.getElementValue( - annotatedForAnno, - annotatedForApplyToSubpackagesElement, - Boolean.class, - true); + return appliesToSubpackages(annotatedForAnno, annotatedForApplyToSubpackagesElement); } /** From e5b3d6ffa7550dbadfea5411d15cab9a1cb8869c Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Mon, 7 Sep 2026 07:35:51 -0400 Subject: [PATCH 6/9] Move appliesToSubpackages to AnnotationUtils It reads no AnnotatedTypeFactory state: given an annotation and one of its elements, it reports whether the annotation applies to subpackages. That is an annotation utility, and it sat on the type factory only because its first caller was there. ReportVisitor, which holds an atypeFactory of its own, had to call it as a static on the factory class to get at it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019jLjeEW1F1aE2E3ax7EWqV --- docs/CHANGELOG.md | 11 +++++---- .../common/util/report/ReportVisitor.java | 3 ++- .../framework/type/AnnotatedTypeFactory.java | 23 +++---------------- .../javacutil/AnnotationUtils.java | 17 ++++++++++++++ 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f617db9c2000..5aafb6cb30d7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -417,11 +417,12 @@ diagnostics is done by installing a `DiagnosticSink`, not by overriding Code that walks up the package chain looking for a package annotation must now gate each step to an enclosing package on that annotation's `applyToSubpackages` element; -the annotated package itself is always in scope. `AnnotatedTypeFactory` has two new -methods for this: the static `appliesToSubpackages(AnnotationMirror, ExecutableElement)`, -and `doesAnnotatedForApplyToSubpackages(AnnotationMirror)` for `@AnnotatedFor`. A null -element, as in a `checker-qual` that predates it, is treated as true, so a package -annotation from such an artifact applies to subpackages as it always did. +the annotated package itself is always in scope. There are two new methods for this: +`AnnotationUtils.appliesToSubpackages(AnnotationMirror, ExecutableElement)`, and +`AnnotatedTypeFactory.doesAnnotatedForApplyToSubpackages(AnnotationMirror)` for +`@AnnotatedFor`. A null element, as in a `checker-qual` that predates it, is treated +as true, so a package annotation from such an artifact applies to subpackages as it +always did. `AnnotatedIntersectionType.summarizeBounds` computes the summary described above, reading each bound's qualifier, explicit or defaulted, uniformly, diff --git a/framework/src/main/java/org/checkerframework/common/util/report/ReportVisitor.java b/framework/src/main/java/org/checkerframework/common/util/report/ReportVisitor.java index 6d63096da3fb..3249d29be8a7 100644 --- a/framework/src/main/java/org/checkerframework/common/util/report/ReportVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/util/report/ReportVisitor.java @@ -29,6 +29,7 @@ import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.util.AnnotatedTypes; +import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; @@ -113,7 +114,7 @@ private void checkReportUse(Tree tree, Element member) { AnnotationMirror reportUse = this.atypeFactory.getDeclAnnotation(loop, ReportUse.class); if (reportUse != null && (!inEnclosingPackage - || AnnotatedTypeFactory.appliesToSubpackages( + || AnnotationUtils.appliesToSubpackages( reportUse, reportUseApplyToSubpackagesElement))) { checker.reportError( tree, diff --git a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java index bc2cd98d3406..959e188605a3 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/AnnotatedTypeFactory.java @@ -5509,7 +5509,7 @@ public AnnotationMirrorSet getQualifierParameterHierarchies(Element element) { getDeclAnnotation(packageElement, HasQualifierParameter.class); if (hasQualifierParameter != null && (isOwnPackage - || appliesToSubpackages( + || AnnotationUtils.appliesToSubpackages( hasQualifierParameter, hasQualifierParameterApplyToSubpackagesElement))) { hasQualifierParameterTops.addAll( @@ -5532,24 +5532,6 @@ public AnnotationMirrorSet getQualifierParameterHierarchies(Element element) { return found; } - /** - * Returns whether an annotation written on a package also applies to that package's - * subpackages. - * - * @param anno an annotation written on a package - * @param applyToSubpackagesElement {@code anno}'s {@code applyToSubpackages} element, or null - * if the {@code checker-qual} on the classpath predates that element - * @return true if {@code anno} applies to subpackages - */ - public static boolean appliesToSubpackages( - AnnotationMirror anno, @Nullable ExecutableElement applyToSubpackagesElement) { - // A checker-qual without the element gives no way to opt out, so an annotation from it - // applies to subpackages, as it always did. - return applyToSubpackagesElement == null - || AnnotationUtils.getElementValue( - anno, applyToSubpackagesElement, Boolean.class, true); - } - /** * Returns a set of supported annotation mirrors corresponding to the annotation classes listed * in the value element of an annotation with class {@code annoClass} on {@code element}. @@ -6918,7 +6900,8 @@ protected void makeConditionConsistentWithOtherMethod( * @return whether {@code annotatedForAnno} applies to subpackages */ public boolean doesAnnotatedForApplyToSubpackages(AnnotationMirror annotatedForAnno) { - return appliesToSubpackages(annotatedForAnno, annotatedForApplyToSubpackagesElement); + return AnnotationUtils.appliesToSubpackages( + annotatedForAnno, annotatedForApplyToSubpackagesElement); } /** diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java index 20e4a18b076a..40d559c30b45 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java @@ -968,6 +968,23 @@ public static T getElementValue( } } + /** + * Returns whether an annotation written on a package also applies to that package's + * subpackages. + * + * @param anno an annotation written on a package + * @param applyToSubpackagesElement {@code anno}'s own {@code applyToSubpackages} element, or + * null if the {@code checker-qual} on the classpath predates that element + * @return true if {@code anno} applies to subpackages + */ + public static boolean appliesToSubpackages( + AnnotationMirror anno, @Nullable ExecutableElement applyToSubpackagesElement) { + // A checker-qual without the element gives no way to opt out, so an annotation from it + // applies to subpackages, as it always did. + return applyToSubpackagesElement == null + || getElementValue(anno, applyToSubpackagesElement, Boolean.class, true); + } + /** * Get the given boolean element of the annotation {@code anno}. * From 3cb2f6df049a6906ff5e5686618f207f2e8b11d5 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Mon, 7 Sep 2026 07:55:29 -0400 Subject: [PATCH 7/9] Test that a subpackage opt-out does not block an enclosing package All three annotations document that applyToSubpackages=false limits only the annotation it is written on, and that an applicable annotation on an enclosing package still applies. All three implement it as a "keep walking" where a simpler reading would stop: the || recursion in BaseTypeChecker.doesAnnotatedForReachSubpackages, the continued loop over enclosing packages in AnnotatedTypeFactory, and the continued walk in ReportVisitor.checkReportUse. Nothing tested it. Every existing test directory holds a single package-info.java, so no test had an opt-out nested inside an opt-in, and all three could have been "simplified" to stop at the first annotated package with the suite still green. These are jtreg rather than JUnit tests because the scenario needs two package-info.java files in one compilation, and the per-directory harness compiles each directory separately -- so the two cannot be in one directory, and javac rejects package annotations in a file named anything else. Each test was confirmed to fail when the enclosing package also opts out. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019jLjeEW1F1aE2E3ax7EWqV --- .../jtreg/subpackages/AnnotatedForNested.java | 9 +++++++++ .../jtreg/subpackages/AnnotatedForNested.out | 4 ++++ .../HasQualifierParameterNested.java | 8 ++++++++ checker/jtreg/subpackages/af/package-info.java | 4 ++++ checker/jtreg/subpackages/af/sub/deep/Deep.java | 17 +++++++++++++++++ .../jtreg/subpackages/af/sub/package-info.java | 4 ++++ checker/jtreg/subpackages/hqp/package-info.java | 5 +++++ .../jtreg/subpackages/hqp/sub/deep/Deep.java | 13 +++++++++++++ .../jtreg/subpackages/hqp/sub/package-info.java | 5 +++++ .../jtreg/subpackages/ReportUseNested.java | 8 ++++++++ framework/jtreg/subpackages/ReportUseNested.out | 3 +++ .../jtreg/subpackages/ru/package-info.java | 4 ++++ .../jtreg/subpackages/ru/sub/deep/Deep.java | 8 ++++++++ .../jtreg/subpackages/ru/sub/package-info.java | 4 ++++ 14 files changed, 96 insertions(+) create mode 100644 checker/jtreg/subpackages/AnnotatedForNested.java create mode 100644 checker/jtreg/subpackages/AnnotatedForNested.out create mode 100644 checker/jtreg/subpackages/HasQualifierParameterNested.java create mode 100644 checker/jtreg/subpackages/af/package-info.java create mode 100644 checker/jtreg/subpackages/af/sub/deep/Deep.java create mode 100644 checker/jtreg/subpackages/af/sub/package-info.java create mode 100644 checker/jtreg/subpackages/hqp/package-info.java create mode 100644 checker/jtreg/subpackages/hqp/sub/deep/Deep.java create mode 100644 checker/jtreg/subpackages/hqp/sub/package-info.java create mode 100644 framework/jtreg/subpackages/ReportUseNested.java create mode 100644 framework/jtreg/subpackages/ReportUseNested.out create mode 100644 framework/jtreg/subpackages/ru/package-info.java create mode 100644 framework/jtreg/subpackages/ru/sub/deep/Deep.java create mode 100644 framework/jtreg/subpackages/ru/sub/package-info.java diff --git a/checker/jtreg/subpackages/AnnotatedForNested.java b/checker/jtreg/subpackages/AnnotatedForNested.java new file mode 100644 index 000000000000..c70bbf840f9b --- /dev/null +++ b/checker/jtreg/subpackages/AnnotatedForNested.java @@ -0,0 +1,9 @@ +/* + * @test + * @summary An AnnotatedFor with applyToSubpackages=false limits only its own annotation. An + * enclosing package whose annotation applies to subpackages still reaches through it, so code in + * the nested subpackage is checked rather than given conservative defaults. + * + * @compile/fail/ref=AnnotatedForNested.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -AuseConservativeDefaultsForUncheckedCode=source,bytecode af/package-info.java af/sub/package-info.java af/sub/deep/Deep.java + */ +public class AnnotatedForNested {} diff --git a/checker/jtreg/subpackages/AnnotatedForNested.out b/checker/jtreg/subpackages/AnnotatedForNested.out new file mode 100644 index 000000000000..36a9fdd43786 --- /dev/null +++ b/checker/jtreg/subpackages/AnnotatedForNested.out @@ -0,0 +1,4 @@ +Deep.java:15:14: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter nn of Deep.take. +found : @Nullable Object +required: @NonNull Object +1 error diff --git a/checker/jtreg/subpackages/HasQualifierParameterNested.java b/checker/jtreg/subpackages/HasQualifierParameterNested.java new file mode 100644 index 000000000000..aee4a3b916c8 --- /dev/null +++ b/checker/jtreg/subpackages/HasQualifierParameterNested.java @@ -0,0 +1,8 @@ +/* + * @test + * @summary A HasQualifierParameter with applyToSubpackages=false limits only its own annotation. + * An enclosing package whose annotation applies to subpackages still reaches through it. + * + * @compile -processor org.checkerframework.checker.tainting.TaintingChecker -Werror hqp/package-info.java hqp/sub/package-info.java hqp/sub/deep/Deep.java + */ +public class HasQualifierParameterNested {} diff --git a/checker/jtreg/subpackages/af/package-info.java b/checker/jtreg/subpackages/af/package-info.java new file mode 100644 index 000000000000..db01de56626e --- /dev/null +++ b/checker/jtreg/subpackages/af/package-info.java @@ -0,0 +1,4 @@ +@AnnotatedFor("nullness") +package af; + +import org.checkerframework.framework.qual.AnnotatedFor; diff --git a/checker/jtreg/subpackages/af/sub/deep/Deep.java b/checker/jtreg/subpackages/af/sub/deep/Deep.java new file mode 100644 index 000000000000..bafb339edfa5 --- /dev/null +++ b/checker/jtreg/subpackages/af/sub/deep/Deep.java @@ -0,0 +1,17 @@ +package af.sub.deep; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Package af.sub sets applyToSubpackages=false, which limits its own annotation to af.sub. It does + * not block package af, whose annotation applies to subpackages and so still reaches here. This + * code is therefore inside an AnnotatedFor scope and its warnings are issued; if the walk up the + * package chain stopped at af.sub, conservative defaults would suppress them. + */ +public class Deep { + void take(Object nn) {} + + void m(@Nullable Object nble) { + take(nble); + } +} diff --git a/checker/jtreg/subpackages/af/sub/package-info.java b/checker/jtreg/subpackages/af/sub/package-info.java new file mode 100644 index 000000000000..0498d465e285 --- /dev/null +++ b/checker/jtreg/subpackages/af/sub/package-info.java @@ -0,0 +1,4 @@ +@AnnotatedFor(value = "nullness", applyToSubpackages = false) +package af.sub; + +import org.checkerframework.framework.qual.AnnotatedFor; diff --git a/checker/jtreg/subpackages/hqp/package-info.java b/checker/jtreg/subpackages/hqp/package-info.java new file mode 100644 index 000000000000..951717241071 --- /dev/null +++ b/checker/jtreg/subpackages/hqp/package-info.java @@ -0,0 +1,5 @@ +@HasQualifierParameter(Tainted.class) +package hqp; + +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.framework.qual.HasQualifierParameter; diff --git a/checker/jtreg/subpackages/hqp/sub/deep/Deep.java b/checker/jtreg/subpackages/hqp/sub/deep/Deep.java new file mode 100644 index 000000000000..838cd551ab61 --- /dev/null +++ b/checker/jtreg/subpackages/hqp/sub/deep/Deep.java @@ -0,0 +1,13 @@ +package hqp.sub.deep; + +import org.checkerframework.checker.tainting.qual.PolyTainted; + +/** + * Package hqp.sub sets applyToSubpackages=false, which limits its own annotation to hqp.sub. It + * does not block package hqp, whose annotation applies to subpackages and so still reaches here. + * The class therefore has a qualifier parameter and the polymorphic qualifier is allowed; if the + * walk up the package chain stopped at hqp.sub, this would be invalid.polymorphic.qualifier.use. + */ +public class Deep { + @PolyTainted int field; +} diff --git a/checker/jtreg/subpackages/hqp/sub/package-info.java b/checker/jtreg/subpackages/hqp/sub/package-info.java new file mode 100644 index 000000000000..02fbc2aee00d --- /dev/null +++ b/checker/jtreg/subpackages/hqp/sub/package-info.java @@ -0,0 +1,5 @@ +@HasQualifierParameter(value = Tainted.class, applyToSubpackages = false) +package hqp.sub; + +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.framework.qual.HasQualifierParameter; diff --git a/framework/jtreg/subpackages/ReportUseNested.java b/framework/jtreg/subpackages/ReportUseNested.java new file mode 100644 index 000000000000..f8f701afa9e9 --- /dev/null +++ b/framework/jtreg/subpackages/ReportUseNested.java @@ -0,0 +1,8 @@ +/* + * @test + * @summary A ReportUse with applyToSubpackages=false limits only its own annotation. An enclosing + * package whose annotation applies to subpackages still reaches through it. + * + * @compile/fail/ref=ReportUseNested.out -XDrawDiagnostics -processor org.checkerframework.common.util.report.ReportChecker ru/package-info.java ru/sub/package-info.java ru/sub/deep/Deep.java + */ +public class ReportUseNested {} diff --git a/framework/jtreg/subpackages/ReportUseNested.out b/framework/jtreg/subpackages/ReportUseNested.out new file mode 100644 index 000000000000..72306ac32c6e --- /dev/null +++ b/framework/jtreg/subpackages/ReportUseNested.out @@ -0,0 +1,3 @@ +Deep.java:8:8: compiler.err.proc.messager: [usage] Usage of ru [PACKAGE] by ru.sub.deep.Deep [CLASS] +Deep.java:8:8: compiler.err.proc.messager: [usage] Usage of ru [PACKAGE] by ru.sub.deep.Deep [CLASS] +2 errors diff --git a/framework/jtreg/subpackages/ru/package-info.java b/framework/jtreg/subpackages/ru/package-info.java new file mode 100644 index 000000000000..5967008992df --- /dev/null +++ b/framework/jtreg/subpackages/ru/package-info.java @@ -0,0 +1,4 @@ +@ReportUse +package ru; + +import org.checkerframework.common.util.report.qual.ReportUse; diff --git a/framework/jtreg/subpackages/ru/sub/deep/Deep.java b/framework/jtreg/subpackages/ru/sub/deep/Deep.java new file mode 100644 index 000000000000..ce85c36b2b9b --- /dev/null +++ b/framework/jtreg/subpackages/ru/sub/deep/Deep.java @@ -0,0 +1,8 @@ +package ru.sub.deep; + +/** + * Package ru.sub sets applyToSubpackages=false, which limits its own annotation to ru.sub. It does + * not block package ru, whose annotation applies to subpackages and so still reaches here, so this + * class is reported. + */ +public class Deep {} diff --git a/framework/jtreg/subpackages/ru/sub/package-info.java b/framework/jtreg/subpackages/ru/sub/package-info.java new file mode 100644 index 000000000000..baaa3969e497 --- /dev/null +++ b/framework/jtreg/subpackages/ru/sub/package-info.java @@ -0,0 +1,4 @@ +@ReportUse(applyToSubpackages = false) +package ru.sub; + +import org.checkerframework.common.util.report.qual.ReportUse; From 5afa89e64117f722b9452b58c4f541863a3ce88e Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Mon, 7 Sep 2026 09:09:44 -0400 Subject: [PATCH 8/9] Add eisop#1990 to the closed issues list The pull request description says it closes the issue, and the convention is that the closing pull request lists it rather than leaving it to a backfill. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019jLjeEW1F1aE2E3ax7EWqV --- docs/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5aafb6cb30d7..9496c9063f6b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -781,7 +781,8 @@ eisop#104, eisop#386, eisop#433, eisop#622, eisop#737, eisop#778, eisop#786, eisop#792, eisop#863, eisop#949, eisop#1015, eisop#1059, eisop#1074, eisop#1244, eisop#1315, eisop#1564, eisop#1592, eisop#1642, eisop#1653, eisop#1735, eisop#1801, eisop#1818, eisop#1819, eisop#1861, eisop#1862, eisop#1863, -eisop#1865, eisop#1887, eisop#1965, eisop#1987, typetools#399, typetools#3203. +eisop#1865, eisop#1887, eisop#1965, eisop#1987, eisop#1990, typetools#399, +typetools#3203. Version 3.49.5-eisop1 (April 26, 2026) From 209d44971f9a0a8d1e365afb74ce0bbe6e66ca11 Mon Sep 17 00:00:00 2001 From: Werner Dietl Date: Mon, 7 Sep 2026 15:19:39 -0400 Subject: [PATCH 9/9] Write @NoQualifierParameter consistently in one paragraph The paragraph named the annotation twice, three lines apart, once with the @ and once without. Both spellings predate this branch, but the second is on a line this branch rewrapped, so fix it here. Reported by Copilot on eisop/jdk#138, which carries the same text. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019jLjeEW1F1aE2E3ax7EWqV --- .../checkerframework/framework/qual/HasQualifierParameter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checker-qual/src/main/java/org/checkerframework/framework/qual/HasQualifierParameter.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/HasQualifierParameter.java index bef795ba87e4..7f1cb3c6eed5 100644 --- a/checker-qual/src/main/java/org/checkerframework/framework/qual/HasQualifierParameter.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/HasQualifierParameter.java @@ -67,7 +67,7 @@ * field to false to limit it to the package itself; doing so does not block an applicable * {@code @HasQualifierParameter} on an enclosing package. It can be disabled on a specific class * and its subclasses by writing {@code @NoQualifierParameter} on that class. This annotation may - * not be written on the same class as {@code NoQualifierParameter} for the same hierarchy. + * not be written on the same class as {@code @NoQualifierParameter} for the same hierarchy. * * @see NoQualifierParameter */