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