diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 33434366b632..6896561cfa7b 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -7,12 +7,17 @@
### User-visible changes
+Added support for IntelliJ IDEA annotation files (`annotations.xml`) via the
+`-AintellijAnnotations` command-line option.
+
### Changes for type system implementers
Renamed `AnnotatedTypes.innerMostType()` to `innermostComponentType()`.
### Closed issues
+(Filled in at release time.)
+
## Version 4.2.3 (2026-09-01)
### User-visible changes
diff --git a/docs/manual/annotating-libraries.tex b/docs/manual/annotating-libraries.tex
index ef6c41a4c768..e8ff4af744db 100644
--- a/docs/manual/annotating-libraries.tex
+++ b/docs/manual/annotating-libraries.tex
@@ -897,7 +897,11 @@
By default, the Checker Framework warns about such problems in a stub
file provided on the command line, but does not warn about built-in stub files.
These command-line options turn the warnings on or off (respectively) for
- all stub files. \\
+ all stub files.
+ An IntelliJ IDEA annotation file (Section~\ref{intellij-annotations}) is always
+ provided on the command line, so the Checker Framework warns about it by default;
+ \<-AstubNoWarnIfNotFound> turns those warnings off.
+ \\
The \<@NoAnnotationFileParserWarning> annotation on a package or type in a stub file
causes no warning to be issued for that package or type, regardless of
the command-line options.
@@ -913,6 +917,8 @@
to report only missing methods/fields, but ignore missing classes, even if
other classes from the same package are present.
Useful if a package spans more than one jar.
+ This also applies to IntelliJ IDEA annotation files
+ (Section~\ref{intellij-annotations}).
\item[\<-AstubWarnIfRedundantWithBytecode>]
Warn if a stub file entry is redundant with bytecode information. The
@@ -1081,6 +1087,29 @@
\end{Verbatim}
+\sectionAndLabel{External annotations in IntelliJ IDEA format}{intellij-annotations}
+
+The Checker Framework can read external annotations stored in IntelliJ
+IDEA's \code{annotations.xml} format. This allows users to create
+\href{https://www.jetbrains.com/help/rider/Code_Analysis__External_Annotations.html}{external
+annotations}
+using
+\href{https://www.jetbrains.com/help/idea/annotating-source-code.html#external-annotations}{IntelliJ
+IDEA's user interface}.
+
+The \code{-AintellijAnnotations} command-line argument takes a
+path-separated list of \code{annotations.xml} files, directories, jar files, or zip files
+containing \code{annotations.xml} files arranged in directory structures matching their package names.
+The path separator is colon on Unix and semicolon on Windows.
+
+For example:
+\begin{Verbatim}
+ javac -processor org.checkerframework.checker.nullness.NullnessChecker \
+ -AintellijAnnotations=path/to/annotations-dir:path/to/annotations.jar \
+ MyFile.java
+\end{Verbatim}
+
+
\sectionAndLabel{Troubleshooting/debugging annotated libraries}{libraries-troubleshooting}
Sometimes, it may seem that a checker is treating a library as unannotated
diff --git a/docs/manual/contributors.tex b/docs/manual/contributors.tex
index a27f91848c97..c260330d6b13 100644
--- a/docs/manual/contributors.tex
+++ b/docs/manual/contributors.tex
@@ -25,6 +25,7 @@
Calvin Loncaric,
Charles Chen,
Charlie Garrett,
+Chimaobi Emeka-Iheonu,
Chris Povirk,
Chris Toxiadis,
Christopher Mackie,
diff --git a/docs/manual/introduction.tex b/docs/manual/introduction.tex
index ffdca9358510..311fd1c11da3 100644
--- a/docs/manual/introduction.tex
+++ b/docs/manual/introduction.tex
@@ -748,6 +748,10 @@
\item \<-Astubs>
List of stub files or directories; see Section~\ref{stub-using}.
+\item \<-AintellijAnnotations>
+ List of IntelliJ IDEA annotation files, directories, jar files, or zip files;
+ see Section~\ref{intellij-annotations}.
+
\item
\<-AstubWarnIfNotFound>,
\<-AstubNoWarnIfNotFound>,
diff --git a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java
index 16a893564e71..56515f49547f 100644
--- a/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java
+++ b/framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java
@@ -271,8 +271,11 @@
// Additional ajava files to use
// org.checkerframework.framework.type.AnnotatedTypeFactory.parserAjavaFiles()
"ajava",
- // Whether to print warnings about types/members in a stub file
- // that were not found on the class path
+ // Annotations in IntelliJ annotations.xml format
+ // org.checkerframework.framework.stub.AnnotationFileElementTypes.parseIntellijAnnotations()
+ "intellijAnnotations",
+ // Whether to print warnings about types/members in a stub file (or IntelliJ
+ // IDEA annotation file) that were not found on the classpath.
// org.checkerframework.framework.stub.AnnotationFileParser.warnIfNotFound
"stubWarnIfNotFound",
"stubNoWarnIfNotFound",
diff --git a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java
index a52a9c459aa0..a0520b50087a 100644
--- a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java
+++ b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileElementTypes.java
@@ -144,6 +144,8 @@ public boolean isParsing() {
*
Stub files returned by {@link BaseTypeChecker#getExtraStubFiles} (treated like those
* listed in @StubFiles annotation)
*
Stub files provided via {@code -Astubs} compiler option
+ *
IntelliJ IDEA external annotations provided via {@code -AintellijAnnotations} compiler
+ * option
*
*
*
If a type is annotated with a qualifier from the same hierarchy in more than one stub file,
@@ -202,6 +204,13 @@ public void parseStubFiles() {
AnnotationFileType.COMMAND_LINE_STUB);
}
+ // 6. Annotations provided via -AintellijAnnotations command-line option
+ String intellijAnnotationsOption = checker.getOption("intellijAnnotations");
+ if (intellijAnnotationsOption != null) {
+ parseIntellijAnnotations(
+ SystemUtil.pathSeparatorSplitter.splitToList(intellijAnnotationsOption));
+ }
+
parsing = false;
if (stubDebug) {
@@ -288,6 +297,80 @@ public void parseAjavaFileWithTree(String ajavaPath, CompilationUnitTree root) {
}
}
+ /**
+ * Parses IntelliJ annotation files.
+ *
+ * @param intellijAnnotationPaths list of files, directories, or jars/zips to parse
+ */
+ public void parseIntellijAnnotations(List intellijAnnotationPaths) {
+ if (intellijAnnotationPaths.isEmpty()) {
+ return;
+ }
+ boolean wasParsing = parsing;
+ parsing = true;
+ try {
+ ProcessingEnvironment processingEnv = factory.getProcessingEnv();
+ if (stubDebug) {
+ AnnotationFileParser.stubDebugStatic(
+ processingEnv, "AFET.parseIntellijAnnotations(%s)", intellijAnnotationPaths);
+ }
+ SourceChecker checker = factory.getChecker();
+ for (String path : intellijAnnotationPaths) {
+ String fullPath = resolveAgainstTestSrc(path);
+
+ List allFiles =
+ AnnotationFileUtil.allAnnotationFiles(
+ fullPath, AnnotationFileType.INTELLIJ_ANNOTATIONS);
+ if (allFiles == null) {
+ checker.message(
+ Diagnostic.Kind.ERROR, "IntelliJ IDEA annotations file not found: " + path);
+ } else if (allFiles.isEmpty()) {
+ // The path exists but contains no annotations.xml file, so the user gets no
+ // annotations from it. That is most likely a mistake, so warn rather than issuing a
+ // note, which javac does not display by default.
+ checker.message(Diagnostic.Kind.WARNING, "No annotations.xml file found within " + path);
+ } else {
+ for (AnnotationFileResource resource : allFiles) {
+ // Closing the stream is safe even for a jar file entry: it does not close the
+ // JarFile that other entries in `allFiles` share.
+ try (BufferedInputStream annotationFileStream =
+ new BufferedInputStream(resource.getInputStream())) {
+ IntelliJAnnotationParser.parseAnnotationsXml(
+ resource.getDescription(),
+ annotationFileStream,
+ factory,
+ processingEnv,
+ annotationFileAnnos);
+ } catch (IOException e) {
+ checker.message(
+ Diagnostic.Kind.ERROR,
+ "Could not read IntelliJ IDEA annotations: " + resource.getDescription());
+ }
+ }
+ }
+ }
+ } finally {
+ parsing = wasParsing;
+ }
+ }
+
+ /**
+ * Returns the path to use for an annotation file that was named on the command line. This is a
+ * special case when running in jtreg, which runs the compiler in a different directory than the
+ * one that contains the test's annotation files.
+ *
+ * @param path a relative or absolute path, from a command-line argument
+ * @return {@code path}, resolved against the {@code test.src} system property if that property is
+ * set and {@code path} is relative
+ */
+ private static String resolveAgainstTestSrc(String path) {
+ String base = System.getProperty("test.src");
+ if (base == null || Paths.get(path).isAbsolute()) {
+ return path;
+ }
+ return base + "/" + path;
+ }
+
/**
* Parses the files in {@code annotationFiles} of the given file type. This includes files listed
* directly in {@code annotationFiles} and for each listed directory, also includes all files
@@ -315,9 +398,7 @@ private void parseAnnotationFiles(List annotationFiles, AnnotationFileTy
processingEnv, "AFET.parseAnnotationFiles(%s, %s)", annotationFiles, fileType);
}
for (String path : annotationFiles) {
- // Special case when running in jtreg.
- String base = System.getProperty("test.src");
- String fullPath = (base == null) ? path : base + "/" + path;
+ String fullPath = resolveAgainstTestSrc(path);
List allFiles =
AnnotationFileUtil.allAnnotationFiles(fullPath, fileType);
diff --git a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java
index f20d73496e59..1e3ae0f57c6f 100644
--- a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java
+++ b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java
@@ -60,7 +60,9 @@ public enum AnnotationFileType {
/** Ajava file being parsed as if it is a stub file. */
AJAVA_AS_STUB,
/** Ajava file provided on command line. */
- AJAVA;
+ AJAVA,
+ /** IntelliJ annotations. */
+ INTELLIJ_ANNOTATIONS;
/**
* Returns true if this represents a stub file.
@@ -70,7 +72,7 @@ public enum AnnotationFileType {
public boolean isStub() {
return switch (this) {
case JDK_STUB, BUILTIN_STUB, COMMAND_LINE_STUB, AJAVA_AS_STUB -> true;
- case AJAVA -> false;
+ case AJAVA, INTELLIJ_ANNOTATIONS -> false;
default -> throw new BugInCF("unhandled case " + this);
};
}
@@ -83,7 +85,7 @@ public boolean isStub() {
public boolean isBuiltIn() {
return switch (this) {
case JDK_STUB, BUILTIN_STUB -> true;
- case COMMAND_LINE_STUB, AJAVA_AS_STUB, AJAVA -> false;
+ case COMMAND_LINE_STUB, AJAVA_AS_STUB, AJAVA, INTELLIJ_ANNOTATIONS -> false;
default -> throw new BugInCF("unhandled case " + this);
};
}
@@ -96,7 +98,7 @@ public boolean isBuiltIn() {
public boolean isCommandLine() {
return switch (this) {
case JDK_STUB, BUILTIN_STUB -> false;
- case COMMAND_LINE_STUB, AJAVA_AS_STUB, AJAVA -> true;
+ case COMMAND_LINE_STUB, AJAVA_AS_STUB, AJAVA, INTELLIJ_ANNOTATIONS -> true;
default -> throw new BugInCF("unhandled case " + this);
};
}
@@ -352,8 +354,11 @@ public void visit(WildcardType n, Void arg) {
/**
* Returns annotation files found at a given file system location (does not look on classpath).
*
- * @param location an annotation file (stub file or ajava file), a jarfile, or a directory. Look
- * for it as an absolute file and relative to the current directory.
+ * @param location an annotation file (a stub file, ajava file, or IntelliJ IDEA annotation file),
+ * a jarfile, or a directory. Look for it as an absolute file and relative to the current
+ * directory. Because it is named explicitly, a file is used no matter what its name is;
+ * within a jarfile or directory, only files whose name indicates that they are of type {@code
+ * fileType} are used.
* @param fileType file type of files to collect
* @return annotation files with the given file type found in the file system (does not look on
* classpath). Returns null if the file system location does not exist; the caller may wish to
@@ -364,7 +369,7 @@ public void visit(WildcardType n, Void arg) {
File file = new File(location);
if (file.exists()) {
List resources = new ArrayList<>();
- addAnnotationFilesToList(file, resources, fileType);
+ addAnnotationFilesToList(file, resources, fileType, true);
return resources;
}
@@ -373,7 +378,7 @@ public void visit(WildcardType n, Void arg) {
file = new File(System.getProperty("user.dir"), location);
if (file.exists()) {
List resources = new ArrayList<>();
- addAnnotationFilesToList(file, resources, fileType);
+ addAnnotationFilesToList(file, resources, fileType, true);
return resources;
}
@@ -400,39 +405,55 @@ private static boolean isAnnotationFile(File f, AnnotationFileType fileType) {
* otherwise
*/
private static boolean isAnnotationFile(String path, AnnotationFileType fileType) {
+ if (fileType == AnnotationFileType.INTELLIJ_ANNOTATIONS) {
+ // Within a directory or an archive, an IntelliJ IDEA annotation file is always named
+ // "annotations.xml". (A file named on the command line may have any name; see
+ // addAnnotationFilesToList.)
+ return "annotations.xml".equals(new File(path).getName());
+ }
return path.endsWith(fileType.isStub() ? ".astub" : ".ajava");
}
- private static boolean isJar(File f) {
- return f.isFile() && f.getName().endsWith(".jar");
+ /**
+ * Returns true if {@code f} is a JAR or ZIP archive file.
+ *
+ * @param f the file to check
+ * @return true if {@code f} is a JAR or ZIP file
+ */
+ private static boolean isJarOrZip(File f) {
+ return f.isFile() && (f.getName().endsWith(".jar") || f.getName().endsWith(".zip"));
}
/**
* Side-effects {@code resources} by adding annotation files of the given file type to it.
*
- * @param location an annotation file (a stub file or ajava file), a jarfile, or a directory. If a
- * stub file or ajava file, add it to the {@code resources} list. If a jarfile, use all
- * annotation files (of type {@code fileType}) contained in it. If a directory, recurse on all
- * files contained in it.
+ * @param location an annotation file (a stub file, ajava file, or IntelliJ IDEA annotation file),
+ * a jarfile, or a directory. If an annotation file, add it to the {@code resources} list. If
+ * a jarfile, use all annotation files (of type {@code fileType}) contained in it. If a
+ * directory, recurse on all files contained in it.
* @param resources the list to add the found files to
* @param fileType type of annotation files to add
+ * @param isUserSupplied true if {@code location} was named by the user (say, on the command line)
+ * rather than being found by searching a directory or an archive. A file named by the user is
+ * used no matter what its name is; a file found by searching is used only if its name
+ * indicates that it is an annotation file of type {@code fileType}.
*/
@SuppressWarnings({
"JdkObsolete", // JarFile.entries()
- "nullness:argument", // inference failed in Arrays.sort
"builder:required.method.not.called" // ownership passed to list of
// JarEntryAnnotationFileResource, where `file` appears in every element of the list
})
private static void addAnnotationFilesToList(
- File location, List resources, AnnotationFileType fileType) {
- if (isAnnotationFile(location, fileType)) {
- resources.add(new FileAnnotationFileResource(location));
- } else if (isJar(location)) {
+ File location,
+ List resources,
+ AnnotationFileType fileType,
+ boolean isUserSupplied) {
+ if (isJarOrZip(location)) {
JarFile file;
try {
file = new JarFile(location);
} catch (IOException e) {
- System.err.println("AnnotationFileUtil: could not process JAR file: " + location);
+ System.err.println("AnnotationFileUtil: could not process archive: " + location);
return;
}
Enumeration entries = file.entries();
@@ -445,10 +466,16 @@ private static void addAnnotationFilesToList(
} else if (location.isDirectory()) {
File[] directoryContents = location.listFiles();
+ if (directoryContents == null) {
+ System.err.println("AnnotationFileUtil: could not list directory: " + location);
+ return;
+ }
Arrays.sort(directoryContents, Comparator.comparing(File::getName));
for (File enclosed : directoryContents) {
- addAnnotationFilesToList(enclosed, resources, fileType);
+ addAnnotationFilesToList(enclosed, resources, fileType, false);
}
+ } else if ((isUserSupplied && location.isFile()) || isAnnotationFile(location, fileType)) {
+ resources.add(new FileAnnotationFileResource(location));
}
}
diff --git a/framework/src/main/java/org/checkerframework/framework/stub/IntelliJAnnotationParser.java b/framework/src/main/java/org/checkerframework/framework/stub/IntelliJAnnotationParser.java
new file mode 100644
index 000000000000..f05a172ec966
--- /dev/null
+++ b/framework/src/main/java/org/checkerframework/framework/stub/IntelliJAnnotationParser.java
@@ -0,0 +1,1241 @@
+package org.checkerframework.framework.stub;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.PrimitiveType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.TypeVariable;
+import javax.lang.model.util.ElementFilter;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+import javax.tools.Diagnostic;
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.checkerframework.checker.signature.qual.CanonicalName;
+import org.checkerframework.framework.qual.FromStubFile;
+import org.checkerframework.framework.source.SourceChecker;
+import org.checkerframework.framework.stub.AnnotationFileParser.AnnotationFileAnnotations;
+import org.checkerframework.framework.type.AnnotatedTypeFactory;
+import org.checkerframework.framework.type.AnnotatedTypeMirror;
+import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
+import org.checkerframework.javacutil.AnnotationBuilder;
+import org.checkerframework.javacutil.AnnotationMirrorSet;
+import org.checkerframework.javacutil.AnnotationUtils;
+import org.checkerframework.javacutil.BugInCF;
+import org.checkerframework.javacutil.ElementUtils;
+import org.plumelib.util.ArrayMap;
+import org.plumelib.util.ArraySet;
+import org.plumelib.util.StringsP;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+/**
+ * Parser for IntelliJ IDEA external annotations format ({@code annotations.xml}).
+ *
+ *
IntelliJ stores "external" annotations in XML files named {@code annotations.xml} located in
+ * directory trees mirroring package names.
+ */
+public final class IntelliJAnnotationParser {
+
+ /** Do not instantiate. */
+ private IntelliJAnnotationParser() {
+ throw new AssertionError("Do not instantiate");
+ }
+
+ /**
+ * Parses an IntelliJ {@code annotations.xml} stream and populates {@code annotationFileAnnos}.
+ *
+ * @param filename the name or path of the file (for diagnostic reporting)
+ * @param inputStream the input stream of the annotations.xml file
+ * @param atypeFactory the type factory
+ * @param processingEnv the processing environment
+ * @param annotationFileAnnos the annotation storage to populate
+ */
+ public static void parseAnnotationsXml(
+ String filename,
+ InputStream inputStream,
+ AnnotatedTypeFactory atypeFactory,
+ ProcessingEnvironment processingEnv,
+ AnnotationFileAnnotations annotationFileAnnos) {
+ SourceChecker checker = atypeFactory.getChecker();
+ Document doc;
+ try {
+ DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
+ try {
+ dbFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+ } catch (Exception ignored) {
+ // Feature unsupported by specific parser
+ }
+ try {
+ dbFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ } catch (Exception ignored) {
+ // Feature unsupported by specific parser
+ }
+ try {
+ dbFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ } catch (Exception ignored) {
+ // Feature unsupported by specific parser
+ }
+ try {
+ dbFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ } catch (Exception ignored) {
+ // Feature unsupported by specific parser
+ }
+ try {
+ dbFactory.setFeature(
+ "http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+ } catch (Exception ignored) {
+ // Feature unsupported by specific parser
+ }
+ dbFactory.setXIncludeAware(false);
+ dbFactory.setExpandEntityReferences(false);
+ DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
+ doc = dBuilder.parse(inputStream);
+ doc.getDocumentElement().normalize();
+ } catch (ParserConfigurationException | SAXException | IOException e) {
+ checker.message(
+ Diagnostic.Kind.WARNING,
+ String.format("Could not parse annotations XML %s: %s", filename, e.getMessage()));
+ return;
+ }
+
+ NodeList itemNodes = doc.getElementsByTagName("item");
+ for (int i = 0; i < itemNodes.getLength(); i++) {
+ Node node = itemNodes.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ org.w3c.dom.Element itemElement = (org.w3c.dom.Element) node;
+ String itemName = itemElement.getAttribute("name");
+ if (itemName == null || itemName.trim().isEmpty()) {
+ continue;
+ }
+
+ try {
+ List annotations =
+ parseItemAnnotations(itemElement, atypeFactory, processingEnv, filename);
+ if (!annotations.isEmpty()) {
+ applyAnnotationsToElement(
+ itemName.trim(),
+ annotations,
+ atypeFactory,
+ processingEnv,
+ annotationFileAnnos,
+ filename);
+ }
+ } catch (BugInCF e) {
+ throw e;
+ } catch (Exception e) {
+ checker.message(
+ Diagnostic.Kind.WARNING,
+ String.format(
+ "Could not apply annotation item '%s' in %s: %s",
+ itemName.trim(), filename, e.getMessage()));
+ }
+ }
+ }
+ }
+
+ /**
+ * Issues a warning about a missing element, unless the -AstubNoWarnIfNotFound option is set.
+ *
+ *
An IntelliJ annotation file is always supplied on the command line, so this warns by
+ * default, as {@link AnnotationFileParser} does for a command-line file.
+ *
+ * @param checker the source checker
+ * @param message the warning message
+ */
+ private static void warnNotFound(SourceChecker checker, String message) {
+ if (!checker.hasOption("stubNoWarnIfNotFound")) {
+ Diagnostic.Kind kind =
+ checker.hasOption("stubWarnNote") ? Diagnostic.Kind.NOTE : Diagnostic.Kind.WARNING;
+ checker.message(kind, message);
+ }
+ }
+
+ /**
+ * Issues a warning about a class that the annotation file mentions but that does not exist,
+ * unless the -AstubNoWarnIfNotFound or -AstubWarnIfNotFoundIgnoresClasses option is set.
+ *
+ * @param checker the source checker
+ * @param message the warning message
+ */
+ private static void warnClassNotFound(SourceChecker checker, String message) {
+ if (!checker.hasOption("stubWarnIfNotFoundIgnoresClasses")) {
+ warnNotFound(checker, message);
+ }
+ }
+
+ /**
+ * Parses the {@code } children of an {@code } element.
+ *
+ * @param itemElement the XML item element containing annotation child tags
+ * @param atypeFactory the type factory
+ * @param processingEnv the processing environment
+ * @param filename the name or path of the file (for diagnostic reporting)
+ * @return a list of parsed and canonicalized {@link AnnotationMirror}s
+ */
+ private static List parseItemAnnotations(
+ org.w3c.dom.Element itemElement,
+ AnnotatedTypeFactory atypeFactory,
+ ProcessingEnvironment processingEnv,
+ String filename) {
+ List result = new ArrayList<>();
+ Elements elements = processingEnv.getElementUtils();
+ String context =
+ String.format("item '%s' in %s", itemElement.getAttribute("name").trim(), filename);
+
+ for (org.w3c.dom.Element annoElement : childElements(itemElement, "annotation")) {
+ String annoName = annoElement.getAttribute("name");
+ if (annoName == null || annoName.trim().isEmpty()) {
+ continue;
+ }
+ annoName = annoName.trim();
+
+ TypeElement annoTypeElt = getTypeElement(annoName, elements);
+ if (annoTypeElt == null) {
+ warnNotFound(atypeFactory.getChecker(), "Unknown annotation: " + annoName);
+ continue;
+ }
+ if (annoTypeElt.getKind() != ElementKind.ANNOTATION_TYPE) {
+ warnNotFound(atypeFactory.getChecker(), "Not an annotation type: " + annoName);
+ continue;
+ }
+
+ try {
+ AnnotationMirror annoMirror =
+ buildAnnotationMirror(
+ annoElement, annoTypeElt, processingEnv, atypeFactory.getChecker(), context);
+ if (annoMirror != null) {
+ AnnotationMirror canonical = atypeFactory.canonicalAnnotation(annoMirror);
+ result.add(canonical != null ? canonical : annoMirror);
+ }
+ } catch (BugInCF e) {
+ throw e;
+ } catch (Exception e) {
+ warnNotFound(
+ atypeFactory.getChecker(),
+ "Failed to build annotation @" + annoName + ": " + e.getMessage());
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns the children of {@code parent} that are elements with the given tag name. Unlike {@link
+ * org.w3c.dom.Element#getElementsByTagName}, this returns only direct children rather than all
+ * descendants.
+ *
+ * @param parent an XML element
+ * @param tagName a tag name
+ * @return the direct children of {@code parent} whose tag name is {@code tagName}
+ */
+ private static List childElements(
+ org.w3c.dom.Element parent, String tagName) {
+ List result = new ArrayList<>();
+ NodeList children = parent.getChildNodes();
+ for (int i = 0; i < children.getLength(); i++) {
+ Node child = children.item(i);
+ if (child.getNodeType() == Node.ELEMENT_NODE && tagName.equals(child.getNodeName())) {
+ result.add((org.w3c.dom.Element) child);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Constructs an {@link AnnotationMirror} from an XML {@code } element.
+ *
+ *
If any {@code } child cannot be parsed, or if some element that has no default value is
+ * not given a value, this issues a warning and returns null, rather than building an annotation
+ * that is missing a mandatory element.
+ *
+ * @param annoElement the XML element for the annotation
+ * @param annoTypeElt the TypeElement corresponding to the annotation
+ * @param processingEnv the processing environment
+ * @param checker the source checker, for issuing diagnostics
+ * @param context a description of the enclosing item, for diagnostics
+ * @return the constructed {@link AnnotationMirror}, or null if construction fails
+ */
+ private static @Nullable AnnotationMirror buildAnnotationMirror(
+ org.w3c.dom.Element annoElement,
+ TypeElement annoTypeElt,
+ ProcessingEnvironment processingEnv,
+ SourceChecker checker,
+ String context) {
+ @SuppressWarnings("signature") // the qualified name of a TypeElement is a canonical name
+ @CanonicalName String canonicalName = annoTypeElt.getQualifiedName().toString();
+ Elements elements = processingEnv.getElementUtils();
+
+ AnnotationBuilder builder = new AnnotationBuilder(processingEnv, canonicalName);
+ Set writtenElements = new ArraySet<>(2); // most annotations have few elements
+ for (org.w3c.dom.Element valElem : childElements(annoElement, "val")) {
+ if (!valElem.hasAttribute("val")) {
+ continue;
+ }
+ String memberName =
+ valElem.hasAttribute("name") ? valElem.getAttribute("name").trim() : "value";
+ String valStr = valElem.getAttribute("val").trim();
+ String problem = setBuilderValue(builder, memberName, valStr, annoTypeElt, processingEnv);
+ if (problem != null) {
+ checker.message(
+ Diagnostic.Kind.WARNING,
+ String.format("Ignoring annotation @%s on %s: %s", canonicalName, context, problem));
+ return null;
+ }
+ writtenElements.add(memberName);
+ }
+
+ for (ExecutableElement annoElt : ElementFilter.methodsIn(annoTypeElt.getEnclosedElements())) {
+ String elementName = annoElt.getSimpleName().toString();
+ if (annoElt.getDefaultValue() == null && !writtenElements.contains(elementName)) {
+ checker.message(
+ Diagnostic.Kind.WARNING,
+ String.format(
+ "Ignoring annotation @%s on %s: no value for element '%s', which has no default",
+ canonicalName, context, elementName));
+ return null;
+ }
+ }
+
+ // Index the values by element name, so that fromName can supply the default value of every
+ // element that the annotations.xml file does not mention.
+ Map extends ExecutableElement, ? extends AnnotationValue> builtValues =
+ builder.build().getElementValues();
+ Map elementValues = new ArrayMap<>(builtValues.size());
+ for (Map.Entry extends ExecutableElement, ? extends AnnotationValue> entry :
+ builtValues.entrySet()) {
+ elementValues.put(entry.getKey().getSimpleName().toString(), entry.getValue());
+ }
+ return AnnotationBuilder.fromName(elements, canonicalName, elementValues);
+ }
+
+ /**
+ * Sets a value on the {@link AnnotationBuilder} based on the expected element type.
+ *
+ * @param builder the annotation builder
+ * @param memberName the name of the annotation element
+ * @param valStr the raw string value from the XML
+ * @param annoTypeElt the TypeElement of the annotation
+ * @param processingEnv the processing environment
+ * @return null if the value was set, or a description of the problem if it was not
+ */
+ private static @Nullable String setBuilderValue(
+ AnnotationBuilder builder,
+ String memberName,
+ String valStr,
+ TypeElement annoTypeElt,
+ ProcessingEnvironment processingEnv) {
+ ExecutableElement memberMethod = null;
+ for (ExecutableElement m : ElementFilter.methodsIn(annoTypeElt.getEnclosedElements())) {
+ if (m.getSimpleName().contentEquals(memberName)) {
+ memberMethod = m;
+ break;
+ }
+ }
+ if (memberMethod == null) {
+ return String.format("the annotation has no element named '%s'", memberName);
+ }
+
+ TypeMirror returnType = memberMethod.getReturnType();
+ if (returnType.getKind() == TypeKind.ARRAY) {
+ ArrayType at = (ArrayType) returnType;
+ TypeMirror compType = at.getComponentType();
+ List items = parseArrayLiteral(valStr);
+ List