| // 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 com.google.devtools.build.lib.util.StringUtilities; | |
| 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, Object.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)); | |
| modules.computeIfAbsent( | |
| annotation.name(), (String k) -> new SkylarkModuleDoc(annotation, moduleClass)); | |
| 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, method, callable); | |
| } | |
| module.setConstructor(new SkylarkConstructorMethodDoc(fullyQualifiedName, method, callable)); | |
| } | |
| private static String getFullyQualifiedName( | |
| String objectName, Method method, SkylarkCallable callable) { | |
| String objectDotExpressionPrefix = | |
| objectName.isEmpty() ? "" : objectName + "."; | |
| String methodName = callable.name().isEmpty() | |
| ? StringUtilities.toPythonStyleFunctionName(method.getName()) | |
| : callable.name(); | |
| return objectDotExpressionPrefix + methodName; | |
| } | |
| } |