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