Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
59d2b4b
Don't write irrelevant annotations in .ajava files
mernst Oct 22, 2023
5e51863
Remove nonsensical annotations
mernst Oct 22, 2023
6b54b61
Early exit, and handle varargs
mernst Oct 22, 2023
b9a34bc
Merge ../checker-framework-branch-master into dont-insert-irrelevant
mernst Oct 23, 2023
b947f16
Remove `@RelevantJavaTypes` annotation from `SameLenChecker`
mernst Oct 23, 2023
f1b7064
Merge ../checker-framework-fork-mernst-branch-SameLenChecker-Relevant…
mernst Oct 23, 2023
9f4523b
Undo a change
mernst Oct 23, 2023
8197f24
Update test
mernst Oct 23, 2023
62e0f64
Merge ../checker-framework-fork-mernst-branch-SameLenChecker-Relevant…
mernst Oct 23, 2023
64e3dc1
Merge ../checker-framework-branch-master into dont-insert-irrelevant
mernst Mar 3, 2026
e1882db
Merge ../checker-framework-branch-master into dont-insert-irrelevant
mernst Mar 3, 2026
63f507c
Merge ../checker-framework-branch-master into dont-insert-irrelevant
mernst Mar 4, 2026
f0eeae5
Merge ../checker-framework-branch-master into dont-insert-irrelevant
mernst Mar 15, 2026
bfa0838
Merge ../checker-framework-branch-master into dont-insert-irrelevant
mernst Mar 20, 2026
cc36287
Merge ../checker-framework-branch-master into dont-insert-irrelevant
mernst Mar 29, 2026
92197b3
Merge ../checker-framework-branch-master into dont-insert-irrelevant
mernst Apr 19, 2026
3104592
Merge ../checker-framework-branch-master into dont-insert-irrelevant
mernst May 3, 2026
56fcd9f
Update comment
mernst May 3, 2026
b9bb730
Code style
mernst May 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,3 @@ annotation @G: @Retention(RUNTIME)
boolean[] fieldC
int fieldD
int fieldE

Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,15 @@
import com.github.javaparser.ast.expr.ObjectCreationExpr;
import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import com.github.javaparser.ast.type.ArrayType;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.ast.type.IntersectionType;
import com.github.javaparser.ast.type.PrimitiveType;
import com.github.javaparser.ast.type.Type;
import com.github.javaparser.ast.type.TypeParameter;
import com.github.javaparser.ast.type.UnionType;
import com.github.javaparser.ast.type.VarType;
import com.github.javaparser.ast.type.WildcardType;
import com.github.javaparser.ast.visitor.CloneVisitor;
import com.github.javaparser.ast.visitor.VoidVisitor;
import com.github.javaparser.printer.DefaultPrettyPrinter;
Expand Down Expand Up @@ -1094,6 +1100,63 @@ public void writeResultsToFile(OutputFormat outputFormat, BaseTypeChecker checke
modifiedFiles.clear();
}

/**
* Returns true if the annotation is relevant (where it appears in the program). This
* implementation is conservative and only returns false if the qualifier is definitely not
* relevant.
*
* @param anno an annotation
* @return true if the annotation might be relevant
*/
boolean annotationIsRelevant(AnnotationExpr anno) {
GenericAnnotatedTypeFactory<?, ?, ?, ?> gatf = (GenericAnnotatedTypeFactory) atypeFactory;
if (gatf.relevantJavaTypes == null) {
return true;
}

// `aname` is a fully-qualified name.
String aName = anno.getNameAsString();
if (!atypeFactory.isSupportedQualifier(aName)) {
// The annotation might be a declaration qualifier, such as a side effect specification.
return true;
}
Node parentNode = anno.getParentNode().get();

if (parentNode instanceof ArrayType) {
return gatf.arraysAreRelevant();
}
if (parentNode instanceof ClassOrInterfaceType classType) {
String simpleName = classType.getName().toString();
String scopedName = classType.getNameWithScope();
// TODO: Do I need to remove type parameters?
return gatf.isRelevant(simpleName) || gatf.isRelevant(scopedName);
}
if (parentNode instanceof IntersectionType) {
return true; // TODO
}
if (parentNode instanceof Parameter param) {
if (param.isVarArgs()) {
return gatf.arraysAreRelevant();
} else {
throw new Error("Unexpected type annotation on non-varargs Parameter: " + param);
}
}
if (parentNode instanceof PrimitiveType) {
return gatf.isRelevant(parentNode.toString());
}
if (parentNode instanceof UnionType) {
return true; // TODO
}
if (parentNode instanceof VarType) {
return gatf.nonprimitivesAreRelevant();
}
if (parentNode instanceof WildcardType) {
return true; // TODO
}

throw new Error("What parent? " + parentNode.getClass().getSimpleName() + " " + parentNode);
}
Comment on lines +1123 to +1158
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Avoid hard failures in relevance filtering during AJAVA writing.

