// Copyright 2014 Google Inc. 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.Function; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.devtools.build.docgen.SkylarkJavaInterfaceExplorer.SkylarkBuiltinMethod; import com.google.devtools.build.docgen.SkylarkJavaInterfaceExplorer.SkylarkJavaMethod; import com.google.devtools.build.docgen.SkylarkJavaInterfaceExplorer.SkylarkModuleDoc; import com.google.devtools.build.lib.packages.MethodLibrary; import com.google.devtools.build.lib.rules.SkylarkModules; import com.google.devtools.build.lib.rules.SkylarkRuleContext; import com.google.devtools.build.lib.syntax.Environment; import com.google.devtools.build.lib.syntax.Environment.NoneType; import com.google.devtools.build.lib.syntax.EvalUtils; import com.google.devtools.build.lib.syntax.SkylarkBuiltin; import com.google.devtools.build.lib.syntax.SkylarkBuiltin.Param; import com.google.devtools.build.lib.syntax.SkylarkCallable; import com.google.devtools.build.lib.syntax.SkylarkModule; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; /** * A class to assemble documentation for Skylark. */ public class SkylarkDocumentationProcessor { private static final String TOP_LEVEL_ID = "_top_level"; private static final boolean USE_TEMPLATE = false; @SkylarkModule(name = "Global objects, functions and modules", doc = "Objects, functions and modules registered in the global environment.") private static final class TopLevelModule {} static SkylarkModule getTopLevelModule() { return TopLevelModule.class.getAnnotation(SkylarkModule.class); } /** * Generates the Skylark documentation to the given output directory. */ public void generateDocumentation(String outputPath) throws IOException, BuildEncyclopediaDocException { BufferedWriter bw = null; File skylarkDocPath = new File(outputPath); try { bw = new BufferedWriter(new FileWriter(skylarkDocPath)); if (USE_TEMPLATE) { bw.write(SourceFileReader.readTemplateContents(DocgenConsts.SKYLARK_BODY_TEMPLATE, ImmutableMap.of( DocgenConsts.VAR_SECTION_SKYLARK_BUILTIN, generateAllBuiltinDoc()))); } else { bw.write(generateAllBuiltinDoc()); } System.out.println("Skylark documentation generated: " + skylarkDocPath.getAbsolutePath()); } finally { if (bw != null) { bw.close(); } } } @VisibleForTesting Map collectModules() { Map modules = new TreeMap<>(); Map builtinModules = collectBuiltinModules(); Map> builtinJavaObjects = collectBuiltinJavaObjects(); modules.putAll(builtinModules); SkylarkJavaInterfaceExplorer explorer = new SkylarkJavaInterfaceExplorer(); for (SkylarkModuleDoc builtinObject : builtinModules.values()) { // Check the return type for built-in functions, it can be a module previously not added. for (SkylarkBuiltinMethod builtinMethod : builtinObject.getBuiltinMethods().values()) { Class type = builtinMethod.annotation.returnType(); if (type.isAnnotationPresent(SkylarkModule.class)) { explorer.collect(type.getAnnotation(SkylarkModule.class), type, modules); } } explorer.collect(builtinObject.getAnnotation(), builtinObject.getClassObject(), modules); } for (Entry> builtinModule : builtinJavaObjects.entrySet()) { explorer.collect(builtinModule.getKey(), builtinModule.getValue(), modules); } return modules; } private String generateAllBuiltinDoc() { Map modules = collectModules(); StringBuilder sb = new StringBuilder(); // Generate the top level module first in the doc SkylarkModuleDoc topLevelModule = modules.remove(getTopLevelModule().name()); generateModuleDoc(topLevelModule, sb); for (SkylarkModuleDoc module : modules.values()) { if (!module.getAnnotation().hidden()) { sb.append("
"); generateModuleDoc(module, sb); } } return sb.toString(); } private void generateModuleDoc(SkylarkModuleDoc module, StringBuilder sb) { SkylarkModule annotation = module.getAnnotation(); sb.append(String.format("

%s

\n", getModuleId(annotation), annotation.name())) .append(annotation.doc()) .append("\n"); sb.append("
    "); // Sort Java and SkylarkBuiltin methods together. The map key is only used for sorting. TreeMap methodMap = new TreeMap<>(); for (SkylarkJavaMethod method : module.getJavaMethods()) { methodMap.put(method.name + method.method.getParameterTypes().length, method); } for (SkylarkBuiltinMethod builtin : module.getBuiltinMethods().values()) { methodMap.put(builtin.annotation.name(), builtin); } for (Object object : methodMap.values()) { if (object instanceof SkylarkJavaMethod) { SkylarkJavaMethod method = (SkylarkJavaMethod) object; generateDirectJavaMethodDoc(annotation.name(), method.name, method.method, method.callable, sb); } if (object instanceof SkylarkBuiltinMethod) { generateBuiltinItemDoc(getModuleId(annotation), (SkylarkBuiltinMethod) object, sb); } } sb.append("
