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