/*
* Copyright 2016-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.facebook.buck.jvm.java;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSetMultimap;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileObject;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
/**
* Tracks which classes are actually read by the compiler by providing a special {@link
* JavaFileManager}.
*/
class ClassUsageTracker {
private static final String FILE_SCHEME = "file";
private static final String JAR_SCHEME = "jar";
private static final String JIMFS_SCHEME = "jimfs"; // Used in tests
// Examples: First anonymous class is Foo$1.class. First local class named Bar is Foo$1Bar.class.
private static final Pattern LOCAL_OR_ANONYMOUS_CLASS = Pattern.compile("^.*\\$\\d.*.class$");
private final ImmutableSetMultimap.Builder<Path, Path> resultBuilder =
ImmutableSetMultimap.builder();
@Nullable private ImmutableSetMultimap<Path, Path> result;
/**
* Returns a {@link JavaFileManager} that tracks which files are opened. Provide this to {@code
* JavaCompiler.getTask} anytime file usage tracking is desired.
*/
public StandardJavaFileManager wrapFileManager(StandardJavaFileManager inner) {
return new UsageTrackingFileManager(inner);
}
/**
* Returns a multimap from JAR path on disk to .class file paths within the jar for any classes
* that were used.
*/
public ImmutableSetMultimap<Path, Path> getClassUsageMap() {
if (result == null) {
result = resultBuilder.build();
}
return result;
}
private void addReadFile(FileObject fileObject) {
Preconditions.checkState(result == null); // Can't add after having built
if (!(fileObject instanceof JavaFileObject)) {
return;
}
JavaFileObject javaFileObject = (JavaFileObject) fileObject;
URI classFileJarUri = javaFileObject.toUri();
if (!classFileJarUri.getScheme().equals(JAR_SCHEME)) {
// Not in a jar; must not have been built with java_library
return;
}
// The jar: scheme is somewhat underspecified. See the JarURLConnection docs
// for the closest thing it has to documentation.
String jarUriSchemeSpecificPart = classFileJarUri.getRawSchemeSpecificPart();
final String[] split = jarUriSchemeSpecificPart.split("!/");
Preconditions.checkState(split.length == 2);
if (isLocalOrAnonymousClass(split[1])) {
// The compiler reads local and anonymous classes because of the naive way in which it
// completes the enclosing class, but changes to them can't affect compilation of dependent
// classes so we don't need to consider them "used".
return;
}
URI jarFileUri = URI.create(split[0]);
Preconditions.checkState(
jarFileUri.getScheme().equals(FILE_SCHEME)
|| jarFileUri.getScheme().equals(JIMFS_SCHEME)); // jimfs is used in tests
Path jarFilePath = Paths.get(jarFileUri);
// Using URI.create here for de-escaping
Path classPath = Paths.get(URI.create(split[1]).toString());
Preconditions.checkState(jarFilePath.isAbsolute());
Preconditions.checkState(!classPath.isAbsolute());
resultBuilder.put(jarFilePath, classPath);
}
private boolean isLocalOrAnonymousClass(String className) {
return LOCAL_OR_ANONYMOUS_CLASS.matcher(className).matches();
}
private class UsageTrackingFileManager extends ForwardingStandardJavaFileManager {
private final FileObjectTracker fileTracker = new FileObjectTracker();
public UsageTrackingFileManager(StandardJavaFileManager fileManager) {
super(fileManager);
}
@Override
public String inferBinaryName(Location location, JavaFileObject file) {
// javac does not play nice with wrapped file objects in this method; so we unwrap
return super.inferBinaryName(location, unwrap(file));
}
@Override
public boolean isSameFile(FileObject a, FileObject b) {
// javac does not play nice with wrapped file objects in this method; so we unwrap
return super.isSameFile(unwrap(a), unwrap(b));
}
private JavaFileObject unwrap(JavaFileObject file) {
if (file instanceof TrackingJavaFileObject) {
return ((TrackingJavaFileObject) file).getJavaFileObject();
}
return file;
}
private FileObject unwrap(FileObject file) {
if (file instanceof JavaFileObject) {
return unwrap((JavaFileObject) file);
}
return file;
}
@Override
public Iterable<? extends JavaFileObject> getJavaFileObjectsFromFiles(
Iterable<? extends File> files) {
return fileManager.getJavaFileObjectsFromFiles(files);
}
@Override
public Iterable<? extends JavaFileObject> getJavaFileObjects(File... files) {
return fileManager.getJavaFileObjects(files);
}
@Override
public Iterable<? extends JavaFileObject> getJavaFileObjectsFromStrings(
Iterable<String> names) {
return fileManager.getJavaFileObjectsFromStrings(names);
}
@Override
public Iterable<? extends JavaFileObject> getJavaFileObjects(String... names) {
return fileManager.getJavaFileObjects(names);
}
@Override
public Iterable<JavaFileObject> list(
Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse)
throws IOException {
Iterable<JavaFileObject> listIterator = super.list(location, packageName, kinds, recurse);
if (location == StandardLocation.ANNOTATION_PROCESSOR_PATH) {
return listIterator;
} else {
return new TrackingIterable(listIterator);
}
}
@Override
public JavaFileObject getJavaFileForInput(
Location location, String className, JavaFileObject.Kind kind) throws IOException {
JavaFileObject javaFileObject = super.getJavaFileForInput(location, className, kind);
if (location == StandardLocation.ANNOTATION_PROCESSOR_PATH) {
return javaFileObject;
} else {
return fileTracker.wrap(javaFileObject);
}
}
@Override
public JavaFileObject getJavaFileForOutput(
Location location, String className, JavaFileObject.Kind kind, FileObject sibling)
throws IOException {
JavaFileObject javaFileObject =
super.getJavaFileForOutput(location, className, kind, sibling);
if (location == StandardLocation.ANNOTATION_PROCESSOR_PATH) {
return javaFileObject;
} else {
return fileTracker.wrap(javaFileObject);
}
}
@Override
public FileObject getFileForInput(Location location, String packageName, String relativeName)
throws IOException {
FileObject fileObject = super.getFileForInput(location, packageName, relativeName);
if (location == StandardLocation.ANNOTATION_PROCESSOR_PATH) {
return fileObject;
} else {
return fileTracker.wrap(fileObject);
}
}
@Override
public FileObject getFileForOutput(
Location location, String packageName, String relativeName, FileObject sibling)
throws IOException {
FileObject fileObject = super.getFileForOutput(location, packageName, relativeName, sibling);
if (location == StandardLocation.ANNOTATION_PROCESSOR_PATH) {
return fileObject;
} else {
return fileTracker.wrap(fileObject);
}
}
private class TrackingIterable implements Iterable<JavaFileObject> {
private final Iterable<? extends JavaFileObject> inner;
public TrackingIterable(final Iterable<? extends JavaFileObject> inner) {
this.inner = inner;
}
@Override
public Iterator<JavaFileObject> iterator() {
return new TrackingIterator(inner.iterator());
}
}
private class TrackingIterator implements Iterator<JavaFileObject> {
private final Iterator<? extends JavaFileObject> inner;
public TrackingIterator(final Iterator<? extends JavaFileObject> inner) {
this.inner = inner;
}
@Override
public boolean hasNext() {
return inner.hasNext();
}
@Override
public JavaFileObject next() {
return fileTracker.wrap(inner.next());
}
@Override
public void remove() {
inner.remove();
}
}
}
private class FileObjectTracker {
private final Map<JavaFileObject, JavaFileObject> javaFileObjectCache = new IdentityHashMap<>();
public FileObject wrap(FileObject inner) {
if (inner instanceof JavaFileObject) {
return wrap((JavaFileObject) inner);
}
return inner;
}
public JavaFileObject wrap(JavaFileObject inner) {
if (!javaFileObjectCache.containsKey(inner)) {
javaFileObjectCache.put(inner, new TrackingJavaFileObject(inner));
}
return Preconditions.checkNotNull(javaFileObjectCache.get(inner));
}
}
private class TrackingJavaFileObject extends ForwardingJavaFileObject<JavaFileObject> {
public TrackingJavaFileObject(JavaFileObject fileObject) {
super(fileObject);
}
public JavaFileObject getJavaFileObject() {
return fileObject;
}
@Override
public InputStream openInputStream() throws IOException {
addReadFile(fileObject);
return super.openInputStream();
}
@Override
public Reader openReader(boolean ignoreEncodingErrors) throws IOException {
addReadFile(fileObject);
return super.openReader(ignoreEncodingErrors);
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
addReadFile(fileObject);
return super.getCharContent(ignoreEncodingErrors);
}
}
}