• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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