At Line 1123 and Line 1159, annotationIsRelevant can throw and abort file generation (anno.getParentNode().get() and throw new Error(...)). This method is documented as conservative; unknown contexts should default to “relevant” instead of crashing.

💡 Suggested fix
-    Node parentNode = anno.getParentNode().get();
+    Optional<Node> maybeParentNode = anno.getParentNode();
+    if (!maybeParentNode.isPresent()) {
+      // Be conservative: if context is unknown, keep the annotation.
+      return true;
+    }
+    Node parentNode = maybeParentNode.get();
...
-      } else {
-        throw new Error("Unexpected type annotation on non-varargs Parameter: " + param);
-      }
+      }
+      // Conservative fallback for non-varargs parameter-level placement.
+      return true;
...
-    throw new Error("What parent? " + parentNode.getClass().getSimpleName() + " " + parentNode);
+    // Conservative fallback for unhandled JavaParser parent node kinds.
+    return true;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java`
around lines 1123 - 1160, annotationIsRelevant currently calls
anno.getParentNode().get() (which can throw NoSuchElementException) and uses
throw new Error(...) for unknown parent cases; change it to be conservative by
replacing anno.getParentNode().get() with a safe Optional check (e.g.,
anno.getParentNode().orElse(null)) and if parentNode is null return true, change
the Parameter non-varargs branch (currently throwing) to return true instead of
throwing, and replace the final throw new Error(...) with a return true so any
unrecognized parent types are treated as relevant rather than aborting AJAVA
writing.


/**
* Write an ajava file to disk.
*
Expand All @@ -1109,7 +1172,9 @@ private void writeAjavaFile(Path outputPath, CompilationUnitAnnos root) {
// and crashes when adding annotations in certain locations.
// LexicalPreservingPrinter.print(root.declaration, writer);

// Do not print invisible qualifiers, to avoid cluttering the output.
// To avoid cluttering the output, do not print:
// * invisible qualifiers
// * irrelevant qualifiers.
Set<String> invisibleQualifierNames = getInvisibleQualifierNames(this.atypeFactory);
DefaultPrettyPrinter prettyPrinter =
new DefaultPrettyPrinter() {
Expand All @@ -1122,6 +1187,9 @@ public void visit(MarkerAnnotationExpr n, Void arg) {
if (invisibleQualifierNames.contains(n.getName().toString())) {
return;
}
if (!annotationIsRelevant(n)) {
return;
}
super.visit(n, arg);
}

Expand All @@ -1130,6 +1198,9 @@ public void visit(SingleMemberAnnotationExpr n, Void arg) {
if (invisibleQualifierNames.contains(n.getName().toString())) {
return;
}
if (!annotationIsRelevant(n)) {
return;
}
super.visit(n, arg);
}

Expand All @@ -1138,6 +1209,9 @@ public void visit(NormalAnnotationExpr n, Void arg) {
if (invisibleQualifierNames.contains(n.getName().toString())) {
return;
}
if (!annotationIsRelevant(n)) {
return;
}
super.visit(n, arg);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,15 +190,26 @@ public abstract class GenericAnnotatedTypeFactory<
* <p>Although a {@code Class<?>} object exists for every element, this does not contain those
* {@code Class<?>} objects because the elements will be compared to TypeMirrors for which Class
* objects may not exist (they might not be on the classpath).
*
* <p>For their names, see {@link #relevantJavaTypeNames}.
*/
public final @Nullable Set<TypeMirror> relevantJavaTypes;

/**
* True if users may write type annotations on arrays. Ignored unless {@link #relevantJavaTypes}
* is non-null.
*/
public final @Nullable Set<String> relevantJavaTypeNames;

/** Whether users may write type annotations on arrays. */
protected final boolean arraysAreRelevant;

/**
* Whether users may write type annotations on non-primitives (classes, arrays, etc.). This is
* redundant with the value of {@link #relevantJavaTypes} but is included for efficiency.
*/
protected final boolean nonprimitivesAreRelevant;

// Flow related fields

/** Should flow be used by default? */
Expand Down Expand Up @@ -368,16 +379,21 @@ protected GenericAnnotatedTypeFactory(BaseTypeChecker checker, boolean useFlow)
checker.getClass().getAnnotation(RelevantJavaTypes.class);
if (relevantJavaTypesAnno == null) {
this.relevantJavaTypes = null;
this.relevantJavaTypeNames = null;
this.arraysAreRelevant = true;
this.nonprimitivesAreRelevant = true;
} else {
Types types = getChecker().getTypeUtils();
Elements elements = getElementUtils();
Class<?>[] classes = relevantJavaTypesAnno.value();
Set<TypeMirror> relevantJavaTypesTemp = new HashSet<>(MapsP.mapCapacity(classes.length));
Set<String> relevantJavaTypeNamesTemp = new HashSet<>(MapsP.mapCapacity(classes.length));
boolean arraysAreRelevantTemp = false;
boolean nonprimitivesAreRelevantTemp = false;
for (Class<?> clazz : classes) {
if (clazz == Object[].class) {
arraysAreRelevantTemp = true;
nonprimitivesAreRelevantTemp = true;
} else if (clazz.isArray()) {
throw new TypeSystemError(
"Don't use arrays other than Object[] in @RelevantJavaTypes on "
Expand All @@ -386,10 +402,24 @@ protected GenericAnnotatedTypeFactory(BaseTypeChecker checker, boolean useFlow)
TypeMirror relevantType = TypesUtils.typeFromClass(clazz, types, elements);
TypeMirror erased = types.erasure(relevantType);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

relevantJavaTypesNames, at least, is only used by WPI (i.e., in this PR). That means that most (all?) of this computation is wasted for non-WPI runs of the checker, which makes me a bit uncomfortable: it does not seem reasonable to slow down the checker in the non-WPI case to speed it up in the WPI case. Can we guard some of this work behind a check that WPI is actually running?

I think this will also complicate the specification of relevantJavaTypeNames, which will need to say something like "always null if WPI is not enabled"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 71 instances of extends BaseTypeChecker in the Checker Framework (excluding checkers used only for testing). Of those:

58 have 0 classes in a @RelevantJavaTypes annotation
5 have 1 class in a @RelevantJavaTypes annotation
2 have 3 classes in a @RelevantJavaTypes annotation
1 has 6 classes in a @RelevantJavaTypes annotation
1 has 8 classes in a @RelevantJavaTypes annotation
4 have 10 classes in a @RelevantJavaTypes annotation

So, I don't think this is a heavy cost, either in time to compute the strings nor in space to store the strings.
But if @smillst agrees with the efficiency concern, I will implement it.

relevantJavaTypesTemp.add(erased);
String typeString = erased.toString();
relevantJavaTypeNamesTemp.add(typeString);
if (clazz.isPrimitive()) {
nonprimitivesAreRelevantTemp = true;
} else {
Comment on lines +407 to +409
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

nonprimitivesAreRelevant is computed with inverted logic.

At Line 406-408, the code sets nonprimitivesAreRelevantTemp = true for primitive classes. That contradicts the field’s meaning and causes incorrect filtering outcomes downstream (notably for var/non-primitive contexts).

💡 Suggested fix
-          if (clazz.isPrimitive()) {
-            nonprimitivesAreRelevantTemp = true;
-          } else {
+          if (!clazz.isPrimitive()) {
+            nonprimitivesAreRelevantTemp = true;
             int dotIndex = typeString.lastIndexOf('.');
             if (dotIndex != -1) {
               // It's a fully-qualified name.  Add the simple name as well.
               // TODO: This might not handle a user writing a nested class like "Map.Entry".
               relevantJavaTypeNamesTemp.add(typeString.substring(dotIndex + 1));
             }
           }

Also applies to: 421-421

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java`
around lines 406 - 408, The variable nonprimitivesAreRelevantTemp is being set
with inverted logic when checking clazz.isPrimitive(): instead of setting it
true for primitive classes, set nonprimitivesAreRelevantTemp = false when
clazz.isPrimitive() and true otherwise (i.e., flip the assignment around the
clazz.isPrimitive() check) so the field nonprimitivesAreRelevant correctly
reflects that non-primitives are relevant; apply the same fix to the other
occurrence updating nonprimitivesAreRelevantTemp at the second site (the other
clazz.isPrimitive() branch).

