001package arez.processor;
002
003import com.palantir.javapoet.ParameterizedTypeName;
004import com.palantir.javapoet.TypeName;
005import java.io.IOException;
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.Collection;
009import java.util.Collections;
010import java.util.HashMap;
011import java.util.HashSet;
012import java.util.LinkedHashMap;
013import java.util.List;
014import java.util.Locale;
015import java.util.Map;
016import java.util.Objects;
017import java.util.Set;
018import java.util.function.Function;
019import java.util.regex.Matcher;
020import java.util.regex.Pattern;
021import java.util.regex.PatternSyntaxException;
022import javax.annotation.Nonnull;
023import javax.annotation.Nullable;
024import javax.annotation.processing.ProcessingEnvironment;
025import javax.annotation.processing.RoundEnvironment;
026import javax.annotation.processing.SupportedAnnotationTypes;
027import javax.annotation.processing.SupportedOptions;
028import javax.annotation.processing.SupportedSourceVersion;
029import javax.lang.model.AnnotatedConstruct;
030import javax.lang.model.SourceVersion;
031import javax.lang.model.element.AnnotationMirror;
032import javax.lang.model.element.AnnotationValue;
033import javax.lang.model.element.Element;
034import javax.lang.model.element.ElementKind;
035import javax.lang.model.element.ExecutableElement;
036import javax.lang.model.element.Modifier;
037import javax.lang.model.element.TypeElement;
038import javax.lang.model.element.VariableElement;
039import javax.lang.model.type.DeclaredType;
040import javax.lang.model.type.ExecutableType;
041import javax.lang.model.type.TypeKind;
042import javax.lang.model.type.TypeMirror;
043import javax.lang.model.util.ElementFilter;
044import javax.lang.model.util.Elements;
045import javax.lang.model.util.Types;
046import org.realityforge.proton.AbstractStandardProcessor;
047import org.realityforge.proton.AnnotationsUtil;
048import org.realityforge.proton.DeferredElementSet;
049import org.realityforge.proton.ElementsUtil;
050import org.realityforge.proton.MemberChecks;
051import org.realityforge.proton.ProcessorException;
052import org.realityforge.proton.StopWatch;
053import org.realityforge.proton.SuperficialValidation;
054import org.realityforge.proton.TypesUtil;
055import static javax.tools.Diagnostic.Kind.*;
056
057/**
058 * Annotation processor that analyzes Arez annotated source and generates models from the annotations.
059 */
060@SupportedAnnotationTypes( "arez.annotations.*" )
061@SupportedSourceVersion( SourceVersion.RELEASE_17 )
062@SupportedOptions( { "arez.defer.unresolved",
063                     "arez.defer.errors",
064                     "arez.debug",
065                     "arez.profile",
066                     "arez.verbose_out_of_round.errors",
067                     "arez.warnings_as_errors" } )
068public final class ArezProcessor
069  extends AbstractStandardProcessor
070{
071  @Nonnull
072  static final Pattern GETTER_PATTERN = Pattern.compile( "^get([A-Z].*)$" );
073  @Nonnull
074  private static final Pattern ON_ACTIVATE_PATTERN = Pattern.compile( "^on([A-Z].*)Activate$" );
075  @Nonnull
076  private static final Pattern ON_DEACTIVATE_PATTERN = Pattern.compile( "^on([A-Z].*)Deactivate$" );
077  @Nonnull
078  private static final Pattern SETTER_PATTERN = Pattern.compile( "^set([A-Z].*)$" );
079  @Nonnull
080  private static final Pattern ISSER_PATTERN = Pattern.compile( "^is([A-Z].*)$" );
081  @Nonnull
082  private static final Pattern OBSERVABLE_INITIAL_METHOD_PATTERN = Pattern.compile( "^getInitial([A-Z].*)$" );
083  @Nonnull
084  private static final Pattern OBSERVABLE_INITIAL_FIELD_PATTERN = Pattern.compile( "^INITIAL_([A-Z].*)$" );
085  @Nonnull
086  private static final List<String> OBJECT_METHODS =
087    Arrays.asList( "hashCode", "equals", "clone", "toString", "finalize", "getClass", "wait", "notifyAll", "notify" );
088  @Nonnull
089  private static final List<String> AREZ_SPECIAL_METHODS =
090    Arrays.asList( "observe", "dispose", "isDisposed", "getArezId" );
091  @Nonnull
092  private static final String AREZ_COMPONENT_LIKE_DESCRIPTION =
093    "@ArezComponentLike or an annotation annotated by @ActAsArezComponent";
094  @Nonnull
095  private static final String AREZ_COMPONENT_LIKE_TYPE_DESCRIPTION = "an Arez component-like type";
096  @Nonnull
097  private static final List<String> MISPLACED_USAGE_ANNOTATION_CLASSNAMES =
098    Arrays.asList( Constants.ACTION_CLASSNAME,
099                   Constants.REQUIRES_TRANSACTION_CLASSNAME,
100                   Constants.OBSERVE_CLASSNAME,
101                   Constants.OBSERVABLE_CLASSNAME,
102                   Constants.MEMOIZE_CLASSNAME,
103                   Constants.MEMOIZE_CONTEXT_PARAMETER_CLASSNAME,
104                   Constants.COMPONENT_ID_CLASSNAME,
105                   Constants.COMPONENT_ID_REF_CLASSNAME,
106                   Constants.COMPONENT_REF_CLASSNAME,
107                   Constants.COMPONENT_NAME_REF_CLASSNAME,
108                   Constants.COMPONENT_TYPE_NAME_REF_CLASSNAME,
109                   Constants.COMPONENT_STATE_REF_CLASSNAME,
110                   Constants.CONTEXT_REF_CLASSNAME,
111                   Constants.OBSERVABLE_VALUE_REF_CLASSNAME,
112                   Constants.COMPUTABLE_VALUE_REF_CLASSNAME,
113                   Constants.OBSERVER_REF_CLASSNAME,
114                   Constants.POST_CONSTRUCT_CLASSNAME,
115                   Constants.PRE_DISPOSE_CLASSNAME,
116                   Constants.POST_DISPOSE_CLASSNAME,
117                   Constants.ON_ACTIVATE_CLASSNAME,
118                   Constants.ON_DEACTIVATE_CLASSNAME,
119                   Constants.ON_DEPS_CHANGE_CLASSNAME,
120                   Constants.REFERENCE_CLASSNAME,
121                   Constants.REFERENCE_ID_CLASSNAME,
122                   Constants.INVERSE_CLASSNAME,
123                   Constants.PRE_INVERSE_REMOVE_CLASSNAME,
124                   Constants.POST_INVERSE_ADD_CLASSNAME,
125                   Constants.AUTO_OBSERVE_CLASSNAME,
126                   Constants.CASCADE_DISPOSE_CLASSNAME,
127                   Constants.COMPONENT_DEPENDENCY_CLASSNAME,
128                   Constants.OBSERVABLE_INITIAL_CLASSNAME,
129                   Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME );
130  @Nonnull
131  private static final List<String> METHOD_ANNOTATIONS =
132    Arrays.asList( Constants.ACTION_CLASSNAME,
133                   Constants.REQUIRES_TRANSACTION_CLASSNAME,
134                   Constants.AUTO_OBSERVE_CLASSNAME,
135                   Constants.OBSERVE_CLASSNAME,
136                   Constants.ON_DEPS_CHANGE_CLASSNAME,
137                   Constants.OBSERVER_REF_CLASSNAME,
138                   Constants.OBSERVABLE_CLASSNAME,
139                   Constants.OBSERVABLE_INITIAL_CLASSNAME,
140                   Constants.OBSERVABLE_VALUE_REF_CLASSNAME,
141                   Constants.MEMOIZE_CLASSNAME,
142                   Constants.MEMOIZE_CONTEXT_PARAMETER_CLASSNAME,
143                   Constants.COMPUTABLE_VALUE_REF_CLASSNAME,
144                   Constants.COMPONENT_REF_CLASSNAME,
145                   Constants.COMPONENT_ID_CLASSNAME,
146                   Constants.COMPONENT_ID_REF_CLASSNAME,
147                   Constants.COMPONENT_NAME_REF_CLASSNAME,
148                   Constants.COMPONENT_TYPE_NAME_REF_CLASSNAME,
149                   Constants.COMPONENT_STATE_REF_CLASSNAME,
150                   Constants.CASCADE_DISPOSE_CLASSNAME,
151                   Constants.CONTEXT_REF_CLASSNAME,
152                   Constants.POST_CONSTRUCT_CLASSNAME,
153                   Constants.PRE_DISPOSE_CLASSNAME,
154                   Constants.POST_DISPOSE_CLASSNAME,
155                    Constants.REFERENCE_CLASSNAME,
156                    Constants.REFERENCE_ID_CLASSNAME,
157                   Constants.INVERSE_CLASSNAME,
158                   Constants.PRE_INVERSE_REMOVE_CLASSNAME,
159                   Constants.POST_INVERSE_ADD_CLASSNAME,
160                    Constants.ON_ACTIVATE_CLASSNAME,
161                    Constants.ON_DEACTIVATE_CLASSNAME,
162                    Constants.COMPONENT_DEPENDENCY_CLASSNAME );
163  @Nonnull
164  private static final Pattern ID_GETTER_PATTERN = Pattern.compile( "^get([A-Z].*)Id$" );
165  @Nonnull
166  private static final Pattern RAW_ID_GETTER_PATTERN = Pattern.compile( "^(.*)Id$" );
167  @Nonnull
168  private static final Pattern OBSERVABLE_REF_PATTERN = Pattern.compile( "^get([A-Z].*)ObservableValue$" );
169  @Nonnull
170  private static final Pattern COMPUTABLE_VALUE_REF_PATTERN = Pattern.compile( "^get([A-Z].*)ComputableValue$" );
171  @Nonnull
172  private static final Pattern OBSERVER_REF_PATTERN = Pattern.compile( "^get([A-Z].*)Observer$" );
173  @Nonnull
174  private static final Pattern ON_DEPS_CHANGE_PATTERN = Pattern.compile( "^on([A-Z].*)DepsChange" );
175  @Nonnull
176  private static final Pattern PRE_INVERSE_REMOVE_PATTERN = Pattern.compile( "^pre([A-Z].*)Remove" );
177  @Nonnull
178  private static final Pattern POST_INVERSE_ADD_PATTERN = Pattern.compile( "^post([A-Z].*)Add" );
179  @Nonnull
180  private static final Pattern CAPTURE_PATTERN = Pattern.compile( "^capture([A-Z].*)" );
181  @Nonnull
182  private static final Pattern POP_PATTERN = Pattern.compile( "^pop([A-Z].*)" );
183  @Nonnull
184  private static final Pattern PUSH_PATTERN = Pattern.compile( "^push([A-Z].*)" );
185  @Nonnull
186  private final DeferredElementSet _deferredTypes = new DeferredElementSet();
187  @Nonnull
188  private final StopWatch _analyzeComponentStopWatch = new StopWatch( "Analyze Component" );
189
190  @Override
191  @Nonnull
192  protected String getIssueTrackerURL()
193  {
194    return "https://github.com/arez/arez/issues";
195  }
196
197  @Nonnull
198  @Override
199  protected String getOptionPrefix()
200  {
201    return "arez";
202  }
203
204  @Override
205  protected void collectStopWatches( @Nonnull final Collection<StopWatch> stopWatches )
206  {
207    stopWatches.add( _analyzeComponentStopWatch );
208  }
209
210  @Override
211  public boolean process( @Nonnull final Set<? extends TypeElement> annotations, @Nonnull final RoundEnvironment env )
212  {
213    debugAnnotationProcessingRootElements( env );
214    collectRootTypeNames( env );
215    if ( !env.processingOver() )
216    {
217      detectMisplacedArezAnnotations( env );
218    }
219    processTypeElements( annotations,
220                         env,
221                         Constants.COMPONENT_CLASSNAME,
222                         _deferredTypes,
223                         _analyzeComponentStopWatch.getName(),
224                         this::process,
225                         _analyzeComponentStopWatch );
226    errorIfProcessingOverAndInvalidTypesDetected( env );
227    clearRootTypeNamesIfProcessingOver( env );
228    return true;
229  }
230
231  private void process( @Nonnull final TypeElement element )
232    throws IOException, ProcessorException
233  {
234    final ComponentDescriptor descriptor = parse( element );
235    emitTypeSpec( descriptor.getPackageName(), ComponentGenerator.buildType( processingEnv, descriptor ) );
236  }
237
238  private void detectMisplacedArezAnnotations( @Nonnull final RoundEnvironment env )
239  {
240    final var componentTypes = findComponentTypes( env );
241    for ( final var annotationClassname : MISPLACED_USAGE_ANNOTATION_CLASSNAMES )
242    {
243      final var annotationType = findTypeElement( annotationClassname );
244      if ( null == annotationType )
245      {
246        continue;
247      }
248
249      for ( final var element : env.getElementsAnnotatedWith( annotationType ) )
250      {
251        final var type = findNearestEnclosingType( element );
252        if ( null == type || !isValidArezAnnotationContainer( element, type, componentTypes ) )
253        {
254          final var message =
255            "@" + annotationType.getSimpleName() + " is only supported within a type annotated by " +
256            "@ArezComponent or " + AREZ_COMPONENT_LIKE_DESCRIPTION;
257          processingEnv.getMessager().printMessage( ERROR, message, element );
258        }
259      }
260    }
261  }
262
263  @Nullable
264  private TypeElement findNearestEnclosingType( @Nonnull final Element element )
265  {
266    var current = element;
267    while ( null != current && !( current instanceof TypeElement ) )
268    {
269      current = current.getEnclosingElement();
270    }
271    return (TypeElement) current;
272  }
273
274  @Nonnull
275  private Set<TypeElement> findComponentTypes( @Nonnull final RoundEnvironment env )
276  {
277    final var annotationType = findTypeElement( Constants.COMPONENT_CLASSNAME );
278    if ( null == annotationType )
279    {
280      return Collections.emptySet();
281    }
282
283    final var componentTypes = new HashSet<TypeElement>();
284    for ( final var element : env.getElementsAnnotatedWith( annotationType ) )
285    {
286      if ( element instanceof TypeElement )
287      {
288        componentTypes.add( (TypeElement) element );
289      }
290    }
291    return componentTypes;
292  }
293
294  private boolean isValidArezAnnotationContainer( @Nonnull final Element element,
295                                                  @Nonnull final TypeElement type,
296                                                  @Nonnull final Set<TypeElement> componentTypes )
297  {
298    if ( isArezComponentAnnotated( type ) || isArezComponentLikeAnnotated( type ) )
299    {
300      return true;
301    }
302    else if ( element instanceof TypeElement )
303    {
304      return false;
305    }
306    else
307    {
308      final var containerType = processingEnv.getTypeUtils().erasure( type.asType() );
309      return
310        componentTypes
311          .stream()
312          .anyMatch( componentType ->
313                       !type.equals( componentType ) &&
314                       processingEnv.getTypeUtils()
315                         .isSubtype( processingEnv.getTypeUtils().erasure( componentType.asType() ), containerType ) );
316    }
317  }
318
319  @Nonnull
320  private ObservableDescriptor addObservable( @Nonnull final ComponentDescriptor component,
321                                              @Nonnull final AnnotationMirror annotation,
322                                              @Nonnull final ExecutableElement method,
323                                              @Nonnull final ExecutableType methodType )
324    throws ProcessorException
325  {
326    MemberChecks.mustBeOverridable( component.getElement(),
327                                    Constants.COMPONENT_CLASSNAME,
328                                    Constants.OBSERVABLE_CLASSNAME,
329                                    method );
330
331    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
332    final boolean expectSetter = AnnotationsUtil.getAnnotationValueValue( annotation, "expectSetter" );
333    final VariableElement readOutsideTransaction =
334      AnnotationsUtil.getAnnotationValueValue( annotation, "readOutsideTransaction" );
335    final VariableElement writeOutsideTransaction =
336      AnnotationsUtil.getAnnotationValueValue( annotation, "writeOutsideTransaction" );
337    final boolean setterAlwaysMutates = AnnotationsUtil.getAnnotationValueValue( annotation, "setterAlwaysMutates" );
338    final TypeMirror equalityComparator =
339      AnnotationsUtil.getAnnotationValueValue( annotation, "equalityComparator" );
340    final Boolean requireInitializer = isInitializerRequired( method );
341
342    final TypeMirror returnType = method.getReturnType();
343    final String methodName = method.getSimpleName().toString();
344    String name;
345    final boolean setter;
346    if ( TypeKind.VOID == returnType.getKind() )
347    {
348      setter = true;
349      //Should be a setter
350      if ( 1 != method.getParameters().size() )
351      {
352        throw new ProcessorException( "@Observable target should be a setter or getter", method );
353      }
354
355      name = deriveName( method, SETTER_PATTERN, declaredName );
356      if ( null == name )
357      {
358        name = methodName;
359      }
360    }
361    else
362    {
363      setter = false;
364      //Must be a getter
365      if ( !method.getParameters().isEmpty() )
366      {
367        throw new ProcessorException( "@Observable target should be a setter or getter", method );
368      }
369      name = getPropertyAccessorName( method, declaredName );
370    }
371    // Override name if supplied by user
372    if ( !Constants.SENTINEL.equals( declaredName ) )
373    {
374      name = declaredName;
375      if ( !SourceVersion.isIdentifier( name ) )
376      {
377        throw new ProcessorException( "@Observable target specified an invalid name '" + name + "'. The " +
378                                      "name must be a valid java identifier.", method );
379      }
380      else if ( SourceVersion.isKeyword( name ) )
381      {
382        throw new ProcessorException( "@Observable target specified an invalid name '" + name + "'. The " +
383                                      "name must not be a java keyword.", method );
384      }
385    }
386    checkNameUnique( component, name, method, Constants.OBSERVABLE_CLASSNAME );
387
388    if ( setter && !expectSetter )
389    {
390      throw new ProcessorException( "Method annotated with @Observable is a setter but defines " +
391                                    "expectSetter = false for observable named " + name, method );
392    }
393
394    final ObservableDescriptor observable = component.findOrCreateObservable( name );
395    final String equalityComparatorClassName = equalityComparator.toString();
396
397    observable.setReadOutsideTransaction( readOutsideTransaction.getSimpleName().toString() );
398    observable.setWriteOutsideTransaction( writeOutsideTransaction.getSimpleName().toString() );
399    if ( !setterAlwaysMutates )
400    {
401      observable.setSetterAlwaysMutates( false );
402    }
403    if ( !expectSetter )
404    {
405      observable.setExpectSetter( false );
406    }
407    if ( !observable.expectSetter() )
408    {
409      if ( observable.hasSetter() )
410      {
411        throw new ProcessorException( "Method annotated with @Observable defines expectSetter = false but a " +
412                                      "setter exists named " + observable.getSetter().getSimpleName() +
413                                      "for observable named " + name, method );
414      }
415    }
416    if ( setter )
417    {
418      observable.setSetterDeclaredEqualityComparator( equalityComparatorClassName );
419      if ( observable.hasSetter() )
420      {
421        throw new ProcessorException( "Method annotated with @Observable defines duplicate setter for " +
422                                      "observable named " + name, method );
423      }
424      if ( !observable.expectSetter() )
425      {
426        throw new ProcessorException( "Method annotated with @Observable defines expectSetter = false but a " +
427                                      "setter exists for observable named " + name, method );
428      }
429      observable.setSetter( method, methodType );
430    }
431    else
432    {
433      observable.setGetterDeclaredEqualityComparator( equalityComparatorClassName );
434      if ( observable.hasGetter() )
435      {
436        throw new ProcessorException( "Method annotated with @Observable defines duplicate getter for " +
437                                      "observable named " + name, method );
438      }
439      observable.setGetter( method, methodType );
440    }
441    if ( null != requireInitializer )
442    {
443      if ( !method.getModifiers().contains( Modifier.ABSTRACT ) )
444      {
445        throw new ProcessorException( "@Observable target set initializer parameter to ENABLED but " +
446                                      "method is not abstract.", method );
447      }
448      final Boolean existing = observable.getInitializer();
449      if ( null == existing )
450      {
451        observable.setInitializer( requireInitializer );
452      }
453      else if ( existing != requireInitializer )
454      {
455        throw new ProcessorException( "@Observable target set initializer parameter to value that differs from " +
456                                      "the paired observable method.", method );
457      }
458    }
459    return observable;
460  }
461
462  private void addObservableValueRef( @Nonnull final ComponentDescriptor component,
463                                      @Nonnull final AnnotationMirror annotation,
464                                      @Nonnull final ExecutableElement method,
465                                      @Nonnull final ExecutableType methodType )
466    throws ProcessorException
467  {
468    mustBeStandardRefMethod( processingEnv,
469                             component,
470                             method,
471                             Constants.OBSERVABLE_VALUE_REF_CLASSNAME );
472
473    final TypeMirror returnType = methodType.getReturnType();
474    if ( TypeKind.DECLARED != returnType.getKind() ||
475         !ElementsUtil.toRawType( returnType ).toString().equals( "arez.ObservableValue" ) )
476    {
477      throw new ProcessorException( "Method annotated with @ObservableValueRef must return an instance of " +
478                                    "arez.ObservableValue", method );
479    }
480
481    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
482    final String name;
483    if ( Constants.SENTINEL.equals( declaredName ) )
484    {
485      name = deriveName( method, OBSERVABLE_REF_PATTERN, declaredName );
486      if ( null == name )
487      {
488        throw new ProcessorException( "Method annotated with @ObservableValueRef should specify name or be " +
489                                      "named according to the convention get[Name]ObservableValue", method );
490      }
491    }
492    else
493    {
494      name = declaredName;
495      if ( !SourceVersion.isIdentifier( name ) )
496      {
497        throw new ProcessorException( "@ObservableValueRef target specified an invalid name '" + name + "'. The " +
498                                      "name must be a valid java identifier.", method );
499      }
500      else if ( SourceVersion.isKeyword( name ) )
501      {
502        throw new ProcessorException( "@ObservableValueRef target specified an invalid name '" + name + "'. The " +
503                                      "name must not be a java keyword.", method );
504      }
505    }
506
507    component.findOrCreateObservable( name ).addRefMethod( method, methodType );
508  }
509
510  private void addComputableValueRef( @Nonnull final ComponentDescriptor component,
511                                      @Nonnull final AnnotationMirror annotation,
512                                      @Nonnull final ExecutableElement method,
513                                      @Nonnull final ExecutableType methodType )
514    throws ProcessorException
515  {
516    mustBeRefMethod( component, method, Constants.COMPUTABLE_VALUE_REF_CLASSNAME );
517    shouldBeInternalRefMethod( processingEnv,
518                               component,
519                               method,
520                               Constants.COMPUTABLE_VALUE_REF_CLASSNAME );
521
522    final TypeMirror returnType = methodType.getReturnType();
523    if ( TypeKind.DECLARED != returnType.getKind() ||
524         !ElementsUtil.toRawType( returnType ).toString().equals( "arez.ComputableValue" ) )
525    {
526      throw new ProcessorException( "Method annotated with @ComputableValueRef must return an instance of " +
527                                    "arez.ComputableValue", method );
528    }
529
530    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
531    final String name;
532    if ( Constants.SENTINEL.equals( declaredName ) )
533    {
534      name = deriveName( method, COMPUTABLE_VALUE_REF_PATTERN, declaredName );
535      if ( null == name )
536      {
537        throw new ProcessorException( "Method annotated with @ComputableValueRef should specify name or be " +
538                                      "named according to the convention get[Name]ComputableValue", method );
539      }
540    }
541    else
542    {
543      name = declaredName;
544      if ( !SourceVersion.isIdentifier( name ) )
545      {
546        throw new ProcessorException( "@ComputableValueRef target specified an invalid name '" + name + "'. The " +
547                                      "name must be a valid java identifier.", method );
548      }
549      else if ( SourceVersion.isKeyword( name ) )
550      {
551        throw new ProcessorException( "@ComputableValueRef target specified an invalid name '" + name + "'. The " +
552                                      "name must not be a java keyword.", method );
553      }
554    }
555
556    MemberChecks.mustBeSubclassCallable( component.getElement(),
557                                         Constants.COMPONENT_CLASSNAME,
558                                         Constants.COMPUTABLE_VALUE_REF_CLASSNAME,
559                                         method );
560    MemberChecks.mustNotThrowAnyExceptions( Constants.COMPUTABLE_VALUE_REF_CLASSNAME, method );
561    component.findOrCreateMemoize( name ).addRefMethod( method, methodType );
562  }
563
564  @Nonnull
565  private String deriveMemoizeName( @Nonnull final ExecutableElement method,
566                                    @Nonnull final AnnotationMirror annotation )
567    throws ProcessorException
568  {
569    final String name = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
570    if ( Constants.SENTINEL.equals( name ) )
571    {
572      return getPropertyAccessorName( method, name );
573    }
574    else
575    {
576      if ( !SourceVersion.isIdentifier( name ) )
577      {
578        throw new ProcessorException( "@Memoize target specified an invalid name '" + name + "'. The " +
579                                      "name must be a valid java identifier.", method );
580      }
581      else if ( SourceVersion.isKeyword( name ) )
582      {
583        throw new ProcessorException( "@Memoize target specified an invalid name '" + name + "'. The " +
584                                      "name must not be a java keyword.", method );
585      }
586      return name;
587    }
588  }
589
590  private void addOnActivate( @Nonnull final ComponentDescriptor component,
591                              @Nonnull final AnnotationMirror annotation,
592                              @Nonnull final ExecutableElement method )
593    throws ProcessorException
594  {
595    final String name =
596      deriveHookName( component,
597                      method,
598                      ON_ACTIVATE_PATTERN,
599                      "Activate",
600                      AnnotationsUtil.getAnnotationValueValue( annotation, "name" ) );
601    setOnActivate( component, component.findOrCreateMemoize( name ), method );
602  }
603
604  private void addOnDeactivate( @Nonnull final ComponentDescriptor component,
605                                @Nonnull final AnnotationMirror annotation,
606                                @Nonnull final ExecutableElement method )
607    throws ProcessorException
608  {
609    final String name =
610      deriveHookName( component,
611                      method,
612                      ON_DEACTIVATE_PATTERN,
613                      "Deactivate",
614                      AnnotationsUtil.getAnnotationValueValue( annotation, "name" ) );
615    MemberChecks.mustBeLifecycleHook( component.getElement(),
616                                      Constants.COMPONENT_CLASSNAME,
617                                      Constants.ON_DEACTIVATE_CLASSNAME,
618                                      method );
619    shouldBeInternalHookMethod( processingEnv,
620                                component,
621                                method,
622                                Constants.ON_DEACTIVATE_CLASSNAME );
623    component.findOrCreateMemoize( name ).setOnDeactivate( method );
624  }
625
626  @Nonnull
627  private String deriveHookName( @Nonnull final ComponentDescriptor component,
628                                 @Nonnull final ExecutableElement method,
629                                 @Nonnull final Pattern pattern,
630                                 @Nonnull final String type,
631                                 @Nonnull final String name )
632    throws ProcessorException
633  {
634    final String value = deriveName( method, pattern, name );
635    if ( null == value )
636    {
637      throw new ProcessorException( "Unable to derive name for @On" + type + " as does not match " +
638                                    "on[Name]" + type + " pattern. Please specify name.", method );
639    }
640    else if ( !SourceVersion.isIdentifier( value ) )
641    {
642      throw new ProcessorException( "@On" + type + " target specified an invalid name '" + value + "'. The " +
643                                    "name must be a valid java identifier.", component.getElement() );
644    }
645    else if ( SourceVersion.isKeyword( value ) )
646    {
647      throw new ProcessorException( "@On" + type + " target specified an invalid name '" + value + "'. The " +
648                                    "name must not be a java keyword.", component.getElement() );
649    }
650    else
651    {
652      return value;
653    }
654  }
655
656  private void addComponentStateRef( @Nonnull final ComponentDescriptor component,
657                                     @Nonnull final AnnotationMirror annotation,
658                                     @Nonnull final ExecutableElement method )
659    throws ProcessorException
660  {
661    mustBeStandardRefMethod( processingEnv,
662                             component,
663                             method,
664                             Constants.COMPONENT_STATE_REF_CLASSNAME );
665
666    final TypeMirror returnType = method.getReturnType();
667    if ( TypeKind.BOOLEAN != returnType.getKind() )
668    {
669      throw new ProcessorException( "@ComponentStateRef target must return a boolean", method );
670    }
671    final VariableElement variableElement = AnnotationsUtil.getAnnotationValueValue( annotation, "value" );
672    final ComponentStateRefDescriptor.State state =
673      ComponentStateRefDescriptor.State.valueOf( variableElement.getSimpleName().toString() );
674
675    component.getComponentStateRefs().add( new ComponentStateRefDescriptor( method, state ) );
676  }
677
678  private void addContextRef( @Nonnull final ComponentDescriptor component, @Nonnull final ExecutableElement method )
679    throws ProcessorException
680  {
681    mustBeStandardRefMethod( processingEnv,
682                             component,
683                             method,
684                             Constants.CONTEXT_REF_CLASSNAME );
685    MemberChecks.mustReturnAnInstanceOf( processingEnv,
686                                         method,
687                                         Constants.OBSERVER_REF_CLASSNAME,
688                                         "arez.ArezContext" );
689    component.getContextRefs().add( method );
690  }
691
692  private void addComponentIdRef( @Nonnull final ComponentDescriptor component,
693                                  @Nonnull final ExecutableElement method )
694  {
695    mustBeRefMethod( component, method, Constants.COMPONENT_ID_REF_CLASSNAME );
696    MemberChecks.mustNotHaveAnyParameters( Constants.COMPONENT_ID_REF_CLASSNAME, method );
697    component.getComponentIdRefs().add( method );
698  }
699
700  private void addComponentRef( @Nonnull final ComponentDescriptor component, @Nonnull final ExecutableElement method )
701    throws ProcessorException
702  {
703    mustBeStandardRefMethod( processingEnv,
704                             component,
705                             method,
706                             Constants.COMPONENT_REF_CLASSNAME );
707    MemberChecks.mustReturnAnInstanceOf( processingEnv,
708                                         method,
709                                         Constants.COMPONENT_REF_CLASSNAME,
710                                         "arez.Component" );
711    component.getComponentRefs().add( method );
712  }
713
714  private void setComponentId( @Nonnull final ComponentDescriptor component,
715                               @Nonnull final ExecutableElement componentId,
716                               @Nonnull final ExecutableType componentIdMethodType )
717    throws ProcessorException
718  {
719    MemberChecks.mustNotBeAbstract( Constants.COMPONENT_ID_CLASSNAME, componentId );
720    MemberChecks.mustBeSubclassCallable( component.getElement(),
721                                         Constants.COMPONENT_CLASSNAME,
722                                         Constants.COMPONENT_ID_CLASSNAME,
723                                         componentId );
724    MemberChecks.mustNotHaveAnyParameters( Constants.COMPONENT_ID_CLASSNAME, componentId );
725    MemberChecks.mustReturnAValue( Constants.COMPONENT_ID_CLASSNAME, componentId );
726    MemberChecks.mustNotThrowAnyExceptions( Constants.COMPONENT_ID_CLASSNAME, componentId );
727
728    if ( null != component.getComponentId() )
729    {
730      throw new ProcessorException( "@ComponentId target duplicates existing method named " +
731                                    component.getComponentId().getSimpleName(), componentId );
732    }
733    else
734    {
735      component.setComponentId( Objects.requireNonNull( componentId ) );
736      component.setComponentIdMethodType( componentIdMethodType );
737    }
738  }
739
740  private void setComponentTypeNameRef( @Nonnull final ComponentDescriptor component,
741                                        @Nonnull final ExecutableElement method )
742    throws ProcessorException
743  {
744    mustBeStandardRefMethod( processingEnv,
745                             component,
746                             method,
747                             Constants.COMPONENT_TYPE_NAME_REF_CLASSNAME );
748    MemberChecks.mustReturnAnInstanceOf( processingEnv,
749                                         method,
750                                         Constants.COMPONENT_TYPE_NAME_REF_CLASSNAME,
751                                         String.class.getName() );
752    component.getComponentTypeNameRefs().add( method );
753  }
754
755  private void addComponentNameRef( @Nonnull final ComponentDescriptor component,
756                                    @Nonnull final ExecutableElement method )
757    throws ProcessorException
758  {
759    mustBeStandardRefMethod( processingEnv,
760                             component,
761                             method,
762                             Constants.COMPONENT_NAME_REF_CLASSNAME );
763    MemberChecks.mustReturnAnInstanceOf( processingEnv,
764                                         method,
765                                         Constants.COMPONENT_NAME_REF_CLASSNAME,
766                                         String.class.getName() );
767    component.getComponentNameRefs().add( method );
768  }
769
770  private void addPostConstruct( @Nonnull final ComponentDescriptor component, @Nonnull final ExecutableElement method )
771    throws ProcessorException
772  {
773    MemberChecks.mustBeLifecycleHook( component.getElement(),
774                                      Constants.COMPONENT_CLASSNAME,
775                                      Constants.POST_CONSTRUCT_CLASSNAME,
776                                      method );
777    shouldBeInternalLifecycleMethod( processingEnv,
778                                     component,
779                                     method,
780                                     Constants.POST_CONSTRUCT_CLASSNAME );
781    component.getPostConstructs().add( method );
782  }
783
784  private void addPreDispose( @Nonnull final ComponentDescriptor component, @Nonnull final ExecutableElement method )
785    throws ProcessorException
786  {
787    MemberChecks.mustBeLifecycleHook( component.getElement(),
788                                      Constants.COMPONENT_CLASSNAME,
789                                      Constants.PRE_DISPOSE_CLASSNAME,
790                                      method );
791    shouldBeInternalLifecycleMethod( processingEnv,
792                                     component,
793                                     method,
794                                     Constants.PRE_DISPOSE_CLASSNAME );
795    component.getPreDisposes().add( method );
796  }
797
798  private void addPostDispose( @Nonnull final ComponentDescriptor component, @Nonnull final ExecutableElement method )
799    throws ProcessorException
800  {
801    MemberChecks.mustBeLifecycleHook( component.getElement(),
802                                      Constants.COMPONENT_CLASSNAME,
803                                      Constants.POST_DISPOSE_CLASSNAME,
804                                      method );
805    shouldBeInternalLifecycleMethod( processingEnv,
806                                     component,
807                                     method,
808                                     Constants.POST_DISPOSE_CLASSNAME );
809    component.getPostDisposes().add( method );
810  }
811
812  private void linkUnAnnotatedObservables( @Nonnull final ComponentDescriptor component,
813                                           @Nonnull final Map<String, CandidateMethod> getters,
814                                           @Nonnull final Map<String, CandidateMethod> setters )
815    throws ProcessorException
816  {
817    for ( final ObservableDescriptor observable : component.getObservables().values() )
818    {
819      if ( !observable.hasSetter() && !observable.hasGetter() )
820      {
821        throw new ProcessorException( "@ObservableValueRef target unable to be associated with an " +
822                                      "Observable property", observable.getRefMethods().get( 0 ).getMethod() );
823      }
824      else if ( !observable.hasSetter() && observable.expectSetter() )
825      {
826        final CandidateMethod candidate = setters.remove( observable.getName() );
827        if ( null != candidate )
828        {
829          MemberChecks.mustBeOverridable( component.getElement(),
830                                          Constants.COMPONENT_CLASSNAME,
831                                          Constants.OBSERVABLE_CLASSNAME,
832                                          candidate.getMethod() );
833          observable.setSetter( candidate.getMethod(), candidate.getMethodType() );
834        }
835        else if ( observable.hasGetter() )
836        {
837          throw new ProcessorException( "@Observable target defined getter but no setter was defined and no " +
838                                        "setter could be automatically determined", observable.getGetter() );
839        }
840      }
841      else if ( !observable.hasGetter() )
842      {
843        final CandidateMethod candidate = getters.remove( observable.getName() );
844        if ( null != candidate )
845        {
846          MemberChecks.mustBeOverridable( component.getElement(),
847                                          Constants.COMPONENT_CLASSNAME,
848                                          Constants.OBSERVABLE_CLASSNAME,
849                                          candidate.getMethod() );
850          observable.setGetter( candidate.getMethod(), candidate.getMethodType() );
851        }
852        else
853        {
854          throw new ProcessorException( "@Observable target defined setter but no getter was defined and no " +
855                                        "getter could be automatically determined", observable.getSetter() );
856        }
857      }
858    }
859
860    // Find pairs of un-annotated abstract setter/getter pairs and treat them as if they
861    // are annotated with @Observable
862    for ( final Map.Entry<String, CandidateMethod> entry : new ArrayList<>( getters.entrySet() ) )
863    {
864      final CandidateMethod getter = entry.getValue();
865      if ( getter.getMethod().getModifiers().contains( Modifier.ABSTRACT ) )
866      {
867        final String name = entry.getKey();
868        final CandidateMethod setter = setters.remove( name );
869        if ( null != setter && setter.getMethod().getModifiers().contains( Modifier.ABSTRACT ) )
870        {
871          final ObservableDescriptor observable = component.findOrCreateObservable( name );
872          observable.setGetter( getter.getMethod(), getter.getMethodType() );
873          observable.setSetter( setter.getMethod(), setter.getMethodType() );
874          getters.remove( name );
875        }
876      }
877    }
878  }
879
880  private void linkUnAnnotatedObserves( @Nonnull final ComponentDescriptor component,
881                                        @Nonnull final Map<String, CandidateMethod> observes,
882                                        @Nonnull final Map<String, List<CandidateMethod>> onDepsChanges )
883    throws ProcessorException
884  {
885    for ( final ObserveDescriptor observe : component.getObserves().values() )
886    {
887      if ( !observe.hasObserve() )
888      {
889        final CandidateMethod candidate = observes.remove( observe.getName() );
890        if ( null != candidate )
891        {
892          observe.setObserveMethod( false,
893                                    Priority.NORMAL,
894                                    true,
895                                    true,
896                                    true,
897                                    "AREZ",
898                                    false,
899                                    false,
900                                    candidate.getMethod(),
901                                    candidate.getMethodType() );
902        }
903        else
904        {
905          throw new ProcessorException( "@OnDepsChange target has no corresponding @Observe that could " +
906                                        "be automatically determined", observe.getFirstOnDepsChange() );
907        }
908      }
909      final var candidates = onDepsChanges.remove( observe.getName() );
910      if ( null != candidates )
911      {
912        for ( final var candidate : candidates )
913        {
914          setOnDepsChange( component, observe, candidate.getMethod() );
915        }
916      }
917    }
918  }
919
920  private void setOnDepsChange( @Nonnull final ComponentDescriptor component,
921                                @Nonnull final ObserveDescriptor observe,
922                                @Nonnull final ExecutableElement method )
923  {
924    MemberChecks.mustNotBeAbstract( Constants.ON_DEPS_CHANGE_CLASSNAME, method );
925    MemberChecks.mustBeSubclassCallable( component.getElement(),
926                                         Constants.COMPONENT_CLASSNAME,
927                                         Constants.ON_DEPS_CHANGE_CLASSNAME,
928                                         method );
929    final var parameters = method.getParameters();
930    if (
931      !(
932        parameters.isEmpty() ||
933        ( 1 == parameters.size() && Constants.OBSERVER_CLASSNAME.equals( parameters.get( 0 ).asType().toString() ) )
934      )
935    )
936    {
937      throw new ProcessorException( "@OnDepsChange target must not have any parameters or must have a single " +
938                                    "parameter of type arez.Observer", method );
939    }
940
941    MemberChecks.mustNotReturnAnyValue( Constants.ON_DEPS_CHANGE_CLASSNAME, method );
942    MemberChecks.mustNotThrowAnyExceptions( Constants.ON_DEPS_CHANGE_CLASSNAME, method );
943    shouldBeInternalHookMethod( processingEnv,
944                                component,
945                                method,
946                                Constants.ON_DEPS_CHANGE_CLASSNAME );
947    observe.setOnDepsChange( method );
948  }
949
950  private void setOnActivate( @Nonnull final ComponentDescriptor component,
951                              @Nonnull final MemoizeDescriptor memoize,
952                              @Nonnull final ExecutableElement method )
953    throws ProcessorException
954  {
955    MemberChecks.mustNotBeAbstract( Constants.ON_ACTIVATE_CLASSNAME, method );
956    MemberChecks.mustBeSubclassCallable( component.getElement(),
957                                         Constants.COMPONENT_CLASSNAME,
958                                         Constants.ON_ACTIVATE_CLASSNAME,
959                                         method );
960
961    final var parameters = method.getParameters();
962    if ( !parameters.isEmpty() &&
963         !( 1 == parameters.size() &&
964            parameters.get( 0 ).asType().toString().startsWith( Constants.COMPUTABLE_VALUE_CLASSNAME ) ) )
965    {
966      MemberChecks.mustNotHaveAnyParameters( Constants.ON_ACTIVATE_CLASSNAME, method );
967    }
968
969    MemberChecks.mustNotReturnAnyValue( Constants.ON_ACTIVATE_CLASSNAME, method );
970    MemberChecks.mustNotThrowAnyExceptions( Constants.ON_ACTIVATE_CLASSNAME, method );
971    shouldBeInternalHookMethod( processingEnv,
972                                component,
973                                method,
974                                Constants.ON_ACTIVATE_CLASSNAME );
975
976    memoize.setOnActivate( method );
977  }
978
979  private void verifyNoDuplicateAnnotations( @Nonnull final ExecutableElement method )
980    throws ProcessorException
981  {
982    final Map<String, Collection<String>> exceptions = new HashMap<>();
983    exceptions.put( Constants.OBSERVABLE_CLASSNAME,
984                    Arrays.asList( Constants.COMPONENT_DEPENDENCY_CLASSNAME,
985                                   Constants.CASCADE_DISPOSE_CLASSNAME,
986                                   Constants.AUTO_OBSERVE_CLASSNAME,
987                                   Constants.REFERENCE_ID_CLASSNAME,
988                                   Constants.INVERSE_CLASSNAME ) );
989    exceptions.put( Constants.REFERENCE_CLASSNAME,
990                    Arrays.asList( Constants.CASCADE_DISPOSE_CLASSNAME,
991                                   Constants.AUTO_OBSERVE_CLASSNAME ) );
992    exceptions.put( Constants.POST_CONSTRUCT_CLASSNAME,
993                    Collections.singletonList( Constants.ACTION_CLASSNAME ) );
994
995    MemberChecks.verifyNoOverlappingAnnotations( method, METHOD_ANNOTATIONS, exceptions );
996  }
997
998  private void verifyNoDuplicateAnnotations( @Nonnull final VariableElement field )
999    throws ProcessorException
1000  {
1001    MemberChecks.verifyNoOverlappingAnnotations( field,
1002                                                 Arrays.asList( Constants.COMPONENT_DEPENDENCY_CLASSNAME,
1003                                                                Constants.CASCADE_DISPOSE_CLASSNAME,
1004                                                                Constants.AUTO_OBSERVE_CLASSNAME,
1005                                                                Constants.OBSERVABLE_INITIAL_CLASSNAME ),
1006                                                 Collections.emptyMap() );
1007  }
1008
1009  @Nonnull
1010  private String getPropertyAccessorName( @Nonnull final ExecutableElement method, @Nonnull final String specifiedName )
1011    throws ProcessorException
1012  {
1013    String name = deriveName( method, GETTER_PATTERN, specifiedName );
1014    if ( null != name )
1015    {
1016      return name;
1017    }
1018    if ( method.getReturnType().getKind() == TypeKind.BOOLEAN )
1019    {
1020      name = deriveName( method, ISSER_PATTERN, specifiedName );
1021      if ( null != name )
1022      {
1023        return name;
1024      }
1025    }
1026    return method.getSimpleName().toString();
1027  }
1028
1029  private void validate( final boolean allowEmpty, @Nonnull final ComponentDescriptor component )
1030    throws ProcessorException
1031  {
1032    component.getCascadeDisposes().values().forEach( CascadeDisposeDescriptor::validate );
1033    component.getAutoObserves().values().forEach( AutoObserveDescriptor::validate );
1034    component.getObservables().values().forEach( ObservableDescriptor::validate );
1035    component.getMemoizes().values().forEach( e -> e.validate( processingEnv ) );
1036    component.getMemoizeContextParameters().values().forEach( p -> p.validate( processingEnv ) );
1037    component.getObserves().values().forEach( ObserveDescriptor::validate );
1038    component.getDependencies().values().forEach( DependencyDescriptor::validate );
1039    component.getReferences().values().forEach( ReferenceDescriptor::validate );
1040    component.getInverses().values().forEach( e -> e.validate( processingEnv ) );
1041
1042    final boolean hasZeroReactiveElements =
1043      component.getObservables().isEmpty() &&
1044      component.getActions().isEmpty() &&
1045      component.getMemoizes().isEmpty() &&
1046      component.getDependencies().isEmpty() &&
1047      component.getAutoObserves().isEmpty() &&
1048      component.getCascadeDisposes().isEmpty() &&
1049      component.getReferences().isEmpty() &&
1050      component.getInverses().isEmpty() &&
1051      component.getObserves().isEmpty();
1052
1053    final TypeElement element = component.getElement();
1054    if ( null != component.getDefaultPriority() &&
1055         component.getMemoizes().isEmpty() &&
1056         component.getObserves().isEmpty() &&
1057         isWarningNotSuppressed( element, Constants.WARNING_UNNECESSARY_DEFAULT_PRIORITY ) )
1058    {
1059      final String message =
1060        MemberChecks.toSimpleName( Constants.COMPONENT_CLASSNAME ) + " target should not specify " +
1061        "the defaultPriority parameter unless it contains methods annotated with either the " +
1062        MemberChecks.toSimpleName( Constants.MEMOIZE_CLASSNAME ) + " annotation or the " +
1063        MemberChecks.toSimpleName( Constants.OBSERVE_CLASSNAME ) + " annotation. " +
1064        suppressedBy( Constants.WARNING_UNNECESSARY_DEFAULT_PRIORITY );
1065      warning( message, element );
1066    }
1067    if ( !allowEmpty && hasZeroReactiveElements )
1068    {
1069      throw new ProcessorException( "@ArezComponent target has no methods annotated with @Action, " +
1070                                    "@AutoObserve, @CascadeDispose, @Memoize, @Observable, @Inverse, " +
1071                                    "@Reference, @ComponentDependency or @Observe", element );
1072    }
1073    else if ( allowEmpty &&
1074              !hasZeroReactiveElements &&
1075              isWarningNotSuppressed( element, Constants.WARNING_UNNECESSARY_ALLOW_EMPTY ) )
1076    {
1077      final String message =
1078        "@ArezComponent target has specified allowEmpty = true but has methods " +
1079        "annotated with @Action, @AutoObserve, @CascadeDispose, @Memoize, @Observable, @Inverse, " +
1080        "@Reference, @ComponentDependency or @Observe. " +
1081        suppressedBy( Constants.WARNING_UNNECESSARY_ALLOW_EMPTY );
1082      warning( message, element );
1083    }
1084
1085    for ( final ExecutableElement componentIdRef : component.getComponentIdRefs() )
1086    {
1087      if ( null != component.getComponentId() &&
1088           !processingEnv.getTypeUtils()
1089             .isSameType( component.getComponentId().getReturnType(), componentIdRef.getReturnType() ) )
1090      {
1091        throw new ProcessorException( "@ComponentIdRef target has a return type " + componentIdRef.getReturnType() +
1092                                      " and a @ComponentId annotated method with a return type " +
1093                                      componentIdRef.getReturnType() + ". The types must match.",
1094                                      element );
1095      }
1096      else if ( null == component.getComponentId() &&
1097                !processingEnv.getTypeUtils()
1098                  .isSameType( processingEnv.getTypeUtils().getPrimitiveType( TypeKind.INT ),
1099                               componentIdRef.getReturnType() ) )
1100      {
1101        throw new ProcessorException( "@ComponentIdRef target has a return type " + componentIdRef.getReturnType() +
1102                                      " but no @ComponentId annotated method. The type is expected to be of " +
1103                                      "type int.", element );
1104      }
1105    }
1106    for ( final ExecutableElement constructor : ElementsUtil.getConstructors( element ) )
1107    {
1108      if ( Elements.Origin.EXPLICIT == processingEnv.getElementUtils().getOrigin( constructor ) &&
1109           constructor.getModifiers().contains( Modifier.PUBLIC ) &&
1110           ElementsUtil.isWarningNotSuppressed( constructor, Constants.WARNING_PUBLIC_CONSTRUCTOR ) )
1111      {
1112        final String instruction =
1113          component.isStingEnabled() ?
1114          "The type is instantiated by the sting injection framework and should have a package-access constructor. " :
1115          "It is recommended that a static create method be added to the component that is responsible " +
1116          "for instantiating the arez implementation class. ";
1117
1118        final String message =
1119          MemberChecks.shouldNot( Constants.COMPONENT_CLASSNAME,
1120                                  "have a public constructor. " + instruction +
1121                                  MemberChecks.suppressedBy( Constants.WARNING_PUBLIC_CONSTRUCTOR,
1122                                                             Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) );
1123        warning( message, constructor );
1124      }
1125    }
1126    if ( null != component.getDeclaredDefaultReadOutsideTransaction() &&
1127         component.getObservables().isEmpty() &&
1128         component.getMemoizes().isEmpty() &&
1129         isWarningNotSuppressed( element, Constants.WARNING_UNNECESSARY_DEFAULT ) )
1130    {
1131      final String message =
1132        "@ArezComponent target has specified a value for the defaultReadOutsideTransaction parameter but does not " +
1133        "contain any methods annotated with either @Memoize or @Observable. " +
1134        suppressedBy( Constants.WARNING_UNNECESSARY_DEFAULT );
1135      warning( message, element );
1136    }
1137    if ( null != component.getDeclaredDefaultWriteOutsideTransaction() &&
1138         component.getObservables().isEmpty() &&
1139         isWarningNotSuppressed( element, Constants.WARNING_UNNECESSARY_DEFAULT ) )
1140    {
1141      final String message =
1142        "@ArezComponent target has specified a value for the defaultWriteOutsideTransaction parameter but does not " +
1143        "contain any methods annotated with @Observable. " +
1144        suppressedBy( Constants.WARNING_UNNECESSARY_DEFAULT );
1145      warning( message, element );
1146    }
1147    if ( null != component.getDeclaredDefaultSkipIfDisposed() &&
1148         component.getActions().isEmpty() &&
1149         isWarningNotSuppressed( element, Constants.WARNING_UNNECESSARY_DEFAULT ) )
1150    {
1151      final String message =
1152        "@ArezComponent target has specified a value for the defaultSkipIfDisposed parameter but does not " +
1153        "contain any methods annotated with @Action. " +
1154        suppressedBy( Constants.WARNING_UNNECESSARY_DEFAULT );
1155      warning( message, element );
1156    }
1157  }
1158
1159  private void processCascadeDisposeFields( @Nonnull final ComponentDescriptor component )
1160  {
1161    ElementsUtil.getFields( component.getElement() )
1162      .stream()
1163      .filter( f -> AnnotationsUtil.hasAnnotationOfType( f, Constants.CASCADE_DISPOSE_CLASSNAME ) )
1164      .forEach( field -> processCascadeDisposeField( component, field ) );
1165  }
1166
1167  private void processAutoObserveFields( @Nonnull final ComponentDescriptor component )
1168  {
1169    ElementsUtil.getFields( component.getElement() )
1170      .stream()
1171      .filter( f -> AnnotationsUtil.hasAnnotationOfType( f, Constants.AUTO_OBSERVE_CLASSNAME ) )
1172      .forEach( field -> processAutoObserveField( component, field ) );
1173  }
1174
1175  private void processCascadeDisposeField( @Nonnull final ComponentDescriptor component,
1176                                           @Nonnull final VariableElement field )
1177  {
1178    verifyNoDuplicateAnnotations( field );
1179    MemberChecks.mustBeSubclassCallable( component.getElement(),
1180                                         Constants.COMPONENT_CLASSNAME,
1181                                         Constants.CASCADE_DISPOSE_CLASSNAME,
1182                                         field );
1183    emitWarningForManagedFieldAccess( component, field, Constants.CASCADE_DISPOSE_CLASSNAME );
1184    mustBeCascadeDisposeTypeCompatible( field );
1185    emitWarningForConflictingDisposeModel( field );
1186    if ( field.getModifiers().contains( Modifier.FINAL ) )
1187    {
1188      verifyFieldHasExplicitNullabilityAnnotation( field, Constants.CASCADE_DISPOSE_CLASSNAME );
1189    }
1190    component.addCascadeDispose( new CascadeDisposeDescriptor( field ) );
1191  }
1192
1193  private void processAutoObserveField( @Nonnull final ComponentDescriptor component,
1194                                        @Nonnull final VariableElement field )
1195  {
1196    verifyNoDuplicateAnnotations( field );
1197    MemberChecks.mustBeSubclassCallable( component.getElement(),
1198                                         Constants.COMPONENT_CLASSNAME,
1199                                         Constants.AUTO_OBSERVE_CLASSNAME,
1200                                         field );
1201    emitWarningForManagedFieldAccess( component, field, Constants.AUTO_OBSERVE_CLASSNAME );
1202    MemberChecks.mustBeFinal( Constants.AUTO_OBSERVE_CLASSNAME, field );
1203    final boolean validateTypeAtRuntime = isAutoObserveValidateTypeAtRuntime( field );
1204    mustBeAutoObserveTypeCompatible( component, validateTypeAtRuntime, field );
1205    verifyFieldHasExplicitNullabilityAnnotation( field, Constants.AUTO_OBSERVE_CLASSNAME );
1206    component.addAutoObserve( new AutoObserveDescriptor( validateTypeAtRuntime, field ) );
1207  }
1208
1209  private void verifyFieldHasExplicitNullabilityAnnotation( @Nonnull final VariableElement field,
1210                                                            @Nonnull final String annotationClassname )
1211  {
1212    final boolean hasNonnullAnnotation = isElementAnnotatedBy( field, AnnotationsUtil.NONNULL_CLASSNAME );
1213    final boolean hasNullableAnnotation = isElementAnnotatedBy( field, AnnotationsUtil.NULLABLE_CLASSNAME );
1214    final String annotationName = annotationClassname.substring( annotationClassname.lastIndexOf( '.' ) + 1 );
1215    if ( hasNonnullAnnotation && hasNullableAnnotation )
1216    {
1217      throw new ProcessorException( "@" + annotationName + " target must not be annotated with both " +
1218                                    AnnotationsUtil.NULLABLE_CLASSNAME + " and " +
1219                                    AnnotationsUtil.NONNULL_CLASSNAME,
1220                                    field );
1221    }
1222    if ( !hasNonnullAnnotation && !hasNullableAnnotation )
1223    {
1224      throw new ProcessorException( "@" + annotationName + " target must be annotated with either " +
1225                                    AnnotationsUtil.NULLABLE_CLASSNAME + " or " +
1226                                    AnnotationsUtil.NONNULL_CLASSNAME,
1227                                    field );
1228    }
1229  }
1230
1231  @Nonnull
1232  private String suppressedBy( @Nonnull final String warning )
1233  {
1234    return MemberChecks.suppressedBy( warning, Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME );
1235  }
1236
1237  private boolean isWarningNotSuppressed( @Nonnull final Element element, @Nonnull final String warning )
1238  {
1239    return !ElementsUtil.isWarningSuppressed( element,
1240                                              warning,
1241                                              Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME );
1242  }
1243
1244  @SuppressWarnings( "SameParameterValue" )
1245  @Nonnull
1246  private String extractName( @Nonnull final ExecutableElement method,
1247                              @Nonnull final Function<ExecutableElement, String> function,
1248                              @Nonnull final String annotationClassname )
1249  {
1250    return AnnotationsUtil.extractName( method, function, annotationClassname, "name", Constants.SENTINEL );
1251  }
1252
1253  private void mustBeCascadeDisposeTypeCompatible( @Nonnull final VariableElement field )
1254  {
1255    final TypeMirror typeMirror = field.asType();
1256    if ( !isAssignable( typeMirror, getDisposableTypeElement() ) )
1257    {
1258      final TypeElement typeElement = (TypeElement) processingEnv.getTypeUtils().asElement( typeMirror );
1259      final AnnotationMirror value =
1260        null != typeElement ?
1261        AnnotationsUtil.findAnnotationByType( typeElement, Constants.COMPONENT_CLASSNAME ) :
1262        null;
1263      if ( null == value )
1264      {
1265        throw new ProcessorException( "@CascadeDispose target must be assignable to " +
1266                                      Constants.DISPOSABLE_CLASSNAME + " or a type annotated with @ArezComponent",
1267                                      field );
1268      }
1269    }
1270  }
1271
1272  private void mustBeAutoObserveTypeCompatible( @Nonnull final ComponentDescriptor component,
1273                                                final boolean validateTypeAtRuntime,
1274                                                @Nonnull final VariableElement field )
1275  {
1276    final TypeMirror type = processingEnv.getTypeUtils().asMemberOf( component.asDeclaredType(), field );
1277    if ( TypeKind.TYPEVAR != type.getKind() && TypeKind.DECLARED != type.getKind() )
1278    {
1279      throw new ProcessorException( "@AutoObserve target must be a non-primitive value", field );
1280    }
1281    if ( validateTypeAtRuntime )
1282    {
1283      final Element element = processingEnv.getTypeUtils().asElement( type );
1284      if ( !( element instanceof TypeElement ) || !isArezComponentLikeAnnotated( (TypeElement) element ) )
1285      {
1286        throw new ProcessorException( "@AutoObserve target specified validateTypeAtRuntime = true but the " +
1287                                      "declared type is not annotated with " + AREZ_COMPONENT_LIKE_DESCRIPTION, field );
1288      }
1289    }
1290    else if ( !isAutoObserveCompileTimeCompatible( type ) )
1291    {
1292      throw new ProcessorException( "@AutoObserve target must be an instance compatible with " +
1293                                    Constants.COMPONENT_OBSERVABLE_CLASSNAME, field );
1294    }
1295  }
1296
1297  private void addCascadeDisposeMethod( @Nonnull final ComponentDescriptor component,
1298                                        @Nonnull final ExecutableElement method,
1299                                        @Nullable final ObservableDescriptor observable )
1300  {
1301    MemberChecks.mustNotHaveAnyParameters( Constants.CASCADE_DISPOSE_CLASSNAME, method );
1302    MemberChecks.mustNotThrowAnyExceptions( Constants.CASCADE_DISPOSE_CLASSNAME, method );
1303    MemberChecks.mustBeSubclassCallable( component.getElement(),
1304                                         Constants.COMPONENT_CLASSNAME,
1305                                         Constants.CASCADE_DISPOSE_CLASSNAME,
1306                                         method );
1307    mustBeCascadeDisposeTypeCompatible( method );
1308    emitWarningForConflictingDisposeModel( method );
1309    component.addCascadeDispose( new CascadeDisposeDescriptor( method, observable ) );
1310  }
1311
1312  private void addAutoObserveMethod( @Nonnull final ComponentDescriptor component,
1313                                     @Nonnull final ExecutableElement method,
1314                                     @Nullable final ObservableDescriptor observable )
1315  {
1316    MemberChecks.mustNotHaveAnyParameters( Constants.AUTO_OBSERVE_CLASSNAME, method );
1317    MemberChecks.mustNotThrowAnyExceptions( Constants.AUTO_OBSERVE_CLASSNAME, method );
1318    MemberChecks.mustBeSubclassCallable( component.getElement(),
1319                                         Constants.COMPONENT_CLASSNAME,
1320                                         Constants.AUTO_OBSERVE_CLASSNAME,
1321                                         method );
1322    MemberChecks.mustReturnAValue( Constants.AUTO_OBSERVE_CLASSNAME, method );
1323    final boolean validateTypeAtRuntime = isAutoObserveValidateTypeAtRuntime( method );
1324    mustBeAutoObserveTypeCompatible( validateTypeAtRuntime, method );
1325    component.addAutoObserve( new AutoObserveDescriptor( validateTypeAtRuntime, method, observable ) );
1326  }
1327
1328  private static boolean isAutoObserveValidateTypeAtRuntime( @Nonnull final AnnotatedConstruct annotatedConstruct )
1329  {
1330    return Boolean.TRUE.equals( AnnotationsUtil
1331                                  .getAnnotationValue( annotatedConstruct,
1332                                                       Constants.AUTO_OBSERVE_CLASSNAME,
1333                                                       "validateTypeAtRuntime" )
1334                                  .getValue() );
1335  }
1336
1337  private void emitWarningForConflictingDisposeModel( @Nonnull final VariableElement field )
1338  {
1339    if ( isWarningNotSuppressed( field, Constants.WARNING_CONFLICTING_DISPOSE_MODEL ) &&
1340         isLivenessDisposedArezComponent( field.asType() ) )
1341    {
1342      final String message =
1343        "Field named '" + field.getSimpleName() + "' is annotated with @" + Constants.CASCADE_DISPOSE_CLASSNAME +
1344        " but has a type that is an Arez component configured with disposeOnDeactivate = true. " +
1345        "Disposal should be managed either by liveness (i.e. disposeOnDeactivate = true) or explicitly " +
1346        "(via @CascadeDispose or manual disposal), but not both. Please choose a single disposal model " +
1347        "or suppress the warning by annotating the field with @SuppressWarnings( \"" +
1348        Constants.WARNING_CONFLICTING_DISPOSE_MODEL + "\" ) or @SuppressArezWarnings( \"" +
1349        Constants.WARNING_CONFLICTING_DISPOSE_MODEL + "\" )";
1350      warning( message, field );
1351    }
1352  }
1353
1354  private void emitWarningForConflictingDisposeModel( @Nonnull final ExecutableElement method )
1355  {
1356    if ( isWarningNotSuppressed( method, Constants.WARNING_CONFLICTING_DISPOSE_MODEL ) &&
1357         isLivenessDisposedArezComponent( method.getReturnType() ) )
1358    {
1359      final String message =
1360        "Method named '" + method.getSimpleName() + "' is annotated with @" + Constants.CASCADE_DISPOSE_CLASSNAME +
1361        " but returns an Arez component configured with disposeOnDeactivate = true. Disposal should be managed " +
1362        "either by liveness (i.e. disposeOnDeactivate = true) or explicitly (via @CascadeDispose or manual " +
1363        "disposal), but not both. Please choose a single disposal model or suppress the warning by annotating " +
1364        "the method with @SuppressWarnings( \"" + Constants.WARNING_CONFLICTING_DISPOSE_MODEL +
1365        "\" ) or @SuppressArezWarnings( \"" + Constants.WARNING_CONFLICTING_DISPOSE_MODEL + "\" )";
1366      warning( message, method );
1367    }
1368  }
1369
1370  private void mustBeCascadeDisposeTypeCompatible( @Nonnull final ExecutableElement method )
1371  {
1372    final TypeMirror typeMirror = method.getReturnType();
1373    if ( !isAssignable( typeMirror, getDisposableTypeElement() ) )
1374    {
1375      final TypeElement typeElement = (TypeElement) processingEnv.getTypeUtils().asElement( typeMirror );
1376      final AnnotationMirror value =
1377        null != typeElement ?
1378        AnnotationsUtil.findAnnotationByType( typeElement, Constants.COMPONENT_CLASSNAME ) :
1379        null;
1380      if ( null == value )
1381      {
1382        //The type of the field must implement {@link arez.Disposable} or must be annotated by {@link ArezComponent}
1383        throw new ProcessorException( "@CascadeDispose target must return a type assignable to " +
1384                                      Constants.DISPOSABLE_CLASSNAME + " or a type annotated with @ArezComponent",
1385                                      method );
1386      }
1387    }
1388  }
1389
1390  private void mustBeAutoObserveTypeCompatible( final boolean validateTypeAtRuntime,
1391                                                @Nonnull final ExecutableElement method )
1392  {
1393    final TypeMirror type = method.getReturnType();
1394    if ( TypeKind.TYPEVAR != type.getKind() && TypeKind.DECLARED != type.getKind() )
1395    {
1396      throw new ProcessorException( "@AutoObserve target must return a non-primitive value", method );
1397    }
1398    if ( validateTypeAtRuntime )
1399    {
1400      final Element element = processingEnv.getTypeUtils().asElement( type );
1401      if ( !( element instanceof TypeElement ) || !isArezComponentLikeAnnotated( (TypeElement) element ) )
1402      {
1403        throw new ProcessorException( "@AutoObserve target specified validateTypeAtRuntime = true but the " +
1404                                      "declared return type is not annotated with " + AREZ_COMPONENT_LIKE_DESCRIPTION,
1405                                      method );
1406      }
1407    }
1408    else if ( !isAutoObserveCompileTimeCompatible( type ) )
1409    {
1410      throw new ProcessorException( "@AutoObserve target must return an instance compatible with " +
1411                                    Constants.COMPONENT_OBSERVABLE_CLASSNAME, method );
1412    }
1413  }
1414
1415  @SuppressWarnings( "BooleanMethodIsAlwaysInverted" )
1416  private boolean isAutoObserveCompileTimeCompatible( @Nonnull final TypeMirror type )
1417  {
1418    if ( isAssignable( type, getTypeElement( Constants.COMPONENT_OBSERVABLE_CLASSNAME ) ) )
1419    {
1420      return true;
1421    }
1422
1423    final Element element = processingEnv.getTypeUtils().asElement( type );
1424    if ( element instanceof TypeElement typeElement )
1425    {
1426      final AnnotationMirror arezComponent =
1427        AnnotationsUtil.findAnnotationByType( typeElement, Constants.COMPONENT_CLASSNAME );
1428      if ( null != arezComponent )
1429      {
1430        final boolean disposeOnDeactivate = getAnnotationParameter( arezComponent, "disposeOnDeactivate" );
1431        return isComponentObservableRequired( arezComponent, disposeOnDeactivate );
1432      }
1433    }
1434    return false;
1435  }
1436
1437  private void addOrUpdateDependency( @Nonnull final ComponentDescriptor component,
1438                                      @Nonnull final ExecutableElement method,
1439                                      @Nonnull final ObservableDescriptor observable )
1440  {
1441    final DependencyDescriptor dependencyDescriptor =
1442      component.getDependencies().computeIfAbsent( method, m -> createMethodDependencyDescriptor( component, method ) );
1443    dependencyDescriptor.setObservable( observable );
1444  }
1445
1446  private void addAction( @Nonnull final ComponentDescriptor component,
1447                          @Nonnull final AnnotationMirror annotation,
1448                          @Nonnull final ExecutableElement method,
1449                          @Nonnull final ExecutableType methodType )
1450    throws ProcessorException
1451  {
1452    MemberChecks.mustBeWrappable( component.getElement(),
1453                                  Constants.COMPONENT_CLASSNAME,
1454                                  Constants.ACTION_CLASSNAME,
1455                                  method );
1456
1457    final String name =
1458      extractName( method, m -> m.getSimpleName().toString(), Constants.ACTION_CLASSNAME );
1459    checkNameUnique( component, name, method, Constants.ACTION_CLASSNAME );
1460    final boolean mutation = AnnotationsUtil.getAnnotationValueValue( annotation, "mutation" );
1461    final boolean requireNewTransaction =
1462      AnnotationsUtil.getAnnotationValueValue( annotation, "requireNewTransaction" );
1463    final boolean reportParameters = AnnotationsUtil.getAnnotationValueValue( annotation, "reportParameters" );
1464    final boolean reportResult = AnnotationsUtil.getAnnotationValueValue( annotation, "reportResult" );
1465    final boolean verifyRequired = AnnotationsUtil.getAnnotationValueValue( annotation, "verifyRequired" );
1466    final boolean skipIfDisposed = isSkipIfDisposed( component, annotation );
1467    if ( !reportParameters && method.getParameters().isEmpty() )
1468    {
1469      throw new ProcessorException( "@Action target must not specify reportParameters parameter " +
1470                                    "when no parameters are present", method );
1471    }
1472    if ( skipIfDisposed && TypeKind.VOID != methodType.getReturnType().getKind() )
1473    {
1474      throw new ProcessorException( "@Action target must not return a value when skipIfDisposed resolves to ENABLE",
1475                                    method );
1476    }
1477    final ActionDescriptor action =
1478      new ActionDescriptor( component,
1479                            name,
1480                            requireNewTransaction,
1481                            mutation,
1482                            verifyRequired,
1483                            reportParameters,
1484                            reportResult,
1485                            skipIfDisposed,
1486                            method,
1487                            methodType );
1488    component.getActions().put( action.getName(), action );
1489  }
1490
1491  private void addRequiresTransaction( @Nonnull final ComponentDescriptor component,
1492                                       @Nonnull final AnnotationMirror annotation,
1493                                       @Nonnull final ExecutableElement method,
1494                                       @Nonnull final ExecutableType methodType )
1495    throws ProcessorException
1496  {
1497    MemberChecks.mustBeWrappable( component.getElement(),
1498                                  Constants.COMPONENT_CLASSNAME,
1499                                  Constants.REQUIRES_TRANSACTION_CLASSNAME,
1500                                  method );
1501
1502    final VariableElement mode = AnnotationsUtil.getAnnotationValueValue( annotation, "mode" );
1503    final VariableElement tracking = AnnotationsUtil.getAnnotationValueValue( annotation, "tracking" );
1504    component.getRequiresTransactions().add( new RequiresTransactionDescriptor( component,
1505                                                                                mode.getSimpleName().toString(),
1506                                                                                tracking.getSimpleName().toString(),
1507                                                                                method,
1508                                                                                methodType ) );
1509  }
1510
1511  private void addObserve( @Nonnull final ComponentDescriptor component,
1512                           @Nonnull final AnnotationMirror annotation,
1513                           @Nonnull final ExecutableElement method,
1514                           @Nonnull final ExecutableType methodType )
1515    throws ProcessorException
1516  {
1517    final String name = deriveObserveName( method, annotation );
1518    checkNameUnique( component, name, method, Constants.OBSERVE_CLASSNAME );
1519    final boolean mutation = AnnotationsUtil.getAnnotationValueValue( annotation, "mutation" );
1520    final boolean observeLowerPriorityDependencies =
1521      AnnotationsUtil.getAnnotationValueValue( annotation, "observeLowerPriorityDependencies" );
1522    final boolean nestedActionsAllowed = AnnotationsUtil.getAnnotationValueValue( annotation, "nestedActionsAllowed" );
1523    final VariableElement priority = AnnotationsUtil.getAnnotationValueValue( annotation, "priority" );
1524    final boolean reportParameters = AnnotationsUtil.getAnnotationValueValue( annotation, "reportParameters" );
1525    final boolean reportResult = AnnotationsUtil.getAnnotationValueValue( annotation, "reportResult" );
1526    final VariableElement executor = AnnotationsUtil.getAnnotationValueValue( annotation, "executor" );
1527    final VariableElement depType = AnnotationsUtil.getAnnotationValueValue( annotation, "depType" );
1528
1529    component
1530      .findOrCreateObserve( name )
1531      .setObserveMethod( mutation,
1532                         toPriority( component.getDefaultPriority(), priority ),
1533                         executor.getSimpleName().toString().equals( "INTERNAL" ),
1534                         reportParameters,
1535                         reportResult,
1536                         depType.getSimpleName().toString(),
1537                         observeLowerPriorityDependencies,
1538                         nestedActionsAllowed,
1539                         method,
1540                         methodType );
1541  }
1542
1543  @Nonnull
1544  private String deriveObserveName( @Nonnull final ExecutableElement method,
1545                                    @Nonnull final AnnotationMirror annotation )
1546    throws ProcessorException
1547  {
1548    final String name = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
1549    if ( Constants.SENTINEL.equals( name ) )
1550    {
1551      return method.getSimpleName().toString();
1552    }
1553    else
1554    {
1555      if ( !SourceVersion.isIdentifier( name ) )
1556      {
1557        throw new ProcessorException( "@Observe target specified an invalid name '" + name + "'. The " +
1558                                      "name must be a valid java identifier.", method );
1559      }
1560      else if ( SourceVersion.isKeyword( name ) )
1561      {
1562        throw new ProcessorException( "@Observe target specified an invalid name '" + name + "'. The " +
1563                                      "name must not be a java keyword.", method );
1564      }
1565      return name;
1566    }
1567  }
1568
1569  private void addOnDepsChange( @Nonnull final ComponentDescriptor component,
1570                                @Nonnull final AnnotationMirror annotation,
1571                                @Nonnull final ExecutableElement method )
1572    throws ProcessorException
1573  {
1574    final String name =
1575      deriveHookName( component, method,
1576                      ON_DEPS_CHANGE_PATTERN,
1577                      "DepsChange",
1578                      AnnotationsUtil.getAnnotationValueValue( annotation, "name" ) );
1579    setOnDepsChange( component, component.findOrCreateObserve( name ), method );
1580  }
1581
1582  private void addObserverRef( @Nonnull final ComponentDescriptor component,
1583                               @Nonnull final AnnotationMirror annotation,
1584                               @Nonnull final ExecutableElement method,
1585                               @Nonnull final ExecutableType methodType )
1586    throws ProcessorException
1587  {
1588    mustBeStandardRefMethod( processingEnv,
1589                             component,
1590                             method,
1591                             Constants.OBSERVER_REF_CLASSNAME );
1592    MemberChecks.mustReturnAnInstanceOf( processingEnv,
1593                                         method,
1594                                         Constants.OBSERVER_REF_CLASSNAME,
1595                                         Constants.OBSERVER_CLASSNAME );
1596
1597    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
1598    final String name;
1599    if ( Constants.SENTINEL.equals( declaredName ) )
1600    {
1601      name = deriveName( method, OBSERVER_REF_PATTERN, declaredName );
1602      if ( null == name )
1603      {
1604        throw new ProcessorException( "Method annotated with @ObserverRef should specify name or be " +
1605                                      "named according to the convention get[Name]Observer", method );
1606      }
1607    }
1608    else
1609    {
1610      name = declaredName;
1611      if ( !SourceVersion.isIdentifier( name ) )
1612      {
1613        throw new ProcessorException( "@ObserverRef target specified an invalid name '" + name + "'. The " +
1614                                      "name must be a valid java identifier.", method );
1615      }
1616      else if ( SourceVersion.isKeyword( name ) )
1617      {
1618        throw new ProcessorException( "@ObserverRef target specified an invalid name '" + name + "'. The " +
1619                                      "name must not be a java keyword.", method );
1620      }
1621    }
1622    component.getObserverRefs().computeIfAbsent( name, s -> new ArrayList<>() )
1623      .add( new CandidateMethod( method, methodType ) );
1624  }
1625
1626  private void addMemoizeContextParameter( @Nonnull final ComponentDescriptor component,
1627                                           @Nonnull final AnnotationMirror annotation,
1628                                           @Nonnull final ExecutableElement method,
1629                                           @Nonnull final ExecutableType methodType )
1630    throws ProcessorException
1631  {
1632    final String methodName = method.getSimpleName().toString();
1633    final MemoizeContextParameterMethodType mcpMethodType =
1634      PUSH_PATTERN.matcher( methodName ).matches() ? MemoizeContextParameterMethodType.Push :
1635      POP_PATTERN.matcher( methodName ).matches() ? MemoizeContextParameterMethodType.Pop :
1636      MemoizeContextParameterMethodType.Capture;
1637    final String name = deriveMemoizeContextParameterName( method, annotation, mcpMethodType );
1638
1639    checkNameUnique( component, name, method, Constants.MEMOIZE_CONTEXT_PARAMETER_CLASSNAME );
1640    final boolean allowEmpty = AnnotationsUtil.getAnnotationValueValue( annotation, "allowEmpty" );
1641    final String pattern = AnnotationsUtil.getAnnotationValueValue( annotation, "pattern" );
1642    final MemoizeContextParameterDescriptor descriptor = component.findOrCreateMemoizeContextParameter( name );
1643
1644    final Pattern compiledPattern;
1645    try
1646    {
1647      compiledPattern = Pattern.compile( pattern );
1648    }
1649    catch ( final PatternSyntaxException e )
1650    {
1651      throw new ProcessorException( "@MemoizeContextParameter target specified a pattern parameter " +
1652                                    "that is not a valid regular expression.", method );
1653    }
1654
1655    if ( MemoizeContextParameterMethodType.Capture == mcpMethodType )
1656    {
1657      descriptor.setCapture( method, methodType, allowEmpty, pattern, compiledPattern );
1658    }
1659    else if ( MemoizeContextParameterMethodType.Push == mcpMethodType )
1660    {
1661      descriptor.setPush( method, methodType, allowEmpty, pattern, compiledPattern );
1662    }
1663    else // MemoizeContextParameterMethodType.Pop == mcpMethodType
1664    {
1665      descriptor.setPop( method, methodType, allowEmpty, pattern, compiledPattern );
1666    }
1667  }
1668
1669  @Nonnull
1670  private String deriveMemoizeContextParameterName( @Nonnull final ExecutableElement method,
1671                                                    @Nonnull final AnnotationMirror annotation,
1672                                                    @Nonnull final MemoizeContextParameterMethodType mcpMethodType )
1673    throws ProcessorException
1674  {
1675    final String name = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
1676    if ( Constants.SENTINEL.equals( name ) )
1677    {
1678      final Pattern pattern =
1679        MemoizeContextParameterMethodType.Push == mcpMethodType ? PUSH_PATTERN :
1680        MemoizeContextParameterMethodType.Pop == mcpMethodType ? POP_PATTERN :
1681        CAPTURE_PATTERN;
1682      final String methodName = method.getSimpleName().toString();
1683      final Matcher matcher = pattern.matcher( methodName );
1684      if ( matcher.find() )
1685      {
1686        final String candidate = matcher.group( 1 );
1687        return firstCharacterToLowerCase( candidate );
1688      }
1689      else
1690      {
1691        // we get here for a capture method that does not start with capture
1692        return methodName;
1693      }
1694    }
1695    else
1696    {
1697      if ( !SourceVersion.isIdentifier( name ) )
1698      {
1699        throw new ProcessorException( "@MemoizeContextParameter target specified an invalid name '" + name +
1700                                      "'. The name must be a valid java identifier.", method );
1701      }
1702      else if ( SourceVersion.isKeyword( name ) )
1703      {
1704        throw new ProcessorException( "@MemoizeContextParameter target specified an invalid name '" + name +
1705                                      "'. The name must not be a java keyword.", method );
1706      }
1707      return name;
1708    }
1709  }
1710
1711  private void addMemoize( @Nonnull final ComponentDescriptor component,
1712                           @Nonnull final AnnotationMirror annotation,
1713                           @Nonnull final ExecutableElement method,
1714                           @Nonnull final ExecutableType methodType )
1715    throws ProcessorException
1716  {
1717    final String name = deriveMemoizeName( method, annotation );
1718    checkNameUnique( component, name, method, Constants.MEMOIZE_CLASSNAME );
1719    final boolean keepAlive = AnnotationsUtil.getAnnotationValueValue( annotation, "keepAlive" );
1720    final boolean reportResult = AnnotationsUtil.getAnnotationValueValue( annotation, "reportResult" );
1721    final boolean observeLowerPriorityDependencies =
1722      AnnotationsUtil.getAnnotationValueValue( annotation, "observeLowerPriorityDependencies" );
1723    final VariableElement readOutsideTransaction =
1724      AnnotationsUtil.getAnnotationValueValue( annotation, "readOutsideTransaction" );
1725    final VariableElement priority = AnnotationsUtil.getAnnotationValueValue( annotation, "priority" );
1726    final VariableElement depType = AnnotationsUtil.getAnnotationValueValue( annotation, "depType" );
1727    final TypeMirror equalityComparator =
1728      AnnotationsUtil.getAnnotationValueValue( annotation, "equalityComparator" );
1729
1730    final String depTypeAsString = depType.getSimpleName().toString();
1731    component.findOrCreateMemoize( name ).setMemoize( method,
1732                                                      methodType,
1733                                                      keepAlive,
1734                                                      toPriority( component.getDefaultPriority(),
1735                                                                  priority ),
1736                                                      reportResult,
1737                                                      observeLowerPriorityDependencies,
1738                                                      readOutsideTransaction.getSimpleName().toString(),
1739                                                      depTypeAsString,
1740                                                      resolveEffectiveEqualityComparator( component.getElement(),
1741                                                                                          Constants.MEMOIZE_CLASSNAME,
1742                                                                                          method,
1743                                                                                          methodType.getReturnType(),
1744                                                                                          equalityComparator.toString() ) );
1745  }
1746
1747  @Nonnull
1748  private Priority toPriority( @Nullable final Priority defaultPriority,
1749                               @Nonnull final VariableElement priorityElement )
1750  {
1751    final String priorityName = priorityElement.getSimpleName().toString();
1752    return "DEFAULT".equals( priorityName ) ?
1753           null != defaultPriority ? defaultPriority : Priority.NORMAL :
1754           Priority.valueOf( priorityName );
1755  }
1756
1757  private void autodetectObservableInitializers( @Nonnull final ComponentDescriptor component )
1758  {
1759    for ( final ObservableDescriptor observable : component.getObservables().values() )
1760    {
1761      if ( null == observable.getInitializer() && observable.hasGetter() )
1762      {
1763        if ( observable.hasSetter() )
1764        {
1765          final boolean initializer =
1766            autodetectInitializer( observable.getGetter() ) && autodetectInitializer( observable.getSetter() );
1767          observable.setInitializer( initializer );
1768        }
1769        else
1770        {
1771          observable.setInitializer( autodetectInitializer( observable.getGetter() ) );
1772        }
1773      }
1774    }
1775  }
1776
1777  private boolean hasDependencyAnnotation( @Nonnull final ExecutableElement method )
1778  {
1779    return AnnotationsUtil.hasAnnotationOfType( method, Constants.COMPONENT_DEPENDENCY_CLASSNAME );
1780  }
1781
1782  private void ensureTargetTypeAligns( @Nonnull final ComponentDescriptor component,
1783                                       @Nonnull final InverseDescriptor descriptor,
1784                                       @Nonnull final TypeMirror target )
1785  {
1786    if ( !processingEnv.getTypeUtils().isSameType( target, component.getElement().asType() ) )
1787    {
1788      throw new ProcessorException( "@Inverse target expected to find an associated @Reference annotation with " +
1789                                    "a target type equal to " + component.getElement().asType() + " but the actual " +
1790                                    "target type is " + target, descriptor.getObservable().getGetter() );
1791    }
1792  }
1793
1794  @Nullable
1795  private TypeElement getInverseManyTypeTarget( @Nonnull final ExecutableElement method )
1796  {
1797    final TypeName typeName = TypeName.get( method.getReturnType() );
1798    if ( typeName instanceof final ParameterizedTypeName type )
1799    {
1800      if ( isSupportedInverseCollectionType( type.rawType().toString() ) && !type.typeArguments().isEmpty() )
1801      {
1802        final TypeElement typeElement = getTypeElement( type.typeArguments().get( 0 ).toString() );
1803        if ( AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.COMPONENT_CLASSNAME ) )
1804        {
1805          return typeElement;
1806        }
1807        else
1808        {
1809          throw new ProcessorException( "@Inverse target expected to return a type annotated with " +
1810                                        Constants.COMPONENT_CLASSNAME, method );
1811        }
1812      }
1813    }
1814    return null;
1815  }
1816
1817  private boolean isSupportedInverseCollectionType( @Nonnull final String typeClassname )
1818  {
1819    return Collection.class.getName().equals( typeClassname ) ||
1820           Set.class.getName().equals( typeClassname ) ||
1821           List.class.getName().equals( typeClassname );
1822  }
1823
1824  @Nonnull
1825  private String getInverseReferenceNameParameter( @Nonnull final ComponentDescriptor component,
1826                                                   @Nonnull final ExecutableElement method )
1827  {
1828    final String declaredName =
1829      (String) AnnotationsUtil.getAnnotationValue( method,
1830                                                   Constants.INVERSE_CLASSNAME,
1831                                                   "referenceName" ).getValue();
1832    final String name;
1833    if ( Constants.SENTINEL.equals( declaredName ) )
1834    {
1835      name = firstCharacterToLowerCase( component.getElement().getSimpleName().toString() );
1836    }
1837    else
1838    {
1839      name = declaredName;
1840      if ( !SourceVersion.isIdentifier( name ) )
1841      {
1842        throw new ProcessorException( "@Inverse target specified an invalid referenceName '" + name + "'. The " +
1843                                      "name must be a valid java identifier.", method );
1844      }
1845      else if ( SourceVersion.isKeyword( name ) )
1846      {
1847        throw new ProcessorException( "@Inverse target specified an invalid referenceName '" + name + "'. The " +
1848                                      "name must not be a java keyword.", method );
1849      }
1850    }
1851    return name;
1852  }
1853
1854  private void linkDependencies( @Nonnull final ComponentDescriptor component,
1855                                 @Nonnull final Collection<CandidateMethod> candidates )
1856  {
1857    component.getObservables()
1858      .values()
1859      .stream()
1860      .filter( ObservableDescriptor::hasGetter )
1861      .filter( o -> hasDependencyAnnotation( o.getGetter() ) )
1862      .forEach( o -> addOrUpdateDependency( component, o.getGetter(), o ) );
1863
1864    component.getMemoizes()
1865      .values()
1866      .stream()
1867      .filter( MemoizeDescriptor::hasMemoize )
1868      .map( MemoizeDescriptor::getMethod )
1869      .filter( this::hasDependencyAnnotation )
1870      .forEach( method1 -> component.addDependency( createMethodDependencyDescriptor( component, method1 ) ) );
1871
1872    candidates
1873      .stream()
1874      .map( CandidateMethod::getMethod )
1875      .filter( this::hasDependencyAnnotation )
1876      .forEach( method -> component.addDependency( createMethodDependencyDescriptor( component, method ) ) );
1877  }
1878
1879  private void linkCascadeDisposeObservables( @Nonnull final ComponentDescriptor component )
1880  {
1881    for ( final ObservableDescriptor observable : component.getObservables().values() )
1882    {
1883      final CascadeDisposeDescriptor cascadeDisposeDescriptor = observable.getCascadeDisposeDescriptor();
1884      if ( null == cascadeDisposeDescriptor )
1885      {
1886        //@CascadeDisposable can only occur on getter so if we don't have it then we look in
1887        // cascadeDisposableDescriptor list to see if we can match getter
1888        final CascadeDisposeDescriptor descriptor = component.getCascadeDisposes().get( observable.getGetter() );
1889        if ( null != descriptor )
1890        {
1891          descriptor.setObservable( observable );
1892        }
1893      }
1894    }
1895  }
1896
1897  private void linkCascadeDisposeReferences( @Nonnull final ComponentDescriptor component )
1898  {
1899    for ( final ReferenceDescriptor reference : component.getReferences().values() )
1900    {
1901      final CascadeDisposeDescriptor cascadeDisposeDescriptor = reference.getCascadeDisposeDescriptor();
1902      if ( null == cascadeDisposeDescriptor && reference.hasMethod() )
1903      {
1904        final CascadeDisposeDescriptor descriptor = component.getCascadeDisposes().get( reference.getMethod() );
1905        if ( null != descriptor )
1906        {
1907          descriptor.setReference( reference );
1908        }
1909      }
1910    }
1911  }
1912
1913  private void linkAutoObserveObservables( @Nonnull final ComponentDescriptor component )
1914  {
1915    for ( final ObservableDescriptor observable : component.getObservables().values() )
1916    {
1917      final AutoObserveDescriptor autoObserveDescriptor = observable.getAutoObserveDescriptor();
1918      if ( null == autoObserveDescriptor )
1919      {
1920        final AutoObserveDescriptor descriptor = component.getAutoObserves().get( observable.getGetter() );
1921        if ( null != descriptor )
1922        {
1923          descriptor.setObservable( observable );
1924        }
1925      }
1926    }
1927  }
1928
1929  private void linkAutoObserveReferences( @Nonnull final ComponentDescriptor component )
1930  {
1931    for ( final ReferenceDescriptor reference : component.getReferences().values() )
1932    {
1933      final AutoObserveDescriptor autoObserveDescriptor = reference.getAutoObserveDescriptor();
1934      if ( null == autoObserveDescriptor && reference.hasMethod() )
1935      {
1936        final AutoObserveDescriptor descriptor = component.getAutoObserves().get( reference.getMethod() );
1937        if ( null != descriptor )
1938        {
1939          descriptor.setReference( reference );
1940        }
1941      }
1942    }
1943  }
1944
1945  private void linkObserverRefs( @Nonnull final ComponentDescriptor component )
1946  {
1947    for ( final Map.Entry<String, List<CandidateMethod>> entry : component.getObserverRefs().entrySet() )
1948    {
1949      final String key = entry.getKey();
1950      final List<CandidateMethod> methods = entry.getValue();
1951      final ObserveDescriptor observed = component.getObserves().get( key );
1952      if ( null != observed )
1953      {
1954        methods.stream().map( CandidateMethod::getMethod ).forEach( observed::addRefMethod );
1955      }
1956      else
1957      {
1958        throw new ProcessorException( "@ObserverRef target defined observer named '" + key + "' but no " +
1959                                      "@Observe method with that name exists", methods.get( 0 ).getMethod() );
1960      }
1961    }
1962  }
1963
1964  private void linkObservableInitials( @Nonnull final ComponentDescriptor component )
1965  {
1966    for ( final ObservableInitialDescriptor observableInitial : component.getObservableInitials().values() )
1967    {
1968      final String name = observableInitial.getName();
1969      final ObservableDescriptor observable = component.getObservables().get( name );
1970      if ( null == observable )
1971      {
1972        throw new ProcessorException( "@ObservableInitial target defined observable named '" + name + "' but no " +
1973                                      "@Observable method with that name exists", observableInitial.getElement() );
1974      }
1975      if ( !observable.hasGetter() )
1976      {
1977        throw new ProcessorException( "@ObservableInitial target defined observable named '" + name + "' but the " +
1978                                      "observable does not define a getter", observableInitial.getElement() );
1979      }
1980      if ( !observable.isAbstract() )
1981      {
1982        throw new ProcessorException( "@ObservableInitial target defined observable named '" + name + "' but the " +
1983                                      "observable is not abstract", observableInitial.getElement() );
1984      }
1985
1986      final TypeMirror observableType = observable.getGetterType().getReturnType();
1987      final TypeMirror initialType = observableInitial.getType();
1988      if ( !processingEnv.getTypeUtils().isSameType( initialType, observableType ) &&
1989           !initialType.toString().equals( observableType.toString() ) )
1990      {
1991        throw new ProcessorException( "@ObservableInitial target defined observable named '" + name +
1992                                      "' with incompatible type. Observable type: " + observableType +
1993                                      " Initial type: " + initialType + ".", observableInitial.getElement() );
1994      }
1995      if ( observable.isGetterNonnull() && !AnnotationsUtil.hasNonnullAnnotation( observableInitial.getElement() ) )
1996      {
1997        throw new ProcessorException( "@ObservableInitial target defined observable named '" + name + "' but " +
1998                                      "the initializer is not annotated with @" + AnnotationsUtil.NONNULL_CLASSNAME,
1999                                      observableInitial.getElement() );
2000      }
2001
2002      final Boolean initializer = observable.getInitializer();
2003      if ( Boolean.TRUE.equals( initializer ) )
2004      {
2005        throw new ProcessorException( "@ObservableInitial target defined observable named '" + name + "' but " +
2006                                      "the observable defines initializer = Feature.ENABLE which is not " +
2007                                      "compatible with @ObservableInitial", observableInitial.getElement() );
2008      }
2009      if ( null == initializer )
2010      {
2011        observable.setInitializer( Boolean.FALSE );
2012      }
2013      observable.setObservableInitial( observableInitial );
2014    }
2015  }
2016
2017  @Nullable
2018  private Boolean isInitializerRequired( @Nonnull final ExecutableElement element )
2019  {
2020    final AnnotationMirror annotation =
2021      AnnotationsUtil.findAnnotationByType( element, Constants.OBSERVABLE_CLASSNAME );
2022    final AnnotationValue v =
2023      null == annotation ? null : AnnotationsUtil.findAnnotationValueNoDefaults( annotation, "initializer" );
2024    final String value = null == v ? "AUTODETECT" : ( (VariableElement) v.getValue() ).getSimpleName().toString();
2025    return switch ( value )
2026    {
2027      case "ENABLE" -> Boolean.TRUE;
2028      case "DISABLE" -> Boolean.FALSE;
2029      default -> null;
2030    };
2031  }
2032
2033  private boolean autodetectInitializer( @Nonnull final ExecutableElement element )
2034  {
2035    return element.getModifiers().contains( Modifier.ABSTRACT ) &&
2036           (
2037             (
2038               // Getter
2039               element.getReturnType().getKind() != TypeKind.VOID &&
2040               AnnotationsUtil.hasNonnullAnnotation( element ) &&
2041               !AnnotationsUtil.hasAnnotationOfType( element, Constants.INVERSE_CLASSNAME )
2042             ) ||
2043             (
2044               // Setter
2045               1 == element.getParameters().size() &&
2046               AnnotationsUtil.hasNonnullAnnotation( element.getParameters().get( 0 ) )
2047             )
2048           );
2049  }
2050
2051  private void checkNameUnique( @Nonnull final ComponentDescriptor component, @Nonnull final String name,
2052                                @Nonnull final ExecutableElement sourceMethod,
2053                                @Nonnull final String sourceAnnotationName )
2054    throws ProcessorException
2055  {
2056    final ActionDescriptor action = component.getActions().get( name );
2057    if ( null != action )
2058    {
2059      throw toException( name,
2060                         sourceAnnotationName,
2061                         sourceMethod,
2062                         Constants.ACTION_CLASSNAME,
2063                         action.getAction() );
2064    }
2065    final MemoizeDescriptor memoize = component.getMemoizes().get( name );
2066    if ( null != memoize && memoize.hasMemoize() )
2067    {
2068      throw toException( name,
2069                         sourceAnnotationName,
2070                         sourceMethod,
2071                         Constants.MEMOIZE_CLASSNAME,
2072                         memoize.getMethod() );
2073    }
2074    // Observe have pairs so let the caller determine whether a duplicate occurs in that scenario
2075    if ( !sourceAnnotationName.equals( Constants.OBSERVE_CLASSNAME ) )
2076    {
2077      final ObserveDescriptor observed = component.getObserves().get( name );
2078      if ( null != observed )
2079      {
2080        throw toException( name,
2081                           sourceAnnotationName,
2082                           sourceMethod,
2083                           Constants.OBSERVE_CLASSNAME,
2084                           observed.getMethod() );
2085      }
2086    }
2087    // Observables have pairs so let the caller determine whether a duplicate occurs in that scenario
2088    if ( !sourceAnnotationName.equals( Constants.OBSERVABLE_CLASSNAME ) )
2089    {
2090      final ObservableDescriptor observable = component.getObservables().get( name );
2091      if ( null != observable )
2092      {
2093        throw toException( name,
2094                           sourceAnnotationName,
2095                           sourceMethod,
2096                           Constants.OBSERVABLE_CLASSNAME,
2097                           observable.getDefiner() );
2098      }
2099    }
2100  }
2101
2102  @Nonnull
2103  private ProcessorException toException( @Nonnull final String name,
2104                                          @Nonnull final String sourceAnnotationName,
2105                                          @Nonnull final ExecutableElement sourceMethod,
2106                                          @Nonnull final String targetAnnotationName,
2107                                          @Nonnull final ExecutableElement targetElement )
2108  {
2109    return new ProcessorException( "Method annotated with " + MemberChecks.toSimpleName( sourceAnnotationName ) +
2110                                   " specified name " + name + " that duplicates " +
2111                                   MemberChecks.toSimpleName( targetAnnotationName ) + " defined by method " +
2112                                   targetElement.getSimpleName(), sourceMethod );
2113  }
2114
2115  private void processComponentDependencyFields( @Nonnull final ComponentDescriptor component )
2116  {
2117    ElementsUtil.getFields( component.getElement() )
2118      .stream()
2119      .filter( f -> AnnotationsUtil.hasAnnotationOfType( f, Constants.COMPONENT_DEPENDENCY_CLASSNAME ) )
2120      .forEach( field -> processComponentDependencyField( component, field ) );
2121  }
2122
2123  private void processObservableInitialFields( @Nonnull final ComponentDescriptor component,
2124                                               @Nonnull final List<VariableElement> fields )
2125  {
2126    fields
2127      .stream()
2128      .filter( f -> AnnotationsUtil.hasAnnotationOfType( f, Constants.OBSERVABLE_INITIAL_CLASSNAME ) )
2129      .forEach( field -> processObservableInitialField( component, field ) );
2130  }
2131
2132  private void processComponentDependencyField( @Nonnull final ComponentDescriptor component,
2133                                                @Nonnull final VariableElement field )
2134  {
2135    verifyNoDuplicateAnnotations( field );
2136    MemberChecks.mustBeSubclassCallable( component.getElement(),
2137                                         Constants.COMPONENT_CLASSNAME,
2138                                         Constants.COMPONENT_DEPENDENCY_CLASSNAME,
2139                                         field );
2140    component.addDependency( createFieldDependencyDescriptor( component, field ) );
2141  }
2142
2143  private void processObservableInitialField( @Nonnull final ComponentDescriptor component,
2144                                              @Nonnull final VariableElement field )
2145  {
2146    verifyNoDuplicateAnnotations( field );
2147    if ( !field.getModifiers().contains( Modifier.STATIC ) )
2148    {
2149      throw new ProcessorException( "@ObservableInitial target must be static", field );
2150    }
2151    if ( field.getModifiers().contains( Modifier.PRIVATE ) )
2152    {
2153      throw new ProcessorException( "@ObservableInitial target must not be private", field );
2154    }
2155    MemberChecks.mustBeFinal( Constants.OBSERVABLE_INITIAL_CLASSNAME, field );
2156
2157    final AnnotationMirror annotation =
2158      AnnotationsUtil.getAnnotationByType( field, Constants.OBSERVABLE_INITIAL_CLASSNAME );
2159    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
2160    final String name = deriveObservableInitialName( field, declaredName );
2161    if ( null == name )
2162    {
2163      throw new ProcessorException( "Field annotated with @ObservableInitial should specify name or be " +
2164                                    "named according to the convention INITIAL_[Name]", field );
2165    }
2166
2167    addObservableInitial( component, new ObservableInitialDescriptor( name, field ) );
2168  }
2169
2170  private void addObservableInitialMethod( @Nonnull final ComponentDescriptor component,
2171                                           @Nonnull final AnnotationMirror annotation,
2172                                           @Nonnull final ExecutableElement method,
2173                                           @Nonnull final ExecutableType methodType )
2174  {
2175    if ( !method.getModifiers().contains( Modifier.STATIC ) )
2176    {
2177      throw new ProcessorException( "@ObservableInitial target must be static", method );
2178    }
2179    if ( method.getModifiers().contains( Modifier.PRIVATE ) )
2180    {
2181      throw new ProcessorException( "@ObservableInitial target must not be private", method );
2182    }
2183    MemberChecks.mustNotBeAbstract( Constants.OBSERVABLE_INITIAL_CLASSNAME, method );
2184    MemberChecks.mustNotHaveAnyParameters( Constants.OBSERVABLE_INITIAL_CLASSNAME, method );
2185    MemberChecks.mustReturnAValue( Constants.OBSERVABLE_INITIAL_CLASSNAME, method );
2186    MemberChecks.mustNotThrowAnyExceptions( Constants.OBSERVABLE_INITIAL_CLASSNAME, method );
2187
2188    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
2189    final String name = deriveObservableInitialName( method, declaredName );
2190    if ( null == name )
2191    {
2192      throw new ProcessorException( "Method annotated with @ObservableInitial should specify name or be " +
2193                                    "named according to the convention getInitial[Name]", method );
2194    }
2195
2196    addObservableInitial( component, new ObservableInitialDescriptor( name, method, methodType ) );
2197  }
2198
2199  private void addObservableInitial( @Nonnull final ComponentDescriptor component,
2200                                     @Nonnull final ObservableInitialDescriptor descriptor )
2201  {
2202    final String name = descriptor.getName();
2203    if ( component.getObservableInitials().containsKey( name ) )
2204    {
2205      throw new ProcessorException( "@ObservableInitial target duplicates existing initializer for observable " +
2206                                    "named " + name, descriptor.getElement() );
2207    }
2208    component.getObservableInitials().put( name, descriptor );
2209  }
2210
2211  @Nullable
2212  private String deriveObservableInitialName( @Nonnull final ExecutableElement method,
2213                                              @Nonnull final String declaredName )
2214  {
2215    if ( Constants.SENTINEL.equals( declaredName ) )
2216    {
2217      return deriveName( method, OBSERVABLE_INITIAL_METHOD_PATTERN, declaredName );
2218    }
2219    else
2220    {
2221      if ( !SourceVersion.isIdentifier( declaredName ) )
2222      {
2223        throw new ProcessorException( "@ObservableInitial target specified an invalid name '" + declaredName +
2224                                      "'. The name must be a valid java identifier.", method );
2225      }
2226      else if ( SourceVersion.isKeyword( declaredName ) )
2227      {
2228        throw new ProcessorException( "@ObservableInitial target specified an invalid name '" + declaredName +
2229                                      "'. The name must not be a java keyword.", method );
2230      }
2231      return declaredName;
2232    }
2233  }
2234
2235  @Nullable
2236  private String deriveObservableInitialName( @Nonnull final VariableElement field,
2237                                              @Nonnull final String declaredName )
2238  {
2239    if ( Constants.SENTINEL.equals( declaredName ) )
2240    {
2241      final String fieldName = field.getSimpleName().toString();
2242      final Matcher matcher = OBSERVABLE_INITIAL_FIELD_PATTERN.matcher( fieldName );
2243      if ( matcher.find() )
2244      {
2245        return constantCaseToLowerCamel( matcher.group( 1 ) );
2246      }
2247      else
2248      {
2249        return null;
2250      }
2251    }
2252    else
2253    {
2254      if ( !SourceVersion.isIdentifier( declaredName ) )
2255      {
2256        throw new ProcessorException( "@ObservableInitial target specified an invalid name '" + declaredName +
2257                                      "'. The name must be a valid java identifier.", field );
2258      }
2259      else if ( SourceVersion.isKeyword( declaredName ) )
2260      {
2261        throw new ProcessorException( "@ObservableInitial target specified an invalid name '" + declaredName +
2262                                      "'. The name must not be a java keyword.", field );
2263      }
2264      return declaredName;
2265    }
2266  }
2267
2268  @Nonnull
2269  private String constantCaseToLowerCamel( @Nonnull final String name )
2270  {
2271    final String[] parts = name.split( "_" );
2272    final StringBuilder sb = new StringBuilder();
2273    for ( final String part : parts )
2274    {
2275      if ( part.isEmpty() )
2276      {
2277        continue;
2278      }
2279      final String lower = part.toLowerCase( Locale.ENGLISH );
2280      if ( sb.isEmpty() )
2281      {
2282        sb.append( lower );
2283      }
2284      else
2285      {
2286        sb.append( Character.toUpperCase( lower.charAt( 0 ) ) );
2287        if ( lower.length() > 1 )
2288        {
2289          sb.append( lower.substring( 1 ) );
2290        }
2291      }
2292    }
2293    return sb.toString();
2294  }
2295
2296  private void addReference( @Nonnull final ComponentDescriptor component,
2297                             @Nonnull final AnnotationMirror annotation,
2298                             @Nonnull final ExecutableElement method,
2299                             @Nonnull final ExecutableType methodType )
2300  {
2301    MemberChecks.mustNotHaveAnyParameters( Constants.REFERENCE_CLASSNAME, method );
2302    MemberChecks.mustBeSubclassCallable( component.getElement(),
2303                                         Constants.COMPONENT_CLASSNAME,
2304                                         Constants.REFERENCE_CLASSNAME,
2305                                         method );
2306    MemberChecks.mustNotThrowAnyExceptions( Constants.REFERENCE_CLASSNAME, method );
2307    MemberChecks.mustReturnAValue( Constants.REFERENCE_CLASSNAME, method );
2308    MemberChecks.mustBeAbstract( Constants.REFERENCE_CLASSNAME, method );
2309
2310    final String name = getReferenceName( annotation, method );
2311    final String linkType = getLinkType( method );
2312    final String inverseName;
2313    final Multiplicity inverseMultiplicity;
2314    if ( hasInverse( annotation ) )
2315    {
2316      inverseMultiplicity = getReferenceInverseMultiplicity( annotation );
2317      inverseName = getReferenceInverseName( component, annotation, method, inverseMultiplicity );
2318      final TypeMirror returnType = method.getReturnType();
2319      if ( !( returnType instanceof DeclaredType ) ||
2320           !AnnotationsUtil.hasAnnotationOfType( ( (DeclaredType) returnType ).asElement(),
2321                                                 Constants.COMPONENT_CLASSNAME ) )
2322      {
2323        throw new ProcessorException( "@Reference target expected to return a type annotated with " +
2324                                      MemberChecks.toSimpleName( Constants.COMPONENT_CLASSNAME ) +
2325                                      " if there is an inverse reference", method );
2326      }
2327    }
2328    else
2329    {
2330      inverseName = null;
2331      inverseMultiplicity = null;
2332    }
2333    final ReferenceDescriptor descriptor = component.findOrCreateReference( name );
2334    descriptor.setMethod( method, methodType, linkType, inverseName, inverseMultiplicity );
2335    verifyMultiplicityOfAssociatedInverseMethod( component, descriptor );
2336  }
2337
2338  private boolean hasInverse( @Nonnull final AnnotationMirror annotation )
2339  {
2340    final VariableElement variableElement = AnnotationsUtil.getAnnotationValueValue( annotation, "inverse" );
2341    return switch ( variableElement.getSimpleName().toString() )
2342    {
2343      case "ENABLE" -> true;
2344      case "DISABLE" -> false;
2345      default -> null != AnnotationsUtil.findAnnotationValueNoDefaults( annotation, "inverseName" ) ||
2346                 null != AnnotationsUtil.findAnnotationValueNoDefaults( annotation, "inverseMultiplicity" );
2347    };
2348  }
2349
2350  private void verifyMultiplicityOfAssociatedReferenceMethod( @Nonnull final ComponentDescriptor component,
2351                                                              @Nonnull final InverseDescriptor descriptor )
2352  {
2353    final Multiplicity multiplicity =
2354      ElementsUtil
2355        .getMethods( descriptor.getTargetType(),
2356                     processingEnv.getElementUtils(),
2357                     processingEnv.getTypeUtils() )
2358        .stream()
2359        .map( m -> {
2360          final AnnotationMirror a =
2361            AnnotationsUtil.findAnnotationByType( m, Constants.REFERENCE_CLASSNAME );
2362          if ( null != a && getReferenceName( a, m ).equals( descriptor.getReferenceName() ) )
2363          {
2364            if ( null == AnnotationsUtil.findAnnotationValueNoDefaults( a, "inverse" ) &&
2365                 null == AnnotationsUtil.findAnnotationValueNoDefaults( a, "inverseName" ) &&
2366                 null == AnnotationsUtil.findAnnotationValueNoDefaults( a, "inverseMultiplicity" ) )
2367            {
2368              throw new ProcessorException( "@Inverse target found an associated @Reference on the method '" +
2369                                            m.getSimpleName() + "' on type '" +
2370                                            descriptor.getTargetType().getQualifiedName() + "' but the " +
2371                                            "annotation has not configured an inverse.",
2372                                            descriptor.getObservable().getGetter() );
2373            }
2374            ensureTargetTypeAligns( component, descriptor, m.getReturnType() );
2375            return getReferenceInverseMultiplicity( a );
2376          }
2377          else
2378          {
2379            return null;
2380          }
2381        } )
2382        .filter( Objects::nonNull )
2383        .findAny()
2384        .orElse( null );
2385    if ( null == multiplicity )
2386    {
2387      throw new ProcessorException( "@Inverse target expected to find an associated @Reference annotation with " +
2388                                    "a name parameter equal to '" + descriptor.getReferenceName() + "' on class " +
2389                                    descriptor.getTargetType().getQualifiedName() + " but is unable to " +
2390                                    "locate a matching method.", descriptor.getObservable().getGetter() );
2391    }
2392
2393    if ( descriptor.getMultiplicity() != multiplicity )
2394    {
2395      throw new ProcessorException( "@Inverse target has a multiplicity of " + descriptor.getMultiplicity() +
2396                                    " but that associated @Reference has a multiplicity of " + multiplicity +
2397                                    ". The multiplicity must align.", descriptor.getObservable().getGetter() );
2398    }
2399  }
2400
2401  @Nonnull
2402  private String getLinkType( @Nonnull final ExecutableElement method )
2403  {
2404    return AnnotationsUtil.getEnumAnnotationParameter( method, Constants.REFERENCE_CLASSNAME, "load" );
2405  }
2406
2407  @Nonnull
2408  private String getReferenceName( @Nonnull final AnnotationMirror annotation,
2409                                   @Nonnull final ExecutableElement method )
2410  {
2411    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
2412    final String name;
2413    if ( Constants.SENTINEL.equals( declaredName ) )
2414    {
2415      final String candidate = deriveName( method, GETTER_PATTERN, declaredName );
2416      if ( null == candidate )
2417      {
2418        name = method.getSimpleName().toString();
2419      }
2420      else
2421      {
2422        name = candidate;
2423      }
2424    }
2425    else
2426    {
2427      name = declaredName;
2428      if ( !SourceVersion.isIdentifier( name ) )
2429      {
2430        throw new ProcessorException( "@Reference target specified an invalid name '" + name + "'. The " +
2431                                      "name must be a valid java identifier.", method );
2432      }
2433      else if ( SourceVersion.isKeyword( name ) )
2434      {
2435        throw new ProcessorException( "@Reference target specified an invalid name '" + name + "'. The " +
2436                                      "name must not be a java keyword.", method );
2437      }
2438    }
2439    return name;
2440  }
2441
2442  @Nonnull
2443  private Multiplicity getReferenceInverseMultiplicity( @Nonnull final AnnotationMirror annotation )
2444  {
2445    final VariableElement variableElement =
2446      AnnotationsUtil.getAnnotationValueValue( annotation, "inverseMultiplicity" );
2447    return switch ( variableElement.getSimpleName().toString() )
2448    {
2449      case "MANY" -> Multiplicity.MANY;
2450      case "ONE" -> Multiplicity.ONE;
2451      default -> Multiplicity.ZERO_OR_ONE;
2452    };
2453  }
2454
2455  @Nonnull
2456  private String getReferenceInverseName( @Nonnull final ComponentDescriptor component,
2457                                          @Nonnull final AnnotationMirror annotation,
2458                                          @Nonnull final ExecutableElement method,
2459                                          @Nonnull final Multiplicity multiplicity )
2460  {
2461    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "inverseName" );
2462    final String name;
2463    if ( Constants.SENTINEL.equals( declaredName ) )
2464    {
2465      final String baseName = component.getElement().getSimpleName().toString();
2466      return firstCharacterToLowerCase( baseName ) + ( Multiplicity.MANY == multiplicity ? "s" : "" );
2467    }
2468    else
2469    {
2470      name = declaredName;
2471      if ( !SourceVersion.isIdentifier( name ) )
2472      {
2473        throw new ProcessorException( "@Reference target specified an invalid inverseName '" + name + "'. The " +
2474                                      "inverseName must be a valid java identifier.", method );
2475      }
2476      else if ( SourceVersion.isKeyword( name ) )
2477      {
2478        throw new ProcessorException( "@Reference target specified an invalid inverseName '" + name + "'. The " +
2479                                      "inverseName must not be a java keyword.", method );
2480      }
2481    }
2482    return name;
2483  }
2484
2485  private void ensureTargetTypeAligns( @Nonnull final ComponentDescriptor component,
2486                                       @Nonnull final ReferenceDescriptor descriptor,
2487                                       @Nonnull final TypeMirror target )
2488  {
2489    if ( !processingEnv.getTypeUtils().isSameType( target, component.getElement().asType() ) )
2490    {
2491      throw new ProcessorException( "@Reference target expected to find an associated @Inverse annotation with " +
2492                                    "a target type equal to " + component.getElement().getQualifiedName() + " but " +
2493                                    "the actual target type is " + target, descriptor.getMethod() );
2494    }
2495  }
2496
2497  private void verifyMultiplicityOfAssociatedInverseMethod( @Nonnull final ComponentDescriptor component,
2498                                                            @Nonnull final ReferenceDescriptor descriptor )
2499  {
2500    final TypeElement element =
2501      (TypeElement) processingEnv.getTypeUtils().asElement( descriptor.getMethod().getReturnType() );
2502    final String defaultInverseName =
2503      descriptor.hasInverse() ?
2504      null :
2505      firstCharacterToLowerCase( component.getElement().getSimpleName().toString() ) + "s";
2506    final Multiplicity multiplicity =
2507      ElementsUtil
2508        .getMethods( element, processingEnv.getElementUtils(), processingEnv.getTypeUtils() )
2509        .stream()
2510        .map( m -> {
2511          final AnnotationMirror a = AnnotationsUtil.findAnnotationByType( m, Constants.INVERSE_CLASSNAME );
2512          if ( null == a )
2513          {
2514            return null;
2515          }
2516          final String inverseName = getInverseName( a, m );
2517          if ( !descriptor.hasInverse() && inverseName.equals( defaultInverseName ) )
2518          {
2519            throw new ProcessorException( "@Reference target has not configured an inverse but there is an " +
2520                                          "associated @Inverse annotated method named '" + m.getSimpleName() +
2521                                          "' on type '" + element.getQualifiedName() + "'.",
2522                                          descriptor.getMethod() );
2523          }
2524          if ( descriptor.hasInverse() && inverseName.equals( descriptor.getInverseName() ) )
2525          {
2526            final TypeElement target = getInverseManyTypeTarget( m );
2527            if ( null != target )
2528            {
2529              ensureTargetTypeAligns( component, descriptor, target.asType() );
2530              return Multiplicity.MANY;
2531            }
2532            else
2533            {
2534              ensureTargetTypeAligns( component, descriptor, m.getReturnType() );
2535              return AnnotationsUtil.hasNonnullAnnotation( m ) ? Multiplicity.ONE : Multiplicity.ZERO_OR_ONE;
2536            }
2537          }
2538          else
2539          {
2540            return null;
2541          }
2542        } )
2543        .filter( Objects::nonNull )
2544        .findAny()
2545        .orElse( null );
2546
2547    if ( descriptor.hasInverse() )
2548    {
2549      if ( null == multiplicity )
2550      {
2551        throw new ProcessorException( "@Reference target expected to find an associated @Inverse annotation " +
2552                                      "with a name parameter equal to '" + descriptor.getInverseName() + "' on " +
2553                                      "class " + descriptor.getMethod().getReturnType() + " but is unable to " +
2554                                      "locate a matching method.", descriptor.getMethod() );
2555      }
2556
2557      final Multiplicity inverseMultiplicity = descriptor.getInverseMultiplicity();
2558      if ( inverseMultiplicity != multiplicity )
2559      {
2560        throw new ProcessorException( "@Reference target has an inverseMultiplicity of " + inverseMultiplicity +
2561                                      " but that associated @Inverse has a multiplicity of " + multiplicity +
2562                                      ". The multiplicity must align.", descriptor.getMethod() );
2563      }
2564    }
2565  }
2566
2567  @Nonnull
2568  private DependencyDescriptor createMethodDependencyDescriptor( @Nonnull final ComponentDescriptor descriptor,
2569                                                                 @Nonnull final ExecutableElement method )
2570  {
2571    MemberChecks.mustNotHaveAnyParameters( Constants.COMPONENT_DEPENDENCY_CLASSNAME, method );
2572    MemberChecks.mustBeSubclassCallable( descriptor.getElement(),
2573                                         Constants.COMPONENT_CLASSNAME,
2574                                         Constants.COMPONENT_DEPENDENCY_CLASSNAME,
2575                                         method );
2576    MemberChecks.mustNotThrowAnyExceptions( Constants.COMPONENT_DEPENDENCY_CLASSNAME, method );
2577    MemberChecks.mustReturnAValue( Constants.COMPONENT_DEPENDENCY_CLASSNAME, method );
2578
2579    final boolean validateTypeAtRuntime = isComponentDependencyValidateTypeAtRuntime( method );
2580    final TypeMirror type = method.getReturnType();
2581    if ( TypeKind.DECLARED != type.getKind() )
2582    {
2583      throw new ProcessorException( "@ComponentDependency target must return a non-primitive value", method );
2584    }
2585    if ( !validateTypeAtRuntime )
2586    {
2587      final TypeElement disposeNotifier = getTypeElement( Constants.DISPOSE_NOTIFIER_CLASSNAME );
2588      if ( !processingEnv.getTypeUtils().isAssignable( type, disposeNotifier.asType() ) )
2589      {
2590        final TypeElement typeElement = (TypeElement) processingEnv.getTypeUtils().asElement( type );
2591        if ( !isArezComponentLikeAnnotated( typeElement ) && !isDisposeTrackableComponent( typeElement ) )
2592        {
2593          throw new ProcessorException( "@ComponentDependency target must return an instance compatible with " +
2594                                        Constants.DISPOSE_NOTIFIER_CLASSNAME + " or a type annotated " +
2595                                        "with @ArezComponent(disposeNotifier=ENABLE) or " +
2596                                        AREZ_COMPONENT_LIKE_DESCRIPTION, method );
2597        }
2598      }
2599    }
2600
2601    final boolean cascade = isActionCascade( method );
2602    return new DependencyDescriptor( descriptor, method, cascade );
2603  }
2604
2605  private static boolean isComponentDependencyValidateTypeAtRuntime( @Nonnull final AnnotatedConstruct annotatedConstruct )
2606  {
2607    return Boolean.TRUE.equals( AnnotationsUtil
2608                                  .getAnnotationValue( annotatedConstruct,
2609                                                       Constants.COMPONENT_DEPENDENCY_CLASSNAME,
2610                                                       "validateTypeAtRuntime" )
2611                                  .getValue() );
2612  }
2613
2614  @Nonnull
2615  private DependencyDescriptor createFieldDependencyDescriptor( @Nonnull final ComponentDescriptor descriptor,
2616                                                                @Nonnull final VariableElement field )
2617  {
2618    MemberChecks.mustBeSubclassCallable( descriptor.getElement(),
2619                                         Constants.COMPONENT_CLASSNAME,
2620                                         Constants.COMPONENT_DEPENDENCY_CLASSNAME,
2621                                         field );
2622    emitWarningForManagedFieldAccess( descriptor, field, Constants.COMPONENT_DEPENDENCY_CLASSNAME );
2623    MemberChecks.mustBeFinal( Constants.COMPONENT_DEPENDENCY_CLASSNAME, field );
2624
2625    final boolean validateTypeAtRuntime = isComponentDependencyValidateTypeAtRuntime( field );
2626    final TypeMirror type = processingEnv.getTypeUtils().asMemberOf( descriptor.asDeclaredType(), field );
2627    if ( TypeKind.TYPEVAR != type.getKind() && TypeKind.DECLARED != type.getKind() )
2628    {
2629      throw new ProcessorException( "@ComponentDependency target must be a non-primitive value", field );
2630    }
2631    if ( !validateTypeAtRuntime )
2632    {
2633      final TypeElement disposeNotifier = getTypeElement( Constants.DISPOSE_NOTIFIER_CLASSNAME );
2634      if ( !processingEnv.getTypeUtils().isAssignable( type, disposeNotifier.asType() ) )
2635      {
2636        final Element element = processingEnv.getTypeUtils().asElement( type );
2637        if ( !( element instanceof TypeElement ) ||
2638             !isArezComponentLikeAnnotated( (TypeElement) element ) &&
2639             !isDisposeTrackableComponent( (TypeElement) element ) )
2640        {
2641          throw new ProcessorException( "@ComponentDependency target must be an instance compatible with " +
2642                                        Constants.DISPOSE_NOTIFIER_CLASSNAME + " or a type annotated " +
2643                                        "with @ArezComponent(disposeNotifier=ENABLE) or " +
2644                                        AREZ_COMPONENT_LIKE_DESCRIPTION, field );
2645        }
2646      }
2647    }
2648
2649    if ( !isActionCascade( field ) )
2650    {
2651      throw new ProcessorException( "@ComponentDependency target defined an action of 'SET_NULL' but the " +
2652                                    "dependency is on a final field and can not be set to null.", field );
2653
2654    }
2655
2656    return new DependencyDescriptor( descriptor, field );
2657  }
2658
2659  private boolean isActionCascade( @Nonnull final Element method )
2660  {
2661    final String value =
2662      AnnotationsUtil.getEnumAnnotationParameter( method,
2663                                                  Constants.COMPONENT_DEPENDENCY_CLASSNAME,
2664                                                  "action" );
2665    return "CASCADE".equals( value );
2666  }
2667
2668  @SuppressWarnings( "BooleanMethodIsAlwaysInverted" )
2669  private boolean isArezComponentAnnotated( @Nonnull final TypeElement typeElement )
2670  {
2671    return AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.COMPONENT_CLASSNAME );
2672  }
2673
2674  @SuppressWarnings( "BooleanMethodIsAlwaysInverted" )
2675  private boolean isArezComponentLikeAnnotated( @Nonnull final TypeElement typeElement )
2676  {
2677    return AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.AREZ_COMPONENT_LIKE_CLASSNAME ) ||
2678           isAnnotatedByActAsArezComponent( typeElement );
2679  }
2680
2681  private boolean isAnnotatedByActAsArezComponent( @Nonnull final TypeElement typeElement )
2682  {
2683    for ( final AnnotationMirror annotation : typeElement.getAnnotationMirrors() )
2684    {
2685      final Element annotationType = annotation.getAnnotationType().asElement();
2686      if ( annotationType instanceof TypeElement && isActAsArezComponentAnnotated( (TypeElement) annotationType ) )
2687      {
2688        return true;
2689      }
2690    }
2691    return false;
2692  }
2693
2694  private boolean isActAsArezComponentAnnotated( @Nonnull final TypeElement annotationType )
2695  {
2696    for ( final AnnotationMirror annotation : annotationType.getAnnotationMirrors() )
2697    {
2698      final Element metaAnnotationType = annotation.getAnnotationType().asElement();
2699      if ( metaAnnotationType instanceof TypeElement &&
2700           isActAsArezComponentAnnotationType( (TypeElement) metaAnnotationType ) )
2701      {
2702        return true;
2703      }
2704    }
2705    return false;
2706  }
2707
2708  private boolean isActAsArezComponentAnnotationType( @Nonnull final TypeElement annotationType )
2709  {
2710    return Constants.ACT_AS_AREZ_COMPONENT_CLASSNAME.equals( annotationType.getQualifiedName().toString() ) ||
2711           annotationType.getSimpleName().contentEquals( Constants.ACT_AS_AREZ_COMPONENT_SIMPLE_NAME );
2712  }
2713
2714  private boolean isArezComponentLikeType( @Nonnull final TypeMirror typeMirror )
2715  {
2716    final Element element = processingEnv.getTypeUtils().asElement( typeMirror );
2717    return element instanceof TypeElement && isArezComponentLikeAnnotated( (TypeElement) element );
2718  }
2719
2720  @SuppressWarnings( "BooleanMethodIsAlwaysInverted" )
2721  private boolean isDisposeTrackableComponent( @Nonnull final TypeElement typeElement )
2722  {
2723    return isArezComponentAnnotated( typeElement ) &&
2724           isDisposableTrackableRequired( typeElement );
2725  }
2726
2727  private boolean isLivenessDisposedArezComponent( @Nonnull final TypeMirror typeMirror )
2728  {
2729    final Element element = processingEnv.getTypeUtils().asElement( typeMirror );
2730    final AnnotationMirror arezComponent = element instanceof TypeElement ?
2731                                           AnnotationsUtil.findAnnotationByType( element,
2732                                                                                 Constants.COMPONENT_CLASSNAME ) :
2733                                           null;
2734    return null != arezComponent && this.<Boolean>getAnnotationParameter( arezComponent, "disposeOnDeactivate" );
2735  }
2736
2737  @Nonnull
2738  private ComponentDescriptor parse( @Nonnull final TypeElement typeElement )
2739    throws ProcessorException
2740  {
2741    if ( ElementKind.CLASS != typeElement.getKind() && ElementKind.INTERFACE != typeElement.getKind() )
2742    {
2743      throw new ProcessorException( "@ArezComponent target must be a class or an interface", typeElement );
2744    }
2745    else if ( typeElement.getModifiers().contains( Modifier.FINAL ) )
2746    {
2747      throw new ProcessorException( "@ArezComponent target must not be final", typeElement );
2748    }
2749    else if ( ElementsUtil.isNonStaticNestedClass( typeElement ) )
2750    {
2751      throw new ProcessorException( "@ArezComponent target must not be a non-static nested class", typeElement );
2752    }
2753    final AnnotationMirror arezComponent =
2754      AnnotationsUtil.getAnnotationByType( typeElement, Constants.COMPONENT_CLASSNAME );
2755    final String declaredName = getAnnotationParameter( arezComponent, "name" );
2756    final boolean disposeOnDeactivate = getAnnotationParameter( arezComponent, "disposeOnDeactivate" );
2757    final boolean observableFlag = isComponentObservableRequired( arezComponent, disposeOnDeactivate );
2758    final boolean service = isService( typeElement );
2759    final boolean disposeNotifierFlag = isDisposableTrackableRequired( typeElement );
2760    final boolean allowEmpty = getAnnotationParameter( arezComponent, "allowEmpty" );
2761    final List<VariableElement> fields = ElementsUtil.getFields( typeElement );
2762    ensureNoFieldInjections( fields );
2763    ensureNoMethodInjections( typeElement );
2764    final boolean sting = isStingIntegrationEnabled( arezComponent, service );
2765
2766    final var defaultReadOutsideTransaction =
2767      AnnotationsUtil.findAnnotationValueNoDefaults( arezComponent, "defaultReadOutsideTransaction" );
2768    final var defaultWriteOutsideTransaction =
2769      AnnotationsUtil.findAnnotationValueNoDefaults( arezComponent, "defaultWriteOutsideTransaction" );
2770    final var defaultSkipIfDisposed =
2771      AnnotationsUtil.findAnnotationValueNoDefaults( arezComponent, "defaultSkipIfDisposed" );
2772
2773    final var requireEquals = isEqualsRequired( arezComponent );
2774    final var requireVerify = isVerifyRequired( arezComponent, typeElement );
2775
2776    if ( !typeElement.getModifiers().contains( Modifier.ABSTRACT ) )
2777    {
2778      throw new ProcessorException( "@ArezComponent target must be abstract", typeElement );
2779    }
2780
2781    final var name =
2782      Constants.SENTINEL.equals( declaredName ) ?
2783      typeElement.getQualifiedName().toString().replace( ".", "_" ) :
2784      declaredName;
2785
2786    if ( !SourceVersion.isIdentifier( name ) )
2787    {
2788      throw new ProcessorException( "@ArezComponent target specified an invalid name '" + name + "'. The " +
2789                                    "name must be a valid java identifier.", typeElement );
2790    }
2791    else if ( SourceVersion.isKeyword( name ) )
2792    {
2793      throw new ProcessorException( "@ArezComponent target specified an invalid name '" + name + "'. The " +
2794                                    "name must not be a java keyword.", typeElement );
2795    }
2796
2797    verifyConstructors( typeElement, sting );
2798
2799    if ( sting && !( (DeclaredType) typeElement.asType() ).getTypeArguments().isEmpty() )
2800    {
2801      throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2802                                                          "enable sting integration and be a parameterized type" ),
2803                                    typeElement );
2804    }
2805    else if ( !sting && AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_EAGER ) )
2806    {
2807      throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2808                                                          "disable sting integration and be annotated with " +
2809                                                          Constants.STING_EAGER ),
2810                                    typeElement );
2811    }
2812    else if ( !sting && AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_TYPED ) )
2813    {
2814      throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2815                                                          "disable sting integration and be annotated with " +
2816                                                          Constants.STING_TYPED ),
2817                                    typeElement );
2818    }
2819    else if ( !sting && AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_NAMED ) )
2820    {
2821      throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2822                                                          "disable sting integration and be annotated with " +
2823                                                          Constants.STING_NAMED ),
2824                                    typeElement );
2825    }
2826    else if ( !observableFlag && disposeOnDeactivate )
2827    {
2828      throw new ProcessorException( "@ArezComponent target has specified observable = DISABLE and " +
2829                                    "disposeOnDeactivate = true which is not a valid combination", typeElement );
2830    }
2831
2832    if ( isWarningNotSuppressed( typeElement, Constants.WARNING_EXTENDS_COMPONENT ) )
2833    {
2834      var parent = typeElement.getSuperclass();
2835      while ( null != parent )
2836      {
2837        final var parentElement = processingEnv.getTypeUtils().asElement( parent );
2838        final var parentTypeElement =
2839          null != parentElement && ElementKind.CLASS == parentElement.getKind() ? (TypeElement) parentElement : null;
2840
2841        if ( null != parentTypeElement &&
2842             AnnotationsUtil.hasAnnotationOfType( parentTypeElement, Constants.COMPONENT_CLASSNAME ) )
2843        {
2844          final var message =
2845            MemberChecks.shouldNot( Constants.COMPONENT_CLASSNAME,
2846                                    "extend a class annotated with the " + Constants.COMPONENT_CLASSNAME +
2847                                    " annotation. " + suppressedBy( Constants.WARNING_EXTENDS_COMPONENT ) );
2848          warning( message, typeElement );
2849        }
2850        parent = null != parentTypeElement ? parentTypeElement.getSuperclass() : null;
2851      }
2852    }
2853
2854    final var methods =
2855      ElementsUtil.getMethods( typeElement, processingEnv.getElementUtils(), processingEnv.getTypeUtils(), true );
2856    final var generateToString = methods.stream().
2857      noneMatch( m -> m.getSimpleName().toString().equals( "toString" ) &&
2858                      m.getParameters().isEmpty() &&
2859                      !( m.getEnclosingElement().getSimpleName().toString().equals( "Object" ) &&
2860                         "java.lang".equals( processingEnv
2861                                               .getElementUtils()
2862                                               .getPackageOf( m.getEnclosingElement() )
2863                                               .getQualifiedName()
2864                                               .toString() ) ) );
2865
2866    final var priority = getDefaultPriority( arezComponent );
2867    final var defaultPriority =
2868      null == priority ? null : "DEFAULT".equals( priority ) ? Priority.NORMAL : Priority.valueOf( priority );
2869
2870    final var defaultReadOutsideTransactionValue =
2871      null == defaultReadOutsideTransaction ?
2872      null :
2873      ( (VariableElement) defaultReadOutsideTransaction.getValue() ).getSimpleName().toString();
2874    final var defaultWriteOutsideTransactionValue =
2875      null == defaultWriteOutsideTransaction ?
2876      null :
2877      ( (VariableElement) defaultWriteOutsideTransaction.getValue() ).getSimpleName().toString();
2878    final var defaultSkipIfDisposedValue =
2879      null == defaultSkipIfDisposed ?
2880      null :
2881      ( (VariableElement) defaultSkipIfDisposed.getValue() ).getSimpleName().toString();
2882
2883    final var descriptor =
2884      new ComponentDescriptor( name,
2885                               defaultPriority,
2886                               observableFlag,
2887                               disposeNotifierFlag,
2888                               disposeOnDeactivate,
2889                               sting,
2890                               requireEquals,
2891                               requireVerify,
2892                               generateToString,
2893                               typeElement,
2894                               defaultReadOutsideTransactionValue,
2895                               defaultWriteOutsideTransactionValue,
2896                               defaultSkipIfDisposedValue );
2897
2898    processObservableInitialFields( descriptor, fields );
2899    analyzeCandidateMethods( descriptor, methods, processingEnv.getTypeUtils() );
2900    validate( allowEmpty, descriptor );
2901
2902    for ( final ObservableDescriptor observable : descriptor.getObservables().values() )
2903    {
2904      final var returnType = observable.getGetterType().getReturnType();
2905      if ( observable.expectSetter() )
2906      {
2907        final var parameterType = observable.getSetterType().getParameterTypes().get( 0 );
2908        if ( !processingEnv.getTypeUtils().isSameType( parameterType, returnType ) &&
2909             !parameterType.toString().equals( returnType.toString() ) )
2910        {
2911          throw new ProcessorException( "@Observable property defines a setter and getter with different types." +
2912                                        " Getter type: " + returnType + " Setter type: " + parameterType + ".",
2913                                        observable.getGetter() );
2914        }
2915      }
2916      final var getterDeclaredComparator = observable.getGetterDeclaredEqualityComparator();
2917      final var setterDeclaredComparator = observable.getSetterDeclaredEqualityComparator();
2918      final var getterExplicit = !Constants.EQUALITY_COMPARATOR_CLASSNAME.equals( getterDeclaredComparator );
2919      final var setterExplicit = !Constants.EQUALITY_COMPARATOR_CLASSNAME.equals( setterDeclaredComparator );
2920      if ( getterExplicit && setterExplicit && !getterDeclaredComparator.equals( setterDeclaredComparator ) )
2921      {
2922        throw new ProcessorException( "@Observable target specified equalityComparator of type '" +
2923                                      setterDeclaredComparator + "' but the paired accessor has already specified " +
2924                                      "equalityComparator of type '" + getterDeclaredComparator + "'.",
2925                                      observable.getSetter() );
2926      }
2927      final var comparatorElement =
2928        getterExplicit ? observable.getGetter() : setterExplicit ? observable.getSetter() : observable.getDefiner();
2929      final var comparatorClassName =
2930        getterExplicit ? getterDeclaredComparator :
2931        setterExplicit ? setterDeclaredComparator :
2932        Constants.EQUALITY_COMPARATOR_CLASSNAME;
2933      observable.setEqualityComparator( resolveEffectiveEqualityComparator( descriptor.getElement(),
2934                                                                            Constants.OBSERVABLE_CLASSNAME,
2935                                                                            comparatorElement,
2936                                                                            returnType,
2937                                                                            comparatorClassName ) );
2938    }
2939
2940    final var idRequired = isIdRequired( arezComponent );
2941    descriptor.setIdRequired( idRequired );
2942    if ( !idRequired )
2943    {
2944      if ( descriptor.hasComponentIdMethod() )
2945      {
2946        throw new ProcessorException( "@ArezComponent target has specified the idRequired = DISABLE " +
2947                                      "annotation parameter but also has annotated a method with @ComponentId " +
2948                                      "that requires idRequired = ENABLE.", typeElement );
2949      }
2950      if ( !descriptor.getComponentIdRefs().isEmpty() )
2951      {
2952        throw new ProcessorException( "@ArezComponent target has specified the idRequired = DISABLE " +
2953                                      "annotation parameter but also has annotated a method with @ComponentIdRef " +
2954                                      "that requires idRequired = ENABLE.", typeElement );
2955      }
2956      if ( !descriptor.getInverses().isEmpty() )
2957      {
2958        throw new ProcessorException( "@ArezComponent target has specified the idRequired = DISABLE " +
2959                                      "annotation parameter but also has annotated a method with @Inverse " +
2960                                      "that requires idRequired = ENABLE.", typeElement );
2961      }
2962    }
2963
2964    warnOnUnmanagedComponentReferences( descriptor, fields );
2965
2966    return descriptor;
2967  }
2968
2969  private boolean isStingIntegrationEnabled( @Nonnull final AnnotationMirror arezComponent, final boolean service )
2970  {
2971    final VariableElement parameter = getAnnotationParameter( arezComponent, "sting" );
2972    final var value = parameter.getSimpleName().toString();
2973    return "ENABLE".equals( value ) ||
2974           ( "AUTODETECT".equals( value ) &&
2975             service &&
2976             null != findTypeElement( Constants.STING_INJECTOR ) );
2977  }
2978
2979  private void verifyConstructors( @Nonnull final TypeElement typeElement, final boolean sting )
2980  {
2981    final var constructors = ElementsUtil.getConstructors( typeElement );
2982    if ( constructors.size() > 1 && sting )
2983    {
2984      throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
2985                                                          "enable sting integration and have multiple constructors" ),
2986                                    typeElement );
2987    }
2988
2989    for ( final var constructor : constructors )
2990    {
2991      if ( constructor.getModifiers().contains( Modifier.PROTECTED ) &&
2992           isWarningNotSuppressed( constructor, Constants.WARNING_PROTECTED_CONSTRUCTOR ) )
2993      {
2994        final var message =
2995          MemberChecks.should( Constants.COMPONENT_CLASSNAME,
2996                               "have a package access constructor. " +
2997                               suppressedBy( Constants.WARNING_PROTECTED_CONSTRUCTOR ) );
2998        warning( message, constructor );
2999      }
3000      verifyConstructorParameters( constructor, sting );
3001    }
3002  }
3003
3004  private void verifyConstructorParameters( @Nonnull final ExecutableElement constructor, final boolean sting )
3005  {
3006    for ( final var parameter : constructor.getParameters() )
3007    {
3008      final var type = parameter.asType();
3009      if ( sting && TypesUtil.containsArrayType( type ) )
3010      {
3011        throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
3012                                                            "enable sting integration and contain a constructor with a parameter that contains an array type" ),
3013                                      parameter );
3014      }
3015      else if ( sting && TypesUtil.containsWildcard( type ) )
3016      {
3017        throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
3018                                                            "enable sting integration and contain a constructor with a parameter that contains a wildcard type parameter" ),
3019                                      parameter );
3020      }
3021      else if ( sting && TypesUtil.containsRawType( type ) )
3022      {
3023        throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
3024                                                            "enable sting integration and contain a constructor with a parameter that contains a raw type" ),
3025                                      parameter );
3026      }
3027      else if ( !sting && AnnotationsUtil.hasAnnotationOfType( parameter, Constants.STING_NAMED ) )
3028      {
3029        throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
3030                                                            "disable sting integration and contain a constructor with a parameter that is annotated with the " +
3031                                                            Constants.STING_NAMED + " annotation" ),
3032                                      parameter );
3033      }
3034      else if ( sting && TypeKind.DECLARED == type.getKind() && !( (DeclaredType) type ).getTypeArguments().isEmpty() )
3035      {
3036        throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
3037                                                            "enable sting integration and contain a constructor with a parameter that contains a parameterized type" ),
3038                                      parameter );
3039      }
3040    }
3041  }
3042
3043  private void ensureNoFieldInjections( @Nonnull final List<VariableElement> fields )
3044  {
3045    for ( final var field : fields )
3046    {
3047      if ( hasInjectAnnotation( field ) )
3048      {
3049        throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
3050                                                            "contain fields annotated by the " +
3051                                                            Constants.INJECT_CLASSNAME +
3052                                                            " annotation. Use constructor injection instead" ),
3053                                      field );
3054      }
3055    }
3056  }
3057
3058  private void ensureNoMethodInjections( @Nonnull final TypeElement typeElement )
3059  {
3060    final var methods =
3061      ElementsUtil.getMethods( typeElement, processingEnv.getElementUtils(), processingEnv.getTypeUtils() );
3062    for ( final var method : methods )
3063    {
3064      if ( hasInjectAnnotation( method ) )
3065      {
3066        throw new ProcessorException( MemberChecks.mustNot( Constants.COMPONENT_CLASSNAME,
3067                                                            "contain methods annotated by the " +
3068                                                            Constants.INJECT_CLASSNAME +
3069                                                            " annotation. Use constructor injection instead" ),
3070                                      method );
3071      }
3072    }
3073  }
3074
3075  private void analyzeCandidateMethods( @Nonnull final ComponentDescriptor componentDescriptor,
3076                                        @Nonnull final List<ExecutableElement> methods,
3077                                        @Nonnull final Types typeUtils )
3078    throws ProcessorException
3079  {
3080    for ( final var method : methods )
3081    {
3082      final var methodName = method.getSimpleName().toString();
3083      if ( AREZ_SPECIAL_METHODS.contains( methodName ) && method.getParameters().isEmpty() )
3084      {
3085        throw new ProcessorException( "Method defined on a class annotated by @ArezComponent uses a name " +
3086                                      "reserved by Arez", method );
3087      }
3088      else if ( methodName.startsWith( ComponentGenerator.FIELD_PREFIX ) ||
3089                methodName.startsWith( ComponentGenerator.OBSERVABLE_DATA_FIELD_PREFIX ) ||
3090                methodName.startsWith( ComponentGenerator.REFERENCE_FIELD_PREFIX ) ||
3091                methodName.startsWith( ComponentGenerator.FRAMEWORK_PREFIX ) )
3092      {
3093        throw new ProcessorException( "Method defined on a class annotated by @ArezComponent uses a name " +
3094                                      "with a prefix reserved by Arez", method );
3095      }
3096    }
3097    final var getters = new HashMap<String, CandidateMethod>();
3098    final var captures = new HashMap<String, CandidateMethod>();
3099    final var pushes = new HashMap<String, CandidateMethod>();
3100    final var pops = new HashMap<String, CandidateMethod>();
3101    final var setters = new HashMap<String, CandidateMethod>();
3102    final var observes = new HashMap<String, CandidateMethod>();
3103    final var onDepsChanges = new LinkedHashMap<String, List<CandidateMethod>>();
3104    for ( final ExecutableElement method : methods )
3105    {
3106      final var methodType =
3107        (ExecutableType) typeUtils.asMemberOf( (DeclaredType) componentDescriptor.getElement().asType(), method );
3108      if ( !analyzeMethod( componentDescriptor, method, methodType ) )
3109      {
3110        /*
3111         * If we get here the method was not annotated so we can try to detect if it is a
3112         * candidate arez method in case some arez annotations are implied via naming conventions.
3113         */
3114        if ( method.getModifiers().contains( Modifier.STATIC ) )
3115        {
3116          continue;
3117        }
3118
3119        final var candidateMethod = new CandidateMethod( method, methodType );
3120        final var voidReturn = method.getReturnType().getKind() == TypeKind.VOID;
3121        final var parameterCount = method.getParameters().size();
3122        String name;
3123
3124        name = deriveName( method, PUSH_PATTERN, Constants.SENTINEL );
3125        if ( voidReturn && 1 == parameterCount && null != name )
3126        {
3127          pushes.put( name, candidateMethod );
3128          continue;
3129        }
3130        name = deriveName( method, POP_PATTERN, Constants.SENTINEL );
3131        if ( voidReturn && 1 == parameterCount && null != name )
3132        {
3133          pops.put( name, candidateMethod );
3134          continue;
3135        }
3136        name = deriveName( method, CAPTURE_PATTERN, Constants.SENTINEL );
3137        if ( !voidReturn && 0 == parameterCount && null != name )
3138        {
3139          captures.put( name, candidateMethod );
3140          continue;
3141        }
3142
3143        if ( !method.getModifiers().contains( Modifier.FINAL ) )
3144        {
3145          name = deriveName( method, SETTER_PATTERN, Constants.SENTINEL );
3146          if ( voidReturn && 1 == parameterCount && null != name )
3147          {
3148            setters.put( name, candidateMethod );
3149            continue;
3150          }
3151          name = deriveName( method, ISSER_PATTERN, Constants.SENTINEL );
3152          if ( !voidReturn && 0 == parameterCount && null != name )
3153          {
3154            getters.put( name, candidateMethod );
3155            continue;
3156          }
3157          name = deriveName( method, GETTER_PATTERN, Constants.SENTINEL );
3158          if ( !voidReturn && 0 == parameterCount && null != name )
3159          {
3160            getters.put( name, candidateMethod );
3161            continue;
3162          }
3163        }
3164        name = deriveName( method, ON_DEPS_CHANGE_PATTERN, Constants.SENTINEL );
3165        if ( voidReturn && null != name )
3166        {
3167          if ( 0 == parameterCount ||
3168               (
3169                 1 == parameterCount &&
3170                 Constants.OBSERVER_CLASSNAME.equals( method.getParameters().get( 0 ).asType().toString() )
3171               )
3172          )
3173          {
3174            onDepsChanges.computeIfAbsent( name, key -> new ArrayList<>() ).add( candidateMethod );
3175            continue;
3176          }
3177        }
3178
3179        final var methodName = method.getSimpleName().toString();
3180        if ( !OBJECT_METHODS.contains( methodName ) )
3181        {
3182          observes.put( methodName, candidateMethod );
3183        }
3184      }
3185    }
3186
3187    linkUnAnnotatedObservables( componentDescriptor, getters, setters );
3188    linkUnAnnotatedObserves( componentDescriptor, observes, onDepsChanges );
3189    linkUnMemoizeContextParameters( componentDescriptor, captures, pushes, pops );
3190    linkObserverRefs( componentDescriptor );
3191    linkCascadeDisposeObservables( componentDescriptor );
3192    linkCascadeDisposeReferences( componentDescriptor );
3193    linkAutoObserveObservables( componentDescriptor );
3194    linkAutoObserveReferences( componentDescriptor );
3195    linkObservableInitials( componentDescriptor );
3196
3197    // CascadeDispose returned false but it was actually processed so lets remove them from getters set
3198
3199    componentDescriptor.getCascadeDisposes().keySet().forEach( method -> {
3200      for ( final var entry : new HashMap<>( getters ).entrySet() )
3201      {
3202        if ( method.equals( entry.getValue().getMethod() ) )
3203        {
3204          getters.remove( entry.getKey() );
3205        }
3206      }
3207    } );
3208
3209    linkMemoizeContextParametersToMemoizes( componentDescriptor );
3210
3211    linkDependencies( componentDescriptor, getters.values() );
3212
3213    autodetectObservableInitializers( componentDescriptor );
3214
3215    /*
3216     * All of the maps will have called remove() for all matching candidates.
3217     * Thus any left are the non-arez methods.
3218     */
3219
3220    ensureNoAbstractMethods( componentDescriptor, getters.values() );
3221    ensureNoAbstractMethods( componentDescriptor, setters.values() );
3222    ensureNoAbstractMethods( componentDescriptor, observes.values() );
3223    ensureNoAbstractMethods( componentDescriptor,
3224                             onDepsChanges.values().stream().flatMap( List::stream ).toList() );
3225
3226    processCascadeDisposeFields( componentDescriptor );
3227    processAutoObserveFields( componentDescriptor );
3228    processComponentDependencyFields( componentDescriptor );
3229  }
3230
3231  private static void linkMemoizeContextParametersToMemoizes( final @Nonnull ComponentDescriptor componentDescriptor )
3232  {
3233    // Link MemoizeContextParameters to associated Memoize descriptors
3234    componentDescriptor
3235      .getMemoizes()
3236      .values()
3237      .forEach( m ->
3238                  componentDescriptor
3239                    .getMemoizeContextParameters()
3240                    .values()
3241                    .forEach( p -> p.tryMatchMemoizeDescriptor( m ) ) );
3242  }
3243
3244  private void linkUnMemoizeContextParameters( @Nonnull final ComponentDescriptor componentDescriptor,
3245                                               @Nonnull final Map<String, CandidateMethod> captures,
3246                                               @Nonnull final Map<String, CandidateMethod> pushes,
3247                                               @Nonnull final Map<String, CandidateMethod> pops )
3248  {
3249    final var parameters = componentDescriptor.getMemoizeContextParameters().values();
3250    for ( final var parameter : parameters )
3251    {
3252      if ( !parameter.hasCapture() )
3253      {
3254        final var capture = captures.remove( parameter.getName() );
3255        if ( null != capture )
3256        {
3257          parameter.linkUnAnnotatedCapture( capture.getMethod(), capture.getMethodType() );
3258        }
3259      }
3260      if ( !parameter.hasPop() )
3261      {
3262        final var pop = pops.remove( parameter.getName() );
3263        if ( null != pop )
3264        {
3265          parameter.linkUnAnnotatedPop( pop.getMethod(), pop.getMethodType() );
3266        }
3267      }
3268      if ( !parameter.hasPush() )
3269      {
3270        final var push = pushes.remove( parameter.getName() );
3271        if ( null != push )
3272        {
3273          parameter.linkUnAnnotatedPush( push.getMethod(), push.getMethodType() );
3274        }
3275      }
3276    }
3277  }
3278
3279  private void ensureNoAbstractMethods( @Nonnull final ComponentDescriptor componentDescriptor,
3280                                        @Nonnull final Collection<CandidateMethod> candidateMethods )
3281  {
3282    candidateMethods
3283      .stream()
3284      .map( CandidateMethod::getMethod )
3285      .filter( m -> m.getModifiers().contains( Modifier.ABSTRACT ) )
3286      .forEach( m -> {
3287        throw new ProcessorException( "@ArezComponent target has an abstract method not implemented by " +
3288                                      "framework. The method is named " + m.getSimpleName(),
3289                                      componentDescriptor.getElement() );
3290      } );
3291  }
3292
3293  private boolean analyzeMethod( @Nonnull final ComponentDescriptor descriptor,
3294                                 @Nonnull final ExecutableElement method,
3295                                 @Nonnull final ExecutableType methodType )
3296    throws ProcessorException
3297  {
3298    emitWarningForUnnecessaryProtectedMethod( descriptor, method );
3299    emitWarningForUnnecessaryFinalMethod( descriptor, method );
3300    verifyNoDuplicateAnnotations( method );
3301
3302    final var action = AnnotationsUtil.findAnnotationByType( method, Constants.ACTION_CLASSNAME );
3303    final var requiresTransaction =
3304      AnnotationsUtil.findAnnotationByType( method, Constants.REQUIRES_TRANSACTION_CLASSNAME );
3305    final var jaxWsAction = AnnotationsUtil.findAnnotationByType( method, Constants.JAX_WS_ACTION_CLASSNAME );
3306    final var observed = AnnotationsUtil.findAnnotationByType( method, Constants.OBSERVE_CLASSNAME );
3307    final var observable = AnnotationsUtil.findAnnotationByType( method, Constants.OBSERVABLE_CLASSNAME );
3308    final var observableInitial =
3309      AnnotationsUtil.findAnnotationByType( method, Constants.OBSERVABLE_INITIAL_CLASSNAME );
3310    final var observableValueRef =
3311      AnnotationsUtil.findAnnotationByType( method, Constants.OBSERVABLE_VALUE_REF_CLASSNAME );
3312    final var memoize = AnnotationsUtil.findAnnotationByType( method, Constants.MEMOIZE_CLASSNAME );
3313    final var memoizeContextParameter =
3314      AnnotationsUtil.findAnnotationByType( method, Constants.MEMOIZE_CONTEXT_PARAMETER_CLASSNAME );
3315    final var computableValueRef =
3316      AnnotationsUtil.findAnnotationByType( method, Constants.COMPUTABLE_VALUE_REF_CLASSNAME );
3317    final var contextRef = AnnotationsUtil.findAnnotationByType( method, Constants.CONTEXT_REF_CLASSNAME );
3318    final var stateRef = AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_STATE_REF_CLASSNAME );
3319    final var componentRef = AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_REF_CLASSNAME );
3320    final var componentId = AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_ID_CLASSNAME );
3321    final var componentIdRef = AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_ID_REF_CLASSNAME );
3322    final var componentTypeName =
3323      AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_TYPE_NAME_REF_CLASSNAME );
3324    final var componentNameRef = AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_NAME_REF_CLASSNAME );
3325    final var postConstruct = AnnotationsUtil.findAnnotationByType( method, Constants.POST_CONSTRUCT_CLASSNAME );
3326    final var ejbPostConstruct = AnnotationsUtil.findAnnotationByType( method, Constants.EJB_POST_CONSTRUCT_CLASSNAME );
3327    final var preDispose = AnnotationsUtil.findAnnotationByType( method, Constants.PRE_DISPOSE_CLASSNAME );
3328    final var postDispose = AnnotationsUtil.findAnnotationByType( method, Constants.POST_DISPOSE_CLASSNAME );
3329    final var onActivate = AnnotationsUtil.findAnnotationByType( method, Constants.ON_ACTIVATE_CLASSNAME );
3330    final var onDeactivate = AnnotationsUtil.findAnnotationByType( method, Constants.ON_DEACTIVATE_CLASSNAME );
3331    final var onDepsChange = AnnotationsUtil.findAnnotationByType( method, Constants.ON_DEPS_CHANGE_CLASSNAME );
3332    final var observerRef = AnnotationsUtil.findAnnotationByType( method, Constants.OBSERVER_REF_CLASSNAME );
3333    final var dependency = AnnotationsUtil.findAnnotationByType( method, Constants.COMPONENT_DEPENDENCY_CLASSNAME );
3334    final var autoObserve = AnnotationsUtil.findAnnotationByType( method, Constants.AUTO_OBSERVE_CLASSNAME );
3335    final var reference = AnnotationsUtil.findAnnotationByType( method, Constants.REFERENCE_CLASSNAME );
3336    final var referenceId = AnnotationsUtil.findAnnotationByType( method, Constants.REFERENCE_ID_CLASSNAME );
3337    final var inverse = AnnotationsUtil.findAnnotationByType( method, Constants.INVERSE_CLASSNAME );
3338    final var preInverseRemove = AnnotationsUtil.findAnnotationByType( method, Constants.PRE_INVERSE_REMOVE_CLASSNAME );
3339    final var postInverseAdd = AnnotationsUtil.findAnnotationByType( method, Constants.POST_INVERSE_ADD_CLASSNAME );
3340    final var cascadeDispose = AnnotationsUtil.findAnnotationByType( method, Constants.CASCADE_DISPOSE_CLASSNAME );
3341
3342    if ( null != observable )
3343    {
3344      final ObservableDescriptor observableDescriptor = addObservable( descriptor,
3345                                                                       observable, method, methodType );
3346      if ( null != referenceId )
3347      {
3348        addReferenceId( descriptor, referenceId, observableDescriptor, method );
3349      }
3350      if ( null != inverse )
3351      {
3352        addInverse( descriptor, inverse, observableDescriptor, method );
3353      }
3354      if ( null != cascadeDispose )
3355      {
3356        addCascadeDisposeMethod( descriptor, method, observableDescriptor );
3357      }
3358      if ( null != autoObserve )
3359      {
3360        addAutoObserveMethod( descriptor, method, observableDescriptor );
3361      }
3362      return true;
3363    }
3364    else if ( null != observableInitial )
3365    {
3366      addObservableInitialMethod( descriptor, observableInitial, method, methodType );
3367      return true;
3368    }
3369    else if ( null != observableValueRef )
3370    {
3371      addObservableValueRef( descriptor, observableValueRef, method, methodType );
3372      return true;
3373    }
3374    else if ( null != action )
3375    {
3376      if ( null != postConstruct )
3377      {
3378        addPostConstruct( descriptor, method );
3379      }
3380      addAction( descriptor, action, method, methodType );
3381      return true;
3382    }
3383    else if ( null != requiresTransaction )
3384    {
3385      addRequiresTransaction( descriptor, requiresTransaction, method, methodType );
3386      return true;
3387    }
3388    else if ( null != observed )
3389    {
3390      addObserve( descriptor, observed, method, methodType );
3391      return true;
3392    }
3393    else if ( null != onDepsChange )
3394    {
3395      addOnDepsChange( descriptor, onDepsChange, method );
3396      return true;
3397    }
3398    else if ( null != observerRef )
3399    {
3400      addObserverRef( descriptor, observerRef, method, methodType );
3401      return true;
3402    }
3403    else if ( null != contextRef )
3404    {
3405      addContextRef( descriptor, method );
3406      return true;
3407    }
3408    else if ( null != stateRef )
3409    {
3410      addComponentStateRef( descriptor, stateRef, method );
3411      return true;
3412    }
3413    else if ( null != memoizeContextParameter )
3414    {
3415      addMemoizeContextParameter( descriptor, memoizeContextParameter, method, methodType );
3416      return true;
3417    }
3418    else if ( null != memoize )
3419    {
3420      addMemoize( descriptor, memoize, method, methodType );
3421      return true;
3422    }
3423    else if ( null != computableValueRef )
3424    {
3425      addComputableValueRef( descriptor, computableValueRef, method, methodType );
3426      return true;
3427    }
3428    else if ( null != reference )
3429    {
3430      if ( null != cascadeDispose )
3431      {
3432        addCascadeDisposeMethod( descriptor, method, null );
3433      }
3434      addReference( descriptor, reference, method, methodType );
3435      if ( null != autoObserve )
3436      {
3437        addAutoObserveMethod( descriptor, method, null );
3438      }
3439      return true;
3440    }
3441    else if ( null != autoObserve )
3442    {
3443      addAutoObserveMethod( descriptor, method, null );
3444      return true;
3445    }
3446    else if ( null != cascadeDispose )
3447    {
3448      addCascadeDisposeMethod( descriptor, method, null );
3449      // Return false so that it can be picked as the getter of an @Observable or linked to a @Reference
3450      return false;
3451    }
3452    else if ( null != componentIdRef )
3453    {
3454      addComponentIdRef( descriptor, method );
3455      return true;
3456    }
3457    else if ( null != componentRef )
3458    {
3459      addComponentRef( descriptor, method );
3460      return true;
3461    }
3462    else if ( null != componentId )
3463    {
3464      setComponentId( descriptor, method, methodType );
3465      return true;
3466    }
3467    else if ( null != componentNameRef )
3468    {
3469      addComponentNameRef( descriptor, method );
3470      return true;
3471    }
3472    else if ( null != componentTypeName )
3473    {
3474      setComponentTypeNameRef( descriptor, method );
3475      return true;
3476    }
3477    else if ( null != jaxWsAction )
3478    {
3479      throw new ProcessorException( "@" + Constants.JAX_WS_ACTION_CLASSNAME + " annotation " +
3480                                    "not supported in components annotated with @ArezComponent, use the @" +
3481                                    Constants.ACTION_CLASSNAME + " annotation instead.",
3482                                    method );
3483    }
3484    else if ( null != ejbPostConstruct )
3485    {
3486      throw new ProcessorException( "@" + Constants.EJB_POST_CONSTRUCT_CLASSNAME + " annotation " +
3487                                    "not supported in components annotated with @ArezComponent, use the @" +
3488                                    Constants.POST_CONSTRUCT_CLASSNAME + " annotation instead.",
3489                                    method );
3490    }
3491    else if ( null != postConstruct )
3492    {
3493      addPostConstruct( descriptor, method );
3494      return true;
3495    }
3496    else if ( null != preDispose )
3497    {
3498      addPreDispose( descriptor, method );
3499      return true;
3500    }
3501    else if ( null != postDispose )
3502    {
3503      addPostDispose( descriptor, method );
3504      return true;
3505    }
3506    else if ( null != onActivate )
3507    {
3508      addOnActivate( descriptor, onActivate, method );
3509      return true;
3510    }
3511    else if ( null != onDeactivate )
3512    {
3513      addOnDeactivate( descriptor, onDeactivate, method );
3514      return true;
3515    }
3516    else if ( null != dependency )
3517    {
3518      descriptor.addDependency( createMethodDependencyDescriptor( descriptor, method ) );
3519      return false;
3520    }
3521    else if ( null != referenceId )
3522    {
3523      addReferenceId( descriptor, referenceId, method );
3524      return true;
3525    }
3526    else if ( null != inverse )
3527    {
3528      addInverse( descriptor, inverse, method, methodType );
3529      return true;
3530    }
3531    else if ( null != preInverseRemove )
3532    {
3533      addPreInverseRemove( descriptor, preInverseRemove, method );
3534      return true;
3535    }
3536    else if ( null != postInverseAdd )
3537    {
3538      addPostInverseAdd( descriptor, postInverseAdd, method );
3539      return true;
3540    }
3541    else
3542    {
3543      return false;
3544    }
3545  }
3546
3547  private void emitWarningForUnnecessaryProtectedMethod( @Nonnull final ComponentDescriptor descriptor,
3548                                                         @Nonnull final ExecutableElement method )
3549  {
3550    if ( method.getModifiers().contains( Modifier.PROTECTED ) &&
3551         Objects.equals( method.getEnclosingElement(), descriptor.getElement() ) &&
3552         ElementsUtil.isWarningNotSuppressed( method,
3553                                              Constants.WARNING_PROTECTED_METHOD,
3554                                              Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) &&
3555         !isMethodAProtectedOverride( descriptor.getElement(), method ) )
3556    {
3557      final var message =
3558        MemberChecks.shouldNot( Constants.COMPONENT_CLASSNAME,
3559                                "declare a protected method. " +
3560                                MemberChecks.suppressedBy( Constants.WARNING_PROTECTED_METHOD,
3561                                                           Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) );
3562      warning( message, method );
3563    }
3564  }
3565
3566  private void emitWarningForManagedFieldAccess( @Nonnull final ComponentDescriptor descriptor,
3567                                                 @Nonnull final VariableElement field,
3568                                                 @Nonnull final String annotationClassname )
3569  {
3570    emitWarningForPublicManagedField( field, annotationClassname );
3571    emitWarningForUnnecessaryProtectedManagedField( descriptor, field );
3572  }
3573
3574  private void emitWarningForPublicManagedField( @Nonnull final VariableElement field,
3575                                                 @Nonnull final String annotationClassname )
3576  {
3577    if ( field.getModifiers().contains( Modifier.PUBLIC ) &&
3578         ElementsUtil.isWarningNotSuppressed( field,
3579                                              Constants.WARNING_PUBLIC_FIELD,
3580                                              Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) )
3581    {
3582      final var message =
3583        MemberChecks.shouldNot( annotationClassname,
3584                                "be public. " +
3585                                MemberChecks.suppressedBy( Constants.WARNING_PUBLIC_FIELD,
3586                                                           Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) );
3587      warning( message, field );
3588    }
3589  }
3590
3591  private void emitWarningForUnnecessaryProtectedManagedField( @Nonnull final ComponentDescriptor descriptor,
3592                                                               @Nonnull final VariableElement field )
3593  {
3594    if ( field.getModifiers().contains( Modifier.PROTECTED ) &&
3595         ElementsUtil.isWarningNotSuppressed( field,
3596                                              Constants.WARNING_PROTECTED_FIELD,
3597                                              Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) &&
3598         !isProtectedFieldOnInheritedTypeInDifferentPackage( descriptor.getElement(), field ) )
3599    {
3600      final var message =
3601        MemberChecks.shouldNot( Constants.COMPONENT_CLASSNAME,
3602                                "declare a protected field. " +
3603                                MemberChecks.suppressedBy( Constants.WARNING_PROTECTED_FIELD,
3604                                                           Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) );
3605      warning( message, field );
3606    }
3607  }
3608
3609  private void emitWarningForUnnecessaryFinalMethod( @Nonnull final ComponentDescriptor descriptor,
3610                                                     @Nonnull final ExecutableElement method )
3611  {
3612    if ( method.getModifiers().contains( Modifier.FINAL ) &&
3613         Objects.equals( method.getEnclosingElement(), descriptor.getElement() ) &&
3614         ElementsUtil.isWarningNotSuppressed( method,
3615                                              Constants.WARNING_FINAL_METHOD,
3616                                              Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) )
3617    {
3618      final var message =
3619        MemberChecks.shouldNot( Constants.COMPONENT_CLASSNAME,
3620                                "declare a final method. " +
3621                                MemberChecks.suppressedBy( Constants.WARNING_FINAL_METHOD,
3622                                                           Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) );
3623      warning( message, method );
3624    }
3625  }
3626
3627  private void addReferenceId( @Nonnull final ComponentDescriptor descriptor,
3628                               @Nonnull final AnnotationMirror annotation,
3629                               @Nonnull final ObservableDescriptor observable,
3630                               @Nonnull final ExecutableElement method )
3631  {
3632    MemberChecks.mustNotHaveAnyParameters( Constants.REFERENCE_ID_CLASSNAME, method );
3633    MemberChecks.mustBeSubclassCallable( descriptor.getElement(),
3634                                         Constants.COMPONENT_CLASSNAME,
3635                                         Constants.REFERENCE_ID_CLASSNAME,
3636                                         method );
3637    MemberChecks.mustNotThrowAnyExceptions( Constants.REFERENCE_ID_CLASSNAME, method );
3638    MemberChecks.mustReturnAValue( Constants.REFERENCE_ID_CLASSNAME, method );
3639
3640    final var name = getReferenceIdName( annotation, method );
3641    descriptor.findOrCreateReference( name ).setObservable( observable );
3642  }
3643
3644  private void addReferenceId( @Nonnull final ComponentDescriptor descriptor,
3645                               @Nonnull final AnnotationMirror annotation,
3646                               @Nonnull final ExecutableElement method )
3647  {
3648    MemberChecks.mustNotHaveAnyParameters( Constants.REFERENCE_ID_CLASSNAME, method );
3649    MemberChecks.mustBeSubclassCallable( descriptor.getElement(),
3650                                         Constants.COMPONENT_CLASSNAME,
3651                                         Constants.REFERENCE_ID_CLASSNAME,
3652                                         method );
3653    MemberChecks.mustNotThrowAnyExceptions( Constants.REFERENCE_ID_CLASSNAME, method );
3654    MemberChecks.mustReturnAValue( Constants.REFERENCE_ID_CLASSNAME, method );
3655
3656    final var name = getReferenceIdName( annotation, method );
3657    descriptor.findOrCreateReference( name ).setIdMethod( method );
3658  }
3659
3660  @Nonnull
3661  private String getReferenceIdName( @Nonnull final AnnotationMirror annotation,
3662                                     @Nonnull final ExecutableElement method )
3663  {
3664    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
3665    final String name;
3666    if ( Constants.SENTINEL.equals( declaredName ) )
3667    {
3668      final var candidate = deriveName( method, ID_GETTER_PATTERN, declaredName );
3669      if ( null == candidate )
3670      {
3671        final String candidate2 = deriveName( method, RAW_ID_GETTER_PATTERN, declaredName );
3672        if ( null == candidate2 )
3673        {
3674          throw new ProcessorException( "@ReferenceId target has not specified a name and does not follow " +
3675                                        "the convention \"get[Name]Id\" or \"[name]Id\"", method );
3676        }
3677        else
3678        {
3679          name = candidate2;
3680        }
3681      }
3682      else
3683      {
3684        name = candidate;
3685      }
3686    }
3687    else
3688    {
3689      name = declaredName;
3690      if ( !SourceVersion.isIdentifier( name ) )
3691      {
3692        throw new ProcessorException( "@ReferenceId target specified an invalid name '" + name + "'. The " +
3693                                      "name must be a valid java identifier.", method );
3694      }
3695      else if ( SourceVersion.isKeyword( name ) )
3696      {
3697        throw new ProcessorException( "@ReferenceId target specified an invalid name '" + name + "'. The " +
3698                                      "name must not be a java keyword.", method );
3699      }
3700    }
3701    return name;
3702  }
3703
3704  private void addPreInverseRemove( @Nonnull final ComponentDescriptor component,
3705                                    @Nonnull final AnnotationMirror annotation,
3706                                    @Nonnull final ExecutableElement method )
3707    throws ProcessorException
3708  {
3709    mustBeHookHook( component.getElement(),
3710                    Constants.PRE_INVERSE_REMOVE_CLASSNAME,
3711                    method );
3712    shouldBeInternalHookMethod( processingEnv,
3713                                component,
3714                                method,
3715                                Constants.PRE_INVERSE_REMOVE_CLASSNAME );
3716    if ( 1 != method.getParameters().size() )
3717    {
3718      throw new ProcessorException( MemberChecks.must( Constants.PRE_INVERSE_REMOVE_CLASSNAME,
3719                                                       "have exactly 1 parameter" ), method );
3720    }
3721    else
3722    {
3723      final var name = getPreInverseRemoveName( annotation, method );
3724      findOrCreateInverseDescriptor( component, name ).addPreInverseRemoveHook( method );
3725    }
3726  }
3727
3728  @Nonnull
3729  private String getPreInverseRemoveName( @Nonnull final AnnotationMirror annotation,
3730                                          @Nonnull final ExecutableElement method )
3731  {
3732    final String name = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
3733    if ( Constants.SENTINEL.equals( name ) )
3734    {
3735      final var candidate = deriveName( method, PRE_INVERSE_REMOVE_PATTERN, name );
3736      if ( null == candidate )
3737      {
3738        throw new ProcessorException( "@PreInverseRemove target has not specified a name and does not follow " +
3739                                      "the convention \"pre[Name]Remove\"", method );
3740      }
3741      else
3742      {
3743        return candidate;
3744      }
3745    }
3746    else
3747    {
3748      if ( !SourceVersion.isIdentifier( name ) )
3749      {
3750        throw new ProcessorException( "@PreInverseRemove target specified an invalid name '" + name + "'. The " +
3751                                      "name must be a valid java identifier", method );
3752      }
3753      else if ( SourceVersion.isKeyword( name ) )
3754      {
3755        throw new ProcessorException( "@PreInverseRemove target specified an invalid name '" + name + "'. The " +
3756                                      "name must not be a java keyword", method );
3757      }
3758      return name;
3759    }
3760  }
3761
3762  private void addPostInverseAdd( @Nonnull final ComponentDescriptor component,
3763                                  @Nonnull final AnnotationMirror annotation,
3764                                  @Nonnull final ExecutableElement method )
3765    throws ProcessorException
3766  {
3767    mustBeHookHook( component.getElement(),
3768                    Constants.POST_INVERSE_ADD_CLASSNAME,
3769                    method );
3770    shouldBeInternalHookMethod( processingEnv,
3771                                component,
3772                                method,
3773                                Constants.POST_INVERSE_ADD_CLASSNAME );
3774    if ( 1 != method.getParameters().size() )
3775    {
3776      throw new ProcessorException( MemberChecks.must( Constants.POST_INVERSE_ADD_CLASSNAME,
3777                                                       "have exactly 1 parameter" ), method );
3778    }
3779    else
3780    {
3781      final var name = getPostInverseAddName( annotation, method );
3782      findOrCreateInverseDescriptor( component, name ).addPostInverseAddHook( method );
3783    }
3784  }
3785
3786  @Nonnull
3787  private String getPostInverseAddName( @Nonnull final AnnotationMirror annotation,
3788                                        @Nonnull final ExecutableElement method )
3789  {
3790    final String name = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
3791    if ( Constants.SENTINEL.equals( name ) )
3792    {
3793      final var candidate = deriveName( method, POST_INVERSE_ADD_PATTERN, name );
3794      if ( null == candidate )
3795      {
3796        throw new ProcessorException( "@PostInverseAdd target has not specified a name and does not follow " +
3797                                      "the convention \"post[Name]Add\"", method );
3798      }
3799      else
3800      {
3801        return candidate;
3802      }
3803    }
3804    else
3805    {
3806      if ( !SourceVersion.isIdentifier( name ) )
3807      {
3808        throw new ProcessorException( "@PostInverseAdd target specified an invalid name '" + name + "'. The " +
3809                                      "name must be a valid java identifier", method );
3810      }
3811      else if ( SourceVersion.isKeyword( name ) )
3812      {
3813        throw new ProcessorException( "@PostInverseAdd target specified an invalid name '" + name + "'. The " +
3814                                      "name must not be a java keyword", method );
3815      }
3816      return name;
3817    }
3818  }
3819
3820  private void addInverse( @Nonnull final ComponentDescriptor descriptor,
3821                           @Nonnull final AnnotationMirror annotation,
3822                           @Nonnull final ExecutableElement method,
3823                           @Nonnull final ExecutableType methodType )
3824  {
3825    MemberChecks.mustNotHaveAnyParameters( Constants.INVERSE_CLASSNAME, method );
3826    MemberChecks.mustBeSubclassCallable( descriptor.getElement(),
3827                                         Constants.COMPONENT_CLASSNAME,
3828                                         Constants.INVERSE_CLASSNAME,
3829                                         method );
3830    MemberChecks.mustNotThrowAnyExceptions( Constants.INVERSE_CLASSNAME, method );
3831    MemberChecks.mustReturnAValue( Constants.INVERSE_CLASSNAME, method );
3832    MemberChecks.mustBeAbstract( Constants.INVERSE_CLASSNAME, method );
3833
3834    final var name = getInverseName( annotation, method );
3835    final var observable = descriptor.findOrCreateObservable( name );
3836    observable.setGetter( method, methodType );
3837
3838    addInverse( descriptor, annotation, observable, method );
3839  }
3840
3841  private void addInverse( @Nonnull final ComponentDescriptor descriptor,
3842                           @Nonnull final AnnotationMirror annotation,
3843                           @Nonnull final ObservableDescriptor observable,
3844                           @Nonnull final ExecutableElement method )
3845  {
3846    MemberChecks.mustNotHaveAnyParameters( Constants.INVERSE_CLASSNAME, method );
3847    MemberChecks.mustBeSubclassCallable( descriptor.getElement(),
3848                                         Constants.COMPONENT_CLASSNAME,
3849                                         Constants.INVERSE_CLASSNAME,
3850                                         method );
3851    MemberChecks.mustNotThrowAnyExceptions( Constants.INVERSE_CLASSNAME, method );
3852    MemberChecks.mustReturnAValue( Constants.INVERSE_CLASSNAME, method );
3853    MemberChecks.mustBeAbstract( Constants.INVERSE_CLASSNAME, method );
3854
3855    final var name = getInverseName( annotation, method );
3856    final var existing = descriptor.getInverses().get( name );
3857    if ( null != existing && existing.hasObservable() )
3858    {
3859      throw new ProcessorException( "@Inverse target defines duplicate inverse for name '" + name +
3860                                    "'. The other inverse is " + existing.getObservable().getGetter(),
3861                                    method );
3862    }
3863    else
3864    {
3865      final var type = method.getReturnType();
3866
3867      final Multiplicity multiplicity;
3868      var targetType = getInverseManyTypeTarget( method );
3869      if ( null != targetType )
3870      {
3871        multiplicity = Multiplicity.MANY;
3872      }
3873      else
3874      {
3875        if ( !( type instanceof DeclaredType ) ||
3876             !AnnotationsUtil.hasAnnotationOfType( ( (DeclaredType) type ).asElement(),
3877                                                   Constants.COMPONENT_CLASSNAME ) )
3878        {
3879          throw new ProcessorException( "@Inverse target expected to return a type annotated with " +
3880                                        Constants.COMPONENT_CLASSNAME, method );
3881        }
3882        targetType = (TypeElement) ( (DeclaredType) type ).asElement();
3883        if ( AnnotationsUtil.hasNonnullAnnotation( method ) )
3884        {
3885          multiplicity = Multiplicity.ONE;
3886        }
3887        else if ( AnnotationsUtil.hasNullableAnnotation( method ) )
3888        {
3889          multiplicity = Multiplicity.ZERO_OR_ONE;
3890        }
3891        else
3892        {
3893          throw new ProcessorException( "@Inverse target expected to be annotated with either " +
3894                                        AnnotationsUtil.NULLABLE_CLASSNAME + " or " +
3895                                        AnnotationsUtil.NONNULL_CLASSNAME, method );
3896        }
3897      }
3898      final var referenceName = getInverseReferenceNameParameter( descriptor, method );
3899      final var inverse = findOrCreateInverseDescriptor( descriptor, name );
3900      final var otherName = firstCharacterToLowerCase( targetType.getSimpleName().toString() );
3901      inverse.setInverse( observable, referenceName, multiplicity, targetType, otherName );
3902      verifyMultiplicityOfAssociatedReferenceMethod( descriptor, inverse );
3903    }
3904  }
3905
3906  @Nonnull
3907  private InverseDescriptor findOrCreateInverseDescriptor( @Nonnull final ComponentDescriptor descriptor,
3908                                                           @Nonnull final String name )
3909  {
3910    return descriptor.getInverses().computeIfAbsent( name, n -> new InverseDescriptor( descriptor, name ) );
3911  }
3912
3913  @Nonnull
3914  private String getInverseName( @Nonnull final AnnotationMirror annotation,
3915                                 @Nonnull final ExecutableElement method )
3916  {
3917    final String declaredName = AnnotationsUtil.getAnnotationValueValue( annotation, "name" );
3918    final String name;
3919    if ( Constants.SENTINEL.equals( declaredName ) )
3920    {
3921      final var candidate = deriveName( method, GETTER_PATTERN, declaredName );
3922      name = null == candidate ? method.getSimpleName().toString() : candidate;
3923    }
3924    else
3925    {
3926      name = declaredName;
3927      if ( !SourceVersion.isIdentifier( name ) )
3928      {
3929        throw new ProcessorException( "@Inverse target specified an invalid name '" + name + "'. The " +
3930                                      "name must be a valid java identifier.", method );
3931      }
3932      else if ( SourceVersion.isKeyword( name ) )
3933      {
3934        throw new ProcessorException( "@Inverse target specified an invalid name '" + name + "'. The " +
3935                                      "name must not be a java keyword.", method );
3936      }
3937    }
3938    return name;
3939  }
3940
3941  private void warnOnUnmanagedComponentReferences( @Nonnull final ComponentDescriptor descriptor,
3942                                                   @Nonnull final List<VariableElement> fields )
3943  {
3944    final var disposeNotifier = getTypeElement( Constants.DISPOSE_NOTIFIER_CLASSNAME );
3945
3946    for ( final var field : fields )
3947    {
3948      if ( !field.getModifiers().contains( Modifier.STATIC ) &&
3949           SuperficialValidation.validateElement( processingEnv, field ) )
3950      {
3951        final var fieldType = getEffectiveFieldType( descriptor, field );
3952        final var fieldTypeElement = asTypeElement( fieldType );
3953        final var isDisposeNotifier = isAssignable( fieldType, disposeNotifier );
3954        final var isTypeAnnotatedByComponentAnnotation =
3955          !isDisposeNotifier && null != fieldTypeElement && isArezComponentAnnotated( fieldTypeElement );
3956        final var isTypeAnnotatedArezComponentLike =
3957          !isDisposeNotifier &&
3958          !isTypeAnnotatedByComponentAnnotation &&
3959          isArezComponentLikeType( fieldType );
3960        if ( isTypeAnnotatedByComponentAnnotation )
3961        {
3962          emitWarningForNonPrivateServiceField( field, fieldTypeElement );
3963        }
3964        if ( isDisposeNotifier || isTypeAnnotatedByComponentAnnotation || isTypeAnnotatedArezComponentLike )
3965        {
3966          if ( !descriptor.isDependencyDefined( field ) &&
3967               !descriptor.isCascadeDisposeDefined( field ) &&
3968               !descriptor.isAutoObserveDefined( field ) &&
3969               ( isDisposeNotifier ||
3970                 isTypeAnnotatedArezComponentLike ||
3971                 verifyReferencesToComponent( fieldTypeElement ) ) &&
3972               isUnmanagedComponentReferenceNotSuppressed( field ) )
3973          {
3974            final var label =
3975              isDisposeNotifier ? "an implementation of DisposeNotifier" :
3976              isTypeAnnotatedByComponentAnnotation ? "an Arez component" :
3977              AREZ_COMPONENT_LIKE_TYPE_DESCRIPTION;
3978            final var message =
3979              "Field named '" + field.getSimpleName() + "' has a type that is " + label +
3980              " but is not annotated with @" + Constants.CASCADE_DISPOSE_CLASSNAME + " or " +
3981              "@" + Constants.COMPONENT_DEPENDENCY_CLASSNAME + " or @" + Constants.AUTO_OBSERVE_CLASSNAME +
3982              ". This scenario can cause errors if the value is disposed. Please " +
3983              "annotate the field as appropriate or suppress the warning by annotating the field with " +
3984              "@SuppressWarnings( \"" + Constants.WARNING_UNMANAGED_COMPONENT_REFERENCE + "\" ) or " +
3985              "@SuppressArezWarnings( \"" + Constants.WARNING_UNMANAGED_COMPONENT_REFERENCE + "\" )";
3986            warning( message, field );
3987          }
3988        }
3989      }
3990    }
3991
3992    for ( final var observable : descriptor.getObservables().values() )
3993    {
3994      if ( observable.isAbstract() )
3995      {
3996        final var getter = observable.getGetter();
3997        if ( SuperficialValidation.validateElement( processingEnv, getter ) )
3998        {
3999          final var returnType = getter.getReturnType();
4000          final var returnElement = TypeKind.DECLARED == returnType.getKind() ? asTypeElement( returnType ) : null;
4001          final var isDisposeNotifier = isAssignable( returnType, disposeNotifier );
4002          final var isTypeAnnotatedByComponentAnnotation =
4003            !isDisposeNotifier && null != returnElement && isArezComponentAnnotated( returnElement );
4004          final var isTypeAnnotatedArezComponentLike =
4005            !isDisposeNotifier &&
4006            !isTypeAnnotatedByComponentAnnotation &&
4007            null != returnElement &&
4008            isArezComponentLikeAnnotated( returnElement );
4009          if ( isDisposeNotifier || isTypeAnnotatedByComponentAnnotation || isTypeAnnotatedArezComponentLike )
4010          {
4011            if ( !descriptor.isDependencyDefined( getter ) &&
4012                 !descriptor.isCascadeDisposeDefined( getter ) &&
4013                 !descriptor.isAutoObserveDefined( getter ) &&
4014                 ( isDisposeNotifier ||
4015                   isTypeAnnotatedArezComponentLike ||
4016                   verifyReferencesToComponent( returnElement ) ) &&
4017                 isUnmanagedComponentReferenceNotSuppressed( getter ) &&
4018                 ( observable.hasSetter() && isUnmanagedComponentReferenceNotSuppressed( observable.getSetter() ) ) )
4019            {
4020              final var label =
4021                isDisposeNotifier ? "an implementation of DisposeNotifier" :
4022                isTypeAnnotatedByComponentAnnotation ? "an Arez component" :
4023                AREZ_COMPONENT_LIKE_TYPE_DESCRIPTION;
4024              final var message =
4025                "Method named '" + getter.getSimpleName() + "' has a return type that is " + label +
4026                " but is not annotated with @" + Constants.CASCADE_DISPOSE_CLASSNAME + " or " +
4027                "@" + Constants.COMPONENT_DEPENDENCY_CLASSNAME + " or @" + Constants.AUTO_OBSERVE_CLASSNAME +
4028                ". This scenario can cause errors. " +
4029                "Please annotate the method as appropriate or suppress the warning by annotating the method with " +
4030                "@SuppressWarnings( \"" + Constants.WARNING_UNMANAGED_COMPONENT_REFERENCE + "\" ) or " +
4031                "@SuppressArezWarnings( \"" + Constants.WARNING_UNMANAGED_COMPONENT_REFERENCE + "\" )";
4032              warning( message, getter );
4033            }
4034          }
4035        }
4036      }
4037    }
4038  }
4039
4040  @Nonnull
4041  private TypeMirror getEffectiveFieldType( @Nonnull final ComponentDescriptor descriptor,
4042                                            @Nonnull final VariableElement field )
4043  {
4044    return processingEnv.getTypeUtils().asMemberOf( descriptor.asDeclaredType(), field );
4045  }
4046
4047  @Nullable
4048  private TypeElement asTypeElement( @Nonnull final TypeMirror type )
4049  {
4050    final var element = processingEnv.getTypeUtils().asElement( type );
4051    return element instanceof TypeElement ? (TypeElement) element : null;
4052  }
4053
4054  private boolean verifyReferencesToComponent( @Nonnull final TypeElement element )
4055  {
4056    assert SuperficialValidation.validateElement( processingEnv, element );
4057
4058    final var verifyReferencesToComponent =
4059      AnnotationsUtil.getEnumAnnotationParameter( element,
4060                                                  Constants.COMPONENT_CLASSNAME,
4061                                                  "verifyReferencesToComponent" );
4062    return switch ( verifyReferencesToComponent )
4063    {
4064      case "ENABLE" -> true;
4065      case "DISABLE" -> false;
4066      default -> isDisposableTrackableRequired( element );
4067    };
4068  }
4069
4070  private boolean isUnmanagedComponentReferenceNotSuppressed( @Nonnull final Element element )
4071  {
4072    return !ElementsUtil.isWarningSuppressed( element,
4073                                              Constants.WARNING_UNMANAGED_COMPONENT_REFERENCE,
4074                                              Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME );
4075  }
4076
4077  private void emitWarningForNonPrivateServiceField( @Nonnull final VariableElement field,
4078                                                     @Nonnull final TypeElement typeElement )
4079  {
4080    if ( !field.getModifiers().contains( Modifier.PRIVATE ) &&
4081         isService( typeElement ) &&
4082         ElementsUtil.isWarningNotSuppressed( field,
4083                                              Constants.WARNING_NON_PRIVATE_SERVICE_FIELD,
4084                                              Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME ) )
4085    {
4086      final var message =
4087        "Field named '" + field.getSimpleName() + "' has a type annotated with @" +
4088        Constants.COMPONENT_CLASSNAME + "(service = ENABLE) and should be private. " +
4089        MemberChecks.suppressedBy( Constants.WARNING_NON_PRIVATE_SERVICE_FIELD,
4090                                   Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME );
4091      warning( message, field );
4092    }
4093  }
4094
4095  @SuppressWarnings( "SameParameterValue" )
4096  private boolean isElementAnnotatedBy( @Nullable final Element element, @Nonnull final String annotation )
4097  {
4098    return null != element &&
4099           SuperficialValidation.validateElement( processingEnv, element ) &&
4100           AnnotationsUtil.hasAnnotationOfType( element, annotation );
4101  }
4102
4103  private boolean isService( @Nonnull final TypeElement typeElement )
4104  {
4105    final var service =
4106      AnnotationsUtil.getEnumAnnotationParameter( typeElement, Constants.COMPONENT_CLASSNAME, "service" );
4107    return switch ( service )
4108    {
4109      case "ENABLE" -> true;
4110      case "DISABLE" -> false;
4111      default -> AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_TYPED ) ||
4112                 AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_NAMED ) ||
4113                 AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.STING_EAGER );
4114    };
4115  }
4116
4117  private boolean isComponentObservableRequired( @Nonnull final AnnotationMirror arezComponent,
4118                                                 final boolean disposeOnDeactivate )
4119  {
4120    final VariableElement variableElement = getAnnotationParameter( arezComponent, "observable" );
4121    return switch ( variableElement.getSimpleName().toString() )
4122    {
4123      case "ENABLE" -> true;
4124      case "DISABLE" -> false;
4125      default -> disposeOnDeactivate;
4126    };
4127  }
4128
4129  private boolean isVerifyRequired( @Nonnull final AnnotationMirror arezComponent,
4130                                    @Nonnull final TypeElement typeElement )
4131  {
4132    final VariableElement parameter = getAnnotationParameter( arezComponent, "verify" );
4133    return switch ( parameter.getSimpleName().toString() )
4134    {
4135      case "ENABLE" -> true;
4136      case "DISABLE" -> false;
4137      default -> ElementsUtil.getMethods( typeElement, processingEnv.getElementUtils(), processingEnv.getTypeUtils() ).
4138        stream().anyMatch( this::hasReferenceAnnotations );
4139    };
4140  }
4141
4142  private boolean isSkipIfDisposed( @Nonnull final ComponentDescriptor component,
4143                                    @Nonnull final AnnotationMirror action )
4144  {
4145    final VariableElement parameter = getAnnotationParameter( action, "skipIfDisposed" );
4146    return switch ( parameter.getSimpleName().toString() )
4147    {
4148      case "ENABLE" -> true;
4149      case "DISABLE" -> false;
4150      default -> component.defaultSkipIfDisposed();
4151    };
4152  }
4153
4154  private boolean hasReferenceAnnotations( @Nonnull final Element method )
4155  {
4156    return AnnotationsUtil.hasAnnotationOfType( method, Constants.REFERENCE_CLASSNAME ) ||
4157           AnnotationsUtil.hasAnnotationOfType( method, Constants.REFERENCE_ID_CLASSNAME ) ||
4158           AnnotationsUtil.hasAnnotationOfType( method, Constants.INVERSE_CLASSNAME );
4159  }
4160
4161  private boolean isEqualsRequired( @Nonnull final AnnotationMirror arezComponent )
4162  {
4163    final VariableElement injectParameter = getAnnotationParameter( arezComponent, "requireEquals" );
4164    return "ENABLE".equals( injectParameter.getSimpleName().toString() );
4165  }
4166
4167  @Nullable
4168  private String getDefaultPriority( @Nonnull final AnnotationMirror arezComponent )
4169  {
4170    final AnnotationValue value =
4171      AnnotationsUtil.findAnnotationValueNoDefaults( arezComponent, "defaultPriority" );
4172    return null == value ? null : ( (VariableElement) value.getValue() ).getSimpleName().toString();
4173  }
4174
4175  private boolean isIdRequired( @Nonnull final AnnotationMirror arezComponent )
4176  {
4177    final VariableElement injectParameter = getAnnotationParameter( arezComponent, "requireId" );
4178    return !"DISABLE".equals( injectParameter.getSimpleName().toString() );
4179  }
4180
4181  private boolean hasInjectAnnotation( @Nonnull final Element method )
4182  {
4183    return AnnotationsUtil.hasAnnotationOfType( method, Constants.INJECT_CLASSNAME );
4184  }
4185
4186  @Nonnull
4187  private <T> T getAnnotationParameter( @Nonnull final AnnotationMirror annotation,
4188                                        @Nonnull final String parameterName )
4189  {
4190    return AnnotationsUtil.getAnnotationValueValue( annotation, parameterName );
4191  }
4192
4193  @Nonnull
4194  private TypeElement getDisposableTypeElement()
4195  {
4196    return getTypeElement( Constants.DISPOSABLE_CLASSNAME );
4197  }
4198
4199  private boolean isDisposableTrackableRequired( @Nonnull final TypeElement element )
4200  {
4201    final var disposeNotifier =
4202      AnnotationsUtil.getEnumAnnotationParameter( element, Constants.COMPONENT_CLASSNAME, "disposeNotifier" );
4203    return switch ( disposeNotifier )
4204    {
4205      case "ENABLE" -> true;
4206      case "DISABLE" -> false;
4207      default -> null == AnnotationsUtil.findAnnotationByType( element, Constants.COMPONENT_CLASSNAME ) ||
4208                 !isService( element );
4209    };
4210  }
4211
4212  @Nonnull
4213  private TypeElement getTypeElement( @Nonnull final String classname )
4214  {
4215    final var typeElement = findTypeElement( classname );
4216    assert null != typeElement;
4217    return typeElement;
4218  }
4219
4220  @Nullable
4221  private TypeElement findTypeElement( @Nonnull final String classname )
4222  {
4223    return processingEnv.getElementUtils().getTypeElement( classname );
4224  }
4225
4226  private boolean isAssignable( @Nonnull final TypeMirror type, @Nonnull final TypeElement typeElement )
4227  {
4228    return processingEnv.getTypeUtils().isAssignable( type, typeElement.asType() );
4229  }
4230
4231  @Nonnull
4232  private String resolveEffectiveEqualityComparator( @Nonnull final TypeElement componentType,
4233                                                     @Nonnull final String annotationName,
4234                                                     @Nonnull final Element element,
4235                                                     @Nonnull final TypeMirror valueType,
4236                                                     @Nonnull final String comparatorClassName )
4237  {
4238    final var effectiveComparator =
4239      Constants.EQUALITY_COMPARATOR_CLASSNAME.equals( comparatorClassName ) ?
4240      deriveDefaultEqualityComparator( valueType ) :
4241      comparatorClassName;
4242    verifyValidEqualityComparator( componentType, annotationName, effectiveComparator, element );
4243    return effectiveComparator;
4244  }
4245
4246  @Nonnull
4247  private String deriveDefaultEqualityComparator( @Nonnull final TypeMirror valueType )
4248  {
4249    if ( TypeKind.DECLARED == valueType.getKind() )
4250    {
4251      final var typeElement = ( (DeclaredType) valueType ).asElement();
4252      final var annotation =
4253        AnnotationsUtil.findAnnotationByType( typeElement, Constants.DEFAULT_EQUALITY_COMPARATOR_CLASSNAME );
4254      if ( null != annotation )
4255      {
4256        final TypeMirror comparatorType = AnnotationsUtil.getAnnotationValueValue( annotation, "value" );
4257        return comparatorType.toString();
4258      }
4259    }
4260    return Constants.OBJECTS_EQUALS_COMPARATOR_CLASSNAME;
4261  }
4262
4263  private void verifyValidEqualityComparator( @Nonnull final TypeElement componentType,
4264                                              @Nonnull final String annotationName,
4265                                              @Nonnull final String comparatorClassName,
4266                                              @Nonnull final Element element )
4267  {
4268    final var comparatorType = getTypeElement( comparatorClassName );
4269    if ( ElementKind.CLASS != comparatorType.getKind() )
4270    {
4271      throw new ProcessorException( annotationName + " resolved equalityComparator of type '" +
4272                                    comparatorClassName + "' but the comparator must be a class.",
4273                                    element );
4274    }
4275    else if ( comparatorType.getModifiers().contains( Modifier.ABSTRACT ) )
4276    {
4277      throw new ProcessorException( annotationName + " resolved equalityComparator of type '" +
4278                                    comparatorClassName + "' but the comparator must not be abstract.",
4279                                    element );
4280    }
4281    else if ( isNonStaticNestedType( comparatorType ) )
4282    {
4283      throw new ProcessorException( annotationName + " resolved equalityComparator of type '" +
4284                                    comparatorClassName + "' but the comparator must be static if nested.",
4285                                    element );
4286    }
4287    else if ( !isTypeAccessibleFromComponent( componentType, comparatorType ) )
4288    {
4289      throw new ProcessorException( annotationName + " resolved equalityComparator of type '" +
4290                                    comparatorClassName + "' but the comparator is not accessible from the " +
4291                                    "generated component.",
4292                                    element );
4293    }
4294    else if ( !hasAccessibleNoArgConstructor( componentType, comparatorType ) )
4295    {
4296      throw new ProcessorException( annotationName + " resolved equalityComparator of type '" +
4297                                    comparatorClassName + "' but the comparator must define an accessible " +
4298                                    "no-arg constructor.",
4299                                    element );
4300    }
4301  }
4302
4303  private boolean hasAccessibleNoArgConstructor( @Nonnull final TypeElement componentType,
4304                                                 @Nonnull final TypeElement comparatorType )
4305  {
4306    final var constructors = ElementFilter.constructorsIn( comparatorType.getEnclosedElements() );
4307    if ( constructors.isEmpty() )
4308    {
4309      return true;
4310    }
4311    else
4312    {
4313      for ( final var constructor : constructors )
4314      {
4315        if ( constructor.getParameters().isEmpty() && isElementAccessibleFromComponent( componentType, constructor ) )
4316        {
4317          return true;
4318        }
4319      }
4320      return false;
4321    }
4322  }
4323
4324  private boolean isTypeAccessibleFromComponent( @Nonnull final TypeElement componentType,
4325                                                 @Nonnull final TypeElement typeElement )
4326  {
4327    if ( isElementAccessibleFromComponent( componentType, typeElement ) )
4328    {
4329      var enclosing = typeElement.getEnclosingElement();
4330      while ( null != enclosing && ElementKind.PACKAGE != enclosing.getKind() )
4331      {
4332        if ( enclosing instanceof final TypeElement enclosingType &&
4333             !isElementAccessibleFromComponent( componentType, enclosingType ) )
4334        {
4335          return false;
4336        }
4337        enclosing = enclosing.getEnclosingElement();
4338      }
4339      return true;
4340    }
4341    else
4342    {
4343      return false;
4344    }
4345  }
4346
4347  private boolean isElementAccessibleFromComponent( @Nonnull final TypeElement componentType,
4348                                                    @Nonnull final Element element )
4349  {
4350    final var modifiers = element.getModifiers();
4351    if ( modifiers.contains( Modifier.PRIVATE ) )
4352    {
4353      return false;
4354    }
4355    else
4356    {
4357      final var owningType = getOwningType( element );
4358      return !ElementsUtil.areTypesInDifferentPackage( owningType, componentType ) ||
4359             modifiers.contains( Modifier.PUBLIC );
4360    }
4361  }
4362
4363  private boolean isProtectedFieldOnInheritedTypeInDifferentPackage( @Nonnull final TypeElement componentType,
4364                                                                     @Nonnull final VariableElement field )
4365  {
4366    final var declaringType = getOwningType( field );
4367    return !Objects.equals( declaringType, componentType ) &&
4368           ElementsUtil.areTypesInDifferentPackage( declaringType, componentType );
4369  }
4370
4371  @Nonnull
4372  private TypeElement getOwningType( @Nonnull final Element element )
4373  {
4374    var current = element;
4375    while ( !( current instanceof TypeElement ) )
4376    {
4377      current = current.getEnclosingElement();
4378    }
4379    return (TypeElement) current;
4380  }
4381
4382  private boolean isNonStaticNestedType( @Nonnull final TypeElement typeElement )
4383  {
4384    final Element enclosing = typeElement.getEnclosingElement();
4385    return ElementKind.PACKAGE != enclosing.getKind() && !typeElement.getModifiers().contains( Modifier.STATIC );
4386  }
4387
4388  private boolean isMethodAProtectedOverride( @Nonnull final TypeElement typeElement,
4389                                              @Nonnull final ExecutableElement method )
4390  {
4391    final var overriddenMethod = ElementsUtil.getOverriddenMethod( processingEnv, typeElement, method );
4392    return null != overriddenMethod && overriddenMethod.getModifiers().contains( Modifier.PROTECTED );
4393  }
4394
4395  private void mustBeStandardRefMethod( @Nonnull final ProcessingEnvironment processingEnv,
4396                                        @Nonnull final ComponentDescriptor descriptor,
4397                                        @Nonnull final ExecutableElement method,
4398                                        @Nonnull final String annotationClassname )
4399  {
4400    mustBeRefMethod( descriptor, method, annotationClassname );
4401    MemberChecks.mustNotHaveAnyParameters( annotationClassname, method );
4402    shouldBeInternalRefMethod( processingEnv, descriptor, method, annotationClassname );
4403  }
4404
4405  private void mustBeRefMethod( @Nonnull final ComponentDescriptor descriptor,
4406                                @Nonnull final ExecutableElement method,
4407                                @Nonnull final String annotationClassname )
4408  {
4409    MemberChecks.mustBeAbstract( annotationClassname, method );
4410    final var typeElement = descriptor.getElement();
4411    MemberChecks.mustNotBePackageAccessInDifferentPackage( typeElement,
4412                                                           Constants.COMPONENT_CLASSNAME,
4413                                                           annotationClassname,
4414                                                           method );
4415    MemberChecks.mustReturnAValue( annotationClassname, method );
4416    MemberChecks.mustNotThrowAnyExceptions( annotationClassname, method );
4417  }
4418
4419  private void mustBeHookHook( @Nonnull final TypeElement targetType,
4420                               @Nonnull final String annotationName,
4421                               @Nonnull final ExecutableElement method )
4422    throws ProcessorException
4423  {
4424    MemberChecks.mustNotBeAbstract( annotationName, method );
4425    MemberChecks.mustBeSubclassCallable( targetType, Constants.COMPONENT_CLASSNAME, annotationName, method );
4426    MemberChecks.mustNotReturnAnyValue( annotationName, method );
4427    MemberChecks.mustNotThrowAnyExceptions( annotationName, method );
4428  }
4429
4430  private void shouldBeInternalRefMethod( @Nonnull final ProcessingEnvironment processingEnv,
4431                                          @Nonnull final ComponentDescriptor descriptor,
4432                                          @Nonnull final ExecutableElement method,
4433                                          @Nonnull final String annotationClassname )
4434  {
4435    if ( MemberChecks.doesMethodNotOverrideInterfaceMethod( processingEnv, descriptor.getElement(), method ) )
4436    {
4437      MemberChecks.shouldNotBePublic( processingEnv,
4438                                      method,
4439                                      annotationClassname,
4440                                      warningKind(),
4441                                      Constants.WARNING_PUBLIC_REF_METHOD,
4442                                      Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME );
4443    }
4444  }
4445
4446  private void shouldBeInternalLifecycleMethod( @Nonnull final ProcessingEnvironment processingEnv,
4447                                                @Nonnull final ComponentDescriptor descriptor,
4448                                                @Nonnull final ExecutableElement method,
4449                                                @Nonnull final String annotationClassname )
4450  {
4451    if ( MemberChecks.doesMethodNotOverrideInterfaceMethod( processingEnv, descriptor.getElement(), method ) )
4452    {
4453      MemberChecks.shouldNotBePublic( processingEnv,
4454                                      method,
4455                                      annotationClassname,
4456                                      warningKind(),
4457                                      Constants.WARNING_PUBLIC_LIFECYCLE_METHOD,
4458                                      Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME );
4459    }
4460  }
4461
4462  private void shouldBeInternalHookMethod( @Nonnull final ProcessingEnvironment processingEnv,
4463                                           @Nonnull final ComponentDescriptor descriptor,
4464                                           @Nonnull final ExecutableElement method,
4465                                           @Nonnull final String annotationClassname )
4466  {
4467    if ( MemberChecks.doesMethodNotOverrideInterfaceMethod( processingEnv, descriptor.getElement(), method ) )
4468    {
4469      MemberChecks.shouldNotBePublic( processingEnv,
4470                                      method,
4471                                      annotationClassname,
4472                                      warningKind(),
4473                                      Constants.WARNING_PUBLIC_HOOK_METHOD,
4474                                      Constants.SUPPRESS_AREZ_WARNINGS_CLASSNAME );
4475    }
4476  }
4477
4478  @Nullable
4479  private String deriveName( @Nonnull final ExecutableElement method,
4480                             @Nonnull final Pattern pattern,
4481                             @Nonnull final String name )
4482    throws ProcessorException
4483  {
4484    if ( Constants.SENTINEL.equals( name ) )
4485    {
4486      final var methodName = method.getSimpleName().toString();
4487      final var matcher = pattern.matcher( methodName );
4488      if ( matcher.find() )
4489      {
4490        final var candidate = matcher.group( 1 );
4491        return firstCharacterToLowerCase( candidate );
4492      }
4493      else
4494      {
4495        return null;
4496      }
4497    }
4498    else
4499    {
4500      return name;
4501    }
4502  }
4503
4504  @Nonnull
4505  private String firstCharacterToLowerCase( @Nonnull final String name )
4506  {
4507    return Character.toLowerCase( name.charAt( 0 ) ) + name.substring( 1 );
4508  }
4509}