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