blob: 407a234be511134ef9181c2447de02aaa3065a24 [file] [log] [blame]
package org.checkerframework.framework.stub;
import com.sun.source.tree.CompilationUnitTree;
import io.github.classgraph.ClassGraph;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.stream.Stream;
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.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic.Kind;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.signature.qual.CanonicalNameOrEmpty;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.framework.qual.StubFiles;
import org.checkerframework.framework.source.SourceChecker;
import org.checkerframework.framework.stub.AnnotationFileParser.AnnotationFileAnnotations;
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.AnnotatedExecutableType;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.Pair;
import org.checkerframework.javacutil.SystemUtil;
import org.checkerframework.javacutil.TypesUtils;
/**
* Holds information about types parsed from annotation files (stub files or ajava files). When
* using an ajava file, only holds information on public elements as with stub files.
*/
public class AnnotationFileElementTypes {
/** Annotations from annotation files (but not from annotated JDK files). */
private final AnnotationFileAnnotations annotationFileAnnos;
/**
* Whether or not a file is currently being parsed. (If one is being parsed, don't try to parse
* another.)
*/
private boolean parsing;
/** AnnotatedTypeFactory. */
private final AnnotatedTypeFactory factory;
/**
* Mapping from fully-qualified class name to corresponding JDK stub file from the file system. By
* contrast, {@link #jdkStubFilesJar} contains JDK stub files from checker.jar.
*/
private final Map<String, Path> jdkStubFiles = new HashMap<>();
/**
* Mapping from fully-qualified class name to corresponding JDK stub files from checker.jar. By
* contrast, {@link #jdkStubFiles} contains JDK stub files from the file system.
*/
private final Map<String, String> jdkStubFilesJar = new HashMap<>();
/** Which version number of the annotated JDK should be used? */
private final String annotatedJdkVersion;
/** Should the JDK be parsed? */
private final boolean shouldParseJdk;
/** Parse all JDK files at startup rather than as needed. */
private final boolean parseAllJdkFiles;
/**
* Creates an empty annotation source.
*
* @param factory AnnotatedTypeFactory
*/
public AnnotationFileElementTypes(AnnotatedTypeFactory factory) {
this.factory = factory;
this.annotationFileAnnos = new AnnotationFileAnnotations();
this.parsing = false;
String release = SystemUtil.getReleaseValue(factory.getProcessingEnv());
this.annotatedJdkVersion =
release != null ? release : String.valueOf(SystemUtil.getJreVersion());
this.shouldParseJdk = !factory.getChecker().hasOption("ignorejdkastub");
this.parseAllJdkFiles = factory.getChecker().hasOption("parseAllJdk");
}
/**
* Returns true if files are currently being parsed; otherwise, false.
*
* @return true if files are currently being parsed; otherwise, false
*/
public boolean isParsing() {
return parsing;
}
/**
* Parses the stub files in the following order:
*
* <ol>
* <li>jdk.astub in the same directory as the checker, if it exists and ignorejdkastub option is
* not supplied
* <li>If parsing annotated JDK as stub files, all package-info.java files under the jdk/
* directory
* <li>Stub files listed in @StubFiles annotation on the checker; must be in same directory as
* the checker
* <li>Stub files returned by {@link BaseTypeChecker#getExtraStubFiles} (treated like those
* listed in @StubFiles annotation)
* <li>Stub files provided via {@code -Astubs} compiler option
* </ol>
*
* <p>If a type is annotated with a qualifier from the same hierarchy in more than one stub file,
* the qualifier in the last stub file is applied.
*
* <p>If using JDK 11, then the JDK stub files are only parsed if a type or declaration annotation
* is requested from a class in that file.
*/
public void parseStubFiles() {
parsing = true;
BaseTypeChecker checker = factory.getChecker();
ProcessingEnvironment processingEnv = factory.getProcessingEnv();
// 1. jdk.astub
// Only look in .jar files, and parse it right away.
if (!checker.hasOption("ignorejdkastub")) {
InputStream jdkStubIn = checker.getClass().getResourceAsStream("jdk.astub");
if (jdkStubIn != null) {
AnnotationFileParser.parseStubFile(
checker.getClass().getResource("jdk.astub").toString(),
jdkStubIn,
factory,
processingEnv,
annotationFileAnnos,
AnnotationFileType.BUILTIN_STUB);
}
String jdkVersionStub = "jdk" + annotatedJdkVersion + ".astub";
InputStream jdkVersionStubIn = checker.getClass().getResourceAsStream(jdkVersionStub);
if (jdkVersionStubIn != null) {
AnnotationFileParser.parseStubFile(
checker.getClass().getResource(jdkVersionStub).toString(),
jdkVersionStubIn,
factory,
processingEnv,
annotationFileAnnos,
AnnotationFileType.BUILTIN_STUB);
}
// 2. Annotated JDK
// This preps but does not parse the JDK files (except package-info.java files).
// The JDK source code files will be parsed later, on demand.
prepJdkStubs();
// prepping the JDK parses all package-info.java files, which sets the `parsing` field to
// false, so re-set it to true.
parsing = true;
}
// 3. Stub files listed in @StubFiles annotation on the checker
StubFiles stubFilesAnnotation = checker.getClass().getAnnotation(StubFiles.class);
if (stubFilesAnnotation != null) {
parseAnnotationFiles(
Arrays.asList(stubFilesAnnotation.value()), AnnotationFileType.BUILTIN_STUB);
}
// 4. Stub files returned by the `getExtraStubFiles()` method
parseAnnotationFiles(checker.getExtraStubFiles(), AnnotationFileType.BUILTIN_STUB);
// 5. Stub files provided via -Astubs command-line option
String stubsOption = checker.getOption("stubs");
if (stubsOption != null) {
parseAnnotationFiles(
Arrays.asList(stubsOption.split(File.pathSeparator)),
AnnotationFileType.COMMAND_LINE_STUB);
}
parsing = false;
}
/** Parses the ajava files passed through the -Aajava command-line option. */
public void parseAjavaFiles() {
parsing = true;
// TODO: Error if this is called more than once?
SourceChecker checker = factory.getChecker();
List<String> ajavaFiles = new ArrayList<>();
String ajavaOption = checker.getOption("ajava");
if (ajavaOption != null) {
Collections.addAll(ajavaFiles, ajavaOption.split(File.pathSeparator));
}
parseAnnotationFiles(ajavaFiles, AnnotationFileType.AJAVA);
parsing = false;
}
/**
* Parses the ajava file at {@code ajavaPath} assuming {@code root} represents the compilation
* unit of that file. Uses {@code root} to get information from javac on specific elements of
* {@code ajavaPath}, enabling storage of more detailed annotation information than with just the
* ajava file.
*
* @param ajavaPath path to an ajava file
* @param root javac tree for the compilation unit stored in {@code ajavaFile}
*/
public void parseAjavaFileWithTree(String ajavaPath, CompilationUnitTree root) {
parsing = true;
SourceChecker checker = factory.getChecker();
ProcessingEnvironment processingEnv = factory.getProcessingEnv();
try {
InputStream in = new FileInputStream(ajavaPath);
AnnotationFileParser.parseAjavaFile(
ajavaPath, in, root, factory, processingEnv, annotationFileAnnos);
} catch (IOException e) {
checker.message(Kind.NOTE, "Could not read ajava file: " + ajavaPath);
}
parsing = false;
}
/**
* Parses the files in {@code annotationFiles} of the given file type. This includes files listed
* directly in {@code annotationFiles} and for each listed directory, also includes all files
* located in that directory (recursively).
*
* @param annotationFiles list of files and directories to parse
* @param fileType the file type of files to parse
*/
private void parseAnnotationFiles(List<String> annotationFiles, AnnotationFileType fileType) {
SourceChecker checker = factory.getChecker();
ProcessingEnvironment processingEnv = factory.getProcessingEnv();
for (String path : annotationFiles) {
// Special case when running in jtreg.
String base = System.getProperty("test.src");
String fullPath = (base == null) ? path : base + "/" + path;
List<AnnotationFileResource> allFiles =
AnnotationFileUtil.allAnnotationFiles(fullPath, fileType);
if (allFiles != null) {
for (AnnotationFileResource resource : allFiles) {
InputStream annotationFileStream;
try {
annotationFileStream = resource.getInputStream();
} catch (IOException e) {
checker.message(
Kind.NOTE, "Could not read annotation resource: " + resource.getDescription());
continue;
}
// We use parseStubFile here even for ajava files because at this stage ajava
// files are parsed as stub files. The extra annotation data in an ajava file is
// parsed when type-checking the ajava file's corresponding Java file.
AnnotationFileParser.parseStubFile(
resource.getDescription(),
annotationFileStream,
factory,
processingEnv,
annotationFileAnnos,
fileType == AnnotationFileType.AJAVA ? AnnotationFileType.AJAVA_AS_STUB : fileType);
}
} else {
// We didn't find the files.
// If the file has a prefix of "checker.jar/" then look for the file in the top
// level directory of the jar that contains the checker.
if (path.startsWith("checker.jar/")) {
path = path.substring("checker.jar/".length());
}
InputStream in = checker.getClass().getResourceAsStream(path);
if (in != null) {
AnnotationFileParser.parseStubFile(
path, in, factory, processingEnv, annotationFileAnnos, fileType);
} else {
// Didn't find the file. Issue a warning.
// When using a compound checker, the target file may be found by the
// current checker's parent checkers. Also check this to avoid a false
// warning. Currently, only the original checker will try to parse the target
// file, the parent checkers are only used to reduce false warnings.
SourceChecker currentChecker = checker;
boolean findByParentCheckers = false;
while (currentChecker != null) {
URL topLevelResource = currentChecker.getClass().getResource("/" + path);
if (topLevelResource != null) {
currentChecker.message(
Kind.WARNING,
path
+ " should be in the same directory as "
+ currentChecker.getClass().getSimpleName()
+ ".class, but is at the top level of a jar file: "
+ topLevelResource);
findByParentCheckers = true;
break;
} else {
currentChecker = currentChecker.getParentChecker();
}
}
// If there exists one parent checker that can find this file, don't report a warning.
if (!findByParentCheckers) {
File parentPath = new File(path).getParentFile();
String parentPathDescription =
(parentPath == null
? "current directory"
: "directory " + parentPath.getAbsolutePath());
String msg =
checker.getClass().getSimpleName()
+ " did not find annotation file or directory "
+ path
+ " on classpath or within "
+ parentPathDescription
+ (fullPath.equals(path) ? "" : (" or at " + fullPath));
StringJoiner sj = new StringJoiner(System.lineSeparator() + " ");
sj.add(msg);
sj.add("Classpath:");
for (URI uri : new ClassGraph().getClasspathURIs()) {
sj.add(uri.toString());
}
checker.message(Kind.WARNING, sj.toString());
}
}
}
}
}
/**
* Returns the annotated type for {@code e} containing only annotations explicitly written in an
* annotation file or {@code null} if {@code e} does not appear in an annotation file.
*
* @param e an Element whose type is returned
* @return an AnnotatedTypeMirror for {@code e} containing only annotations explicitly written in
* the annotation file and in the element. {@code null} is returned if {@code element} does
* not appear in an annotation file.
*/
public AnnotatedTypeMirror getAnnotatedTypeMirror(Element e) {
if (parsing) {
return null;
}
parseEnclosingClass(e);
AnnotatedTypeMirror type = annotationFileAnnos.atypes.get(e);
return type == null ? null : type.deepCopy();
}
/**
* Returns the set of declaration annotations for {@code e} containing only annotations explicitly
* written in an annotation file or the empty set if {@code e} does not appear in an annotation
* file.
*
* @param elt element for which annotations are returned
* @return an AnnotatedTypeMirror for {@code e} containing only annotations explicitly written in
* the annotation file and in the element. {@code null} is returned if {@code element} does
* not appear in an annotation file.
*/
public Set<AnnotationMirror> getDeclAnnotation(Element elt) {
if (parsing) {
return Collections.emptySet();
}
parseEnclosingClass(elt);
String eltName = ElementUtils.getQualifiedName(elt);
if (annotationFileAnnos.declAnnos.containsKey(eltName)) {
return annotationFileAnnos.declAnnos.get(eltName);
}
return Collections.emptySet();
}
/**
* Returns the method type of the most specific fake override for the given element, when used as
* a member of the given type.
*
* @param elt element for which annotations are returned
* @param receiverType the type of the class that contains member (or a subtype of it)
* @return the most specific AnnotatedTypeMirror for {@code elt} that is a fake override, or null
* if there are no fake overrides
*/
public @Nullable AnnotatedTypeMirror getFakeOverride(
Element elt, AnnotatedTypeMirror receiverType) {
if (parsing) {
throw new BugInCF("parsing while calling getFakeOverride");
}
if (elt.getKind() != ElementKind.METHOD) {
return null;
}
ExecutableElement method = (ExecutableElement) elt;
TypeMirror methodReceiverType = method.getReceiverType();
if (methodReceiverType != null && methodReceiverType.getKind() == TypeKind.NONE) {
return null;
}
// This is a list of pairs of (where defined, method type) for fake overrides. The second
// element of each pair is currently always an AnnotatedExecutableType.
List<Pair<TypeMirror, AnnotatedTypeMirror>> candidates =
annotationFileAnnos.fakeOverrides.get(elt);
if (candidates == null || candidates.isEmpty()) {
return null;
}
TypeMirror receiverTypeMirror = receiverType.getUnderlyingType();
// A list of fake receiver types.
List<TypeMirror> applicableClasses = new ArrayList<>();
List<TypeMirror> applicableInterfaces = new ArrayList<>();
for (Pair<TypeMirror, AnnotatedTypeMirror> candidatePair : candidates) {
TypeMirror fakeLocation = candidatePair.first;
AnnotatedExecutableType candidate = (AnnotatedExecutableType) candidatePair.second;
if (factory.types.isSameType(receiverTypeMirror, fakeLocation)) {
return candidate;
} else if (factory.types.isSubtype(receiverTypeMirror, fakeLocation)) {
TypeElement fakeElement = TypesUtils.getTypeElement(fakeLocation);
switch (fakeElement.getKind()) {
case CLASS:
case ENUM:
applicableClasses.add(fakeLocation);
break;
case INTERFACE:
case ANNOTATION_TYPE:
applicableInterfaces.add(fakeLocation);
break;
default:
throw new BugInCF(
"What type? %s %s %s", fakeElement.getKind(), fakeElement.getClass(), fakeElement);
}
}
}
if (applicableClasses.isEmpty() && applicableInterfaces.isEmpty()) {
return null;
}
TypeMirror fakeReceiverType =
TypesUtils.mostSpecific(
!applicableClasses.isEmpty() ? applicableClasses : applicableInterfaces,
factory.getProcessingEnv());
if (fakeReceiverType == null) {
StringJoiner message = new StringJoiner(System.lineSeparator());
message.add(
String.format(
"No most specific fake override found for %s with receiver %s."
+ " These fake overrides are applicable:",
elt, receiverTypeMirror));
for (TypeMirror candidate : applicableClasses) {
message.add(" class candidate: " + candidate);
}
for (TypeMirror candidate : applicableInterfaces) {
message.add(" interface candidate: " + candidate);
}
throw new BugInCF(message.toString());
}
for (Pair<TypeMirror, AnnotatedTypeMirror> candidatePair : candidates) {
TypeMirror candidateReceiverType = candidatePair.first;
if (factory.types.isSameType(fakeReceiverType, candidateReceiverType)) {
return (AnnotatedExecutableType) candidatePair.second;
}
}
throw new BugInCF(
"No match for %s in %s %s %s",
fakeReceiverType, candidates, applicableClasses, applicableInterfaces);
}
///
/// End of public methods, private helper methods follow
///
/**
* Parses the outermost enclosing class of {@code e} if there exists an annotation file for it and
* it has not already been parsed.
*
* @param e element whose outermost enclosing class will be parsed
*/
private void parseEnclosingClass(Element e) {
if (!shouldParseJdk) {
return;
}
String className = getOutermostEnclosingClass(e);
if (className == null || className.isEmpty()) {
return;
}
if (jdkStubFiles.containsKey(className)) {
parseJdkStubFile(jdkStubFiles.get(className));
jdkStubFiles.remove(className);
} else if (jdkStubFilesJar.containsKey(className)) {
parseJdkJarEntry(jdkStubFilesJar.get(className));
jdkStubFilesJar.remove(className);
}
}
/**
* Returns the fully qualified name of the outermost enclosing class of {@code e} or {@code null}
* if no such class exists for {@code e}.
*
* @param e an element whose outermost enclosing class to return
* @return the canonical name of the outermost enclosing class of {@code e} or {@code null} if no
* class encloses {@code e}
*/
private @CanonicalNameOrEmpty String getOutermostEnclosingClass(Element e) {
TypeElement enclosingClass = ElementUtils.enclosingTypeElement(e);
if (enclosingClass == null) {
return null;
}
while (true) {
Element element = enclosingClass.getEnclosingElement();
if (element == null || element.getKind() == ElementKind.PACKAGE) {
break;
}
TypeElement t = ElementUtils.enclosingTypeElement(element);
if (t == null) {
break;
}
enclosingClass = t;
}
@SuppressWarnings("signature:assignment" // https://tinyurl.com/cfissue/658:
// Name.toString should be @PolySignature
)
@CanonicalNameOrEmpty String result = enclosingClass.getQualifiedName().toString();
return result;
}
/**
* Parses the stub file in {@code path}.
*
* @param path path to file to parse
*/
private void parseJdkStubFile(Path path) {
parsing = true;
try (FileInputStream jdkStub = new FileInputStream(path.toFile())) {
AnnotationFileParser.parseJdkFileAsStub(
path.toFile().getName(),
jdkStub,
factory,
factory.getProcessingEnv(),
annotationFileAnnos);
} catch (IOException e) {
throw new BugInCF("cannot open the jdk stub file " + path, e);
} finally {
parsing = false;
}
}
/**
* Parses the stub file in the given jar entry.
*
* @param jarEntryName name of the jar entry to parse
*/
private void parseJdkJarEntry(String jarEntryName) {
JarURLConnection connection = getJarURLConnectionToJdk();
parsing = true;
try (JarFile jarFile = connection.getJarFile()) {
InputStream jdkStub;
try {
jdkStub = jarFile.getInputStream(jarFile.getJarEntry(jarEntryName));
} catch (IOException e) {
throw new BugInCF("cannot open the jdk stub file " + jarEntryName, e);
}
AnnotationFileParser.parseJdkFileAsStub(
jarEntryName, jdkStub, factory, factory.getProcessingEnv(), annotationFileAnnos);
} catch (IOException e) {
throw new BugInCF("cannot open the Jar file " + connection.getEntryName(), e);
} catch (BugInCF e) {
throw new BugInCF("Exception while parsing " + jarEntryName + ": " + e.getMessage(), e);
} finally {
parsing = false;
}
}
/**
* Returns a JarURLConnection to "/jdk*".
*
* @return a JarURLConnection to "/jdk*"
*/
private JarURLConnection getJarURLConnectionToJdk() {
URL resourceURL = factory.getClass().getResource("/annotated-jdk");
JarURLConnection connection;
try {
connection = (JarURLConnection) resourceURL.openConnection();
// disable caching / connection sharing of the low level URLConnection to the Jarfile
connection.setDefaultUseCaches(false);
connection.setUseCaches(false);
connection.connect();
} catch (IOException e) {
throw new BugInCF("cannot open a connection to the Jar file " + resourceURL.getFile(), e);
}
return connection;
}
/**
* Walk through the jdk directory and create a mapping, {@link #jdkStubFiles}, from file name to
* the class contained with in it. Also, parses all package-info.java files.
*/
private void prepJdkStubs() {
if (!shouldParseJdk) {
return;
}
URL resourceURL = factory.getClass().getResource("/annotated-jdk");
if (resourceURL == null) {
if (factory.getChecker().hasOption("permitMissingJdk")
// temporary, for backward compatibility
|| factory.getChecker().hasOption("nocheckjdk")) {
return;
}
throw new BugInCF("JDK not found");
} else if (resourceURL.getProtocol().contentEquals("jar")) {
prepJdkFromJar(resourceURL);
} else if (resourceURL.getProtocol().contentEquals("file")) {
prepJdkFromFile(resourceURL);
} else {
if (factory.getChecker().hasOption("permitMissingJdk")
// temporary, for backward compatibility
|| factory.getChecker().hasOption("nocheckjdk")) {
return;
}
throw new BugInCF("JDK not found");
}
}
/**
* Walk through the JDK directory and create a mapping, {@link #jdkStubFiles}, from file name to
* the class contained with in it. Also, parses all package-info.java files.
*
* @param resourceURL the URL pointing to the JDK directory
*/
private void prepJdkFromFile(URL resourceURL) {
Path root;
try {
root = Paths.get(resourceURL.toURI());
} catch (URISyntaxException e) {
throw new BugInCF("Can parse URL: " + resourceURL.toString(), e);
}
try (Stream<Path> walk = Files.walk(root)) {
List<Path> paths =
walk.filter(p -> Files.isRegularFile(p) && p.toString().endsWith(".java"))
.collect(Collectors.toList());
for (Path path : paths) {
if (path.getFileName().toString().equals("package-info.java")) {
parseJdkStubFile(path);
continue;
}
if (path.getFileName().toString().equals("module-info.java")) {
// JavaParser can't parse module-info files, so skip them.
continue;
}
if (parseAllJdkFiles) {
parseJdkStubFile(path);
continue;
}
Path relativePath = root.relativize(path);
// 4: /src/<module>/share/classes
Path savepath = relativePath.subpath(4, relativePath.getNameCount());
String s = savepath.toString().replace(".java", "").replace(File.separatorChar, '.');
jdkStubFiles.put(s, path);
}
} catch (IOException e) {
throw new BugInCF("prepJdkFromFile(" + resourceURL + ")", e);
}
}
/**
* Walk through the JDK directory and create a mapping, {@link #jdkStubFilesJar}, from file name
* to the class contained with in it. Also, parses all package-info.java files.
*
* @param resourceURL the URL pointing to the JDK directory
*/
private void prepJdkFromJar(URL resourceURL) {
JarURLConnection connection = getJarURLConnectionToJdk();
try (JarFile jarFile = connection.getJarFile()) {
for (Enumeration<JarEntry> e = jarFile.entries(); e.hasMoreElements(); ) {
JarEntry jarEntry = e.nextElement();
// filter out directories and non-class files
if (!jarEntry.isDirectory()
&& jarEntry.getName().endsWith(".java")
&& jarEntry.getName().startsWith("annotated-jdk")
// JavaParser can't parse module-info files, so skip them.
&& !jarEntry.getName().contains("module-info")) {
String jarEntryName = jarEntry.getName();
if (parseAllJdkFiles) {
parseJdkJarEntry(jarEntryName);
continue;
}
int index = jarEntry.getName().indexOf("/share/classes/");
String shortName =
jarEntryName
.substring(index + "/share/classes/".length())
.replace(".java", "")
.replace('/', '.');
jdkStubFilesJar.put(shortName, jarEntryName);
if (jarEntryName.endsWith("package-info.java")) {
parseJdkJarEntry(jarEntryName);
}
}
}
} catch (IOException e) {
throw new BugInCF("cannot open the Jar file " + resourceURL.getFile(), e);
}
}
}