All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.checkerframework.framework.stub.AnnotationFileParser Maven / Gradle / Ivy

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.RecordDeclaration;
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.NodeWithTypeParameters;
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 org.checkerframework.checker.formatter.qual.FormatMethod;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
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.type.AnnotatedTypeMirror.ErrorTypeKindException;
import org.checkerframework.framework.util.JavaParserUtil;
import org.checkerframework.javacutil.AnnotationBuilder;
import org.checkerframework.javacutil.AnnotationMirrorSet;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.TreeUtils;
import org.checkerframework.javacutil.UserError;
import org.plumelib.util.ArrayMap;
import org.plumelib.util.CollectionsPlume;
import org.plumelib.util.IPair;
import org.plumelib.util.SystemPlume;

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 java.util.stream.Collectors;

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;

// 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.
 *
 * 

The first main entry point is {@link #parseStubFile(String, InputStream, AnnotatedTypeFactory, * ProcessingEnvironment, AnnotationFileAnnotations, AnnotationFileUtil.AnnotationFileType, * AnnotationFileElementTypes)}, 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. * *

The second main entry point is {@link #parseAjavaFile(String, InputStream, * CompilationUnitTree, AnnotatedTypeFactory, ProcessingEnvironment, AnnotationFileAnnotations, * AnnotationFileElementTypes)}. This behaves the same as {@link * AnnotationFileParser#parseStubFile(String, InputStream, AnnotatedTypeFactory, * ProcessingEnvironment, AnnotationFileAnnotations, AnnotationFileUtil.AnnotationFileType, * AnnotationFileElementTypes)}, but takes an ajava file instead. * *

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. * *

Non-JDK stub files override JDK stub files. (Ordinarily, if two stubs are provided, they * are merged.) * *

For a built-in stub file, * *

    *
  • private declarations are ignored, *
  • some warning messages are not issued, and *
*/ 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 @Nullable StubUnit stubUnit; /** The processing environment */ private final ProcessingEnvironment processingEnv; /** The type factory */ private final AnnotatedTypeFactory atypeFactory; /** The element utilities */ private final Elements elements; /** The manager that controls the stub file parsing process. */ private final AnnotationFileElementTypes fileElementTypes; /** * 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. * *

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 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 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 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. */ private @Nullable List<@Nullable 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. * *

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. */ private @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. * *

This is a fine-grained mapping that contains all sorts of elements; contrast with * {@link #fakeOverrides}. */ public final Map 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. * *

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 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>> fakeOverrides = new HashMap<>(1); /** Maps fully qualified record name to information in the stub file. */ public final Map records = new HashMap<>(); } /** Information about a record from a stub file. */ public static class RecordStub { /** * A map from name to record component. It must have deterministic insertion/iteration * order: the order that they are declared in the record header. */ public final Map componentsByName; /** * If the canonical constructor is given in the stubs, the annotated types (in component * declaration order) for the constructor. Null if not present in the stubs. */ public @MonotonicNonNull List componentsInCanonicalConstructor; /** * Creates a new RecordStub. * * @param componentsByName a map from name to record component. It must have deterministic * insertion/iteration order: the order that they are declared in the record header. */ public RecordStub(Map componentsByName) { this.componentsByName = componentsByName; } /** * Returns the annotated types for the parameters to the canonical constructor. This is * either from explicit annotations on the constructor in the stubs, otherwise it's taken * from the annotations on the record components in the stubs. * * @return the annotated types for the parameters to the canonical constructor */ public List getComponentsInCanonicalConstructor() { if (componentsInCanonicalConstructor != null) { return componentsInCanonicalConstructor; } else { return CollectionsPlume.mapList(c -> c.type, componentsByName.values()); } } } /** * Information about a record component: its type, and whether there was an accessor in the * stubs for that component. That is, for a component "foo" was there a method named exactly * "foo()" in the stubs. If so, annotations on that accessor will take precedence over * annotations that would otherwise be copied from the component in the stubs to the acessor. */ public static class RecordComponentStub { /** The type of the record component. */ public final AnnotatedTypeMirror type; /** * The set of all annotations on the declaration of the record component. If applicable * these will be copied to the corresponding field, accessor method, and compact canonical * constructor parameter. */ private final AnnotationMirrorSet allAnnotations; /** Whether this component has an accessor of exactly the same name in the stubs file. */ private boolean hasAccessorInStubs = false; /** * Creates a new RecordComponentStub with the given type. * * @param type the type of the record component * @param allAnnotations the declaration annotations on the component */ public RecordComponentStub(AnnotatedTypeMirror type, AnnotationMirrorSet allAnnotations) { this.type = type; this.allAnnotations = allAnnotations; } /** * Get the record component annotations that are applicable to the given element kind. * * @param elementKind the element kind to apply to (e.g., FIELD, METHOD) * @return the set of annotations from the component that apply */ public AnnotationMirrorSet getAnnotationsForTarget(ElementKind elementKind) { AnnotationMirrorSet filtered = new AnnotationMirrorSet(); for (AnnotationMirror annoMirror : allAnnotations) { 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(elementKind)) { // `annoMirror` is applicable to `elt` filtered.add(annoMirror); } } return filtered; } /** * Returns whether there is an accessor in a stub file. * * @return true if some stub file contains an accessor for this record component */ public boolean hasAccessorInStubs() { return hasAccessorInStubs; } } /** * 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 the type factory * @param processingEnv the processing environment * @param fileType the type of file being parsed (stub file or ajava file) and its source * @param fileElementTypes the manager that controls the stub file parsing process */ private AnnotationFileParser( String filename, AnnotatedTypeFactory atypeFactory, ProcessingEnvironment processingEnv, AnnotationFileType fileType, AnnotationFileElementTypes fileElementTypes) { this.filename = filename; this.atypeFactory = atypeFactory; this.processingEnv = processingEnv; this.elements = processingEnv.getElementUtils(); this.fileType = fileType; this.root = null; this.fileElementTypes = fileElementTypes; // TODO: This should use SourceChecker.getOptions() to allow // setting these flags per checker. Map options = processingEnv.getOptions(); boolean stubWarnIfNotFoundOption = options.containsKey("stubWarnIfNotFound"); boolean stubNoWarnIfNotFoundOption = options.containsKey("stubNoWarnIfNotFound"); if (stubWarnIfNotFoundOption && stubNoWarnIfNotFoundOption) { throw new UserError( "Do not supply both -AstubWarnIfNotFound and -AstubNoWarnIfNotFound."); } this.warnIfNotFound = stubWarnIfNotFoundOption || (fileType.isCommandLine() && !stubNoWarnIfNotFoundOption); 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 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 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 createNameToAnnotationMap( List typeElements) { Map 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 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}. * *

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 getImportedAnnotations() { Map 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(); 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. IPair<@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 the type factory * @param processingEnv the processing environment * @param annotationFileAnnos annotations from the annotation file; side-effected by this method * @param fileType the annotation file type and source * @param fileElementTypes the manager that controls the stub file parsing process */ public static void parseStubFile( String filename, InputStream inputStream, AnnotatedTypeFactory atypeFactory, ProcessingEnvironment processingEnv, AnnotationFileAnnotations annotationFileAnnos, AnnotationFileType fileType, AnnotationFileElementTypes fileElementTypes) { AnnotationFileParser afp = new AnnotationFileParser( filename, atypeFactory, processingEnv, fileType, fileElementTypes); try { afp.parseStubUnit(inputStream); afp.process(annotationFileAnnos); } catch (ParseProblemException e) { for (Problem p : e.getProblems()) { afp.warn(null, p.getVerboseMessage()); } } catch (Throwable t) { afp.warn(null, "Parse problem: " + t); } } /** * 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 the type factory * @param processingEnv the processing environment * @param ajavaAnnos annotations from the ajava file; side-effected by this method * @param fileElementTypes the manager that controls the stub file parsing process */ public static void parseAjavaFile( String filename, InputStream inputStream, CompilationUnitTree root, AnnotatedTypeFactory atypeFactory, ProcessingEnvironment processingEnv, AnnotationFileAnnotations ajavaAnnos, AnnotationFileElementTypes fileElementTypes) { AnnotationFileParser afp = new AnnotationFileParser( filename, atypeFactory, processingEnv, AnnotationFileType.AJAVA, fileElementTypes); 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, filename + ": " + p.getVerboseMessage()); } } catch (Throwable t) { afp.warn(null, "Parse problem: " + t); } } /** * 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 the type factory * @param processingEnv the processing environment * @param stubAnnos annotations from the stub file; side-effected by this method * @param fileElementTypes the manager that controls the stub file parsing process */ public static void parseJdkFileAsStub( String filename, InputStream inputStream, AnnotatedTypeFactory atypeFactory, ProcessingEnvironment processingEnv, AnnotationFileAnnotations stubAnnos, AnnotationFileElementTypes fileElementTypes) { Map options = processingEnv.getOptions(); boolean debugAnnotationFileParser = options.containsKey("stubDebug"); if (debugAnnotationFileParser) { stubDebugStatic( processingEnv, "parseJdkFileAsStub(%s, _, %s, _, _)%n", filename, atypeFactory.getClass().getSimpleName()); } parseStubFile( filename, inputStream, atypeFactory, processingEnv, stubAnnos, AnnotationFileType.JDK_STUB, fileElementTypes); } /** * 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. * *

Subsequently, all work uses the AST. * * @param inputStream the stream from which to read an annotation file */ private void parseStubUnit(InputStream inputStream) { stubDebug( "started parsing annotation file %s for %s", filename, atypeFactory.getClass().getSimpleName()); 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))); if (debugAnnotationFileParser) { stubDebug( "finished parsing annotation file %s for %s", filename, atypeFactory.getClass().getSimpleName()); } } /** * 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()) { PackageDeclaration pDecl = cu.getPackageDeclaration().get(); packageAnnos = pDecl.getAnnotations(); if (debugAnnotationFileParser || (!warnIfNotFoundIgnoresClasses && !hasNoAnnotationFileParserWarning(packageAnnos))) { String packageName = pDecl.getName().toString(); if (elements.getPackageElement(packageName) == null) { stubWarnNotFound(pDecl, "package not found: " + packageName); } } processPackage(pDecl); } else { packageAnnos = null; typeBeingParsed = new FqName(null, null); } if (fileType.isStub()) { if (cu.getTypes() != null) { for (TypeDeclaration typeDeclaration : cu.getTypes()) { Optional typeDeclName = typeDeclaration.getFullyQualifiedName(); typeDeclName.ifPresent(fileElementTypes::preProcessTopLevelType); try { // Not processing an ajava file, so ignore the return value. processTypeDecl(typeDeclaration, null, null); } finally { typeDeclName.ifPresent(fileElementTypes::postProcessTopLevelType); } } } } 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: * *

    *
  • 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. *
  • It is not an ajava file and {@code -AmergeStubsWithSource} was not supplied. As * described at https://eisop.github.io/cf/manual/#stub-multiple-specifications, source * files take precedence over stub files unless {@code -AmergeStubsWithSource} is * supplied. As described at https://eisop.github.io/cf/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). *
* * @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() && fileType != AnnotationFileType.AJAVA_AS_STUB && !mergeStubsWithSource)) && node.getModifiers().contains(Modifier.privateModifier()); } /** * Returns the string representation of {@code n}, one one line, truncated to {@code length} * characters. * * @param n a JavaParser node * @param length the maximum length of the string representation * @return the truncated string representation of {@code n} */ private String javaParserNodeToStringTruncated(Node n, int length) { String oneLine = n.toString() .replace("\t", " ") .replace("\n", " ") .replace("\r", " ") .replaceAll(" +", " "); if (oneLine.length() <= length) { return oneLine; } else { return oneLine.substring(0, length - 3) + "..."; } } /** * Process a type declaration: copy its annotations to {@code #annotationFileAnnos}. * *

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 @Nullable List processTypeDecl( TypeDeclaration typeDecl, @Nullable 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 typeDeclTypeParameters = null; if (typeElt.getKind() == ElementKind.ENUM) { if (!(typeDecl instanceof EnumDeclaration)) { warn( typeDecl, innerName + " is an enum, but stub file declared it as " + javaParserNodeToStringTruncated(typeDecl, 100)); 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 " + javaParserNodeToStringTruncated(typeDecl, 100)); return null; } typeDeclTypeParameters = processType(typeDecl, typeElt); typeParameters.addAll(typeDeclTypeParameters); } else if (typeDecl instanceof ClassOrInterfaceDeclaration) { // TODO: This test is never satisfied, because it is the opposite of that on the line // above. if (!(typeDecl instanceof ClassOrInterfaceDeclaration)) { warn( typeDecl, innerName + " is a class or interface, but stub file declared it as " + javaParserNodeToStringTruncated(typeDecl, 100)); return null; } typeDeclTypeParameters = processType(typeDecl, typeElt); typeParameters.addAll(typeDeclTypeParameters); } else if (typeDecl instanceof RecordDeclaration) { typeDeclTypeParameters = processType(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; } if (typeDecl instanceof RecordDeclaration) { RecordDeclaration recordDecl = (RecordDeclaration) typeDecl; NodeList recordMembers = recordDecl.getParameters(); Map byName = ArrayMap.newArrayMapOrLinkedHashMap(recordMembers.size()); for (Parameter recordMember : recordMembers) { RecordComponentStub stub = processRecordField( recordMember, findFieldElement( typeElt, recordMember.getNameAsString(), recordMember)); byName.put(recordMember.getNameAsString(), stub); } annotationFileAnnos.records.put( recordDecl.getFullyQualifiedName().get(), new RecordStub(byName)); } IPair>, Map>>> members = getMembers(typeDecl, typeElt, typeDecl); for (Map.Entry> entry : members.first.entrySet()) { Element elt = entry.getKey(); BodyDeclaration decl = entry.getValue(); switch (elt.getKind()) { case FIELD: processField((FieldDeclaration) decl, (VariableElement) elt); break; case ENUM_CONSTANT: // Enum constants can occur as fields in stubs files when their // type has an annotation on it, e.g. see DeviceTypeTest which ends up with // the TRACKER enum constant annotated with DefaultType: if (decl instanceof FieldDeclaration) { processField((FieldDeclaration) decl, (VariableElement) elt); } else if (decl instanceof EnumConstantDeclaration) { processEnumConstant((EnumConstantDeclaration) decl, (VariableElement) elt); } else { throw new BugInCF( "unexpected decl type " + decl.getClass() + " for ENUM_CONSTANT kind, original: " + decl); } 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>> entry : members.second.entrySet()) { ExecutableElement fakeOverridden = (ExecutableElement) entry.getKey(); List> 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 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 processType(TypeDeclaration decl, TypeElement elt) { recordDeclAnnotation(elt, decl.getAnnotations(), decl); AnnotatedDeclaredType type = atypeFactory.fromElement(elt); annotate(type, decl.getAnnotations(), decl); List typeArguments = type.getTypeArguments(); List typeParameters; if (decl instanceof NodeWithTypeParameters) { typeParameters = ((NodeWithTypeParameters) decl).getTypeParameters(); } else { typeParameters = Collections.emptyList(); } // 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( "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); if (decl instanceof ClassOrInterfaceDeclaration) { annotateSupertypes((ClassOrInterfaceDeclaration) decl, type); } putMerge(annotationFileAnnos.atypes, elt, type); List 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 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 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 direct 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 direct 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 @Nullable List 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; try { methodType = atypeFactory.fromElement(elt); } catch (ErrorTypeKindException e) { stubWarnNotFound(decl, "Error type kind occurred: " + e.getLocalizedMessage()); return Collections.emptyList(); } 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()) { MethodDeclaration methodDeclaration = (MethodDeclaration) decl; if (methodDeclaration.getParameters().isEmpty()) { String qualRecordName = ElementUtils.getQualifiedName(elt.getEnclosingElement()); RecordStub recordStub = annotationFileAnnos.records.get(qualRecordName); if (recordStub != null) { RecordComponentStub recordComponentStub = recordStub.componentsByName.get(methodDeclaration.getNameAsString()); if (recordComponentStub != null) { recordComponentStub.hasAccessorInStubs = true; } } } try { annotate( methodType.getReturnType(), methodDeclaration.getType(), decl.getAnnotations(), decl); } catch (ErrorTypeKindException e) { // See https://github.com/typetools/checker-framework/issues/244 . // Issue a warning, to enable fixes to the classpath. stubWarnNotFound(decl, "Error type kind occurred: " + e); } } else { assert decl.isConstructorDeclaration(); if (AnnotationFileUtil.isCanonicalConstructor(elt, atypeFactory.types)) { // If this is the (user-written) canonical constructor, record that the component // annotations should not be automatically transferred: String qualRecordName = ElementUtils.getQualifiedName(elt.getEnclosingElement()); if (annotationFileAnnos.records.containsKey(qualRecordName)) { List parameters = elt.getParameters(); ArrayList annotatedParameters = new ArrayList<>(parameters.size()); for (int i = 0; i < parameters.size(); i++) { VariableElement parameter = parameters.get(i); AnnotatedTypeMirror atm = AnnotatedTypeMirror.createType( parameter.asType(), atypeFactory, false); annotate(atm, decl.getParameter(i).getAnnotations(), decl.getParameter(i)); annotatedParameters.add(atm); } annotationFileAnnos.records.get(qualRecordName) .componentsInCanonicalConstructor = annotatedParameters; } } 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, receiverParameter.getAnnotations()); } else { warn( receiverParameter, "parseParameter: static method %s cannot have receiver annotations %s", methodType, receiverParameter.getAnnotations()); } } else { // Add declaration annotations. annotate( methodType.getReceiverType(), receiverParameter.getAnnotations(), receiverParameter); // Add type annotations. annotate( methodType.getReceiverType(), receiverParameter.getType(), receiverParameter.getAnnotations(), receiverParameter); } } if (warnIfStubRedundantWithBytecode && methodType.toString().equals(origMethodType.toString()) && fileType != AnnotationFileType.BUILTIN_STUB) { warn( decl, String.format( "stub file specification is same as bytecode 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 the element for {@code method} * @param methodType the annotated type of {@code method} */ private void processParameters( CallableDeclaration method, ExecutableElement elt, AnnotatedExecutableType methodType) { List params = method.getParameters(); List paramElts = elt.getParameters(); List 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. * *

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 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 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 @Nullable 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}: * *

    *
  1. the annotations from {@code typeDef}, and *
  2. any type annotations that parsed as declaration annotations (i.e., type annotations in * {@code declAnnos}). *
