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