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