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.extension.memoized.processor; 17 18 import static com.google.auto.common.GeneratedAnnotationSpecs.generatedAnnotationSpec; 19 import static com.google.auto.common.MoreStreams.toImmutableList; 20 import static com.google.auto.common.MoreStreams.toImmutableMap; 21 import static com.google.auto.common.MoreStreams.toImmutableSet; 22 import static com.google.auto.value.extension.memoized.processor.ClassNames.MEMOIZED_NAME; 23 import static com.google.auto.value.extension.memoized.processor.MemoizedValidator.getAnnotationMirror; 24 import static com.google.common.base.Predicates.equalTo; 25 import static com.google.common.base.Predicates.not; 26 import static com.google.common.collect.Iterables.filter; 27 import static com.google.common.collect.Iterables.getOnlyElement; 28 import static com.squareup.javapoet.MethodSpec.constructorBuilder; 29 import static com.squareup.javapoet.MethodSpec.methodBuilder; 30 import static com.squareup.javapoet.TypeSpec.classBuilder; 31 import static java.util.stream.Collectors.joining; 32 import static java.util.stream.Collectors.toList; 33 import static javax.lang.model.element.Modifier.ABSTRACT; 34 import static javax.lang.model.element.Modifier.FINAL; 35 import static javax.lang.model.element.Modifier.PRIVATE; 36 import static javax.lang.model.element.Modifier.PUBLIC; 37 import static javax.lang.model.element.Modifier.STATIC; 38 import static javax.lang.model.element.Modifier.TRANSIENT; 39 import static javax.lang.model.element.Modifier.VOLATILE; 40 import static javax.lang.model.type.TypeKind.VOID; 41 import static javax.lang.model.util.ElementFilter.methodsIn; 42 import static javax.tools.Diagnostic.Kind.ERROR; 43 44 import com.google.auto.common.MoreElements; 45 import com.google.auto.service.AutoService; 46 import com.google.auto.value.extension.AutoValueExtension; 47 import com.google.common.collect.ImmutableList; 48 import com.google.common.collect.ImmutableMap; 49 import com.google.common.collect.ImmutableSet; 50 import com.google.errorprone.annotations.FormatMethod; 51 import com.squareup.javapoet.AnnotationSpec; 52 import com.squareup.javapoet.ClassName; 53 import com.squareup.javapoet.CodeBlock; 54 import com.squareup.javapoet.FieldSpec; 55 import com.squareup.javapoet.JavaFile; 56 import com.squareup.javapoet.MethodSpec; 57 import com.squareup.javapoet.ParameterizedTypeName; 58 import com.squareup.javapoet.TypeName; 59 import com.squareup.javapoet.TypeSpec; 60 import com.squareup.javapoet.TypeVariableName; 61 import java.util.List; 62 import java.util.Optional; 63 import java.util.Set; 64 import javax.annotation.processing.Messager; 65 import javax.annotation.processing.ProcessingEnvironment; 66 import javax.lang.model.SourceVersion; 67 import javax.lang.model.element.AnnotationMirror; 68 import javax.lang.model.element.ExecutableElement; 69 import javax.lang.model.element.Modifier; 70 import javax.lang.model.element.TypeElement; 71 import javax.lang.model.type.TypeMirror; 72 import javax.lang.model.util.Elements; 73 import javax.lang.model.util.Types; 74 import javax.tools.Diagnostic.Kind; 75 76 /** 77 * An extension that implements the {@link com.google.auto.value.extension.memoized.Memoized} 78 * contract. 79 */ 80 @AutoService(AutoValueExtension.class) 81 public final class MemoizeExtension extends AutoValueExtension { 82 private static final ImmutableSet<String> DO_NOT_PULL_DOWN_ANNOTATIONS = 83 ImmutableSet.of(Override.class.getCanonicalName(), MEMOIZED_NAME); 84 85 // Maven is configured to shade (rewrite) com.google packages to prevent dependency conflicts. 86 // Split up the package here with a call to concat to prevent Maven from finding and rewriting it, 87 // so that this will be able to find the LazyInit annotation if it's on the classpath. 88 private static final ClassName LAZY_INIT = 89 ClassName.get("com".concat(".google.errorprone.annotations.concurrent"), "LazyInit"); 90 91 private static final AnnotationSpec SUPPRESS_WARNINGS = 92 AnnotationSpec.builder(SuppressWarnings.class).addMember("value", "$S", "Immutable").build(); 93 94 @Override incrementalType(ProcessingEnvironment processingEnvironment)95 public IncrementalExtensionType incrementalType(ProcessingEnvironment processingEnvironment) { 96 return IncrementalExtensionType.ISOLATING; 97 } 98 99 @Override applicable(Context context)100 public boolean applicable(Context context) { 101 return !memoizedMethods(context).isEmpty(); 102 } 103 104 @Override generateClass( Context context, String className, String classToExtend, boolean isFinal)105 public String generateClass( 106 Context context, String className, String classToExtend, boolean isFinal) { 107 return new Generator(context, className, classToExtend, isFinal).generate(); 108 } 109 memoizedMethods(Context context)110 private static ImmutableSet<ExecutableElement> memoizedMethods(Context context) { 111 return methodsIn(context.autoValueClass().getEnclosedElements()).stream() 112 .filter(m -> getAnnotationMirror(m, MEMOIZED_NAME).isPresent()) 113 .collect(toImmutableSet()); 114 } 115 116 static final class Generator { 117 private final Context context; 118 private final String className; 119 private final String classToExtend; 120 private final boolean isFinal; 121 private final Elements elements; 122 private final Types types; 123 private final SourceVersion sourceVersion; 124 private final Messager messager; 125 private final Optional<AnnotationSpec> lazyInitAnnotation; 126 private boolean hasErrors; 127 Generator(Context context, String className, String classToExtend, boolean isFinal)128 Generator(Context context, String className, String classToExtend, boolean isFinal) { 129 this.context = context; 130 this.className = className; 131 this.classToExtend = classToExtend; 132 this.isFinal = isFinal; 133 this.elements = context.processingEnvironment().getElementUtils(); 134 this.types = context.processingEnvironment().getTypeUtils(); 135 this.sourceVersion = context.processingEnvironment().getSourceVersion(); 136 this.messager = context.processingEnvironment().getMessager(); 137 this.lazyInitAnnotation = getLazyInitAnnotation(elements); 138 } 139 generate()140 String generate() { 141 142 TypeSpec.Builder generated = 143 classBuilder(className) 144 .superclass(superType()) 145 .addAnnotations( 146 context.classAnnotationsToCopy(context.autoValueClass()).stream() 147 .map(AnnotationSpec::get) 148 .collect(toImmutableList())) 149 .addTypeVariables(annotatedTypeVariableNames()) 150 .addModifiers(isFinal ? FINAL : ABSTRACT) 151 .addMethod(constructor()); 152 generatedAnnotationSpec(elements, sourceVersion, MemoizeExtension.class) 153 .ifPresent(generated::addAnnotation); 154 155 for (ExecutableElement method : memoizedMethods(context)) { 156 MethodOverrider methodOverrider = new MethodOverrider(method); 157 generated.addFields(methodOverrider.fields()); 158 generated.addMethod(methodOverrider.method()); 159 } 160 if (isHashCodeMemoized() && !isEqualsFinal()) { 161 generated.addMethod(equalsWithHashCodeCheck()); 162 } 163 if (hasErrors) { 164 return null; 165 } 166 return JavaFile.builder(context.packageName(), generated.build()).build().toString(); 167 } 168 169 superType()170 private TypeName superType() { 171 ClassName superType = ClassName.get(context.packageName(), classToExtend); 172 ImmutableList<TypeVariableName> typeVariableNames = typeVariableNames(); 173 174 return typeVariableNames.isEmpty() 175 ? superType 176 : ParameterizedTypeName.get(superType, typeVariableNames.toArray(new TypeName[] {})); 177 } 178 typeVariableNames()179 private ImmutableList<TypeVariableName> typeVariableNames() { 180 return context.autoValueClass().getTypeParameters().stream() 181 .map(TypeVariableName::get) 182 .collect(toImmutableList()); 183 } 184 annotatedTypeVariableNames()185 private ImmutableList<TypeVariableName> annotatedTypeVariableNames() { 186 return context.autoValueClass().getTypeParameters().stream() 187 .map( 188 p -> 189 TypeVariableName.get(p) 190 .annotated( 191 p.getAnnotationMirrors().stream() 192 .map(AnnotationSpec::get) 193 .collect(toImmutableList()))) 194 .collect(toImmutableList()); 195 } 196 constructor()197 private MethodSpec constructor() { 198 MethodSpec.Builder constructor = constructorBuilder(); 199 // TODO(b/35944623): Replace this with a standard way of avoiding keywords. 200 Set<String> propertyNames = context.properties().keySet(); 201 ImmutableMap<String, String> parameterNames = 202 propertyNames.stream() 203 .collect( 204 toImmutableMap(name -> name, name -> generateIdentifier(name, propertyNames))); 205 context 206 .propertyTypes() 207 .forEach( 208 (name, type) -> 209 constructor.addParameter(annotatedType(type), parameterNames.get(name))); 210 String superParams = 211 context.properties().keySet().stream().map(parameterNames::get).collect(joining(", ")); 212 constructor.addStatement("super($L)", superParams); 213 return constructor.build(); 214 } 215 generateIdentifier(String name, Set<String> existingNames)216 private static String generateIdentifier(String name, Set<String> existingNames) { 217 if (!SourceVersion.isKeyword(name)) { 218 return name; 219 } 220 for (int i = 0;; i++) { 221 String newName = name + i; 222 if (!existingNames.contains(newName)) { 223 return newName; 224 } 225 } 226 } 227 228 229 isHashCodeMemoized()230 private boolean isHashCodeMemoized() { 231 return memoizedMethods(context).stream() 232 .anyMatch(method -> method.getSimpleName().contentEquals("hashCode")); 233 } 234 isEqualsFinal()235 private boolean isEqualsFinal() { 236 TypeMirror objectType = elements.getTypeElement(Object.class.getCanonicalName()).asType(); 237 ExecutableElement equals = 238 MoreElements.getLocalAndInheritedMethods(context.autoValueClass(), types, elements) 239 .stream() 240 .filter(method -> method.getSimpleName().contentEquals("equals")) 241 .filter(method -> method.getParameters().size() == 1) 242 .filter( 243 method -> 244 types.isSameType(getOnlyElement(method.getParameters()).asType(), objectType)) 245 .findFirst() 246 .get(); 247 return equals.getModifiers().contains(FINAL); 248 } 249 equalsWithHashCodeCheck()250 private MethodSpec equalsWithHashCodeCheck() { 251 return methodBuilder("equals") 252 .addModifiers(PUBLIC) 253 .returns(TypeName.BOOLEAN) 254 .addAnnotation(Override.class) 255 .addParameter(TypeName.OBJECT, "that") 256 .beginControlFlow("if (this == that)") 257 .addStatement("return true") 258 .endControlFlow() 259 .addStatement( 260 "return that instanceof $N " 261 + "&& this.hashCode() == that.hashCode() " 262 + "&& super.equals(that)", 263 className) 264 .build(); 265 } 266 267 /** 268 * Determines the required fields and overriding method for a {@link 269 * com.google.auto.value.extension.memoized.Memoized @Memoized} method. 270 */ 271 private final class MethodOverrider { 272 private final ExecutableElement method; 273 private final MethodSpec.Builder override; 274 private final FieldSpec cacheField; 275 private final ImmutableList.Builder<FieldSpec> fields = ImmutableList.builder(); 276 MethodOverrider(ExecutableElement method)277 MethodOverrider(ExecutableElement method) { 278 this.method = method; 279 validate(); 280 cacheField = 281 buildCacheField( 282 annotatedType(method.getReturnType()), method.getSimpleName().toString()); 283 fields.add(cacheField); 284 override = 285 methodBuilder(method.getSimpleName().toString()) 286 .addAnnotation(Override.class) 287 .returns(cacheField.type) 288 .addExceptions( 289 method.getThrownTypes().stream().map(TypeName::get).collect(toList())) 290 .addModifiers(filter(method.getModifiers(), not(equalTo(ABSTRACT)))); 291 for (AnnotationMirror annotation : context.methodAnnotationsToCopy(method)) { 292 AnnotationSpec annotationSpec = AnnotationSpec.get(annotation); 293 if (pullDownMethodAnnotation(annotation)) { 294 override.addAnnotation(annotationSpec); 295 } 296 } 297 298 InitializationStrategy checkStrategy = strategy(); 299 fields.addAll(checkStrategy.additionalFields()); 300 override 301 .beginControlFlow("if ($L)", checkStrategy.checkMemoized()) 302 .beginControlFlow("synchronized (this)") 303 .beginControlFlow("if ($L)", checkStrategy.checkMemoized()) 304 .addStatement("$N = super.$L()", cacheField, method.getSimpleName()) 305 .addCode(checkStrategy.setMemoized()) 306 .endControlFlow() 307 .endControlFlow() 308 .endControlFlow() 309 .addStatement("return $N", cacheField); 310 } 311 312 /** The fields that should be added to the subclass. */ fields()313 Iterable<FieldSpec> fields() { 314 return fields.build(); 315 } 316 317 /** The overriding method that should be added to the subclass. */ method()318 MethodSpec method() { 319 return override.build(); 320 } 321 validate()322 private void validate() { 323 if (method.getReturnType().getKind().equals(VOID)) { 324 printMessage(ERROR, "@Memoized methods cannot be void"); 325 } 326 if (!method.getParameters().isEmpty()) { 327 printMessage(ERROR, "@Memoized methods cannot have parameters"); 328 } 329 checkIllegalModifier(PRIVATE); 330 checkIllegalModifier(FINAL); 331 checkIllegalModifier(STATIC); 332 333 if (!overridesObjectMethod("hashCode") && !overridesObjectMethod("toString")) { 334 checkIllegalModifier(ABSTRACT); 335 } 336 } 337 checkIllegalModifier(Modifier modifier)338 private void checkIllegalModifier(Modifier modifier) { 339 if (method.getModifiers().contains(modifier)) { 340 printMessage(ERROR, "@Memoized methods cannot be %s", modifier.toString()); 341 } 342 } 343 344 @FormatMethod printMessage(Kind kind, String format, Object... args)345 private void printMessage(Kind kind, String format, Object... args) { 346 if (kind.equals(ERROR)) { 347 hasErrors = true; 348 } 349 messager.printMessage(kind, String.format(format, args), method); 350 } 351 overridesObjectMethod(String methodName)352 private boolean overridesObjectMethod(String methodName) { 353 return elements.overrides(method, objectMethod(methodName), context.autoValueClass()); 354 } 355 objectMethod(String methodName)356 private ExecutableElement objectMethod(String methodName) { 357 TypeElement object = elements.getTypeElement(Object.class.getName()); 358 return methodsIn(object.getEnclosedElements()).stream() 359 .filter(m -> m.getSimpleName().contentEquals(methodName)) 360 .findFirst() 361 .orElseThrow( 362 () -> 363 new IllegalArgumentException( 364 String.format("No method in Object named \"%s\"", methodName))); 365 } 366 pullDownMethodAnnotation(AnnotationMirror annotation)367 private boolean pullDownMethodAnnotation(AnnotationMirror annotation) { 368 return !DO_NOT_PULL_DOWN_ANNOTATIONS.contains( 369 MoreElements.asType(annotation.getAnnotationType().asElement()) 370 .getQualifiedName() 371 .toString()); 372 } 373 374 /** 375 * Builds a {@link FieldSpec} for use in property caching. Field will be {@code private 376 * transient volatile} and have the given type and name. If the @LazyInit annotation is 377 * available it is added as well. 378 */ buildCacheField(TypeName type, String name)379 private FieldSpec buildCacheField(TypeName type, String name) { 380 FieldSpec.Builder builder = FieldSpec.builder(type, name, PRIVATE, TRANSIENT, VOLATILE); 381 if (lazyInitAnnotation.isPresent()) { 382 builder.addAnnotation(lazyInitAnnotation.get()); 383 builder.addAnnotation(SUPPRESS_WARNINGS); 384 } 385 return builder.build(); 386 } 387 strategy()388 InitializationStrategy strategy() { 389 if (method.getReturnType().getKind().isPrimitive()) { 390 return new CheckBooleanField(); 391 } 392 if (containsNullable(method.getAnnotationMirrors()) 393 || containsNullable(method.getReturnType().getAnnotationMirrors())) { 394 return new CheckBooleanField(); 395 } 396 return new NullMeansUninitialized(); 397 } 398 399 private abstract class InitializationStrategy { 400 additionalFields()401 abstract Iterable<FieldSpec> additionalFields(); 402 checkMemoized()403 abstract CodeBlock checkMemoized(); 404 setMemoized()405 abstract CodeBlock setMemoized(); 406 } 407 408 private final class NullMeansUninitialized extends InitializationStrategy { 409 @Override additionalFields()410 Iterable<FieldSpec> additionalFields() { 411 return ImmutableList.of(); 412 } 413 414 @Override checkMemoized()415 CodeBlock checkMemoized() { 416 return CodeBlock.of("$N == null", cacheField); 417 } 418 419 @Override setMemoized()420 CodeBlock setMemoized() { 421 return CodeBlock.builder() 422 .beginControlFlow("if ($N == null)", cacheField) 423 .addStatement( 424 "throw new NullPointerException($S)", 425 method.getSimpleName() + "() cannot return null") 426 .endControlFlow() 427 .build(); 428 } 429 } 430 431 private final class CheckBooleanField extends InitializationStrategy { 432 433 private final FieldSpec field = 434 buildCacheField(TypeName.BOOLEAN, method.getSimpleName() + "$Memoized"); 435 436 @Override additionalFields()437 Iterable<FieldSpec> additionalFields() { 438 return ImmutableList.of(field); 439 } 440 441 @Override checkMemoized()442 CodeBlock checkMemoized() { 443 return CodeBlock.of("!$N", field); 444 } 445 446 @Override setMemoized()447 CodeBlock setMemoized() { 448 return CodeBlock.builder().addStatement("$N = true", field).build(); 449 } 450 } 451 } 452 } 453 454 /** Returns the errorprone {@code @LazyInit} annotation if it is found on the classpath. */ getLazyInitAnnotation(Elements elements)455 private static Optional<AnnotationSpec> getLazyInitAnnotation(Elements elements) { 456 if (elements.getTypeElement(LAZY_INIT.toString()) == null) { 457 return Optional.empty(); 458 } 459 return Optional.of(AnnotationSpec.builder(LAZY_INIT).build()); 460 } 461 462 /** True if one of the given annotations is {@code @Nullable} in any package. */ containsNullable(List<? extends AnnotationMirror> annotations)463 private static boolean containsNullable(List<? extends AnnotationMirror> annotations) { 464 return annotations.stream() 465 .map(a -> a.getAnnotationType().asElement().getSimpleName()) 466 .anyMatch(n -> n.contentEquals("Nullable")); 467 } 468 469 470 /** Translate a {@link TypeMirror} into a {@link TypeName}, including type annotations. */ annotatedType(TypeMirror type)471 private static TypeName annotatedType(TypeMirror type) { 472 List<AnnotationSpec> annotations = 473 type.getAnnotationMirrors().stream().map(AnnotationSpec::get).collect(toList()); 474 return TypeName.get(type).annotated(annotations); 475 } 476 477 } 478