int dotIndex = typeString.lastIndexOf('.');
if (dotIndex != -1) {
// It's a fully-qualified name. Add the simple name as well.
// TODO: This might not handle a user writing a nested class like "Map.Entry".
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the TODO comments here and about handling type variables (at WholeProgramInferenceJavaParserStorage line 1100 or so), I think it's clear we need a test case for this change where an annotation's relevant Java types include at least one class like Map.Entry: it has a type parameter, and it is an inner class (and is usually referenced that way).

Probably the easiest way to test that would be to add another annotation to the AinferTestChecker with the appropriate relevant type, and a corresponding test that ensures it can be inferred in the appropriate place.

relevantJavaTypeNamesTemp.add(typeString.substring(dotIndex + 1));
}
}
}
}
this.relevantJavaTypes = Collections.unmodifiableSet(relevantJavaTypesTemp);
this.relevantJavaTypeNames = Collections.unmodifiableSet(relevantJavaTypeNamesTemp);
this.arraysAreRelevant = arraysAreRelevantTemp;
this.nonprimitivesAreRelevant = nonprimitivesAreRelevantTemp;
}

contractsUtils = createContractsFromMethod();
Expand Down Expand Up @@ -2497,9 +2527,9 @@ public final boolean isRelevant(TypeMirror tm) {
if (tm.getKind() != TypeKind.PACKAGE && tm.getKind() != TypeKind.MODULE) {
tm = types.erasure(tm);
}
Boolean cachedResult = isRelevantCache.get(tm);
if (cachedResult != null) {
return cachedResult;
Boolean resultBoxed = isRelevantCache.get(tm);
if (resultBoxed != null) {
return resultBoxed;
}
boolean result = isRelevantImpl(tm);
isRelevantCache.put(tm, result);
Expand Down Expand Up @@ -2531,6 +2561,9 @@ public final boolean isRelevant(AnnotatedTypeMirror tm) {
* <p>Clients should never call this. Call {@link #isRelevant} instead. This is a helper method
* for {@link #isRelevant}.
*
* <p>This should <b>not</b> be called if {@code relevantJavaTypes == null ||
* relevantJavaTypes.contains(tm))}.
*
* @param tm a type
* @return true if users can write type annotations from this type system on the given Java type
*/
Expand Down Expand Up @@ -2618,6 +2651,43 @@ protected boolean isRelevantImpl(TypeMirror tm) {
}
}

/**
* Returns true if users can write type annotations from this type system directly on the given
* Java type.
*
* <p>For a compound type, returns true only if it is permitted to write a type qualifier on the
* top level of the compound type. That is, this method may return false, when it is possible to
* write type qualifiers on elements of the type.
*
* <p>Subclasses should override {@code #isRelevantImpl} instead of this method.
*
* @param type a fully-qualified or simple type; should not be an array (use {@link
* #arraysAreRelevant} instead)
* @return true if users can write type annotations from this type system directly on the given
* Java type
*/
public final boolean isRelevant(String type) {
return relevantJavaTypeNames == null || relevantJavaTypeNames.contains(type);
}

/**
* Returns true if users can write type annotations from this type system on array types.
*
* @return true if users can write type annotations from this type system on array types
*/
public final boolean arraysAreRelevant() {
return arraysAreRelevant;
}

/**
* Returns true if users can write type annotations from this type system on non-primitive types.
*
* @return true if users can write type annotations from this type system on non-primitive types
*/
public final boolean nonprimitivesAreRelevant() {
return nonprimitivesAreRelevant;
}

/** The cached message about relevant types. */
private @MonotonicNonNull String irrelevantExtraMessage = null;

Expand Down