* * @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 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 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. @SuppressWarnings( "optional:optional.collection") // JavaParser uses Optional Optional> oDeclTypeArgs = declType.getTypeArguments(); List adeclTypeArgs = adeclType.getTypeArguments(); if (oDeclTypeArgs.isPresent() && !oDeclTypeArgs.get().isEmpty() && !adeclTypeArgs.isEmpty()) { NodeList declTypeArgs = oDeclTypeArgs.get(); if (declTypeArgs.size() != adeclTypeArgs.size()) { warn( astNode, String.format( "Mismatch in type argument size between %s (%d) and %s (%d)", declType, declTypeArgs.size(), adeclType, adeclTypeArgs.size())); break; } for (int i = 0; i < declTypeArgs.size(); ++i) { annotate(adeclTypeArgs.get(i), declTypeArgs.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 if (primaryAnnotations.isEmpty()) { // Unannotated unbounded wildcard "?": remove any existing annotations and // add the annotations from the type variable corresponding to the wildcard. wildcardType.getExtendsBound().clearAnnotations(); wildcardType.getSuperBound().clearAnnotations(); AnnotatedTypeVariable atv = (AnnotatedTypeVariable) atypeFactory.getAnnotatedType( wildcardType.getTypeVariable().asElement()); wildcardType .getExtendsBound() .addAnnotations(atv.getUpperBound().getAnnotations()); wildcardType .getSuperBound() .addAnnotations(atv.getLowerBound().getAnnotations()); } else { // Annotated unbounded wildcard "@A ?": use annotations. 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); } /** * Processes a parameter in a record header (i.e., a record component). * * @param decl the parameter in the record header * @param elt the corresponding variable declaration element * @return a representation of the record component in the stub file */ private RecordComponentStub processRecordField(Parameter decl, VariableElement elt) { markAsFromStubFile(elt); recordDeclAnnotation(elt, decl.getAnnotations(), decl); // AnnotationFileParser parses all annotations in type annotation position as type // annotations. recordDeclAnnotation(elt, decl.getType().getAnnotations(), decl); AnnotatedTypeMirror fieldType = atypeFactory.fromElement(elt); annotate(fieldType, decl.getType(), decl.getAnnotations(), decl); putMerge(annotationFileAnnos.atypes, elt, fieldType); AnnotationMirrorSet annos = new AnnotationMirrorSet(); for (AnnotationExpr annotation : decl.getAnnotations()) { AnnotationMirror annoMirror = getAnnotation(annotation, allAnnotations); annos.add(annoMirror); } return new RecordComponentStub(fieldType, annos); } /** * 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 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; if null, nothing is done * @param astNode where to report errors */ private void annotate( AnnotatedTypeMirror type, @Nullable List 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 annotations, NodeWithRange astNode) { if (annotations == null || annotations.isEmpty()) { return; } AnnotationMirrorSet annos = new AnnotationMirrorSet(); 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), AnnotationMirrorSet.singleton(fromStubFileAnno)); } private void annotateTypeParameters( BodyDeclaration decl, // for debugging Object elt, // for debugging; TypeElement or ExecutableElement List typeArguments, List 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); // Handle type bounds if (param.getTypeBound() == null || param.getTypeBound().isEmpty()) { // No type 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); if (param.getTypeBound().size() == 1) { // The additional declAnnos (third argument) is always null in this call to // `annotate`, but the type bound (second argument) might have annotations. annotate(paramType.getUpperBound(), param.getTypeBound().get(0), null, param); } else { // param.getTypeBound().size() > 1 ArrayList typeBoundsWithAnotations = new ArrayList<>(param.getTypeBound().size()); for (ClassOrInterfaceType typeBound : param.getTypeBound()) { if (!typeBound.getAnnotations().isEmpty()) { typeBoundsWithAnotations.add(typeBound); } } int numBounds = typeBoundsWithAnotations.size(); if (numBounds == 0) { // nothing to do } else if (numBounds == 1) { annotate( paramType.getUpperBound(), typeBoundsWithAnotations.get(0), null, param); } else { // TODO: add support for intersection types // One problem is that `annotate()` removes any existing annotations from // the same qualifier hierarchies, so paramType.getLowerBound() would end up // with the annotations of only the last type bound. // String msg = // String.format( // "annotateTypeParameters: multiple type bounds: // typeParameters=%s; " // + "param #%d=%s; bounds=%s; decl=%s; elt=%s (%s).", // typeParameters, // i, // param, // param.getTypeBound(), // decl.toString().replace(LINE_SEPARATOR, " "), // elt.toString().replace(LINE_SEPARATOR, " "), // elt.getClass()); // warn(decl, msg); stubWarnNotFound( param, "annotations on intersection types are not yet supported: " + param); } } if (param.getTypeBound().size() == 1 && param.getTypeBound().get(0).getAnnotations().isEmpty() && paramType .getUpperBound() .getUnderlyingType() .toString() .contentEquals("java.lang.Object")) { // If there is an explicit "T extends Object" type parameter bound, // treat it like an explicit use of "Object" in code. AnnotatedTypeMirror ub = atypeFactory.getAnnotatedType(Object.class); paramType.getUpperBound().replaceAnnotations(ub.getAnnotations()); } } putMerge( annotationFileAnnos.atypes, paramType.getUnderlyingType().asElement(), paramType); } } /** * Returns a pair of mappings. For each member declaration of the JavaParser type declaration * {@code typeDecl}: * *
    *
  • If {@code typeElt} contains a member element for it, the first mapping maps the member * element to it. *
  • If it is a fake override, the second mapping maps each element it overrides to it. *
  • Otherwise, does nothing. *
