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