001package react4j.processor;
002
003import com.palantir.javapoet.TypeName;
004import java.io.IOException;
005import java.util.ArrayList;
006import java.util.Arrays;
007import java.util.Collection;
008import java.util.Collections;
009import java.util.Comparator;
010import java.util.LinkedHashMap;
011import java.util.List;
012import java.util.Set;
013import java.util.StringJoiner;
014import java.util.regex.Matcher;
015import java.util.regex.Pattern;
016import javax.annotation.Nonnull;
017import javax.annotation.Nullable;
018import javax.annotation.processing.RoundEnvironment;
019import javax.annotation.processing.SupportedAnnotationTypes;
020import javax.annotation.processing.SupportedOptions;
021import javax.annotation.processing.SupportedSourceVersion;
022import javax.lang.model.SourceVersion;
023import javax.lang.model.element.AnnotationMirror;
024import javax.lang.model.element.AnnotationValue;
025import javax.lang.model.element.Element;
026import javax.lang.model.element.ElementKind;
027import javax.lang.model.element.ExecutableElement;
028import javax.lang.model.element.Modifier;
029import javax.lang.model.element.TypeElement;
030import javax.lang.model.element.TypeParameterElement;
031import javax.lang.model.element.VariableElement;
032import javax.lang.model.type.ExecutableType;
033import javax.lang.model.type.TypeKind;
034import javax.lang.model.type.TypeMirror;
035import javax.lang.model.type.TypeVariable;
036import javax.lang.model.util.Elements;
037import javax.lang.model.util.Types;
038import org.realityforge.proton.AbstractStandardProcessor;
039import org.realityforge.proton.AnnotationsUtil;
040import org.realityforge.proton.DeferredElementSet;
041import org.realityforge.proton.ElementsUtil;
042import org.realityforge.proton.MemberChecks;
043import org.realityforge.proton.ProcessorException;
044import org.realityforge.proton.StopWatch;
045
046/**
047 * Annotation processor that analyzes React4j annotated source code and generates models from the annotations.
048 */
049@SuppressWarnings( "Duplicates" )
050@SupportedAnnotationTypes( Constants.VIEW_CLASSNAME )
051@SupportedSourceVersion( SourceVersion.RELEASE_17 )
052@SupportedOptions( { "react4j.defer.unresolved",
053                     "react4j.defer.errors",
054                     "react4j.debug",
055                     "react4j.profile",
056                     "react4j.verbose_out_of_round.errors",
057                     "react4j.warnings_as_errors" } )
058public final class React4jProcessor
059  extends AbstractStandardProcessor
060{
061  private static final String SENTINEL_NAME = "<default>";
062  private static final Pattern DEFAULT_GETTER_PATTERN = Pattern.compile( "^get([A-Z].*)Default$" );
063  private static final Pattern VALIDATE_INPUT_PATTERN = Pattern.compile( "^validate([A-Z].*)$" );
064  private static final Pattern LAST_INPUT_PATTERN = Pattern.compile( "^last([A-Z].*)$" );
065  private static final Pattern PREV_INPUT_PATTERN = Pattern.compile( "^prev([A-Z].*)$" );
066  private static final Pattern INPUT_PATTERN = Pattern.compile( "^([a-z].*)$" );
067  private static final Pattern GETTER_PATTERN = Pattern.compile( "^get([A-Z].*)$" );
068  private static final Pattern ISSER_PATTERN = Pattern.compile( "^is([A-Z].*)$" );
069  @Nonnull
070  private final DeferredElementSet _deferredTypes = new DeferredElementSet();
071  @Nonnull
072  private final StopWatch _analyzeViewStopWatch = new StopWatch( "Analyze View" );
073
074  @Override
075  protected void collectStopWatches( @Nonnull final Collection<StopWatch> stopWatches )
076  {
077    stopWatches.add( _analyzeViewStopWatch );
078  }
079
080  @Override
081  public boolean process( @Nonnull final Set<? extends TypeElement> annotations, @Nonnull final RoundEnvironment env )
082  {
083    debugAnnotationProcessingRootElements( env );
084    collectRootTypeNames( env );
085    processTypeElements( annotations,
086                         env,
087                         Constants.VIEW_CLASSNAME,
088                         _deferredTypes,
089                         _analyzeViewStopWatch.getName(),
090                         this::process,
091                         _analyzeViewStopWatch );
092    errorIfProcessingOverAndInvalidTypesDetected( env );
093    clearRootTypeNamesIfProcessingOver( env );
094    return true;
095  }
096
097  @Override
098  @Nonnull
099  protected String getIssueTrackerURL()
100  {
101    return "https://github.com/react4j/react4j/issues";
102  }
103
104  @Nonnull
105  @Override
106  protected String getOptionPrefix()
107  {
108    return "react4j";
109  }
110
111  private void process( @Nonnull final TypeElement element )
112    throws IOException, ProcessorException
113  {
114    final ViewDescriptor descriptor = parse( element );
115    final String packageName = descriptor.getPackageName();
116    emitTypeSpec( packageName, ViewGenerator.buildType( processingEnv, descriptor ) );
117    emitTypeSpec( packageName, BuilderGenerator.buildType( processingEnv, descriptor ) );
118    if ( descriptor.needsInjection() )
119    {
120      emitTypeSpec( packageName, FactoryGenerator.buildType( processingEnv, descriptor ) );
121    }
122  }
123
124  /**
125   * Return true if there is any method annotated with @PostConstruct.
126   */
127  private boolean hasPostConstruct( @Nonnull final List<ExecutableElement> methods )
128  {
129    return
130      methods.stream().anyMatch( e -> AnnotationsUtil.hasAnnotationOfType( e, Constants.POST_CONSTRUCT_CLASSNAME ) );
131  }
132
133  @Nonnull
134  private ViewDescriptor parse( @Nonnull final TypeElement typeElement )
135  {
136    final String name = deriveViewName( typeElement );
137    final ViewType type = extractViewType( typeElement );
138    final boolean exportBuilder = extractExportBuilder( typeElement );
139    final List<ExecutableElement> methods =
140      ElementsUtil.getMethods( typeElement, processingEnv.getElementUtils(), processingEnv.getTypeUtils() );
141
142    final boolean hasPostConstruct = hasPostConstruct( methods );
143    final boolean shouldSetDefaultPriority = shouldSetDefaultPriority( methods );
144
145    MemberChecks.mustNotBeFinal( Constants.VIEW_CLASSNAME, typeElement );
146    MemberChecks.mustBeAbstract( Constants.VIEW_CLASSNAME, typeElement );
147    if ( ElementKind.CLASS != typeElement.getKind() )
148    {
149      throw new ProcessorException( MemberChecks.must( Constants.VIEW_CLASSNAME, "be a class" ),
150                                    typeElement );
151    }
152    else if ( ElementsUtil.isNonStaticNestedClass( typeElement ) )
153    {
154      throw new ProcessorException( MemberChecks.toSimpleName( Constants.VIEW_CLASSNAME ) +
155                                    " target must not be a non-static nested class",
156                                    typeElement );
157    }
158    final List<ExecutableElement> constructors = ElementsUtil.getConstructors( typeElement );
159    if ( 1 != constructors.size() || !isConstructorValid( constructors.get( 0 ) ) )
160    {
161      throw new ProcessorException( MemberChecks.must( Constants.VIEW_CLASSNAME,
162                                                       "have a single, package-access constructor or the default constructor" ),
163                                    typeElement );
164    }
165    final ExecutableElement constructor = constructors.get( 0 );
166    verifyConstructorParameterOrder( constructor );
167    verifyPostConstructMethodName( methods );
168
169    final boolean sting = deriveSting( constructor );
170    final boolean notSyntheticConstructor =
171      Elements.Origin.EXPLICIT == processingEnv.getElementUtils().getOrigin( constructor );
172
173    final ViewDescriptor descriptor =
174      new ViewDescriptor( name,
175                          typeElement,
176                          constructor,
177                          type,
178                          exportBuilder,
179                          sting,
180                          notSyntheticConstructor,
181                          hasPostConstruct,
182                          shouldSetDefaultPriority );
183
184    if ( typeElement.getModifiers().contains( Modifier.PUBLIC ) &&
185         ElementsUtil.isWarningNotSuppressed( typeElement,
186                                              Constants.WARNING_PUBLIC_VIEW,
187                                              Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) )
188    {
189      final String message =
190        MemberChecks.shouldNot( Constants.VIEW_CLASSNAME,
191                                "be public. " +
192                                MemberChecks.suppressedBy( Constants.WARNING_PUBLIC_VIEW,
193                                                           Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) );
194      warning( message, typeElement );
195    }
196
197    for ( final Element element : descriptor.getElement().getEnclosedElements() )
198    {
199      if ( ElementKind.METHOD == element.getKind() )
200      {
201        final ExecutableElement method = (ExecutableElement) element;
202        if ( method.getModifiers().contains( Modifier.PUBLIC ) &&
203             MemberChecks.doesMethodNotOverrideInterfaceMethod( processingEnv, typeElement, method ) &&
204             ElementsUtil.isWarningNotSuppressed( method,
205                                                  Constants.WARNING_PUBLIC_METHOD,
206                                                  Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) )
207        {
208          final String message =
209            MemberChecks.shouldNot( Constants.VIEW_CLASSNAME,
210                                    "declare a public method. " +
211                                    MemberChecks.suppressedBy( Constants.WARNING_PUBLIC_METHOD,
212                                                               Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) );
213          warning( message, method );
214        }
215        if ( method.getModifiers().contains( Modifier.FINAL ) &&
216             ElementsUtil.isWarningNotSuppressed( method,
217                                                  Constants.WARNING_FINAL_METHOD,
218                                                  Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) )
219        {
220          final String message =
221            MemberChecks.shouldNot( Constants.VIEW_CLASSNAME,
222                                    "declare a final method. " +
223                                    MemberChecks.suppressedBy( Constants.WARNING_FINAL_METHOD,
224                                                               Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) );
225          warning( message, method );
226        }
227        if ( method.getModifiers().contains( Modifier.PROTECTED ) &&
228             ElementsUtil.isWarningNotSuppressed( method,
229                                                  Constants.WARNING_PROTECTED_METHOD,
230                                                  Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) &&
231             !isMethodAProtectedOverride( typeElement, method ) )
232        {
233          final String message =
234            MemberChecks.shouldNot( Constants.VIEW_CLASSNAME,
235                                    "declare a protected method. " +
236                                    MemberChecks.suppressedBy( Constants.WARNING_PROTECTED_METHOD,
237                                                               Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) );
238          warning( message, method );
239        }
240      }
241    }
242
243    determineViewCapabilities( descriptor, typeElement );
244    determineInputs( descriptor, methods );
245    determinePreludeCheckCandidates( descriptor, typeElement, methods );
246    determineInputValidatesMethods( descriptor, methods );
247    determineOnInputChangeMethods( descriptor, methods );
248    determineDefaultInputsMethods( descriptor, methods );
249    determineDefaultInputsFields( descriptor );
250    determinePreUpdateMethod( typeElement, descriptor, methods );
251    determinePostMountOrUpdateMethod( typeElement, descriptor, methods );
252    determinePostUpdateMethod( typeElement, descriptor, methods );
253    determinePostMountMethod( typeElement, descriptor, methods );
254    determineOnErrorMethod( typeElement, descriptor, methods );
255    determineScheduleRenderMethods( typeElement, descriptor, methods );
256    determinePublishMethods( typeElement, descriptor, methods );
257    determinePreRenderMethods( typeElement, descriptor, methods );
258    determinePostRenderMethods( typeElement, descriptor, methods );
259    determineRenderMethod( typeElement, descriptor, methods );
260
261    for ( final InputDescriptor input : descriptor.getInputs() )
262    {
263      if ( !isInputRequired( input ) )
264      {
265        input.markAsOptional();
266      }
267      else
268      {
269        if ( input.isFromTreeContext() )
270        {
271          throw new ProcessorException( MemberChecks.mustNot( Constants.INPUT_CLASSNAME,
272                                                              "specify require=ENABLE when fromTreeContext=true" ),
273                                        input.getElement() );
274        }
275      }
276    }
277
278    /*
279     * Sorting must occur after @InputDefault has been processed to ensure the sorting
280     * correctly sorts optional inputs after required inputs.
281     */
282    descriptor.sortInputs();
283
284    verifyInputsNotAnnotatedWithArezAnnotations( descriptor );
285
286    return descriptor;
287  }
288
289  private boolean isMethodAProtectedOverride( @Nonnull final TypeElement typeElement,
290                                              @Nonnull final ExecutableElement method )
291  {
292    final ExecutableElement overriddenMethod = ElementsUtil.getOverriddenMethod( processingEnv, typeElement, method );
293    return null != overriddenMethod && overriddenMethod.getModifiers().contains( Modifier.PROTECTED );
294  }
295
296  private boolean deriveSting( @Nonnull final ExecutableElement constructor )
297  {
298    return !getInjectableConstructorParameters( constructor ).isEmpty() &&
299           null != processingEnv.getElementUtils().getTypeElement( Constants.STING_INJECTABLE_CLASSNAME );
300  }
301
302  private void verifyConstructorParameterOrder( @Nonnull final ExecutableElement constructor )
303  {
304    if ( Elements.Origin.EXPLICIT == processingEnv.getElementUtils().getOrigin( constructor ) &&
305         constructor.getParameters().size() > 1 &&
306         ElementsUtil.isWarningNotSuppressed( constructor,
307                                              Constants.WARNING_CONSTRUCTOR_PARAMETER_ORDER,
308                                              Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) )
309    {
310      final StringJoiner actualOrder = new StringJoiner( ", " );
311      int maxObservedGroup = -1;
312      boolean invalidOrder = false;
313      for ( final VariableElement parameter : constructor.getParameters() )
314      {
315        final int group = classifyConstructorParameterGroup( parameter );
316        actualOrder.add( getConstructorParameterGroupLabel( group ) );
317        if ( group < maxObservedGroup )
318        {
319          invalidOrder = true;
320        }
321        else
322        {
323          maxObservedGroup = group;
324        }
325      }
326
327      if ( invalidOrder )
328      {
329        final String message =
330          MemberChecks.should( Constants.VIEW_CLASSNAME,
331                               "declare constructor parameters in the order inject, tree, input. Actual order: " +
332                               actualOrder + ". " +
333                               MemberChecks.suppressedBy( Constants.WARNING_CONSTRUCTOR_PARAMETER_ORDER,
334                                                          Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) );
335        warning( message, constructor );
336      }
337    }
338  }
339
340  private void verifyPostConstructMethodName( @Nonnull final List<ExecutableElement> methods )
341  {
342    final List<ExecutableElement> postConstructMethods =
343      methods.stream()
344        .filter( e -> AnnotationsUtil.hasAnnotationOfType( e, Constants.POST_CONSTRUCT_CLASSNAME ) )
345        .toList();
346
347    if ( 1 == postConstructMethods.size() )
348    {
349      final ExecutableElement method = postConstructMethods.get( 0 );
350      if ( !"postConstruct".contentEquals( method.getSimpleName() ) &&
351           ElementsUtil.isWarningNotSuppressed( method,
352                                                Constants.WARNING_POST_CONSTRUCT_NAME,
353                                                Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) )
354      {
355        final String message =
356          MemberChecks.should( Constants.POST_CONSTRUCT_CLASSNAME,
357                               "be named 'postConstruct' when it is the only @PostConstruct method in the @View. " +
358                               MemberChecks.suppressedBy( Constants.WARNING_POST_CONSTRUCT_NAME,
359                                                          Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) );
360        warning( message, method );
361      }
362    }
363  }
364
365  private int classifyConstructorParameterGroup( @Nonnull final VariableElement parameter )
366  {
367    return isInputParameter( parameter ) ? isFromTreeContextInput( parameter ) ? 1 : 2 : 0;
368  }
369
370  @Nonnull
371  private String getConstructorParameterGroupLabel( final int group )
372  {
373    return switch ( group )
374    {
375      case 0 -> "inject";
376      case 1 -> "tree";
377      case 2 -> "input";
378      default -> throw new IllegalArgumentException( "Unexpected constructor parameter group: " + group );
379    };
380  }
381
382  @Nonnull
383  private List<VariableElement> getInjectableConstructorParameters( @Nonnull final ExecutableElement constructor )
384  {
385    return constructor.getParameters().stream()
386      .map( parameter -> (VariableElement) parameter )
387      .filter( parameter -> !isInputParameter( parameter ) )
388      .toList();
389  }
390
391  private boolean isInputParameter( @Nonnull final VariableElement parameter )
392  {
393    return AnnotationsUtil.hasAnnotationOfType( parameter, Constants.INPUT_CLASSNAME );
394  }
395
396  private boolean isConstructorValid( @Nonnull final ExecutableElement ctor )
397  {
398    if ( Elements.Origin.EXPLICIT != processingEnv.getElementUtils().getOrigin( ctor ) )
399    {
400      return true;
401    }
402    else
403    {
404      final Set<Modifier> modifiers = ctor.getModifiers();
405      return
406        !modifiers.contains( Modifier.PRIVATE ) &&
407        !modifiers.contains( Modifier.PUBLIC ) &&
408        !modifiers.contains( Modifier.PROTECTED );
409    }
410  }
411
412  private void verifyInputsNotAnnotatedWithArezAnnotations( @Nonnull final ViewDescriptor descriptor )
413  {
414    for ( final InputDescriptor input : descriptor.getInputs() )
415    {
416      final Element element = input.getElement();
417      for ( final AnnotationMirror mirror : element.getAnnotationMirrors() )
418      {
419        final String classname = mirror.getAnnotationType().toString();
420        if ( classname.startsWith( "arez.annotations." ) )
421        {
422          throw new ProcessorException( "@Input target must not be annotated with any arez annotations but " +
423                                        "is annotated by '" + classname + "'.", element );
424        }
425      }
426    }
427  }
428
429  private void determineOnInputChangeMethods( @Nonnull final ViewDescriptor descriptor,
430                                              @Nonnull final List<ExecutableElement> methods )
431  {
432    final List<ExecutableElement> onInputChangeMethods =
433      methods
434        .stream()
435        .filter( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.ON_INPUT_CHANGE_CLASSNAME ) )
436        .toList();
437
438    final ArrayList<OnInputChangeDescriptor> onInputChangeDescriptors = new ArrayList<>();
439    for ( final ExecutableElement method : onInputChangeMethods )
440    {
441      final VariableElement phase = (VariableElement)
442        AnnotationsUtil.getAnnotationValue( method, Constants.ON_INPUT_CHANGE_CLASSNAME, "phase" ).getValue();
443      final boolean preUpdate = phase.getSimpleName().toString().equals( "PRE" );
444
445      final List<? extends VariableElement> parameters = method.getParameters();
446      final ExecutableType methodType = resolveMethodType( descriptor, method );
447      final List<? extends TypeMirror> parameterTypes = methodType.getParameterTypes();
448
449      MemberChecks.mustBeSubclassCallable( descriptor.getElement(),
450                                           Constants.VIEW_CLASSNAME,
451                                           Constants.ON_INPUT_CHANGE_CLASSNAME,
452                                           method );
453      MemberChecks.mustNotThrowAnyExceptions( Constants.ON_INPUT_CHANGE_CLASSNAME, method );
454      MemberChecks.mustNotReturnAnyValue( Constants.ON_INPUT_CHANGE_CLASSNAME, method );
455
456      final int parameterCount = parameters.size();
457      if ( 0 == parameterCount )
458      {
459        throw new ProcessorException( "@OnInputChange target must have at least 1 parameter.", method );
460      }
461      final List<InputDescriptor> inputDescriptors = new ArrayList<>( parameterCount );
462      for ( int i = 0; i < parameterCount; i++ )
463      {
464        final VariableElement parameter = parameters.get( i );
465        final String name = deriveOnInputChangeName( parameter );
466        final InputDescriptor input = descriptor.findInputNamed( name );
467        if ( null == input )
468        {
469          throw new ProcessorException( "@OnInputChange target has a parameter named '" +
470                                        parameter.getSimpleName() + "' and the parameter is associated with a " +
471                                        "@Input named '" + name + "' but there is no corresponding @Input " +
472                                        "annotated method.", parameter );
473        }
474        final Types typeUtils = processingEnv.getTypeUtils();
475        if ( !typeUtils.isAssignable( parameterTypes.get( i ), input.getType() ) )
476        {
477          throw new ProcessorException( "@OnInputChange target has a parameter named '" +
478                                        parameter.getSimpleName() + "' and the parameter type is not " +
479                                        "assignable to the return type of the associated @Input annotated method.",
480                                        method );
481        }
482        final boolean mismatchedNullability =
483          (
484            AnnotationsUtil.hasNonnullAnnotation( parameter ) &&
485            AnnotationsUtil.hasNullableAnnotation( input.getElement() )
486          ) ||
487          (
488            AnnotationsUtil.hasNullableAnnotation( parameter ) &&
489            input.isNonNull() );
490
491        if ( mismatchedNullability )
492        {
493          throw new ProcessorException( "@OnInputChange target has a parameter named '" +
494                                        parameter.getSimpleName() + "' that has a nullability annotation " +
495                                        "incompatible with the associated @Input method named " +
496                                        method.getSimpleName(), method );
497        }
498        if ( input.isImmutable() )
499        {
500          throw new ProcessorException( "@OnInputChange target has a parameter named '" +
501                                        parameter.getSimpleName() + "' that is associated with an immutable @Input.",
502                                        method );
503        }
504        inputDescriptors.add( input );
505      }
506      onInputChangeDescriptors.add( new OnInputChangeDescriptor( method, inputDescriptors, preUpdate ) );
507    }
508    descriptor.setOnInputChangeDescriptors( onInputChangeDescriptors );
509  }
510
511  @Nonnull
512  private String deriveOnInputChangeName( @Nonnull final VariableElement parameter )
513  {
514    final AnnotationValue value =
515      AnnotationsUtil.findAnnotationValue( parameter, Constants.INPUT_REF_CLASSNAME, "value" );
516
517    if ( null != value )
518    {
519      return (String) value.getValue();
520    }
521    else
522    {
523      final String parameterName = parameter.getSimpleName().toString();
524      if ( LAST_INPUT_PATTERN.matcher( parameterName ).matches() ||
525           PREV_INPUT_PATTERN.matcher( parameterName ).matches() )
526      {
527        return Character.toLowerCase( parameterName.charAt( 4 ) ) + parameterName.substring( 5 );
528      }
529      else if ( INPUT_PATTERN.matcher( parameterName ).matches() )
530      {
531        return parameterName;
532      }
533      else
534      {
535        throw new ProcessorException( "@OnInputChange target has a parameter named '" + parameterName +
536                                      "' is not explicitly associated with a input using @InputRef nor does it " +
537                                      "follow required naming conventions 'prev[MyInput]', 'last[MyInput]' or " +
538                                      "'[myInput]'.", parameter );
539      }
540    }
541  }
542
543  private void determineInputValidatesMethods( @Nonnull final ViewDescriptor descriptor,
544                                               @Nonnull final List<ExecutableElement> methods )
545  {
546    final List<ExecutableElement> inputValidateMethods =
547      methods
548        .stream()
549        .filter( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.INPUT_VALIDATE_CLASSNAME ) )
550        .toList();
551
552    for ( final ExecutableElement method : inputValidateMethods )
553    {
554      final String name = deriveInputValidateName( method );
555      final InputDescriptor input = descriptor.findInputNamed( name );
556      if ( null == input )
557      {
558        throw new ProcessorException( "@InputValidate target for input named '" + name + "' has no corresponding " +
559                                      "@Input annotated method.", method );
560      }
561      if ( 1 != method.getParameters().size() )
562      {
563        throw new ProcessorException( "@InputValidate target must have exactly 1 parameter", method );
564      }
565      final ExecutableType methodType = resolveMethodType( descriptor, method );
566      if ( !processingEnv.getTypeUtils().isAssignable( methodType.getParameterTypes().get( 0 ), input.getType() ) )
567      {
568        throw new ProcessorException( "@InputValidate target has a parameter type that is not assignable to the " +
569                                      "return type of the associated @Input annotated method.", method );
570      }
571      MemberChecks.mustBeSubclassCallable( descriptor.getElement(),
572                                           Constants.VIEW_CLASSNAME,
573                                           Constants.INPUT_VALIDATE_CLASSNAME,
574                                           method );
575      MemberChecks.mustNotThrowAnyExceptions( Constants.INPUT_VALIDATE_CLASSNAME, method );
576      MemberChecks.mustNotReturnAnyValue( Constants.INPUT_VALIDATE_CLASSNAME, method );
577
578      final VariableElement param = method.getParameters().get( 0 );
579      final boolean mismatchedNullability =
580        (
581          AnnotationsUtil.hasNonnullAnnotation( param ) &&
582          AnnotationsUtil.hasNullableAnnotation( input.getElement() )
583        ) ||
584        (
585          AnnotationsUtil.hasNullableAnnotation( param ) &&
586          input.isNonNull() );
587
588      if ( mismatchedNullability )
589      {
590        throw new ProcessorException( "@InputValidate target has a parameter that has a nullability annotation " +
591                                      "incompatible with the associated @Input method named " +
592                                      input.getElement().getSimpleName(), method );
593      }
594      input.setValidateMethod( method );
595    }
596  }
597
598  @Nonnull
599  private String deriveInputValidateName( @Nonnull final Element element )
600    throws ProcessorException
601  {
602    final String name =
603      (String) AnnotationsUtil.getAnnotationValue( element, Constants.INPUT_VALIDATE_CLASSNAME, "name" )
604        .getValue();
605
606    if ( isSentinelName( name ) )
607    {
608      final String deriveName = deriveName( element, VALIDATE_INPUT_PATTERN, name );
609      if ( null == deriveName )
610      {
611        throw new ProcessorException( "@InputValidate target has not specified name nor is it named according " +
612                                      "to the convention 'validate[Name]Input'.", element );
613      }
614      return deriveName;
615    }
616    else
617    {
618      if ( !SourceVersion.isIdentifier( name ) )
619      {
620        throw new ProcessorException( "@InputValidate target specified an invalid name '" + name + "'. The " +
621                                      "name must be a valid java identifier.", element );
622      }
623      else if ( SourceVersion.isKeyword( name ) )
624      {
625        throw new ProcessorException( "@InputValidate target specified an invalid name '" + name + "'. The " +
626                                      "name must not be a java keyword.", element );
627      }
628      return name;
629    }
630  }
631
632  private void determineDefaultInputsMethods( @Nonnull final ViewDescriptor descriptor,
633                                              @Nonnull final List<ExecutableElement> methods )
634  {
635    final List<ExecutableElement> defaultInputsMethods =
636      methods
637        .stream()
638        .filter( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.INPUT_DEFAULT_CLASSNAME ) )
639        .toList();
640
641    for ( final ExecutableElement method : defaultInputsMethods )
642    {
643      final String name = deriveInputDefaultName( method );
644      final InputDescriptor input = descriptor.findInputNamed( name );
645      if ( null == input )
646      {
647        throw new ProcessorException( "@InputDefault target for input named '" + name + "' has no corresponding " +
648                                      "@Input annotated method.", method );
649      }
650      final ExecutableType methodType = resolveMethodType( descriptor, method );
651      if ( !processingEnv.getTypeUtils().isAssignable( methodType.getReturnType(), input.getType() ) )
652      {
653        throw new ProcessorException( "@InputDefault target has a return type that is not assignable to the " +
654                                      "return type of the associated @Input annotated method.", method );
655      }
656      MemberChecks.mustBeStaticallySubclassCallable( descriptor.getElement(),
657                                                     Constants.VIEW_CLASSNAME,
658                                                     Constants.INPUT_DEFAULT_CLASSNAME,
659                                                     method );
660      MemberChecks.mustNotHaveAnyParameters( Constants.INPUT_DEFAULT_CLASSNAME, method );
661      MemberChecks.mustNotThrowAnyExceptions( Constants.INPUT_DEFAULT_CLASSNAME, method );
662      MemberChecks.mustReturnAValue( Constants.INPUT_DEFAULT_CLASSNAME, method );
663
664      input.setDefaultMethod( method );
665    }
666  }
667
668  private void determineDefaultInputsFields( @Nonnull final ViewDescriptor descriptor )
669  {
670    final List<VariableElement> defaultInputsFields =
671      ElementsUtil.getFields( descriptor.getElement() ).stream()
672        .filter( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.INPUT_DEFAULT_CLASSNAME ) )
673        .toList();
674
675    for ( final VariableElement field : defaultInputsFields )
676    {
677      final String name = deriveInputDefaultName( field );
678      final InputDescriptor input = descriptor.findInputNamed( name );
679      if ( null == input )
680      {
681        throw new ProcessorException( "@InputDefault target for input named '" + name + "' has no corresponding " +
682                                      "@Input annotated method.", field );
683      }
684      if ( !processingEnv.getTypeUtils().isAssignable( field.asType(), input.getType() ) )
685      {
686        throw new ProcessorException( "@InputDefault target has a type that is not assignable to the " +
687                                      "return type of the associated @Input annotated method.", field );
688      }
689      MemberChecks.mustBeStaticallySubclassCallable( descriptor.getElement(),
690                                                     Constants.VIEW_CLASSNAME,
691                                                     Constants.INPUT_DEFAULT_CLASSNAME,
692                                                     field );
693      MemberChecks.mustBeFinal( Constants.INPUT_DEFAULT_CLASSNAME, field );
694      input.setDefaultField( field );
695    }
696  }
697
698  @Nonnull
699  private String deriveInputDefaultName( @Nonnull final Element element )
700    throws ProcessorException
701  {
702    final String name =
703      (String) AnnotationsUtil.getAnnotationValue( element, Constants.INPUT_DEFAULT_CLASSNAME, "name" )
704        .getValue();
705
706    if ( isSentinelName( name ) )
707    {
708      if ( element instanceof ExecutableElement )
709      {
710        final String deriveName = deriveName( element, DEFAULT_GETTER_PATTERN, name );
711        if ( null == deriveName )
712        {
713          throw new ProcessorException( "@InputDefault target has not specified name nor is it named according " +
714                                        "to the convention 'get[Name]Default'.", element );
715        }
716        return deriveName;
717      }
718      else
719      {
720        final String fieldName = element.getSimpleName().toString();
721        boolean matched = true;
722        final int lengthPrefix = "DEFAULT_".length();
723        final int length = fieldName.length();
724        if ( fieldName.startsWith( "DEFAULT_" ) && length > lengthPrefix )
725        {
726          for ( int i = lengthPrefix; i < length; i++ )
727          {
728            final char ch = fieldName.charAt( i );
729            if ( Character.isLowerCase( ch ) ||
730                 (
731                   ( i != lengthPrefix || !Character.isJavaIdentifierStart( ch ) ) &&
732                   ( i == lengthPrefix || !Character.isJavaIdentifierPart( ch ) )
733                 ) )
734            {
735              matched = false;
736              break;
737            }
738          }
739        }
740        else
741        {
742          matched = false;
743        }
744        if ( matched )
745        {
746          return uppercaseConstantToPascalCase( fieldName.substring( lengthPrefix ) );
747        }
748        else
749        {
750          throw new ProcessorException( "@InputDefault target has not specified name nor is it named according " +
751                                        "to the convention 'DEFAULT_[NAME]'.", element );
752        }
753      }
754    }
755    else
756    {
757      if ( !SourceVersion.isIdentifier( name ) )
758      {
759        throw new ProcessorException( "@InputDefault target specified an invalid name '" + name + "'. The " +
760                                      "name must be a valid java identifier.", element );
761      }
762      else if ( SourceVersion.isKeyword( name ) )
763      {
764        throw new ProcessorException( "@InputDefault target specified an invalid name '" + name + "'. The " +
765                                      "name must not be a java keyword.", element );
766      }
767      return name;
768    }
769  }
770
771  @Nonnull
772  private String uppercaseConstantToPascalCase( @Nonnull final String candidate )
773  {
774    final String s = candidate.toLowerCase();
775    final StringBuilder sb = new StringBuilder();
776    boolean uppercase = false;
777    for ( int i = 0; i < s.length(); i++ )
778    {
779      final char ch = s.charAt( i );
780      if ( '_' == ch )
781      {
782        uppercase = true;
783      }
784      else if ( uppercase )
785      {
786        sb.append( Character.toUpperCase( ch ) );
787        uppercase = false;
788      }
789      else
790      {
791        sb.append( ch );
792      }
793    }
794    return sb.toString();
795  }
796
797  private void determineInputs( @Nonnull final ViewDescriptor descriptor,
798                                @Nonnull final List<ExecutableElement> methods )
799  {
800    final List<InputDescriptor> inputs = new ArrayList<>();
801    methods
802      .stream()
803      .filter( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.INPUT_CLASSNAME ) )
804      .map( m -> createMethodInputDescriptor( descriptor, methods, m ) )
805      .forEach( input -> addInputDescriptor( inputs, input ) );
806    descriptor
807      .getConstructor()
808      .getParameters()
809      .stream()
810      .filter( this::isInputParameter )
811      .map( p -> createConstructorInputDescriptor( descriptor, p ) )
812      .forEach( input -> addInputDescriptor( inputs, input ) );
813
814    final var childrenInput = inputs.stream().filter( p -> p.getName().equals( "children" ) ).findAny().orElse( null );
815    final var childInput = inputs.stream().filter( p -> p.getName().equals( "child" ) ).findAny().orElse( null );
816    if ( null != childrenInput && null != childInput )
817    {
818      throw new ProcessorException( "Multiple candidate children @Input annotated methods: " +
819                                    childrenInput.getElement().getSimpleName() + " and " +
820                                    childInput.getElement().getSimpleName(),
821                                    childrenInput.getElement() );
822    }
823
824    descriptor.setInputs( inputs );
825  }
826
827  private boolean isDisposableDerivableAtCompileTime( @Nonnull final Element type )
828  {
829    final var kind = type.getKind();
830    if ( ElementKind.CLASS == kind &&
831         AnnotationsUtil.hasAnnotationOfType( type, Constants.AREZ_COMPONENT_CLASSNAME ) )
832    {
833      return true;
834    }
835    else if ( ElementKind.CLASS == kind || ElementKind.INTERFACE == kind )
836    {
837      if ( AnnotationsUtil.hasAnnotationOfType( type, Constants.AREZ_COMPONENT_LIKE_CLASSNAME ) )
838      {
839        return true;
840      }
841      else
842      {
843        final var typeElement = processingEnv.getElementUtils().getTypeElement( Constants.DISPOSABLE_CLASSNAME );
844        return null != typeElement &&
845               processingEnv.getTypeUtils().isAssignable( type.asType(), typeElement.asType() );
846      }
847    }
848    else
849    {
850      return false;
851    }
852  }
853
854  private void determinePreludeCheckCandidates( @Nonnull final ViewDescriptor descriptor,
855                                                @Nonnull final TypeElement typeElement,
856                                                @Nonnull final List<ExecutableElement> methods )
857  {
858    final var candidates = new ArrayList<PreludeChecksDescriptor>();
859
860    final var fields = new LinkedHashMap<String, VariableElement>();
861    for ( final var member : processingEnv.getElementUtils().getAllMembers( typeElement ) )
862    {
863      if ( ElementKind.FIELD == member.getKind() )
864      {
865        fields.putIfAbsent( member.getSimpleName().toString(), (VariableElement) member );
866      }
867    }
868
869    for ( final var field : fields.values() )
870    {
871      for ( final var annotation : new String[]{ Constants.COMPONENT_DEPENDENCY_CLASSNAME,
872                                                 Constants.AUTO_OBSERVE_CLASSNAME } )
873      {
874        if ( AnnotationsUtil.hasAnnotationOfType( field, annotation ) )
875        {
876          MemberChecks.mustNotBePackageAccessInDifferentPackage( descriptor.getElement(),
877                                                                 Constants.VIEW_CLASSNAME,
878                                                                 annotation,
879                                                                 field );
880          final var fieldType = processingEnv.getTypeUtils().asMemberOf( descriptor.getDeclaredType(), field );
881          final var observationMode = determinePreludeCheckObservationMode( field, fieldType );
882          candidates.add( new PreludeChecksDescriptor( field, fieldType, observationMode ) );
883        }
884      }
885    }
886    for ( final var method : methods )
887    {
888      for ( final var annotation : new String[]{ Constants.COMPONENT_DEPENDENCY_CLASSNAME,
889                                                 Constants.AUTO_OBSERVE_CLASSNAME } )
890      {
891        if ( AnnotationsUtil.hasAnnotationOfType( method, annotation ) )
892        {
893          MemberChecks.mustNotBePackageAccessInDifferentPackage( descriptor.getElement(),
894                                                                 Constants.VIEW_CLASSNAME,
895                                                                 annotation,
896                                                                 method );
897          final var returnType = resolveMethodType( descriptor, method ).getReturnType();
898          final var observationMode = determinePreludeCheckObservationMode( method, returnType );
899          candidates.add( new PreludeChecksDescriptor( method, returnType, observationMode ) );
900        }
901      }
902    }
903
904    descriptor.setPreludeCheckCandidates( candidates );
905  }
906
907  private void addInputDescriptor( @Nonnull final List<InputDescriptor> inputs, @Nonnull final InputDescriptor input )
908  {
909    final var existing = inputs.stream().filter( p -> p.getName().equals( input.getName() ) ).findAny().orElse( null );
910    if ( null != existing )
911    {
912      throw new ProcessorException( "Multiple @Input declarations for input named '" + input.getName() +
913                                    "': " + existing.getElement().getSimpleName() + " and " +
914                                    input.getElement().getSimpleName(),
915                                    input.getElement() );
916    }
917    inputs.add( input );
918  }
919
920  private boolean isInputRequired( @Nonnull final InputDescriptor input )
921  {
922    final String requiredValue = input.getRequiredValue();
923    if ( "ENABLE".equals( requiredValue ) )
924    {
925      return true;
926    }
927    else if ( "DISABLE".equals( requiredValue ) )
928    {
929      return false;
930    }
931    else if ( input.isFromTreeContext() )
932    {
933      return false;
934    }
935    else
936    {
937      return !input.hasDefaultMethod() &&
938             !input.hasDefaultField() &&
939             !AnnotationsUtil.hasNullableAnnotation( input.getElement() );
940    }
941  }
942
943  @Nonnull
944  private InputDescriptor createMethodInputDescriptor( @Nonnull final ViewDescriptor descriptor,
945                                                       @Nonnull final List<ExecutableElement> methods,
946                                                       @Nonnull final ExecutableElement method )
947  {
948    final String name = deriveInputName( method );
949    final ExecutableType methodType = resolveMethodType( descriptor, method );
950
951    verifyNoDuplicateAnnotations( method );
952    MemberChecks.mustBeAbstract( Constants.INPUT_CLASSNAME, method );
953    MemberChecks.mustNotHaveAnyParameters( Constants.INPUT_CLASSNAME, method );
954    MemberChecks.mustReturnAValue( Constants.INPUT_CLASSNAME, method );
955    MemberChecks.mustNotThrowAnyExceptions( Constants.INPUT_CLASSNAME, method );
956    MemberChecks.mustNotBePackageAccessInDifferentPackage( descriptor.getElement(),
957                                                           Constants.VIEW_CLASSNAME,
958                                                           Constants.INPUT_CLASSNAME,
959                                                           method );
960    final TypeMirror returnType = method.getReturnType();
961    if ( !returnType.getKind().isPrimitive() &&
962         !AnnotationsUtil.hasNonnullAnnotation( method ) &&
963         !AnnotationsUtil.hasNullableAnnotation( method ) &&
964         ElementsUtil.isWarningNotSuppressed( method,
965                                              Constants.WARNING_MISSING_INPUT_NULLABILITY,
966                                              Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) )
967    {
968      final String message =
969        MemberChecks.shouldNot( Constants.INPUT_CLASSNAME,
970                                "return a non-primitive type without a @Nonnull or @Nullable annotation. " +
971                                MemberChecks.suppressedBy( Constants.WARNING_MISSING_INPUT_NULLABILITY,
972                                                           Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) );
973      warning( message, method );
974    }
975    validateInputNameAndType( name, returnType, method );
976
977    if ( returnType instanceof final TypeVariable typeVariable )
978    {
979      final String typeVariableName = typeVariable.asElement().getSimpleName().toString();
980      List<? extends TypeParameterElement> typeParameters = method.getTypeParameters();
981      if ( typeParameters.stream().anyMatch( p -> p.getSimpleName().toString().equals( typeVariableName ) ) )
982      {
983        throw new ProcessorException( "@Input named '" + name + "' is has a type variable as a return type " +
984                                      "that is declared on the method.", method );
985      }
986    }
987    final String qualifier = (String) AnnotationsUtil
988      .getAnnotationValue( method, Constants.INPUT_CLASSNAME, "qualifier" ).getValue();
989    final boolean fromTreeContextInput = isFromTreeContextInput( method );
990    final Element inputType = processingEnv.getTypeUtils().asElement( returnType );
991    final boolean observable = isInputObservable( methods, method );
992    final boolean disposable = null != inputType && isDisposableDerivableAtCompileTime( inputType );
993    final TypeName typeName = TypeName.get( returnType );
994    if ( typeName.isBoxedPrimitive() && AnnotationsUtil.hasNonnullAnnotation( method ) )
995    {
996      throw new ProcessorException( "@Input named '" + name + "' is a boxed primitive annotated with a " +
997                                    "@Nonnull annotation. The return type should be the primitive type.",
998                                    method );
999    }
1000    if ( !"".equals( qualifier ) && !fromTreeContextInput )
1001    {
1002      throw new ProcessorException( MemberChecks.mustNot( Constants.INPUT_CLASSNAME,
1003                                                          "specify qualifier unless fromTreeContext=true" ),
1004                                    method );
1005    }
1006    final String requiredValue =
1007      ( (VariableElement) AnnotationsUtil.getAnnotationValue( method, Constants.INPUT_CLASSNAME, "require" )
1008        .getValue() )
1009        .getSimpleName().toString();
1010
1011    final InputDescriptor inputDescriptor =
1012      new InputDescriptor( descriptor,
1013                           name,
1014                           qualifier,
1015                           method,
1016                           returnType,
1017                           method,
1018                           methodType,
1019                           null,
1020                           fromTreeContextInput,
1021                           true,
1022                           observable,
1023                           disposable,
1024                           null,
1025                           requiredValue );
1026    if ( inputDescriptor.mayNeedMutableInputAccessedInPostConstructInvariant() )
1027    {
1028      if ( ElementsUtil.isWarningSuppressed( method,
1029                                             Constants.WARNING_MUTABLE_INPUT_ACCESSED_IN_POST_CONSTRUCT,
1030                                             Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) )
1031      {
1032        inputDescriptor.suppressMutableInputAccessedInPostConstruct();
1033      }
1034    }
1035    return inputDescriptor;
1036  }
1037
1038  @Nonnull
1039  private InputDescriptor createConstructorInputDescriptor( @Nonnull final ViewDescriptor descriptor,
1040                                                            @Nonnull final VariableElement parameter )
1041  {
1042    final String name = deriveInputName( parameter );
1043    final TypeMirror type = parameter.asType();
1044    if ( !type.getKind().isPrimitive() &&
1045         !AnnotationsUtil.hasNonnullAnnotation( parameter ) &&
1046         !AnnotationsUtil.hasNullableAnnotation( parameter ) &&
1047         ElementsUtil.isWarningNotSuppressed( parameter,
1048                                              Constants.WARNING_MISSING_INPUT_NULLABILITY,
1049                                              Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) )
1050    {
1051      final String message =
1052        MemberChecks.shouldNot( Constants.INPUT_CLASSNAME,
1053                                "return a non-primitive type without a @Nonnull or @Nullable annotation. " +
1054                                MemberChecks.suppressedBy( Constants.WARNING_MISSING_INPUT_NULLABILITY,
1055                                                           Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) );
1056      warning( message, parameter );
1057    }
1058    validateInputNameAndType( name, type, parameter );
1059
1060    final String qualifier = (String) AnnotationsUtil
1061      .getAnnotationValue( parameter, Constants.INPUT_CLASSNAME, "qualifier" ).getValue();
1062    final boolean fromTreeContextInput = isFromTreeContextInput( parameter );
1063    final Element inputType = processingEnv.getTypeUtils().asElement( type );
1064    //final boolean observable = isInputObservable( methods, method );
1065    final var observable =
1066      ( (VariableElement) AnnotationsUtil
1067        .getAnnotationValue( parameter, Constants.INPUT_CLASSNAME, "observable" )
1068        .getValue() )
1069        .getSimpleName()
1070        .toString();
1071    if ( "ENABLE".equals( observable ) )
1072    {
1073      throw new ProcessorException( "@Input target must not specify observable=ENABLE " +
1074                                    "for an immutable input.", parameter );
1075    }
1076    final boolean disposable = null != inputType && isDisposableDerivableAtCompileTime( inputType );
1077    final TypeName typeName = TypeName.get( type );
1078    if ( typeName.isBoxedPrimitive() && AnnotationsUtil.hasNonnullAnnotation( parameter ) )
1079    {
1080      throw new ProcessorException( "@Input named '" + name + "' is a boxed primitive annotated with a " +
1081                                    "@Nonnull annotation. The return type should be the primitive type.",
1082                                    parameter );
1083    }
1084    final ImmutableInputKeyStrategy strategy = getImmutableInputKeyStrategy( typeName, inputType );
1085    if ( !"".equals( qualifier ) && !fromTreeContextInput )
1086    {
1087      throw new ProcessorException( MemberChecks.mustNot( Constants.INPUT_CLASSNAME,
1088                                                          "specify qualifier unless fromTreeContext=true" ),
1089                                    parameter );
1090    }
1091    final String requiredValue =
1092      ( (VariableElement) AnnotationsUtil.getAnnotationValue( parameter, Constants.INPUT_CLASSNAME, "require" )
1093        .getValue() )
1094        .getSimpleName().toString();
1095
1096    return new InputDescriptor( descriptor,
1097                                name,
1098                                qualifier,
1099                                parameter,
1100                                type,
1101                                null,
1102                                null,
1103                                parameter,
1104                                fromTreeContextInput,
1105                                false,
1106                                false,
1107                                disposable,
1108                                strategy,
1109                                requiredValue );
1110  }
1111
1112  @Nonnull
1113  private ImmutableInputKeyStrategy getImmutableInputKeyStrategy( @Nonnull final TypeName typeName,
1114                                                                  @Nullable final Element element )
1115  {
1116    if ( typeName.toString().equals( "java.lang.String" ) )
1117    {
1118      return ImmutableInputKeyStrategy.IS_STRING;
1119    }
1120    else if ( typeName.isBoxedPrimitive() || typeName.isPrimitive() )
1121    {
1122      return ImmutableInputKeyStrategy.TO_STRING;
1123    }
1124    else if ( null != element )
1125    {
1126      if ( ( ElementKind.CLASS == element.getKind() || ElementKind.INTERFACE == element.getKind() ) &&
1127           isAssignableToKeyed( element ) )
1128      {
1129        return ImmutableInputKeyStrategy.KEYED;
1130      }
1131      else if ( ( ElementKind.CLASS == element.getKind() || ElementKind.INTERFACE == element.getKind() ) &&
1132                (
1133                  isAssignableToIdentifiable( element ) ||
1134                  AnnotationsUtil.hasAnnotationOfType( element, Constants.AREZ_COMPONENT_LIKE_CLASSNAME ) ||
1135                  ( AnnotationsUtil.hasAnnotationOfType( element, Constants.AREZ_COMPONENT_CLASSNAME ) &&
1136                    isIdRequired( (TypeElement) element ) )
1137                ) )
1138      {
1139        return ImmutableInputKeyStrategy.AREZ_IDENTIFIABLE;
1140      }
1141      else if ( ElementKind.ENUM == element.getKind() )
1142      {
1143        return ImmutableInputKeyStrategy.ENUM;
1144      }
1145    }
1146    return ImmutableInputKeyStrategy.DYNAMIC;
1147  }
1148
1149  private boolean isAssignableToKeyed( @Nonnull final Element element )
1150  {
1151    final TypeElement typeElement = processingEnv.getElementUtils().getTypeElement( Constants.KEYED_CLASSNAME );
1152    return processingEnv.getTypeUtils().isAssignable( element.asType(), typeElement.asType() );
1153  }
1154
1155  private boolean isAssignableToIdentifiable( @Nonnull final Element element )
1156  {
1157    final TypeElement typeElement = processingEnv.getElementUtils().getTypeElement( Constants.IDENTIFIABLE_CLASSNAME );
1158    final TypeMirror identifiableErasure = processingEnv.getTypeUtils().erasure( typeElement.asType() );
1159    return processingEnv.getTypeUtils().isAssignable( element.asType(), identifiableErasure );
1160  }
1161
1162  /**
1163   * The logic from this method has been cloned from Arez.
1164   * One day we should consider improving Arez so that this is not required somehow?
1165   */
1166  private boolean isIdRequired( @Nonnull final TypeElement element )
1167  {
1168    final VariableElement requireIdParameter = (VariableElement)
1169      AnnotationsUtil.getAnnotationValue( element, Constants.AREZ_COMPONENT_CLASSNAME, "requireId" )
1170        .getValue();
1171    return !"DISABLE".equals( requireIdParameter.getSimpleName().toString() );
1172  }
1173
1174  @Nonnull
1175  private String deriveInputName( @Nonnull final Element element )
1176    throws ProcessorException
1177  {
1178    final String specifiedName =
1179      (String) AnnotationsUtil.getAnnotationValue( element, Constants.INPUT_CLASSNAME, "name" ).getValue();
1180
1181    final String name;
1182    if ( element instanceof ExecutableElement method )
1183    {
1184      name = getPropertyAccessorName( method, specifiedName );
1185    }
1186    else
1187    {
1188      name = isSentinelName( specifiedName ) ? element.getSimpleName().toString() : specifiedName;
1189    }
1190    if ( !SourceVersion.isIdentifier( name ) )
1191    {
1192      throw new ProcessorException( "@Input target specified an invalid name '" + specifiedName + "'. The " +
1193                                    "name must be a valid java identifier.", element );
1194    }
1195    else if ( SourceVersion.isKeyword( name ) )
1196    {
1197      throw new ProcessorException( "@Input target specified an invalid name '" + specifiedName + "'. The " +
1198                                    "name must not be a java keyword.", element );
1199    }
1200    else
1201    {
1202      return name;
1203    }
1204  }
1205
1206  private void validateInputNameAndType( @Nonnull final String name,
1207                                         @Nonnull final TypeMirror type,
1208                                         @Nonnull final Element element )
1209  {
1210    if ( "build".equals( name ) )
1211    {
1212      throw new ProcessorException( "@Input named 'build' is invalid as it conflicts with the method named " +
1213                                    "build() that is used in the generated Builder classes",
1214                                    element );
1215    }
1216    else if ( "child".equals( name ) &&
1217              ( type.getKind() != TypeKind.DECLARED && !"react4j.ReactNode".equals( type.toString() ) ) )
1218    {
1219      throw new ProcessorException( "@Input named 'child' should be of type react4j.ReactNode", element );
1220    }
1221    else if ( "children".equals( name ) &&
1222              ( type.getKind() != TypeKind.DECLARED && !"react4j.ReactNode[]".equals( type.toString() ) ) )
1223    {
1224      throw new ProcessorException( "@Input named 'children' should be of type react4j.ReactNode[]", element );
1225    }
1226  }
1227
1228  private void determineOnErrorMethod( @Nonnull final TypeElement typeElement,
1229                                       @Nonnull final ViewDescriptor descriptor,
1230                                       @Nonnull final List<ExecutableElement> methods )
1231  {
1232    for ( final ExecutableElement method : methods )
1233    {
1234      if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.ON_ERROR_CLASSNAME ) )
1235      {
1236        MemberChecks.mustNotBeAbstract( Constants.ON_ERROR_CLASSNAME, method );
1237        MemberChecks.mustBeSubclassCallable( typeElement,
1238                                             Constants.VIEW_CLASSNAME,
1239                                             Constants.ON_ERROR_CLASSNAME,
1240                                             method );
1241        MemberChecks.mustNotReturnAnyValue( Constants.ON_ERROR_CLASSNAME, method );
1242        MemberChecks.mustNotThrowAnyExceptions( Constants.ON_ERROR_CLASSNAME, method );
1243
1244        boolean infoFound = false;
1245        boolean errorFound = false;
1246        for ( final VariableElement parameter : method.getParameters() )
1247        {
1248          final TypeName typeName = TypeName.get( parameter.asType() );
1249          if ( typeName.toString().equals( Constants.ERROR_INFO_CLASSNAME ) )
1250          {
1251            if ( infoFound )
1252            {
1253              throw new ProcessorException( "@OnError target has multiple parameters of type " +
1254                                            Constants.ERROR_INFO_CLASSNAME,
1255                                            method );
1256            }
1257            infoFound = true;
1258          }
1259          else if ( typeName.toString().equals( Constants.JS_ERROR_CLASSNAME ) )
1260          {
1261            if ( errorFound )
1262            {
1263              throw new ProcessorException( "@OnError target has multiple parameters of type " +
1264                                            Constants.JS_ERROR_CLASSNAME,
1265                                            method );
1266            }
1267            errorFound = true;
1268          }
1269          else
1270          {
1271            throw new ProcessorException( "@OnError target has parameter of invalid type named " +
1272                                          parameter.getSimpleName(),
1273                                          parameter );
1274          }
1275        }
1276        descriptor.setOnError( method );
1277      }
1278    }
1279  }
1280
1281  private void determineScheduleRenderMethods( @Nonnull final TypeElement typeElement,
1282                                               @Nonnull final ViewDescriptor descriptor,
1283                                               @Nonnull final List<ExecutableElement> methods )
1284  {
1285    final List<ScheduleRenderDescriptor> scheduleRenderDescriptors = new ArrayList<>();
1286    for ( final ExecutableElement method : methods )
1287    {
1288      final AnnotationMirror annotation =
1289        AnnotationsUtil.findAnnotationByType( method, Constants.SCHEDULE_RENDER_CLASSNAME );
1290      if ( null != annotation )
1291      {
1292        MemberChecks.mustBeAbstract( Constants.SCHEDULE_RENDER_CLASSNAME, method );
1293        MemberChecks.mustBeSubclassCallable( typeElement,
1294                                             Constants.VIEW_CLASSNAME,
1295                                             Constants.SCHEDULE_RENDER_CLASSNAME,
1296                                             method );
1297        MemberChecks.mustNotReturnAnyValue( Constants.SCHEDULE_RENDER_CLASSNAME, method );
1298        MemberChecks.mustNotThrowAnyExceptions( Constants.SCHEDULE_RENDER_CLASSNAME, method );
1299
1300        final ViewType viewType = descriptor.getType();
1301        if ( ViewType.STATEFUL != viewType )
1302        {
1303          final String message =
1304            MemberChecks.mustNot( Constants.SCHEDULE_RENDER_CLASSNAME,
1305                                  "be enclosed in a type if it is annotated by @View(type=" + viewType +
1306                                  "). The type must be STATEFUL" );
1307          throw new ProcessorException( message, method );
1308        }
1309
1310        final boolean skipShouldViewUpdate =
1311          AnnotationsUtil.getAnnotationValueValue( annotation, "skipShouldViewUpdate" );
1312
1313        scheduleRenderDescriptors.add( new ScheduleRenderDescriptor( method, skipShouldViewUpdate ) );
1314      }
1315    }
1316    descriptor.setScheduleRenderDescriptors( scheduleRenderDescriptors );
1317  }
1318
1319  private void determinePublishMethods( @Nonnull final TypeElement typeElement,
1320                                        @Nonnull final ViewDescriptor descriptor,
1321                                        @Nonnull final List<ExecutableElement> methods )
1322  {
1323    final List<PublishDescriptor> descriptors = new ArrayList<>();
1324    for ( final ExecutableElement method : methods )
1325    {
1326      final AnnotationMirror annotation = AnnotationsUtil.findAnnotationByType( method, Constants.PUBLISH_CLASSNAME );
1327      if ( null != annotation )
1328      {
1329        MemberChecks.mustBeSubclassCallable( typeElement,
1330                                             Constants.VIEW_CLASSNAME,
1331                                             Constants.PUBLISH_CLASSNAME,
1332                                             method );
1333        MemberChecks.mustNotHaveAnyParameters( Constants.PUBLISH_CLASSNAME, method );
1334        MemberChecks.mustNotHaveAnyTypeParameters( Constants.PUBLISH_CLASSNAME, method );
1335        MemberChecks.mustReturnAValue( Constants.PUBLISH_CLASSNAME, method );
1336        MemberChecks.mustNotThrowAnyExceptions( Constants.PUBLISH_CLASSNAME, method );
1337
1338        final String qualifier = AnnotationsUtil.getAnnotationValueValue( annotation, "qualifier" );
1339        final ExecutableType methodType = resolveMethodType( descriptor, method );
1340
1341        if ( TypeKind.TYPEVAR == methodType.getReturnType().getKind() )
1342        {
1343          throw new ProcessorException( MemberChecks.mustNot( Constants.PUBLISH_CLASSNAME, "return a type variable" ),
1344                                        method );
1345        }
1346
1347        descriptors.add( new PublishDescriptor( qualifier, method, methodType ) );
1348      }
1349    }
1350    descriptor.setPublishDescriptors( descriptors );
1351  }
1352
1353  private void determinePreRenderMethods( @Nonnull final TypeElement typeElement,
1354                                          @Nonnull final ViewDescriptor descriptor,
1355                                          @Nonnull final List<ExecutableElement> methods )
1356  {
1357    final List<RenderHookDescriptor> descriptors = new ArrayList<>();
1358    for ( final ExecutableElement method : methods )
1359    {
1360      final AnnotationMirror annotation =
1361        AnnotationsUtil.findAnnotationByType( method, Constants.PRE_RENDER_CLASSNAME );
1362      if ( null != annotation )
1363      {
1364        MemberChecks.mustBeSubclassCallable( typeElement,
1365                                             Constants.VIEW_CLASSNAME,
1366                                             Constants.PRE_RENDER_CLASSNAME,
1367                                             method );
1368        MemberChecks.mustNotBeAbstract( Constants.PRE_RENDER_CLASSNAME, method );
1369        MemberChecks.mustNotHaveAnyParameters( Constants.PRE_RENDER_CLASSNAME, method );
1370        MemberChecks.mustNotHaveAnyTypeParameters( Constants.PRE_RENDER_CLASSNAME, method );
1371        MemberChecks.mustNotReturnAnyValue( Constants.PRE_RENDER_CLASSNAME, method );
1372        MemberChecks.mustNotThrowAnyExceptions( Constants.PRE_RENDER_CLASSNAME, method );
1373
1374        final int sortOrder = AnnotationsUtil.getAnnotationValueValue( annotation, "sortOrder" );
1375        final ExecutableType methodType = resolveMethodType( descriptor, method );
1376
1377        descriptors.add( new RenderHookDescriptor( sortOrder, method, methodType ) );
1378      }
1379    }
1380    descriptors.sort( Comparator.comparingInt( RenderHookDescriptor::getSortOrder ) );
1381    descriptor.setPreRenderDescriptors( descriptors );
1382  }
1383
1384  private void determinePostRenderMethods( @Nonnull final TypeElement typeElement,
1385                                           @Nonnull final ViewDescriptor descriptor,
1386                                           @Nonnull final List<ExecutableElement> methods )
1387  {
1388    final List<RenderHookDescriptor> descriptors = new ArrayList<>();
1389    for ( final ExecutableElement method : methods )
1390    {
1391      final AnnotationMirror annotation =
1392        AnnotationsUtil.findAnnotationByType( method, Constants.POST_RENDER_CLASSNAME );
1393      if ( null != annotation )
1394      {
1395        MemberChecks.mustBeSubclassCallable( typeElement,
1396                                             Constants.VIEW_CLASSNAME,
1397                                             Constants.POST_RENDER_CLASSNAME,
1398                                             method );
1399        MemberChecks.mustNotBeAbstract( Constants.POST_RENDER_CLASSNAME, method );
1400        MemberChecks.mustNotHaveAnyParameters( Constants.POST_RENDER_CLASSNAME, method );
1401        MemberChecks.mustNotHaveAnyTypeParameters( Constants.POST_RENDER_CLASSNAME, method );
1402        MemberChecks.mustNotReturnAnyValue( Constants.POST_RENDER_CLASSNAME, method );
1403        MemberChecks.mustNotThrowAnyExceptions( Constants.POST_RENDER_CLASSNAME, method );
1404
1405        final int sortOrder = AnnotationsUtil.getAnnotationValueValue( annotation, "sortOrder" );
1406        final ExecutableType methodType = resolveMethodType( descriptor, method );
1407
1408        descriptors.add( new RenderHookDescriptor( sortOrder, method, methodType ) );
1409      }
1410    }
1411    descriptors.sort( Comparator.comparingInt( RenderHookDescriptor::getSortOrder ) );
1412    descriptor.setPostRenderDescriptors( descriptors );
1413  }
1414
1415  private void determineRenderMethod( @Nonnull final TypeElement typeElement,
1416                                      @Nonnull final ViewDescriptor descriptor,
1417                                      @Nonnull final List<ExecutableElement> methods )
1418  {
1419    boolean foundRender = false;
1420    for ( final ExecutableElement method : methods )
1421    {
1422      final AnnotationMirror annotation =
1423        AnnotationsUtil.findAnnotationByType( method, Constants.RENDER_CLASSNAME );
1424      if ( null != annotation )
1425      {
1426        MemberChecks.mustNotBeAbstract( Constants.RENDER_CLASSNAME, method );
1427        MemberChecks.mustBeSubclassCallable( typeElement,
1428                                             Constants.VIEW_CLASSNAME,
1429                                             Constants.RENDER_CLASSNAME,
1430                                             method );
1431        MemberChecks.mustNotHaveAnyParameters( Constants.RENDER_CLASSNAME, method );
1432        MemberChecks.mustReturnAnInstanceOf( processingEnv,
1433                                             method,
1434                                             Constants.RENDER_CLASSNAME,
1435                                             Constants.VNODE_CLASSNAME );
1436        MemberChecks.mustNotThrowAnyExceptions( Constants.RENDER_CLASSNAME, method );
1437        MemberChecks.mustNotHaveAnyTypeParameters( Constants.RENDER_CLASSNAME, method );
1438        if ( !method.getReturnType().getKind().isPrimitive() &&
1439             !AnnotationsUtil.hasNonnullAnnotation( method ) &&
1440             !AnnotationsUtil.hasNullableAnnotation( method ) &&
1441             ElementsUtil.isWarningNotSuppressed( method,
1442                                                  Constants.WARNING_MISSING_RENDER_NULLABILITY,
1443                                                  Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) )
1444        {
1445          final String message =
1446            MemberChecks.should( Constants.RENDER_CLASSNAME,
1447                                 "be annotated by a @Nonnull or a @Nullable annotation. " +
1448                                 MemberChecks.suppressedBy( Constants.WARNING_MISSING_RENDER_NULLABILITY,
1449                                                            Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) );
1450          warning( message, method );
1451        }
1452
1453        descriptor.setRender( method );
1454        foundRender = true;
1455      }
1456    }
1457    final boolean requireRender = descriptor.requireRender();
1458    if ( requireRender && !foundRender )
1459    {
1460      throw new ProcessorException( MemberChecks.must( Constants.VIEW_CLASSNAME,
1461                                                       "contain a method annotated with the " +
1462                                                       MemberChecks.toSimpleName( Constants.RENDER_CLASSNAME ) +
1463                                                       " annotation or must specify type=NO_RENDER" ),
1464                                    typeElement );
1465    }
1466    else if ( !requireRender )
1467    {
1468      if ( foundRender )
1469      {
1470        throw new ProcessorException( MemberChecks.mustNot( Constants.VIEW_CLASSNAME,
1471                                                            "contain a method annotated with the " +
1472                                                            MemberChecks.toSimpleName( Constants.RENDER_CLASSNAME ) +
1473                                                            " annotation or must not specify type=NO_RENDER" ),
1474                                      typeElement );
1475      }
1476      else if ( !descriptor.hasConstructor() &&
1477                !descriptor.hasPostConstruct() &&
1478                null == descriptor.getPostMount() &&
1479                null == descriptor.getPostRender() &&
1480                null == descriptor.getPreUpdate() &&
1481                null == descriptor.getPostUpdate() &&
1482                descriptor.getPreRenderDescriptors().isEmpty() &&
1483                descriptor.getPostRenderDescriptors().isEmpty() &&
1484                !descriptor.hasPreUpdateOnInputChange() &&
1485                !descriptor.hasPostUpdateOnInputChange() )
1486      {
1487        throw new ProcessorException( MemberChecks.must( Constants.VIEW_CLASSNAME,
1488                                                         "contain lifecycle methods if the the @View(type=NO_RENDER) parameter is specified" ),
1489                                      typeElement );
1490      }
1491    }
1492  }
1493
1494  private void determinePostMountMethod( @Nonnull final TypeElement typeElement,
1495                                         @Nonnull final ViewDescriptor descriptor,
1496                                         @Nonnull final List<ExecutableElement> methods )
1497  {
1498    for ( final ExecutableElement method : methods )
1499    {
1500      if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.POST_MOUNT_CLASSNAME ) )
1501      {
1502        MemberChecks.mustBeLifecycleHook( typeElement,
1503                                          Constants.VIEW_CLASSNAME,
1504                                          Constants.POST_MOUNT_CLASSNAME,
1505                                          method );
1506        descriptor.setPostMount( method );
1507      }
1508    }
1509  }
1510
1511  private void determinePostMountOrUpdateMethod( @Nonnull final TypeElement typeElement,
1512                                                 @Nonnull final ViewDescriptor descriptor,
1513                                                 @Nonnull final List<ExecutableElement> methods )
1514  {
1515    for ( final ExecutableElement method : methods )
1516    {
1517      if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.POST_MOUNT_OR_UPDATE_CLASSNAME ) )
1518      {
1519        MemberChecks.mustBeLifecycleHook( typeElement,
1520                                          Constants.VIEW_CLASSNAME,
1521                                          Constants.POST_MOUNT_OR_UPDATE_CLASSNAME,
1522                                          method );
1523        descriptor.setPostRender( method );
1524      }
1525    }
1526  }
1527
1528  private void determinePostUpdateMethod( @Nonnull final TypeElement typeElement,
1529                                          @Nonnull final ViewDescriptor descriptor,
1530                                          @Nonnull final List<ExecutableElement> methods )
1531  {
1532    for ( final ExecutableElement method : methods )
1533    {
1534      if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.POST_UPDATE_CLASSNAME ) )
1535      {
1536        MemberChecks.mustBeLifecycleHook( typeElement,
1537                                          Constants.VIEW_CLASSNAME,
1538                                          Constants.POST_UPDATE_CLASSNAME,
1539                                          method );
1540        descriptor.setPostUpdate( method );
1541      }
1542    }
1543  }
1544
1545  private void determinePreUpdateMethod( @Nonnull final TypeElement typeElement,
1546                                         @Nonnull final ViewDescriptor descriptor,
1547                                         @Nonnull final List<ExecutableElement> methods )
1548  {
1549    for ( final ExecutableElement method : methods )
1550    {
1551      if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.PRE_UPDATE_CLASSNAME ) )
1552      {
1553        MemberChecks.mustBeLifecycleHook( typeElement,
1554                                          Constants.VIEW_CLASSNAME,
1555                                          Constants.PRE_UPDATE_CLASSNAME,
1556                                          method );
1557        descriptor.setPreUpdate( method );
1558      }
1559    }
1560  }
1561
1562  private ExecutableType resolveMethodType( @Nonnull final ViewDescriptor descriptor,
1563                                            @Nonnull final ExecutableElement method )
1564  {
1565    return (ExecutableType) processingEnv.getTypeUtils().asMemberOf( descriptor.getDeclaredType(), method );
1566  }
1567
1568  @Nonnull
1569  private String deriveViewName( @Nonnull final TypeElement typeElement )
1570  {
1571    final String name =
1572      (String) AnnotationsUtil.getAnnotationValue( typeElement, Constants.VIEW_CLASSNAME, "name" )
1573        .getValue();
1574
1575    if ( isSentinelName( name ) )
1576    {
1577      return typeElement.getSimpleName().toString();
1578    }
1579    else
1580    {
1581      if ( !SourceVersion.isIdentifier( name ) )
1582      {
1583        throw new ProcessorException( MemberChecks.toSimpleName( Constants.VIEW_CLASSNAME ) +
1584                                      " target specified an invalid name '" + name + "'. The " +
1585                                      "name must be a valid java identifier.", typeElement );
1586      }
1587      else if ( SourceVersion.isKeyword( name ) )
1588      {
1589        throw new ProcessorException( MemberChecks.toSimpleName( Constants.VIEW_CLASSNAME ) +
1590                                      " target specified an invalid name '" + name + "'. The " +
1591                                      "name must not be a java keyword.", typeElement );
1592      }
1593      return name;
1594    }
1595  }
1596
1597  private void determineViewCapabilities( @Nonnull final ViewDescriptor descriptor,
1598                                          @Nonnull final TypeElement typeElement )
1599  {
1600    if ( AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.AREZ_COMPONENT_CLASSNAME ) )
1601    {
1602      throw new ProcessorException( MemberChecks.mustNot( Constants.VIEW_CLASSNAME,
1603                                                          "be annotated with the " +
1604                                                          MemberChecks.toSimpleName( Constants.AREZ_COMPONENT_CLASSNAME ) +
1605                                                          " as React4j will add the annotation." ),
1606                                    typeElement );
1607    }
1608
1609    if ( descriptor.needsInjection() && !descriptor.getDeclaredType().getTypeArguments().isEmpty() )
1610    {
1611      throw new ProcessorException( MemberChecks.toSimpleName( Constants.VIEW_CLASSNAME ) +
1612                                    " target has enabled injection integration but the class " +
1613                                    "has type arguments which is incompatible with injection integration.",
1614                                    typeElement );
1615    }
1616  }
1617
1618  @Nonnull
1619  private ViewType extractViewType( @Nonnull final TypeElement typeElement )
1620  {
1621    final VariableElement declaredTypeEnum = (VariableElement)
1622      AnnotationsUtil
1623        .getAnnotationValue( typeElement, Constants.VIEW_CLASSNAME, "type" )
1624        .getValue();
1625    return ViewType.valueOf( declaredTypeEnum.getSimpleName().toString() );
1626  }
1627
1628  private boolean extractExportBuilder( @Nonnull final TypeElement typeElement )
1629  {
1630    return (Boolean) AnnotationsUtil.getAnnotationValue( typeElement, Constants.VIEW_CLASSNAME, "exportBuilder" )
1631      .getValue();
1632  }
1633
1634  private boolean isInputObservable( @Nonnull final List<ExecutableElement> methods,
1635                                     @Nonnull final Element element )
1636  {
1637    final var parameter = (VariableElement)
1638      AnnotationsUtil.getAnnotationValue( element, Constants.INPUT_CLASSNAME, "observable" ).getValue();
1639    return switch ( parameter.getSimpleName().toString() )
1640    {
1641      case "ENABLE" -> true;
1642      case "DISABLE" -> false;
1643      default -> hasAnyArezObserverMethods( methods );
1644    };
1645  }
1646
1647  private boolean hasAnyArezObserverMethods( @Nonnull final List<ExecutableElement> methods )
1648  {
1649    return
1650      methods
1651        .stream()
1652        .anyMatch( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.MEMOIZE_CLASSNAME ) ||
1653                        ( AnnotationsUtil.hasAnnotationOfType( m, Constants.OBSERVE_CLASSNAME ) &&
1654                          ( !m.getParameters().isEmpty() || !m.getSimpleName().toString().equals( "trackRender" ) ) ) );
1655  }
1656
1657  @Nonnull
1658  private ObserveMode determinePreludeCheckObservationMode( @Nonnull final Element element,
1659                                                            @Nonnull final TypeMirror type )
1660  {
1661    if ( type.getKind().isPrimitive() )
1662    {
1663      return ObserveMode.NO_OBSERVE;
1664    }
1665    else
1666    {
1667      final var typeElement = processingEnv.getTypeUtils().asElement( type );
1668      if ( typeElement instanceof TypeElement )
1669      {
1670        final var resolution = resolveArezComponentObservable( (TypeElement) typeElement );
1671        if ( ArezComponentObservableResolution.DISABLED == resolution )
1672        {
1673          return ObserveMode.NO_OBSERVE;
1674        }
1675        else if ( ArezComponentObservableResolution.ENABLED == resolution || isAssignableToComponentObservable( type ) )
1676        {
1677          return AnnotationsUtil.hasNonnullAnnotation( element ) ?
1678                 ObserveMode.OBSERVE_NONNULL :
1679                 ObserveMode.OBSERVE_NULLABLE;
1680        }
1681        else if ( canTypeUseRuntimeComponentObservableCheck( type, typeElement ) )
1682        {
1683          // Type does not implement `arez.component.ComponentObservable` but it is not final so try at runtime
1684          return ObserveMode.RUNTIME_CHECK;
1685        }
1686        else
1687        {
1688          // Can never implement arez.component.ComponentObservable
1689          return ObserveMode.NO_OBSERVE;
1690        }
1691      }
1692      else
1693      {
1694        return ObserveMode.NO_OBSERVE;
1695      }
1696    }
1697  }
1698
1699  private boolean canTypeUseRuntimeComponentObservableCheck( @Nonnull final TypeMirror type,
1700                                                             @Nullable final Element typeElement )
1701  {
1702    return TypeKind.TYPEVAR == type.getKind() ||
1703           null != typeElement &&
1704           ( ElementKind.INTERFACE == typeElement.getKind() ||
1705             ( ElementKind.CLASS == typeElement.getKind() &&
1706               !typeElement.getModifiers().contains( Modifier.FINAL ) ) );
1707  }
1708
1709  private boolean isAssignableToComponentObservable( @Nonnull final TypeMirror type )
1710  {
1711    final var typeElement = processingEnv.getElementUtils().getTypeElement( Constants.COMPONENT_OBSERVABLE_CLASSNAME );
1712    return null != typeElement && processingEnv.getTypeUtils().isAssignable( type, typeElement.asType() );
1713  }
1714
1715  @Nonnull
1716  private ArezComponentObservableResolution resolveArezComponentObservable( @Nonnull final TypeElement element )
1717  {
1718    if ( !AnnotationsUtil.hasAnnotationOfType( element, Constants.AREZ_COMPONENT_CLASSNAME ) )
1719    {
1720      return ArezComponentObservableResolution.NOT_AREZ_COMPONENT;
1721    }
1722
1723    final VariableElement observableParameter = (VariableElement)
1724      AnnotationsUtil.getAnnotationValue( element, Constants.AREZ_COMPONENT_CLASSNAME, "observable" ).getValue();
1725    return switch ( observableParameter.getSimpleName().toString() )
1726    {
1727      case "ENABLE" -> ArezComponentObservableResolution.ENABLED;
1728      case "DISABLE" -> ArezComponentObservableResolution.DISABLED;
1729      default ->
1730      {
1731        final boolean disposeOnDeactivate = (Boolean)
1732          AnnotationsUtil.getAnnotationValue( element, Constants.AREZ_COMPONENT_CLASSNAME, "disposeOnDeactivate" )
1733            .getValue();
1734        yield disposeOnDeactivate ?
1735              ArezComponentObservableResolution.ENABLED :
1736              ArezComponentObservableResolution.DISABLED;
1737      }
1738    };
1739  }
1740
1741  private enum ArezComponentObservableResolution
1742  {
1743    ENABLED,
1744    DISABLED,
1745    NOT_AREZ_COMPONENT
1746  }
1747
1748  private boolean isFromTreeContextInput( @Nonnull final Element element )
1749  {
1750    return (Boolean) AnnotationsUtil.getAnnotationValue( element, Constants.INPUT_CLASSNAME, "fromTreeContext" )
1751      .getValue();
1752  }
1753
1754  private boolean shouldSetDefaultPriority( @Nonnull final List<ExecutableElement> methods )
1755  {
1756    return
1757      methods
1758        .stream()
1759        .filter( method -> !method.getModifiers().contains( Modifier.PRIVATE ) )
1760        .anyMatch( method -> AnnotationsUtil.hasAnnotationOfType( method, Constants.MEMOIZE_CLASSNAME ) ||
1761                             AnnotationsUtil.hasAnnotationOfType( method, Constants.OBSERVE_CLASSNAME ) );
1762  }
1763
1764  private void verifyNoDuplicateAnnotations( @Nonnull final ExecutableElement method )
1765    throws ProcessorException
1766  {
1767    final List<String> annotations =
1768      Arrays.asList( Constants.INPUT_DEFAULT_CLASSNAME,
1769                     Constants.INPUT_VALIDATE_CLASSNAME,
1770                     Constants.ON_INPUT_CHANGE_CLASSNAME,
1771                     Constants.INPUT_CLASSNAME );
1772    MemberChecks.verifyNoOverlappingAnnotations( method, annotations, Collections.emptyMap() );
1773  }
1774
1775  private boolean isSentinelName( @Nonnull final String name )
1776  {
1777    return SENTINEL_NAME.equals( name );
1778  }
1779
1780  @Nonnull
1781  private String getPropertyAccessorName( @Nonnull final ExecutableElement method,
1782                                          @Nonnull final String specifiedName )
1783    throws ProcessorException
1784  {
1785    String name = deriveName( method, GETTER_PATTERN, specifiedName );
1786    if ( null != name )
1787    {
1788      return name;
1789    }
1790    else if ( method.getReturnType().getKind() == TypeKind.BOOLEAN )
1791    {
1792      name = deriveName( method, ISSER_PATTERN, specifiedName );
1793      if ( null != name )
1794      {
1795        return name;
1796      }
1797    }
1798    return method.getSimpleName().toString();
1799  }
1800
1801  @Nullable
1802  private String deriveName( @Nonnull final Element method, @Nonnull final Pattern pattern, @Nonnull final String name )
1803    throws ProcessorException
1804  {
1805    if ( isSentinelName( name ) )
1806    {
1807      final String methodName = method.getSimpleName().toString();
1808      final Matcher matcher = pattern.matcher( methodName );
1809      if ( matcher.find() )
1810      {
1811        final String candidate = matcher.group( 1 );
1812        return Character.toLowerCase( candidate.charAt( 0 ) ) + candidate.substring( 1 );
1813      }
1814      else
1815      {
1816        return null;
1817      }
1818    }
1819    else
1820    {
1821      return name;
1822    }
1823  }
1824}