* * 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 IPair>, Map>>> 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> elementsToDecl = new LinkedHashMap<>(); Map>> 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 IPair.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. * *

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}. * *

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> elementsToDecl, Map>> 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> l = fakeOverrideDecls.computeIfAbsent( overriddenMethod, __ -> new ArrayList<>(1)); 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("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. * *

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 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 javacParams, NodeList 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 annotations = decl.getAnnotations(); annotate( methodType.getReturnType(), ((MethodDeclaration) decl).getType(), annotations, decl); List> l = annotationFileAnnos.fakeOverrides.computeIfAbsent( element, __ -> new ArrayList<>(1)); l.add(IPair.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 types, NodeWithRange astNode) { String typeString = type.getNameAsString(); for (AnnotatedDeclaredType supertype : types) { if (supertype .getUnderlyingType() .asElement() .getSimpleName() .contentEquals(typeString)) { return supertype; } } stubWarnNotFound(astNode, "direct supertype " + typeString + " not found"); if (debugAnnotationFileParser) { stubDebug("direct supertypes that were searched:"); for (AnnotatedDeclaredType supertype : types) { stubDebug(" %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) { 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(" type declarations of %s:", typeElt); for (TypeElement method : ElementFilter.typesIn(typeElt.getEnclosedElements())) { stubDebug(" %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) { 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(" type declarations of %s:", typeElt); for (TypeElement method : ElementFilter.typesIn(typeElt.getEnclosedElements())) { stubDebug(" %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) { 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; } String wantedMethodName = methodDecl.getNameAsString(); int wantedMethodParams = (methodDecl.getParameters() == null) ? 0 : methodDecl.getParameters().size(); 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.NONE) { // 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 -AstubDebug to receive a more useful error message."); } else { stubWarnNotFound( methodDecl, "method " + wantedMethodString + " not found in type " + typeElt); if (debugAnnotationFileParser) { stubDebug(" methods of %s:", typeElt); for (ExecutableElement method : ElementFilter.methodsIn(typeElt.getEnclosedElements())) { stubDebug(" %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; } int wantedMethodParams = (constructorDecl.getParameters() == null) ? 0 : constructorDecl.getParameters().size(); 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(" %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) { 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(" %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 @Nullable 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 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 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 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.type.incompatible") // 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}. * *


     *   @interface Anno { long value(); }
     *   @Anno(1)
     * 
* * 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 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 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) { IPair<@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 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.type.incompatible") // 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 the key type * @param 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 void putIfAbsent(Map 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 set of declaration annotations on it, as written in the annotation file; is * not modified */ private void putOrAddToDeclAnnos(String key, AnnotationMirrorSet annos) { AnnotationMirrorSet stored = annotationFileAnnos.declAnnos.get(key); if (stored == null) { annotationFileAnnos.declAnnos.put(key, new AnnotationMirrorSet(annos)); } else { // TODO: Currently, we assume there can be at most one annotation of the same name // in both `stored` and `annos`. Maybe we should consider the situation of multiple // entries having the same name. AnnotationMirrorSet annotationsToAdd = annos; if (fileType == AnnotationFileType.JDK_STUB) { // JDK annotations should not replace any annotation of the same type. annotationsToAdd = annos.stream() .filter(am -> !AnnotationUtils.containsSameByName(stored, am)) .collect(Collectors.toCollection(AnnotationMirrorSet::new)); } else { // Annotations that are not from the annotated JDK may replace existing // annotations of the same type. stored.removeIf(am -> AnnotationUtils.containsSameByName(annos, am)); } stored.addAll(annotationsToAdd); } } /** * 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 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); } // existingType is already in the map, so no need to put into m. } 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 the key type for the maps * @param the value type for the maps */ public static void putAllNew(Map m, Map m2) { for (Map.Entry e2 : m2.entrySet()) { putIfAbsent(m, e2.getKey(), e2.getValue()); } } /////////////////////////////////////////////////////////////////////////// /// Issue warnings /// /** The warnings that have been issued so far. */ private static final Set 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 (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 {@link #debugAnnotationFileParser} is true, * prints the given warning as a diagnostic message. * * @param fmt format string * @param args arguments to the format string */ @FormatMethod private void stubDebug(String fmt, Object... args) { if (debugAnnotationFileParser) { String warning = String.format(fmt, args); if (warnings.add(warning)) { System.out.flush(); SystemPlume.sleep(1); processingEnv .getMessager() .printMessage( javax.tools.Diagnostic.Kind.NOTE, "AnnotationFileParser: " + warning); System.out.flush(); SystemPlume.sleep(1); } } } /** * If {@code warning} hasn't been printed yet, prints the given warning as a diagnostic message. * Ignores {@code debugAnnotationFileParser}. * * @param processingEnv the processing environment * @param fmt format string * @param args arguments to the format string */ @FormatMethod /*package-private*/ static void stubDebugStatic( ProcessingEnvironment processingEnv, String fmt, Object... args) { String warning = String.format(fmt, args); if (warnings.add(warning)) { System.out.flush(); SystemPlume.sleep(1); processingEnv .getMessager() .printMessage( javax.tools.Diagnostic.Kind.NOTE, "AnnotationFileParser: " + warning); System.out.flush(); SystemPlume.sleep(1); } } /** * 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 { /** Default constructor. */ private AjavaAnnotationCollectorVisitor() {} // This method overrides super.visitCompilationUnit() to prevent parsing import // statements. Requiring imports in both ajava file and the source file to be // exactly same is error-prone and unnecessary. @Override public Void visitCompilationUnit(CompilationUnitTree javacTree, Node javaParserNode) { CompilationUnit node = castNode(CompilationUnit.class, javaParserNode, javacTree); processCompilationUnit(javacTree, node); visitOptional(javacTree.getPackage(), node.getPackageDeclaration()); visitLists(javacTree.getTypeDecls(), node.getTypes()); return null; } @Override public Void visitClass(ClassTree javacTree, Node javaParserNode) { List typeDeclTypeParameters = null; boolean shouldProcessTypeDecl = javaParserNode instanceof TypeDeclaration && !(javaParserNode instanceof AnnotationDeclaration); Optional typeDeclName = Optional.empty(); boolean callListener = false; if (shouldProcessTypeDecl) { TypeDeclaration typeDecl = (TypeDeclaration) javaParserNode; typeDeclName = typeDecl.getFullyQualifiedName(); callListener = typeDeclName.isPresent() && typeDecl.isTopLevelType(); } if (callListener) { @SuppressWarnings("optional:method.invocation.invalid") // from callListener String typeDeclNameString = typeDeclName.get(); fileElementTypes.preProcessTopLevelType(typeDeclNameString); } try { if (shouldProcessTypeDecl) { typeDeclTypeParameters = processTypeDecl((TypeDeclaration) javaParserNode, null, javacTree); } super.visitClass(javacTree, javaParserNode); } finally { if (typeDeclTypeParameters != null) { typeParameters.removeAll(typeDeclTypeParameters); } if (callListener) { @SuppressWarnings("optional:method.invocation.invalid") // from callListener String typeDeclNameString = typeDeclName.get(); fileElementTypes.postProcessTopLevelType(typeDeclNameString); } } return null; } @Override public Void visitVariable(VariableTree javacTree, Node javaParserNode) { VariableElement elt = TreeUtils.elementFromDeclaration(javacTree); if (elt != null) { if (elt.getKind() == ElementKind.FIELD) { VariableDeclarator varDecl = (VariableDeclarator) javaParserNode; processField((FieldDeclaration) varDecl.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) { List variablesToClear = null; Element elt = TreeUtils.elementFromDeclaration(javacTree); if (javaParserNode instanceof CallableDeclaration) { variablesToClear = processCallableDeclaration( (CallableDeclaration) javaParserNode, (ExecutableElement) 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 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 final @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 final @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(@Nullable 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; } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy