blob: 4ca7079d33e25294764ca6342670c789d34de901 [file] [log] [blame]
package org.checkerframework.framework.stub;
import com.github.javaparser.ParseProblemException;
import com.github.javaparser.Position;
import com.github.javaparser.Problem;
import com.github.javaparser.ast.AccessSpecifier;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.ImportDeclaration;
import com.github.javaparser.ast.Modifier;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.PackageDeclaration;
import com.github.javaparser.ast.StubUnit;
import com.github.javaparser.ast.body.AnnotationDeclaration;
import com.github.javaparser.ast.body.BodyDeclaration;
import com.github.javaparser.ast.body.CallableDeclaration;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.body.EnumConstantDeclaration;
import com.github.javaparser.ast.body.EnumDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.Parameter;
import com.github.javaparser.ast.body.ReceiverParameter;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.expr.ArrayInitializerExpr;
import com.github.javaparser.ast.expr.BooleanLiteralExpr;
import com.github.javaparser.ast.expr.CharLiteralExpr;
import com.github.javaparser.ast.expr.ClassExpr;
import com.github.javaparser.ast.expr.DoubleLiteralExpr;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.FieldAccessExpr;
import com.github.javaparser.ast.expr.IntegerLiteralExpr;
import com.github.javaparser.ast.expr.LongLiteralExpr;
import com.github.javaparser.ast.expr.MarkerAnnotationExpr;
import com.github.javaparser.ast.expr.MemberValuePair;
import com.github.javaparser.ast.expr.NameExpr;
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
import com.github.javaparser.ast.expr.NullLiteralExpr;
import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import com.github.javaparser.ast.expr.UnaryExpr;
import com.github.javaparser.ast.nodeTypes.NodeWithRange;
import com.github.javaparser.ast.nodeTypes.modifiers.NodeWithAccessModifiers;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.ast.type.PrimitiveType;
import com.github.javaparser.ast.type.ReferenceType;
import com.github.javaparser.ast.type.Type;
import com.github.javaparser.ast.type.TypeParameter;
import com.github.javaparser.ast.type.WildcardType;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.VariableTree;
import java.io.File;
import java.io.InputStream;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
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.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.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 org.checkerframework.checker.formatter.qual.FormatMethod;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.signature.qual.CanonicalName;
import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers;
import org.checkerframework.checker.signature.qual.FullyQualifiedName;
import org.checkerframework.framework.ajava.DefaultJointVisitor;
import org.checkerframework.framework.qual.AnnotatedFor;
import org.checkerframework.framework.qual.FromStubFile;
import org.checkerframework.framework.stub.AnnotationFileUtil.AnnotationFileType;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedWildcardType;
import org.checkerframework.framework.util.JavaParserUtil;
import org.checkerframework.framework.util.element.ElementAnnotationUtil.ErrorTypeKindException;
import org.checkerframework.javacutil.AnnotationBuilder;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.Pair;
import org.checkerframework.javacutil.TreeUtils;
// From an implementation perspective, this class represents a single annotation file (stub file or
// ajava file), notably its annotated types and its declaration annotations.
// From a client perspective, it has static methods as described below in the Javadoc.
/**
* This class has three static methods. Each method parses an annotation file and adds annotations
* to the {@link AnnotationFileAnnotations} passed as an argument.
*
* <p>The first main entry point is {@link #parseStubFile(String, InputStream, AnnotatedTypeFactory,
* ProcessingEnvironment, AnnotationFileAnnotations, AnnotationFileUtil.AnnotationFileType)}, which
* side-effects its last argument. It operates in two steps. First, it calls the Annotation File
* Parser to parse an annotation file. Then, it walks the AST to create/collect types and
* declaration annotations.
*
* <p>The second main entry point is {@link #parseAjavaFile(String, InputStream,
* CompilationUnitTree, AnnotatedTypeFactory, ProcessingEnvironment, AnnotationFileAnnotations)}.
* This behaves the same as {@link AnnotationFileParser#parseStubFile(String, InputStream,
* AnnotatedTypeFactory, ProcessingEnvironment, AnnotationFileAnnotations,
* AnnotationFileUtil.AnnotationFileType)}, but takes an ajava file instead.
*
* <p>The other entry point is {@link #parseJdkFileAsStub}.
*/
public class AnnotationFileParser {
/**
* The type of file being parsed: stub file or ajava file. Also indicates its source, such as from
* the JDK, built in, or from the command line.
*
* <p>Non-JDK stub files override JDK stub files. (Ordinarily, if two stubs are provided, they are
* merged.)
*
* <p>For a built-in stub file,
*
* <ul>
* <li>private declarations are ignored,
* <li>some warning messages are not issued, and
* </ul>
*/
private final AnnotationFileType fileType;
/**
* If parsing an ajava file, represents the javac tree for the compilation root of the file being
* parsed.
*/
private CompilationUnitTree root;
/**
* Whether to print warnings about types/members that were not found. The warning states that a
* class/field in the file is not found on the user's real classpath. Since the file may contain
* packages that are not on the classpath, this can be OK, so default to false.
*/
private final boolean warnIfNotFound;
/**
* Whether to ignore missing classes even when warnIfNotFound is set to true. This allows the
* files to contain classes not in the classpath (even if another class in the classpath has the
* same package), but still warn if members of the class (methods, fields) are missing. This
* option does nothing unless warnIfNotFound is also set.
*/
private final boolean warnIfNotFoundIgnoresClasses;
/** Whether to print warnings about stub files that overwrite annotations from bytecode. */
private final boolean warnIfStubOverwritesBytecode;
/**
* Whether to print warnings about stub files that are redundant with annotations from bytecode.
*/
private final boolean warnIfStubRedundantWithBytecode;
/** The diagnostic kind for stub file warnings: NOTE or WARNING. */
private final Diagnostic.Kind stubWarnDiagnosticKind;
/** Whether to print verbose debugging messages. */
private final boolean debugAnnotationFileParser;
/** The name of the file being processed; used only for diagnostic messages. */
private final String filename;
/**
* The AST of the parsed file that this class is processing. May be null if there was a problem
* parsing the file. (TODO: Should the Checker Framework just halt in that case?)
*/
// Not final in order to accommodate a default value.
private StubUnit stubUnit;
private final ProcessingEnvironment processingEnv;
private final AnnotatedTypeFactory atypeFactory;
private final Elements elements;
/**
* The set of annotations found in the file. Keys are both fully-qualified and simple names. There
* are two entries for each annotation: the annotation's simple name and its fully-qualified name.
*
* <p>The map is populated from import statements and also by {@link #getAnnotation(
* AnnotationExpr, Map)} for annotations that are used fully-qualified.
*
* @see #getImportedAnnotations
*/
private Map<String, TypeElement> allAnnotations;
/**
* A list of the fully-qualified names of enum constants and static fields with constant values
* that have been imported.
*/
private final List<@FullyQualifiedName String> importedConstants = new ArrayList<>();
/** A map of imported fully-qualified type names to type elements. */
private final Map<String, TypeElement> importedTypes = new HashMap<>();
/** The annotation {@code @FromStubFile}. */
private final AnnotationMirror fromStubFileAnno;
/**
* List of AnnotatedTypeMirrors for class or method type parameters that are in scope of the
* elements currently parsed.
*/
private final List<AnnotatedTypeVariable> typeParameters = new ArrayList<>();
/**
* The annotations on the declared package of the complation unit being processed. Contains null
* if not processing a compilation unit or if the file has no declared package.
*/
@Nullable List<AnnotationExpr> packageAnnos;
// The following variables are stored in the AnnotationFileParser because otherwise they would
// need to be passed through everywhere, which would be verbose.
/**
* The name of the type that is currently being parsed. After processing a package declaration but
* before processing a type declaration, the type part of this may be null.
*
* <p>It is used both for resolving symbols and for error messages.
*/
private FqName typeBeingParsed;
/**
* Contains the annotations of the file currently being processed, or null if not currently
* processing a file. The {@code process*} methods side-effect this data structure.
*/
@Nullable AnnotationFileAnnotations annotationFileAnnos;
/** The line separator. */
private static final String LINE_SEPARATOR = System.lineSeparator().intern();
/** Whether or not the {@code -AmergeStubsWithSource} command-line argument was passed. */
private final boolean mergeStubsWithSource;
/**
* The result of calling AnnotationFileParser.parse: the annotated types and declaration
* annotations from the file.
*/
public static class AnnotationFileAnnotations {
/**
* Map from element to its type as declared in the annotation file.
*
* <p>This is a fine-grained mapping that contains all sorts of elements; contrast with {@link
* #fakeOverrides}.
*/
public final Map<Element, AnnotatedTypeMirror> atypes = new HashMap<>();
/**
* Map from a name (actually declaration element string) to the set of declaration annotations
* on it, as written in the annotation file.
*
* <p>Map keys cannot be Element, because a different Element appears in the annotation files
* than in the real files. So, map keys are the verbose element name, as returned by
* ElementUtils.getQualifiedName.
*/
public final Map<String, Set<AnnotationMirror>> declAnnos = new HashMap<>(1);
/**
* Map from a method element to all the fake overrides of it. Given a key {@code ee}, the fake
* overrides are always in subtypes of {@code ee.getEnclosingElement()}, which is the same as
* {@code ee.getReceiverType()}.
*/
public final Map<ExecutableElement, List<Pair<TypeMirror, AnnotatedTypeMirror>>> fakeOverrides =
new HashMap<>(1);
}
/**
* Create a new AnnotationFileParser object, which will parse and extract annotations from the
* given file.
*
* @param filename name of annotation file, used only for diagnostic messages
* @param atypeFactory AnnotatedTypeFactory to use
* @param processingEnv ProcessingEnvironment to use
* @param fileType the type of file being parsed (stub file or ajava file) and its source
*/
private AnnotationFileParser(
String filename,
AnnotatedTypeFactory atypeFactory,
ProcessingEnvironment processingEnv,
AnnotationFileType fileType) {
this.filename = filename;
this.atypeFactory = atypeFactory;
this.processingEnv = processingEnv;
this.elements = processingEnv.getElementUtils();
this.fileType = fileType;
this.root = null;
// TODO: This should use SourceChecker.getOptions() to allow
// setting these flags per checker.
Map<String, String> options = processingEnv.getOptions();
this.warnIfNotFound = fileType.isCommandLine() || options.containsKey("stubWarnIfNotFound");
this.warnIfNotFoundIgnoresClasses = options.containsKey("stubWarnIfNotFoundIgnoresClasses");
this.warnIfStubOverwritesBytecode = options.containsKey("stubWarnIfOverwritesBytecode");
this.warnIfStubRedundantWithBytecode =
options.containsKey("stubWarnIfRedundantWithBytecode")
&& atypeFactory.shouldWarnIfStubRedundantWithBytecode();
this.stubWarnDiagnosticKind =
options.containsKey("stubWarnNote") ? Diagnostic.Kind.NOTE : Diagnostic.Kind.WARNING;
this.debugAnnotationFileParser = options.containsKey("stubDebug");
this.fromStubFileAnno = AnnotationBuilder.fromClass(elements, FromStubFile.class);
this.mergeStubsWithSource = atypeFactory.getChecker().hasOption("mergeStubsWithSource");
}
/**
* Sets the root of the file currently being parsed to {@code root}.
*
* @param root compilation unit for the file being parsed
*/
private void setRoot(CompilationUnitTree root) {
this.root = root;
}
/**
* All annotations defined in the package (but not those nested within classes in the package).
* Keys are both fully-qualified and simple names.
*
* @param packageElement a package
* @return a map from annotation name to TypeElement
*/
public static Map<String, TypeElement> annosInPackage(PackageElement packageElement) {
return createNameToAnnotationMap(ElementFilter.typesIn(packageElement.getEnclosedElements()));
}
/**
* All annotations declared (directly) within a class. Keys are both fully-qualified and simple
* names.
*
* @param typeElement a type
* @return a map from annotation name to TypeElement
*/
public static Map<String, TypeElement> annosInType(TypeElement typeElement) {
return createNameToAnnotationMap(ElementFilter.typesIn(typeElement.getEnclosedElements()));
}
/**
* All annotations declared within any of the given elements.
*
* @param typeElements the elements whose annotations to retrieve
* @return a map from annotation names (both fully-qualified and simple names) to TypeElement
*/
public static Map<String, TypeElement> createNameToAnnotationMap(List<TypeElement> typeElements) {
Map<String, TypeElement> result = new HashMap<>();
for (TypeElement typeElm : typeElements) {
if (typeElm.getKind() == ElementKind.ANNOTATION_TYPE) {
putIfAbsent(result, typeElm.getSimpleName().toString(), typeElm);
putIfAbsent(result, typeElm.getQualifiedName().toString(), typeElm);
}
}
return result;
}
/**
* Get all members of a Type that are importable in an annotation file. Currently these are values
* of enums, or compile time constants.
*
* @param typeElement the type whose members to return
* @return a list of fully-qualified member names
*/
private static List<@FullyQualifiedName String> getImportableMembers(TypeElement typeElement) {
List<VariableElement> memberElements =
ElementFilter.fieldsIn(typeElement.getEnclosedElements());
List<@FullyQualifiedName String> result = new ArrayList<>();
for (VariableElement varElement : memberElements) {
if (varElement.getConstantValue() != null
|| varElement.getKind() == ElementKind.ENUM_CONSTANT) {
@SuppressWarnings("signature") // string concatenation
@FullyQualifiedName String fqName =
typeElement.getQualifiedName().toString() + "." + varElement.getSimpleName().toString();
result.add(fqName);
}
}
return result;
}
/**
* Returns all annotations imported by the annotation file, as a value for {@link
* #allAnnotations}. Note that this also modifies {@link #importedConstants} and {@link
* #importedTypes}.
*
* <p>This method misses annotations that are not imported. The {@link #getAnnotation} method
* compensates for this deficiency by adding any fully-qualified annotation that it encounters.
*
* @return a map from names to TypeElement, for all annotations imported by the annotation file.
* Two entries for each annotation: one for the simple name and another for the
* fully-qualified name, with the same value.
* @see #allAnnotations
*/
private Map<String, TypeElement> getImportedAnnotations() {
Map<String, TypeElement> result = new HashMap<>();
// TODO: The size can be greater than 1, but this ignores all but the first element.
assert !stubUnit.getCompilationUnits().isEmpty();
CompilationUnit cu = stubUnit.getCompilationUnits().get(0);
if (cu.getImports() == null) {
return result;
}
for (ImportDeclaration importDecl : cu.getImports()) {
try {
if (importDecl.isAsterisk()) {
@SuppressWarnings("signature" // https://tinyurl.com/cfissue/3094:
// com.github.javaparser.ast.expr.Name inherits toString,
// so there can be no annotation for it
)
@DotSeparatedIdentifiers String imported = importDecl.getName().toString();
if (importDecl.isStatic()) {
// Wildcard import of members of a type (class or interface)
TypeElement element = getTypeElement(imported, "Imported type not found", importDecl);
if (element != null) {
// Find nested annotations
// Find compile time constant fields, or values of an enum
putAllNew(result, annosInType(element));
importedConstants.addAll(getImportableMembers(element));
addEnclosingTypesToImportedTypes(element);
}
} else {
// Wildcard import of members of a package
PackageElement element = findPackage(imported, importDecl);
if (element != null) {
putAllNew(result, annosInPackage(element));
addEnclosingTypesToImportedTypes(element);
}
}
} else {
// A single (non-wildcard) import.
@SuppressWarnings("signature" // importDecl is non-wildcard, so its name is
// @FullyQualifiedName
)
@FullyQualifiedName String imported = importDecl.getNameAsString();
final TypeElement importType = elements.getTypeElement(imported);
if (importType == null && !importDecl.isStatic()) {
// Class or nested class (according to JSL), but we can't resolve
stubWarnNotFound(importDecl, "Imported type not found: " + imported);
} else if (importType == null) {
// static import of field or method.
Pair<@FullyQualifiedName String, String> typeParts =
AnnotationFileUtil.partitionQualifiedName(imported);
String type = typeParts.first;
String fieldName = typeParts.second;
TypeElement enclType =
getTypeElement(
type,
String.format("Enclosing type of static field %s not found", fieldName),
importDecl);
if (enclType != null) {
// Don't use findFieldElement(enclType, fieldName), because we don't
// want a warning, imported might be a method.
for (VariableElement field : ElementUtils.getAllFieldsIn(enclType, elements)) {
// field.getSimpleName() is a CharSequence, not a String
if (fieldName.equals(field.getSimpleName().toString())) {
importedConstants.add(imported);
}
}
}
} else if (importType.getKind() == ElementKind.ANNOTATION_TYPE) {
// Single annotation or nested annotation
TypeElement annoElt = elements.getTypeElement(imported);
if (annoElt != null) {
putIfAbsent(result, annoElt.getSimpleName().toString(), annoElt);
importedTypes.put(annoElt.getSimpleName().toString(), annoElt);
} else {
stubWarnNotFound(importDecl, "Could not load import: " + imported);
}
} else {
// Class or nested class
// TODO: Is this needed?
importedConstants.add(imported);
TypeElement element = getTypeElement(imported, "Imported type not found", importDecl);
importedTypes.put(element.getSimpleName().toString(), element);
}
}
} catch (AssertionError error) {
stubWarnNotFound(importDecl, error.toString());
}
}
return result;
}
// If a member is imported, then consider every containing class to also be imported.
private void addEnclosingTypesToImportedTypes(Element element) {
for (Element enclosedEle : element.getEnclosedElements()) {
if (enclosedEle.getKind().isClass()) {
importedTypes.put(enclosedEle.getSimpleName().toString(), (TypeElement) enclosedEle);
}
}
}
/**
* The main entry point. Parse a stub file and side-effects the {@code annotationFileAnnos}
* argument.
*
* @param filename name of stub file, used only for diagnostic messages
* @param inputStream of stub file to parse
* @param atypeFactory AnnotatedTypeFactory to use
* @param processingEnv ProcessingEnvironment to use
* @param annotationFileAnnos annotations from the annotation file; side-effected by this method
* @param fileType the annotation file type and source
*/
public static void parseStubFile(
String filename,
InputStream inputStream,
AnnotatedTypeFactory atypeFactory,
ProcessingEnvironment processingEnv,
AnnotationFileAnnotations annotationFileAnnos,
AnnotationFileType fileType) {
AnnotationFileParser afp =
new AnnotationFileParser(filename, atypeFactory, processingEnv, fileType);
try {
afp.parseStubUnit(inputStream);
afp.process(annotationFileAnnos);
} catch (ParseProblemException e) {
for (Problem p : e.getProblems()) {
afp.warn(null, p.getVerboseMessage());
}
}
}
/**
* The main entry point when parsing an ajava file. Parses an ajava file and side-effects the last
* two arguments.
*
* @param filename name of ajava file, used only for diagnostic messages
* @param inputStream of ajava file to parse
* @param root javac tree for the file to be parsed
* @param atypeFactory AnnotatedTypeFactory to use
* @param processingEnv ProcessingEnvironment to use
* @param ajavaAnnos annotations from the ajava file; side-effected by this method
*/
public static void parseAjavaFile(
String filename,
InputStream inputStream,
CompilationUnitTree root,
AnnotatedTypeFactory atypeFactory,
ProcessingEnvironment processingEnv,
AnnotationFileAnnotations ajavaAnnos) {
AnnotationFileParser afp =
new AnnotationFileParser(filename, atypeFactory, processingEnv, AnnotationFileType.AJAVA);
try {
afp.parseStubUnit(inputStream);
JavaParserUtil.concatenateAddedStringLiterals(afp.stubUnit);
afp.setRoot(root);
afp.process(ajavaAnnos);
} catch (ParseProblemException e) {
for (Problem p : e.getProblems()) {
afp.warn(null, p.getVerboseMessage());
}
}
}
/**
* Parse a stub file that is a part of the annotated JDK and side-effects the {@code stubAnnos}
* argument.
*
* @param filename name of stub file, used only for diagnostic messages
* @param inputStream of stub file to parse
* @param atypeFactory AnnotatedTypeFactory to use
* @param processingEnv ProcessingEnvironment to use
* @param stubAnnos annotations from the stub file; side-effected by this method
*/
public static void parseJdkFileAsStub(
String filename,
InputStream inputStream,
AnnotatedTypeFactory atypeFactory,
ProcessingEnvironment processingEnv,
AnnotationFileAnnotations stubAnnos) {
parseStubFile(
filename, inputStream, atypeFactory, processingEnv, stubAnnos, AnnotationFileType.JDK_STUB);
}
/**
* Delegate to the Stub Parser to parse the annotation file to an AST, and save it in {@link
* #stubUnit}. Also sets {@link #allAnnotations}. Does not copy annotations out of {@link
* #stubUnit}; that is done by the {@code process*} methods.
*
* <p>Subsequently, all work uses the AST.
*
* @param inputStream the stream from which to read an annotation file
*/
private void parseStubUnit(InputStream inputStream) {
if (debugAnnotationFileParser) {
stubDebug(String.format("parsing annotation file %s", filename));
}
stubUnit = JavaParserUtil.parseStubUnit(inputStream);
// getImportedAnnotations() also modifies importedConstants and importedTypes. This should
// be refactored to be nicer.
allAnnotations = getImportedAnnotations();
if (allAnnotations.isEmpty()
&& fileType.isStub()
&& fileType != AnnotationFileType.AJAVA_AS_STUB) {
// Issue a warning if the stub file contains no import statements. The warning is
// incorrect if the stub file contains fully-qualified annotations.
stubWarnNotFound(
null,
String.format(
"No supported annotations found! Does stub file %s import them?", filename));
}
// Annotations in java.lang might be used without an import statement, so add them in case.
allAnnotations.putAll(annosInPackage(findPackage("java.lang", null)));
}
/**
* Process {@link #stubUnit}, which is the AST produced by {@link #parseStubUnit}. Processing
* means copying annotations from Stub Parser data structures to {@code #annotationFileAnnos}.
*
* @param annotationFileAnnos annotations from the file; side-effected by this method
*/
private void process(AnnotationFileAnnotations annotationFileAnnos) {
this.annotationFileAnnos = annotationFileAnnos;
processStubUnit(this.stubUnit);
this.annotationFileAnnos = null;
}
/**
* Process the given StubUnit: copy its annotations to {@code #annotationFileAnnos}.
*
* @param su the StubUnit to process
*/
private void processStubUnit(StubUnit su) {
for (CompilationUnit cu : su.getCompilationUnits()) {
processCompilationUnit(cu);
}
}
/**
* Process the given CompilationUnit: copy its annotations to {@code #annotationFileAnnos}.
*
* @param cu the CompilationUnit to process
*/
private void processCompilationUnit(CompilationUnit cu) {
if (!cu.getPackageDeclaration().isPresent()) {
packageAnnos = null;
typeBeingParsed = new FqName(null, null);
} else {
PackageDeclaration pDecl = cu.getPackageDeclaration().get();
packageAnnos = pDecl.getAnnotations();
processPackage(pDecl);
}
if (fileType.isStub()) {
if (cu.getTypes() != null) {
for (TypeDeclaration<?> typeDeclaration : cu.getTypes()) {
// Not processing an ajava file, so ignore the return value.
processTypeDecl(typeDeclaration, null, null);
}
}
} else {
root.accept(new AjavaAnnotationCollectorVisitor(), cu);
}
packageAnnos = null;
}
/**
* Process the given package declaration: copy its annotations to {@code #annotationFileAnnos}.
*
* @param packDecl the package declaration to process
*/
private void processPackage(PackageDeclaration packDecl) {
assert (packDecl != null);
if (!isAnnotatedForThisChecker(packDecl.getAnnotations())) {
return;
}
String packageName = packDecl.getNameAsString();
typeBeingParsed = new FqName(packageName, null);
Element elem = elements.getPackageElement(packageName);
// If the element lookup fails (that is, elem == null), it's because we have an annotation for a
// package that isn't on the classpath, which is fine.
if (elem != null) {
recordDeclAnnotation(elem, packDecl.getAnnotations(), packDecl);
}
// TODO: Handle atypes???
}
/**
* Returns true if the given program construct need not be read: it is private and one of the
* following is true:
*
* <ul>
* <li>It is in the annotated JDK. Private constructs can't be referenced outside of the JDK and
* might refer to types that are not accessible.
* <li>It is not an ajava file and {@code -AmergeStubsWithSource} was not supplied. As described
* at https://checkerframework.org/manual/#stub-multiple-specifications, source files take
* precedence over stub files unless {@code -AmergeStubsWithSource} is supplied. As
* described at https://checkerframework.org/manual/#ajava-using, source files do not take
* precedence over ajava files (when reading an ajava file, it is as if {@code
* -AmergeStubsWithSource} were supplied).
* </ul>
*
* @param node a declaration
* @return true if the given program construct is in the annotated JDK and is private
*/
private boolean skipNode(NodeWithAccessModifiers<?> node) {
// Must include everything with no access modifier, because stub files are allowed to omit the
// access modifier. Also, interface methods have no access modifier, but they are still public.
// Must include protected JDK methods. For example, Object.clone is protected, but it contains
// annotations that apply to calls like `super.clone()` and `myArray.clone()`.
return (fileType == AnnotationFileType.BUILTIN_STUB
|| (fileType.isStub() && !mergeStubsWithSource))
&& node.getModifiers().contains(Modifier.privateModifier());
}
/**
* Process a type declaration: copy its annotations to {@code #annotationFileAnnos}.
*
* <p>This method stores the declaration's type parameters in {@link #typeParameters}. When
* processing an ajava file, where traversal is handled externaly by a {@link
* org.checkerframework.framework.ajava.JointJavacJavaParserVisitor}, these type variables must be
* removed after processing the type's members. Otherwise, this method removes them.
*
* @param typeDecl the type declaration to process
* @param outertypeName the name of the containing class, when processing a nested class;
* otherwise null
* @param classTree the tree corresponding to typeDecl if processing an ajava file, null otherwise
* @return a list of types variables for {@code typeDecl}. Only non-null if processing an ajava
* file, in which case the contents should be removed from {@link #typeParameters} after
* processing the type declaration's members
*/
private List<AnnotatedTypeVariable> processTypeDecl(
TypeDeclaration<?> typeDecl, String outertypeName, @Nullable ClassTree classTree) {
assert typeBeingParsed != null;
if (skipNode(typeDecl)) {
return null;
}
String innerName;
@FullyQualifiedName String fqTypeName;
TypeElement typeElt;
if (classTree != null) {
typeElt = TreeUtils.elementFromDeclaration(classTree);
innerName = typeElt.getQualifiedName().toString();
typeBeingParsed = new FqName(typeBeingParsed.packageName, innerName);
fqTypeName = typeBeingParsed.toString();
} else {
String packagePrefix = outertypeName == null ? "" : outertypeName + ".";
innerName = packagePrefix + typeDecl.getNameAsString();
typeBeingParsed = new FqName(typeBeingParsed.packageName, innerName);
fqTypeName = typeBeingParsed.toString();
typeElt = elements.getTypeElement(fqTypeName);
}
if (!isAnnotatedForThisChecker(typeDecl.getAnnotations())) {
return null;
}
if (typeElt == null) {
if (debugAnnotationFileParser
|| (!warnIfNotFoundIgnoresClasses
&& !hasNoAnnotationFileParserWarning(typeDecl.getAnnotations())
&& !hasNoAnnotationFileParserWarning(packageAnnos))) {
if (elements.getAllTypeElements(fqTypeName).isEmpty()) {
stubWarnNotFound(typeDecl, "Type not found: " + fqTypeName);
} else {
stubWarnNotFound(
typeDecl,
"Type not found uniquely: "
+ fqTypeName
+ " : "
+ elements.getAllTypeElements(fqTypeName));
}
}
return null;
}
List<AnnotatedTypeVariable> typeDeclTypeParameters = null;
if (typeElt.getKind() == ElementKind.ENUM) {
if (!(typeDecl instanceof EnumDeclaration)) {
warn(
typeDecl,
innerName
+ " is an enum, but stub file declared it as "
+ typeDecl.toString().split("\\R", 2)[0]
+ "...");
return null;
}
typeDeclTypeParameters = processEnum((EnumDeclaration) typeDecl, typeElt);
typeParameters.addAll(typeDeclTypeParameters);
} else if (typeElt.getKind() == ElementKind.ANNOTATION_TYPE) {
if (!(typeDecl instanceof AnnotationDeclaration)) {
warn(
typeDecl,
innerName
+ " is an annotation, but stub file declared it as "
+ typeDecl.toString().split("\\R", 2)[0]
+ "...");
return null;
}
stubWarnNotFound(typeDecl, "Skipping annotation type: " + fqTypeName);
} else if (typeDecl instanceof ClassOrInterfaceDeclaration) {
if (!(typeDecl instanceof ClassOrInterfaceDeclaration)) {
warn(
typeDecl,
innerName
+ " is a class or interface, but stub file declared it as "
+ typeDecl.toString().split("\\R", 2)[0]
+ "...");
return null;
}
typeDeclTypeParameters = processType((ClassOrInterfaceDeclaration) typeDecl, typeElt);
typeParameters.addAll(typeDeclTypeParameters);
} // else it's an EmptyTypeDeclaration. TODO: An EmptyTypeDeclaration can have
// annotations, right?
// If processing an ajava file, then traversal is handled by a visitor, rather than the rest
// of this method.
if (fileType == AnnotationFileType.AJAVA) {
return typeDeclTypeParameters;
}
Pair<Map<Element, BodyDeclaration<?>>, Map<Element, List<BodyDeclaration<?>>>> members =
getMembers(typeDecl, typeElt, typeDecl);
for (Map.Entry<Element, BodyDeclaration<?>> entry : members.first.entrySet()) {
final Element elt = entry.getKey();
final BodyDeclaration<?> decl = entry.getValue();
switch (elt.getKind()) {
case FIELD:
processField((FieldDeclaration) decl, (VariableElement) elt);
break;
case ENUM_CONSTANT:
processEnumConstant((EnumConstantDeclaration) decl, (VariableElement) elt);
break;
case CONSTRUCTOR:
case METHOD:
processCallableDeclaration((CallableDeclaration<?>) decl, (ExecutableElement) elt);
break;
case CLASS:
case INTERFACE:
// Not processing an ajava file, so ignore the return value.
processTypeDecl((ClassOrInterfaceDeclaration) decl, innerName, null);
break;
case ENUM:
// Not processing an ajava file, so ignore the return value.
processTypeDecl((EnumDeclaration) decl, innerName, null);
break;
default:
/* do nothing */
stubWarnNotFound(decl, "AnnotationFileParser ignoring: " + elt);
break;
}
}
for (Map.Entry<Element, List<BodyDeclaration<?>>> entry : members.second.entrySet()) {
ExecutableElement fakeOverridden = (ExecutableElement) entry.getKey();
List<BodyDeclaration<?>> fakeOverrideDecls = entry.getValue();
for (BodyDeclaration<?> bodyDecl : fakeOverrideDecls) {
processFakeOverride(fakeOverridden, (CallableDeclaration<?>) bodyDecl, typeElt);
}
}
if (typeDeclTypeParameters != null) {
typeParameters.removeAll(typeDeclTypeParameters);
}
return null;
}
/**
* Returns true if the argument contains {@code @NoAnnotationFileParserWarning}.
*
* @param aexprs collection of annotation expressions
* @return true if {@code aexprs} contains {@code @NoAnnotationFileParserWarning}
*/
private boolean hasNoAnnotationFileParserWarning(Iterable<AnnotationExpr> aexprs) {
if (aexprs == null) {
return false;
}
for (AnnotationExpr anno : aexprs) {
if (anno.getNameAsString().equals("NoAnnotationFileParserWarning")) {
return true;
}
}
return false;
}
/**
* Process the type's declaration: copy its annotations to {@code #annotationFileAnnos}. Does not
* process any of its members. Returns the type's type parameter declarations.
*
* @param decl a type declaration
* @param elt the type's element
* @return the type's type parameter declarations
*/
private List<AnnotatedTypeVariable> processType(
ClassOrInterfaceDeclaration decl, TypeElement elt) {
recordDeclAnnotation(elt, decl.getAnnotations(), decl);
AnnotatedDeclaredType type = atypeFactory.fromElement(elt);
annotate(type, decl.getAnnotations(), decl);
final List<? extends AnnotatedTypeMirror> typeArguments = type.getTypeArguments();
final List<TypeParameter> typeParameters = decl.getTypeParameters();
// It can be the case that args=[] and params=null, so don't crash in that case.
// if ((typeParameters == null) != (typeArguments == null)) {
// throw new Error(String.format("parseType (%s, %s): inconsistent nullness for args and
// params%n args = %s%n params = %s%n", decl, elt, typeArguments, typeParameters));
// }
if (debugAnnotationFileParser) {
int numParams = (typeParameters == null ? 0 : typeParameters.size());
int numArgs = (typeArguments == null ? 0 : typeArguments.size());
if (numParams != numArgs) {
stubDebug(
String.format(
"parseType: mismatched sizes for typeParameters=%s (size %d) and typeArguments=%s"
+ " (size %d); decl=%s; elt=%s (%s); type=%s (%s); typeBeingParsed=%s",
typeParameters,
numParams,
typeArguments,
numArgs,
decl.toString().replace(LINE_SEPARATOR, " "),
elt.toString().replace(LINE_SEPARATOR, " "),
elt.getClass(),
type,
type.getClass(),
typeBeingParsed));
stubDebug("Proceeding despite mismatched sizes");
}
}
annotateTypeParameters(decl, elt, typeArguments, typeParameters);
annotateSupertypes(decl, type);
putMerge(annotationFileAnnos.atypes, elt, type);
List<AnnotatedTypeVariable> typeVariables = new ArrayList<>(type.getTypeArguments().size());
for (AnnotatedTypeMirror typeV : type.getTypeArguments()) {
if (typeV.getKind() != TypeKind.TYPEVAR) {
warn(
decl,
"expected an AnnotatedTypeVariable but found type kind "
+ typeV.getKind()
+ ": "
+ typeV);
} else {
typeVariables.add((AnnotatedTypeVariable) typeV);
}
}
return typeVariables;
}
/**
* Process an enum: copy its annotations to {@code #annotationFileAnnos}. Returns the enum's type
* parameter declarations.
*
* @param decl enum declaration
* @param elt element representing enum
* @return the enum's type parameter declarations
*/
private List<AnnotatedTypeVariable> processEnum(EnumDeclaration decl, TypeElement elt) {
recordDeclAnnotation(elt, decl.getAnnotations(), decl);
AnnotatedDeclaredType type = atypeFactory.fromElement(elt);
annotate(type, decl.getAnnotations(), decl);
putMerge(annotationFileAnnos.atypes, elt, type);
List<AnnotatedTypeVariable> typeVariables = new ArrayList<>(type.getTypeArguments().size());
for (AnnotatedTypeMirror typeV : type.getTypeArguments()) {
if (typeV.getKind() != TypeKind.TYPEVAR) {
warn(
decl,
"expected an AnnotatedTypeVariable but found type kind "
+ typeV.getKind()
+ ": "
+ typeV);
} else {
typeVariables.add((AnnotatedTypeVariable) typeV);
}
}
return typeVariables;
}
private void annotateSupertypes(
ClassOrInterfaceDeclaration typeDecl, AnnotatedDeclaredType type) {
if (typeDecl.getExtendedTypes() != null) {
for (ClassOrInterfaceType supertype : typeDecl.getExtendedTypes()) {
AnnotatedDeclaredType annotatedSupertype =
findAnnotatedType(supertype, type.directSupertypes(), typeDecl);
if (annotatedSupertype == null) {
warn(
typeDecl,
"stub file does not match bytecode: "
+ "could not find superclass "
+ supertype
+ " from type "
+ type);
} else {
annotate(annotatedSupertype, supertype, null, typeDecl);
}
}
}
if (typeDecl.getImplementedTypes() != null) {
for (ClassOrInterfaceType supertype : typeDecl.getImplementedTypes()) {
AnnotatedDeclaredType annotatedSupertype =
findAnnotatedType(supertype, type.directSupertypes(), typeDecl);
if (annotatedSupertype == null) {
warn(
typeDecl,
"stub file does not match bytecode: "
+ "could not find superinterface "
+ supertype
+ " from type "
+ type);
} else {
annotate(annotatedSupertype, supertype, null, typeDecl);
}
}
}
}
/**
* Process a method or constructor declaration: copy its annotations to {@code
* #annotationFileAnnos}.
*
* @param decl a method or constructor declaration, as read from an annotation file
* @param elt the method or constructor's element
* @return type variables for the method
*/
private List<AnnotatedTypeVariable> processCallableDeclaration(
CallableDeclaration<?> decl, ExecutableElement elt) {
if (!isAnnotatedForThisChecker(decl.getAnnotations())) {
return null;
}
// Declaration annotations
recordDeclAnnotation(elt, decl.getAnnotations(), decl);
if (decl.isMethodDeclaration()) {
// AnnotationFileParser parses all annotations in type annotation position as type
// annotations.
recordDeclAnnotation(elt, ((MethodDeclaration) decl).getType().getAnnotations(), decl);
}
markAsFromStubFile(elt);
AnnotatedExecutableType methodType = atypeFactory.fromElement(elt);
AnnotatedExecutableType origMethodType =
warnIfStubRedundantWithBytecode ? methodType.deepCopy() : null;
// Type Parameters
annotateTypeParameters(decl, elt, methodType.getTypeVariables(), decl.getTypeParameters());
typeParameters.addAll(methodType.getTypeVariables());
// Return type, from declaration annotations on the method or constructor
if (decl.isMethodDeclaration()) {
try {
annotate(
methodType.getReturnType(),
((MethodDeclaration) decl).getType(),
decl.getAnnotations(),
decl);
} catch (ErrorTypeKindException e) {
// Do nothing, per Issue #244.
}
} else {
assert decl.isConstructorDeclaration();
annotate(methodType.getReturnType(), decl.getAnnotations(), decl);
}
// Parameters
processParameters(decl, elt, methodType);
// Receiver
if (decl.getReceiverParameter().isPresent()) {
ReceiverParameter receiverParameter = decl.getReceiverParameter().get();
if (methodType.getReceiverType() == null) {
if (decl.isConstructorDeclaration()) {
warn(
receiverParameter,
"parseParameter: constructor %s of a top-level class cannot have receiver"
+ " annotations %s",
methodType,
decl.getReceiverParameter().get().getAnnotations());
} else {
warn(
receiverParameter,
"parseParameter: static method %s cannot have receiver annotations %s",
methodType,
decl.getReceiverParameter().get().getAnnotations());
}
} else {
// Add declaration annotations.
annotate(
methodType.getReceiverType(),
decl.getReceiverParameter().get().getAnnotations(),
receiverParameter);
// Add type annotations.
annotate(
methodType.getReceiverType(),
decl.getReceiverParameter().get().getType(),
decl.getReceiverParameter().get().getAnnotations(),
receiverParameter);
}
}
if (warnIfStubRedundantWithBytecode
&& methodType.toString().equals(origMethodType.toString())
&& fileType != AnnotationFileType.BUILTIN_STUB) {
warn(
decl,
String.format(
"redundant stub file specification for %s", ElementUtils.getQualifiedName(elt)));
}
// Store the type.
putMerge(annotationFileAnnos.atypes, elt, methodType);
if (fileType.isStub()) {
typeParameters.removeAll(methodType.getTypeVariables());
}
return methodType.getTypeVariables();
}
/**
* Process the parameters of a method or constructor declaration: copy their annotations to {@code
* #annotationFileAnnos}.
*
* @param method a Method or Constructor declaration
* @param elt ExecutableElement of {@code method}
* @param methodType annotated type of {@code method}
*/
private void processParameters(
CallableDeclaration<?> method, ExecutableElement elt, AnnotatedExecutableType methodType) {
List<Parameter> params = method.getParameters();
List<? extends VariableElement> paramElts = elt.getParameters();
List<? extends AnnotatedTypeMirror> paramTypes = methodType.getParameterTypes();
for (int i = 0; i < methodType.getParameterTypes().size(); ++i) {
VariableElement paramElt = paramElts.get(i);
AnnotatedTypeMirror paramType = paramTypes.get(i);
Parameter param = params.get(i);
recordDeclAnnotation(paramElt, param.getAnnotations(), param);
recordDeclAnnotation(paramElt, param.getType().getAnnotations(), param);
if (param.isVarArgs()) {
assert paramType.getKind() == TypeKind.ARRAY;
// The "type" of param is actually the component type of the vararg.
// For example, in "Object..." the type would be "Object".
annotate(
((AnnotatedArrayType) paramType).getComponentType(),
param.getType(),
param.getAnnotations(),
param);
// The "VarArgsAnnotations" are those just before "...".
annotate(paramType, param.getVarArgsAnnotations(), param);
} else {
annotate(paramType, param.getType(), param.getAnnotations(), param);
putMerge(annotationFileAnnos.atypes, paramElt, paramType);
}
}
}
/**
* Clear (remove) existing annotations on the type.
*
* <p>Stub files override annotations read from .class files. Using {@code replaceAnnotation}
* usually achieves this; however, for annotations on type variables, it is sometimes necessary to
* remove an existing annotation, leaving no annotation on the type variable. This method does so.
*
* @param atype the type to modify
* @param typeDef the type from the annotation file, used only for diagnostic messages
*/
@SuppressWarnings("unused") // for disabled warning message
private void clearAnnotations(AnnotatedTypeMirror atype, Type typeDef) {
// TODO: only produce output if the removed annotation isn't the top or default
// annotation in the type hierarchy. See https://tinyurl.com/cfissue/2759 .
/*
if (!atype.getAnnotations().isEmpty()) {
stubWarnOverwritesBytecode(
String.format(
"in file %s at line %s removed existing annotations on type: %s",
filename.substring(filename.lastIndexOf('/') + 1),
typeDef.getBegin().get().line,
atype.toString(true)));
}
*/
// Clear existing annotations, which only makes a difference for
// type variables, but doesn't hurt in other cases.
atype.clearAnnotations();
}
/**
* Add the annotations from {@code type} to {@code atype}. Type annotations that parsed as
* declaration annotations (i.e., type annotations in {@code declAnnos}) are applied to the
* innermost component type.
*
* @param atype annotated type to which to add annotations
* @param type parsed type
* @param declAnnos annotations stored on the declaration of the variable with this type or null
* @param astNode where to report errors
*/
private void annotateAsArray(
AnnotatedArrayType atype,
ReferenceType type,
@Nullable NodeList<AnnotationExpr> declAnnos,
NodeWithRange<?> astNode) {
annotateInnermostComponentType(atype, declAnnos, astNode);
Type typeDef = type;
AnnotatedTypeMirror currentAtype = atype;
while (typeDef.isArrayType()) {
if (currentAtype.getKind() != TypeKind.ARRAY) {
warn(astNode, "Mismatched array lengths; atype: " + atype + "%n type: " + type);
return;
}
// handle generic type
clearAnnotations(currentAtype, typeDef);
List<AnnotationExpr> annotations = typeDef.getAnnotations();
if (annotations != null) {
annotate(currentAtype, annotations, astNode);
}
typeDef = ((com.github.javaparser.ast.type.ArrayType) typeDef).getComponentType();
currentAtype = ((AnnotatedArrayType) currentAtype).getComponentType();
}
if (currentAtype.getKind() == TypeKind.ARRAY) {
warn(astNode, "Mismatched array lengths; atype: " + atype + "%n type: " + type);
}
}
private ClassOrInterfaceType unwrapDeclaredType(Type type) {
if (type instanceof ClassOrInterfaceType) {
return (ClassOrInterfaceType) type;
} else if (type instanceof ReferenceType && type.getArrayLevel() == 0) {
return unwrapDeclaredType(type.getElementType());
} else {
return null;
}
}
/**
* Add to formal parameter {@code atype}:
*
* <ol>
* <li>the annotations from {@code typeDef}, and
* <li>any type annotations that parsed as declaration annotations (i.e., type annotations in
* {@code declAnnos}).
* </ol>
*
* @param atype annotated type to which to add annotations
* @param typeDef parsed type
* @param declAnnos annotations stored on the declaration of the variable with this type, or null
* @param astNode where to report errors
*/
private void annotate(
AnnotatedTypeMirror atype,
Type typeDef,
@Nullable NodeList<AnnotationExpr> declAnnos,
NodeWithRange<?> astNode) {
if (atype.getKind() == TypeKind.ARRAY) {
if (typeDef instanceof ReferenceType) {
annotateAsArray((AnnotatedArrayType) atype, (ReferenceType) typeDef, declAnnos, astNode);
} else {
warn(astNode, "expected ReferenceType but found: " + typeDef);
}
return;
}
clearAnnotations(atype, typeDef);
// Primary annotations for the type of a variable declaration are not stored in typeDef, but
// rather as declaration annotations (passed as declAnnos to this method). But, if typeDef
// is not the type of a variable, then the primary annotations are stored in typeDef.
NodeList<AnnotationExpr> primaryAnnotations;
if (typeDef.getAnnotations().isEmpty() && declAnnos != null) {
primaryAnnotations = declAnnos;
} else {
primaryAnnotations = typeDef.getAnnotations();
}
if (atype.getKind() != TypeKind.WILDCARD) {
// The primary annotation on a wildcard applies to the super or extends bound and
// are added below.
annotate(atype, primaryAnnotations, astNode);
}
switch (atype.getKind()) {
case DECLARED:
ClassOrInterfaceType declType = unwrapDeclaredType(typeDef);
if (declType == null) {
break;
}
AnnotatedDeclaredType adeclType = (AnnotatedDeclaredType) atype;
// Process type arguments.
if (declType.getTypeArguments().isPresent()
&& !declType.getTypeArguments().get().isEmpty()
&& !adeclType.getTypeArguments().isEmpty()) {
if (declType.getTypeArguments().get().size() != adeclType.getTypeArguments().size()) {
warn(
astNode,
String.format(
"Mismatch in type argument size between %s (%d) and %s (%d)",
declType,
declType.getTypeArguments().get().size(),
adeclType,
adeclType.getTypeArguments().size()));
break;
}
for (int i = 0; i < declType.getTypeArguments().get().size(); ++i) {
annotate(
adeclType.getTypeArguments().get(i),
declType.getTypeArguments().get().get(i),
null,
astNode);
}
}
break;
case WILDCARD:
AnnotatedWildcardType wildcardType = (AnnotatedWildcardType) atype;
// Ensure that the file also has a wildcard type, report an error otherwise
if (!typeDef.isWildcardType()) {
// We throw an error here, as otherwise we are just getting a generic cast error
// on the very next line.
warn(
astNode,
"Wildcard type <"
+ atype
+ "> does not match type in stubs file"
+ filename
+ ": <"
+ typeDef
+ ">"
+ " while parsing "
+ typeBeingParsed);
return;
}
WildcardType wildcardDef = (WildcardType) typeDef;
if (wildcardDef.getExtendedType().isPresent()) {
annotate(
wildcardType.getExtendsBound(), wildcardDef.getExtendedType().get(), null, astNode);
annotate(wildcardType.getSuperBound(), primaryAnnotations, astNode);
} else if (wildcardDef.getSuperType().isPresent()) {
annotate(wildcardType.getSuperBound(), wildcardDef.getSuperType().get(), null, astNode);
annotate(wildcardType.getExtendsBound(), primaryAnnotations, astNode);
} else {
annotate(atype, primaryAnnotations, astNode);
}
break;
case TYPEVAR:
// Add annotations from the declaration of the TypeVariable
AnnotatedTypeVariable typeVarUse = (AnnotatedTypeVariable) atype;
Types typeUtils = processingEnv.getTypeUtils();
for (AnnotatedTypeVariable typePar : typeParameters) {
if (typeUtils.isSameType(typePar.getUnderlyingType(), atype.getUnderlyingType())) {
atypeFactory.replaceAnnotations(typePar.getUpperBound(), typeVarUse.getUpperBound());
atypeFactory.replaceAnnotations(typePar.getLowerBound(), typeVarUse.getLowerBound());
}
}
break;
default:
// No additional annotations to add.
}
}
/**
* Process the field declaration in decl: copy its annotations to {@code #annotationFileAnnos}.
*
* @param decl the declaration in the annotation file
* @param elt the element representing that same declaration
*/
private void processField(FieldDeclaration decl, VariableElement elt) {
if (skipNode(decl)) {
// Don't process private fields of the JDK. They can't be referenced outside of the JDK
// and might refer to types that are not accessible.
return;
}
markAsFromStubFile(elt);
recordDeclAnnotation(elt, decl.getAnnotations(), decl);
// AnnotationFileParser parses all annotations in type annotation position as type annotations
recordDeclAnnotation(elt, decl.getElementType().getAnnotations(), decl);
AnnotatedTypeMirror fieldType = atypeFactory.fromElement(elt);
VariableDeclarator fieldVarDecl = null;
String eltName = elt.getSimpleName().toString();
for (VariableDeclarator var : decl.getVariables()) {
if (var.getName().toString().equals(eltName)) {
fieldVarDecl = var;
break;
}
}
assert fieldVarDecl != null;
annotate(fieldType, fieldVarDecl.getType(), decl.getAnnotations(), fieldVarDecl);
putMerge(annotationFileAnnos.atypes, elt, fieldType);
}
/**
* Adds the annotations present on the declaration of an enum constant to the ATM of that
* constant.
*
* @param decl the enum constant, in Javaparser AST form (the source of annotations)
* @param elt the enum constant declaration, as an element (the destination for annotations)
*/
private void processEnumConstant(EnumConstantDeclaration decl, VariableElement elt) {
markAsFromStubFile(elt);
recordDeclAnnotation(elt, decl.getAnnotations(), decl);
AnnotatedTypeMirror enumConstType = atypeFactory.fromElement(elt);
annotate(enumConstType, decl.getAnnotations(), decl);
putMerge(annotationFileAnnos.atypes, elt, enumConstType);
}
/**
* Returns the innermost component type of {@code type}.
*
* @param type array type
* @return the innermost component type of {@code type}
*/
private AnnotatedTypeMirror innermostComponentType(AnnotatedArrayType type) {
AnnotatedTypeMirror componentType = type;
while (componentType.getKind() == TypeKind.ARRAY) {
componentType = ((AnnotatedArrayType) componentType).getComponentType();
}
return componentType;
}
/**
* Adds {@code annotations} to the innermost component type of {@code type}.
*
* @param type array type
* @param annotations annotations to add
* @param astNode where to report errors
*/
private void annotateInnermostComponentType(
AnnotatedArrayType type, List<AnnotationExpr> annotations, NodeWithRange<?> astNode) {
annotate(innermostComponentType(type), annotations, astNode);
}
/**
* Annotate the type with the given type annotations, removing any existing annotations from the
* same qualifier hierarchies.
*
* @param type the type to annotate
* @param annotations the new annotations for the type
* @param astNode where to report errors
*/
private void annotate(
AnnotatedTypeMirror type, List<AnnotationExpr> annotations, NodeWithRange<?> astNode) {
if (annotations == null) {
return;
}
for (AnnotationExpr annotation : annotations) {
AnnotationMirror annoMirror = getAnnotation(annotation, allAnnotations);
if (annoMirror != null) {
type.replaceAnnotation(annoMirror);
} else {
// TODO: Maybe always warn here. It's so easy to forget an import statement and
// have an annotation silently ignored.
stubWarnNotFound(astNode, "Unknown annotation " + annotation);
}
}
}
/**
* Adds to {@code annotationFileAnnos} all the annotations in {@code annotations} that are
* applicable to {@code elt}'s location. For example, if an annotation is a type annotation but
* {@code elt} is a field declaration, the type annotation will be ignored.
*
* @param elt the element to be annotated
* @param annotations set of annotations that may be applicable to elt
* @param astNode where to report errors
*/
private void recordDeclAnnotation(
Element elt, List<AnnotationExpr> annotations, NodeWithRange<?> astNode) {
if (annotations == null || annotations.isEmpty()) {
return;
}
Set<AnnotationMirror> annos = AnnotationUtils.createAnnotationSet();
for (AnnotationExpr annotation : annotations) {
AnnotationMirror annoMirror = getAnnotation(annotation, allAnnotations);
if (annoMirror != null) {
// The @Target annotation on `annotation`/`annoMirror`
Target target = annoMirror.getAnnotationType().asElement().getAnnotation(Target.class);
// Only add the declaration annotation if the annotation applies to the element.
if (AnnotationUtils.getElementKindsForTarget(target).contains(elt.getKind())) {
// `annoMirror` is applicable to `elt`
annos.add(annoMirror);
}
} else {
// TODO: Maybe always warn here. It's so easy to forget an import statement and
// have an annotation silently ignored.
stubWarnNotFound(astNode, String.format("Unknown annotation %s", annotation));
}
}
String eltName = ElementUtils.getQualifiedName(elt);
putOrAddToDeclAnnos(eltName, annos);
}
/**
* Adds the declaration annotation {@code @FromStubFile} to {@link #annotationFileAnnos}, unless
* we are parsing the JDK as a stub file.
*
* @param elt an element to be annotated as {@code @FromStubFile}
*/
private void markAsFromStubFile(Element elt) {
if (fileType == AnnotationFileType.AJAVA || fileType == AnnotationFileType.JDK_STUB) {
return;
}
putOrAddToDeclAnnos(
ElementUtils.getQualifiedName(elt), Collections.singleton(fromStubFileAnno));
}
private void annotateTypeParameters(
BodyDeclaration<?> decl, // for debugging
Object elt, // for debugging; TypeElement or ExecutableElement
List<? extends AnnotatedTypeMirror> typeArguments,
List<TypeParameter> typeParameters) {
if (typeParameters == null) {
return;
}
if (typeParameters.size() != typeArguments.size()) {
String msg =
String.format(
"annotateTypeParameters: mismatched sizes: typeParameters (size %d)=%s; "
+ " typeArguments (size %d)=%s; decl=%s; elt=%s (%s).",
typeParameters.size(),
typeParameters,
typeArguments.size(),
typeArguments,
decl.toString().replace(LINE_SEPARATOR, " "),
elt.toString().replace(LINE_SEPARATOR, " "),
elt.getClass());
if (!debugAnnotationFileParser) {
msg = msg + "; for more details, run with -AstubDebug";
}
warn(decl, msg);
return;
}
for (int i = 0; i < typeParameters.size(); ++i) {
TypeParameter param = typeParameters.get(i);
AnnotatedTypeVariable paramType = (AnnotatedTypeVariable) typeArguments.get(i);
if (param.getTypeBound() == null || param.getTypeBound().isEmpty()) {
// No bound so annotations are both lower and upper bounds
annotate(paramType, param.getAnnotations(), param);
} else if (param.getTypeBound() != null && !param.getTypeBound().isEmpty()) {
annotate(paramType.getLowerBound(), param.getAnnotations(), param);
annotate(paramType.getUpperBound(), param.getTypeBound().get(0), null, param);
if (param.getTypeBound().size() > 1) {
// TODO: add support for intersection types
stubWarnNotFound(param, "Annotations on intersection types are not yet supported");
}
}
putMerge(annotationFileAnnos.atypes, paramType.getUnderlyingType().asElement(), paramType);
}
}
/**
* Returns a pair of mappings. For each member declaration of the JavaParser type declaration
* {@code typeDecl}:
*
* <ul>
* <li>If {@code typeElt} contains a member element for it, the first mapping maps the member
* element to it.
* <li>If it is a fake override, the second mapping maps each element it overrides to it.
* <li>Otherwise, does nothing.
* </ul>
*
* This method does not read or write the field {@link #annotationFileAnnos}.
*
* @param typeDecl a JavaParser type declaration
* @param typeElt the javac element for {@code typeDecl}
* @return two mappings: from javac elements to their JavaParser declaration, and from javac
* elements to fake overrides of them
* @param astNode where to report errors
*/
private Pair<Map<Element, BodyDeclaration<?>>, Map<Element, List<BodyDeclaration<?>>>> getMembers(
TypeDeclaration<?> typeDecl, TypeElement typeElt, NodeWithRange<?> astNode) {
assert (typeElt.getSimpleName().contentEquals(typeDecl.getNameAsString())
|| typeDecl.getNameAsString().endsWith("$" + typeElt.getSimpleName()))
: String.format("%s %s", typeElt.getSimpleName(), typeDecl.getName());
Map<Element, BodyDeclaration<?>> elementsToDecl = new LinkedHashMap<>();
Map<Element, List<BodyDeclaration<?>>> fakeOverrideDecls = new LinkedHashMap<>();
for (BodyDeclaration<?> member : typeDecl.getMembers()) {
putNewElement(
elementsToDecl, fakeOverrideDecls, typeElt, member, typeDecl.getNameAsString(), astNode);
}
// For an enum type declaration, also add the enum constants
if (typeDecl instanceof EnumDeclaration) {
EnumDeclaration enumDecl = (EnumDeclaration) typeDecl;
// getEntries() gives the list of enum constant declarations
for (BodyDeclaration<?> member : enumDecl.getEntries()) {
putNewElement(
elementsToDecl,
fakeOverrideDecls,
typeElt,
member,
typeDecl.getNameAsString(),
astNode);
}
}
return Pair.of(elementsToDecl, fakeOverrideDecls);
}
// Used only by getMembers().
/**
* If {@code typeElt} contains an element for {@code member}, adds to {@code elementsToDecl} a
* mapping from member's element to member. Does nothing if a mapping already exists.
*
* <p>Otherwise (if there is no element for {@code member}), adds to {@code fakeOverrideDecls}
* zero or more mappings. Each mapping is from an element that {@code member} would override to
* {@code member}.
*
* <p>This method does not read or write field {@link annotationFileAnnos}.
*
* @param elementsToDecl the mapping that is side-effected by this method
* @param fakeOverrideDecls fake overrides, also side-effected by this method
* @param typeElt the class in which {@code member} is declared
* @param member the stub file declaration of a method
* @param typeDeclName used only for debugging
* @param astNode where to report errors
*/
private void putNewElement(
Map<Element, BodyDeclaration<?>> elementsToDecl,
Map<Element, List<BodyDeclaration<?>>> fakeOverrideDecls,
TypeElement typeElt,
BodyDeclaration<?> member,
String typeDeclName,
NodeWithRange<?> astNode) {
if (member instanceof MethodDeclaration) {
MethodDeclaration method = (MethodDeclaration) member;
Element elt = findElement(typeElt, method, /*noWarn=*/ true);
if (elt != null) {
putIfAbsent(elementsToDecl, elt, method);
} else {
ExecutableElement overriddenMethod = fakeOverriddenMethod(typeElt, method);
if (overriddenMethod == null) {
// Didn't find the element and it isn't a fake override. Issue a warning.
findElement(typeElt, method, /*noWarn=*/ false);
} else {
List<BodyDeclaration<?>> l =
fakeOverrideDecls.computeIfAbsent(overriddenMethod, __ -> new ArrayList<>());
l.add(member);
}
}
} else if (member instanceof ConstructorDeclaration) {
Element elt = findElement(typeElt, (ConstructorDeclaration) member);
if (elt != null) {
putIfAbsent(elementsToDecl, elt, member);
}
} else if (member instanceof FieldDeclaration) {
FieldDeclaration fieldDecl = (FieldDeclaration) member;
for (VariableDeclarator var : fieldDecl.getVariables()) {
Element varelt = findElement(typeElt, var);
if (varelt != null) {
putIfAbsent(elementsToDecl, varelt, fieldDecl);
}
}
} else if (member instanceof EnumConstantDeclaration) {
Element elt = findElement(typeElt, (EnumConstantDeclaration) member, astNode);
if (elt != null) {
putIfAbsent(elementsToDecl, elt, member);
}
} else if (member instanceof ClassOrInterfaceDeclaration) {
Element elt = findElement(typeElt, (ClassOrInterfaceDeclaration) member);
if (elt != null) {
putIfAbsent(elementsToDecl, elt, member);
}
} else if (member instanceof EnumDeclaration) {
Element elt = findElement(typeElt, (EnumDeclaration) member);
if (elt != null) {
putIfAbsent(elementsToDecl, elt, member);
}
} else {
stubDebug(
String.format("Ignoring element of type %s in %s", member.getClass(), typeDeclName));
}
}
/**
* Given a method declaration that does not correspond to an element, returns the method it
* directly overrides or implements. As Java does, this prefers a method in a superclass to one in
* an interface.
*
* <p>As with regular overrides, the parameter types must be exact matches; contravariance is not
* permitted.
*
* @param typeElt the type in which the method appears
* @param methodDecl the method declaration that does not correspond to an element
* @return the methods that the given method declaration would override, or null if none
*/
private @Nullable ExecutableElement fakeOverriddenMethod(
TypeElement typeElt, MethodDeclaration methodDecl) {
for (Element elt : typeElt.getEnclosedElements()) {
if (elt.getKind() != ElementKind.METHOD) {
continue;
}
ExecutableElement candidate = (ExecutableElement) elt;
if (!candidate.getSimpleName().contentEquals(methodDecl.getName().getIdentifier())) {
continue;
}
List<? extends VariableElement> candidateParams = candidate.getParameters();
if (sameTypes(candidateParams, methodDecl.getParameters())) {
return candidate;
}
}
TypeElement superType = ElementUtils.getSuperClass(typeElt);
if (superType != null) {
ExecutableElement result = fakeOverriddenMethod(superType, methodDecl);
if (result != null) {
return result;
}
}
for (TypeMirror interfaceTypeMirror : typeElt.getInterfaces()) {
TypeElement interfaceElement = (TypeElement) ((DeclaredType) interfaceTypeMirror).asElement();
ExecutableElement result = fakeOverriddenMethod(interfaceElement, methodDecl);
if (result != null) {
return result;
}
}
return null;
}
/**
* Returns true if the two signatures (represented as lists of formal parameters) are the same. No
* contravariance is permitted.
*
* @param javacParams parameter list in javac form
* @param javaParserParams parameter list in JavaParser form
* @return true if the two signatures are the same
*/
private boolean sameTypes(
List<? extends VariableElement> javacParams, NodeList<Parameter> javaParserParams) {
if (javacParams.size() != javaParserParams.size()) {
return false;
}
for (int i = 0; i < javacParams.size(); i++) {
TypeMirror javacType = javacParams.get(i).asType();
Parameter javaParserParam = javaParserParams.get(i);
Type javaParserType = javaParserParam.getType();
if (javacType.getKind() == TypeKind.TYPEVAR) {
// TODO: Hack, need to viewpoint-adapt.
javacType = ((TypeVariable) javacType).getUpperBound();
}
if (!sameType(javacType, javaParserType)) {
return false;
}
}
return true;
}
/**
* Returns true if the two types are the same.
*
* @param javacType type in javac form
* @param javaParserType type in JavaParser form
* @return true if the two types are the same
*/
private boolean sameType(TypeMirror javacType, Type javaParserType) {
switch (javacType.getKind()) {
case BOOLEAN:
return javaParserType.equals(PrimitiveType.booleanType());
case BYTE:
return javaParserType.equals(PrimitiveType.byteType());
case CHAR:
return javaParserType.equals(PrimitiveType.charType());
case DOUBLE:
return javaParserType.equals(PrimitiveType.doubleType());
case FLOAT:
return javaParserType.equals(PrimitiveType.floatType());
case INT:
return javaParserType.equals(PrimitiveType.intType());
case LONG:
return javaParserType.equals(PrimitiveType.longType());
case SHORT:
return javaParserType.equals(PrimitiveType.shortType());
case DECLARED:
case TYPEVAR:
if (!(javaParserType instanceof ClassOrInterfaceType)) {
return false;
}
com.sun.tools.javac.code.Type javacTypeInternal = (com.sun.tools.javac.code.Type) javacType;
ClassOrInterfaceType javaParserClassType = (ClassOrInterfaceType) javaParserType;
// Use asString() because toString() includes annotations.
String javaParserString = javaParserClassType.asString();
Element javacElement = javacTypeInternal.asElement();
// Check both fully-qualified name and simple name.
return javacElement.toString().equals(javaParserString)
|| javacElement.getSimpleName().contentEquals(javaParserString);
case ARRAY:
return javaParserType.isArrayType()
&& sameType(
((ArrayType) javacType).getComponentType(),
javaParserType.asArrayType().getComponentType());
default:
throw new BugInCF("Unhandled type %s of kind %s", javacType, javacType.getKind());
}
}
/**
* Process a fake override: copy its annotations to the fake overrides part of {@code
* #annotationFileAnnos}.
*
* @param element a real element
* @param decl a fake override of the element
* @param fakeLocation where the fake override was defined
*/
private void processFakeOverride(
ExecutableElement element, CallableDeclaration<?> decl, TypeElement fakeLocation) {
// This is a fresh type, which this code may side-effect.
AnnotatedExecutableType methodType = atypeFactory.getAnnotatedType(element);
// Here is a hacky solution that does not use the visitor. It just handles the return type.
// TODO: Walk the type and the declaration, copying annotations from the declaration to the
// element. I think PR #3977 has a visitor that does that, which I should use after it is
// merged.
// The annotations on the method. These include type annotations on the return type.
NodeList<AnnotationExpr> annotations = decl.getAnnotations();
annotate(methodType.getReturnType(), ((MethodDeclaration) decl).getType(), annotations, decl);
List<Pair<TypeMirror, AnnotatedTypeMirror>> l =
annotationFileAnnos.fakeOverrides.computeIfAbsent(element, __ -> new ArrayList<>());
l.add(Pair.of(fakeLocation.asType(), methodType));
}
/**
* Return the annotated type corresponding to {@code type}, or null if none exists. More
* specifically, returns the element of {@code types} whose name matches {@code type}.
*
* @param type the type to search for
* @param types the list of AnnotatedDeclaredTypes to search in
* @param astNode where to report errors
* @return the annotated type in {@code types} corresponding to {@code type}, or null if none
* exists
*/
private @Nullable AnnotatedDeclaredType findAnnotatedType(
ClassOrInterfaceType type, List<AnnotatedDeclaredType> types, NodeWithRange<?> astNode) {
String typeString = type.getNameAsString();
for (AnnotatedDeclaredType supertype : types) {
if (supertype.getUnderlyingType().asElement().getSimpleName().contentEquals(typeString)) {
return supertype;
}
}
stubWarnNotFound(astNode, "Supertype " + typeString + " not found");
if (debugAnnotationFileParser) {
stubDebug("Supertypes that were searched:");
for (AnnotatedDeclaredType supertype : types) {
stubDebug(String.format(" %s", supertype));
}
}
return null;
}
/**
* Looks for the nested type element in the typeElt and returns it if the element has the same
* name as provided class or interface declaration. In case nested element is not found it returns
* null.
*
* @param typeElt an element where nested type element should be looked for
* @param ciDecl class or interface declaration which name should be found among nested elements
* of the typeElt
* @return nested in typeElt element with the name of the class or interface or null if nested
* element is not found
*/
private @Nullable Element findElement(TypeElement typeElt, ClassOrInterfaceDeclaration ciDecl) {
final String wantedClassOrInterfaceName = ciDecl.getNameAsString();
for (TypeElement typeElement : ElementUtils.getAllTypeElementsIn(typeElt)) {
if (wantedClassOrInterfaceName.equals(typeElement.getSimpleName().toString())) {
return typeElement;
}
}
stubWarnNotFound(
ciDecl, "Class/interface " + wantedClassOrInterfaceName + " not found in type " + typeElt);
if (debugAnnotationFileParser) {
stubDebug(String.format(" Here are the type declarations of %s:", typeElt));
for (TypeElement method : ElementFilter.typesIn(typeElt.getEnclosedElements())) {
stubDebug(String.format(" %s", method));
}
}
return null;
}
/**
* Looks for the nested enum element in the typeElt and returns it if the element has the same
* name as provided enum declaration. In case nested element is not found it returns null.
*
* @param typeElt an element where nested enum element should be looked for
* @param enumDecl enum declaration which name should be found among nested elements of the
* typeElt
* @return nested in typeElt enum element with the name of the provided enum or null if nested
* element is not found
*/
private @Nullable Element findElement(TypeElement typeElt, EnumDeclaration enumDecl) {
final String wantedEnumName = enumDecl.getNameAsString();
for (TypeElement typeElement : ElementUtils.getAllTypeElementsIn(typeElt)) {
if (wantedEnumName.equals(typeElement.getSimpleName().toString())) {
return typeElement;
}
}
stubWarnNotFound(enumDecl, "Enum " + wantedEnumName + " not found in type " + typeElt);
if (debugAnnotationFileParser) {
stubDebug(String.format(" Here are the type declarations of %s:", typeElt));
for (TypeElement method : ElementFilter.typesIn(typeElt.getEnclosedElements())) {
stubDebug(String.format(" %s", method));
}
}
return null;
}
/**
* Looks for an enum constant element in the typeElt and returns it if the element has the same
* name as provided. In case enum constant element is not found it returns null.
*
* @param typeElt type element where enum constant element should be looked for
* @param enumConstDecl the declaration of the enum constant
* @param astNode where to report errors
* @return enum constant element in typeElt with the provided name or null if enum constant
* element is not found
*/
private @Nullable VariableElement findElement(
TypeElement typeElt, EnumConstantDeclaration enumConstDecl, NodeWithRange<?> astNode) {
final String enumConstName = enumConstDecl.getNameAsString();
return findFieldElement(typeElt, enumConstName, astNode);
}
/**
* Looks for a method element in {@code typeElt} that has the same name and formal parameter types
* as {@code methodDecl}. Returns null, and possibly issues a warning, if no such method element
* is found.
*
* @param typeElt type element where method element should be looked for
* @param methodDecl method declaration with signature that should be found among methods in the
* typeElt
* @param noWarn if true, don't issue a warning if the element is not found
* @return method element in typeElt with the same signature as the provided method declaration or
* null if method element is not found
*/
private @Nullable ExecutableElement findElement(
TypeElement typeElt, MethodDeclaration methodDecl, boolean noWarn) {
if (skipNode(methodDecl)) {
return null;
}
final String wantedMethodName = methodDecl.getNameAsString();
final int wantedMethodParams =
(methodDecl.getParameters() == null) ? 0 : methodDecl.getParameters().size();
final String wantedMethodString = AnnotationFileUtil.toString(methodDecl);
for (ExecutableElement method : ElementFilter.methodsIn(typeElt.getEnclosedElements())) {
if (wantedMethodParams == method.getParameters().size()
&& wantedMethodName.contentEquals(method.getSimpleName().toString())
&& ElementUtils.getSimpleSignature(method).equals(wantedMethodString)) {
return method;
}
}
if (!noWarn) {
if (methodDecl.getAccessSpecifier() == AccessSpecifier.PACKAGE_PRIVATE) {
// This might be a false positive warning. The stub parser permits a stub file to
// omit the access specifier, but package-private methods aren't in the TypeElement.
stubWarnNotFound(
methodDecl,
"Package-private method "
+ wantedMethodString
+ " not found in type "
+ typeElt
+ System.lineSeparator()
+ "If the method is not package-private, add an access specifier in the stub file"
+ " and use pass -AstubDebug to receive a more useful error message.");
} else {
stubWarnNotFound(
methodDecl, "Method " + wantedMethodString + " not found in type " + typeElt);
if (debugAnnotationFileParser) {
stubDebug(String.format(" Here are the methods of %s:", typeElt));
for (ExecutableElement method : ElementFilter.methodsIn(typeElt.getEnclosedElements())) {
stubDebug(String.format(" %s", method));
}
}
}
}
return null;
}
/**
* Looks for a constructor element in the typeElt and returns it if the element has the same
* signature as provided constructor declaration. In case constructor element is not found it
* returns null.
*
* @param typeElt type element where constructor element should be looked for
* @param constructorDecl constructor declaration with signature that should be found among
* constructors in the typeElt
* @return constructor element in typeElt with the same signature as the provided constructor
* declaration or null if constructor element is not found
*/
private @Nullable ExecutableElement findElement(
TypeElement typeElt, ConstructorDeclaration constructorDecl) {
if (skipNode(constructorDecl)) {
return null;
}
final int wantedMethodParams =
(constructorDecl.getParameters() == null) ? 0 : constructorDecl.getParameters().size();
final String wantedMethodString = AnnotationFileUtil.toString(constructorDecl);
for (ExecutableElement method : ElementFilter.constructorsIn(typeElt.getEnclosedElements())) {
if (wantedMethodParams == method.getParameters().size()
&& ElementUtils.getSimpleSignature(method).equals(wantedMethodString)) {
return method;
}
}
stubWarnNotFound(
constructorDecl, "Constructor " + wantedMethodString + " not found in type " + typeElt);
if (debugAnnotationFileParser) {
for (ExecutableElement method : ElementFilter.constructorsIn(typeElt.getEnclosedElements())) {
stubDebug(String.format(" %s", method));
}
}
return null;
}
/**
* Returns the element for the given variable.
*
* @param typeElt the type in which the variable is contained
* @param variable the variable whose element to return
* @return the element for the given variable
*/
private VariableElement findElement(TypeElement typeElt, VariableDeclarator variable) {
final String fieldName = variable.getNameAsString();
return findFieldElement(typeElt, fieldName, variable);
}
/**
* Looks for a field element in the typeElt and returns it if the element has the same name as
* provided. In case field element is not found it returns null.
*
* @param typeElt type element where field element should be looked for
* @param fieldName field name that should be found
* @param astNode where to report errors
* @return field element in typeElt with the provided name or null if field element is not found
*/
private @Nullable VariableElement findFieldElement(
TypeElement typeElt, String fieldName, NodeWithRange<?> astNode) {
for (VariableElement field : ElementUtils.getAllFieldsIn(typeElt, elements)) {
// field.getSimpleName() is a CharSequence, not a String
if (fieldName.equals(field.getSimpleName().toString())) {
return field;
}
}
stubWarnNotFound(astNode, "Field " + fieldName + " not found in type " + typeElt);
if (debugAnnotationFileParser) {
for (VariableElement field : ElementFilter.fieldsIn(typeElt.getEnclosedElements())) {
stubDebug(String.format(" %s", field));
}
}
return null;
}
/**
* Given a fully-qualified type name, return a TypeElement for it, or null if none exists. Also
* cache in importedTypes.
*
* @param name a fully-qualified type name
* @return a TypeElement for the name, or null
*/
private @Nullable TypeElement getTypeElementOrNull(@FullyQualifiedName String name) {
TypeElement typeElement = elements.getTypeElement(name);
if (typeElement != null) {
importedTypes.put(name, typeElement);
}
// for debugging: warn("getTypeElementOrNull(%s) => %s", name, typeElement);
return typeElement;
}
/**
* Get the type element for the given fully-qualified type name. If none is found, issue a warning
* and return null.
*
* @param typeName a type name
* @param msg a warning message to issue if the type element for {@code typeName} cannot be found
* @param astNode where to report errors
* @return the type element for the given fully-qualified type name, or null
*/
private TypeElement getTypeElement(
@FullyQualifiedName String typeName, String msg, NodeWithRange<?> astNode) {
TypeElement classElement = elements.getTypeElement(typeName);
if (classElement == null) {
stubWarnNotFound(astNode, msg + ": " + typeName);
}
return classElement;
}
/**
* Returns the element for the given package.
*
* @param packageName the package's name
* @param astNode where to report errors
* @return the element for the given package
*/
private PackageElement findPackage(String packageName, NodeWithRange<?> astNode) {
PackageElement packageElement = elements.getPackageElement(packageName);
if (packageElement == null) {
stubWarnNotFound(astNode, "Imported package not found: " + packageName);
}
return packageElement;
}
/**
* Returns true if one of the annotations is {@link AnnotatedFor} and this checker is in its list
* of checkers. If none of the annotations are {@code AnnotatedFor}, then also return true.
*
* @param annotations a list of JavaParser annotations
* @return true if one of the annotations is {@link AnnotatedFor} and its list of checkers does
* not contain this checker
*/
private boolean isAnnotatedForThisChecker(List<AnnotationExpr> annotations) {
if (fileType == AnnotationFileType.JDK_STUB) {
// The JDK stubs have purity annotations that should be read for all checkers.
// TODO: Parse the JDK stubs, but only save the declaration annotations.
return true;
}
for (AnnotationExpr ae : annotations) {
if (ae.getNameAsString().equals("AnnotatedFor")
|| ae.getNameAsString().equals("org.checkerframework.framework.qual.AnnotatedFor")) {
AnnotationMirror af = getAnnotation(ae, allAnnotations);
if (atypeFactory.areSameByClass(af, AnnotatedFor.class)) {
return atypeFactory.doesAnnotatedForApplyToThisChecker(af);
}
}
}
return true;
}
/**
* Convert {@code annotation} into an AnnotationMirror. Returns null if the annotation isn't
* supported by the checker or if some error occurred while converting it.
*
* @param annotation syntax tree for an annotation
* @param allAnnotations map from simple name to annotation definition; side-effected by this
* method
* @return the AnnotationMirror for the annotation, or null if it cannot be built
*/
private @Nullable AnnotationMirror getAnnotation(
AnnotationExpr annotation, Map<String, TypeElement> allAnnotations) {
@SuppressWarnings("signature") // https://tinyurl.com/cfissue/3094
@FullyQualifiedName String annoNameFq = annotation.getNameAsString();
TypeElement annoTypeElt = allAnnotations.get(annoNameFq);
if (annoTypeElt == null) {
// If the annotation was not imported, then #getImportedAnnotations did not add it to the
// allAnnotations field. This code adds the annotation when it is encountered (i.e. here).
// Note that this does not call AnnotationFileParser#getTypeElement to avoid a spurious
// diagnostic if the annotation is actually unknown.
annoTypeElt = elements.getTypeElement(annoNameFq);
if (annoTypeElt == null) {
// Not a supported annotation -> ignore
return null;
}
putAllNew(allAnnotations, createNameToAnnotationMap(Collections.singletonList(annoTypeElt)));
}
@SuppressWarnings("signature") // not anonymous, so name is not empty
@CanonicalName String annoName = annoTypeElt.getQualifiedName().toString();
if (annotation instanceof MarkerAnnotationExpr) {
return AnnotationBuilder.fromName(elements, annoName);
} else if (annotation instanceof NormalAnnotationExpr) {
NormalAnnotationExpr nrmanno = (NormalAnnotationExpr) annotation;
AnnotationBuilder builder = new AnnotationBuilder(processingEnv, annoName);
List<MemberValuePair> pairs = nrmanno.getPairs();
if (pairs != null) {
for (MemberValuePair mvp : pairs) {
String member = mvp.getNameAsString();
Expression exp = mvp.getValue();
try {
builderAddElement(builder, member, exp);
} catch (AnnotationFileParserException e) {
warn(
exp,
"For annotation %s, could not add %s=%s because %s",
annotation,
member,
exp,
e.getMessage());
return null;
}
}
}
return builder.build();
} else if (annotation instanceof SingleMemberAnnotationExpr) {
SingleMemberAnnotationExpr sglanno = (SingleMemberAnnotationExpr) annotation;
AnnotationBuilder builder = new AnnotationBuilder(processingEnv, annoName);
Expression valExpr = sglanno.getMemberValue();
try {
builderAddElement(builder, "value", valExpr);
} catch (AnnotationFileParserException e) {
warn(
valExpr,
"For annotation %s, could not add value=%s because %s",
annotation,
valExpr,
e.getMessage());
return null;
}
return builder.build();
} else {
throw new BugInCF("AnnotationFileParser: unknown annotation type: " + annotation);
}
}
/**
* Returns the value of {@code expr}.
*
* @param name the name of an annotation element/argument, used for diagnostic messages
* @param expr the expression to determine the value of
* @param valueKind the type of the result
* @return the value of {@code expr}
* @throws AnnotationFileParserException if a problem occurred getting the value
*/
private Object getValueOfExpressionInAnnotation(String name, Expression expr, TypeKind valueKind)
throws AnnotationFileParserException {
if (expr instanceof FieldAccessExpr || expr instanceof NameExpr) {
VariableElement elem;
if (expr instanceof NameExpr) {
elem = findVariableElement((NameExpr) expr);
} else {
elem = findVariableElement((FieldAccessExpr) expr);
}
if (elem == null) {
throw new AnnotationFileParserException(String.format("variable %s not found", expr));
}
Object value = elem.getConstantValue() != null ? elem.getConstantValue() : elem;
if (value instanceof Number) {
return convert((Number) value, valueKind);
} else {
return value;
}
} else if (expr instanceof StringLiteralExpr) {
return ((StringLiteralExpr) expr).asString();
} else if (expr instanceof BooleanLiteralExpr) {
return ((BooleanLiteralExpr) expr).getValue();
} else if (expr instanceof CharLiteralExpr) {
return convert((int) ((CharLiteralExpr) expr).asChar(), valueKind);
} else if (expr instanceof DoubleLiteralExpr) {
// No conversion needed if the expression is a double, the annotation value must be a
// double, too.
return ((DoubleLiteralExpr) expr).asDouble();
} else if (expr instanceof IntegerLiteralExpr) {
return convert(((IntegerLiteralExpr) expr).asNumber(), valueKind);
} else if (expr instanceof LongLiteralExpr) {
return convert(((LongLiteralExpr) expr).asNumber(), valueKind);
} else if (expr instanceof UnaryExpr) {
switch (expr.toString()) {
// Special-case the minimum values. Separately parsing a "-" and a value
// doesn't correctly handle the minimum values, because the absolute value of
// the smallest member of an integral type is larger than the largest value.
case "-9223372036854775808L":
case "-9223372036854775808l":
return convert(Long.MIN_VALUE, valueKind, false);
case "-2147483648":
return convert(Integer.MIN_VALUE, valueKind, false);
default:
if (((UnaryExpr) expr).getOperator() == UnaryExpr.Operator.MINUS) {
Object value =
getValueOfExpressionInAnnotation(
name, ((UnaryExpr) expr).getExpression(), valueKind);
if (value instanceof Number) {
return convert((Number) value, valueKind, true);
}
}
throw new AnnotationFileParserException(
"unexpected Unary annotation expression: " + expr);
}
} else if (expr instanceof ClassExpr) {
ClassExpr classExpr = (ClassExpr) expr;
@SuppressWarnings("signature") // Type.toString(): @FullyQualifiedName
@FullyQualifiedName String className = classExpr.getType().toString();
if (importedTypes.containsKey(className)) {
return importedTypes.get(className).asType();
}
TypeElement typeElement = findTypeOfName(className);
if (typeElement == null) {
throw new AnnotationFileParserException("unknown class name " + className);
}
return typeElement.asType();
} else if (expr instanceof NullLiteralExpr) {
throw new AnnotationFileParserException("Illegal annotation value null, for " + name);
} else {
throw new AnnotationFileParserException("Unexpected annotation expression: " + expr);
}
}
/**
* Returns the TypeElement with the name {@code name}, if one exists. Otherwise, checks the class
* and package of {@code typeBeingParsed} for a class named {@code name}.
*
* @param name classname (simple, or Outer.Inner, or fully-qualified)
* @return the TypeElement for {@code name}, or null if not found
*/
@SuppressWarnings("signature:argument") // string concatenation
private @Nullable TypeElement findTypeOfName(@FullyQualifiedName String name) {
String packageName = typeBeingParsed.packageName;
String packagePrefix = (packageName == null) ? "" : packageName + ".";
// warn("findTypeOfName(%s), typeBeingParsed %s %s", name, packageName, enclosingClass);
// As soon as typeElement is set to a non-null value, it will be returned.
TypeElement typeElement = getTypeElementOrNull(name);
if (typeElement == null && packageName != null) {
typeElement = getTypeElementOrNull(packagePrefix + name);
}
String enclosingClass = typeBeingParsed.className;
while (typeElement == null && enclosingClass != null) {
typeElement = getTypeElementOrNull(packagePrefix + enclosingClass + "." + name);
int lastDot = enclosingClass.lastIndexOf('.');
if (lastDot == -1) {
break;
} else {
enclosingClass = enclosingClass.substring(0, lastDot);
}
}
if (typeElement == null && !"java.lang".equals(packageName)) {
typeElement = getTypeElementOrNull("java.lang." + name);
}
return typeElement;
}
/**
* Converts {@code number} to {@code expectedKind}.
*
* <pre><code>
* &nbsp; @interface Anno { long value(); }
* &nbsp; @Anno(1)
* </code></pre>
*
* To properly build @Anno, the IntegerLiteralExpr "1" must be converted from an int to a long.
*/
private Object convert(Number number, TypeKind expectedKind) {
return convert(number, expectedKind, false);
}
/**
* Converts {@code number} to {@code expectedKind}. The value converted is multiplied by -1 if
* {@code negate} is true
*
* @param number a Number value to be converted
* @param expectedKind one of type {byte, short, int, long, char, float, double}
* @param negate whether to negate the value of the Number Object while converting
* @return the converted Object
*/
private Object convert(Number number, TypeKind expectedKind, boolean negate) {
byte scalefactor = (byte) (negate ? -1 : 1);
switch (expectedKind) {
case BYTE:
return number.byteValue() * scalefactor;
case SHORT:
return number.shortValue() * scalefactor;
case INT:
return number.intValue() * scalefactor;
case LONG:
return number.longValue() * scalefactor;
case CHAR:
// It's not possible for `number` to be negative when `expectedkind` is a CHAR, and
// casting a negative value to char is illegal.
if (negate) {
throw new BugInCF(
"convert(%s, %s, %s): can't negate a char", number, expectedKind, negate);
}
return (char) number.intValue();
case FLOAT:
return number.floatValue() * scalefactor;
case DOUBLE:
return number.doubleValue() * scalefactor;
default:
throw new BugInCF("Unexpected expectedKind: " + expectedKind);
}
}
/**
* Adds an annotation element (argument) to {@code builder}. The element is a Java expression.
*
* @param builder the builder to side-effect
* @param name the element name
* @param expr the element value
* @throws AnnotationFileParserException if the expression cannot be parsed and added to {@code
* builder}
*/
private void builderAddElement(AnnotationBuilder builder, String name, Expression expr)
throws AnnotationFileParserException {
ExecutableElement var = builder.findElement(name);
TypeMirror declaredType = var.getReturnType();
TypeKind valueKind;
if (declaredType.getKind() == TypeKind.ARRAY) {
valueKind = ((ArrayType) declaredType).getComponentType().getKind();
} else {
valueKind = declaredType.getKind();
}
if (expr instanceof ArrayInitializerExpr) {
if (declaredType.getKind() != TypeKind.ARRAY) {
throw new AnnotationFileParserException(
"unhandled annotation attribute type: " + expr + " and declaredType: " + declaredType);
}
List<Expression> arrayExpressions = ((ArrayInitializerExpr) expr).getValues();
Object[] values = new Object[arrayExpressions.size()];
for (int i = 0; i < arrayExpressions.size(); ++i) {
Expression eltExpr = arrayExpressions.get(i);
values[i] = getValueOfExpressionInAnnotation(name, eltExpr, valueKind);
}
builder.setValue(name, values);
} else {
Object value = getValueOfExpressionInAnnotation(name, expr, valueKind);
if (declaredType.getKind() == TypeKind.ARRAY) {
Object[] valueArray = {value};
builder.setValue(name, valueArray);
} else {
builderSetValue(builder, name, value);
}
}
}
/**
* Cast to non-array values so that correct the correct AnnotationBuilder#setValue method is
* called. (Different types of values are handled differently.)
*
* @param builder the builder to side-effect
* @param name the element name
* @param value the element value
*/
private void builderSetValue(AnnotationBuilder builder, String name, Object value) {
if (value instanceof Boolean) {
builder.setValue(name, (Boolean) value);
} else if (value instanceof Character) {
builder.setValue(name, (Character) value);
} else if (value instanceof Class<?>) {
builder.setValue(name, (Class<?>) value);
} else if (value instanceof Double) {
builder.setValue(name, (Double) value);
} else if (value instanceof Enum<?>) {
builder.setValue(name, (Enum<?>) value);
} else if (value instanceof Float) {
builder.setValue(name, (Float) value);
} else if (value instanceof Integer) {
builder.setValue(name, (Integer) value);
} else if (value instanceof Long) {
builder.setValue(name, (Long) value);
} else if (value instanceof Short) {
builder.setValue(name, (Short) value);
} else if (value instanceof String) {
builder.setValue(name, (String) value);
} else if (value instanceof TypeMirror) {
builder.setValue(name, (TypeMirror) value);
} else if (value instanceof VariableElement) {
builder.setValue(name, (VariableElement) value);
} else {
throw new BugInCF("Unexpected builder value: %s", value);
}
}
/**
* Mapping of a name access expression that has already been encountered to the resolved variable
* element.
*/
private final Map<NameExpr, VariableElement> findVariableElementNameCache = new HashMap<>();
/**
* Returns the element for the given variable.
*
* @param nexpr the variable name
* @return the element for the given variable
*/
private @Nullable VariableElement findVariableElement(NameExpr nexpr) {
if (findVariableElementNameCache.containsKey(nexpr)) {
return findVariableElementNameCache.get(nexpr);
}
VariableElement res = null;
boolean importFound = false;
for (String imp : importedConstants) {
Pair<@FullyQualifiedName String, String> partitionedName =
AnnotationFileUtil.partitionQualifiedName(imp);
String typeName = partitionedName.first;
String fieldName = partitionedName.second;
if (fieldName.equals(nexpr.getNameAsString())) {
TypeElement enclType =
getTypeElement(
typeName,
String.format("Enclosing type of static import %s not found", fieldName),
nexpr);
if (enclType == null) {
return null;
} else {
importFound = true;
res = findFieldElement(enclType, fieldName, nexpr);
break;
}
}
}
if (res == null) {
if (importFound) {
// TODO: Is this warning redundant? Maybe imported but invalid types or fields will
// have warnings from above.
stubWarnNotFound(nexpr, nexpr.getName() + " was imported but not found");
} else {
stubWarnNotFound(nexpr, "Static field " + nexpr.getName() + " is not imported");
}
}
findVariableElementNameCache.put(nexpr, res);
return res;
}
/**
* Mapping of a field access expression that has already been encountered to the resolved variable
* element.
*/
private final Map<FieldAccessExpr, VariableElement> findVariableElementFieldCache =
new HashMap<>();
/**
* Returns the VariableElement for the given field access.
*
* @param faexpr a field access expression
* @return the VariableElement for the given field access
*/
@SuppressWarnings("signature:argument") // string manipulation
private @Nullable VariableElement findVariableElement(FieldAccessExpr faexpr) {
if (findVariableElementFieldCache.containsKey(faexpr)) {
return findVariableElementFieldCache.get(faexpr);
}
TypeElement rcvElt = elements.getTypeElement(faexpr.getScope().toString());
if (rcvElt == null) {
// Search importedConstants for full annotation name.
for (String imp : importedConstants) {
// TODO: should this use AnnotationFileUtil.partitionQualifiedName?
String[] importDelimited = imp.split("\\.");
if (importDelimited[importDelimited.length - 1].equals(faexpr.getScope().toString())) {
StringBuilder fullAnnotation = new StringBuilder();
for (int i = 0; i < importDelimited.length - 1; i++) {
fullAnnotation.append(importDelimited[i]);
fullAnnotation.append('.');
}
fullAnnotation.append(faexpr.getScope().toString());
rcvElt = elements.getTypeElement(fullAnnotation);
break;
}
}
if (rcvElt == null) {
stubWarnNotFound(faexpr, "Type " + faexpr.getScope() + " not found");
return null;
}
}
VariableElement res = findFieldElement(rcvElt, faexpr.getNameAsString(), faexpr);
findVariableElementFieldCache.put(faexpr, res);
return res;
}
///////////////////////////////////////////////////////////////////////////
/// Map utilities
///
/**
* Just like Map.put, but does not override any existing value in the map.
*
* @param <K> the key type
* @param <V> the value type
* @param m a map
* @param key a key
* @param value the value to associate with the key, if the key isn't already in the map
*/
public static <K, V> void putIfAbsent(Map<K, V> m, K key, V value) {
if (key == null) {
throw new BugInCF("AnnotationFileParser: key is null for value " + value);
}
if (!m.containsKey(key)) {
m.put(key, value);
}
}
/**
* If the key is already in the {@code annotationFileAnnos.declAnnos} map, then add the annos to
* the map value. Otherwise put the key and the annos in the map.
*
* @param key a name (actually declaration element string)
* @param annos the the set of declaration annotations on it, as written in the annotation file
*/
private void putOrAddToDeclAnnos(String key, Set<AnnotationMirror> annos) {
Set<AnnotationMirror> stored = annotationFileAnnos.declAnnos.get(key);
if (stored == null) {
annotationFileAnnos.declAnnos.put(key, new HashSet<>(annos));
} else {
if (fileType != AnnotationFileType.JDK_STUB) {
stored.addAll(annos);
} else {
// JDK annotations should not replace any annotation of the same type.
List<AnnotationMirror> origStored = new ArrayList<>(stored);
for (AnnotationMirror anno : annos) {
if (!AnnotationUtils.containsSameByName(origStored, anno)) {
stored.add(anno);
}
}
}
}
}
/**
* Just like Map.put, but modifies an existing annotated type for the given key in {@code m}. If
* {@code m} already has an annotated type for {@code key}, each annotation in {@code newType}
* will replace annotations from the same hierarchy at the same location in the existing annotated
* type. Annotations in other hierarchies will be preserved.
*
* @param m the map to put the new type into
* @param key the key for the map
* @param newType the new type for the key
*/
private void putMerge(
Map<Element, AnnotatedTypeMirror> m, Element key, AnnotatedTypeMirror newType) {
if (key == null) {
throw new BugInCF("AnnotationFileParser: key is null");
}
if (m.containsKey(key)) {
AnnotatedTypeMirror existingType = m.get(key);
// If the newType is from a JDK stub file, then keep the existing type. This
// way user-supplied stub files override JDK stub files.
// This works because the JDK is always parsed last, on demand, after all other stub files.
if (fileType != AnnotationFileType.JDK_STUB) {
atypeFactory.replaceAnnotations(newType, existingType);
}
m.put(key, existingType);
} else {
m.put(key, newType);
}
}
/**
* Just like Map.putAll, but modifies existing values using {@link #putIfAbsent(Map, Object,
* Object)}.
*
* @param m the destination map
* @param m2 the source map
* @param <K> the key type for the maps
* @param <V> the value type for the maps
*/
public static <K, V> void putAllNew(Map<K, V> m, Map<K, V> m2) {
for (Map.Entry<K, V> e2 : m2.entrySet()) {
putIfAbsent(m, e2.getKey(), e2.getValue());
}
}
///////////////////////////////////////////////////////////////////////////
/// Issue warnings
///
/** The warnings that have been issued so far. */
private static final Set<String> warnings = new HashSet<>();
/**
* Issues the given warning about missing elements, only if it has not been previously issued and
* the -AstubWarnIfNotFound command-line argument was passed.
*
* @param astNode where to report errors
* @param warning warning to print
*/
private void stubWarnNotFound(NodeWithRange<?> astNode, String warning) {
stubWarnNotFound(astNode, warning, warnIfNotFound);
}
/**
* Issues the given warning about missing elements, only if it has not been previously issued and
* the {@code warnIfNotFound} formal parameter is true.
*
* @param astNode where to report errors
* @param warning warning to print
* @param warnIfNotFound whether to print warnings about types/members that were not found
*/
private void stubWarnNotFound(NodeWithRange<?> astNode, String warning, boolean warnIfNotFound) {
if ((fileType.isCommandLine() || warnIfNotFound) || debugAnnotationFileParser) {
warn(astNode, warning);
}
}
/**
* Issues the given warning about overwriting bytecode, only if it has not been previously issued
* and the -AstubWarnIfOverwritesBytecode command-line argument was passed.
*
* @param astNode where to report errors
* @param message the warning message to print
*/
@SuppressWarnings("UnusedMethod") // not currently used
private void stubWarnOverwritesBytecode(NodeWithRange<?> astNode, String message) {
if (warnIfStubOverwritesBytecode || debugAnnotationFileParser) {
warn(astNode, message);
}
}
/**
* Issues a warning, only if it has not been previously issued.
*
* @param astNode where to report errors
* @param warning a format string
* @param args the arguments for {@code warning}
*/
@FormatMethod
private void warn(@Nullable NodeWithRange<?> astNode, String warning, Object... args) {
if (!fileType.isBuiltIn()) {
warn(astNode, String.format(warning, args));
}
}
/**
* Issues a warning, only if it has not been previously issued.
*
* @param astNode where to report errors
* @param warning a warning message
*/
private void warn(@Nullable NodeWithRange<?> astNode, String warning) {
if (fileType != AnnotationFileType.JDK_STUB) {
if (warnings.add(warning)) {
processingEnv
.getMessager()
.printMessage(stubWarnDiagnosticKind, fileAndLine(astNode) + warning);
}
}
}
/**
* If {@code warning} hasn't been printed yet, and {@code debugAnnotationFileParser} is true,
* prints the given warning as a diagnostic message.
*
* @param warning warning to print
*/
private void stubDebug(String warning) {
if (debugAnnotationFileParser && warnings.add(warning)) {
processingEnv
.getMessager()
.printMessage(javax.tools.Diagnostic.Kind.NOTE, "AnnotationFileParser: " + warning);
}
}
/**
* After obtaining the JavaParser AST for an ajava file and the javac tree for its corresponding
* Java file, walks both in tandem. For each program construct with annotations, stores the
* annotations from the ajava file in {@link #annotationFileAnnos} by calling the process method
* corresponding to that construct, such as {@link #processCallableDeclaration} or {@link
* #processField}.
*/
private class AjavaAnnotationCollectorVisitor extends DefaultJointVisitor {
@Override
public Void visitClass(ClassTree javacTree, Node javaParserNode) {
List<AnnotatedTypeVariable> typeDeclTypeParameters = null;
if (javaParserNode instanceof TypeDeclaration<?>
&& !(javaParserNode instanceof AnnotationDeclaration)) {
typeDeclTypeParameters =
processTypeDecl((TypeDeclaration<?>) javaParserNode, null, javacTree);
}
super.visitClass(javacTree, javaParserNode);
if (typeDeclTypeParameters != null) {
typeParameters.removeAll(typeDeclTypeParameters);
}
return null;
}
@Override
public Void visitVariable(VariableTree javacTree, Node javaParserNode) {
if (TreeUtils.elementFromTree(javacTree) != null) {
VariableElement elt = TreeUtils.elementFromDeclaration(javacTree);
if (elt != null) {
if (elt.getKind() == ElementKind.FIELD) {
processField((FieldDeclaration) javaParserNode.getParentNode().get(), elt);
}
if (elt.getKind() == ElementKind.ENUM_CONSTANT) {
processEnumConstant((EnumConstantDeclaration) javaParserNode, elt);
}
}
}
super.visitVariable(javacTree, javaParserNode);
return null;
}
@Override
public Void visitMethod(MethodTree javacTree, Node javaParserNode) {
ExecutableElement elt = TreeUtils.elementFromDeclaration(javacTree);
List<AnnotatedTypeVariable> variablesToClear = null;
if (javaParserNode instanceof CallableDeclaration<?>) {
variablesToClear = processCallableDeclaration((CallableDeclaration<?>) javaParserNode, elt);
}
super.visitMethod(javacTree, javaParserNode);
if (variablesToClear != null) {
typeParameters.removeAll(variablesToClear);
}
return null;
}
}
/**
* Return the prefix for a warning line: A file name, line number, and column number.
*
* @param astNode where to report errors
* @return file name, line number, and column number
*/
private String fileAndLine(NodeWithRange<?> astNode) {
String filenamePrinted =
(processingEnv.getOptions().containsKey("nomsgtext")
? new File(filename).getName()
: filename);
Optional<Position> begin = astNode == null ? Optional.empty() : astNode.getBegin();
String lineAndColumn = (begin.isPresent() ? begin.get() + ":" : "");
return filenamePrinted + ":" + lineAndColumn + " ";
}
/** An exception indicating a problem while parsing an annotation file. */
public static class AnnotationFileParserException extends Exception {
private static final long serialVersionUID = 20201222;
/**
* Create a new AnnotationFileParserException.
*
* @param message a description of the problem
*/
AnnotationFileParserException(String message) {
super(message);
}
}
///////////////////////////////////////////////////////////////////////////
/// Parse state
///
/** Represents a class: its package name and name (including outer class names if any). */
private static class FqName {
/** Name of the package being parsed, or null. */
public @Nullable String packageName;
/**
* Name of the type being parsed. Includes outer class names if any. Null if the parser has
* parsed a package declaration but has not yet gotten to a type declaration.
*/
public @Nullable String className;
/**
* Create a new FqName, which represents a class.
*
* @param packageName name of the package, or null
* @param className unqualified name of the type, including outer class names if any. May be
* null.
*/
public FqName(String packageName, @Nullable String className) {
this.packageName = packageName;
this.className = className;
}
/** Fully-qualified name of the class. */
@Override
@SuppressWarnings("signature") // string concatenation
public @FullyQualifiedName String toString() {
if (packageName == null) {
return className;
} else {
return packageName + "." + className;
}
}
}
}