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