| // 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.base.Preconditions; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Iterables; |
| import com.google.devtools.build.docgen.annot.DocCategory; |
| import com.google.devtools.build.docgen.annot.DocumentMethods; |
| import com.google.devtools.build.docgen.annot.StarlarkConstructor; |
| import com.google.devtools.build.docgen.starlark.StarlarkBuiltinDoc; |
| import com.google.devtools.build.docgen.starlark.StarlarkConstructorMethodDoc; |
| import com.google.devtools.build.docgen.starlark.StarlarkJavaMethodDoc; |
| import com.google.devtools.build.lib.util.Classpath; |
| import com.google.devtools.build.lib.util.Classpath.ClassPathException; |
| import java.lang.reflect.Method; |
| import java.util.Map; |
| import java.util.TreeMap; |
| import javax.annotation.Nullable; |
| import net.starlark.java.annot.StarlarkAnnotations; |
| import net.starlark.java.annot.StarlarkBuiltin; |
| import net.starlark.java.annot.StarlarkMethod; |
| import net.starlark.java.eval.Starlark; |
| import net.starlark.java.eval.StarlarkSemantics; |
| import net.starlark.java.eval.StarlarkValue; |
| |
| /** A helper class that collects Starlark module documentation. */ |
| final class StarlarkDocumentationCollector { |
| @StarlarkBuiltin( |
| name = "globals", |
| category = DocCategory.TOP_LEVEL_TYPE, |
| doc = "Objects, functions and modules registered in the global environment.") |
| private static final class TopLevelModule implements StarlarkValue {} |
| |
| private StarlarkDocumentationCollector() {} |
| |
| /** Returns the StarlarkBuiltin annotation for the top-level Starlark module. */ |
| public static StarlarkBuiltin getTopLevelModule() { |
| return TopLevelModule.class.getAnnotation(StarlarkBuiltin.class); |
| } |
| |
| private static ImmutableMap<String, StarlarkBuiltinDoc> all; |
| |
| /** Applies {@link #collectModules} to all Bazel and Starlark classes. */ |
| static synchronized ImmutableMap<String, StarlarkBuiltinDoc> getAllModules() |
| throws ClassPathException { |
| if (all == null) { |
| all = |
| collectModules( |
| Iterables.concat( |
| Classpath.findClasses("com/google/devtools/build"), // Bazel |
| Classpath.findClasses("net/starlark/java"))); // Starlark |
| } |
| return all; |
| } |
| |
| /** |
| * Collects the documentation for all Starlark modules comprised of the given classes and returns |
| * a map from the name of each Starlark module to its documentation. |
| */ |
| static ImmutableMap<String, StarlarkBuiltinDoc> collectModules(Iterable<Class<?>> classes) { |
| Map<String, StarlarkBuiltinDoc> modules = new TreeMap<>(); |
| // The top level module first. |
| // (This is a special case of {@link StarlarkBuiltinDoc} as it has no object name). |
| StarlarkBuiltin topLevelModule = getTopLevelModule(); |
| modules.put( |
| topLevelModule.name(), |
| new StarlarkBuiltinDoc(topLevelModule, /*title=*/ "Globals", TopLevelModule.class)); |
| |
| // Creating module documentation is done in three passes. |
| // 1. Add all classes/interfaces annotated with @StarlarkBuiltin with documented = true. |
| for (Class<?> candidateClass : classes) { |
| if (candidateClass.isAnnotationPresent(StarlarkBuiltin.class)) { |
| collectStarlarkModule(candidateClass, modules); |
| } |
| } |
| |
| // 2. Add all object methods and global functions. |
| // |
| // Also, explicitly process the Starlark interpreter's MethodLibrary |
| // class, which defines None, len, range, etc. |
| // TODO(adonovan): do this without peeking into the implementation, |
| // e.g. by looking at Starlark.UNIVERSE, something like this: |
| // |
| // for (Map<String, Object> e : Starlark.UNIVERSE.entrySet()) { |
| // if (e.getValue() instanceof BuiltinFunction) { |
| // BuiltinFunction fn = (BuiltinFunction) e.getValue(); |
| // topLevelModuleDoc.addMethod( |
| // new StarlarkJavaMethodDoc("", fn.getJavaMethod(), fn.getAnnotation())); |
| // } |
| // } |
| // |
| // Note that BuiltinFunction doesn't actually have getJavaMethod. |
| // |
| for (Class<?> candidateClass : classes) { |
| if (candidateClass.isAnnotationPresent(StarlarkBuiltin.class)) { |
| collectModuleMethods(candidateClass, modules); |
| } |
| if (candidateClass.isAnnotationPresent(DocumentMethods.class) |
| || candidateClass.getName().equals("net.starlark.java.eval.MethodLibrary")) { |
| collectDocumentedMethods(candidateClass, modules); |
| } |
| } |
| |
| // 3. Add all constructors. |
| for (Class<?> candidateClass : classes) { |
| if (candidateClass.isAnnotationPresent(StarlarkBuiltin.class) |
| || candidateClass.isAnnotationPresent(DocumentMethods.class)) { |
| collectConstructorMethods(candidateClass, modules); |
| } |
| } |
| |
| return ImmutableMap.copyOf(modules); |
| } |
| |
| /** |
| * Returns the {@link StarlarkBuiltinDoc} entry representing the collection of top level |
| * functions. (This is a special case of {@link StarlarkBuiltinDoc} as it has no object name). |
| */ |
| private static StarlarkBuiltinDoc getTopLevelModuleDoc(Map<String, StarlarkBuiltinDoc> modules) { |
| return modules.get(getTopLevelModule().name()); |
| } |
| |
| /** |
| * Adds a single {@link StarlarkBuiltinDoc} entry to {@code modules} representing the given {@code |
| * moduleClass}, if it is a documented module. |
| */ |
| private static void collectStarlarkModule( |
| Class<?> moduleClass, Map<String, StarlarkBuiltinDoc> modules) { |
| if (moduleClass.equals(TopLevelModule.class)) { |
| // The top level module doc is a special case and is handled separately. |
| return; |
| } |
| |
| StarlarkBuiltin moduleAnnotation = |
| Preconditions.checkNotNull(moduleClass.getAnnotation(StarlarkBuiltin.class)); |
| |
| if (moduleAnnotation.documented()) { |
| StarlarkBuiltinDoc previousModuleDoc = modules.get(moduleAnnotation.name()); |
| if (previousModuleDoc == null) { |
| modules.put( |
| moduleAnnotation.name(), |
| new StarlarkBuiltinDoc(moduleAnnotation, moduleAnnotation.name(), moduleClass)); |
| } else { |
| // Handle a strange corner-case: If moduleClass has a subclass which is also |
| // annotated with {@link StarlarkBuiltin} with the same name, and also has the same |
| // module-level docstring, then the subclass 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.) |
| validateCompatibleModules(previousModuleDoc.getClassObject(), moduleClass); |
| |
| if (previousModuleDoc.getClassObject().isAssignableFrom(moduleClass)) { |
| // The new module is a subclass of the old module, so use the subclass. |
| modules.put( |
| moduleAnnotation.name(), |
| new StarlarkBuiltinDoc( |
| moduleAnnotation, /*title=*/ moduleAnnotation.name(), moduleClass)); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Validate that it is acceptable that the given module classes with the same module name |
| * co-exist. |
| */ |
| private static void validateCompatibleModules(Class<?> one, Class<?> two) { |
| StarlarkBuiltin moduleOne = one.getAnnotation(StarlarkBuiltin.class); |
| StarlarkBuiltin moduleTwo = two.getAnnotation(StarlarkBuiltin.class); |
| if (one.isAssignableFrom(two) || two.isAssignableFrom(one)) { |
| if (!moduleOne.doc().equals(moduleTwo.doc())) { |
| throw new IllegalStateException( |
| String.format( |
| "%s and %s are related modules but have mismatching documentation for '%s'", |
| one, two, moduleOne.name())); |
| } |
| } else { |
| throw new IllegalStateException( |
| String.format( |
| "%s and %s are unrelated modules with documentation for '%s'", |
| one, two, moduleOne.name())); |
| } |
| } |
| |
| private static void collectModuleMethods( |
| Class<?> moduleClass, Map<String, StarlarkBuiltinDoc> modules) { |
| StarlarkBuiltin moduleAnnotation = |
| Preconditions.checkNotNull(moduleClass.getAnnotation(StarlarkBuiltin.class)); |
| |
| if (moduleAnnotation.documented()) { |
| StarlarkBuiltinDoc moduleDoc = |
| Preconditions.checkNotNull(modules.get(moduleAnnotation.name())); |
| |
| if (moduleClass == moduleDoc.getClassObject()) { |
| for (Map.Entry<Method, StarlarkMethod> entry : |
| Starlark.getMethodAnnotations(moduleClass).entrySet()) { |
| // Only collect methods not annotated with @StarlarkConstructor. |
| // Methods with @StarlarkConstructor are added later. |
| if (!entry.getKey().isAnnotationPresent(StarlarkConstructor.class)) { |
| moduleDoc.addMethod( |
| new StarlarkJavaMethodDoc(moduleDoc.getName(), entry.getKey(), entry.getValue())); |
| } |
| } |
| } |
| } |
| } |
| |
| @Nullable |
| private static Method getSelfCallConstructorMethod(Class<?> objectClass) { |
| Method selfCallMethod = Starlark.getSelfCallMethod(StarlarkSemantics.DEFAULT, objectClass); |
| if (selfCallMethod != null && selfCallMethod.isAnnotationPresent(StarlarkConstructor.class)) { |
| return selfCallMethod; |
| } |
| return null; |
| } |
| |
| /** |
| * Adds {@link StarlarkJavaMethodDoc} entries to the top level module, one for |
| * each @StarlarkMethod method defined in the given @DocumentMethods class {@code moduleClass}. |
| */ |
| private static void collectDocumentedMethods( |
| Class<?> moduleClass, Map<String, StarlarkBuiltinDoc> modules) { |
| StarlarkBuiltinDoc topLevelModuleDoc = getTopLevelModuleDoc(modules); |
| |
| for (Map.Entry<Method, StarlarkMethod> entry : |
| Starlark.getMethodAnnotations(moduleClass).entrySet()) { |
| // Only add non-constructor global library methods. Constructors are added later. |
| if (!entry.getKey().isAnnotationPresent(StarlarkConstructor.class)) { |
| topLevelModuleDoc.addMethod( |
| new StarlarkJavaMethodDoc("", entry.getKey(), entry.getValue())); |
| } |
| } |
| } |
| |
| private static void collectConstructor(Map<String, StarlarkBuiltinDoc> modules, Method method) { |
| Preconditions.checkNotNull(method.getAnnotation(StarlarkConstructor.class)); |
| |
| StarlarkBuiltin builtinType = StarlarkAnnotations.getStarlarkBuiltin(method.getReturnType()); |
| if (builtinType == null || !builtinType.documented()) { |
| // The class of the constructed object type has no documentation, so no place to add |
| // constructor information. |
| return; |
| } |
| StarlarkMethod methodAnnot = |
| Preconditions.checkNotNull(method.getAnnotation(StarlarkMethod.class)); |
| StarlarkBuiltinDoc doc = modules.get(builtinType.name()); |
| doc.setConstructor(new StarlarkConstructorMethodDoc(builtinType.name(), method, methodAnnot)); |
| } |
| |
| /** |
| * Collect two types of constructor methods: |
| * |
| * <p>1. Methods that are annotated with @StarlarkConstructor. |
| * |
| * <p>2. Structfield methods that return an object which itself has a method with selfCall = true, |
| * and is annotated with @StarlarkConstructor. (For example, suppose Foo has a structfield method |
| * 'bar'. If Foo.bar is itself callable, and is a constructor, then Foo.bar() should be treated |
| * like a constructor method.) |
| */ |
| private static void collectConstructorMethods( |
| Class<?> moduleClass, Map<String, StarlarkBuiltinDoc> modules) { |
| Method selfCallConstructor = getSelfCallConstructorMethod(moduleClass); |
| if (selfCallConstructor != null) { |
| collectConstructor(modules, selfCallConstructor); |
| } |
| |
| for (Method method : Starlark.getMethodAnnotations(moduleClass).keySet()) { |
| if (method.isAnnotationPresent(StarlarkConstructor.class)) { |
| collectConstructor(modules, method); |
| } |
| Class<?> returnClass = method.getReturnType(); |
| Method returnClassConstructor = getSelfCallConstructorMethod(returnClass); |
| if (returnClassConstructor != null) { |
| collectConstructor(modules, returnClassConstructor); |
| } |
| } |
| } |
| } |