// 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. * *

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 collectModules() throws ClassPathException { Map 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 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 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. * *

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 modules) { Set> done = new HashSet<>(); Deque> 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 methods = FuncallExpression.collectSkylarkMethodsWithAnnotation(c); for (Map.Entry 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 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 getSelfCallConstructorMethod( Class objectClass) { ImmutableMap methods = FuncallExpression.collectSkylarkMethodsWithAnnotation(objectClass); for (Map.Entry 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 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 modules, Class moduleClass) { SkylarkModuleDoc topLevelModuleDoc = getTopLevelModuleDoc(modules); ImmutableMap methods = FuncallExpression.collectSkylarkMethodsWithAnnotation(moduleClass); for (Map.Entry 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 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; } }