aboutsummaryrefslogtreecommitdiff
path: root/value/src/main/java/com/google/auto/value/processor/PropertyBuilderClassifier.java
blob: dafb58200c20226bf867cd6ba4ef710182af2331 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
/*
 * Copyright 2016 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 com.google.auto.common.MoreElements;
import com.google.auto.common.MoreTypes;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

/**
 * Classifies methods inside builder types that return builders for properties. For example, if
 * {@code @AutoValue} class Foo has a method {@code ImmutableList<String> bar()} then Foo.Builder
 * can have a method {@code ImmutableList.Builder<String> barBuilder()}. This class checks that a
 * method like {@code barBuilder()} follows the rules, and if so constructs a {@link
 * PropertyBuilder} instance with information about {@code barBuilder}.
 *
 * @author Éamonn McManus
 */
class PropertyBuilderClassifier {
  private final ErrorReporter errorReporter;
  private final Types typeUtils;
  private final Elements elementUtils;
  private final BuilderMethodClassifier builderMethodClassifier;
  private final ImmutableBiMap<ExecutableElement, String> getterToPropertyName;
  private final ImmutableMap<ExecutableElement, TypeMirror> getterToPropertyType;
  private final EclipseHack eclipseHack;

  PropertyBuilderClassifier(
      ErrorReporter errorReporter,
      Types typeUtils,
      Elements elementUtils,
      BuilderMethodClassifier builderMethodClassifier,
      ImmutableBiMap<ExecutableElement, String> getterToPropertyName,
      ImmutableMap<ExecutableElement, TypeMirror> getterToPropertyType,
      EclipseHack eclipseHack) {
    this.errorReporter = errorReporter;
    this.typeUtils = typeUtils;
    this.elementUtils = elementUtils;
    this.builderMethodClassifier = builderMethodClassifier;
    this.getterToPropertyName = getterToPropertyName;
    this.getterToPropertyType = getterToPropertyType;
    this.eclipseHack = eclipseHack;
  }

  /**
   * Information about a property builder, referenced from the autovalue.vm template. A property
   * called bar (defined by a method bar() or getBar()) can have a property builder called
   * barBuilder(). For example, if {@code bar()} returns {@code ImmutableSet<String>} then {@code
   * barBuilder()} might return {@code ImmutableSet.Builder<String>}.
   */
  public static class PropertyBuilder {
    private final ExecutableElement propertyBuilderMethod;
    private final String name;
    private final String builderType;
    private final TypeMirror builderTypeMirror;
    private final String initializer;
    private final String beforeInitDefault;
    private final String initDefault;
    private final String builtToBuilder;
    private final String copyAll;

    PropertyBuilder(
        ExecutableElement propertyBuilderMethod,
        String builderType,
        TypeMirror builderTypeMirror,
        String initializer,
        String beforeInitDefault,
        String initDefault,
        String builtToBuilder,
        String copyAll) {
      this.propertyBuilderMethod = propertyBuilderMethod;
      this.name = propertyBuilderMethod.getSimpleName() + "$";
      this.builderType = builderType;
      this.builderTypeMirror = builderTypeMirror;
      this.initializer = initializer;
      this.beforeInitDefault = beforeInitDefault;
      this.initDefault = initDefault;
      this.builtToBuilder = builtToBuilder;
      this.copyAll = copyAll;
    }

    /** The property builder method, for example {@code barBuilder()}. */
    public ExecutableElement getPropertyBuilderMethod() {
      return propertyBuilderMethod;
    }

    public String getAccess() {
      return SimpleMethod.access(propertyBuilderMethod);
    }

    /** The name of the field to hold this builder. */
    public String getName() {
      return name;
    }

    /** The type of the builder, for example {@code ImmutableSet.Builder<String>}. */
    public String getBuilderType() {
      return builderType;
    }

    TypeMirror getBuilderTypeMirror() {
      return builderTypeMirror;
    }

    /** An initializer for the builder field, for example {@code ImmutableSet.builder()}. */
    public String getInitializer() {
      return initializer;
    }

    /**
     * An empty string, or a complete statement to be included before the expression returned by
     * {@link #getInitDefault()}.
     */
    public String getBeforeInitDefault() {
      return beforeInitDefault;
    }

