blob: 0704c3888f72633863381a080b3cd93a0668da11 [file] [log] [blame]
// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.docgen;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.docgen.skylark.SkylarkBuiltinMethodDoc;
import com.google.devtools.build.docgen.skylark.SkylarkConstructorMethodDoc;
import com.google.devtools.build.docgen.skylark.SkylarkJavaMethodDoc;
import com.google.devtools.build.docgen.skylark.SkylarkModuleDoc;
import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
import com.google.devtools.build.lib.skylarkinterface.SkylarkConstructor;
import com.google.devtools.build.lib.skylarkinterface.SkylarkGlobalLibrary;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModuleCategory;
import com.google.devtools.build.lib.skylarkinterface.SkylarkSignature;
import com.google.devtools.build.lib.syntax.FuncallExpression;
import com.google.devtools.build.lib.syntax.Runtime;
import com.google.devtools.build.lib.util.Classpath;
import com.google.devtools.build.lib.util.Classpath.ClassPathException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.annotation.Nullable;
/**
* A helper class that collects Skylark module documentation.
*/
final class SkylarkDocumentationCollector {
@SkylarkModule(
name = "globals",
title = "Globals",
category = SkylarkModuleCategory.TOP_LEVEL_TYPE,
doc = "Objects, functions and modules registered in the global environment."
)
private static final class TopLevelModule {}
// Common prefix of packages that may contain Skylark modules.
private static final String MODULES_PACKAGE_PREFIX = "com/google/devtools/build";
private SkylarkDocumentationCollector() {}
/**
* Returns the SkylarkModule annotation for the top-level Skylark module.
*/
public static SkylarkModule getTopLevelModule() {
return TopLevelModule.class.getAnnotation(SkylarkModule.class);
}
/**
* Collects the documentation for all Skylark modules and returns a map that maps Skylark module
* name to the module documentation.
*
* <p>WARNING: This method no longer supports the specification of additional module classes via
* parameters. Instead, all module classes are being picked up automatically.
*/
public static Map<String, SkylarkModuleDoc> collectModules()
throws ClassPathException {
Map<String, SkylarkModuleDoc> modules = new TreeMap<>();
for (Class<?> candidateClass : Classpath.findClasses(MODULES_PACKAGE_PREFIX)) {
SkylarkModule moduleAnnotation = candidateClass.getAnnotation(SkylarkModule.class);
if (moduleAnnotation != null) {
collectJavaObjects(moduleAnnotation, candidateClass, modules);
} else if (candidateClass.getAnnotation(SkylarkGlobalLibrary.class) != null) {
collectBuiltinMethods(modules, candidateClass);
}
collectBuiltinDoc(modules, candidateClass.getDeclaredFields());
}
return modules;
}
private static SkylarkModuleDoc getTopLevelModuleDoc(Map<String, SkylarkModuleDoc> modules) {
SkylarkModule annotation = getTopLevelModule();
modules.computeIfAbsent(
annotation.name(), (String k) -> new SkylarkModuleDoc(annotation, TopLevelModule.class));
return modules.get(annotation.name());
}
private static SkylarkModuleDoc getSkylarkModuleDoc(
Class<?> moduleClass, Map<String, SkylarkModuleDoc> modules) {
if (moduleClass.equals(Object.class)) {
return getTopLevelModuleDoc(modules);
}
SkylarkModule annotation = Preconditions.checkNotNull(
Runtime.getSkylarkNamespace(moduleClass).getAnnotation(SkylarkModule.class));
SkylarkModuleDoc previousModuleDoc = modules.get(annotation.name());
if (previousModuleDoc == null || !previousModuleDoc.getAnnotation().documented()) {
// There is no registered module doc by that name, or the current candidate is "undocumented".
modules.put(annotation.name(), new SkylarkModuleDoc(annotation, moduleClass));
} else if (previousModuleDoc.getClassObject() != moduleClass && annotation.documented()) {
// Both modules generate documentation for the same name. This is fine if one is a
// subclass of the other, in which case the subclass documentation takes precedence.
// (This is useful if one module is a "common" stable module, and its subclass is
// an experimental module that also supports all stable methods.)
if (previousModuleDoc.getClassObject().isAssignableFrom(moduleClass)) {
modules.put(annotation.name(), new SkylarkModuleDoc(annotation, moduleClass));
} else if (moduleClass.isAssignableFrom(previousModuleDoc.getClassObject())) {
// This case means the subclass was processed first, so discard the superclass.
} else {
throw new IllegalStateException(
String.format(
"%s and %s are both modules with the same documentation for '%s'",
moduleClass,
previousModuleDoc.getClassObject(),
previousModuleDoc.getAnnotation().name()));
}
}
return modules.get(annotation.name());
}
/**
* Collects and returns all the Java objects reachable in Skylark from (and including)
* firstClass with the corresponding SkylarkModule annotation.
*
* <p>Note that the {@link SkylarkModule} annotation for firstClass - firstModule -
* is also an input parameter, because some top level Skylark built-in objects and methods
* are not annotated on the class, but on a field referencing them.
*/
@VisibleForTesting
static void collectJavaObjects(SkylarkModule firstModule, Class<?> firstClass,
Map<String, SkylarkModuleDoc> modules) {
Set<Class<?>> done = new HashSet<>();
Deque<Class<?>> toProcess = new ArrayDeque<>();
toProcess.addLast(firstClass);
while (!toProcess.isEmpty()) {
Class<?> c = toProcess.removeFirst();
if (done.contains(c)) {
continue;
}
SkylarkModuleDoc module = getSkylarkModuleDoc(c, modules);
done.add(c);
if (module.javaMethodsNotCollected()) {
ImmutableMap<Method, SkylarkCallable> methods =
FuncallExpression.collectSkylarkMethodsWithAnnotation(c);
for (Map.Entry<Method, SkylarkCallable> entry : methods.entrySet()) {
if (entry.getKey().isAnnotationPresent(SkylarkConstructor.class)) {
collectConstructor(modules, module.getName(), entry.getKey(), entry.getValue());
} else {
module.addMethod(
new SkylarkJavaMethodDoc(module.getName(), entry.getKey(), entry.getValue()));
}
Class<?> returnClass = entry.getKey().getReturnType();
if (returnClass.isAnnotationPresent(SkylarkModule.class)) {
toProcess.addLast(returnClass);
} else {
Map.Entry<Method, SkylarkCallable> selfCallConstructor =
getSelfCallConstructorMethod(returnClass);
if (selfCallConstructor != null) {
// If the class to be processed is not annotated with @SkylarkModule, then its
// @SkylarkCallable methods are not processed, as it does not have its own
// documentation page. However, if it is a callable object (has a selfCall method)
// that is also a constructor for another type, we still want to ensure that method
// is documented.
// This is used for builtin providers, which typically are not marked @SkylarkModule,
// but which have selfCall constructors for their corresponding Info class.
// For example, the "mymodule" module may return a callable object at mymodule.foo
// which constructs instances of the Bar class. The type returned by mymodule.foo
// may have no documentation, but mymodule.foo should be documented as a
// constructor of Bar objects.
collectConstructor(modules, module.getName(),
selfCallConstructor.getKey(), selfCallConstructor.getValue());
}
}
}
}
}
}
@Nullable
private static Map.Entry<Method, SkylarkCallable> getSelfCallConstructorMethod(
Class<?> objectClass) {
ImmutableMap<Method, SkylarkCallable> methods =
FuncallExpression.collectSkylarkMethodsWithAnnotation(objectClass);
for (Map.Entry<Method, SkylarkCallable> entry : methods.entrySet()) {
if (entry.getValue().selfCall()
&& entry.getKey().isAnnotationPresent(SkylarkConstructor.class)) {
// It's illegal, and checked by the interpreter, for there to be more than one method
// annotated with selfCall. Thus, it's valid to return on the first find.
return entry;
}
}
return null;
}
private static void collectBuiltinDoc(Map<String, SkylarkModuleDoc> modules, Field[] fields) {
for (Field field : fields) {
if (field.isAnnotationPresent(SkylarkSignature.class)) {
SkylarkSignature skylarkSignature = field.getAnnotation(SkylarkSignature.class);
Class<?> moduleClass = skylarkSignature.objectType();
SkylarkModuleDoc module = getSkylarkModuleDoc(moduleClass, modules);
module.addMethod(new SkylarkBuiltinMethodDoc(module, skylarkSignature, field.getType()));
}
}
}
private static void collectBuiltinMethods(
Map<String, SkylarkModuleDoc> modules, Class<?> moduleClass) {
SkylarkModuleDoc topLevelModuleDoc = getTopLevelModuleDoc(modules);
ImmutableMap<Method, SkylarkCallable> methods =
FuncallExpression.collectSkylarkMethodsWithAnnotation(moduleClass);
for (Map.Entry<Method, SkylarkCallable> entry : methods.entrySet()) {
if (entry.getKey().isAnnotationPresent(SkylarkConstructor.class)) {
collectConstructor(modules, "", entry.getKey(), entry.getValue());
} else {
topLevelModuleDoc.addMethod(new SkylarkJavaMethodDoc("", entry.getKey(), entry.getValue()));
}
}
}
private static void collectConstructor(Map<String, SkylarkModuleDoc> modules,
String originatingModuleName, Method method, SkylarkCallable callable) {
SkylarkConstructor constructorAnnotation =
Preconditions.checkNotNull(method.getAnnotation(SkylarkConstructor.class));
Class<?> objectClass = constructorAnnotation.objectType();
SkylarkModuleDoc module = getSkylarkModuleDoc(objectClass, modules);
String fullyQualifiedName;
if (!constructorAnnotation.receiverNameForDoc().isEmpty()) {
fullyQualifiedName = constructorAnnotation.receiverNameForDoc();
} else {
fullyQualifiedName = getFullyQualifiedName(originatingModuleName, callable);
}
module.setConstructor(new SkylarkConstructorMethodDoc(fullyQualifiedName, method, callable));
}
private static String getFullyQualifiedName(
String objectName, SkylarkCallable callable) {
String objectDotExpressionPrefix = objectName.isEmpty() ? "" : objectName + ".";
String methodName = callable.name();
return objectDotExpressionPrefix + methodName;
}
}