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