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