blob: 711b138c42b0e9a3e0ebfaa77b82e1fc3b865338 [file] [log] [blame]
/*
* Copyright 2018 Google LLC
*
* 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.auto.value.processor;
import static com.google.auto.common.MoreElements.getLocalAndInheritedMethods;
import static com.google.auto.value.processor.ClassNames.AUTO_ONE_OF_NAME;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
import com.google.auto.common.AnnotationMirrors;
import com.google.auto.common.MoreElements;
import com.google.auto.common.MoreTypes;
import com.google.auto.service.AutoService;
import com.google.auto.value.processor.MissingTypes.MissingTypeException;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.annotation.processing.Processor;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import net.ltgt.gradle.incap.IncrementalAnnotationProcessor;
import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType;
/**
* Javac annotation processor (compiler plugin) for {@linkplain com.google.auto.value.AutoOneOf
* one-of} types; user code never references this class.
*
* @author Éamonn McManus
* @see <a href="https://github.com/google/auto/tree/master/value">AutoValue User's Guide</a>
*/
@AutoService(Processor.class)
@SupportedAnnotationTypes(AUTO_ONE_OF_NAME)
@IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.ISOLATING)
public class AutoOneOfProcessor extends AutoValueishProcessor {
public AutoOneOfProcessor() {
super(AUTO_ONE_OF_NAME);
}
@Override
boolean propertiesCanBeVoid() {
return true;
}
@Override
public ImmutableSet<String> getSupportedOptions() {
return ImmutableSet.of(Nullables.NULLABLE_OPTION);
}
@Override
void processType(TypeElement autoOneOfType) {
if (autoOneOfType.getKind() != ElementKind.CLASS) {
errorReporter()
.abortWithError(
autoOneOfType,
"[AutoOneOfNotClass] @" + AUTO_ONE_OF_NAME + " only applies to classes");
}
checkModifiersIfNested(autoOneOfType);
DeclaredType kindMirror = mirrorForKindType(autoOneOfType);
// We are going to classify the methods of the @AutoOneOf class into several categories.
// This covers the methods in the class itself and the ones it inherits from supertypes.
// First, the only concrete (non-abstract) methods we are interested in are overrides of
// Object methods (equals, hashCode, toString), which signal that we should not generate
// an implementation of those methods.
// Then, each abstract method is one of the following:
// (1) A property getter, like "abstract String foo()" or "abstract String getFoo()".
// (2) A kind getter, which is a method that returns the enum in @AutoOneOf. For
// example if we have @AutoOneOf(PetKind.class), this would be a method that returns
// PetKind.
// If there are abstract methods that don't fit any of the categories above, that is an error
// which we signal explicitly to avoid confusion.
ImmutableSet<ExecutableElement> methods =
getLocalAndInheritedMethods(
autoOneOfType, processingEnv.getTypeUtils(), processingEnv.getElementUtils());
ImmutableSet<ExecutableElement> abstractMethods = abstractMethodsIn(methods);
ExecutableElement kindGetter =
findKindGetterOrAbort(autoOneOfType, kindMirror, abstractMethods);
Set<ExecutableElement> otherMethods = new LinkedHashSet<>(abstractMethods);
otherMethods.remove(kindGetter);
ImmutableMap<ExecutableElement, TypeMirror> propertyMethodsAndTypes =
propertyMethodsIn(otherMethods, autoOneOfType);
ImmutableBiMap<String, ExecutableElement> properties =
propertyNameToMethodMap(propertyMethodsAndTypes.keySet());
validateMethods(autoOneOfType, abstractMethods, propertyMethodsAndTypes.keySet(), kindGetter);
ImmutableMap<String, String> propertyToKind =
propertyToKindMap(kindMirror, properties.keySet());
String subclass = generatedClassName(autoOneOfType, "AutoOneOf_");
AutoOneOfTemplateVars vars = new AutoOneOfTemplateVars();
vars.generatedClass = TypeSimplifier.simpleNameOf(subclass);
vars.propertyToKind = propertyToKind;
defineSharedVarsForType(autoOneOfType, methods, vars);
defineVarsForType(autoOneOfType, vars, propertyMethodsAndTypes, kindGetter);
String text = vars.toText();
text = TypeEncoder.decode(text, processingEnv, vars.pkg, autoOneOfType.asType());
text = Reformatter.fixup(text);
writeSourceFile(subclass, text, autoOneOfType);
}
private DeclaredType mirrorForKindType(TypeElement autoOneOfType) {
// The annotation is guaranteed to be present by the contract of Processor#process
AnnotationMirror oneOfAnnotation = getAnnotationMirror(autoOneOfType, AUTO_ONE_OF_NAME).get();
AnnotationValue kindValue = AnnotationMirrors.getAnnotationValue(oneOfAnnotation, "value");
Object value = kindValue.getValue();
if (value instanceof TypeMirror) {
TypeMirror kindType = (TypeMirror) value;
switch (kindType.getKind()) {
case DECLARED:
return MoreTypes.asDeclared(kindType);
case ERROR:
throw new MissingTypeException(MoreTypes.asError(kindType));
default:
break;
}
}
throw new MissingTypeException(null);
}
private ImmutableMap<String, String> propertyToKindMap(
DeclaredType kindMirror, ImmutableSet<String> propertyNames) {
// We require a one-to-one correspondence between the property names and the enum constants.
// We must have transformName(propertyName) = transformName(constantName) for each one.
// So we build two maps, transformName(propertyName) → propertyName and
// transformName(constantName) → constant. The key sets of the two maps must match, and we
// can then join them to make propertyName → constantName.
TypeElement kindElement = MoreElements.asType(kindMirror.asElement());
Map<String, String> transformedPropertyNames =
propertyNames.stream().collect(toMap(this::transformName, s -> s));
Map<String, Element> transformedEnumConstants =
kindElement.getEnclosedElements().stream()
.filter(e -> e.getKind().equals(ElementKind.ENUM_CONSTANT))
.collect(toMap(e -> transformName(e.getSimpleName().toString()), e -> e));
if (transformedPropertyNames.keySet().equals(transformedEnumConstants.keySet())) {
ImmutableMap.Builder<String, String> mapBuilder = ImmutableMap.builder();
for (String transformed : transformedPropertyNames.keySet()) {
mapBuilder.put(
transformedPropertyNames.get(transformed),
transformedEnumConstants.get(transformed).getSimpleName().toString());
}
return mapBuilder.build();
}
// The names don't match. Emit errors for the differences.
// Properties that have no enum constant
transformedPropertyNames.forEach(
(transformed, property) -> {
if (!transformedEnumConstants.containsKey(transformed)) {
errorReporter()
.reportError(
kindElement,
"[AutoOneOfNoEnumConstant] Enum has no constant with name corresponding to"
+ " property '%s'",
property);
}
});
// Enum constants that have no property
transformedEnumConstants.forEach(
(transformed, constant) -> {
if (!transformedPropertyNames.containsKey(transformed)) {
errorReporter()
.reportError(
constant,
"[AutoOneOfBadEnumConstant] Name of enum constant '%s' does not correspond to"
+ " any property name",
constant.getSimpleName());
}
});
throw new AbortProcessingException();
}
private String transformName(String s) {
return s.toLowerCase(Locale.ROOT).replace("_", "");
}
private ExecutableElement findKindGetterOrAbort(
TypeElement autoOneOfType,
TypeMirror kindMirror,
ImmutableSet<ExecutableElement> abstractMethods) {
Set<ExecutableElement> kindGetters =
abstractMethods.stream()
.filter(e -> sameType(kindMirror, e.getReturnType()))
.filter(e -> e.getParameters().isEmpty())
.collect(toSet());
switch (kindGetters.size()) {
case 0:
errorReporter()
.reportError(
autoOneOfType,
"[AutoOneOfNoKindGetter] %s must have a no-arg abstract method returning %s",
autoOneOfType,
kindMirror);
break;
case 1:
return Iterables.getOnlyElement(kindGetters);
default:
for (ExecutableElement getter : kindGetters) {
errorReporter()
.reportError(
getter,
"[AutoOneOfTwoKindGetters] More than one abstract method returns %s",
kindMirror);
}
}
throw new AbortProcessingException();
}
private void validateMethods(
TypeElement type,
ImmutableSet<ExecutableElement> abstractMethods,
ImmutableSet<ExecutableElement> propertyMethods,
ExecutableElement kindGetter) {
for (ExecutableElement method : abstractMethods) {
if (propertyMethods.contains(method)) {
checkReturnType(type, method);
} else if (!method.equals(kindGetter)
&& objectMethodToOverride(method) == ObjectMethod.NONE) {
// This could reasonably be an error, were it not for an Eclipse bug in
// ElementUtils.override that sometimes fails to recognize that one method overrides
// another, and therefore leaves us with both an abstract method and the subclass method
// that overrides it. This shows up in AutoValueTest.LukesBase for example.
// The compilation will fail anyway because the generated concrete classes won't
// implement this alien method.
errorReporter()
.reportWarning(
method,
"[AutoOneOfParams] Abstract methods in @AutoOneOf classes must have no parameters");
}
}
errorReporter().abortIfAnyError();
}
private void defineVarsForType(
TypeElement type,
AutoOneOfTemplateVars vars,
ImmutableMap<ExecutableElement, TypeMirror> propertyMethodsAndTypes,
ExecutableElement kindGetter) {
vars.props =
propertySet(
propertyMethodsAndTypes, ImmutableListMultimap.of(), ImmutableListMultimap.of());
vars.kindGetter = kindGetter.getSimpleName().toString();
vars.kindType = TypeEncoder.encode(kindGetter.getReturnType());
TypeElement javaIoSerializable = elementUtils().getTypeElement("java.io.Serializable");
vars.serializable =
javaIoSerializable != null // just in case
&& typeUtils().isAssignable(type.asType(), javaIoSerializable.asType());
}
@Override
Optional<String> nullableAnnotationForMethod(ExecutableElement propertyMethod) {
if (nullableAnnotationFor(propertyMethod, propertyMethod.getReturnType()).isPresent()) {
errorReporter()
.reportError(
propertyMethod, "[AutoOneOfNullable] @AutoOneOf properties cannot be @Nullable");
}
return Optional.empty();
}
private static boolean sameType(TypeMirror t1, TypeMirror t2) {
return MoreTypes.equivalence().equivalent(t1, t2);
}
}