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