"); } private String getModuleId(SkylarkModule annotation) { if (annotation == getTopLevelModule()) { return TOP_LEVEL_ID; } else { return annotation.name(); } } private void generateBuiltinItemDoc( String moduleId, SkylarkBuiltinMethod method, StringBuilder sb) { SkylarkBuiltin annotation = method.annotation; if (annotation.hidden()) { return; } sb.append(String.format("
  • %s

    \n", moduleId, annotation.name(), annotation.name())); if (com.google.devtools.build.lib.syntax.Function.class.isAssignableFrom(method.fieldClass)) { sb.append(getSignature(moduleId, annotation)); } else { if (!annotation.returnType().equals(Object.class)) { sb.append("" + getTypeAnchor(annotation.returnType()) + "
    "); } } sb.append(annotation.doc() + "\n"); printParams(moduleId, annotation, sb); } private void printParams(String moduleId, SkylarkBuiltin annotation, StringBuilder sb) { if (annotation.mandatoryParams().length + annotation.optionalParams().length > 0) { sb.append("

    Parameters

    \n"); printParams(moduleId, annotation.name(), annotation.mandatoryParams(), sb); printParams(moduleId, annotation.name(), annotation.optionalParams(), sb); } else { sb.append("
    \n"); } } private void generateDirectJavaMethodDoc(String objectName, String methodName, Method method, SkylarkCallable annotation, StringBuilder sb) { if (annotation.hidden()) { return; } sb.append(String.format("
  • %s

    \n%s\n", objectName, methodName, methodName, getSignature(objectName, methodName, method))) .append(annotation.doc()) .append(getReturnTypeExtraMessage(annotation)) .append("\n"); } private String getReturnTypeExtraMessage(SkylarkCallable annotation) { if (annotation.allowReturnNones()) { return " May return None.\n"; } return ""; } private String getSignature(String objectName, String methodName, Method method) { String args = method.getAnnotation(SkylarkCallable.class).structField() ? "" : "(" + getParameterString(method) + ")"; return String.format("%s %s.%s%s
    ", getTypeAnchor(method.getReturnType()), objectName, methodName, args); } private String getSignature(String objectName, SkylarkBuiltin method) { List argList = new ArrayList<>(); for (Param param : method.mandatoryParams()) { argList.add(param.name()); } for (Param param : method.optionalParams()) { argList.add(param.name() + "?"); } String args = "(" + Joiner.on(", ").join(argList) + ")"; if (!objectName.equals(TOP_LEVEL_ID)) { return String.format("%s %s.%s%s
    \n", getTypeAnchor(method.returnType()), objectName, method.name(), args); } else { return String.format("%s %s%s
    \n", getTypeAnchor(method.returnType()), method.name(), args); } } private String getTypeAnchor(Class returnType, Class generic1) { return getTypeAnchor(returnType) + " of " + getTypeAnchor(generic1) + "s"; } private String getTypeAnchor(Class returnType) { if (returnType.equals(String.class)) { return "string"; } else if (Map.class.isAssignableFrom(returnType)) { return "dict"; } else if (returnType.equals(Void.TYPE) || returnType.equals(NoneType.class)) { return "None"; } else if (returnType.isAnnotationPresent(SkylarkModule.class)) { // TODO(bazel-team): this can produce dead links for types don't show up in the doc. // The correct fix is to generate those types (e.g. SkylarkFileType) too. String module = returnType.getAnnotation(SkylarkModule.class).name(); return "" + module + ""; } else { return EvalUtils.getDataTypeNameFromClass(returnType); } } private String getParameterString(Method method) { return Joiner.on(", ").join(Iterables.transform( ImmutableList.copyOf(method.getParameterTypes()), new Function, String>() { @Override public String apply(Class input) { return getTypeAnchor(input); } })); } private void printParams(String moduleId, String methodName, Param[] params, StringBuilder sb) { if (params.length > 0) { sb.append("
      \n"); for (Param param : params) { String paramType = param.type().equals(Object.class) ? "" : (param.generic1().equals(Object.class) ? " (" + getTypeAnchor(param.type()) + ")" : " (" + getTypeAnchor(param.type(), param.generic1()) + ")"); sb.append(String.format("\t
    • %s%s: ", moduleId, methodName, param.name(), param.name(), paramType)) .append(param.doc()) .append("\n\t
    • \n"); } sb.append("
    \n"); } } private Map collectBuiltinModules() { Map modules = new HashMap<>(); collectBuiltinDoc(modules, Environment.class.getDeclaredFields()); collectBuiltinDoc(modules, MethodLibrary.class.getDeclaredFields()); for (Class moduleClass : SkylarkModules.MODULES) { collectBuiltinDoc(modules, moduleClass.getDeclaredFields()); } return modules; } private Map> collectBuiltinJavaObjects() { Map> modules = new HashMap<>(); collectBuiltinModule(modules, SkylarkRuleContext.class); return modules; } /** * Returns the top level modules and functions with their documentation in a command-line * printable format. */ public Map collectTopLevelModules() { Map modules = new TreeMap<>(); for (SkylarkModuleDoc doc : collectBuiltinModules().values()) { if (doc.getAnnotation() == getTopLevelModule()) { for (Map.Entry entry : doc.getBuiltinMethods().entrySet()) { if (!entry.getValue().annotation.hidden()) { modules.put(entry.getKey(), DocgenConsts.toCommandLineFormat(entry.getValue().annotation.doc())); } } } else { modules.put(doc.getAnnotation().name(), DocgenConsts.toCommandLineFormat(doc.getAnnotation().doc())); } } return modules; } /** * Returns the API doc for the specified Skylark object in a command line printable format, * params[0] identifies either a module or a top-level object, the optional params[1] identifies a * method in the module.
    * Returns null if no Skylark object is found. */ public String getCommandLineAPIDoc(String[] params) { Map modules = collectModules(); SkylarkModuleDoc toplevelModuleDoc = modules.get(getTopLevelModule().name()); if (modules.containsKey(params[0])) { // Top level module SkylarkModuleDoc module = modules.get(params[0]); if (params.length == 1) { String moduleName = module.getAnnotation().name(); StringBuilder sb = new StringBuilder(); sb.append(moduleName).append("\n\t").append(module.getAnnotation().doc()).append("\n"); // Print the signature of all built-in methods for (SkylarkBuiltinMethod method : module.getBuiltinMethods().values()) { printBuiltinFunctionDoc(moduleName, method.annotation, sb); } // Print all Java methods for (SkylarkJavaMethod method : module.getJavaMethods()) { printJavaFunctionDoc(moduleName, method, sb); } return DocgenConsts.toCommandLineFormat(sb.toString()); } else { return getFunctionDoc(module.getAnnotation().name(), params[1], module); } } else if (toplevelModuleDoc.getBuiltinMethods().containsKey(params[0])){ // Top level object / function return getFunctionDoc(null, params[0], toplevelModuleDoc); } return null; } private String getFunctionDoc(String moduleName, String methodName, SkylarkModuleDoc module) { if (module.getBuiltinMethods().containsKey(methodName)) { // Create the doc for the built-in function SkylarkBuiltinMethod method = module.getBuiltinMethods().get(methodName); StringBuilder sb = new StringBuilder(); printBuiltinFunctionDoc(moduleName, method.annotation, sb); printParams(moduleName, method.annotation, sb); return DocgenConsts.removeDuplicatedNewLines(DocgenConsts.toCommandLineFormat(sb.toString())); } else { // Search if there are matching Java functions StringBuilder sb = new StringBuilder(); boolean foundMatchingMethod = false; for (SkylarkJavaMethod method : module.getJavaMethods()) { if (method.name.equals(methodName)) { printJavaFunctionDoc(moduleName, method, sb); foundMatchingMethod = true; } } if (foundMatchingMethod) { return DocgenConsts.toCommandLineFormat(sb.toString()); } } return null; } private void printBuiltinFunctionDoc( String moduleName, SkylarkBuiltin annotation, StringBuilder sb) { if (moduleName != null) { sb.append(moduleName).append("."); } sb.append(annotation.name()).append("\n\t").append(annotation.doc()).append("\n"); } private void printJavaFunctionDoc(String moduleName, SkylarkJavaMethod method, StringBuilder sb) { sb.append(getSignature(moduleName, method.name, method.method)) .append("\t").append(method.callable.doc()).append("\n"); } private void collectBuiltinModule( Map> modules, Class moduleClass) { if (moduleClass.isAnnotationPresent(SkylarkModule.class)) { SkylarkModule skylarkModule = moduleClass.getAnnotation(SkylarkModule.class); modules.put(skylarkModule, moduleClass); } } private void collectBuiltinDoc(Map modules, Field[] fields) { for (Field field : fields) { if (field.isAnnotationPresent(SkylarkBuiltin.class)) { SkylarkBuiltin skylarkBuiltin = field.getAnnotation(SkylarkBuiltin.class); Class moduleClass = skylarkBuiltin.objectType(); SkylarkModule skylarkModule = moduleClass.equals(Object.class) ? getTopLevelModule() : moduleClass.getAnnotation(SkylarkModule.class); if (!modules.containsKey(skylarkModule.name())) { modules.put(skylarkModule.name(), new SkylarkModuleDoc(skylarkModule, moduleClass)); } modules.get(skylarkModule.name()).getBuiltinMethods() .put(skylarkBuiltin.name(), new SkylarkBuiltinMethod(skylarkBuiltin, field.getType())); } } } }