• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2016 Google LLC
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 com.google.auto.common.MoreElements;
19 import com.google.auto.common.MoreTypes;
20 import com.google.common.collect.ImmutableBiMap;
21 import com.google.common.collect.ImmutableList;
22 import com.google.common.collect.ImmutableMap;
23 import com.google.common.collect.ImmutableSet;
24 import java.util.LinkedHashMap;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Optional;
28 import javax.lang.model.element.AnnotationMirror;
29 import javax.lang.model.element.ElementKind;
30 import javax.lang.model.element.ExecutableElement;
31 import javax.lang.model.element.Modifier;
32 import javax.lang.model.element.TypeElement;
33 import javax.lang.model.type.DeclaredType;
34 import javax.lang.model.type.ExecutableType;
35 import javax.lang.model.type.TypeKind;
36 import javax.lang.model.type.TypeMirror;
37 import javax.lang.model.util.ElementFilter;
38 import javax.lang.model.util.Elements;
39 import javax.lang.model.util.Types;
40 
41 /**
42  * Classifies methods inside builder types that return builders for properties. For example, if
43  * {@code @AutoValue} class Foo has a method {@code ImmutableList<String> bar()} then Foo.Builder
44  * can have a method {@code ImmutableList.Builder<String> barBuilder()}. This class checks that a
45  * method like {@code barBuilder()} follows the rules, and if so constructs a {@link
46  * PropertyBuilder} instance with information about {@code barBuilder}.
47  *
48  * @author Éamonn McManus
49  */
50 class PropertyBuilderClassifier {
51   private final ErrorReporter errorReporter;
52   private final Types typeUtils;
53   private final Elements elementUtils;
54   private final BuilderMethodClassifier builderMethodClassifier;
55   private final ImmutableBiMap<ExecutableElement, String> getterToPropertyName;
56   private final ImmutableMap<ExecutableElement, TypeMirror> getterToPropertyType;
57   private final EclipseHack eclipseHack;
58 
PropertyBuilderClassifier( ErrorReporter errorReporter, Types typeUtils, Elements elementUtils, BuilderMethodClassifier builderMethodClassifier, ImmutableBiMap<ExecutableElement, String> getterToPropertyName, ImmutableMap<ExecutableElement, TypeMirror> getterToPropertyType, EclipseHack eclipseHack)59   PropertyBuilderClassifier(
60       ErrorReporter errorReporter,
61       Types typeUtils,
62       Elements elementUtils,
63       BuilderMethodClassifier builderMethodClassifier,
64       ImmutableBiMap<ExecutableElement, String> getterToPropertyName,
65       ImmutableMap<ExecutableElement, TypeMirror> getterToPropertyType,
66       EclipseHack eclipseHack) {
67     this.errorReporter = errorReporter;
68     this.typeUtils = typeUtils;
69     this.elementUtils = elementUtils;
70     this.builderMethodClassifier = builderMethodClassifier;
71     this.getterToPropertyName = getterToPropertyName;
72     this.getterToPropertyType = getterToPropertyType;
73     this.eclipseHack = eclipseHack;
74   }
75 
76   /**
77    * Information about a property builder, referenced from the autovalue.vm template. A property
78    * called bar (defined by a method bar() or getBar()) can have a property builder called
79    * barBuilder(). For example, if {@code bar()} returns {@code ImmutableSet<String>} then {@code
80    * barBuilder()} might return {@code ImmutableSet.Builder<String>}.
81    */
82   public static class PropertyBuilder {
83     private final ExecutableElement propertyBuilderMethod;
84     private final String name;
85     private final String builderType;
86     private final TypeMirror builderTypeMirror;
87     private final String initializer;
88     private final String beforeInitDefault;
89     private final String initDefault;
90     private final String builtToBuilder;
91     private final String copyAll;
92 
PropertyBuilder( ExecutableElement propertyBuilderMethod, String builderType, TypeMirror builderTypeMirror, String initializer, String beforeInitDefault, String initDefault, String builtToBuilder, String copyAll)93     PropertyBuilder(
94         ExecutableElement propertyBuilderMethod,
95         String builderType,
96         TypeMirror builderTypeMirror,
97         String initializer,
98         String beforeInitDefault,
99         String initDefault,
100         String builtToBuilder,
101         String copyAll) {
102       this.propertyBuilderMethod = propertyBuilderMethod;
103       this.name = propertyBuilderMethod.getSimpleName() + "$";
104       this.builderType = builderType;
105       this.builderTypeMirror = builderTypeMirror;
106       this.initializer = initializer;
107       this.beforeInitDefault = beforeInitDefault;
108       this.initDefault = initDefault;
109       this.builtToBuilder = builtToBuilder;
110       this.copyAll = copyAll;
111     }
112 
113     /** The property builder method, for example {@code barBuilder()}. */
getPropertyBuilderMethod()114     public ExecutableElement getPropertyBuilderMethod() {
115       return propertyBuilderMethod;
116     }
117 
getAccess()118     public String getAccess() {
119       return SimpleMethod.access(propertyBuilderMethod);
120     }
121 
122     /** The name of the field to hold this builder. */
getName()123     public String getName() {
124       return name;
125     }
126 
127     /** The type of the builder, for example {@code ImmutableSet.Builder<String>}. */
getBuilderType()128     public String getBuilderType() {
129       return builderType;
130     }
131 
getBuilderTypeMirror()132     TypeMirror getBuilderTypeMirror() {
133       return builderTypeMirror;
134     }
135 
136     /** An initializer for the builder field, for example {@code ImmutableSet.builder()}. */
getInitializer()137     public String getInitializer() {
138       return initializer;
139     }
140 
141     /**
142      * An empty string, or a complete statement to be included before the expression returned by
143      * {@link #getInitDefault()}.
144      */
getBeforeInitDefault()145     public String getBeforeInitDefault() {
146       return beforeInitDefault;
147     }
148 
149     /**
150      * An expression to return a default instance of the type that this builder builds. For example,
151      * if this is an {@code ImmutableList<String>} then the method {@code ImmutableList.of()} will
152      * correctly return an empty {@code ImmutableList<String>}, assuming the appropriate context for
153      * type inference. The expression here can assume that the statement from {@link
154      * #getBeforeInitDefault} has preceded it.
155      */
getInitDefault()156     public String getInitDefault() {
157       return initDefault;
158     }
159 
160     /**
161      * A method to convert the built type back into a builder. Unfortunately Guava collections don't
162      * have this (you can't say {@code myImmutableMap.toBuilder()}), but for other types such as
163      * {@code @AutoValue} types this is {@code toBuilder()}.
164      */
getBuiltToBuilder()165     public String getBuiltToBuilder() {
166       return builtToBuilder;
167     }
168 
169     /**
170      * The method to copy another collection into this builder. It is {@code addAll} for
171      * one-dimensional collections like {@code ImmutableList} and {@code ImmutableSet}, and it is
172      * {@code putAll} for two-dimensional collections like {@code ImmutableMap} and {@code
173      * ImmutableTable}.
174      */
getCopyAll()175     public String getCopyAll() {
176       return copyAll;
177     }
178   }
179 
180   // Our @AutoValue class `Foo` has a property `Bar bar()` or `Bar getBar()` and we've encountered
181   // a builder method like `BarBuilder barBuilder()`. Here `BarBuilder` can have any name (its name
182   // doesn't have to be the name of `Bar` with `Builder` stuck on the end), but `barBuilder()` does
183   // have to be the name of the property with `Builder` stuck on the end. The requirements for the
184   // `BarBuilder` type are:
185   // (1) It must have an instance method called `build()` that returns `Bar`. If the type of
186   //     `bar()` is `Bar<String>` then the type of `build()` must be `Bar<String>`.
187   // (2) `BarBuilder` must have a public no-arg constructor, or `Bar` must have a static method
188   //     `naturalOrder(), `builder()`, or `newBuilder()` that returns `BarBuilder`. The
189   //     `naturalOrder()` case is specifically for ImmutableSortedSet and ImmutableSortedMap.
190   // (3) If `Foo` has a `toBuilder()` method, or if we have both `barBuilder()` and `setBar(Bar)`
191   //     methods, then `Bar` must have an instance method `BarBuilder toBuilder()`, or `BarBuilder`
192   //     must have an `addAll` or `putAll` method that accepts an argument of type `Bar`.
193   //
194   // This method outputs an error and returns Optional.empty() if the barBuilder() method has a
195   // problem.
makePropertyBuilder(ExecutableElement method, String property)196   Optional<PropertyBuilder> makePropertyBuilder(ExecutableElement method, String property) {
197     TypeMirror barBuilderTypeMirror = builderMethodClassifier.builderMethodReturnType(method);
198     if (barBuilderTypeMirror.getKind() != TypeKind.DECLARED) {
199       errorReporter.reportError(
200           method,
201           "[AutoValueOddBuilderMethod] Method looks like a property builder, but its return type"
202               + " is not a class or interface");
203       return Optional.empty();
204     }
205     DeclaredType barBuilderDeclaredType = MoreTypes.asDeclared(barBuilderTypeMirror);
206     TypeElement barBuilderTypeElement = MoreTypes.asTypeElement(barBuilderTypeMirror);
207     Map<String, ExecutableElement> barBuilderNoArgMethods = noArgMethodsOf(barBuilderTypeElement);
208 
209     ExecutableElement barGetter = getterToPropertyName.inverse().get(property);
210     TypeMirror barTypeMirror = getterToPropertyType.get(barGetter);
211     if (barTypeMirror.getKind() != TypeKind.DECLARED) {
212       errorReporter.reportError(
213           method,
214           "[AutoValueBadBuilderMethod] Method looks like a property builder, but the type of"
215               + " property %s is not a class or interface",
216           property);
217       return Optional.empty();
218     }
219     if (isNullable(barGetter)) {
220       errorReporter.reportError(
221           barGetter,
222           "[AutoValueNullBuilder] Property %s has a property builder so it cannot be @Nullable",
223           property);
224     }
225     TypeElement barTypeElement = MoreTypes.asTypeElement(barTypeMirror);
226     Map<String, ExecutableElement> barNoArgMethods = noArgMethodsOf(barTypeElement);
227 
228     // Condition (1), must have build() method returning Bar.
229     ExecutableElement build = barBuilderNoArgMethods.get("build");
230     if (build == null || build.getModifiers().contains(Modifier.STATIC)) {
231       errorReporter.reportError(
232           method,
233           "[AutoValueBuilderNotBuildable] Method looks like a property builder, but it returns %s"
234               + " which does not have a non-static build() method",
235           barBuilderTypeElement);
236       return Optional.empty();
237     }
238 
239     // We've determined that `BarBuilder` has a method `build()`. But it must return `Bar`.
240     // And if the type of `bar()` is Bar<String> then `BarBuilder.build()` must return Bar<String>.
241     TypeMirror buildType = eclipseHack.methodReturnType(build, barBuilderDeclaredType);
242     if (!MoreTypes.equivalence().equivalent(barTypeMirror, buildType)) {
243       errorReporter.reportError(
244           method,
245           "[AutoValueBuilderWrongType] Property builder for %s has type %s whose build() method"
246               + " returns %s instead of %s",
247           property,
248           barBuilderTypeElement,
249           buildType,
250           barTypeMirror);
251       return Optional.empty();
252     }
253 
254     Optional<ExecutableElement> maybeBuilderMaker =
255         builderMaker(barNoArgMethods, barBuilderTypeElement);
256     if (!maybeBuilderMaker.isPresent()) {
257       errorReporter.reportError(
258           method,
259           "[AutoValueCantMakePropertyBuilder] Method looks like a property builder, but its type"
260               + " %s does not have a public constructor and %s does not have a static builder() or"
261               + " newBuilder() method that returns %s",
262           barBuilderTypeElement,
263           barTypeElement,
264           barBuilderTypeElement);
265       return Optional.empty();
266     }
267     ExecutableElement builderMaker = maybeBuilderMaker.get();
268 
269     String barBuilderType = TypeEncoder.encodeWithAnnotations(barBuilderTypeMirror);
270     String rawBarType = TypeEncoder.encodeRaw(barTypeMirror);
271     String initializer =
272         (builderMaker.getKind() == ElementKind.CONSTRUCTOR)
273             ? "new " + barBuilderType + "()"
274             : rawBarType + "." + builderMaker.getSimpleName() + "()";
275     String builtToBuilder = null;
276     String copyAll = null;
277     ExecutableElement toBuilder = barNoArgMethods.get("toBuilder");
278     if (toBuilder != null
279         && !toBuilder.getModifiers().contains(Modifier.STATIC)
280         && typeUtils.isAssignable(
281             typeUtils.erasure(toBuilder.getReturnType()),
282             typeUtils.erasure(barBuilderTypeMirror))) {
283       builtToBuilder = toBuilder.getSimpleName().toString();
284     } else {
285       Optional<ExecutableElement> maybeCopyAll =
286           addAllPutAll(barBuilderTypeElement, barBuilderDeclaredType, barTypeMirror);
287       if (maybeCopyAll.isPresent()) {
288         copyAll = maybeCopyAll.get().getSimpleName().toString();
289       }
290     }
291     ExecutableElement barOf = barNoArgMethods.get("of");
292     boolean hasOf = (barOf != null && barOf.getModifiers().contains(Modifier.STATIC));
293     // An expression (initDefault) to make a default one of these, plus optionally a statement
294     // (beforeInitDefault) that prepares the expression. For a collection, beforeInitDefault is
295     // empty and initDefault is (e.g.) `ImmutableList.of()`. For a nested value type,
296     // beforeInitDefault is (e.g.)
297     //   `NestedAutoValueType.Builder foo$builder = NestedAutoValueType.builder();`
298     // and initDefault is `foo$builder.build();`. The reason for the separate statement is to
299     // exploit type inference rather than having to write `NestedAutoValueType.<Bar>build();`.
300     String beforeInitDefault;
301     String initDefault;
302     if (hasOf) {
303       beforeInitDefault = "";
304       initDefault = rawBarType + ".of()";
305     } else {
306       String localBuilder = property + "$builder";
307       beforeInitDefault = barBuilderType + " " + localBuilder + " = " + initializer + ";";
308       initDefault = localBuilder + ".build()";
309     }
310 
311     PropertyBuilder propertyBuilder =
312         new PropertyBuilder(
313             method,
314             barBuilderType,
315             barBuilderTypeMirror,
316             initializer,
317             beforeInitDefault,
318             initDefault,
319             builtToBuilder,
320             copyAll);
321     return Optional.of(propertyBuilder);
322   }
323 
324   private static final ImmutableSet<String> BUILDER_METHOD_NAMES =
325       ImmutableSet.of("naturalOrder", "builder", "newBuilder");
326 
327   // (2) `BarBuilder must have a public no-arg constructor, or `Bar` must have a visible static
328   //      method `naturalOrder(), `builder()`, or `newBuilder()` that returns `BarBuilder`.
builderMaker( Map<String, ExecutableElement> barNoArgMethods, TypeElement barBuilderTypeElement)329   private Optional<ExecutableElement> builderMaker(
330       Map<String, ExecutableElement> barNoArgMethods, TypeElement barBuilderTypeElement) {
331     for (String builderMethodName : BUILDER_METHOD_NAMES) {
332       ExecutableElement method = barNoArgMethods.get(builderMethodName);
333       if (method != null
334           && method.getModifiers().contains(Modifier.STATIC)
335           && typeUtils.isSameType(
336               typeUtils.erasure(method.getReturnType()),
337               typeUtils.erasure(barBuilderTypeElement.asType()))) {
338         // TODO(emcmanus): check visibility. We don't want to require public for @AutoValue
339         // builders. By not checking visibility we risk accepting something as a builder maker
340         // and then failing when the generated code tries to call Bar.builder(). But the risk
341         // seems small.
342         return Optional.of(method);
343       }
344     }
345     return ElementFilter.constructorsIn(barBuilderTypeElement.getEnclosedElements())
346         .stream()
347         .filter(c -> c.getParameters().isEmpty())
348         .filter(c -> c.getModifiers().contains(Modifier.PUBLIC))
349         .findFirst();
350   }
351 
noArgMethodsOf(TypeElement type)352   private Map<String, ExecutableElement> noArgMethodsOf(TypeElement type) {
353     // Can't easily use ImmutableMap here because getAllMembers could return more than one method
354     // with the same name.
355     Map<String, ExecutableElement> methods = new LinkedHashMap<>();
356     for (ExecutableElement method : ElementFilter.methodsIn(elementUtils.getAllMembers(type))) {
357       if (method.getParameters().isEmpty() && !isStaticInterfaceMethodNotIn(method, type)) {
358         methods.put(method.getSimpleName().toString(), method);
359       }
360     }
361     return methods;
362   }
363 
364   // Work around an Eclipse compiler bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=547185
365   // The result of Elements.getAllMembers includes static methods declared in superinterfaces.
366   // That's wrong because those aren't inherited. So this method checks whether the given method is
367   // a static interface method not in the given type.
isStaticInterfaceMethodNotIn(ExecutableElement method, TypeElement type)368   private static boolean isStaticInterfaceMethodNotIn(ExecutableElement method, TypeElement type) {
369     return method.getModifiers().contains(Modifier.STATIC)
370         && !method.getEnclosingElement().equals(type)
371         && method.getEnclosingElement().getKind().equals(ElementKind.INTERFACE);
372   }
373 
374   private static final ImmutableSet<String> ADD_ALL_PUT_ALL = ImmutableSet.of("addAll", "putAll");
375 
376   // We have `Bar bar()` and `Foo.Builder toBuilder()` in the @AutoValue type Foo, and we have
377   // `BarBuilder barBuilder()` in Foo.Builder. That means that we need to be able to make a
378   // `BarBuilder` from a `Bar` as part of the implementation of `Foo.toBuilder()`. We can do that
379   // if `Bar` has a method `BarBuilder toBuilder()`, but what if it doesn't? For example, Guava's
380   // `ImmutableList` doesn't have a method `ImmutableList.Builder toBuilder()`. So we also allow it
381   // to work if `BarBuilder` has a method `addAll(T)` or `putAll(T)`, where `Bar` is assignable to
382   // `T`. `ImmutableList.Builder<E>` does have a method `addAll(Iterable<? extends E>)` and
383   // `ImmutableList<E>` is assignable to `Iterable<? extends E>`, so that works.
addAllPutAll( TypeElement barBuilderTypeElement, DeclaredType barBuilderDeclaredType, TypeMirror barTypeMirror)384   private Optional<ExecutableElement> addAllPutAll(
385       TypeElement barBuilderTypeElement,
386       DeclaredType barBuilderDeclaredType,
387       TypeMirror barTypeMirror) {
388     return MoreElements.getLocalAndInheritedMethods(barBuilderTypeElement, typeUtils, elementUtils)
389         .stream()
390         .filter(
391             method ->
392                 ADD_ALL_PUT_ALL.contains(method.getSimpleName().toString())
393                     && method.getParameters().size() == 1)
394         .filter(
395             method -> {
396               ExecutableType methodMirror =
397                   MoreTypes.asExecutable(typeUtils.asMemberOf(barBuilderDeclaredType, method));
398               return typeUtils.isAssignable(barTypeMirror, methodMirror.getParameterTypes().get(0));
399             })
400         .findFirst();
401   }
402 
isNullable(ExecutableElement getter)403   private static boolean isNullable(ExecutableElement getter) {
404     List<List<? extends AnnotationMirror>> annotationLists =
405         ImmutableList.of(
406             getter.getAnnotationMirrors(), getter.getReturnType().getAnnotationMirrors());
407     for (List<? extends AnnotationMirror> annotations : annotationLists) {
408       for (AnnotationMirror annotation : annotations) {
409         if (annotation.getAnnotationType().asElement().getSimpleName().contentEquals("Nullable")) {
410           return true;
411         }
412       }
413     }
414     return false;
415   }
416 }
417