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}