
net.corda.plugins.apiscanner.ScanApi Maven / Gradle / Ivy
Show all versions of api-scanner Show documentation
package net.corda.plugins.apiscanner;
import io.github.classgraph.*;
import org.gradle.api.DefaultTask;
import org.gradle.api.InvalidUserCodeException;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.Directory;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileSystemLocation;
import org.gradle.api.file.ProjectLayout;
import org.gradle.api.file.RegularFile;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.provider.SetProperty;
import org.gradle.api.tasks.CompileClasspath;
import org.gradle.api.tasks.Console;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.OutputFiles;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.SkipWhenEmpty;
import org.gradle.api.tasks.TaskAction;
import org.gradle.work.DisableCachingByDefault;
import javax.annotation.Nonnull;
import javax.inject.Inject;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.annotation.Inherited;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import static java.util.Collections.sort;
import static java.util.Collections.swap;
import static java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableSet;
import static java.util.stream.Collectors.partitioningBy;
import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static net.corda.plugins.apiscanner.ApiScanner.GROUP_NAME;
import static org.gradle.api.tasks.PathSensitivity.RELATIVE;
@SuppressWarnings({"unused", "rawtypes", "WeakerAccess"})
@DisableCachingByDefault
class ScanApi extends DefaultTask {
private static final int CLASS_MASK = Modifier.classModifiers();
private static final int INTERFACE_MASK = Modifier.interfaceModifiers() & ~Modifier.ABSTRACT;
/**
* The VARARG modifier for methods has the same value as the TRANSIENT modifier for fields.
* Unfortunately, {@link Modifier#methodModifiers() methodModifiers} doesn't include this
* flag, and so we need to add it back ourselves.
*
* @see Java 8 Specification
*
* Also, the 0x1000 mask is the one used for methods that are generated by the compiler.
* It's not publicly accessible at the moment, see: {@link Modifier#SYNTHETIC}
*/
private static final int METHOD_MASK = Modifier.methodModifiers() | Modifier.TRANSIENT | 0x1000;
private static final int FIELD_MASK = Modifier.fieldModifiers();
private static final int VISIBILITY_MASK = Modifier.PUBLIC | Modifier.PROTECTED;
private static final String ENUM_BASE_CLASS = "java.lang.Enum";
private static final String DONOTIMPLEMENT_ANNOTATION_NAME = "net.corda.v5.base.annotations.DoNotImplement";
private static final String INTERNAL_ANNOTATION_NAME = ".CordaInternal";
private static final String DEFAULT_INTERNAL_ANNOTATION = "net.corda.v5.base.annotations" + INTERNAL_ANNOTATION_NAME;
private static final Set ANNOTATION_BLACKLIST;
static {
Set blacklist = new LinkedHashSet<>();
blacklist.add("kotlin.jvm.JvmField");
blacklist.add("kotlin.jvm.JvmOverloads");
blacklist.add("kotlin.jvm.JvmStatic");
blacklist.add("kotlin.jvm.JvmDefault");
blacklist.add("kotlin.Deprecated");
blacklist.add("java.lang.Deprecated");
blacklist.add(DEFAULT_INTERNAL_ANNOTATION);
ANNOTATION_BLACKLIST = unmodifiableSet(blacklist);
}
/**
* This information has been lifted from:
*
* @link KotlinClassHeader.Kind
* @link JvmAnnotationNames
*/
private static final String KOTLIN_METADATA = "kotlin.Metadata";
private static final String KOTLIN_CLASSTYPE_METHOD = "k";
private static final int KOTLIN_SYNTHETIC = 3;
private final ConfigurableFileCollection sources;
private final ConfigurableFileCollection classpath;
private final Provider> targets;
private final SetProperty excludePackages;
private final SetProperty excludeClasses;
private final MapProperty excludeMethods;
private final Provider outputDir;
private final Property verbose;
@Inject
public ScanApi(@Nonnull ObjectFactory objects, @Nonnull ProjectLayout layout) {
sources = objects.fileCollection();
classpath = objects.fileCollection();
excludePackages = objects.setProperty(String.class);
excludeClasses = objects.setProperty(String.class);
excludeMethods = objects.mapProperty(String.class, Set.class);
verbose = objects.property(Boolean.class).convention(false);
outputDir = layout.getBuildDirectory().dir("api");
targets = outputDir.flatMap(dir ->
sources.getElements().map(files ->
files.stream().map(file -> toTarget(dir, file)).collect(toSet())
)
);
setDescription("Summarises the target JAR's public and protected API elements.");
setGroup(GROUP_NAME);
}
@PathSensitive(RELATIVE)
@SkipWhenEmpty
@InputFiles
public FileCollection getSources() {
return sources;
}
void setSources(Object... sources) {
this.sources.setFrom(sources);
this.sources.disallowChanges();
}
@CompileClasspath
@InputFiles
public FileCollection getClasspath() {
return classpath;
}
void setClasspath(FileCollection classpath) {
this.classpath.setFrom(classpath);
this.classpath.disallowChanges();
}
@Input
public Provider extends Set> getExcludePackages() {
return excludePackages;
}
void setExcludePackages(Provider extends Set> excludePackages) {
this.excludePackages.set(excludePackages);
}
@Input
public Provider extends Set> getExcludeClasses() {
return excludeClasses;
}
void setExcludeClasses(Provider extends Set> excludeClasses) {
this.excludeClasses.set(excludeClasses);
}
@Input
public Provider extends Map> getExcludeMethods() {
return excludeMethods;
}
@SuppressWarnings("unchecked")
void setExcludeMethods(@Nonnull Provider extends Map> excludeMethods) {
this.excludeMethods.empty()
.putAll(excludeMethods.map(m -> {
Map> result = new LinkedHashMap<>();
m.forEach((key, value) -> result.put(key, new LinkedHashSet<>(value)));
return result;
}));
}
@OutputFiles
public Provider> getTargets() {
return targets;
}
@Console
public Provider getVerbose() {
return verbose;
}
void setVerbose(Provider verbose) {
this.verbose.set(verbose);
}
@Nonnull
private static RegularFile toTargetFile(@Nonnull Directory outputDir, @Nonnull File source) {
return outputDir.file(source.getName().replaceAll("\\.jar$", ".txt"));
}
@Nonnull
private static RegularFile toTarget(Directory outputDir, @Nonnull FileSystemLocation source) {
return toTargetFile(outputDir, source.getAsFile());
}
@TaskAction
public void scan() {
try (Scanner scanner = new Scanner(classpath)) {
for (File source : sources) {
scanner.scan(source);
}
} catch (IOException e) {
getLogger().error("Failed to write API file", e);
throw new InvalidUserCodeException(e.getMessage(), e);
}
}
class Scanner implements Closeable {
private final URLClassLoader classpathLoader;
private final Class extends Annotation> metadataClass;
private final Method classTypeMethod;
private Collection internalAnnotations;
private Collection invisibleAnnotations;
private Collection inheritedAnnotations;
@SuppressWarnings("unchecked")
Scanner(URLClassLoader classpathLoader) {
this.classpathLoader = classpathLoader;
this.invisibleAnnotations = ANNOTATION_BLACKLIST;
this.inheritedAnnotations = emptySet();
this.internalAnnotations = emptySet();
Class extends Annotation> kClass;
Method kMethod;
try {
kClass = (Class) Class.forName(KOTLIN_METADATA, true, classpathLoader);
kMethod = kClass.getDeclaredMethod(KOTLIN_CLASSTYPE_METHOD);
} catch (ClassNotFoundException | NoSuchMethodException e) {
kClass = null;
kMethod = null;
}
metadataClass = kClass;
classTypeMethod = kMethod;
}
Scanner(FileCollection classpath) throws MalformedURLException {
this(new URLClassLoader(toURLs(classpath)));
}
@Override
public void close() throws IOException {
classpathLoader.close();
}
void scan(File source) {
File target = outputDir.map(dir -> toTargetFile(dir, source)).get().getAsFile();
getLogger().info("API file: {}", target.getAbsolutePath());
try (
URLClassLoader appLoader = new URLClassLoader(new URL[]{toURL(source)}, classpathLoader);
ApiPrintWriter writer = new ApiPrintWriter(target, "UTF-8")
) {
scan(writer, appLoader);
} catch (IOException e) {
getLogger().error("API scan has failed", e);
throw new InvalidUserCodeException(e.getMessage(), e);
}
}
void scan(ApiPrintWriter writer, ClassLoader appLoader) {
try (ScanResult result = new ClassGraph()
.rejectPackages(excludePackages.get().toArray(new String[0]))
.rejectClasses(excludeClasses.get().toArray(new String[0]))
.overrideClassLoaders(appLoader)
.ignoreParentClassLoaders()
.ignoreMethodVisibility()
.ignoreFieldVisibility()
.disableDirScanning()
.enableStaticFinalFieldConstantInitializerValues()
.enableExternalClasses()
.enableAnnotationInfo()
.enableClassInfo()
.enableMethodInfo()
.enableFieldInfo()
.verbose(verbose.get())
.scan()) {
loadAnnotationCaches(result);
getLogger().info("Annotations:");
getLogger().info("- Inherited: {}", inheritedAnnotations);
getLogger().info("- Internal: {}", internalAnnotations);
getLogger().info("- Invisible: {}", invisibleAnnotations);
writeApis(writer, result);
}
}
private void loadAnnotationCaches(@Nonnull ScanResult result) {
ClassInfoList scannedAnnotations = result.getAllAnnotations();
Set internal = scannedAnnotations.getNames().stream()
.filter(s -> s.endsWith(INTERNAL_ANNOTATION_NAME))
.collect(toCollection(LinkedHashSet::new));
internal.add(DEFAULT_INTERNAL_ANNOTATION);
internalAnnotations = unmodifiableSet(internal);
Set invisible = internalAnnotations.stream()
.flatMap(a -> scannedAnnotations
.filter(i -> i.hasAnnotation(a))
.getNames()
.stream())
.collect(toCollection(LinkedHashSet::new));
invisible.addAll(ANNOTATION_BLACKLIST);
invisible.addAll(internal);
invisibleAnnotations = unmodifiableSet(invisible);
List inherited = scannedAnnotations
.filter(a -> a.loadClass().isAnnotationPresent(Inherited.class))
.getNames();
inheritedAnnotations = unmodifiableSet(new LinkedHashSet<>(inherited));
}
private void writeApis(ApiPrintWriter writer, @Nonnull ScanResult result) {
Map allInfo = result.getAllClassesAsMap();
result.getAllClasses().getNames().forEach(className -> {
if (className.contains(".internal.")) {
// These classes belong to internal Corda packages.
return;
}
ClassInfo classInfo = allInfo.get(className);
if (classInfo.isExternalClass()) {
// Ignore classes that belong to one of our target ClassLoader's parents.
return;
}
if (classInfo.isAnnotation() && !isVisibleAnnotation(className)) {
// Exclude these annotations from the output,
// e.g. because they're internal to Kotlin or Corda.
return;
}
if (hasInternalAnnotation(classInfo.getAnnotations().directOnly().getNames())) {
// Excludes classes annotated with any @CordaInternal annotation.
return;
}
Class> javaClass = result.loadClass(className, false);
if (!isVisible(javaClass.getModifiers())) {
// Excludes private and package-protected classes
return;
}
if (classInfo.getFullyQualifiedDefiningMethodName() != null) {
// Ignore Kotlin auto-generated internal classes
// which are not part of the api
return;
}
int kotlinClassType = getKotlinClassType(javaClass);
if (kotlinClassType == KOTLIN_SYNTHETIC) {
// Exclude classes synthesised by the Kotlin compiler.
return;
}
writeClass(writer, classInfo);
writeMethods(writer, classInfo.getDeclaredMethodAndConstructorInfo());
writeFields(writer, classInfo.getDeclaredFieldInfo());
writer.println("##");
});
}
private void writeClass(ApiPrintWriter writer, @Nonnull ClassInfo classInfo) {
if (classInfo.isAnnotation()) {
writer.println(classInfo, INTERFACE_MASK, emptyList());
} else if (classInfo.isStandardClass()) {
writer.println(classInfo, CLASS_MASK, toNames(readClassAnnotationsFor(classInfo)).visible);
} else {
writer.println(classInfo, INTERFACE_MASK, toNames(readInterfaceAnnotationsFor(classInfo)).visible);
}
}
private void writeMethods(ApiPrintWriter writer, List methods) {
sort(methods);
for (MethodInfo method : methods) {
AnnotationInfoList methodAnnotations = method.getAnnotationInfo().directOnly();
if (isVisible(method.getModifiers()) // Only public and protected methods
&& !isExcluded(method) // Filter out methods explicitly excluded
&& isValid(method.getModifiers(), METHOD_MASK) // Excludes bridge methods
// Excludes methods annotated as @CordaInternal
&& !hasInternalAnnotation(methodAnnotations.getNames())
&& !isEnumConstructor(method)
&& !isKotlinInternalScope(method)) {
writer.println(method, methodAnnotations.filter(this::isVisibleAnnotation), " ");
}
}
}
private void writeFields(ApiPrintWriter writer, List fields) {
sort(fields);
for (FieldInfo field : fields) {
AnnotationInfoList fieldAnnotations = field.getAnnotationInfo().directOnly();
if (isVisible(field.getModifiers())
&& isValid(field.getModifiers(), FIELD_MASK)
&& !hasInternalAnnotation(fieldAnnotations.getNames())) {
writer.println(field, fieldAnnotations.filter(this::isVisibleAnnotation), " ");
}
}
}
private int getKotlinClassType(Class> javaClass) {
if (metadataClass != null) {
Annotation metadata = javaClass.getAnnotation(metadataClass);
if (metadata != null) {
try {
return (int) classTypeMethod.invoke(metadata);
} catch (IllegalAccessException | InvocationTargetException e) {
Throwable ex = (e instanceof InvocationTargetException) ? e.getCause() : e;
getLogger().error("Failed to read Kotlin annotation", ex);
throw new InvalidUserCodeException(ex.getMessage(), ex);
}
}
}
return 0;
}
@Nonnull
private Names toNames(@Nonnull Collection classes) {
Map> partitioned = classes.stream()
.map(ClassInfo::getName)
.filter(ScanApi::isApplicationClass)
.collect(partitioningBy(this::isVisibleAnnotation, toCollection(ArrayList::new)));
List visible = partitioned.get(true);
int idx = visible.indexOf(DONOTIMPLEMENT_ANNOTATION_NAME);
if (idx != -1) {
// Raise @DoNotImplement to top of list.
swap(visible, 0, idx);
sort(visible.subList(1, visible.size()));
} else {
sort(visible);
}
return new Names(visible, ordering(partitioned.get(false)));
}
@Nonnull
private Set readClassAnnotationsFor(@Nonnull ClassInfo classInfo) {
// The annotation ordering doesn't matter, as they will be sorted later.
Set annotations = new HashSet<>(classInfo.getAnnotations().directOnly());
annotations.addAll(selectInheritedAnnotations(classInfo.getSuperclasses()));
annotations.addAll(selectInheritedAnnotations(classInfo.getInterfaces().getImplementedInterfaces()));
return annotations;
}
@Nonnull
private Set readInterfaceAnnotationsFor(@Nonnull ClassInfo classInfo) {
// The annotation ordering doesn't matter, as they will be sorted later.
Set annotations = new HashSet<>(classInfo.getAnnotations().directOnly());
annotations.addAll(selectInheritedAnnotations(classInfo.getInterfaces()));
return annotations;
}
/**
* Returns those annotations which have themselves been annotated as "Inherited".
*/
private List selectInheritedAnnotations(@Nonnull Collection classes) {
return classes.stream()
.flatMap(cls -> cls.getAnnotations().directOnly().stream())
.filter(ann -> inheritedAnnotations.contains(ann.getName()))
.collect(toList());
}
private boolean isVisibleAnnotation(@Nonnull AnnotationInfo annotation) {
return isVisibleAnnotation(annotation.getName());
}
private boolean isVisibleAnnotation(String className) {
return !invisibleAnnotations.contains(className);
}
private boolean hasInternalAnnotation(@Nonnull Collection annotationNames) {
return annotationNames.stream().anyMatch(internalAnnotations::contains);
}
}
private static > List ordering(List list) {
sort(list);
return list;
}
private static boolean isKotlinInternalScope(@Nonnull MethodInfo method) {
return method.getName().indexOf('$') >= 0;
}
// Kotlin 1.2 declares Enum constructors as protected, although
// both Java and Kotlin 1.3 declare them as private. But exclude
// them because Enum classes are final anyway.
private static boolean isEnumConstructor(@Nonnull MethodInfo method) {
return method.isConstructor() && method.getClassInfo().extendsSuperclass(ENUM_BASE_CLASS);
}
private static boolean isValid(int modifiers, int mask) {
return (modifiers & mask) == modifiers;
}
private boolean isExcluded(@Nonnull MethodInfo method) {
final String methodSignature = method.getName() + method.getTypeDescriptorStr();
final String className = method.getClassInfo().getName();
Provider extends Set> excluded = excludeMethods.getting(className);
return excluded.isPresent() && excluded.get().contains(methodSignature);
}
private static boolean isVisible(int accessFlags) {
return (accessFlags & VISIBILITY_MASK) != 0;
}
private static boolean isApplicationClass(@Nonnull String typeName) {
return !typeName.startsWith("java.") && !typeName.startsWith("kotlin.");
}
@Nonnull
private static URL toURL(@Nonnull File file) throws MalformedURLException {
return file.toURI().toURL();
}
@Nonnull
private static URL[] toURLs(@Nonnull Iterable files) throws MalformedURLException {
List urls = new LinkedList<>();
for (File file : files) {
urls.add(toURL(file));
}
return urls.toArray(new URL[0]);
}
}
class Names {
List visible;
@SuppressWarnings("WeakerAccess")
List hidden;
Names(List visible, List hidden) {
this.visible = unmodifiable(visible);
this.hidden = unmodifiable(hidden);
}
private static List unmodifiable(@Nonnull List list) {
return list.isEmpty() ? emptyList() : unmodifiableList(new ArrayList<>(list));
}
}