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 ( "build".equals( name ) )
877    {
878      throw new ProcessorException( "@Input named 'build' is invalid as it conflicts with the method named " +
879                                    "build() that is used in the generated Builder classes",
880                                    method );
881    }
882    else if ( "child".equals( name ) &&
883              ( returnType.getKind() != TypeKind.DECLARED && !"react4j.ReactNode".equals( returnType.toString() ) ) )
884    {
885      throw new ProcessorException( "@Input named 'child' should be of type react4j.ReactNode", method );
886    }
887    else if ( "children".equals( name ) &&
888              ( returnType.getKind() != TypeKind.DECLARED && !"react4j.ReactNode[]".equals( returnType.toString() ) ) )
889    {
890      throw new ProcessorException( "@Input named 'children' should be of type react4j.ReactNode[]", method );
891    }
892
893    if ( returnType instanceof final TypeVariable typeVariable )
894    {
895      final String typeVariableName = typeVariable.asElement().getSimpleName().toString();
896      List<? extends TypeParameterElement> typeParameters = method.getTypeParameters();
897      if ( typeParameters.stream().anyMatch( p -> p.getSimpleName().toString().equals( typeVariableName ) ) )
898      {
899        throw new ProcessorException( "@Input named '" + name + "' is has a type variable as a return type " +
900                                      "that is declared on the method.", method );
901      }
902    }
903    final String qualifier = (String) AnnotationsUtil
904      .getAnnotationValue( method, Constants.INPUT_CLASSNAME, "qualifier" ).getValue();
905    final boolean contextInput = isContextInput( method );
906    final Element inputType = processingEnv.getTypeUtils().asElement( returnType );
907    final boolean immutable = isInputImmutable( method );
908    final boolean observable = isInputObservable( methods, method, immutable );
909    final boolean disposable = null != inputType && isInputDisposable( method, inputType );
910    final TypeName typeName = TypeName.get( returnType );
911    if ( typeName.isBoxedPrimitive() && AnnotationsUtil.hasNonnullAnnotation( method ) )
912    {
913      throw new ProcessorException( "@Input named '" + name + "' is a boxed primitive annotated with a " +
914                                    "@Nonnull annotation. The return type should be the primitive type.",
915                                    method );
916    }
917    final ImmutableInputKeyStrategy strategy = immutable ? getImmutableInputKeyStrategy( typeName, inputType ) : null;
918    if ( !"".equals( qualifier ) && !contextInput )
919    {
920      throw new ProcessorException( MemberChecks.mustNot( Constants.INPUT_CLASSNAME,
921                                                          "specify qualifier parameter unless source=CONTEXT is also specified" ),
922                                    method );
923    }
924    final String requiredValue =
925      ( (VariableElement) AnnotationsUtil.getAnnotationValue( method, Constants.INPUT_CLASSNAME, "require" )
926        .getValue() )
927        .getSimpleName().toString();
928
929    final boolean dependency = isInputDependency( method, immutable, disposable );
930
931    final InputDescriptor inputDescriptor =
932      new InputDescriptor( descriptor,
933                           name,
934                           qualifier,
935                           method,
936                           methodType,
937                           inputType,
938                           contextInput,
939                           !immutable,
940                           observable,
941                           disposable,
942                           dependency,
943                           strategy,
944                           requiredValue );
945    if ( inputDescriptor.mayNeedMutableInputAccessedInPostConstructInvariant() )
946    {
947      if ( ElementsUtil.isWarningSuppressed( method,
948                                             Constants.WARNING_MUTABLE_INPUT_ACCESSED_IN_POST_CONSTRUCT,
949                                             Constants.SUPPRESS_REACT4J_WARNINGS_CLASSNAME ) )
950      {
951        inputDescriptor.suppressMutableInputAccessedInPostConstruct();
952      }
953    }
954    return inputDescriptor;
955  }
956
957  @Nonnull
958  private ImmutableInputKeyStrategy getImmutableInputKeyStrategy( @Nonnull final TypeName typeName,
959                                                                  @Nullable final Element element )
960  {
961    if ( typeName.toString().equals( "java.lang.String" ) )
962    {
963      return ImmutableInputKeyStrategy.IS_STRING;
964    }
965    else if ( typeName.isBoxedPrimitive() || typeName.isPrimitive() )
966    {
967      return ImmutableInputKeyStrategy.TO_STRING;
968    }
969    else if ( null != element )
970    {
971      if ( ( ElementKind.CLASS == element.getKind() || ElementKind.INTERFACE == element.getKind() ) &&
972           isAssignableToKeyed( element ) )
973      {
974        return ImmutableInputKeyStrategy.KEYED;
975      }
976      else if ( ( ElementKind.CLASS == element.getKind() || ElementKind.INTERFACE == element.getKind() ) &&
977                (
978                  isAssignableToIdentifiable( element ) ||
979                  AnnotationsUtil.hasAnnotationOfType( element, Constants.ACT_AS_COMPONENT_CLASSNAME ) ||
980                  ( AnnotationsUtil.hasAnnotationOfType( element, Constants.AREZ_COMPONENT_CLASSNAME ) &&
981                    isIdRequired( (TypeElement) element ) )
982                ) )
983      {
984        return ImmutableInputKeyStrategy.AREZ_IDENTIFIABLE;
985      }
986      else if ( ElementKind.ENUM == element.getKind() )
987      {
988        return ImmutableInputKeyStrategy.ENUM;
989      }
990    }
991    return ImmutableInputKeyStrategy.DYNAMIC;
992  }
993
994  private boolean isAssignableToKeyed( @Nonnull final Element element )
995  {
996    final TypeElement typeElement = processingEnv.getElementUtils().getTypeElement( Constants.KEYED_CLASSNAME );
997    return processingEnv.getTypeUtils().isAssignable( element.asType(), typeElement.asType() );
998  }
999
1000  private boolean isAssignableToIdentifiable( @Nonnull final Element element )
1001  {
1002    final TypeElement typeElement = processingEnv.getElementUtils().getTypeElement( Constants.IDENTIFIABLE_CLASSNAME );
1003    final TypeMirror identifiableErasure = processingEnv.getTypeUtils().erasure( typeElement.asType() );
1004    return processingEnv.getTypeUtils().isAssignable( element.asType(), identifiableErasure );
1005  }
1006
1007  /**
1008   * The logic from this method has been cloned from Arez.
1009   * One day we should consider improving Arez so that this is not required somehow?
1010   */
1011  private boolean isIdRequired( @Nonnull final TypeElement element )
1012  {
1013    final VariableElement requireIdParameter = (VariableElement)
1014      AnnotationsUtil.getAnnotationValue( element, Constants.AREZ_COMPONENT_CLASSNAME, "requireId" )
1015        .getValue();
1016    return !"DISABLE".equals( requireIdParameter.getSimpleName().toString() );
1017  }
1018
1019  @Nonnull
1020  private String deriveInputName( @Nonnull final ExecutableElement method )
1021    throws ProcessorException
1022  {
1023    final String specifiedName =
1024      (String) AnnotationsUtil.getAnnotationValue( method, Constants.INPUT_CLASSNAME, "name" ).getValue();
1025
1026    final String name = getPropertyAccessorName( method, specifiedName );
1027    if ( !SourceVersion.isIdentifier( name ) )
1028    {
1029      throw new ProcessorException( "@Input target specified an invalid name '" + specifiedName + "'. The " +
1030                                    "name must be a valid java identifier.", method );
1031    }
1032    else if ( SourceVersion.isKeyword( name ) )
1033    {
1034      throw new ProcessorException( "@Input target specified an invalid name '" + specifiedName + "'. The " +
1035                                    "name must not be a java keyword.", method );
1036    }
1037    else
1038    {
1039      return name;
1040    }
1041  }
1042
1043  private void determineOnErrorMethod( @Nonnull final TypeElement typeElement,
1044                                       @Nonnull final ViewDescriptor descriptor,
1045                                       @Nonnull final List<ExecutableElement> methods )
1046  {
1047    for ( final ExecutableElement method : methods )
1048    {
1049      if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.ON_ERROR_CLASSNAME ) )
1050      {
1051        MemberChecks.mustNotBeAbstract( Constants.ON_ERROR_CLASSNAME, method );
1052        MemberChecks.mustBeSubclassCallable( typeElement,
1053                                             Constants.VIEW_CLASSNAME,
1054                                             Constants.ON_ERROR_CLASSNAME,
1055                                             method );
1056        MemberChecks.mustNotReturnAnyValue( Constants.ON_ERROR_CLASSNAME, method );
1057        MemberChecks.mustNotThrowAnyExceptions( Constants.ON_ERROR_CLASSNAME, method );
1058
1059        boolean infoFound = false;
1060        boolean errorFound = false;
1061        for ( final VariableElement parameter : method.getParameters() )
1062        {
1063          final TypeName typeName = TypeName.get( parameter.asType() );
1064          if ( typeName.toString().equals( Constants.ERROR_INFO_CLASSNAME ) )
1065          {
1066            if ( infoFound )
1067            {
1068              throw new ProcessorException( "@OnError target has multiple parameters of type " +
1069                                            Constants.ERROR_INFO_CLASSNAME,
1070                                            method );
1071            }
1072            infoFound = true;
1073          }
1074          else if ( typeName.toString().equals( Constants.JS_ERROR_CLASSNAME ) )
1075          {
1076            if ( errorFound )
1077            {
1078              throw new ProcessorException( "@OnError target has multiple parameters of type " +
1079                                            Constants.JS_ERROR_CLASSNAME,
1080                                            method );
1081            }
1082            errorFound = true;
1083          }
1084          else
1085          {
1086            throw new ProcessorException( "@OnError target has parameter of invalid type named " +
1087                                          parameter.getSimpleName().toString(),
1088                                          parameter );
1089          }
1090        }
1091        descriptor.setOnError( method );
1092      }
1093    }
1094  }
1095
1096  private void determineScheduleRenderMethods( @Nonnull final TypeElement typeElement,
1097                                               @Nonnull final ViewDescriptor descriptor,
1098                                               @Nonnull final List<ExecutableElement> methods )
1099  {
1100    final List<ScheduleRenderDescriptor> scheduleRenderDescriptors = new ArrayList<>();
1101    for ( final ExecutableElement method : methods )
1102    {
1103      final AnnotationMirror annotation =
1104        AnnotationsUtil.findAnnotationByType( method, Constants.SCHEDULE_RENDER_CLASSNAME );
1105      if ( null != annotation )
1106      {
1107        MemberChecks.mustBeAbstract( Constants.SCHEDULE_RENDER_CLASSNAME, method );
1108        MemberChecks.mustBeSubclassCallable( typeElement,
1109                                             Constants.VIEW_CLASSNAME,
1110                                             Constants.SCHEDULE_RENDER_CLASSNAME,
1111                                             method );
1112        MemberChecks.mustNotReturnAnyValue( Constants.SCHEDULE_RENDER_CLASSNAME, method );
1113        MemberChecks.mustNotThrowAnyExceptions( Constants.SCHEDULE_RENDER_CLASSNAME, method );
1114
1115        final ViewType viewType = descriptor.getType();
1116        if ( ViewType.STATEFUL != viewType )
1117        {
1118          final String message =
1119            MemberChecks.mustNot( Constants.SCHEDULE_RENDER_CLASSNAME,
1120                                  "be enclosed in a type if it is annotated by @View(type=" + viewType +
1121                                  "). The type must be STATEFUL" );
1122          throw new ProcessorException( message, method );
1123        }
1124
1125        final boolean skipShouldViewUpdate =
1126          AnnotationsUtil.getAnnotationValueValue( annotation, "skipShouldViewUpdate" );
1127
1128        scheduleRenderDescriptors.add( new ScheduleRenderDescriptor( method, skipShouldViewUpdate ) );
1129      }
1130    }
1131    descriptor.setScheduleRenderDescriptors( scheduleRenderDescriptors );
1132  }
1133
1134  private void determinePublishMethods( @Nonnull final TypeElement typeElement,
1135                                        @Nonnull final ViewDescriptor descriptor,
1136                                        @Nonnull final List<ExecutableElement> methods )
1137  {
1138    final List<PublishDescriptor> descriptors = new ArrayList<>();
1139    for ( final ExecutableElement method : methods )
1140    {
1141      final AnnotationMirror annotation = AnnotationsUtil.findAnnotationByType( method, Constants.PUBLISH_CLASSNAME );
1142      if ( null != annotation )
1143      {
1144        MemberChecks.mustBeSubclassCallable( typeElement,
1145                                             Constants.VIEW_CLASSNAME,
1146                                             Constants.PUBLISH_CLASSNAME,
1147                                             method );
1148        MemberChecks.mustNotHaveAnyParameters( Constants.PUBLISH_CLASSNAME, method );
1149        MemberChecks.mustNotHaveAnyTypeParameters( Constants.PUBLISH_CLASSNAME, method );
1150        MemberChecks.mustReturnAValue( Constants.PUBLISH_CLASSNAME, method );
1151        MemberChecks.mustNotThrowAnyExceptions( Constants.PUBLISH_CLASSNAME, method );
1152
1153        final String qualifier = AnnotationsUtil.getAnnotationValueValue( annotation, "qualifier" );
1154        final ExecutableType methodType = resolveMethodType( descriptor, method );
1155
1156        if ( TypeKind.TYPEVAR == methodType.getReturnType().getKind() )
1157        {
1158          throw new ProcessorException( MemberChecks.mustNot( Constants.PUBLISH_CLASSNAME, "return a type variable" ),
1159                                        method );
1160        }
1161
1162        descriptors.add( new PublishDescriptor( qualifier, method, methodType ) );
1163      }
1164    }
1165    descriptor.setPublishDescriptors( descriptors );
1166  }
1167
1168  private void determinePreRenderMethods( @Nonnull final TypeElement typeElement,
1169                                          @Nonnull final ViewDescriptor descriptor,
1170                                          @Nonnull final List<ExecutableElement> methods )
1171  {
1172    final List<RenderHookDescriptor> descriptors = new ArrayList<>();
1173    for ( final ExecutableElement method : methods )
1174    {
1175      final AnnotationMirror annotation =
1176        AnnotationsUtil.findAnnotationByType( method, Constants.PRE_RENDER_CLASSNAME );
1177      if ( null != annotation )
1178      {
1179        MemberChecks.mustBeSubclassCallable( typeElement,
1180                                             Constants.VIEW_CLASSNAME,
1181                                             Constants.PRE_RENDER_CLASSNAME,
1182                                             method );
1183        MemberChecks.mustNotBeAbstract( Constants.PRE_RENDER_CLASSNAME, method );
1184        MemberChecks.mustNotHaveAnyParameters( Constants.PRE_RENDER_CLASSNAME, method );
1185        MemberChecks.mustNotHaveAnyTypeParameters( Constants.PRE_RENDER_CLASSNAME, method );
1186        MemberChecks.mustNotReturnAnyValue( Constants.PRE_RENDER_CLASSNAME, method );
1187        MemberChecks.mustNotThrowAnyExceptions( Constants.PRE_RENDER_CLASSNAME, method );
1188
1189        final int sortOrder = AnnotationsUtil.getAnnotationValueValue( annotation, "sortOrder" );
1190        final ExecutableType methodType = resolveMethodType( descriptor, method );
1191
1192        descriptors.add( new RenderHookDescriptor( sortOrder, method, methodType ) );
1193      }
1194    }
1195    descriptors.sort( Comparator.comparingInt( RenderHookDescriptor::getSortOrder ) );
1196    descriptor.setPreRenderDescriptors( descriptors );
1197  }
1198
1199  private void determinePostRenderMethods( @Nonnull final TypeElement typeElement,
1200                                           @Nonnull final ViewDescriptor descriptor,
1201                                           @Nonnull final List<ExecutableElement> methods )
1202  {
1203    final List<RenderHookDescriptor> descriptors = new ArrayList<>();
1204    for ( final ExecutableElement method : methods )
1205    {
1206      final AnnotationMirror annotation =
1207        AnnotationsUtil.findAnnotationByType( method, Constants.POST_RENDER_CLASSNAME );
1208      if ( null != annotation )
1209      {
1210        MemberChecks.mustBeSubclassCallable( typeElement,
1211                                             Constants.VIEW_CLASSNAME,
1212                                             Constants.POST_RENDER_CLASSNAME,
1213                                             method );
1214        MemberChecks.mustNotBeAbstract( Constants.POST_RENDER_CLASSNAME, method );
1215        MemberChecks.mustNotHaveAnyParameters( Constants.POST_RENDER_CLASSNAME, method );
1216        MemberChecks.mustNotHaveAnyTypeParameters( Constants.POST_RENDER_CLASSNAME, method );
1217        MemberChecks.mustNotReturnAnyValue( Constants.POST_RENDER_CLASSNAME, method );
1218        MemberChecks.mustNotThrowAnyExceptions( Constants.POST_RENDER_CLASSNAME, method );
1219
1220        final int sortOrder = AnnotationsUtil.getAnnotationValueValue( annotation, "sortOrder" );
1221        final ExecutableType methodType = resolveMethodType( descriptor, method );
1222
1223        descriptors.add( new RenderHookDescriptor( sortOrder, method, methodType ) );
1224      }
1225    }
1226    descriptors.sort( Comparator.comparingInt( RenderHookDescriptor::getSortOrder ) );
1227    descriptor.setPostRenderDescriptors( descriptors );
1228  }
1229
1230  private void determineRenderMethod( @Nonnull final TypeElement typeElement,
1231                                      @Nonnull final ViewDescriptor descriptor,
1232                                      @Nonnull final List<ExecutableElement> methods )
1233  {
1234    boolean foundRender = false;
1235    for ( final ExecutableElement method : methods )
1236    {
1237      final AnnotationMirror annotation =
1238        AnnotationsUtil.findAnnotationByType( method, Constants.RENDER_CLASSNAME );
1239      if ( null != annotation )
1240      {
1241        MemberChecks.mustNotBeAbstract( Constants.RENDER_CLASSNAME, method );
1242        MemberChecks.mustBeSubclassCallable( typeElement,
1243                                             Constants.VIEW_CLASSNAME,
1244                                             Constants.RENDER_CLASSNAME,
1245                                             method );
1246        MemberChecks.mustNotHaveAnyParameters( Constants.RENDER_CLASSNAME, method );
1247        MemberChecks.mustReturnAnInstanceOf( processingEnv,
1248                                             method,
1249                                             Constants.RENDER_CLASSNAME,
1250                                             Constants.VNODE_CLASSNAME );
1251        MemberChecks.mustNotThrowAnyExceptions( Constants.RENDER_CLASSNAME, method );
1252        MemberChecks.mustNotHaveAnyTypeParameters( Constants.RENDER_CLASSNAME, method );
1253
1254        descriptor.setRender( method );
1255        foundRender = true;
1256      }
1257    }
1258    final boolean requireRender = descriptor.requireRender();
1259    if ( requireRender && !foundRender )
1260    {
1261      throw new ProcessorException( MemberChecks.must( Constants.VIEW_CLASSNAME,
1262                                                       "contain a method annotated with the " +
1263                                                       MemberChecks.toSimpleName( Constants.RENDER_CLASSNAME ) +
1264                                                       " annotation or must specify type=NO_RENDER" ),
1265                                    typeElement );
1266    }
1267    else if ( !requireRender )
1268    {
1269      if ( foundRender )
1270      {
1271        throw new ProcessorException( MemberChecks.mustNot( Constants.VIEW_CLASSNAME,
1272                                                            "contain a method annotated with the " +
1273                                                            MemberChecks.toSimpleName( Constants.RENDER_CLASSNAME ) +
1274                                                            " annotation or must not specify type=NO_RENDER" ),
1275                                      typeElement );
1276      }
1277      else if ( !descriptor.hasConstructor() &&
1278                !descriptor.hasPostConstruct() &&
1279                null == descriptor.getPostMount() &&
1280                null == descriptor.getPostRender() &&
1281                null == descriptor.getPreUpdate() &&
1282                null == descriptor.getPostUpdate() &&
1283                descriptor.getPreRenderDescriptors().isEmpty() &&
1284                descriptor.getPostRenderDescriptors().isEmpty() &&
1285                !descriptor.hasPreUpdateOnInputChange() &&
1286                !descriptor.hasPostUpdateOnInputChange() )
1287      {
1288        throw new ProcessorException( MemberChecks.must( Constants.VIEW_CLASSNAME,
1289                                                         "contain lifecycle methods if the the @View(type=NO_RENDER) parameter is specified" ),
1290                                      typeElement );
1291      }
1292    }
1293  }
1294
1295  private void determinePostMountMethod( @Nonnull final TypeElement typeElement,
1296                                         @Nonnull final ViewDescriptor descriptor,
1297                                         @Nonnull final List<ExecutableElement> methods )
1298  {
1299    for ( final ExecutableElement method : methods )
1300    {
1301      if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.POST_MOUNT_CLASSNAME ) )
1302      {
1303        MemberChecks.mustBeLifecycleHook( typeElement,
1304                                          Constants.VIEW_CLASSNAME,
1305                                          Constants.POST_MOUNT_CLASSNAME,
1306                                          method );
1307        descriptor.setPostMount( method );
1308      }
1309    }
1310  }
1311
1312  private void determinePostMountOrUpdateMethod( @Nonnull final TypeElement typeElement,
1313                                                 @Nonnull final ViewDescriptor descriptor,
1314                                                 @Nonnull final List<ExecutableElement> methods )
1315  {
1316    for ( final ExecutableElement method : methods )
1317    {
1318      if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.POST_MOUNT_OR_UPDATE_CLASSNAME ) )
1319      {
1320        MemberChecks.mustBeLifecycleHook( typeElement,
1321                                          Constants.VIEW_CLASSNAME,
1322                                          Constants.POST_MOUNT_OR_UPDATE_CLASSNAME,
1323                                          method );
1324        descriptor.setPostRender( method );
1325      }
1326    }
1327  }
1328
1329  private void determinePostUpdateMethod( @Nonnull final TypeElement typeElement,
1330                                          @Nonnull final ViewDescriptor descriptor,
1331                                          @Nonnull final List<ExecutableElement> methods )
1332  {
1333    for ( final ExecutableElement method : methods )
1334    {
1335      if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.POST_UPDATE_CLASSNAME ) )
1336      {
1337        MemberChecks.mustBeLifecycleHook( typeElement,
1338                                          Constants.VIEW_CLASSNAME,
1339                                          Constants.POST_UPDATE_CLASSNAME,
1340                                          method );
1341        descriptor.setPostUpdate( method );
1342      }
1343    }
1344  }
1345
1346  private void determinePreUpdateMethod( @Nonnull final TypeElement typeElement,
1347                                         @Nonnull final ViewDescriptor descriptor,
1348                                         @Nonnull final List<ExecutableElement> methods )
1349  {
1350    for ( final ExecutableElement method : methods )
1351    {
1352      if ( AnnotationsUtil.hasAnnotationOfType( method, Constants.PRE_UPDATE_CLASSNAME ) )
1353      {
1354        MemberChecks.mustBeLifecycleHook( typeElement,
1355                                          Constants.VIEW_CLASSNAME,
1356                                          Constants.PRE_UPDATE_CLASSNAME,
1357                                          method );
1358        descriptor.setPreUpdate( method );
1359      }
1360    }
1361  }
1362
1363  private ExecutableType resolveMethodType( @Nonnull final ViewDescriptor descriptor,
1364                                            @Nonnull final ExecutableElement method )
1365  {
1366    return (ExecutableType) processingEnv.getTypeUtils().asMemberOf( descriptor.getDeclaredType(), method );
1367  }
1368
1369  @Nonnull
1370  private String deriveViewName( @Nonnull final TypeElement typeElement )
1371  {
1372    final String name =
1373      (String) AnnotationsUtil.getAnnotationValue( typeElement, Constants.VIEW_CLASSNAME, "name" )
1374        .getValue();
1375
1376    if ( isSentinelName( name ) )
1377    {
1378      return typeElement.getSimpleName().toString();
1379    }
1380    else
1381    {
1382      if ( !SourceVersion.isIdentifier( name ) )
1383      {
1384        throw new ProcessorException( MemberChecks.toSimpleName( Constants.VIEW_CLASSNAME ) +
1385                                      " target specified an invalid name '" + name + "'. The " +
1386                                      "name must be a valid java identifier.", typeElement );
1387      }
1388      else if ( SourceVersion.isKeyword( name ) )
1389      {
1390        throw new ProcessorException( MemberChecks.toSimpleName( Constants.VIEW_CLASSNAME ) +
1391                                      " target specified an invalid name '" + name + "'. The " +
1392                                      "name must not be a java keyword.", typeElement );
1393      }
1394      return name;
1395    }
1396  }
1397
1398  private void determineViewCapabilities( @Nonnull final ViewDescriptor descriptor,
1399                                          @Nonnull final TypeElement typeElement )
1400  {
1401    if ( AnnotationsUtil.hasAnnotationOfType( typeElement, Constants.AREZ_COMPONENT_CLASSNAME ) )
1402    {
1403      throw new ProcessorException( MemberChecks.mustNot( Constants.VIEW_CLASSNAME,
1404                                                          "be annotated with the " +
1405                                                          MemberChecks.toSimpleName( Constants.AREZ_COMPONENT_CLASSNAME ) +
1406                                                          " as React4j will add the annotation." ),
1407                                    typeElement );
1408    }
1409
1410    if ( descriptor.needsInjection() && !descriptor.getDeclaredType().getTypeArguments().isEmpty() )
1411    {
1412      throw new ProcessorException( MemberChecks.toSimpleName( Constants.VIEW_CLASSNAME ) +
1413                                    " target has enabled injection integration but the class " +
1414                                    "has type arguments which is incompatible with injection integration.",
1415                                    typeElement );
1416    }
1417  }
1418
1419  @Nonnull
1420  private ViewType extractViewType( @Nonnull final TypeElement typeElement )
1421  {
1422    final VariableElement declaredTypeEnum = (VariableElement)
1423      AnnotationsUtil
1424        .getAnnotationValue( typeElement, Constants.VIEW_CLASSNAME, "type" )
1425        .getValue();
1426    return ViewType.valueOf( declaredTypeEnum.getSimpleName().toString() );
1427  }
1428
1429  private boolean isInputObservable( @Nonnull final List<ExecutableElement> methods,
1430                                     @Nonnull final ExecutableElement method,
1431                                     final boolean immutable )
1432  {
1433    final VariableElement parameter = (VariableElement)
1434      AnnotationsUtil.getAnnotationValue( method, Constants.INPUT_CLASSNAME, "observable" ).getValue();
1435    return switch ( parameter.getSimpleName().toString() )
1436    {
1437      case "ENABLE" ->
1438      {
1439        if ( immutable )
1440        {
1441          throw new ProcessorException( "@Input target has specified both immutable=true and " +
1442                                        "observable=ENABLE which is an invalid combination.",
1443                                        method );
1444        }
1445        yield true;
1446      }
1447      case "DISABLE" -> false;
1448      default -> hasAnyArezObserverMethods( methods );
1449    };
1450  }
1451
1452  private boolean hasAnyArezObserverMethods( @Nonnull final List<ExecutableElement> methods )
1453  {
1454    return
1455      methods
1456        .stream()
1457        .anyMatch( m -> AnnotationsUtil.hasAnnotationOfType( m, Constants.MEMOIZE_CLASSNAME ) ||
1458                        ( AnnotationsUtil.hasAnnotationOfType( m, Constants.OBSERVE_CLASSNAME ) &&
1459                          ( !m.getParameters().isEmpty() || !m.getSimpleName().toString().equals( "trackRender" ) ) ) );
1460  }
1461
1462  private boolean isInputImmutable( @Nonnull final ExecutableElement method )
1463  {
1464    return (Boolean) AnnotationsUtil.getAnnotationValue( method, Constants.INPUT_CLASSNAME, "immutable" )
1465      .getValue();
1466  }
1467
1468  private boolean isInputDependency( @Nonnull final ExecutableElement method,
1469                                     final boolean immutable,
1470                                     final boolean disposable )
1471  {
1472    final VariableElement parameter = (VariableElement)
1473      AnnotationsUtil.getAnnotationValue( method, Constants.INPUT_CLASSNAME, "dependency" ).getValue();
1474    return switch ( parameter.getSimpleName().toString() )
1475    {
1476      case "ENABLE" ->
1477      {
1478        if ( !immutable )
1479        {
1480          throw new ProcessorException( "@Input target must be immutable if dependency=ENABLE is specified",
1481                                        method );
1482        }
1483        else if ( !disposable )
1484        {
1485          throw new ProcessorException( "@Input target must be disposable if dependency=ENABLE is specified",
1486                                        method );
1487        }
1488        yield true;
1489      }
1490      case "DISABLE" -> false;
1491      default -> immutable && disposable;
1492    };
1493  }
1494
1495  private boolean isInputDisposable( @Nonnull final ExecutableElement method, @Nonnull final Element inputType )
1496  {
1497    final VariableElement parameter = (VariableElement)
1498      AnnotationsUtil.getAnnotationValue( method, Constants.INPUT_CLASSNAME, "disposable" ).getValue();
1499    return switch ( parameter.getSimpleName().toString() )
1500    {
1501      case "ENABLE" -> true;
1502      case "DISABLE" -> false;
1503      default -> (
1504                   ElementKind.CLASS == inputType.getKind() &&
1505                   AnnotationsUtil.hasAnnotationOfType( inputType, Constants.AREZ_COMPONENT_CLASSNAME )
1506                 ) ||
1507                 (
1508                   ( ElementKind.CLASS == inputType.getKind() || ElementKind.INTERFACE == inputType.getKind() ) &&
1509                   AnnotationsUtil.hasAnnotationOfType( inputType, Constants.ACT_AS_COMPONENT_CLASSNAME )
1510                 );
1511    };
1512  }
1513
1514  private boolean isContextInput( @Nonnull final ExecutableElement method )
1515  {
1516    final VariableElement parameter = (VariableElement)
1517      AnnotationsUtil.getAnnotationValue( method, Constants.INPUT_CLASSNAME, "source" ).getValue();
1518    return "CONTEXT".equals( parameter.getSimpleName().toString() );
1519  }
1520
1521  private boolean shouldSetDefaultPriority( @Nonnull final List<ExecutableElement> methods )
1522  {
1523    return
1524      methods
1525        .stream()
1526        .filter( method -> !method.getModifiers().contains( Modifier.PRIVATE ) )
1527        .anyMatch( method -> AnnotationsUtil.hasAnnotationOfType( method, Constants.MEMOIZE_CLASSNAME ) ||
1528                             AnnotationsUtil.hasAnnotationOfType( method, Constants.OBSERVE_CLASSNAME ) );
1529  }
1530
1531  private void verifyNoDuplicateAnnotations( @Nonnull final ExecutableElement method )
1532    throws ProcessorException
1533  {
1534    final List<String> annotations =
1535      Arrays.asList( Constants.INPUT_DEFAULT_CLASSNAME,
1536                     Constants.INPUT_VALIDATE_CLASSNAME,
1537                     Constants.ON_INPUT_CHANGE_CLASSNAME,
1538                     Constants.INPUT_CLASSNAME );
1539    MemberChecks.verifyNoOverlappingAnnotations( method, annotations, Collections.emptyMap() );
1540  }
1541
1542  private boolean isSentinelName( @Nonnull final String name )
1543  {
1544    return SENTINEL_NAME.equals( name );
1545  }
1546
1547  @Nonnull
1548  private String getPropertyAccessorName( @Nonnull final ExecutableElement method,
1549                                          @Nonnull final String specifiedName )
1550    throws ProcessorException
1551  {
1552    String name = deriveName( method, GETTER_PATTERN, specifiedName );
1553    if ( null != name )
1554    {
1555      return name;
1556    }
1557    else if ( method.getReturnType().getKind() == TypeKind.BOOLEAN )
1558    {
1559      name = deriveName( method, ISSER_PATTERN, specifiedName );
1560      if ( null != name )
1561      {
1562        return name;
1563      }
1564    }
1565    return method.getSimpleName().toString();
1566  }
1567
1568  @Nullable
1569  private String deriveName( @Nonnull final Element method, @Nonnull final Pattern pattern, @Nonnull final String name )
1570    throws ProcessorException
1571  {
1572    if ( isSentinelName( name ) )
1573    {
1574      final String methodName = method.getSimpleName().toString();
1575      final Matcher matcher = pattern.matcher( methodName );
1576      if ( matcher.find() )
1577      {
1578        final String candidate = matcher.group( 1 );
1579        return Character.toLowerCase( candidate.charAt( 0 ) ) + candidate.substring( 1 );
1580      }
1581      else
1582      {
1583        return null;
1584      }
1585    }
1586    else
1587    {
1588      return name;
1589    }
1590  }
1591}