1 /*
2  * Copyright 2018 The Android Open Source Project
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 androidx.appsearch.compiler;
17 
18 import static androidx.appsearch.compiler.IntrospectionHelper.generateClassHierarchy;
19 import static androidx.appsearch.compiler.IntrospectionHelper.getDocumentAnnotation;
20 
21 import static java.util.Objects.requireNonNull;
22 import static java.util.stream.Collectors.groupingBy;
23 
24 import androidx.annotation.RestrictTo;
25 import androidx.appsearch.compiler.annotationwrapper.DataPropertyAnnotation;
26 import androidx.appsearch.compiler.annotationwrapper.MetadataPropertyAnnotation;
27 import androidx.appsearch.compiler.annotationwrapper.PropertyAnnotation;
28 
29 import org.jspecify.annotations.NonNull;
30 import org.jspecify.annotations.Nullable;
31 
32 import java.util.Collection;
33 import java.util.HashMap;
34 import java.util.LinkedHashMap;
35 import java.util.LinkedHashSet;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Set;
39 import java.util.function.Predicate;
40 
41 import javax.annotation.processing.ProcessingEnvironment;
42 import javax.lang.model.element.AnnotationMirror;
43 import javax.lang.model.element.Element;
44 import javax.lang.model.element.ExecutableElement;
45 import javax.lang.model.element.Modifier;
46 import javax.lang.model.element.TypeElement;
47 import javax.lang.model.util.Elements;
48 
49 /**
50  * Processes @Document annotations.
51  *
52  * @see AnnotatedGetterAndFieldAccumulator for the DocumentModel's invariants with regards to its
53  * getter and field definitions.
54  *
55  * @exportToFramework:hide
56  */
57 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
58 class DocumentModel {
59     private static final String CLASS_SUFFIX = ".class";
60 
61     private final IntrospectionHelper mHelper;
62 
63     private final Elements mElementUtil;
64 
65     private final TypeElement mClass;
66 
67     // The name of the original class annotated with @Document
68     private final String mQualifiedDocumentClassName;
69 
70     private final String mSchemaName;
71 
72     private final LinkedHashSet<TypeElement> mParentTypes;
73 
74     private final LinkedHashSet<AnnotatedGetterOrField> mAnnotatedGettersAndFields;
75 
76     private final @NonNull AnnotatedGetterOrField mIdAnnotatedGetterOrField;
77 
78     private final @NonNull AnnotatedGetterOrField mNamespaceAnnotatedGetterOrField;
79 
80     private final @NonNull Map<AnnotatedGetterOrField, PropertyAccessor> mAccessors;
81 
82     private final @NonNull DocumentClassCreationInfo mDocumentClassCreationInfo;
83 
DocumentModel( @onNull ProcessingEnvironment env, @NonNull TypeElement clazz, @Nullable TypeElement generatedAutoValueElement)84     private DocumentModel(
85             @NonNull ProcessingEnvironment env,
86             @NonNull TypeElement clazz,
87             @Nullable TypeElement generatedAutoValueElement)
88             throws ProcessingException {
89         if (clazz.getModifiers().contains(Modifier.PRIVATE)) {
90             throw new ProcessingException("@Document annotated class is private", clazz);
91         }
92 
93         mHelper = new IntrospectionHelper(env);
94         mElementUtil = env.getElementUtils();
95         mClass = clazz;
96         mQualifiedDocumentClassName = generatedAutoValueElement != null
97                 ? generatedAutoValueElement.getQualifiedName().toString()
98                 : clazz.getQualifiedName().toString();
99         mParentTypes = getParentSchemaTypes(clazz);
100 
101         List<TypeElement> classHierarchy = generateClassHierarchy(clazz);
102         mSchemaName = computeSchemaName(classHierarchy);
103         mAnnotatedGettersAndFields = scanAnnotatedGettersAndFields(classHierarchy, env);
104 
105         requireNoDuplicateMetadataProperties();
106         mIdAnnotatedGetterOrField = requireGetterOrFieldMatchingPredicate(
107                 getterOrField -> getterOrField.getAnnotation() == MetadataPropertyAnnotation.ID,
108                 /* errorMessage= */"All @Document classes must have exactly one field annotated "
109                         + "with @Id");
110         mNamespaceAnnotatedGetterOrField = requireGetterOrFieldMatchingPredicate(
111                 getterOrField ->
112                         getterOrField.getAnnotation() == MetadataPropertyAnnotation.NAMESPACE,
113                 /* errorMessage= */"All @Document classes must have exactly one field annotated "
114                         + "with @Namespace");
115 
116         LinkedHashSet<ExecutableElement> allMethods = mHelper.getAllMethods(clazz);
117         mAccessors = inferPropertyAccessors(mAnnotatedGettersAndFields, allMethods, mHelper);
118         mDocumentClassCreationInfo =
119                 DocumentClassCreationInfo.infer(clazz, mAnnotatedGettersAndFields, mHelper);
120     }
121 
scanAnnotatedGettersAndFields( @onNull List<TypeElement> hierarchy, @NonNull ProcessingEnvironment env)122     private static LinkedHashSet<AnnotatedGetterOrField> scanAnnotatedGettersAndFields(
123             @NonNull List<TypeElement> hierarchy,
124             @NonNull ProcessingEnvironment env) throws ProcessingException {
125         AnnotatedGetterAndFieldAccumulator accumulator = new AnnotatedGetterAndFieldAccumulator();
126         for (TypeElement type : hierarchy) {
127             for (Element enclosedElement : type.getEnclosedElements()) {
128                 AnnotatedGetterOrField getterOrField =
129                         AnnotatedGetterOrField.tryCreateFor(enclosedElement, env);
130                 if (getterOrField == null) {
131                     continue;
132                 }
133                 accumulator.add(getterOrField);
134             }
135         }
136         return accumulator.getAccumulatedGettersAndFields();
137     }
138 
139     /**
140      * Makes sure {@link #mAnnotatedGettersAndFields} does not contain two getters/fields
141      * annotated with the same metadata annotation e.g. it doesn't make sense for a document to
142      * have two {@code @Document.Id}s.
143      */
requireNoDuplicateMetadataProperties()144     private void requireNoDuplicateMetadataProperties() throws ProcessingException {
145         Map<MetadataPropertyAnnotation, List<AnnotatedGetterOrField>> annotationToGettersAndFields =
146                 mAnnotatedGettersAndFields.stream()
147                         .filter(getterOrField ->
148                                 getterOrField.getAnnotation().getPropertyKind()
149                                         == PropertyAnnotation.Kind.METADATA_PROPERTY)
150                         .collect(groupingBy((getterOrField) ->
151                                 (MetadataPropertyAnnotation) getterOrField.getAnnotation()));
152         for (Map.Entry<MetadataPropertyAnnotation, List<AnnotatedGetterOrField>> entry :
153                 annotationToGettersAndFields.entrySet()) {
154             MetadataPropertyAnnotation annotation = entry.getKey();
155             List<AnnotatedGetterOrField> gettersAndFields = entry.getValue();
156             if (gettersAndFields.size() > 1) {
157                 // Can show the error on any of the duplicates. Just pick the first first.
158                 throw new ProcessingException(
159                         "Duplicate member annotated with @"
160                                 + annotation.getClassName().simpleName(),
161                         gettersAndFields.get(0).getElement());
162             }
163         }
164     }
165 
166     /**
167      * Makes sure {@link #mAnnotatedGettersAndFields} contains a getter/field that matches the
168      * predicate.
169      *
170      * @return The matched getter/field.
171      * @throws ProcessingException with the error message if no match.
172      */
requireGetterOrFieldMatchingPredicate( @onNull Predicate<AnnotatedGetterOrField> predicate, @NonNull String errorMessage)173     private @NonNull AnnotatedGetterOrField requireGetterOrFieldMatchingPredicate(
174             @NonNull Predicate<AnnotatedGetterOrField> predicate,
175             @NonNull String errorMessage) throws ProcessingException {
176         return mAnnotatedGettersAndFields.stream()
177                 .filter(predicate)
178                 .findFirst()
179                 .orElseThrow(() -> new ProcessingException(errorMessage, mClass));
180     }
181 
182     /**
183      * Tries to create an {@link DocumentModel} from the given {@link Element}.
184      *
185      * @throws ProcessingException if the @{@code Document}-annotated class is invalid.
186      */
createPojoModel( @onNull ProcessingEnvironment env, @NonNull TypeElement clazz)187     public static DocumentModel createPojoModel(
188             @NonNull ProcessingEnvironment env, @NonNull TypeElement clazz)
189             throws ProcessingException {
190         return new DocumentModel(env, clazz, null);
191     }
192 
193     /**
194      * Tries to create an {@link DocumentModel} from the given AutoValue {@link Element} and
195      * corresponding generated class.
196      *
197      * @throws ProcessingException if the @{@code Document}-annotated class is invalid.
198      */
createAutoValueModel( @onNull ProcessingEnvironment env, @NonNull TypeElement clazz, @NonNull TypeElement generatedAutoValueElement)199     public static DocumentModel createAutoValueModel(
200             @NonNull ProcessingEnvironment env, @NonNull TypeElement clazz,
201             @NonNull TypeElement generatedAutoValueElement)
202             throws ProcessingException {
203         return new DocumentModel(env, clazz, generatedAutoValueElement);
204     }
205 
getClassElement()206     public @NonNull TypeElement getClassElement() {
207         return mClass;
208     }
209 
210     /**
211      * The name of the original class annotated with @Document
212      *
213      * @return the class name
214      */
getQualifiedDocumentClassName()215     public @NonNull String getQualifiedDocumentClassName() {
216         return mQualifiedDocumentClassName;
217     }
218 
getSchemaName()219     public @NonNull String getSchemaName() {
220         return mSchemaName;
221     }
222 
223     /**
224      * Returns the set of parent classes specified in @Document via the "parent" parameter.
225      */
getParentTypes()226     public @NonNull Set<TypeElement> getParentTypes() {
227         return mParentTypes;
228     }
229 
230     /**
231      * Returns all getters/fields (declared or inherited) annotated with some
232      * {@link PropertyAnnotation}.
233      */
getAnnotatedGettersAndFields()234     public @NonNull Set<AnnotatedGetterOrField> getAnnotatedGettersAndFields() {
235         return mAnnotatedGettersAndFields;
236     }
237 
238     /**
239      * Returns the getter/field annotated with {@code @Document.Id}.
240      */
getIdAnnotatedGetterOrField()241     public @NonNull AnnotatedGetterOrField getIdAnnotatedGetterOrField() {
242         return mIdAnnotatedGetterOrField;
243     }
244 
245     /**
246      * Returns the getter/field annotated with {@code @Document.Namespace}.
247      */
getNamespaceAnnotatedGetterOrField()248     public @NonNull AnnotatedGetterOrField getNamespaceAnnotatedGetterOrField() {
249         return mNamespaceAnnotatedGetterOrField;
250     }
251 
252     /**
253      * Returns the public/package-private accessor for an annotated getter/field (may be private).
254      */
getAccessor(@onNull AnnotatedGetterOrField getterOrField)255     public @NonNull PropertyAccessor getAccessor(@NonNull AnnotatedGetterOrField getterOrField) {
256         PropertyAccessor accessor = mAccessors.get(getterOrField);
257         if (accessor == null) {
258             throw new IllegalArgumentException(
259                     "No such getter/field belongs to this DocumentModel: " + getterOrField);
260         }
261         return accessor;
262     }
263 
getDocumentClassCreationInfo()264     public @NonNull DocumentClassCreationInfo getDocumentClassCreationInfo() {
265         return mDocumentClassCreationInfo;
266     }
267 
268     /**
269      * Infers the {@link PropertyAccessor} for each of the {@link AnnotatedGetterOrField}.
270      *
271      * <p>Each accessor may be the {@link AnnotatedGetterOrField} itself or some other non-private
272      * getter.
273      */
inferPropertyAccessors( @onNull Collection<AnnotatedGetterOrField> annotatedGettersAndFields, @NonNull Collection<ExecutableElement> allMethods, @NonNull IntrospectionHelper helper)274     private static @NonNull Map<AnnotatedGetterOrField, PropertyAccessor> inferPropertyAccessors(
275             @NonNull Collection<AnnotatedGetterOrField> annotatedGettersAndFields,
276             @NonNull Collection<ExecutableElement> allMethods,
277             @NonNull IntrospectionHelper helper) throws ProcessingException {
278         Map<AnnotatedGetterOrField, PropertyAccessor> accessors = new HashMap<>();
279         for (AnnotatedGetterOrField getterOrField : annotatedGettersAndFields) {
280             accessors.put(
281                     getterOrField,
282                     PropertyAccessor.infer(getterOrField, allMethods, helper));
283         }
284         return accessors;
285     }
286 
287     /**
288      * Returns the parent types mentioned within the {@code @Document} annotation.
289      */
getParentSchemaTypes( @onNull TypeElement documentClass)290     private @NonNull LinkedHashSet<TypeElement> getParentSchemaTypes(
291             @NonNull TypeElement documentClass) throws ProcessingException {
292         AnnotationMirror documentAnnotation = requireNonNull(getDocumentAnnotation(documentClass));
293         Map<String, Object> params = mHelper.getAnnotationParams(documentAnnotation);
294         LinkedHashSet<TypeElement> parentsSchemaTypes = new LinkedHashSet<>();
295         Object parentsParam = params.get("parent");
296         if (parentsParam instanceof List) {
297             for (Object parent : (List<?>) parentsParam) {
298                 String parentClassName = parent.toString();
299                 parentClassName = parentClassName.substring(0,
300                         parentClassName.length() - CLASS_SUFFIX.length());
301                 parentsSchemaTypes.add(mElementUtil.getTypeElement(parentClassName));
302             }
303         }
304         if (!parentsSchemaTypes.isEmpty() && params.get("name").toString().isEmpty()) {
305             throw new ProcessingException(
306                     "All @Document classes with a parent must explicitly provide a name",
307                     mClass);
308         }
309         return parentsSchemaTypes;
310     }
311 
312     /**
313      * Computes the schema name for a Document class given its hierarchy of parent @Document
314      * classes.
315      *
316      * <p>The schema name will be the most specific Document class that has an explicit schema name,
317      * to allow the schema name to be manually set with the "name" annotation. If no such Document
318      * class exists, use the name of the root Document class, so that performing a query on the base
319      * \@Document class will also return child @Document classes.
320      *
321      * @param hierarchy List of classes annotated with \@Document, with the root class at the
322      *                  beginning and the final class at the end
323      * @return the final schema name for the class at the end of the hierarchy
324      */
computeSchemaName(List<TypeElement> hierarchy)325     private @NonNull String computeSchemaName(List<TypeElement> hierarchy) {
326         for (int i = hierarchy.size() - 1; i >= 0; i--) {
327             AnnotationMirror documentAnnotation = getDocumentAnnotation(hierarchy.get(i));
328             if (documentAnnotation == null) {
329                 continue;
330             }
331             Map<String, Object> params = mHelper.getAnnotationParams(documentAnnotation);
332             String name = params.get("name").toString();
333             if (!name.isEmpty()) {
334                 return name;
335             }
336         }
337         // Nobody had a name annotation -- use the class name of the root document in the hierarchy
338         TypeElement rootDocumentClass = hierarchy.get(0);
339         AnnotationMirror rootDocumentAnnotation = getDocumentAnnotation(rootDocumentClass);
340         if (rootDocumentAnnotation == null) {
341             return mClass.getSimpleName().toString();
342         }
343         // Documents don't need an explicit name annotation, can use the class name
344         return rootDocumentClass.getSimpleName().toString();
345     }
346 
347     /**
348      * Accumulates and de-duplicates {@link AnnotatedGetterOrField}s within a class hierarchy and
349      * ensures all of the following:
350      *
351      * <ol>
352      *     <li>
353      *         The same getter/field doesn't appear in the class hierarchy with different
354      *         annotation types e.g.
355      *
356      *         <pre>
357      *         {@code
358      *         @Document
359      *         class Parent {
360      *             @Document.StringProperty
361      *             public String getProp();
362      *         }
363      *
364      *         @Document
365      *         class Child extends Parent {
366      *             @Document.Id
367      *             public String getProp();
368      *         }
369      *         }
370      *         </pre>
371      *     </li>
372      *     <li>
373      *         The same getter/field doesn't appear twice with different serialized names e.g.
374      *
375      *         <pre>
376      *         {@code
377      *         @Document
378      *         class Parent {
379      *             @Document.StringProperty("foo")
380      *             public String getProp();
381      *         }
382      *
383      *         @Document
384      *         class Child extends Parent {
385      *             @Document.StringProperty("bar")
386      *             public String getProp();
387      *         }
388      *         }
389      *         </pre>
390      *     </li>
391      *     <li>
392      *         The same serialized name doesn't appear on two separate getters/fields e.g.
393      *
394      *         <pre>
395      *         {@code
396      *         @Document
397      *         class Gift {
398      *             @Document.StringProperty("foo")
399      *             String mField;
400      *
401      *             @Document.LongProperty("foo")
402      *             Long getProp();
403      *         }
404      *         }
405      *         </pre>
406      *     </li>
407      *     <li>
408      *         Two annotated element do not have the same normalized name because this hinders with
409      *         downstream logic that tries to infer {@link CreationMethod}s e.g.
410      *
411      *         <pre>
412      *         {@code
413      *         @Document
414      *         class Gift {
415      *             @Document.StringProperty
416      *             String mFoo;
417      *
418      *             @Document.StringProperty
419      *             String getFoo() {...}
420      *             void setFoo(String value) {...}
421      *         }
422      *         }
423      *         </pre>
424      *     </li>
425      * </ol>
426      *
427      * @see CreationMethod#inferParamAssociationsAndCreate
428      */
429     private static final class AnnotatedGetterAndFieldAccumulator {
430         private final Map<String, AnnotatedGetterOrField> mJvmNameToGetterOrField =
431                 new LinkedHashMap<>();
432         private final Map<String, AnnotatedGetterOrField> mSerializedNameToGetterOrField =
433                 new HashMap<>();
434         private final Map<String, AnnotatedGetterOrField> mNormalizedNameToGetterOrField =
435                 new HashMap<>();
436 
AnnotatedGetterAndFieldAccumulator()437         AnnotatedGetterAndFieldAccumulator() {
438         }
439 
440         /**
441          * Adds the {@link AnnotatedGetterOrField} to the accumulator.
442          *
443          * <p>{@link AnnotatedGetterOrField} that appear again are considered to be overridden
444          * versions and replace the older ones.
445          *
446          * <p>Hence, this method should be called with {@link AnnotatedGetterOrField}s from the
447          * least specific types to the most specific type.
448          */
add(@onNull AnnotatedGetterOrField getterOrField)449         void add(@NonNull AnnotatedGetterOrField getterOrField) throws ProcessingException {
450             String jvmName = getterOrField.getJvmName();
451             AnnotatedGetterOrField existingGetterOrField = mJvmNameToGetterOrField.get(jvmName);
452 
453             if (existingGetterOrField == null) {
454                 // First time we're seeing this getter or field
455                 mJvmNameToGetterOrField.put(jvmName, getterOrField);
456 
457                 requireUniqueNormalizedName(getterOrField);
458                 mNormalizedNameToGetterOrField.put(
459                         getterOrField.getNormalizedName(), getterOrField);
460 
461                 if (hasDataPropertyAnnotation(getterOrField)) {
462                     requireSerializedNameNeverSeenBefore(getterOrField);
463                     mSerializedNameToGetterOrField.put(
464                             getSerializedName(getterOrField), getterOrField);
465                 }
466             } else {
467                 // Seen this getter or field before. It showed up again because of overriding.
468                 requireAnnotationTypeIsConsistent(existingGetterOrField, getterOrField);
469                 // Replace the old entries
470                 mJvmNameToGetterOrField.put(jvmName, getterOrField);
471                 mNormalizedNameToGetterOrField.put(
472                         getterOrField.getNormalizedName(), getterOrField);
473 
474                 if (hasDataPropertyAnnotation(getterOrField)) {
475                     requireSerializedNameIsConsistent(existingGetterOrField, getterOrField);
476                     // Replace the old entry
477                     mSerializedNameToGetterOrField.put(
478                             getSerializedName(getterOrField), getterOrField);
479                 }
480             }
481         }
482 
getAccumulatedGettersAndFields()483         @NonNull LinkedHashSet<AnnotatedGetterOrField> getAccumulatedGettersAndFields() {
484             return new LinkedHashSet<>(mJvmNameToGetterOrField.values());
485         }
486 
487         /**
488          * Makes sure the getter/field's normalized name either never appeared before, or if it did,
489          * did so for the same getter/field and re-appeared likely because of overriding.
490          */
requireUniqueNormalizedName( @onNull AnnotatedGetterOrField getterOrField)491         private void requireUniqueNormalizedName(
492                 @NonNull AnnotatedGetterOrField getterOrField) throws ProcessingException {
493             AnnotatedGetterOrField existingGetterOrField =
494                     mNormalizedNameToGetterOrField.get(getterOrField.getNormalizedName());
495             if (existingGetterOrField == null) {
496                 // Never seen this normalized name before
497                 return;
498             }
499             if (existingGetterOrField.getJvmName().equals(getterOrField.getJvmName())) {
500                 // Same getter/field appeared again (likely because of overriding). Ok.
501                 return;
502             }
503             throw new ProcessingException(
504                     ("Normalized name \"%s\" is already taken up by pre-existing %s. "
505                             + "Please rename this getter/field to something else.").formatted(
506                             getterOrField.getNormalizedName(),
507                             createSignatureString(existingGetterOrField)),
508                     getterOrField.getElement());
509         }
510 
511         /**
512          * Makes sure a new getter/field is never annotated with a serialized name that is
513          * already given to some other getter/field.
514          *
515          * <p>Assumes the getter/field is annotated with a {@link DataPropertyAnnotation}.
516          */
requireSerializedNameNeverSeenBefore( @onNull AnnotatedGetterOrField getterOrField)517         private void requireSerializedNameNeverSeenBefore(
518                 @NonNull AnnotatedGetterOrField getterOrField) throws ProcessingException {
519             String serializedName = getSerializedName(getterOrField);
520             AnnotatedGetterOrField existingGetterOrField =
521                     mSerializedNameToGetterOrField.get(serializedName);
522             if (existingGetterOrField != null) {
523                 throw new ProcessingException(
524                         "Cannot give property the name '%s' because it is already used for %s"
525                                 .formatted(serializedName, existingGetterOrField.getJvmName()),
526                         getterOrField.getElement());
527             }
528         }
529 
530         /**
531          * Returns the serialized name that should be used for the property in the database.
532          *
533          * <p>Assumes the getter/field is annotated with a {@link DataPropertyAnnotation}.
534          */
getSerializedName( @onNull AnnotatedGetterOrField getterOrField)535         private static @NonNull String getSerializedName(
536                 @NonNull AnnotatedGetterOrField getterOrField) {
537             DataPropertyAnnotation annotation =
538                     (DataPropertyAnnotation) getterOrField.getAnnotation();
539             return annotation.getName();
540         }
541 
hasDataPropertyAnnotation( @onNull AnnotatedGetterOrField getterOrField)542         private static boolean hasDataPropertyAnnotation(
543                 @NonNull AnnotatedGetterOrField getterOrField) {
544             PropertyAnnotation annotation = getterOrField.getAnnotation();
545             return annotation.getPropertyKind() == PropertyAnnotation.Kind.DATA_PROPERTY;
546         }
547 
548         /**
549          * Makes sure the annotation type didn't change when overriding e.g.
550          * {@code @StringProperty -> @Id}.
551          */
requireAnnotationTypeIsConsistent( @onNull AnnotatedGetterOrField existingGetterOrField, @NonNull AnnotatedGetterOrField overriddenGetterOfField)552         private static void requireAnnotationTypeIsConsistent(
553                 @NonNull AnnotatedGetterOrField existingGetterOrField,
554                 @NonNull AnnotatedGetterOrField overriddenGetterOfField)
555                 throws ProcessingException {
556             PropertyAnnotation existingAnnotation = existingGetterOrField.getAnnotation();
557             PropertyAnnotation overriddenAnnotation = overriddenGetterOfField.getAnnotation();
558             if (!existingAnnotation.getClassName().equals(overriddenAnnotation.getClassName())) {
559                 throw new ProcessingException(
560                         ("Property type must stay consistent when overriding annotated members "
561                                 + "but changed from @%s -> @%s").formatted(
562                                 existingAnnotation.getClassName().simpleName(),
563                                 overriddenAnnotation.getClassName().simpleName()),
564                         overriddenGetterOfField.getElement());
565             }
566         }
567 
568         /**
569          * Makes sure the serialized name didn't change when overriding.
570          *
571          * <p>Assumes the getter/field is annotated with a {@link DataPropertyAnnotation}.
572          */
requireSerializedNameIsConsistent( @onNull AnnotatedGetterOrField existingGetterOrField, @NonNull AnnotatedGetterOrField overriddenGetterOrField)573         private static void requireSerializedNameIsConsistent(
574                 @NonNull AnnotatedGetterOrField existingGetterOrField,
575                 @NonNull AnnotatedGetterOrField overriddenGetterOrField)
576                 throws ProcessingException {
577             String existingSerializedName = getSerializedName(existingGetterOrField);
578             String overriddenSerializedName = getSerializedName(overriddenGetterOrField);
579             if (!existingSerializedName.equals(overriddenSerializedName)) {
580                 throw new ProcessingException(
581                         ("Property name within the annotation must stay consistent when overriding "
582                                 + "annotated members but changed from '%s' -> '%s'".formatted(
583                                 existingSerializedName, overriddenSerializedName)),
584                         overriddenGetterOrField.getElement());
585             }
586         }
587 
createSignatureString( @onNull AnnotatedGetterOrField getterOrField)588         private static @NonNull String createSignatureString(
589                 @NonNull AnnotatedGetterOrField getterOrField) {
590             return getterOrField.getJvmType()
591                     + " "
592                     + getterOrField.getElement().getEnclosingElement().getSimpleName()
593                     + "#"
594                     + getterOrField.getJvmName()
595                     + (getterOrField.isGetter() ? "()" : "");
596         }
597     }
598 }
599