    /**
     * An expression to return a default instance of the type that this builder builds. For example,
     * if this is an {@code ImmutableList<String>} then the method {@code ImmutableList.of()} will
     * correctly return an empty {@code ImmutableList<String>}, assuming the appropriate context for
     * type inference. The expression here can assume that the statement from {@link
     * #getBeforeInitDefault} has preceded it.
     */
    public String getInitDefault() {
      return initDefault;
    }

    /**
     * A method to convert the built type back into a builder. Unfortunately Guava collections don't
     * have this (you can't say {@code myImmutableMap.toBuilder()}), but for other types such as
     * {@code @AutoValue} types this is {@code toBuilder()}.
     */
    public String getBuiltToBuilder() {
      return builtToBuilder;
    }

    /**
     * The method to copy another collection into this builder. It is {@code addAll} for
     * one-dimensional collections like {@code ImmutableList} and {@code ImmutableSet}, and it is
     * {@code putAll} for two-dimensional collections like {@code ImmutableMap} and {@code
     * ImmutableTable}.
     */
    public String getCopyAll() {
      return copyAll;
    }
  }

  // Our @AutoValue class `Foo` has a property `Bar bar()` or `Bar getBar()` and we've encountered
  // a builder method like `BarBuilder barBuilder()`. Here `BarBuilder` can have any name (its name
  // doesn't have to be the name of `Bar` with `Builder` stuck on the end), but `barBuilder()` does
  // have to be the name of the property with `Builder` stuck on the end. The requirements for the
  // `BarBuilder` type are:
  // (1) It must have an instance method called `build()` that returns `Bar`. If the type of
  //     `bar()` is `Bar<String>` then the type of `build()` must be `Bar<String>`.
  // (2) `BarBuilder` must have a public no-arg constructor, or `Bar` must have a static method
  //     `naturalOrder(), `builder()`, or `newBuilder()` that returns `BarBuilder`. The
  //     `naturalOrder()` case is specifically for ImmutableSortedSet and ImmutableSortedMap.
  // (3) If `Foo` has a `toBuilder()` method, or if we have both `barBuilder()` and `setBar(Bar)`
  //     methods, then `Bar` must have an instance method `BarBuilder toBuilder()`, or `BarBuilder`
  //     must have an `addAll` or `putAll` method that accepts an argument of type `Bar`.
  //
  // This method outputs an error and returns Optional.empty() if the barBuilder() method has a
  // problem.
  Optional<PropertyBuilder> makePropertyBuilder(ExecutableElement method, String property) {
    TypeMirror barBuilderTypeMirror = builderMethodClassifier.builderMethodReturnType(method);
    if (barBuilderTypeMirror.getKind() != TypeKind.DECLARED) {
      errorReporter.reportError(
          method,
          "Method looks like a property builder, but its return type is not a class or interface");
      return Optional.empty();
    }
    DeclaredType barBuilderDeclaredType = MoreTypes.asDeclared(barBuilderTypeMirror);
    TypeElement barBuilderTypeElement = MoreTypes.asTypeElement(barBuilderTypeMirror);
    Map<String, ExecutableElement> barBuilderNoArgMethods = noArgMethodsOf(barBuilderTypeElement);

    ExecutableElement barGetter = getterToPropertyName.inverse().get(property);
    TypeMirror barTypeMirror = getterToPropertyType.get(barGetter);
    if (barTypeMirror.getKind() != TypeKind.DECLARED) {
      errorReporter.reportError(
          method,
          "Method looks like a property builder, but the type of property %s is not a class or"
              + " interface",
          property);
      return Optional.empty();
    }
    if (isNullable(barGetter)) {
      errorReporter.reportError(
          barGetter,
          "Property %s has a property builder so it cannot be @Nullable",
          property);
    }
    TypeElement barTypeElement = MoreTypes.asTypeElement(barTypeMirror);
    Map<String, ExecutableElement> barNoArgMethods = noArgMethodsOf(barTypeElement);

    // Condition (1), must have build() method returning Bar.
    ExecutableElement build = barBuilderNoArgMethods.get("build");
    if (build == null || build.getModifiers().contains(Modifier.STATIC)) {
      errorReporter.reportError(
          method,
          "Method looks like a property builder, but it returns %s which does not have a"
              + " non-static build() method",
          barBuilderTypeElement);
      return Optional.empty();
    }

    // We've determined that `BarBuilder` has a method `build()`. But it must return `Bar`.
    // And if the type of `bar()` is Bar<String> then `BarBuilder.build()` must return Bar<String>.
    TypeMirror buildType = eclipseHack.methodReturnType(build, barBuilderDeclaredType);
    if (!MoreTypes.equivalence().equivalent(barTypeMirror, buildType)) {
      errorReporter.reportError(
          method,
          "Property builder for %s has type %s whose build() method returns %s instead of %s",
          property,
          barBuilderTypeElement,
          buildType,
          barTypeMirror);
      return Optional.empty();
    }

    Optional<ExecutableElement> maybeBuilderMaker =
        builderMaker(barNoArgMethods, barBuilderTypeElement);
    if (!maybeBuilderMaker.isPresent()) {
      errorReporter.reportError(
          method,
          "Method looks like a property builder, but its type %s does not have a public"
              + " constructor and %s does not have a static builder() or newBuilder() method that"
              + " returns %s",
          barBuilderTypeElement,
          barTypeElement,
          barBuilderTypeElement);
      return Optional.empty();
    }
    ExecutableElement builderMaker = maybeBuilderMaker.get();

    String barBuilderType = TypeEncoder.encodeWithAnnotations(barBuilderTypeMirror);
    String rawBarType = TypeEncoder.encodeRaw(barTypeMirror);
    String initializer =
        (builderMaker.getKind() == ElementKind.CONSTRUCTOR)
            ? "new " + barBuilderType + "()"
            : rawBarType + "." + builderMaker.getSimpleName() + "()";
    String builtToBuilder = null;
    String copyAll = null;
    ExecutableElement toBuilder = barNoArgMethods.get("toBuilder");
    if (toBuilder != null
        && !toBuilder.getModifiers().contains(Modifier.STATIC)
        && typeUtils.isAssignable(
            typeUtils.erasure(toBuilder.getReturnType()),
            typeUtils.erasure(barBuilderTypeMirror))) {
      builtToBuilder = toBuilder.getSimpleName().toString();
    } else {
      Optional<ExecutableElement> maybeCopyAll =
          addAllPutAll(barBuilderTypeElement, barBuilderDeclaredType, barTypeMirror);
      if (maybeCopyAll.isPresent()) {
        copyAll = maybeCopyAll.get().getSimpleName().toString();
      }
    }
    ExecutableElement barOf = barNoArgMethods.get("of");
    boolean hasOf = (barOf != null && barOf.getModifiers().contains(Modifier.STATIC));
    // An expression (initDefault) to make a default one of these, plus optionally a statement
    // (beforeInitDefault) that prepares the expression. For a collection, beforeInitDefault is
    // empty and initDefault is (e.g.) `ImmutableList.of()`. For a nested value type,
    // beforeInitDefault is (e.g.)
    //   `NestedAutoValueType.Builder foo$builder = NestedAutoValueType.builder();`
    // and initDefault is `foo$builder.build();`. The reason for the separate statement is to
    // exploit type inference rather than having to write `NestedAutoValueType.<Bar>build();`.
    String beforeInitDefault;
    String initDefault;
    if (hasOf) {
      beforeInitDefault = "";
      initDefault = rawBarType + ".of()";
    } else {
      String localBuilder = property + "$builder";
      beforeInitDefault = barBuilderType + " " + localBuilder + " = " + initializer + ";";
      initDefault = localBuilder + ".build()";
    }

    PropertyBuilder propertyBuilder =
        new PropertyBuilder(
            method,
            barBuilderType,
            barBuilderTypeMirror,
            initializer,
            beforeInitDefault,
            initDefault,
            builtToBuilder,
            copyAll);
    return Optional.of(propertyBuilder);
  }

