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}