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