  private static final ImmutableSet<String> BUILDER_METHOD_NAMES =
      ImmutableSet.of("naturalOrder", "builder", "newBuilder");

  // (2) `BarBuilder must have a public no-arg constructor, or `Bar` must have a visible static
  //      method `naturalOrder(), `builder()`, or `newBuilder()` that returns `BarBuilder`.
  private Optional<ExecutableElement> builderMaker(
      Map<String, ExecutableElement> barNoArgMethods, TypeElement barBuilderTypeElement) {
    for (String builderMethodName : BUILDER_METHOD_NAMES) {
      ExecutableElement method = barNoArgMethods.get(builderMethodName);
      if (method != null
          && method.getModifiers().contains(Modifier.STATIC)
          && typeUtils.isSameType(
              typeUtils.erasure(method.getReturnType()),
              typeUtils.erasure(barBuilderTypeElement.asType()))) {
        // TODO(emcmanus): check visibility. We don't want to require public for @AutoValue
        // builders. By not checking visibility we risk accepting something as a builder maker
        // and then failing when the generated code tries to call Bar.builder(). But the risk
        // seems small.
        return Optional.of(method);
      }
    }
    return ElementFilter.constructorsIn(barBuilderTypeElement.getEnclosedElements())
        .stream()
        .filter(c -> c.getParameters().isEmpty())
        .filter(c -> c.getModifiers().contains(Modifier.PUBLIC))
        .findFirst();
  }

