blob: 96d8b8f620575355027afb19256638f5a645a957 [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 net.starlark.java.annot.processor;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.SetMultimap;
import com.google.errorprone.annotations.FormatMethod;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.WildcardType;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import net.starlark.java.annot.Param;
import net.starlark.java.annot.ParamType;
import net.starlark.java.annot.StarlarkBuiltin;
import net.starlark.java.annot.StarlarkMethod;
/**
* Annotation processor for {@link StarlarkMethod}. See that class for requirements.
*
* <p>These properties can be relied upon at runtime without additional checks.
*/
@SupportedAnnotationTypes({
"net.starlark.java.annot.StarlarkMethod",
"net.starlark.java.annot.StarlarkBuiltin"
})
public class StarlarkMethodProcessor extends AbstractProcessor {
private Types types;
private Elements elements;
private Messager messager;
// A set containing a TypeElement for each class with a StarlarkMethod.selfCall annotation.
private Set<Element> classesWithSelfcall;
// A multimap where keys are class element, and values are the callable method names identified in
// that class (where "method name" is StarlarkMethod.name).
private SetMultimap<Element, String> processedClassMethods;
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
this.types = env.getTypeUtils();
this.elements = env.getElementUtils();
this.messager = env.getMessager();
this.classesWithSelfcall = new HashSet<>();
this.processedClassMethods = LinkedHashMultimap.create();
}
private TypeMirror getType(String canonicalName) {
return elements.getTypeElement(canonicalName).asType();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
TypeMirror stringType = getType("java.lang.String");
TypeMirror integerType = getType("java.lang.Integer");
TypeMirror booleanType = getType("java.lang.Boolean");
TypeMirror listType = getType("java.util.List");
TypeMirror mapType = getType("java.util.Map");
TypeMirror starlarkValueType = getType("net.starlark.java.eval.StarlarkValue");
// Ensure StarlarkBuiltin-annotated classes implement StarlarkValue.
for (Element cls : roundEnv.getElementsAnnotatedWith(StarlarkBuiltin.class)) {
if (!types.isAssignable(cls.asType(), starlarkValueType)) {
errorf(
cls,
"class %s has StarlarkBuiltin annotation but does not implement StarlarkValue",
cls.getSimpleName());
}
}
for (Element element : roundEnv.getElementsAnnotatedWith(StarlarkMethod.class)) {
// Only methods are annotated with StarlarkMethod.
// This is ensured by the @Target(ElementType.METHOD) annotation.
ExecutableElement method = (ExecutableElement) element;
if (!method.getModifiers().contains(Modifier.PUBLIC)) {
errorf(method, "StarlarkMethod-annotated methods must be public.");
}
if (method.getModifiers().contains(Modifier.STATIC)) {
errorf(method, "StarlarkMethod-annotated methods cannot be static.");
}
// Check the annotation itself.
StarlarkMethod annot = method.getAnnotation(StarlarkMethod.class);
if (annot.name().isEmpty()) {
errorf(method, "StarlarkMethod.name must be non-empty.");
}
Element cls = method.getEnclosingElement();
if (!processedClassMethods.put(cls, annot.name())) {
errorf(method, "Containing class defines more than one method named '%s'.", annot.name());
}
if (annot.documented() && annot.doc().isEmpty()) {
errorf(method, "The 'doc' string must be non-empty if 'documented' is true.");
}
if (annot.structField()) {
checkStructFieldAnnotation(method, annot);
} else if (annot.useStarlarkSemantics()) {
errorf(
method,
"a StarlarkMethod-annotated method with structField=false may not also specify"
+ " useStarlarkSemantics. (Instead, set useStarlarkThread and call"
+ " getSemantics().)");
}
if (annot.selfCall() && !classesWithSelfcall.add(cls)) {
errorf(method, "Containing class has more than one selfCall method defined.");
}
boolean hasFlag = false;
if (!annot.enableOnlyWithFlag().isEmpty()) {
if (!hasPlusMinusPrefix(annot.enableOnlyWithFlag())) {
errorf(method, "enableOnlyWithFlag name must have a + or - prefix");
}
hasFlag = true;
}
if (!annot.disableWithFlag().isEmpty()) {
if (!hasPlusMinusPrefix(annot.disableWithFlag())) {
errorf(method, "disableWithFlag name must have a + or - prefix");
}
if (hasFlag) {
errorf(
method,
"Only one of StarlarkMethod.enableOnlyWithFlag and StarlarkMethod.disableWithFlag"
+ " may be specified.");
}
hasFlag = true;
}
if (annot.allowReturnNones() != (method.getAnnotation(Nullable.class) != null)) {
errorf(method, "Method must be annotated with @Nullable iff allowReturnNones is set.");
}
checkParameters(method, annot);
// Verify that result type, if final, might satisfy Starlark.fromJava.
// (If the type is non-final we can't prove that all subclasses are invalid.)
TypeMirror ret = method.getReturnType();
if (ret.getKind() == TypeKind.DECLARED) {
DeclaredType obj = (DeclaredType) ret;
if (obj.asElement().getModifiers().contains(Modifier.FINAL)
&& !types.isSameType(ret, stringType)
&& !types.isSameType(ret, integerType)
&& !types.isSameType(ret, booleanType)
&& !types.isAssignable(obj, starlarkValueType)
&& !types.isAssignable(obj, listType)
&& !types.isAssignable(obj, mapType)) {
errorf(
method,
"StarlarkMethod-annotated method %s returns %s, which has no legal Starlark values"
+ " (see Starlark.fromJava)",
method.getSimpleName(),
ret);
}
}
}
// Returning false allows downstream processors to work on the same annotations
return false;
}
// TODO(adonovan): obviate these checks by separating field/method interfaces.
private void checkStructFieldAnnotation(ExecutableElement method, StarlarkMethod annot) {
// useStructField is incompatible with special thread-related parameters,
// because unlike a method, which is actively called within a thread,
// a field is a passive part of a data structure that may be accessed
// from Java threads that don't have anything to do with Starlark threads.
// However, the StarlarkSemantics is available even to fields,
// because it is a required parameter for all attribute-selection
// operations x.f.
//
// Not having a thread forces implementations to assume Mutability=null,
// which is not quite right. Perhaps one day we can abolish Mutability
// in favor of a tracing approach as in go.starlark.net.
if (annot.useStarlarkThread()) {
errorf(
method,
"a StarlarkMethod-annotated method with structField=true may not also specify"
+ " useStarlarkThread");
}
if (!annot.extraPositionals().name().isEmpty()) {
errorf(
method,
"a StarlarkMethod-annotated method with structField=true may not also specify"
+ " extraPositionals");
}
if (!annot.extraKeywords().name().isEmpty()) {
errorf(
method,
"a StarlarkMethod-annotated method with structField=true may not also specify"
+ " extraKeywords");
}
if (annot.selfCall()) {
errorf(
method,
"a StarlarkMethod-annotated method with structField=true may not also specify"
+ " selfCall=true");
}
int nparams = annot.parameters().length;
if (nparams > 0) {
errorf(
method,
"method %s is annotated structField=true but also has %d Param annotations",
method.getSimpleName(),
nparams);
}
}
private void checkParameters(ExecutableElement method, StarlarkMethod annot) {
List<? extends VariableElement> params = method.getParameters();
boolean allowPositionalNext = true;
boolean allowPositionalOnlyNext = true;
boolean allowNonDefaultPositionalNext = true;
boolean hasUndocumentedMethods = false;
// Check @Param annotations match parameters.
Param[] paramAnnots = annot.parameters();
for (int i = 0; i < paramAnnots.length; i++) {
Param paramAnnot = paramAnnots[i];
if (i >= params.size()) {
errorf(
method,
"method %s has %d Param annotations but only %d parameters",
method.getSimpleName(),
paramAnnots.length,
params.size());
return;
}
VariableElement param = params.get(i);
checkParameter(param, paramAnnot);
// Check parameter ordering.
if (paramAnnot.positional()) {
if (!allowPositionalNext) {
errorf(
param,
"Positional parameter '%s' is specified after one or more non-positional parameters",
paramAnnot.name());
}
if (!paramAnnot.named() && !allowPositionalOnlyNext) {
errorf(
param,
"Positional-only parameter '%s' is specified after one or more named or undocumented"
+ " parameters",
paramAnnot.name());
}
if (paramAnnot.defaultValue().isEmpty()) { // There is no default value.
if (!allowNonDefaultPositionalNext) {
errorf(
param,
"Positional parameter '%s' has no default value but is specified after one "
+ "or more positional parameters with default values",
paramAnnot.name());
}
} else { // There is a default value.
// No positional parameters without a default value can come after this parameter.
allowNonDefaultPositionalNext = false;
}
} else { // Not positional.
// No positional parameters can come after this parameter.
allowPositionalNext = false;
if (!paramAnnot.named()) {
errorf(param, "Parameter '%s' must be either positional or named", paramAnnot.name());
}
}
if (!paramAnnot.documented()) {
hasUndocumentedMethods = true;
}
if (paramAnnot.named() || !paramAnnot.documented()) {
// No positional-only parameters can come after this parameter.
allowPositionalOnlyNext = false;
}
}
if (hasUndocumentedMethods && !annot.extraKeywords().name().isEmpty()) {
errorf(
method,
"Method '%s' has undocumented parameters but also allows extra keyword parameters",
annot.name());
}
checkSpecialParams(method, annot);
}
// Checks consistency of a single parameter with its Param annotation.
private void checkParameter(Element param, Param paramAnnot) {
TypeMirror paramType = param.asType(); // type of the Java method parameter
// Give helpful hint for parameter of type Integer.
TypeMirror integerType = getType("java.lang.Integer");
if (types.isSameType(paramType, integerType)) {
errorf(
param,
"use StarlarkInt, not Integer for parameter '%s' (and see Starlark.toInt)",
paramAnnot.name());
}
// Reject an entry of Param.allowedTypes if not assignable to the parameter variable.
for (ParamType paramTypeAnnot : paramAnnot.allowedTypes()) {
TypeMirror t = getParamTypeType(paramTypeAnnot);
if (!types.isAssignable(t, types.erasure(paramType))) {
errorf(
param,
"annotated allowedTypes entry %s of parameter '%s' is not assignable to variable of "
+ "type %s",
t,
paramAnnot.name(),
paramType);
}
}
// Reject generic types C<T> other than C<?>,
// since reflective calls check only the toplevel class.
if (paramType instanceof DeclaredType) {
DeclaredType declaredType = (DeclaredType) paramType;
for (TypeMirror typeArg : declaredType.getTypeArguments()) {
if (!(typeArg instanceof WildcardType)) {
errorf(
param,
"parameter '%s' has generic type %s, but only wildcard type parameters are"
+ " allowed. Type inference in a Starlark-exposed method is unsafe. See"
+ " StarlarkMethod class documentation for details.",
param.getSimpleName(),
paramType);
}
}
}
// Check sense of flag-controlled parameters.
boolean hasFlag = false;
if (!paramAnnot.enableOnlyWithFlag().isEmpty()) {
if (!hasPlusMinusPrefix(paramAnnot.enableOnlyWithFlag())) {
errorf(param, "enableOnlyWithFlag name must have a + or - prefix");
}
hasFlag = true;
}
if (!paramAnnot.disableWithFlag().isEmpty()) {
if (!hasPlusMinusPrefix(paramAnnot.disableWithFlag())) {
errorf(param, "disableWithFlag name must have a + or - prefix");
}
if (hasFlag) {
errorf(
param,
"Parameter '%s' has enableOnlyWithFlag and disableWithFlag set. At most one may be set",
paramAnnot.name());
}
hasFlag = true;
}
if (hasFlag == paramAnnot.valueWhenDisabled().isEmpty()) {
errorf(
param,
hasFlag
? "Parameter '%s' may be disabled by semantic flag, thus valueWhenDisabled must be"
+ " set"
: "Parameter '%s' has valueWhenDisabled set, but is always enabled",
paramAnnot.name());
}
// Ensure positional arguments are documented.
if (!paramAnnot.documented() && paramAnnot.positional()) {
errorf(
param, "Parameter '%s' must be documented because it is positional.", paramAnnot.name());
}
}
private static boolean hasPlusMinusPrefix(String s) {
return s.charAt(0) == '-' || s.charAt(0) == '+';
}
// Returns the logical type of ParamType.type.
private static TypeMirror getParamTypeType(ParamType paramType) {
// See explanation of this hack at Element.getAnnotation
// and at https://stackoverflow.com/a/10167558.
try {
paramType.type();
throw new IllegalStateException("unreachable");
} catch (MirroredTypeException ex) {
return ex.getTypeMirror();
}
}
private void checkSpecialParams(ExecutableElement method, StarlarkMethod annot) {
if (!annot.extraPositionals().enableOnlyWithFlag().isEmpty()
|| !annot.extraPositionals().disableWithFlag().isEmpty()) {
errorf(method, "The extraPositionals parameter may not be toggled by semantic flag");
}
if (!annot.extraKeywords().enableOnlyWithFlag().isEmpty()
|| !annot.extraKeywords().disableWithFlag().isEmpty()) {
errorf(method, "The extraKeywords parameter may not be toggled by semantic flag");
}
List<? extends VariableElement> params = method.getParameters();
int index = annot.parameters().length;
// insufficient parameters?
int special = numExpectedSpecialParams(annot);
if (index + special > params.size()) {
errorf(
method,
"method %s is annotated with %d Params plus %d special parameters, but has only %d"
+ " parameter variables",
method.getSimpleName(),
index,
special,
params.size());
return; // not safe to proceed
}
if (!annot.extraPositionals().name().isEmpty()) {
VariableElement param = params.get(index++);
// Allow any supertype of Tuple.
TypeMirror tupleType =
types.getDeclaredType(elements.getTypeElement("net.starlark.java.eval.Tuple"));
if (!types.isAssignable(tupleType, param.asType())) {
errorf(
param,
"extraPositionals special parameter '%s' has type %s, to which a Tuple cannot be"
+ " assigned",
param.getSimpleName(),
param.asType());
}
}
if (!annot.extraKeywords().name().isEmpty()) {
VariableElement param = params.get(index++);
// Allow any supertype of Dict<String, Object>.
TypeMirror dictOfStringObjectType =
types.getDeclaredType(
elements.getTypeElement("net.starlark.java.eval.Dict"),
getType("java.lang.String"),
getType("java.lang.Object"));
if (!types.isAssignable(dictOfStringObjectType, param.asType())) {
errorf(
param,
"extraKeywords special parameter '%s' has type %s, to which Dict<String, Object>"
+ " cannot be assigned",
param.getSimpleName(),
param.asType());
}
}
if (annot.useStarlarkThread()) {
VariableElement param = params.get(index++);
TypeMirror threadType = getType("net.starlark.java.eval.StarlarkThread");
if (!types.isSameType(threadType, param.asType())) {
errorf(
param,
"for useStarlarkThread special parameter '%s', got type %s, want StarlarkThread",
param.getSimpleName(),
param.asType());
}
}
if (annot.useStarlarkSemantics()) {
VariableElement param = params.get(index++);
TypeMirror semanticsType = getType("net.starlark.java.eval.StarlarkSemantics");
if (!types.isSameType(semanticsType, param.asType())) {
errorf(
param,
"for useStarlarkSemantics special parameter '%s', got type %s, want StarlarkSemantics",
param.getSimpleName(),
param.asType());
}
}
// surplus parameters?
if (index < params.size()) {
errorf(
params.get(index), // first surplus parameter
"method %s is annotated with %d Params plus %d special parameters, yet has %d parameter"
+ " variables",
method.getSimpleName(),
annot.parameters().length,
special,
params.size());
}
}
private static int numExpectedSpecialParams(StarlarkMethod annot) {
int n = 0;
n += annot.extraPositionals().name().isEmpty() ? 0 : 1;
n += annot.extraKeywords().name().isEmpty() ? 0 : 1;
n += annot.useStarlarkThread() ? 1 : 0;
n += annot.useStarlarkSemantics() ? 1 : 0;
return n;
}
// Reports a (formatted) error and fails the compilation.
@FormatMethod
private void errorf(Element e, String format, Object... args) {
messager.printMessage(Diagnostic.Kind.ERROR, String.format(format, args), e);
}
}