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