  private Map<String, ExecutableElement> noArgMethodsOf(TypeElement type) {
    // Can't easily use ImmutableMap here because getAllMembers could return more than one method
    // with the same name.
    Map<String, ExecutableElement> methods = new LinkedHashMap<>();
    for (ExecutableElement method : ElementFilter.methodsIn(elementUtils.getAllMembers(type))) {
      if (method.getParameters().isEmpty() && !isStaticInterfaceMethodNotIn(method, type)) {
        methods.put(method.getSimpleName().toString(), method);
      }
    }
    return methods;
  }

  // Work around an Eclipse compiler bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=547185
  // The result of Elements.getAllMembers includes static methods declared in superinterfaces.
  // That's wrong because those aren't inherited. So this method checks whether the given method is
  // a static interface method not in the given type.
  private static boolean isStaticInterfaceMethodNotIn(ExecutableElement method, TypeElement type) {
    return method.getModifiers().contains(Modifier.STATIC)
        && !method.getEnclosingElement().equals(type)
        && method.getEnclosingElement().getKind().equals(ElementKind.INTERFACE);
  }

  private static final ImmutableSet<String> ADD_ALL_PUT_ALL = ImmutableSet.of("addAll", "putAll");

  // We have `Bar bar()` and `Foo.Builder toBuilder()` in the @AutoValue type Foo, and we have
  // `BarBuilder barBuilder()` in Foo.Builder. That means that we need to be able to make a
  // `BarBuilder` from a `Bar` as part of the implementation of `Foo.toBuilder()`. We can do that
  // if `Bar` has a method `BarBuilder toBuilder()`, but what if it doesn't? For example, Guava's
  // `ImmutableList` doesn't have a method `ImmutableList.Builder toBuilder()`. So we also allow it
  // to work if `BarBuilder` has a method `addAll(T)` or `putAll(T)`, where `Bar` is assignable to
  // `T`. `ImmutableList.Builder<E>` does have a method `addAll(Iterable<? extends E>)` and
  // `ImmutableList<E>` is assignable to `Iterable<? extends E>`, so that works.
  private Optional<ExecutableElement> addAllPutAll(
      TypeElement barBuilderTypeElement,
      DeclaredType barBuilderDeclaredType,
      TypeMirror barTypeMirror) {
    return MoreElements.getLocalAndInheritedMethods(barBuilderTypeElement, typeUtils, elementUtils)
        .stream()
        .filter(
            method ->
                ADD_ALL_PUT_ALL.contains(method.getSimpleName().toString())
                    && method.getParameters().size() == 1)
        .filter(
            method -> {
              ExecutableType methodMirror =
                  MoreTypes.asExecutable(typeUtils.asMemberOf(barBuilderDeclaredType, method));
              return typeUtils.isAssignable(barTypeMirror, methodMirror.getParameterTypes().get(0));
            })
        .findFirst();
  }

  private static boolean isNullable(ExecutableElement getter) {
    List<List<? extends AnnotationMirror>> annotationLists =
        ImmutableList.of(
            getter.getAnnotationMirrors(), getter.getReturnType().getAnnotationMirrors());
    for (List<? extends AnnotationMirror> annotations : annotationLists) {
      for (AnnotationMirror annotation : annotations) {
        if (annotation.getAnnotationType().asElement().getSimpleName().contentEquals("Nullable")) {
          return true;
        }
      }
    }
    return false;
  }
}