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