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