blob: 00c6bf252540febe2b9f7a1110c84b03f817540a [file] [log] [blame]
// Copyright 2018 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.collect.ImmutableList;
import com.google.devtools.build.docgen.builtin.BuiltinProtos.ApiContext;
import com.google.devtools.build.docgen.builtin.BuiltinProtos.Builtins;
import com.google.devtools.build.docgen.builtin.BuiltinProtos.Callable;
import com.google.devtools.build.docgen.builtin.BuiltinProtos.Param;
import com.google.devtools.build.docgen.builtin.BuiltinProtos.Type;
import com.google.devtools.build.docgen.builtin.BuiltinProtos.Value;
import com.google.devtools.build.docgen.skylark.SkylarkConstructorMethodDoc;
import com.google.devtools.build.docgen.skylark.SkylarkMethodDoc;
import com.google.devtools.build.docgen.skylark.SkylarkModuleDoc;
import com.google.devtools.build.docgen.skylark.SkylarkParamDoc;
import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
import com.google.devtools.build.lib.skylarkinterface.SkylarkInterfaceUtils;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
import com.google.devtools.build.lib.syntax.BaseFunction;
import com.google.devtools.build.lib.syntax.BuiltinCallable;
import com.google.devtools.build.lib.syntax.CallUtils;
import com.google.devtools.build.lib.syntax.FunctionSignature;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.common.options.OptionsParser;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/** The main class for the Skylark documentation generator. */
public class ApiExporter {
private static void appendTypes(
Builtins.Builder builtins,
Map<String, SkylarkModuleDoc> types,
List<RuleDocumentation> nativeRules)
throws BuildEncyclopediaDocException {
for (Entry<String, SkylarkModuleDoc> modEntry : types.entrySet()) {
SkylarkModuleDoc mod = modEntry.getValue();
Type.Builder type = Type.newBuilder();
type.setName(mod.getName());
type.setDoc(mod.getDocumentation());
for (SkylarkMethodDoc meth : mod.getJavaMethods()) {
// Constructors are exported as global symbols.
if (!(meth instanceof SkylarkConstructorMethodDoc)) {
Value.Builder value = collectMethodInfo(meth);
// Methods from the native package are available as top level functions in BUILD files.
if (mod.getName().equals("native")) {
value.setApiContext(ApiContext.BUILD);
builtins.addGlobal(value);
value.setApiContext(ApiContext.BZL);
type.addField(value);
} else {
value.setApiContext(ApiContext.ALL);
type.addField(value);
}
}
}
// Native rules are available in BZL file as methods of the native package.
if (mod.getName().equals("native")) {
for (RuleDocumentation rule : nativeRules) {
Value.Builder field = collectRuleInfo(rule);
field.setApiContext(ApiContext.BZL);
type.addField(field);
}
}
builtins.addType(type);
}
}
// Globals are available for both BUILD and BZL files.
private static void appendGlobals(Builtins.Builder builtins, Map<String, Object> globalMethods) {
for (Entry<String, Object> entry : globalMethods.entrySet()) {
Object obj = entry.getValue();
Value.Builder value = Value.newBuilder();
if (obj instanceof BaseFunction) {
value = valueFromFunction((BaseFunction) obj);
} else if (obj instanceof BuiltinCallable) {
value = valueFromAnnotation(((BuiltinCallable) obj).getAnnotation());
} else {
value.setName(entry.getKey());
}
value.setApiContext(ApiContext.ALL);
builtins.addGlobal(value);
}
}
private static void appendBzlGlobals(
Builtins.Builder builtins, Map<String, Object> starlarkGlobals) {
for (Entry<String, Object> entry : starlarkGlobals.entrySet()) {
Object obj = entry.getValue();
Value.Builder value = Value.newBuilder();
if (obj instanceof BaseFunction) {
value = valueFromFunction((BaseFunction) obj);
} else {
SkylarkModule typeModule = SkylarkInterfaceUtils.getSkylarkModule(obj.getClass());
if (typeModule != null) {
SkylarkCallable selfCall = CallUtils.getSelfCallAnnotation(obj.getClass());
if (selfCall != null) {
value = valueFromAnnotation(selfCall);
} else {
value.setName(entry.getKey());
value.setType(entry.getKey());
value.setDoc(typeModule.doc());
}
}
}
value.setApiContext(ApiContext.BZL);
builtins.addGlobal(value);
}
}
// Native rules are available as top level functions in BUILD files.
private static void appendNativeRules(
Builtins.Builder builtins, List<RuleDocumentation> nativeRules)
throws BuildEncyclopediaDocException {
for (RuleDocumentation rule : nativeRules) {
Value.Builder global = collectRuleInfo(rule);
global.setApiContext(ApiContext.BUILD);
builtins.addGlobal(global);
}
}
private static Value.Builder valueFromFunction(BaseFunction func) {
return collectFunctionInfo(func.getName(), func.getSignature(), func.getDefaultValues());
}
private static Value.Builder valueFromAnnotation(SkylarkCallable annot) {
Pair<FunctionSignature, List<String>> pair = getSignature(annot);
return collectFunctionInfo(annot.name(), pair.first, pair.second);
}
// defaultValues may be values or expressions; we only call toString on them.
private static Value.Builder collectFunctionInfo(
String funcName, FunctionSignature sig, List<?> defaultValues) {
Value.Builder value = Value.newBuilder();
value.setName(funcName);
Callable.Builder callable = Callable.newBuilder();
ImmutableList<String> paramNames = sig.getParameterNames();
int positionals = sig.numMandatoryPositionals();
int optionals = sig.numOptionals();
int nameIndex = 0;
for (int i = 0; i < positionals; i++) {
Param.Builder param = Param.newBuilder();
param.setName(paramNames.get(nameIndex));
param.setIsMandatory(true);
callable.addParam(param);
nameIndex++;
}
for (int i = 0; i < optionals; i++) {
Param.Builder param = Param.newBuilder();
param.setName(paramNames.get(nameIndex));
param.setIsMandatory(false);
param.setDefaultValue(defaultValues.get(i).toString());
callable.addParam(param);
nameIndex++;
}
if (sig.hasVarargs()) {
Param.Builder param = Param.newBuilder();
param.setName("*" + paramNames.get(nameIndex));
param.setIsMandatory(false);
param.setIsStarArg(true);
nameIndex++;
callable.addParam(param);
}
if (sig.hasKwargs()) {
Param.Builder param = Param.newBuilder();
param.setIsMandatory(false);
param.setIsStarStarArg(true);
param.setName("**" + paramNames.get(nameIndex));
callable.addParam(param);
}
value.setCallable(callable);
return value;
}
private static Value.Builder collectMethodInfo(SkylarkMethodDoc meth) {
Value.Builder field = Value.newBuilder();
field.setName(meth.getShortName());
field.setDoc(meth.getDocumentation());
if (meth.isCallable()) {
Callable.Builder callable = Callable.newBuilder();
for (SkylarkParamDoc par : meth.getParams()) {
Param.Builder param = newParam(par.getName(), par.getDefaultValue().isEmpty());
param.setType(par.getType());
param.setDoc(par.getDocumentation());
param.setDefaultValue(par.getDefaultValue());
callable.addParam(param);
}
callable.setReturnType(meth.getReturnType());
field.setCallable(callable);
} else {
field.setType(meth.getReturnType());
}
return field;
}
private static Param.Builder newParam(String name, Boolean isMandatory) {
Param.Builder param = Param.newBuilder();
param.setName(name);
param.setIsMandatory(isMandatory);
return param;
}
private static Value.Builder collectRuleInfo(RuleDocumentation rule)
throws BuildEncyclopediaDocException {
Value.Builder value = Value.newBuilder();
value.setName(rule.getRuleName());
value.setDoc(rule.getHtmlDocumentation());
Callable.Builder callable = Callable.newBuilder();
// All native rules have attribute "name". It is not included in the attributes list and needs
// to be added separately.
callable.addParam(newParam("name", true));
for (RuleDocumentationAttribute attr : rule.getAttributes()) {
callable.addParam(newParam(attr.getAttributeName(), attr.isMandatory()));
}
value.setCallable(callable);
return value;
}
private static void writeBuiltins(String filename, Builtins.Builder builtins) throws IOException {
try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(filename))) {
Builtins build = builtins.build();
build.writeTo(out);
}
}
private static void printUsage(OptionsParser parser) {
System.err.println(
"Usage: api_exporter_bin -n product_name -p rule_class_provider (-i input_dir)+\n"
+ " -f outputFile [-b blacklist] [-h]\n\n"
+ "Exports all Starlark builtins to a file including the embedded native rules.\n"
+ "The product name (-n), rule class provider (-p), output file (-f) and at least \n"
+ " one input_dir (-i) must be specified.\n");
System.err.println(
parser.describeOptionsWithDeprecatedCategories(
Collections.<String, String>emptyMap(), OptionsParser.HelpVerbosity.LONG));
}
public static void main(String[] args) {
OptionsParser parser =
OptionsParser.builder().optionsClasses(BuildEncyclopediaOptions.class).build();
parser.parseAndExitUponError(args);
BuildEncyclopediaOptions options = parser.getOptions(BuildEncyclopediaOptions.class);
if (options.help) {
printUsage(parser);
Runtime.getRuntime().exit(0);
}
if (options.productName.isEmpty()
|| options.inputDirs.isEmpty()
|| options.provider.isEmpty()
|| options.outputFile.isEmpty()) {
printUsage(parser);
Runtime.getRuntime().exit(1);
}
try {
SymbolFamilies symbols =
new SymbolFamilies(
options.productName, options.provider, options.inputDirs, options.blacklist);
Builtins.Builder builtins = Builtins.newBuilder();
appendTypes(builtins, symbols.getTypes(), symbols.getNativeRules());
appendGlobals(builtins, symbols.getGlobals());
appendBzlGlobals(builtins, symbols.getBzlGlobals());
appendNativeRules(builtins, symbols.getNativeRules());
writeBuiltins(options.outputFile, builtins);
} catch (Throwable e) {
System.err.println("ERROR: " + e.getMessage());
e.printStackTrace();
}
}
// Extracts signature and parameter default value expressions from a SkylarkCallable annotation.
private static Pair<FunctionSignature, List<String>> getSignature(SkylarkCallable annot) {
// Build-time annotation processing ensures mandatory parameters do not follow optional ones.
int mandatoryPositionals = 0;
int optionalPositionals = 0;
int mandatoryNamedOnly = 0;
int optionalNamedOnly = 0;
boolean hasStar = false;
String star = null;
String starStar = null;
ArrayList<String> params = new ArrayList<>();
ArrayList<String> defaults = new ArrayList<>();
// optional named-only parameters are kept aside to be spliced after the mandatory ones.
ArrayList<String> optionalNamedOnlyParams = new ArrayList<>();
ArrayList<String> optionalNamedOnlyDefaultValues = new ArrayList<>();
for (com.google.devtools.build.lib.skylarkinterface.Param param : annot.parameters()) {
// Implicit * or *args parameter separates transition from positional to named.
// f (..., *, ... ) or f(..., *args, ...)
if (param.named() && !param.positional() && !hasStar) {
hasStar = true;
if (!annot.extraPositionals().name().isEmpty()) {
star = annot.extraPositionals().name();
}
}
boolean mandatory = param.defaultValue().isEmpty();
if (mandatory) {
// f(..., name, ...): required parameter
params.add(param.name());
if (hasStar) {
mandatoryNamedOnly++;
} else {
mandatoryPositionals++;
}
} else {
// f(..., name=value, ...): optional parameter
if (hasStar) {
optionalNamedOnly++;
optionalNamedOnlyParams.add(param.name());
optionalNamedOnlyDefaultValues.add(orNone(param.defaultValue()));
} else {
optionalPositionals++;
params.add(param.name());
defaults.add(orNone(param.defaultValue()));
}
}
}
params.addAll(optionalNamedOnlyParams);
defaults.addAll(optionalNamedOnlyDefaultValues);
// f(..., *args, ...)
if (!annot.extraPositionals().name().isEmpty() && !hasStar) {
star = annot.extraPositionals().name();
}
if (star != null) {
params.add(star);
}
// f(..., **kwargs)
if (!annot.extraKeywords().name().isEmpty()) {
starStar = annot.extraKeywords().name();
params.add(starStar);
}
// TODO(adonovan): simplify; the sole caller doesn't need a complete FunctionSignature.
FunctionSignature signature =
FunctionSignature.create(
mandatoryPositionals,
optionalPositionals,
mandatoryNamedOnly,
optionalNamedOnly,
star != null,
starStar != null,
ImmutableList.copyOf(params));
return Pair.of(signature, defaults);
}
private static String orNone(String x) {
return x.isEmpty() ? "None" : x;
}
}