Skip to content

Commit 6ee8d68

Browse files
eamonnmcmanusronshapiro
authored andcommitted
First version of @AutoOneOf processor
RELNOTES=Introduced @AutoOneOf for tagged-union types in Java. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182536936
1 parent db6f181 commit 6ee8d68

File tree

9 files changed

+1607
-187
lines changed

9 files changed

+1607
-187
lines changed

value/src/it/functional/src/test/java/com/google/auto/value/AutoOneOfTest.java

Lines changed: 418 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (C) 2018 Google, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.auto.value;
17+
18+
import java.lang.annotation.ElementType;
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
import java.lang.annotation.Target;
22+
23+
/**
24+
* Specifies that the annotated class is a <em>one-of</em> class, also known as a
25+
* <a href="https://en.wikipedia.org/wiki/Tagged_union"><em>tagged union</em></a>.
26+
* An {@code @AutoOneOf} class is very similar to an {@link AutoValue @AutoValue} class, in that its
27+
* abstract methods define a set of properties. But unlike {@code @AutoValue}, only one of those
28+
* properties is defined in any given instance.
29+
*
30+
* <pre>{@code @AutoOneOf(StringOrInteger.Kind.class)
31+
* public abstract class StringOrInteger {
32+
* public enum Kind {STRING, INTEGER}
33+
*
34+
* public abstract Kind getKind();
35+
*
36+
* public abstract String string();
37+
* public abstract int integer();
38+
*
39+
* public static StringOrInteger string(String s) {
40+
* return AutoOneOf_StringOrInteger.string(s);
41+
* }
42+
*
43+
* public static StringOrInteger integer(int i) {
44+
* return AutoOneOf_StringOrInteger.integer(i);
45+
* }
46+
* }
47+
*
48+
* String client(StringOrInteger stringOrInteger) {
49+
* switch (stringOrInteger.getKind()) {
50+
* case STRING:
51+
* return "the string '" + stringOrInteger.string() + "'";
52+
* case INTEGER:
53+
* return "the integer " + stringOrInteger.integer();
54+
* }
55+
* throw new AssertionError();
56+
* }}</pre>
57+
*
58+
* <!-- TODO(emcmanus): replace this example with a link to yet-to-be-written documentation. -->
59+
*
60+
* @author Chris Nokleberg
61+
* @author Éamonn McManus
62+
*/
63+
@Retention(RetentionPolicy.CLASS)
64+
@Target(ElementType.TYPE)
65+
public @interface AutoOneOf {
66+
/**
67+
* Specifies an enum that has one entry per variant in the one-of.
68+
*/
69+
Class<? extends Enum<?>> value();
70+
}
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
/*
2+
* Copyright (C) 2018 Google, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.auto.value.processor;
17+
18+
import static com.google.auto.common.GeneratedAnnotations.generatedAnnotation;
19+
import static com.google.auto.common.MoreElements.getLocalAndInheritedMethods;
20+
import static java.util.stream.Collectors.toMap;
21+
import static java.util.stream.Collectors.toSet;
22+
23+
import com.google.auto.common.AnnotationMirrors;
24+
import com.google.auto.common.MoreElements;
25+
import com.google.auto.common.MoreTypes;
26+
import com.google.auto.service.AutoService;
27+
import com.google.auto.value.AutoOneOf;
28+
import com.google.common.collect.ImmutableBiMap;
29+
import com.google.common.collect.ImmutableList;
30+
import com.google.common.collect.ImmutableMap;
31+
import com.google.common.collect.ImmutableSet;
32+
import com.google.common.collect.Iterables;
33+
import java.util.LinkedHashMap;
34+
import java.util.LinkedHashSet;
35+
import java.util.Locale;
36+
import java.util.Map;
37+
import java.util.Optional;
38+
import java.util.Set;
39+
import javax.annotation.processing.Processor;
40+
import javax.annotation.processing.SupportedOptions;
41+
import javax.lang.model.element.AnnotationMirror;
42+
import javax.lang.model.element.AnnotationValue;
43+
import javax.lang.model.element.Element;
44+
import javax.lang.model.element.ElementKind;
45+
import javax.lang.model.element.ExecutableElement;
46+
import javax.lang.model.element.TypeElement;
47+
import javax.lang.model.type.DeclaredType;
48+
import javax.lang.model.type.TypeKind;
49+
import javax.lang.model.type.TypeMirror;
50+
51+
/**
52+
* Javac annotation processor (compiler plugin) for one-of types; user code never references this
53+
* class.
54+
*
55+
* @see AutoOneOf
56+
* @see <a href="https://github.com/google/auto/tree/master/value">AutoValue User's Guide</a>
57+
*
58+
* @author Éamonn McManus
59+
*/
60+
@AutoService(Processor.class)
61+
@SupportedOptions("com.google.auto.value.OmitIdentifiers")
62+
public class AutoOneOfProcessor extends AutoValueOrOneOfProcessor {
63+
public AutoOneOfProcessor() {
64+
super(AutoOneOf.class);
65+
}
66+
67+
@Override
68+
void processType(TypeElement autoOneOfType) {
69+
if (autoOneOfType.getKind() != ElementKind.CLASS) {
70+
errorReporter().abortWithError(
71+
"@" + AutoOneOf.class.getName() + " only applies to classes", autoOneOfType);
72+
}
73+
checkModifiersIfNested(autoOneOfType);
74+
DeclaredType kindMirror = mirrorForKindType(autoOneOfType);
75+
76+
// We are going to classify the methods of the @AutoOneOf class into several categories.
77+
// This covers the methods in the class itself and the ones it inherits from supertypes.
78+
// First, the only concrete (non-abstract) methods we are interested in are overrides of
79+
// Object methods (equals, hashCode, toString), which signal that we should not generate
80+
// an implementation of those methods.
81+
// Then, each abstract method is one of the following:
82+
// (1) A property getter, like "abstract String foo()" or "abstract String getFoo()".
83+
// (2) A kind getter, which is a method that returns the enum in @AutoOneOf. For
84+
// example if we have @AutoOneOf(PetKind.class), this would be a method that returns
85+
// PetKind.
86+
// If there are abstract methods that don't fit any of the categories above, that is an error
87+
// which we signal explicitly to avoid confusion.
88+
89+
ImmutableSet<ExecutableElement> methods = getLocalAndInheritedMethods(
90+
autoOneOfType, processingEnv.getTypeUtils(), processingEnv.getElementUtils());
91+
ImmutableSet<ExecutableElement> abstractMethods = abstractMethodsIn(methods);
92+
ExecutableElement kindGetter =
93+
findKindGetterOrAbort(autoOneOfType, kindMirror, abstractMethods);
94+
Set<ExecutableElement> otherMethods = new LinkedHashSet<>(abstractMethods);
95+
otherMethods.remove(kindGetter);
96+
97+
ImmutableSet<ExecutableElement> propertyMethods = propertyMethodsIn(otherMethods);
98+
ImmutableBiMap<String, ExecutableElement> properties = propertyNameToMethodMap(propertyMethods);
99+
validateMethods(autoOneOfType, abstractMethods, propertyMethods, kindGetter);
100+
ImmutableMap<String, String> propertyToKind =
101+
propertyToKindMap(kindMirror, properties.keySet());
102+
103+
String subclass = generatedClassName(autoOneOfType, "AutoOneOf_");
104+
AutoOneOfTemplateVars vars = new AutoOneOfTemplateVars();
105+
vars.pkg = TypeSimplifier.packageNameOf(autoOneOfType);
106+
vars.origClass = TypeSimplifier.classNameOf(autoOneOfType);
107+
vars.simpleClassName = TypeSimplifier.simpleNameOf(vars.origClass);
108+
vars.generatedClass = TypeSimplifier.simpleNameOf(subclass);
109+
vars.types = processingEnv.getTypeUtils();
110+
vars.propertyToKind = propertyToKind;
111+
Set<ObjectMethod> methodsToGenerate = determineObjectMethodsToGenerate(methods);
112+
vars.toString = methodsToGenerate.contains(ObjectMethod.TO_STRING);
113+
vars.equals = methodsToGenerate.contains(ObjectMethod.EQUALS);
114+
vars.hashCode = methodsToGenerate.contains(ObjectMethod.HASH_CODE);
115+
defineVarsForType(autoOneOfType, vars, propertyMethods, kindGetter);
116+
117+
String text = vars.toText();
118+
text = TypeEncoder.decode(text, processingEnv, vars.pkg, autoOneOfType.asType());
119+
text = Reformatter.fixup(text);
120+
writeSourceFile(subclass, text, autoOneOfType);
121+
}
122+
123+
private DeclaredType mirrorForKindType(TypeElement autoOneOfType) {
124+
Optional<AnnotationMirror> oneOfAnnotation =
125+
MoreElements.getAnnotationMirror(autoOneOfType, AutoOneOf.class).toJavaUtil();
126+
if (!oneOfAnnotation.isPresent()) {
127+
// This shouldn't happen unless the compilation environment is buggy,
128+
// but it has happened in the past and can crash the compiler.
129+
errorReporter().abortWithError("annotation processor for @AutoOneOf was invoked with a type"
130+
+ " that does not have that annotation; this is probably a compiler bug", autoOneOfType);
131+
}
132+
AnnotationValue kindValue =
133+
AnnotationMirrors.getAnnotationValue(oneOfAnnotation.get(), "value");
134+
Object value = kindValue.getValue();
135+
if (value instanceof TypeMirror && ((TypeMirror) value).getKind().equals(TypeKind.DECLARED)) {
136+
return MoreTypes.asDeclared((TypeMirror) value);
137+
} else {
138+
// This is presumably because the referenced type doesn't exist.
139+
throw new MissingTypeException();
140+
}
141+
}
142+
143+
private ImmutableMap<String, String> propertyToKindMap(
144+
DeclaredType kindMirror, ImmutableSet<String> propertyNames) {
145+
// We require a one-to-one correspondence between the property names and the enum constants.
146+
// We must have transformName(propertyName) = transformName(constantName) for each one.
147+
// So we build two maps, transformName(propertyName) → propertyName and
148+
// transformName(constantName) → constant. The key sets of the two maps must match, and we
149+
// can then join them to make propertyName → constantName.
150+
TypeElement kindElement = MoreElements.asType(kindMirror.asElement());
151+
Map<String, String> transformedPropertyNames = propertyNames
152+
.stream()
153+
.collect(toMap(this::transformName, s -> s));
154+
Map<String, Element> transformedEnumConstants = kindElement.getEnclosedElements()
155+
.stream()
156+
.filter(e -> e.getKind().equals(ElementKind.ENUM_CONSTANT))
157+
.collect(toMap(e -> transformName(e.getSimpleName().toString()), e -> e));
158+
159+
if (transformedPropertyNames.keySet().equals(transformedEnumConstants.keySet())) {
160+
ImmutableMap.Builder<String, String> mapBuilder = ImmutableMap.builder();
161+
for (String transformed : transformedPropertyNames.keySet()) {
162+
mapBuilder.put(
163+
transformedPropertyNames.get(transformed),
164+
transformedEnumConstants.get(transformed).getSimpleName().toString());
165+
}
166+
return mapBuilder.build();
167+
}
168+
169+
// The names don't match. Emit errors for the differences.
170+
// Properties that have no enum constant
171+
transformedPropertyNames.forEach(
172+
(transformed, property) -> {
173+
if (!transformedEnumConstants.containsKey(transformed)) {
174+
errorReporter().reportError(
175+
"Enum has no constant with name corresponding to property '" + property + "'",
176+
kindElement);
177+
}
178+
});
179+
// Enum constants that have no property
180+
transformedEnumConstants.forEach(
181+
(transformed, constant) -> {
182+
if (!transformedPropertyNames.containsKey(transformed)) {
183+
errorReporter().reportError(
184+
"Name of enum constant '" + constant.getSimpleName()
185+
+ "' does not correspond to any property name",
186+
constant);
187+
}
188+
});
189+
throw new AbortProcessingException();
190+
}
191+
192+
private String transformName(String s) {
193+
return s.toLowerCase(Locale.ROOT).replace("_", "");
194+
}
195+
196+
private ExecutableElement findKindGetterOrAbort(
197+
TypeElement autoOneOfType,
198+
TypeMirror kindMirror,
199+
ImmutableSet<ExecutableElement> abstractMethods) {
200+
Set<ExecutableElement> kindGetters = abstractMethods
201+
.stream()
202+
.filter(e -> sameType(kindMirror, e.getReturnType()))
203+
.filter(e -> e.getParameters().isEmpty())
204+
.collect(toSet());
205+
switch (kindGetters.size()) {
206+
case 0:
207+
errorReporter().reportError(
208+
autoOneOfType + " must have a no-arg abstract method returning " + kindMirror,
209+
autoOneOfType);
210+
break;
211+
case 1:
212+
return Iterables.getOnlyElement(kindGetters);
213+
default:
214+
for (ExecutableElement getter : kindGetters) {
215+
errorReporter().reportError(
216+
"More than one abstract method returns " + kindMirror, getter);
217+
}
218+
}
219+
throw new AbortProcessingException();
220+
}
221+
222+
private void validateMethods(
223+
TypeElement type,
224+
ImmutableSet<ExecutableElement> abstractMethods,
225+
ImmutableSet<ExecutableElement> propertyMethods,
226+
ExecutableElement kindGetter) {
227+
for (ExecutableElement method : abstractMethods) {
228+
if (propertyMethods.contains(method)) {
229+
checkReturnType(type, method);
230+
} else if (!method.equals(kindGetter)
231+
&& objectMethodToOverride(method) == ObjectMethod.NONE) {
232+
// This could reasonably be an error, were it not for an Eclipse bug in
233+
// ElementUtils.override that sometimes fails to recognize that one method overrides
234+
// another, and therefore leaves us with both an abstract method and the subclass method
235+
// that overrides it. This shows up in AutoValueTest.LukesBase for example.
236+
// The compilation will fail anyway because the generated concrete classes won't
237+
// implement this alien method.
238+
String message =
239+
"Abstract methods in @" + AutoOneOf.class.getSimpleName()
240+
+ " classes must be non-void with no parameters";
241+
errorReporter().reportWarning(message, method);
242+
}
243+
}
244+
errorReporter().abortIfAnyError();
245+
}
246+
247+
private void defineVarsForType(
248+
TypeElement type,
249+
AutoOneOfTemplateVars vars,
250+
ImmutableSet<ExecutableElement> propertyMethods,
251+
ExecutableElement kindGetter) {
252+
vars.generated =
253+
generatedAnnotation(elementUtils())
254+
.map(annotation -> TypeEncoder.encode(annotation.asType()))
255+
.orElse("");
256+
Map<ExecutableElement, ImmutableList<AnnotationMirror>> annotatedPropertyMethods =
257+
emptyListForEachMethod(propertyMethods);
258+
vars.props = propertySet(type, annotatedPropertyMethods);
259+
vars.formalTypes = TypeEncoder.formalTypeParametersString(type);
260+
vars.actualTypes = TypeSimplifier.actualTypeParametersString(type);
261+
vars.wildcardTypes = wildcardTypeParametersString(type);
262+
vars.kindGetter = kindGetter.getSimpleName().toString();
263+
vars.kindType = TypeEncoder.encode(kindGetter.getReturnType());
264+
}
265+
266+
Map<ExecutableElement, ImmutableList<AnnotationMirror>> emptyListForEachMethod(
267+
ImmutableSet<ExecutableElement> propertyMethods) {
268+
return propertyMethods
269+
.stream()
270+
.collect(toMap(m -> m, m -> ImmutableList.of(), (a, b) -> a, () -> new LinkedHashMap<>()));
271+
}
272+
273+
@Override
274+
Optional<String> nullableAnnotationForMethod(
275+
ExecutableElement propertyMethod, ImmutableList<AnnotationMirror> methodAnnotations) {
276+
return Optional.empty();
277+
}
278+
279+
private static boolean sameType(TypeMirror t1, TypeMirror t2) {
280+
return MoreTypes.equivalence().equivalent(t1, t2);
281+
}
282+
}

0 commit comments

Comments
 (0)